mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-21 14:38:43 +00:00
Add check for records pre-delete action (for security and better errors); 404s and ids in 207s for bulk update & delete; ignore non-editable fields;
This commit is contained in:
@ -92,6 +92,95 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(GenerateOpenApiSpecAction.class);
|
||||
|
||||
public static final String GET_DESCRIPTION = """
|
||||
Get one record from this table, by specifying its primary key as a path parameter.
|
||||
""";
|
||||
|
||||
public static final String QUERY_DESCRIPTION = """
|
||||
Execute a query on this table, using query criteria as specified in query string parameters.
|
||||
|
||||
* Pagination is managed via the `pageNo` & `pageSize` query string parameters. pageNo starts at 1. pageSize defaults to 50.
|
||||
* By default, the response includes the total count of records that match the query criteria. The count can be omitted by specifying `includeCount=false`
|
||||
* By default, results are sorted by the table's primary key, descending. This can be changed by specifying the `orderBy` query string parameter, following SQL ORDER BY syntax (e.g., `fieldName1 ASC, fieldName2 DESC`)
|
||||
* By default, all given query criteria are combined using logical AND. This can be changed by specifying the query string parameter `booleanOperator=OR`.
|
||||
* Each field on the table can be used as a query criteria. Each query criteria field can be specified on the query string any number of times.
|
||||
* By default, all criteria use the equals operator (e.g., `myField=value` means records will be returned where myField equals value). Alternative operators can be used as follows:
|
||||
* Equals: `myField=value`
|
||||
* Not Equals: `myField=!value`
|
||||
* Less Than: `myField=<value`
|
||||
* Greater Than: `myField=>value`
|
||||
* Less Than or Equals: `myField=<=value`
|
||||
* Greater Than or Equals: `myField=>=value`
|
||||
* Empty (or null): `myField=EMPTY`
|
||||
* Not Empty: `myField=!EMPTY`
|
||||
* Between: `myField=BETWEEN value1,value2` (two values must be given, separated by commas)
|
||||
* Not Between: `myField=!BETWEEN value1,value2` (two values must be given, separated by commas)
|
||||
* In: `myField=IN value1,value2,...,valueN` (one or more values must be given, separated by commas)
|
||||
* Not In: `myField=!IN value1,value2,...,valueN` (one or more values must be given, separated by commas)
|
||||
* Like: `myField=LIKE value` (using standard SQL % and _ wildcards)
|
||||
* Not Like: `myField=!LIKE value` (using standard SQL % and _ wildcards)
|
||||
""";
|
||||
|
||||
public static final String INSERT_DESCRIPTION = """
|
||||
Insert one record into this table by supplying the values to be inserted in the request body.
|
||||
* The request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body.
|
||||
* Any unrecognized field names in the body will cause a 400 error.
|
||||
* Any read-only (non-editable) fields provided in the body will be silently ignored.
|
||||
|
||||
Upon success, a status code of 201 (`Created`) is returned, and the generated value for the primary key will be returned in the response body object.
|
||||
""";
|
||||
|
||||
public static final String UPDATE_DESCRIPTION = """
|
||||
Update one record in this table, by specifying its primary key as a path parameter, and by supplying values to be updated in the request body.
|
||||
|
||||
* Only the fields provided in the request body will be updated.
|
||||
* To remove a value from a field, supply the key for the field, with a null value.
|
||||
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
|
||||
* Any unrecognized field names in the body will cause a 400 error.
|
||||
* Any read-only (non-editable) fields provided in the body will be silently ignored.
|
||||
* Note that if the request body includes the primary key, it will be ignored. Only the primary key value path parameter will be used.
|
||||
|
||||
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
|
||||
""";
|
||||
|
||||
public static final String DELETE_DESCRIPTION = """
|
||||
Delete one record from this table, by specifying its primary key as a path parameter.
|
||||
|
||||
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
|
||||
""";
|
||||
|
||||
public static final String BULK_INSERT_DESCRIPTION = """
|
||||
Insert one or more records into this table by supplying array of records with values to be inserted, in the request body.
|
||||
* The objects in the request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body
|
||||
* Any unrecognized field names in the body will cause a 400 error.
|
||||
* Any read-only (non-editable) fields provided in the body will be silently ignored.
|
||||
|
||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
||||
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
|
||||
* For sub-status codes of 201 (`Created`), and the generated value for the primary key will be returned in the response body object.
|
||||
""";
|
||||
|
||||
public static final String BULK_UPDATE_DESCRIPTION = """
|
||||
Update one or more records in this table, by supplying an array of records, with primary keys and values to be updated, in the request body.
|
||||
* Only the fields provided in the request body will be updated.
|
||||
* To remove a value from a field, supply the key for the field, with a null value.
|
||||
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
|
||||
* Any unrecognized field names in the body will cause a 400 error.
|
||||
* Any read-only (non-editable) fields provided in the body will be silently ignored.
|
||||
|
||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
||||
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
|
||||
* Each input object's primary key will also be included in the corresponding response object.
|
||||
""";
|
||||
|
||||
public static final String BULK_DELETE_DESCRIPTION = """
|
||||
Delete one or more records from this table, by supplying an array of primary key values in the request body.
|
||||
|
||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
||||
* The 1st primary key in the request will have its response in the 1st object in the response, and so-forth.
|
||||
* Each input primary key will also be included in the corresponding response object.
|
||||
""";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -144,6 +233,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
LinkedHashMap<String, String> scopes = new LinkedHashMap<>();
|
||||
// todo, or not todo? .withScopes(scopes)
|
||||
// seems to make a lot of "noise" on the Auth page, and for no obvious benefit...
|
||||
securitySchemes.put("OAuth2", new OAuth2()
|
||||
.withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow()
|
||||
.withTokenUrl("/api/oauth/token"))));
|
||||
@ -297,35 +387,20 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
.withAllOf(ListBuilder.of(
|
||||
new Schema().withRef("#/components/schemas/" + tableApiName)))))));
|
||||
|
||||
// todo...?
|
||||
// includeAssociatedOrderLines=false&includeAssociatedExtrinsics=false&includeAssociatedOrderLinesExtrinsics
|
||||
// includeAssociatedRecords=none
|
||||
// includeAssociatedRecords=all
|
||||
// includeAssociatedRecords=orderLines
|
||||
// includeAssociatedRecords=orderLines,orderLines.extrinsics
|
||||
// includeAssociatedRecords=extrinsics,orderLines,orderLines.extrinsics
|
||||
|
||||
//////////////////////////////////////
|
||||
// paths and methods for this table //
|
||||
//////////////////////////////////////
|
||||
Method queryGet = new Method()
|
||||
.withSummary("Search for " + tableLabel + " records by query string")
|
||||
.withDescription("""
|
||||
Execute a query on this table, using query criteria as specified in query string parameters.
|
||||
|
||||
* Pagination is managed via the `pageNo` & `pageSize` query string parameters. pageNo starts at 1. pageSize defaults to 50.
|
||||
* By default, the response includes the total count of records that match the query criteria. The count can be omitted by specifying `includeCount=false`
|
||||
* By default, results are sorted by the table's primary key, descending. This can be changed by specifying the `orderBy` query string parameter, following SQL ORDER BY syntax (e.g., `fieldName1 ASC, fieldName2 DESC`)
|
||||
* By default, all given query criteria are combined using logical AND. This can be changed by specifying the query string parameter `booleanOperator=OR`.
|
||||
* Each field on the table can be used as a query criteria. Each query criteria field can be specified on the query string any number of times.
|
||||
* By default, all criteria use the equals operator (e.g., `myField=value` means records will be returned where myField equals value). Alternative operators can be used as follows:
|
||||
* Equals: `myField=value`
|
||||
* Not Equals: `myField=!value`
|
||||
* Less Than: `myField=<value`
|
||||
* Greater Than: `myField=>value`
|
||||
* Less Than or Equals: `myField=<=value`
|
||||
* Greater Than or Equals: `myField=>=value`
|
||||
* Empty (or null): `myField=EMPTY`
|
||||
* Not Empty: `myField=!EMPTY`
|
||||
* Between: `myField=BETWEEN value1,value2` (two values must be given, separated by commas)
|
||||
* Not Between: `myField=!BETWEEN value1,value2` (two values must be given, separated by commas)
|
||||
* In: `myField=IN value1,value2,...,valueN` (one or more values must be given, separated by commas)
|
||||
* Not In: `myField=!IN value1,value2,...,valueN` (one or more values must be given, separated by commas)
|
||||
* Like: `myField=LIKE value` (using standard SQL % and _ wildcards)
|
||||
* Not Like: `myField=!LIKE value` (using standard SQL % and _ wildcards)
|
||||
""")
|
||||
.withDescription(QUERY_DESCRIPTION)
|
||||
.withOperationId("query" + tableApiNameUcFirst)
|
||||
.withTags(ListBuilder.of(tableLabel))
|
||||
.withParameters(ListBuilder.of(
|
||||
@ -390,9 +465,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method idGet = new Method()
|
||||
.withSummary("Get one " + tableLabel + " by " + primaryKeyLabel)
|
||||
.withDescription("""
|
||||
Get one record from this table, by specifying its primary key as a path parameter.
|
||||
""")
|
||||
.withDescription(GET_DESCRIPTION)
|
||||
.withOperationId("get" + tableApiNameUcFirst)
|
||||
.withTags(ListBuilder.of(tableLabel))
|
||||
.withParameters(ListBuilder.of(
|
||||
@ -412,16 +485,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method idPatch = new Method()
|
||||
.withSummary("Update one " + tableLabel)
|
||||
.withDescription("""
|
||||
Update one record in this table, by specifying its primary key as a path parameter, and by supplying values to be updated in the request body.
|
||||
|
||||
* Only the fields provided in the request body will be updated.
|
||||
* To remove a value from a field, supply the key for the field, with a null value.
|
||||
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
|
||||
* Note that if the request body includes the primary key, it will be ignored. Only the primary key value path parameter will be used.
|
||||
|
||||
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
|
||||
""")
|
||||
.withDescription(UPDATE_DESCRIPTION)
|
||||
.withOperationId("update" + tableApiNameUcFirst)
|
||||
.withTags(ListBuilder.of(tableLabel))
|
||||
.withParameters(ListBuilder.of(
|
||||
@ -443,11 +507,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method idDelete = new Method()
|
||||
.withSummary("Delete one " + tableLabel)
|
||||
.withDescription("""
|
||||
Delete one record from this table, by specifying its primary key as a path parameter.
|
||||
|
||||
Upon success, a status code of 204 (`No Content`) is returned, with no response body.
|
||||
""")
|
||||
.withDescription(DELETE_DESCRIPTION)
|
||||
.withOperationId("delete" + tableApiNameUcFirst)
|
||||
.withTags(ListBuilder.of(tableLabel))
|
||||
.withParameters(ListBuilder.of(
|
||||
@ -473,12 +533,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method slashPost = new Method()
|
||||
.withSummary("Create one " + tableLabel)
|
||||
.withDescription("""
|
||||
Insert one record into this table by supplying the values to be inserted in the request body.
|
||||
* The request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body.
|
||||
|
||||
Upon success, a status code of 201 (`Created`) is returned, and the generated value for the primary key will be returned in the response body object.
|
||||
""")
|
||||
.withDescription(INSERT_DESCRIPTION)
|
||||
.withRequestBody(new RequestBody()
|
||||
.withRequired(true)
|
||||
.withDescription("Values for the " + tableLabel + " record to create.")
|
||||
@ -508,14 +563,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
////////////////
|
||||
Method bulkPost = new Method()
|
||||
.withSummary("Create multiple " + tableLabel + " records")
|
||||
.withDescription("""
|
||||
Insert one or more records into this table by supplying array of records with values to be inserted, in the request body.
|
||||
* The objects in the request body should not include a value for the table's primary key. Rather, a value will be generated and returned in a successful response's body
|
||||
|
||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
||||
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
|
||||
* For sub-status codes of 201 (`Created`), and the generated value for the primary key will be returned in the response body object.
|
||||
""")
|
||||
.withDescription(BULK_INSERT_DESCRIPTION)
|
||||
.withRequestBody(new RequestBody()
|
||||
.withRequired(true)
|
||||
.withDescription("Values for the " + tableLabel + " records to create.")
|
||||
@ -530,15 +578,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method bulkPatch = new Method()
|
||||
.withSummary("Update multiple " + tableLabel + " records")
|
||||
.withDescription("""
|
||||
Update one or more records in this table, by supplying an array of records, with primary keys and values to be updated, in the request body.
|
||||
* Only the fields provided in the request body will be updated.
|
||||
* To remove a value from a field, supply the key for the field, with a null value.
|
||||
* The request body does not need to contain all fields from the table. Rather, only the fields to be updated should be supplied.
|
||||
|
||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
||||
* The 1st record in the request will have its response in the 1st object in the response, and so-forth.
|
||||
""")
|
||||
.withDescription(BULK_UPDATE_DESCRIPTION)
|
||||
.withRequestBody(new RequestBody()
|
||||
.withRequired(true)
|
||||
.withDescription("Values for the " + tableLabel + " records to update.")
|
||||
@ -559,12 +599,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
|
||||
Method bulkDelete = new Method()
|
||||
.withSummary("Delete multiple " + tableLabel + " records")
|
||||
.withDescription("""
|
||||
Delete one or more records from this table, by supplying an array of primary key values in the request body.
|
||||
|
||||
An HTTP 207 (`Multi-Status`) code is generally returned, with an array of objects giving the individual sub-status codes for each record in the request body.
|
||||
* The 1st primary key in the request will have its response in the 1st object in the response, and so-forth.
|
||||
""")
|
||||
.withDescription(BULK_DELETE_DESCRIPTION)
|
||||
.withRequestBody(new RequestBody()
|
||||
.withRequired(true)
|
||||
.withDescription(primaryKeyLabel + " values for the " + tableLabel + " records to delete.")
|
||||
@ -835,20 +870,34 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
case "patch" -> ListBuilder.of(
|
||||
MapBuilder.of(LinkedHashMap::new)
|
||||
.with("statusCode", HttpStatus.NO_CONTENT.getCode())
|
||||
.with("statusText", HttpStatus.NO_CONTENT.getMessage()).build(),
|
||||
.with("statusText", HttpStatus.NO_CONTENT.getMessage())
|
||||
.with(primaryKeyApiName, "47").build(),
|
||||
MapBuilder.of(LinkedHashMap::new)
|
||||
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
||||
.with("error", "Could not update " + tableLabel + ": Duplicate value in unique key field.").build()
|
||||
.with("error", "Could not update " + tableLabel + ": Missing value in required field: My Field.")
|
||||
.with(primaryKeyApiName, "47").build(),
|
||||
MapBuilder.of(LinkedHashMap::new)
|
||||
.with("statusCode", HttpStatus.NOT_FOUND.getCode())
|
||||
.with("statusText", HttpStatus.NOT_FOUND.getMessage())
|
||||
.with("error", "The requested " + tableLabel + " to update was not found.")
|
||||
.with(primaryKeyApiName, "47").build()
|
||||
);
|
||||
case "delete" -> ListBuilder.of(
|
||||
MapBuilder.of(LinkedHashMap::new)
|
||||
.with("statusCode", HttpStatus.NO_CONTENT.getCode())
|
||||
.with("statusText", HttpStatus.NO_CONTENT.getMessage()).build(),
|
||||
.with("statusText", HttpStatus.NO_CONTENT.getMessage())
|
||||
.with(primaryKeyApiName, "47").build(),
|
||||
MapBuilder.of(LinkedHashMap::new)
|
||||
.with("statusCode", HttpStatus.BAD_REQUEST.getCode())
|
||||
.with("statusText", HttpStatus.BAD_REQUEST.getMessage())
|
||||
.with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.").build()
|
||||
.with("error", "Could not delete " + tableLabel + ": Foreign key constraint violation.")
|
||||
.with(primaryKeyApiName, "47").build(),
|
||||
MapBuilder.of(LinkedHashMap::new)
|
||||
.with("statusCode", HttpStatus.NOT_FOUND.getCode())
|
||||
.with("statusText", HttpStatus.NOT_FOUND.getMessage())
|
||||
.with("error", "The requested " + tableLabel + " to delete was not found.")
|
||||
.with(primaryKeyApiName, "47").build()
|
||||
);
|
||||
default -> throw (new IllegalArgumentException("Unrecognized method: " + method));
|
||||
};
|
||||
@ -857,10 +906,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
properties.put("statusCode", new Schema().withType("integer"));
|
||||
properties.put("statusText", new Schema().withType("string"));
|
||||
properties.put("error", new Schema().withType("string"));
|
||||
if(method.equalsIgnoreCase("post"))
|
||||
{
|
||||
properties.put(primaryKeyApiName, new Schema().withType(getFieldType(primaryKeyField)));
|
||||
}
|
||||
properties.put(primaryKeyApiName, new Schema().withType(getFieldType(primaryKeyField)));
|
||||
|
||||
return new Response()
|
||||
.withDescription("Multiple statuses. See body for details.")
|
||||
@ -870,9 +916,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction<GenerateO
|
||||
.withItems(new Schema()
|
||||
.withType("object")
|
||||
.withProperties(properties))
|
||||
.withExample(example)
|
||||
)
|
||||
));
|
||||
.withExample(example))));
|
||||
}
|
||||
|
||||
|
||||
|
@ -34,6 +34,7 @@ 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.logging.QLogger;
|
||||
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;
|
||||
@ -50,6 +51,8 @@ import org.json.JSONObject;
|
||||
*******************************************************************************/
|
||||
public class QRecordApiAdapter
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QRecordApiAdapter.class);
|
||||
|
||||
private static Map<Pair<String, String>, List<QFieldMetaData>> fieldListCache = new HashMap<>();
|
||||
private static Map<Pair<String, String>, Map<String, QFieldMetaData>> fieldMapCache = new HashMap<>();
|
||||
|
||||
@ -105,7 +108,7 @@ public class QRecordApiAdapter
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiVersion) throws QException
|
||||
public static QRecord apiJsonObjectToQRecord(JSONObject jsonObject, String tableName, String apiVersion, boolean includePrimaryKey) throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// make map of apiFieldNames (e.g., names as api uses them) to QFieldMetaData //
|
||||
@ -134,6 +137,22 @@ public class QRecordApiAdapter
|
||||
QFieldMetaData field = apiFieldsMap.get(jsonKey);
|
||||
Object value = jsonObject.isNull(jsonKey) ? null : jsonObject.get(jsonKey);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// generally, omit non-editable fields - //
|
||||
// however - if we're asked to include the primary key (and this is the primary key), then include it //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!field.getIsEditable())
|
||||
{
|
||||
if(includePrimaryKey && field.getName().equals(table.getPrimaryKeyField()))
|
||||
{
|
||||
LOG.trace("Even though field [" + field.getName() + "] is not editable, we'll use it, because it's the primary key, and we've been asked to include primary keys");
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ApiFieldMetaData apiFieldMetaData = ApiFieldMetaData.of(field);
|
||||
if(StringUtils.hasContent(apiFieldMetaData.getReplacedByFieldName()))
|
||||
{
|
||||
@ -146,6 +165,11 @@ public class QRecordApiAdapter
|
||||
}
|
||||
else if(associationMap.containsKey(jsonKey))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, if it's an association - process that (recursively as a list of other records) //
|
||||
// todo - should probably define in meta-data if an association is included in the api or not!! //
|
||||
// and what its name is too... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Association association = associationMap.get(jsonKey);
|
||||
Object value = jsonObject.get(jsonKey);
|
||||
if(value instanceof JSONArray jsonArray)
|
||||
@ -154,7 +178,7 @@ public class QRecordApiAdapter
|
||||
{
|
||||
if(subObject instanceof JSONObject subJsonObject)
|
||||
{
|
||||
QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion);
|
||||
QRecord subRecord = apiJsonObjectToQRecord(subJsonObject, association.getAssociatedTableName(), apiVersion, includePrimaryKey);
|
||||
qRecord.withAssociatedRecord(association.getName(), subRecord);
|
||||
}
|
||||
else
|
||||
|
@ -1066,7 +1066,7 @@ public class QJavalinApiHandler
|
||||
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
|
||||
JSONObject jsonObject = new JSONObject(jsonTokener);
|
||||
|
||||
insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version)));
|
||||
insertInput.setRecords(List.of(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false)));
|
||||
|
||||
if(jsonTokener.more())
|
||||
{
|
||||
@ -1142,7 +1142,7 @@ public class QJavalinApiHandler
|
||||
for(int i = 0; i < jsonArray.length(); i++)
|
||||
{
|
||||
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version));
|
||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false));
|
||||
}
|
||||
|
||||
if(jsonTokener.more())
|
||||
@ -1248,7 +1248,7 @@ public class QJavalinApiHandler
|
||||
for(int i = 0; i < jsonArray.length(); i++)
|
||||
{
|
||||
JSONObject jsonObject = jsonArray.getJSONObject(i);
|
||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version));
|
||||
recordList.add(QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, true));
|
||||
}
|
||||
|
||||
if(jsonTokener.more())
|
||||
@ -1280,23 +1280,47 @@ public class QJavalinApiHandler
|
||||
// process records to build response //
|
||||
///////////////////////////////////////
|
||||
List<Map<String, Serializable>> response = new ArrayList<>();
|
||||
int i = 0;
|
||||
for(QRecord record : updateOutput.getRecords())
|
||||
{
|
||||
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||
response.add(outputRecord);
|
||||
|
||||
try
|
||||
{
|
||||
QRecord inputRecord = updateInput.getRecords().get(i);
|
||||
Serializable primaryKey = inputRecord.getValue(table.getPrimaryKeyField());
|
||||
outputRecord.put(table.getPrimaryKeyField(), primaryKey);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//////////
|
||||
// omit //
|
||||
//////////
|
||||
}
|
||||
|
||||
List<String> errors = record.getErrors();
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||
outputRecord.put("error", "Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
|
||||
if(areAnyErrorsNotFound(errors))
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.NO_CONTENT.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.NO_CONTENT.getMessage());
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
QJavalinAccessLogger.logEndSuccess();
|
||||
@ -1312,6 +1336,16 @@ public class QJavalinApiHandler
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static boolean areAnyErrorsNotFound(List<String> errors)
|
||||
{
|
||||
return errors.stream().anyMatch(e -> e.startsWith(UpdateAction.NOT_FOUND_ERROR_PREFIX) || e.startsWith(DeleteAction.NOT_FOUND_ERROR_PREFIX));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1390,25 +1424,35 @@ public class QJavalinApiHandler
|
||||
///////////////////////////////////////
|
||||
List<Map<String, Serializable>> response = new ArrayList<>();
|
||||
|
||||
List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors();
|
||||
Map<String, String> primaryKeyToErrorMap = new HashMap<>();
|
||||
List<QRecord> recordsWithErrors = deleteOutput.getRecordsWithErrors();
|
||||
Map<String, List<String>> primaryKeyToErrorsMap = new HashMap<>();
|
||||
for(QRecord recordWithError : CollectionUtils.nonNullList(recordsWithErrors))
|
||||
{
|
||||
String primaryKey = recordWithError.getValueString(table.getPrimaryKeyField());
|
||||
primaryKeyToErrorMap.put(primaryKey, StringUtils.join(", ", recordWithError.getErrors()));
|
||||
primaryKeyToErrorsMap.put(primaryKey, recordWithError.getErrors());
|
||||
}
|
||||
|
||||
for(Serializable primaryKey : deleteInput.getPrimaryKeys())
|
||||
{
|
||||
LinkedHashMap<String, Serializable> outputRecord = new LinkedHashMap<>();
|
||||
response.add(outputRecord);
|
||||
outputRecord.put(table.getPrimaryKeyField(), primaryKey);
|
||||
|
||||
String primaryKeyString = ValueUtils.getValueAsString((primaryKey));
|
||||
if(primaryKeyToErrorMap.containsKey(primaryKeyString))
|
||||
String primaryKeyString = ValueUtils.getValueAsString(primaryKey);
|
||||
List<String> errors = primaryKeyToErrorsMap.get(primaryKeyString);
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||
outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + primaryKeyToErrorMap.get(primaryKeyString));
|
||||
outputRecord.put("error", "Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors));
|
||||
if(areAnyErrorsNotFound(errors))
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.NOT_FOUND.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.NOT_FOUND.getMessage());
|
||||
}
|
||||
else
|
||||
{
|
||||
outputRecord.put("statusCode", HttpStatus.Code.BAD_REQUEST.getCode());
|
||||
outputRecord.put("statusText", HttpStatus.Code.BAD_REQUEST.getMessage());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1453,20 +1497,6 @@ public class QJavalinApiHandler
|
||||
|
||||
PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT);
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// throw a not found error if the record isn't found //
|
||||
///////////////////////////////////////////////////////
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(tableName);
|
||||
getInput.setPrimaryKey(primaryKey);
|
||||
GetAction getAction = new GetAction();
|
||||
GetOutput getOutput = getAction.execute(getInput);
|
||||
if(getOutput.getRecord() == null)
|
||||
{
|
||||
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
|
||||
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if(!StringUtils.hasContent(context.body()))
|
||||
@ -1477,7 +1507,7 @@ public class QJavalinApiHandler
|
||||
JSONTokener jsonTokener = new JSONTokener(context.body().trim());
|
||||
JSONObject jsonObject = new JSONObject(jsonTokener);
|
||||
|
||||
QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version);
|
||||
QRecord qRecord = QRecordApiAdapter.apiJsonObjectToQRecord(jsonObject, tableName, version, false);
|
||||
qRecord.setValue(table.getPrimaryKeyField(), primaryKey);
|
||||
updateInput.setRecords(List.of(qRecord));
|
||||
|
||||
@ -1501,7 +1531,17 @@ public class QJavalinApiHandler
|
||||
List<String> errors = updateOutput.getRecords().get(0).getErrors();
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
throw (new QException("Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)));
|
||||
if(areAnyErrorsNotFound(errors))
|
||||
{
|
||||
throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - could be smarter here, about some of these errors being 400, not 500... e.g., a missing required field //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
throw (new QException("Error updating " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(errors)));
|
||||
}
|
||||
}
|
||||
|
||||
QJavalinAccessLogger.logEndSuccess();
|
||||
@ -1540,20 +1580,6 @@ public class QJavalinApiHandler
|
||||
|
||||
PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE);
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// throw a not found error if the record isn't found //
|
||||
///////////////////////////////////////////////////////
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(tableName);
|
||||
getInput.setPrimaryKey(primaryKey);
|
||||
GetAction getAction = new GetAction();
|
||||
GetOutput getOutput = getAction.execute(getInput);
|
||||
if(getOutput.getRecord() == null)
|
||||
{
|
||||
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
|
||||
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
||||
}
|
||||
|
||||
///////////////////
|
||||
// do the delete //
|
||||
///////////////////
|
||||
@ -1561,7 +1587,14 @@ public class QJavalinApiHandler
|
||||
DeleteOutput deleteOutput = deleteAction.execute(deleteInput);
|
||||
if(CollectionUtils.nullSafeHasContents(deleteOutput.getRecordsWithErrors()))
|
||||
{
|
||||
throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors())));
|
||||
if(areAnyErrorsNotFound(deleteOutput.getRecordsWithErrors().get(0).getErrors()))
|
||||
{
|
||||
throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("Error deleting " + table.getLabel() + ": " + StringUtils.joinWithCommasAndAnd(deleteOutput.getRecordsWithErrors().get(0).getErrors())));
|
||||
}
|
||||
}
|
||||
|
||||
QJavalinAccessLogger.logEndSuccess();
|
||||
|
Reference in New Issue
Block a user