Add LIKE criteria operator; more api endpoints to understand versions, get swagger json; more field name mapping

This commit is contained in:
2023-03-24 10:20:26 -05:00
parent 74cf24a00e
commit 17d4c81cc3
11 changed files with 374 additions and 43 deletions

View File

@ -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,

View File

@ -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();
}
} }

View File

@ -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));
}
} }

View File

@ -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 ?";

View File

@ -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");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -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()

View File

@ -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);
} }

View File

@ -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));
} }

View File

@ -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);
}
} }

View File

@ -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
*******************************************************************************/ *******************************************************************************/

View File

@ -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");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/