Initial support for associated records (implemented insert, delete).

Include "api" on audit.
This commit is contained in:
2023-03-27 09:52:39 -05:00
parent 17d4c81cc3
commit ba805a4c92
15 changed files with 1052 additions and 85 deletions

View File

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.APIVersionRange;
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput;
@ -66,8 +67,10 @@ 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.Association;
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.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
@ -204,7 +207,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
boolean updateCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_UPDATE);
boolean deleteCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_DELETE);
boolean insertCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_INSERT);
boolean countCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_COUNT);
boolean countCapability = table.isCapabilityEnabled(tableBackend, Capability.TABLE_COUNT); // todo - look at this - if table doesn't have count, don't include it in its input/output, etc
if(!queryCapability && !getCapability && !updateCapability && !deleteCapability && !insertCapability)
{
@ -221,6 +224,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
String primaryKeyApiName = ApiFieldMetaData.getEffectiveApiFieldName(primaryKeyField);
List<QFieldMetaData> tableApiFields = new GetTableApiFieldsAction().execute(new GetTableApiFieldsInput().withTableName(tableName).withVersion(version)).getFields();
///////////////////////////////
// permissions for the table //
///////////////////////////////
String tableReadPermissionName = PermissionsHelper.getTablePermissionName(tableName, TablePermissionSubType.READ);
if(StringUtils.hasContent(tableReadPermissionName))
{
@ -256,13 +262,15 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
.withName(tableLabel)
.withDescription("Operations on the " + tableLabel + " table."));
//////////////////////////////////////
// build the schemas for this table //
//////////////////////////////////////
//////////////////////////////////////////////////////////////////
// build the schemas for this table //
// start with the full table minus its pkey (e.g., for posting) //
//////////////////////////////////////////////////////////////////
LinkedHashMap<String, Schema> tableFieldsWithoutPrimaryKey = new LinkedHashMap<>();
componentSchemas.put(tableApiName + "WithoutPrimaryKey", new Schema()
Schema tableWithoutPrimaryKeySchema = new Schema()
.withType("object")
.withProperties(tableFieldsWithoutPrimaryKey));
.withProperties(tableFieldsWithoutPrimaryKey);
componentSchemas.put(tableApiName + "WithoutPrimaryKey", tableWithoutPrimaryKeySchema);
for(QFieldMetaData field : tableApiFields)
{
@ -271,46 +279,26 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
continue;
}
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);
Schema fieldSchema = getFieldSchema(table, field);
tableFieldsWithoutPrimaryKey.put(ApiFieldMetaData.getEffectiveApiFieldName(field), fieldSchema);
}
//////////////////////////////////
// recursively add associations //
//////////////////////////////////
addAssociations(table, tableWithoutPrimaryKeySchema);
/////////////////////////////////////////////////
// full version of table (w/o pkey + the pkey) //
/////////////////////////////////////////////////
componentSchemas.put(tableApiName, new Schema()
.withType("object")
.withAllOf(ListBuilder.of(new Schema().withRef("#/components/schemas/" + tableApiName + "WithoutPrimaryKey")))
.withProperties(MapBuilder.of(
primaryKeyApiName, new Schema()
.withType(getFieldType(table.getField(primaryKeyName)))
.withFormat(getFieldFormat(table.getField(primaryKeyName)))
.withDescription(primaryKeyLabel + " for the " + tableLabel + ". Primary Key.")
))
);
.withProperties(MapBuilder.of(primaryKeyApiName, getFieldSchema(table, table.getField(primaryKeyName)))));
//////////////////////////////////////////////////////////////////////////////
// table as a search result (the base search result, plus the table itself) //
//////////////////////////////////////////////////////////////////////////////
componentSchemas.put(tableApiName + "SearchResult", new Schema()
.withType("object")
.withAllOf(ListBuilder.of(new Schema().withRef("#/components/schemas/baseSearchResultFields")))
@ -577,6 +565,59 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
/*******************************************************************************
**
*******************************************************************************/
private static void addAssociations(QTableMetaData table, Schema tableWithoutPrimaryKeySchema)
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
String associatedTableName = association.getAssociatedTableName();
QTableMetaData associatedTable = QContext.getQInstance().getTable(associatedTableName);
ApiTableMetaData associatedApiTableMetaData = Objects.requireNonNullElse(ApiTableMetaData.of(associatedTable), new ApiTableMetaData());
String associatedTableApiName = StringUtils.hasContent(associatedApiTableMetaData.getApiTableName()) ? associatedApiTableMetaData.getApiTableName() : associatedTableName;
tableWithoutPrimaryKeySchema.getProperties().put(association.getName(), new Schema()
.withType("array")
.withItems(new Schema().withRef("#/components/schemas/" + associatedTableApiName)));
}
}
/*******************************************************************************
**
*******************************************************************************/
private Schema getFieldSchema(QTableMetaData table, QFieldMetaData field)
{
Schema fieldSchema = new Schema()
.withType(getFieldType(table.getField(field.getName())))
.withFormat(getFieldFormat(table.getField(field.getName())))
.withDescription(field.getLabel() + " for the " + table.getLabel() + ".");
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
QPossibleValueSource possibleValueSource = QContext.getQInstance().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 = QContext.getQInstance().getTable(possibleValueSource.getTableName());
fieldSchema.setDescription(fieldSchema.getDescription() + " Values in this field come from the primary key of the " + sourceTable.getLabel() + " table");
}
}
return fieldSchema;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -32,11 +32,16 @@ import java.util.stream.Collectors;
import com.kingsrook.qqq.api.javalin.QBadRequestException;
import com.kingsrook.qqq.api.model.actions.GetTableApiFieldsInput;
import com.kingsrook.qqq.api.model.metadata.fields.ApiFieldMetaData;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
@ -94,6 +99,13 @@ public class QRecordApiAdapter
List<String> unrecognizedFieldNames = new ArrayList<>();
QRecord qRecord = new QRecord();
Map<String, Association> associationMap = new HashMap<>();
QTableMetaData table = QContext.getQInstance().getTable(tableName);
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
associationMap.put(association.getName(), association);
}
//////////////////////////////////////////
// iterate over keys in the json object //
//////////////////////////////////////////
@ -117,6 +129,30 @@ public class QRecordApiAdapter
qRecord.setValue(field.getName(), value);
}
}
else if(associationMap.containsKey(jsonKey))
{
Association association = associationMap.get(jsonKey);
Object value = jsonObject.get(jsonKey);
if(value instanceof JSONArray jsonArray)
{
for(Object subObject : jsonArray)
{
if(subObject instanceof JSONObject subJsonObject)
{
QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion);
qRecord.withAssociatedRecord(association.getName(), subRecord);
}
else
{
throw (new QBadRequestException("Found a " + value.getClass().getSimpleName() + " in the array under key " + jsonKey + ", but a JSON object is required here."));
}
}
}
else
{
throw (new QBadRequestException("Found a " + value.getClass().getSimpleName() + " at key " + jsonKey + ", but a JSON array is required here."));
}
}
else
{
///////////////////////////////////////////////////

View File

@ -76,6 +76,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@ -253,7 +254,7 @@ public class QJavalinApiHandler
String version = context.pathParam("version");
GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version);
if(context.pathParam("tableName") != null)
if(StringUtils.hasContent(context.pathParam("tableName")))
{
input.setTableName(context.pathParam("tableName"));
}
@ -273,9 +274,10 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
public static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException
public static void setupSession(Context context, AbstractActionInput input, String version) throws QModuleDispatchException, QAuthenticationException
{
QJavalinImplementation.setupSession(context, input);
QSession session = QJavalinImplementation.setupSession(context, input);
session.setValue("apiVersion", version);
}
@ -296,8 +298,8 @@ public class QJavalinApiHandler
GetInput getInput = new GetInput();
setupSession(context, getInput);
QJavalinAccessLogger.logStart("get", logPair("table", tableName), logPair("primaryKey", primaryKey));
setupSession(context, getInput, version);
QJavalinAccessLogger.logStart("apiGet", logPair("table", tableName), logPair("primaryKey", primaryKey));
getInput.setTableName(tableName);
// i think not for api... getInput.setShouldGenerateDisplayValues(true);
@ -354,7 +356,7 @@ public class QJavalinApiHandler
String tableName = table.getName();
QueryInput queryInput = new QueryInput();
setupSession(context, queryInput);
setupSession(context, queryInput, version);
QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName));
queryInput.setTableName(tableName);
@ -785,8 +787,8 @@ public class QJavalinApiHandler
InsertInput insertInput = new InsertInput();
setupSession(context, insertInput);
QJavalinAccessLogger.logStart("insert", logPair("table", tableName));
setupSession(context, insertInput, version);
QJavalinAccessLogger.logStart("apiInsert", logPair("table", tableName));
insertInput.setTableName(tableName);
@ -852,8 +854,8 @@ public class QJavalinApiHandler
InsertInput insertInput = new InsertInput();
setupSession(context, insertInput);
QJavalinAccessLogger.logStart("bulkInsert", logPair("table", tableName));
setupSession(context, insertInput, version);
QJavalinAccessLogger.logStart("apiBulkInsert", logPair("table", tableName));
insertInput.setTableName(tableName);
@ -958,8 +960,8 @@ public class QJavalinApiHandler
UpdateInput updateInput = new UpdateInput();
setupSession(context, updateInput);
QJavalinAccessLogger.logStart("bulkUpdate", logPair("table", tableName));
setupSession(context, updateInput, version);
QJavalinAccessLogger.logStart("apiBulkUpdate", logPair("table", tableName));
updateInput.setTableName(tableName);
@ -1063,8 +1065,8 @@ public class QJavalinApiHandler
DeleteInput deleteInput = new DeleteInput();
setupSession(context, deleteInput);
QJavalinAccessLogger.logStart("bulkDelete", logPair("table", tableName));
setupSession(context, deleteInput, version);
QJavalinAccessLogger.logStart("apiBulkDelete", logPair("table", tableName));
deleteInput.setTableName(tableName);
@ -1182,8 +1184,8 @@ public class QJavalinApiHandler
UpdateInput updateInput = new UpdateInput();
setupSession(context, updateInput);
QJavalinAccessLogger.logStart("update", logPair("table", tableName));
setupSession(context, updateInput, version);
QJavalinAccessLogger.logStart("apiUpdate", logPair("table", tableName));
updateInput.setTableName(tableName);
@ -1268,8 +1270,8 @@ public class QJavalinApiHandler
DeleteInput deleteInput = new DeleteInput();
setupSession(context, deleteInput);
QJavalinAccessLogger.logStart("delete", logPair("table", tableName));
setupSession(context, deleteInput, version);
QJavalinAccessLogger.logStart("apiDelete", logPair("table", tableName));
deleteInput.setTableName(tableName);
deleteInput.setPrimaryKeys(List.of(primaryKey));