mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Add LIKE criteria operator; more api endpoints to understand versions, get swagger json; more field name mapping
This commit is contained in:
@ -33,6 +33,8 @@ public enum QCriteriaOperator
|
|||||||
IN,
|
IN,
|
||||||
NOT_IN,
|
NOT_IN,
|
||||||
IS_NULL_OR_IN,
|
IS_NULL_OR_IN,
|
||||||
|
LIKE,
|
||||||
|
NOT_LIKE,
|
||||||
STARTS_WITH,
|
STARTS_WITH,
|
||||||
ENDS_WITH,
|
ENDS_WITH,
|
||||||
CONTAINS,
|
CONTAINS,
|
||||||
|
@ -123,6 +123,8 @@ public class BackendQueryFilterUtils
|
|||||||
case CONTAINS -> testContains(criterion, fieldName, value);
|
case CONTAINS -> testContains(criterion, fieldName, value);
|
||||||
case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
|
case NOT_CONTAINS -> !testContains(criterion, fieldName, value);
|
||||||
case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value);
|
case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value);
|
||||||
|
case LIKE -> testLike(criterion, fieldName, value);
|
||||||
|
case NOT_LIKE -> !testLike(criterion, fieldName, value);
|
||||||
case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
|
case STARTS_WITH -> testStartsWith(criterion, fieldName, value);
|
||||||
case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
|
case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value);
|
||||||
case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
|
case ENDS_WITH -> testEndsWith(criterion, fieldName, value);
|
||||||
@ -152,6 +154,21 @@ public class BackendQueryFilterUtils
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static boolean testLike(QFilterCriteria criterion, String fieldName, Serializable value)
|
||||||
|
{
|
||||||
|
String stringValue = getStringFieldValue(value, fieldName, criterion);
|
||||||
|
String criterionValue = getFirstStringCriterionValue(criterion);
|
||||||
|
|
||||||
|
String regex = sqlLikeToRegex(criterionValue);
|
||||||
|
|
||||||
|
return (stringValue.matches(regex));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Based on an incoming boolean value (accumulator), a new value, and a boolean
|
** Based on an incoming boolean value (accumulator), a new value, and a boolean
|
||||||
** operator, update the accumulator, and if we can then short-circuit remaining
|
** operator, update the accumulator, and if we can then short-circuit remaining
|
||||||
@ -514,4 +531,38 @@ public class BackendQueryFilterUtils
|
|||||||
return recordList;
|
return recordList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** ... written by ChatGPT
|
||||||
|
*******************************************************************************/
|
||||||
|
static String sqlLikeToRegex(String sqlLikeExpression)
|
||||||
|
{
|
||||||
|
StringBuilder regex = new StringBuilder("^");
|
||||||
|
|
||||||
|
for(int i = 0; i < sqlLikeExpression.length(); i++)
|
||||||
|
{
|
||||||
|
char c = sqlLikeExpression.charAt(i);
|
||||||
|
|
||||||
|
if(c == '%')
|
||||||
|
{
|
||||||
|
regex.append(".*");
|
||||||
|
}
|
||||||
|
else if(c == '_')
|
||||||
|
{
|
||||||
|
regex.append(".");
|
||||||
|
}
|
||||||
|
else if("[]^$|\\(){}.*+?".indexOf(c) >= 0)
|
||||||
|
{
|
||||||
|
regex.append("\\").append(c);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
regex.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
regex.append("$");
|
||||||
|
return regex.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,78 @@ class BackendQueryFilterUtilsTest
|
|||||||
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_BETWEEN, List.of(1, 3)), "f", 2));
|
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_BETWEEN, List.of(1, 3)), "f", 2));
|
||||||
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_BETWEEN, List.of(1, 3)), "f", 3));
|
assertFalse(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_BETWEEN, List.of(1, 3)), "f", 3));
|
||||||
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_BETWEEN, List.of(1, 3)), "f", 4));
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_BETWEEN, List.of(1, 3)), "f", 4));
|
||||||
|
|
||||||
|
////////////////
|
||||||
|
// like & not //
|
||||||
|
////////////////
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.LIKE, "Test"), "f", "Test"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.LIKE, "T%"), "f", "Test"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.LIKE, "T_st"), "f", "Test"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "Test"), "f", "Tst"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T%"), "f", "Rest"));
|
||||||
|
assertTrue(BackendQueryFilterUtils.doesCriteriaMatch(new QFilterCriteria("f", QCriteriaOperator.NOT_LIKE, "T_st"), "f", "Toast"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testLikeDarPercent()
|
||||||
|
{
|
||||||
|
String pattern = BackendQueryFilterUtils.sqlLikeToRegex("Dar%");
|
||||||
|
assertTrue("Darin".matches(pattern));
|
||||||
|
assertTrue("Dar".matches(pattern));
|
||||||
|
assertFalse("Not Darin".matches(pattern));
|
||||||
|
assertFalse("David".matches(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testLikeDPercentIn()
|
||||||
|
{
|
||||||
|
String pattern = BackendQueryFilterUtils.sqlLikeToRegex("D%in");
|
||||||
|
assertTrue("Darin".matches(pattern));
|
||||||
|
assertFalse("Dar".matches(pattern));
|
||||||
|
assertFalse("Not Darin".matches(pattern));
|
||||||
|
assertTrue("Davin".matches(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testLikeDPercentUnderscoreN()
|
||||||
|
{
|
||||||
|
String pattern = BackendQueryFilterUtils.sqlLikeToRegex("D%_n");
|
||||||
|
assertTrue("Darin".matches(pattern));
|
||||||
|
assertTrue("Daron".matches(pattern));
|
||||||
|
assertTrue("Dan".matches(pattern));
|
||||||
|
assertFalse("Dar".matches(pattern));
|
||||||
|
assertFalse("Not Darin".matches(pattern));
|
||||||
|
assertTrue("Davin".matches(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testLikeDarUnderscore()
|
||||||
|
{
|
||||||
|
String pattern = BackendQueryFilterUtils.sqlLikeToRegex("Dar_");
|
||||||
|
assertFalse("Darin".matches(pattern));
|
||||||
|
assertFalse("Dar".matches(pattern));
|
||||||
|
assertTrue("Dart".matches(pattern));
|
||||||
|
assertFalse("Not Darin".matches(pattern));
|
||||||
|
assertFalse("David".matches(pattern));
|
||||||
|
}
|
||||||
}
|
}
|
@ -601,6 +601,18 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case LIKE:
|
||||||
|
{
|
||||||
|
clause += " LIKE ?";
|
||||||
|
expectedNoOfParams = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NOT_LIKE:
|
||||||
|
{
|
||||||
|
clause += " NOT LIKE ?";
|
||||||
|
expectedNoOfParams = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case STARTS_WITH:
|
case STARTS_WITH:
|
||||||
{
|
{
|
||||||
clause += " LIKE ?";
|
clause += " LIKE ?";
|
||||||
|
@ -213,6 +213,46 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void testLike() throws QException
|
||||||
|
{
|
||||||
|
QueryInput queryInput = initQueryRequest();
|
||||||
|
queryInput.setFilter(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria()
|
||||||
|
.withFieldName("email")
|
||||||
|
.withOperator(QCriteriaOperator.LIKE)
|
||||||
|
.withValues(List.of("%kelk%")))
|
||||||
|
);
|
||||||
|
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
|
||||||
|
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows");
|
||||||
|
Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void testNotLike() throws QException
|
||||||
|
{
|
||||||
|
QueryInput queryInput = initQueryRequest();
|
||||||
|
queryInput.setFilter(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria()
|
||||||
|
.withFieldName("email")
|
||||||
|
.withOperator(QCriteriaOperator.NOT_LIKE)
|
||||||
|
.withValues(List.of("%kelk%")))
|
||||||
|
);
|
||||||
|
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
|
||||||
|
assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows");
|
||||||
|
Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -63,6 +63,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||||
@ -106,7 +109,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
)
|
)
|
||||||
.withServers(ListBuilder.of(new Server()
|
.withServers(ListBuilder.of(new Server()
|
||||||
.withDescription("Localhost development")
|
.withDescription("Localhost development")
|
||||||
.withUrl("http://localhost:8000/api")
|
.withUrl("http://localhost:8000/api/" + version)
|
||||||
));
|
));
|
||||||
|
|
||||||
openAPI.setTags(new ArrayList<>());
|
openAPI.setTags(new ArrayList<>());
|
||||||
@ -163,6 +166,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
{
|
{
|
||||||
String tableName = table.getName();
|
String tableName = table.getName();
|
||||||
|
|
||||||
|
if(input.getTableName() != null && !input.getTableName().equals(tableName))
|
||||||
|
{
|
||||||
|
LOG.debug("Omitting table [" + tableName + "] because it is not the requested table [" + input.getTableName() + "]");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(table.getIsHidden())
|
if(table.getIsHidden())
|
||||||
{
|
{
|
||||||
LOG.debug("Omitting table [" + tableName + "] because it is marked as hidden");
|
LOG.debug("Omitting table [" + tableName + "] because it is marked as hidden");
|
||||||
@ -209,11 +218,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
String primaryKeyName = table.getPrimaryKeyField();
|
String primaryKeyName = table.getPrimaryKeyField();
|
||||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||||
String primaryKeyLabel = primaryKeyField.getLabel();
|
String primaryKeyLabel = primaryKeyField.getLabel();
|
||||||
|
String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(primaryKeyField);
|
||||||
List<QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
|
List<QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
|
||||||
|
|
||||||
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(primaryKeyField);
|
|
||||||
String primaryKeyApiName = (apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.getApiFieldName())) ? apiFieldMetaData.getApiFieldName() : primaryKeyName;
|
|
||||||
|
|
||||||
String tableReadPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.READ);
|
String tableReadPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.READ);
|
||||||
if(StringUtils.hasContent(tableReadPermissionName))
|
if(StringUtils.hasContent(tableReadPermissionName))
|
||||||
{
|
{
|
||||||
@ -257,18 +264,40 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
|||||||
.withType("object")
|
.withType("object")
|
||||||
.withProperties(tableFieldsWithoutPrimaryKey));
|
.withProperties(tableFieldsWithoutPrimaryKey));
|
||||||
|
|
||||||
for(QFieldMetaData tableApiField : tableApiFields)
|
for(QFieldMetaData field : tableApiFields)
|
||||||
{
|
{
|
||||||
if(primaryKeyName.equals(tableApiField.getName()))
|
if(primaryKeyName.equals(field.getName()))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
tableFieldsWithoutPrimaryKey.put(tableApiField.getName(), new Schema()
|
String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field);
|
||||||
.withType(getFieldType(table.getField(tableApiField.getName())))
|
|
||||||
.withFormat(getFieldFormat(table.getField(tableApiField.getName())))
|
Schema fieldSchema = new Schema()
|
||||||
.withDescription(tableApiField.getLabel() + " for the " + tableLabel + ".")
|
.withType(getFieldType(table.getField(field.getName())))
|
||||||
);
|
.withFormat(getFieldFormat(table.getField(field.getName())))
|
||||||
|
.withDescription(field.getLabel() + " for the " + tableLabel + ".");
|
||||||
|
|
||||||
|
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
|
||||||
|
{
|
||||||
|
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
|
||||||
|
if(QPossibleValueSourceType.ENUM.equals(possibleValueSource.getType()))
|
||||||
|
{
|
||||||
|
List<String> enumValues = new ArrayList<>();
|
||||||
|
for(QPossibleValue<?> enumValue : possibleValueSource.getEnumValues())
|
||||||
|
{
|
||||||
|
enumValues.add(enumValue.getId() + "=" + enumValue.getLabel());
|
||||||
|
}
|
||||||
|
fieldSchema.setEnumValues(enumValues);
|
||||||
|
}
|
||||||
|
else if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()))
|
||||||
|
{
|
||||||
|
QTableMetaData sourceTable = qInstance.getTable(possibleValueSource.getTableName());
|
||||||
|
fieldSchema.setDescription(fieldSchema.getDescription() + " Values in this field come from the primary key of the " + sourceTable.getLabel() + " table");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableFieldsWithoutPrimaryKey.put(apiFieldName, fieldSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentSchemas.put(tableApiName, new Schema()
|
componentSchemas.put(tableApiName, new Schema()
|
||||||
|
@ -67,12 +67,7 @@ public class QRecordApiAdapter
|
|||||||
|
|
||||||
// todo - what about display values / possible values?
|
// todo - what about display values / possible values?
|
||||||
|
|
||||||
String apiFieldName = apiFieldMetaData.getApiFieldName();
|
String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field);
|
||||||
if(!StringUtils.hasContent(apiFieldName))
|
|
||||||
{
|
|
||||||
apiFieldName = field.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
|
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
|
||||||
{
|
{
|
||||||
outputRecord.put(apiFieldName, record.getValue(apiFieldMetaData.getReplacedByFieldName()));
|
outputRecord.put(apiFieldName, record.getValue(apiFieldMetaData.getReplacedByFieldName()));
|
||||||
@ -149,16 +144,7 @@ public class QRecordApiAdapter
|
|||||||
Pair<String, String> key = new Pair<>(tableName, apiVersion);
|
Pair<String, String> key = new Pair<>(tableName, apiVersion);
|
||||||
if(!fieldMapCache.containsKey(key))
|
if(!fieldMapCache.containsKey(key))
|
||||||
{
|
{
|
||||||
Map<String, QFieldMetaData> map = getTableApiFieldList(tableName, apiVersion).stream().collect(Collectors.toMap(f ->
|
Map<String, QFieldMetaData> map = getTableApiFieldList(tableName, apiVersion).stream().collect(Collectors.toMap(f -> (ApiFieldMetaData.getEffectiveApiFieldName(f)), f -> f));
|
||||||
{
|
|
||||||
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(f);
|
|
||||||
String apiFieldName = apiFieldMetaData.getApiFieldName();
|
|
||||||
if(!StringUtils.hasContent(apiFieldName))
|
|
||||||
{
|
|
||||||
apiFieldName = f.getName();
|
|
||||||
}
|
|
||||||
return (apiFieldName);
|
|
||||||
}, f -> f));
|
|
||||||
fieldMapCache.put(key, map);
|
fieldMapCache.put(key, map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction;
|
import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction;
|
||||||
import com.kingsrook.qqq.api.actions.QRecordApiAdapter;
|
import com.kingsrook.qqq.api.actions.QRecordApiAdapter;
|
||||||
import com.kingsrook.qqq.api.model.APIVersion;
|
import com.kingsrook.qqq.api.model.APIVersion;
|
||||||
@ -129,10 +130,14 @@ public class QJavalinApiHandler
|
|||||||
{
|
{
|
||||||
ApiBuilder.path("/api/{version}", () -> // todo - configurable, that /api/ bit?
|
ApiBuilder.path("/api/{version}", () -> // todo - configurable, that /api/ bit?
|
||||||
{
|
{
|
||||||
ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpec);
|
ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml);
|
||||||
|
ApiBuilder.get("/openapi.json", QJavalinApiHandler::doSpecJson);
|
||||||
|
|
||||||
ApiBuilder.path("/{tableName}", () ->
|
ApiBuilder.path("/{tableName}", () ->
|
||||||
{
|
{
|
||||||
|
ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml);
|
||||||
|
ApiBuilder.get("/openapi.json", QJavalinApiHandler::doSpecJson);
|
||||||
|
|
||||||
ApiBuilder.post("/", QJavalinApiHandler::doInsert);
|
ApiBuilder.post("/", QJavalinApiHandler::doInsert);
|
||||||
|
|
||||||
ApiBuilder.get("/query", QJavalinApiHandler::doQuery);
|
ApiBuilder.get("/query", QJavalinApiHandler::doQuery);
|
||||||
@ -151,6 +156,10 @@ public class QJavalinApiHandler
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ApiBuilder.get("/api/versions.json", QJavalinApiHandler::doVersions);
|
||||||
|
|
||||||
|
ApiBuilder.before("/*", QJavalinApiHandler::setupCORS);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// default all other /api/ requests (for the methods we support) to a standard 404 response //
|
// default all other /api/ requests (for the methods we support) to a standard 404 response //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -163,6 +172,45 @@ public class QJavalinApiHandler
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void doVersions(Context context)
|
||||||
|
{
|
||||||
|
ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(qInstance);
|
||||||
|
|
||||||
|
Map<String, Object> rs = new HashMap<>();
|
||||||
|
rs.put("supportedVersions", apiInstanceMetaData.getSupportedVersions().stream().map(String::valueOf).collect(Collectors.toList()));
|
||||||
|
rs.put("currentVersion", apiInstanceMetaData.getCurrentVersion().toString());
|
||||||
|
|
||||||
|
context.contentType(ContentType.APPLICATION_JSON);
|
||||||
|
context.result(JsonUtils.toJson(rs));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void setupCORS(Context context)
|
||||||
|
{
|
||||||
|
if(StringUtils.hasContent(context.header("Origin")))
|
||||||
|
{
|
||||||
|
context.res().setHeader("Access-Control-Allow-Origin", context.header("Origin"));
|
||||||
|
context.res().setHeader("Vary", "Origin");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.res().setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH, OPTIONS");
|
||||||
|
context.header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization, Accept, content-type, authorization, accept");
|
||||||
|
context.header("Access-Control-Allow-Credentials", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -176,7 +224,7 @@ public class QJavalinApiHandler
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static void doSpec(Context context)
|
private static void doSpecYaml(Context context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -194,6 +242,34 @@ public class QJavalinApiHandler
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void doSpecJson(Context context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
QContext.init(qInstance, null);
|
||||||
|
String version = context.pathParam("version");
|
||||||
|
GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version);
|
||||||
|
|
||||||
|
if(context.pathParam("tableName") != null)
|
||||||
|
{
|
||||||
|
input.setTableName(context.pathParam("tableName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(input);
|
||||||
|
context.contentType(ContentType.JSON);
|
||||||
|
context.result(output.getJson());
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
handleException(context, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -576,22 +652,20 @@ public class QJavalinApiHandler
|
|||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
// order of these is important (e.g., because some are a sub-string of others!!) //
|
// order of these is important (e.g., because some are a sub-string of others!!) //
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
EQ("=", QCriteriaOperator.EQUALS, QCriteriaOperator.NOT_EQUALS, true, 1),
|
EQ("=", QCriteriaOperator.EQUALS, QCriteriaOperator.NOT_EQUALS, 1),
|
||||||
LTE("<=", QCriteriaOperator.LESS_THAN_OR_EQUALS, QCriteriaOperator.GREATER_THAN, false, 1),
|
LTE("<=", QCriteriaOperator.LESS_THAN_OR_EQUALS, null, 1),
|
||||||
GTE(">=", QCriteriaOperator.GREATER_THAN_OR_EQUALS, QCriteriaOperator.LESS_THAN, false, 1),
|
GTE(">=", QCriteriaOperator.GREATER_THAN_OR_EQUALS, null, 1),
|
||||||
LT("<", QCriteriaOperator.LESS_THAN, QCriteriaOperator.GREATER_THAN_OR_EQUALS, false, 1),
|
LT("<", QCriteriaOperator.LESS_THAN, null, 1),
|
||||||
GT(">", QCriteriaOperator.GREATER_THAN, QCriteriaOperator.LESS_THAN_OR_EQUALS, false, 1),
|
GT(">", QCriteriaOperator.GREATER_THAN, null, 1),
|
||||||
EMPTY("EMPTY", QCriteriaOperator.IS_BLANK, QCriteriaOperator.IS_NOT_BLANK, true, 0),
|
EMPTY("EMPTY", QCriteriaOperator.IS_BLANK, QCriteriaOperator.IS_NOT_BLANK, 0),
|
||||||
BETWEEN("BETWEEN ", QCriteriaOperator.BETWEEN, QCriteriaOperator.NOT_BETWEEN, true, 2),
|
BETWEEN("BETWEEN ", QCriteriaOperator.BETWEEN, QCriteriaOperator.NOT_BETWEEN, 2),
|
||||||
IN("IN ", QCriteriaOperator.IN, QCriteriaOperator.NOT_IN, true, null),
|
IN("IN ", QCriteriaOperator.IN, QCriteriaOperator.NOT_IN, null),
|
||||||
// todo MATCHES
|
LIKE("LIKE ", QCriteriaOperator.LIKE, QCriteriaOperator.NOT_LIKE, 1);
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
private final String prefix;
|
private final String prefix;
|
||||||
private final QCriteriaOperator positiveOperator;
|
private final QCriteriaOperator positiveOperator;
|
||||||
private final QCriteriaOperator negativeOperator;
|
private final QCriteriaOperator negativeOperator;
|
||||||
private final boolean supportsNot;
|
|
||||||
private final Integer noOfValues; // null means many (IN)
|
private final Integer noOfValues; // null means many (IN)
|
||||||
|
|
||||||
|
|
||||||
@ -599,12 +673,11 @@ public class QJavalinApiHandler
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
Operator(String prefix, QCriteriaOperator positiveOperator, QCriteriaOperator negativeOperator, boolean supportsNot, Integer noOfValues)
|
Operator(String prefix, QCriteriaOperator positiveOperator, QCriteriaOperator negativeOperator, Integer noOfValues)
|
||||||
{
|
{
|
||||||
this.prefix = prefix;
|
this.prefix = prefix;
|
||||||
this.positiveOperator = positiveOperator;
|
this.positiveOperator = positiveOperator;
|
||||||
this.negativeOperator = negativeOperator;
|
this.negativeOperator = negativeOperator;
|
||||||
this.supportsNot = supportsNot;
|
|
||||||
this.noOfValues = noOfValues;
|
this.noOfValues = noOfValues;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -635,7 +708,7 @@ public class QJavalinApiHandler
|
|||||||
if(value.startsWith(op.prefix))
|
if(value.startsWith(op.prefix))
|
||||||
{
|
{
|
||||||
selectedOperator = op;
|
selectedOperator = op;
|
||||||
if(!selectedOperator.supportsNot && isNot)
|
if(selectedOperator.negativeOperator == null && isNot)
|
||||||
{
|
{
|
||||||
throw (new QBadRequestException("Unsupported operator: !" + selectedOperator.prefix));
|
throw (new QBadRequestException("Unsupported operator: !" + selectedOperator.prefix));
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
|||||||
public class GenerateOpenApiSpecInput extends AbstractActionInput
|
public class GenerateOpenApiSpecInput extends AbstractActionInput
|
||||||
{
|
{
|
||||||
private String version;
|
private String version;
|
||||||
|
private String tableName;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -63,4 +64,35 @@ public class GenerateOpenApiSpecInput extends AbstractActionInput
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for tableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getTableName()
|
||||||
|
{
|
||||||
|
return (this.tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for tableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setTableName(String tableName)
|
||||||
|
{
|
||||||
|
this.tableName = tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for tableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public GenerateOpenApiSpecInput withTableName(String tableName)
|
||||||
|
{
|
||||||
|
this.tableName = tableName;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.model.metadata.fields;
|
|||||||
import com.kingsrook.qqq.api.ApiMiddlewareType;
|
import com.kingsrook.qqq.api.ApiMiddlewareType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QMiddlewareFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QMiddlewareFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -63,6 +64,22 @@ public class ApiFieldMetaData extends QMiddlewareFieldMetaData
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static String getEffectiveApiFieldName(QFieldMetaData field)
|
||||||
|
{
|
||||||
|
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field);
|
||||||
|
if(apiFieldMetaData != null && StringUtils.hasContent(apiFieldMetaData.apiFieldName))
|
||||||
|
{
|
||||||
|
return (apiFieldMetaData.apiFieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (field.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for initialVersion
|
** Getter for initialVersion
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -292,6 +292,23 @@ class QJavalinApiHandlerTest extends BaseTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testQuery200LikeAndNotLike() throws QException
|
||||||
|
{
|
||||||
|
insertSimpsons();
|
||||||
|
String PERCENT = "%25";
|
||||||
|
assertPersonQueryFindsFirstNames(List.of("Homer"), "firstName=LIKE Ho" + PERCENT);
|
||||||
|
assertPersonQueryFindsFirstNames(List.of("Homer"), "firstName=LIKE Ho_er");
|
||||||
|
|
||||||
|
assertPersonQueryFindsFirstNames(List.of("Marge", "Bart", "Lisa", "Maggie"), "firstName=!LIKE Homer&orderBy=id");
|
||||||
|
assertPersonQueryFindsFirstNames(List.of("Homer"), "firstName=!LIKE " + PERCENT + "a" + PERCENT + "&orderBy=id");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
Reference in New Issue
Block a user