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

@ -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<GenerateO
)
.withServers(ListBuilder.of(new Server()
.withDescription("Localhost development")
.withUrl("http://localhost:8000/api")
.withUrl("http://localhost:8000/api/" + version)
));
openAPI.setTags(new ArrayList<>());
@ -163,6 +166,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
{
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())
{
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();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
String primaryKeyLabel = primaryKeyField.getLabel();
String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(primaryKeyField);
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);
if(StringUtils.hasContent(tableReadPermissionName))
{
@ -257,18 +264,40 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withType("object")
.withProperties(tableFieldsWithoutPrimaryKey));
for(QFieldMetaData tableApiField : tableApiFields)
for(QFieldMetaData field : tableApiFields)
{
if(primaryKeyName.equals(tableApiField.getName()))
if(primaryKeyName.equals(field.getName()))
{
continue;
}
tableFieldsWithoutPrimaryKey.put(tableApiField.getName(), new Schema()
.withType(getFieldType(table.getField(tableApiField.getName())))
.withFormat(getFieldFormat(table.getField(tableApiField.getName())))
.withDescription(tableApiField.getLabel() + " for the " + tableLabel + ".")
);
String apiFieldName = ApiFieldMetaData.getEffectiveApiFieldName(field);
Schema fieldSchema = new Schema()
.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()

View File

@ -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<String, String> key = new Pair<>(tableName, apiVersion);
if(!fieldMapCache.containsKey(key))
{
Map<String, QFieldMetaData> 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<String, QFieldMetaData> map = getTableApiFieldList(tableName, apiVersion).stream().collect(Collectors.toMap(f -> (ApiFieldMetaData.getEffectiveApiFieldName(f)), f -> f));
fieldMapCache.put(key, map);
}

View File

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

View File

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

View File

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