diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java index abf3b81c..35352192 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java @@ -33,6 +33,8 @@ public enum QCriteriaOperator IN, NOT_IN, IS_NULL_OR_IN, + LIKE, + NOT_LIKE, STARTS_WITH, ENDS_WITH, CONTAINS, diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java index 96d006c5..a08aa458 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java @@ -123,6 +123,8 @@ public class BackendQueryFilterUtils case CONTAINS -> testContains(criterion, fieldName, value); case NOT_CONTAINS -> !testContains(criterion, fieldName, 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 NOT_STARTS_WITH -> !testStartsWith(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 ** operator, update the accumulator, and if we can then short-circuit remaining @@ -514,4 +531,38 @@ public class BackendQueryFilterUtils 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(); + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java index 765a9faf..25d9beb0 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtilsTest.java @@ -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", 3)); 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)); + } } \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 034a4af0..b88ad2c2 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -601,6 +601,18 @@ public abstract class AbstractRDBMSAction implements QActionInterface } break; } + case LIKE: + { + clause += " LIKE ?"; + expectedNoOfParams = 1; + break; + } + case NOT_LIKE: + { + clause += " NOT LIKE ?"; + expectedNoOfParams = 1; + break; + } case STARTS_WITH: { clause += " LIKE ?"; diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index c537463a..4d846e19 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -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"); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index 87a8fd76..5ea168d6 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -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.fields.QFieldMetaData; 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.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -106,7 +109,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction()); @@ -163,6 +166,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction 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); if(StringUtils.hasContent(tableReadPermissionName)) { @@ -257,18 +264,40 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction 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() diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java index 16c6602a..7862ed31 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java @@ -67,12 +67,7 @@ public class QRecordApiAdapter // todo - what about display values / possible values? - String apiFieldName = apiFieldMetaData.getApiFieldName(); - if(!StringUtils.hasContent(apiFieldName)) - { - apiFieldName = field.getName(); - } - + String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field); if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName())) { outputRecord.put(apiFieldName, record.getValue(apiFieldMetaData.getReplacedByFieldName())); @@ -149,16 +144,7 @@ public class QRecordApiAdapter Pair key = new Pair<>(tableName, apiVersion); if(!fieldMapCache.containsKey(key)) { - Map map = getTableApiFieldList(tableName, apiVersion).stream().collect(Collectors.toMap(f -> - { - ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(f); - String apiFieldName = apiFieldMetaData.getApiFieldName(); - if(!StringUtils.hasContent(apiFieldName)) - { - apiFieldName = f.getName(); - } - return (apiFieldName); - }, f -> f)); + Map map = getTableApiFieldList(tableName, apiVersion).stream().collect(Collectors.toMap(f -> (ApiFieldMetaData.getEffectiveApiFieldName(f)), f -> f)); fieldMapCache.put(key, map); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 0fab9400..10612098 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction; import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APIVersion; @@ -129,10 +130,14 @@ public class QJavalinApiHandler { 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.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml); + ApiBuilder.get("/openapi.json", QJavalinApiHandler::doSpecJson); + ApiBuilder.post("/", QJavalinApiHandler::doInsert); 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 // ////////////////////////////////////////////////////////////////////////////////////////////// @@ -163,6 +172,45 @@ public class QJavalinApiHandler + /******************************************************************************* + ** + *******************************************************************************/ + private static void doVersions(Context context) + { + ApiInstanceMetaData apiInstanceMetaData = ApiInstanceMetaData.of(qInstance); + + Map 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 { @@ -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!!) // /////////////////////////////////////////////////////////////////////////////////// - EQ("=", QCriteriaOperator.EQUALS, QCriteriaOperator.NOT_EQUALS, true, 1), - LTE("<=", QCriteriaOperator.LESS_THAN_OR_EQUALS, QCriteriaOperator.GREATER_THAN, false, 1), - GTE(">=", QCriteriaOperator.GREATER_THAN_OR_EQUALS, QCriteriaOperator.LESS_THAN, false, 1), - LT("<", QCriteriaOperator.LESS_THAN, QCriteriaOperator.GREATER_THAN_OR_EQUALS, false, 1), - GT(">", QCriteriaOperator.GREATER_THAN, QCriteriaOperator.LESS_THAN_OR_EQUALS, false, 1), - EMPTY("EMPTY", QCriteriaOperator.IS_BLANK, QCriteriaOperator.IS_NOT_BLANK, true, 0), - BETWEEN("BETWEEN ", QCriteriaOperator.BETWEEN, QCriteriaOperator.NOT_BETWEEN, true, 2), - IN("IN ", QCriteriaOperator.IN, QCriteriaOperator.NOT_IN, true, null), - // todo MATCHES - ; + EQ("=", QCriteriaOperator.EQUALS, QCriteriaOperator.NOT_EQUALS, 1), + LTE("<=", QCriteriaOperator.LESS_THAN_OR_EQUALS, null, 1), + GTE(">=", QCriteriaOperator.GREATER_THAN_OR_EQUALS, null, 1), + LT("<", QCriteriaOperator.LESS_THAN, null, 1), + GT(">", QCriteriaOperator.GREATER_THAN, null, 1), + EMPTY("EMPTY", QCriteriaOperator.IS_BLANK, QCriteriaOperator.IS_NOT_BLANK, 0), + BETWEEN("BETWEEN ", QCriteriaOperator.BETWEEN, QCriteriaOperator.NOT_BETWEEN, 2), + IN("IN ", QCriteriaOperator.IN, QCriteriaOperator.NOT_IN, null), + LIKE("LIKE ", QCriteriaOperator.LIKE, QCriteriaOperator.NOT_LIKE, 1); private final String prefix; private final QCriteriaOperator positiveOperator; private final QCriteriaOperator negativeOperator; - private final boolean supportsNot; 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.positiveOperator = positiveOperator; this.negativeOperator = negativeOperator; - this.supportsNot = supportsNot; this.noOfValues = noOfValues; } } @@ -635,7 +708,7 @@ public class QJavalinApiHandler if(value.startsWith(op.prefix)) { selectedOperator = op; - if(!selectedOperator.supportsNot && isNot) + if(selectedOperator.negativeOperator == null && isNot) { throw (new QBadRequestException("Unsupported operator: !" + selectedOperator.prefix)); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecInput.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecInput.java index 96bc57ca..97e7dd1a 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecInput.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/actions/GenerateOpenApiSpecInput.java @@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; public class GenerateOpenApiSpecInput extends AbstractActionInput { private String version; + private String tableName; @@ -63,4 +64,35 @@ public class GenerateOpenApiSpecInput extends AbstractActionInput 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java index 47bc1026..c4d9c82c 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/fields/ApiFieldMetaData.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.model.metadata.fields; 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.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 *******************************************************************************/ diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index f95ec8b2..ec3de2d3 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -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"); + } + + + /******************************************************************************* ** *******************************************************************************/