diff --git a/packages/cli/generators/rest-crud/index.js b/packages/cli/generators/rest-crud/index.js index 5a9f921e7341..3692ec95c31d 100644 --- a/packages/cli/generators/rest-crud/index.js +++ b/packages/cli/generators/rest-crud/index.js @@ -367,7 +367,19 @@ module.exports = class RestCrudGenerator extends ArtifactGenerator { debug(`artifactInfo: ${inspect(this.artifactInfo)}`); debug(`Copying artifact to: ${dest}`); } - + this.artifactInfo.upsert = false; + if (this.options.upsert.includes('*')) { + this.artifactInfo.upsert = true; + } else { + this.options.upsert.forEach(modelName => { + if ( + this.artifactInfo.modelName.toLowerCase() === + modelName.toLowerCase() + ) { + this.artifactInfo.upsert = true; + } + }); + } this.copyTemplatedFiles(source, dest, this.artifactInfo); } diff --git a/packages/cli/generators/rest-crud/templates/src/model-endpoints/model.rest-config-template.ts.ejs b/packages/cli/generators/rest-crud/templates/src/model-endpoints/model.rest-config-template.ts.ejs index 703a5f1112c8..858f45d6b2d9 100644 --- a/packages/cli/generators/rest-crud/templates/src/model-endpoints/model.rest-config-template.ts.ejs +++ b/packages/cli/generators/rest-crud/templates/src/model-endpoints/model.rest-config-template.ts.ejs @@ -7,5 +7,6 @@ const config: ModelCrudRestApiConfig = { dataSource: '<%= dataSourceName %>', basePath: '<%= basePath %>', readonly: <%= readonly %>, + upsert: <%= upsert %>, }; module.exports = config; diff --git a/packages/repository/src/connectors/crud.connector.ts b/packages/repository/src/connectors/crud.connector.ts index 6ebf61060479..2971cf0f263f 100644 --- a/packages/repository/src/connectors/crud.connector.ts +++ b/packages/repository/src/connectors/crud.connector.ts @@ -25,6 +25,19 @@ export interface CrudConnector extends Connector { options?: Options, ): Promise; + /** + * Update an existing entity or Create if it does not exist + * @param modelClass - The model class + * @param entity - The entity instance or data + * @param options - Options for the operation + * @returns A promise of the entity created + */ + upsert( + modelClass: Class, + entity: EntityData, + options?: Options, + ): Promise; + /** * Create multiple entities * @param modelClass - The model class diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 3df52ed95514..6dec226cd2f4 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -493,6 +493,12 @@ export class DefaultCrudRepository< return this.toEntity(model); } + async upsert(entity: DataObject, options?: Options): Promise { + const data = await this.entityToData(entity, options); + const model = await ensurePromise(this.modelClass.upsert(data, options)); + return this.toEntity(model); + } + async createAll(entities: DataObject[], options?: Options): Promise { // perform persist hook const data = await Promise.all( diff --git a/packages/repository/src/repositories/repository.ts b/packages/repository/src/repositories/repository.ts index 6cb2cd507eb1..8d345fe7b83a 100644 --- a/packages/repository/src/repositories/repository.ts +++ b/packages/repository/src/repositories/repository.ts @@ -79,6 +79,15 @@ export interface CrudRepository< */ create(dataObject: DataObject, options?: Options): Promise; + /** + * Update an existing entity or Create if it does not exist + * @param modelClass - The model class + * @param entity - The entity instance or data + * @param options - Options for the operation + * @returns A promise of the entity created + */ + upsert(dataObject: DataObject, options?: Options): Promise; + /** * Create all records * @param dataObjects - An array of data to be created @@ -284,6 +293,12 @@ export class CrudRepositoryImpl ); } + upsert(entity: DataObject, options?: Options): Promise { + return this.toModel( + this.connector.upsert(this.entityClass, entity, options), + ); + } + createAll(entities: DataObject[], options?: Options): Promise { return this.toModels( this.connector.createAll!(this.entityClass, entities, options), diff --git a/packages/rest-crud/src/crud-rest.controller.ts b/packages/rest-crud/src/crud-rest.controller.ts index 0b8446712c9c..577f1eee8d12 100644 --- a/packages/rest-crud/src/crud-rest.controller.ts +++ b/packages/rest-crud/src/crud-rest.controller.ts @@ -105,6 +105,7 @@ export interface CrudRestControllerOptions { * Whether to generate readonly APIs */ readonly?: boolean; + upsert?: boolean; } /** @@ -276,14 +277,42 @@ export function defineCrudRestController< } } + @api({basePath: options.basePath, paths: {}}) + class CrudRestControllerWithUpsertImpl extends CrudRestControllerImpl { + constructor( + public readonly repository: EntityCrudRepository, + ) { + super(repository); + } + @post('/upsert', { + ...response.model(200, `${modelName} instance created`, modelCtor), + }) + async upsert( + @body(modelCtor, { + title: `New${modelName}`, + exclude: modelCtor.getIdProperties() as (keyof T)[], + }) + data: Omit, + ): Promise { + return this.repository.upsert( + // FIXME(bajtos) Improve repository API to support this use case + // with no explicit type-casts required + data as DataObject, + ); + } + } + const controllerName = modelName + 'Controller'; const defineNamedController = new Function( 'controllerClass', `return class ${controllerName} extends controllerClass {}`, ); - const controller = defineNamedController( - options.readonly ? ReadonlyRestControllerImpl : CrudRestControllerImpl, - ); + let controllerImplementation = ReadonlyRestControllerImpl; + if (options.readonly) controllerImplementation = ReadonlyRestControllerImpl; + if (options.upsert) + controllerImplementation = CrudRestControllerWithUpsertImpl; + + const controller = defineNamedController(controllerImplementation); assert.equal(controller.name, controllerName); return controller; }