From bd3e5df8ba54ee3bad4a60c5235df7165d1c0b15 Mon Sep 17 00:00:00 2001 From: Jan Jannek Date: Thu, 19 Dec 2024 15:20:29 +0100 Subject: [PATCH] DATAAPI-36: spec updated to support different response types --- handlers/rest-apis/data/v1/SingleRecord.cfc | 2 +- i18n/dataapi.properties | 7 ++- services/DataApiConfigurationService.cfc | 36 ++++++++++++ services/DataApiService.cfc | 19 ++++--- services/DataApiSpecService.cfc | 63 +++++++++++++++++---- 5 files changed, 107 insertions(+), 20 deletions(-) diff --git a/handlers/rest-apis/data/v1/SingleRecord.cfc b/handlers/rest-apis/data/v1/SingleRecord.cfc index 00605a5..16b23a4 100644 --- a/handlers/rest-apis/data/v1/SingleRecord.cfc +++ b/handlers/rest-apis/data/v1/SingleRecord.cfc @@ -83,7 +83,7 @@ component { , title = "Not found" , message = "No [#arguments.entity#] record was found with ID [#arguments.recordId#]" ); - } else if ( IsStruct( updated ) ) { // updated and configured to either return the full record or the id only + } else if ( IsStruct( updated ) || IsArray( updated ) ) { // updated and configured to either return the full record or the id only (in an array) restResponse.setData( updated ); } else { // updated but configured to return an empty response restResponse.noData(); diff --git a/i18n/dataapi.properties b/i18n/dataapi.properties index 2063306..0323805 100644 --- a/i18n/dataapi.properties +++ b/i18n/dataapi.properties @@ -82,19 +82,24 @@ operation.get.by.id.params.recordId=ID of the **{1}** record that you wish to fe operation.put.description=Used to batch update **{1}** records. operation.put.200.description=Array of updated **{1}** records. +operation.put.200.description.idonly=Array of updated **{1}** records IDs. +operation.put.200.description.empty=Empty operation.put.422.description=Data validation failure. Returned when one or more entities failed validation. No records will have been updated. operation.put.body.description=Array of **{1}** objects to update. Missing fields per item will be ignored and only fields given will be updated. operation.put.by.id.description=Used to update an individual **{1}** record by ID. operation.put.by.id.200.description=Updated **{1}** record object +operation.put.by.id.200.description.idonly=ID of the updated **{1}** record object +operation.put.by.id.200.description.empty=Empty operation.put.by.id.404.description=No record found for the given ID operation.put.by.id.422.description=Data validation failure. Returned when one or more fields have failed validation. operation.put.by.id.params.recordId=ID of the **{1}** record that you wish to update. operation.put.by.id.body.description=**{1}** object with fields to update. Missing fields will be ignored. - operation.post.description=Used to create **{1}** records. operation.post.200.description=Array of created **{1}** records. +operation.post.200.description.idonly=Array of created **{1}** record IDs. +operation.post.200.description.empty=Empty operation.post.422.description=Data validation failure. Returned when one or more entities failed validation. No records will have been created. operation.post.body.description=Array of **{1}** objects to create. diff --git a/services/DataApiConfigurationService.cfc b/services/DataApiConfigurationService.cfc index 7e146f8..0fd6326 100755 --- a/services/DataApiConfigurationService.cfc +++ b/services/DataApiConfigurationService.cfc @@ -63,6 +63,42 @@ component { return _entityConfigOption( arguments.entity, "responseTypeOnUpdate", DEFAULT_RESPONSE_TYPE ); } + public boolean function entityUseRecordResponseOnInsert( required string entity ) { + return isRecordResponseType( entityResponseTypeOnInsert( arguments.entity ) ); + } + + public boolean function entityUseIdOnlyResponseOnInsert( required string entity ) { + return isIdOnlyResponseType( entityResponseTypeOnInsert( arguments.entity ) ); + } + + public boolean function entityUseEmptyResponseOnInsert( required string entity ) { + return isEmptyResponseType( entityResponseTypeOnInsert( arguments.entity ) ); + } + + public boolean function entityUseRecordResponseOnUpdate( required string entity ) { + return isRecordResponseType( entityResponseTypeOnUpdate( arguments.entity ) ); + } + + public boolean function entityUseIdOnlyResponseOnUpdate( required string entity ) { + return isIdOnlyResponseType( entityResponseTypeOnUpdate( arguments.entity ) ); + } + + public boolean function entityUseEmptyResponseOnUpdate( required string entity ) { + return isEmptyResponseType( entityResponseTypeOnUpdate( arguments.entity ) ); + } + + public boolean function isRecordResponseType( required string responseType ) { + return arguments.responseType == VALID_RESPONSE_TYPES.RECORD; + } + + public boolean function isIdOnlyResponseType( required string responseType ) { + return arguments.responseType == VALID_RESPONSE_TYPES.IDONLY; + } + + public boolean function isEmptyResponseType( required string responseType ) { + return arguments.responseType == VALID_RESPONSE_TYPES.EMPTY; + } + public string function getEntityObject( required string entity, string namespace=_getDataApiNamespace() ) { var args = arguments; var cacheKey = "getEntityObject" & args.namespace & args.entity; diff --git a/services/DataApiService.cfc b/services/DataApiService.cfc index 79a8cb6..21bf6ad 100644 --- a/services/DataApiService.cfc +++ b/services/DataApiService.cfc @@ -136,9 +136,9 @@ component { interceptDataArgs.responseType = _getConfigService().validateResponseType( interceptDataArgs.responseType ); - if ( interceptDataArgs.responseType == "empty" ) { + if ( _getConfigService().isEmptyResponseType( interceptDataArgs.responseType ) ) { return ""; - } else if ( interceptDataArgs.responseType == "idonly" ) { + } else if ( _getConfigService().isIdOnlyResponseType( interceptDataArgs.responseType ) ) { return newId; } @@ -160,13 +160,16 @@ component { interceptDataArgs.responseType = _getConfigService().validateResponseType( interceptDataArgs.responseType ); + var isEmptyResponseType = _getConfigService().isEmptyResponseType( interceptDataArgs.responseType ); + var isIdOnlyResponseType = _getConfigService().isIdOnlyResponseType( interceptDataArgs.responseType ); + for( var record in records ) { recordId = record[ idField ] ?: ""; if ( Len( Trim( recordId ) ) ) { if ( updateSingleRecord( arguments.entity, record, recordId ) ) { - if ( interceptDataArgs.responseType == "empty" ) { + if ( isEmptyResponseType ) { continue; - } else if ( interceptDataArgs.responseType == "idonly" ) { + } else if ( isIdOnlyResponseType ) { ArrayAppend( updated, recordId ); } else { ArrayAppend( updated, getSingleRecord( entity, recordId, [] ) ); @@ -181,7 +184,7 @@ component { interceptDataArgs.responseType = _getConfigService().validateResponseType( interceptDataArgs.responseType ); - return interceptDataArgs.responseType == "empty" ? "" : interceptDataArgs.updated; + return _getConfigService().isEmptyResponseType( interceptDataArgs.responseType ) ? "" : interceptDataArgs.updated; } public any function updateSingleRecord( required string entity, required struct data, required string recordId, boolean returnResponse=false ) { @@ -208,11 +211,11 @@ component { if ( recordsUpdated == 0 ) { return 0; } - else if ( interceptDataArgs.responseType == "record" ) { + else if ( _getConfigService().isRecordResponseType( interceptDataArgs.responseType ) ) { return getSingleRecord( arguments.entity, arguments.recordId, [] ); } - else if ( interceptDataArgs.responseType == "idonly" ) { - return { id=arguments.recordId }; + else if ( _getConfigService().isIdOnlyResponseType( interceptDataArgs.responseType ) ) { + return [ arguments.recordId ]; } else { // empty return ""; diff --git a/services/DataApiSpecService.cfc b/services/DataApiSpecService.cfc index 9e50c2b..b3f8185 100644 --- a/services/DataApiSpecService.cfc +++ b/services/DataApiSpecService.cfc @@ -223,10 +223,17 @@ component { private void function _addEntitySpecs( required struct spec ) { var configService = _getConfigService(); - var entities = _getConfigService().getEntities(); - var entityNames = StructKeyArray( entities ); - var tags = []; - var categories = {}; + var entities = _getConfigService().getEntities(); + var entityNames = StructKeyArray( entities ); + var tags = []; + var categories = {}; + + var emptyResponseContent = { "text/plain" = { schema={ type="string" } } }; + var idArrayResponseContent = { "application/json" = { schema={ type="array", items={ type="string" } } } }; + + var updateSingleResponseContent = ""; + var updateMultiResponseContent = ""; + var insertMultiResponseContent = ""; entityNames.sort( "textnocase" ); @@ -243,6 +250,21 @@ component { tag[ "x-sort-order" ] = _i18nNamespaced( uri="dataapi:entity.#entityName#.sort.order", defaultValue=tag.name ); + var insertResponseI18nSuffix = ""; + var updateResponseI18nSuffix = ""; + + if ( configService.entityUseEmptyResponseOnInsert( entityName ) ) { + insertResponseI18nSuffix = ".empty"; + } else if ( configService.entityUseIdOnlyResponseOnInsert( entityName ) ) { + insertResponseI18nSuffix = ".idonly"; + } + + if ( configService.entityUseEmptyResponseOnUpdate( entityName ) ) { + updateResponseI18nSuffix = ".empty"; + } else if ( configService.entityUseIdOnlyResponseOnUpdate( entityName ) ) { + updateResponseI18nSuffix = ".idonly"; + } + if ( Len( Trim( category ) ) ) { tag[ "x-category" ] = category; @@ -349,6 +371,18 @@ component { } if ( configService.entityVerbIsSupported( entityName, "put" ) ) { + + if ( configService.entityUseEmptyResponseOnUpdate( entityName ) ) { + updateSingleResponseContent = emptyResponseContent; + updateMultiResponseContent = emptyResponseContent; + } else if ( configService.entityUseIdOnlyResponseOnUpdate( entityName ) ) { + updateSingleResponseContent = idArrayResponseContent; + updateMultiResponseContent = idArrayResponseContent; + } else { // record (default behaviour) + updateSingleResponseContent = { "application/json" = { schema={ "$ref"="##/components/schemas/#entityName#" } } }; + updateMultiResponseContent = { "application/json" = { schema={ type="array", items={"$ref"="##/components/schemas/#entityName#" } } } }; + } + spec.paths[ "/entity/#entityName#/" ].put = { tags = [ entityTag ] , summary = "PUT /entity/#entityName#/" @@ -362,8 +396,8 @@ component { } , responses = { "200" = { - description = _i18nNamespaced( uri="dataapi:operation.#entityName#.put.200.description", defaultValue=_i18nNamespaced( uri="dataapi:operation.put.200.description", defaultValue="", data=[ entitySingular ] ) ) - , content = { "application/json" = { schema={ type="array", items={"$ref"="##/components/schemas/#entityName#" } } } } + description = _i18nNamespaced( uri="dataapi:operation.#entityName#.put.200.description#updateResponseI18nSuffix#", defaultValue=_i18nNamespaced( uri="dataapi:operation.put.200.description#updateResponseI18nSuffix#", defaultValue="", data=[ entitySingular ] ) ) + , content = updateMultiResponseContent } , "422" = { description = _i18nNamespaced( uri="dataapi:operation.#entityName#.put.422.description", defaultValue=_i18nNamespaced( uri="dataapi:operation.put.422.description", defaultValue="", data=[ entitySingular ] ) ) @@ -385,8 +419,8 @@ component { } , responses = { "200" = { - description = _i18nNamespaced( uri="dataapi:operation.#entityName#.put.by.id.200.description", defaultValue=_i18nNamespaced( uri="dataapi:operation.put.by.id.200.description", defaultValue="", data=[ entitySingular ] ) ) - , content = { "application/json" = { schema={ "$ref"="##/components/schemas/#entityName#" } } } + description = _i18nNamespaced( uri="dataapi:operation.#entityName#.put.by.id.200.description#updateResponseI18nSuffix#", defaultValue=_i18nNamespaced( uri="dataapi:operation.put.by.id.200.description#updateResponseI18nSuffix#", defaultValue="", data=[ entitySingular ] ) ) + , content = updateSingleResponseContent } , "404" = { description = _i18nNamespaced( uri="dataapi:operation.#entityName#.put.by.id.404.description", defaultValue=_i18nNamespaced( uri="dataapi:operation.put.by.id.404.description", defaultValue="", data=[ entitySingular ] ) ) @@ -407,6 +441,15 @@ component { } if ( configService.entityVerbIsSupported( entityName, "post" ) ) { + + if ( configService.entityUseEmptyResponseOnInsert( entityName ) ) { + insertMultiResponseContent = emptyResponseContent; + } else if ( configService.entityUseIdOnlyResponseOnInsert( entityName ) ) { + insertMultiResponseContent = idArrayResponseContent; + } else { // record (default behaviour) + insertMultiResponseContent = { "application/json" = { schema={ type="array", items={"$ref"="##/components/schemas/#entityName#" } } } }; + } + spec.paths[ "/entity/#entityName#/" ].post = { tags = [ entityTag ] , summary = "POST /entity/#entityName#/" @@ -420,8 +463,8 @@ component { } , responses = { "200" = { - description = _i18nNamespaced( uri="dataapi:operation.#entityName#.post.200.description", defaultValue=_i18nNamespaced( uri="dataapi:operation.post.200.description", defaultValue="", data=[ entitySingular ] ) ) - , content = { "application/json" = { schema={ type="array", items={"$ref"="##/components/schemas/#entityName#" } } } } + description = _i18nNamespaced( uri="dataapi:operation.#entityName#.post.200.description#insertResponseI18nSuffix#", defaultValue=_i18nNamespaced( uri="dataapi:operation.post.200.description#insertResponseI18nSuffix#", defaultValue="", data=[ entitySingular ] ) ) + , content = insertMultiResponseContent } , "422" = { description = _i18nNamespaced( uri="dataapi:operation.#entityName#.post.422.description", defaultValue=_i18nNamespaced( uri="dataapi:operation.post.422.description", defaultValue="", data=[ entitySingular ] ) )