Skip to content

Commit 87ad002

Browse files
committed
feat: support upsert endpoint
Signed-off-by: Muhammad Aaqil <[email protected]>
1 parent 9014d9b commit 87ad002

File tree

6 files changed

+80
-4
lines changed

6 files changed

+80
-4
lines changed

packages/cli/generators/rest-crud/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,19 @@ module.exports = class RestCrudGenerator extends ArtifactGenerator {
367367
debug(`artifactInfo: ${inspect(this.artifactInfo)}`);
368368
debug(`Copying artifact to: ${dest}`);
369369
}
370-
370+
this.artifactInfo.upsert = false;
371+
if (this.options.upsert.includes('*')) {
372+
this.artifactInfo.upsert = true;
373+
} else {
374+
this.options.upsert.forEach(modelName => {
375+
if (
376+
this.artifactInfo.modelName.toLowerCase() ===
377+
modelName.toLowerCase()
378+
) {
379+
this.artifactInfo.upsert = true;
380+
}
381+
});
382+
}
371383
this.copyTemplatedFiles(source, dest, this.artifactInfo);
372384
}
373385

packages/cli/generators/rest-crud/templates/src/model-endpoints/model.rest-config-template.ts.ejs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ const config: ModelCrudRestApiConfig = {
77
dataSource: '<%= dataSourceName %>',
88
basePath: '<%= basePath %>',
99
readonly: <%= readonly %>,
10+
upsert: <%= upsert %>,
1011
};
1112
module.exports = config;

packages/repository/src/connectors/crud.connector.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ export interface CrudConnector extends Connector {
2525
options?: Options,
2626
): Promise<EntityData>;
2727

28+
/**
29+
* Update an existing entity or Create if it does not exist
30+
* @param modelClass - The model class
31+
* @param entity - The entity instance or data
32+
* @param options - Options for the operation
33+
* @returns A promise of the entity created
34+
*/
35+
upsert(
36+
modelClass: Class<Entity>,
37+
entity: EntityData,
38+
options?: Options,
39+
): Promise<EntityData>;
40+
2841
/**
2942
* Create multiple entities
3043
* @param modelClass - The model class

packages/repository/src/repositories/legacy-juggler-bridge.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,12 @@ export class DefaultCrudRepository<
493493
return this.toEntity(model);
494494
}
495495

496+
async upsert(entity: DataObject<T>, options?: Options): Promise<T> {
497+
const data = await this.entityToData(entity, options);
498+
const model = await ensurePromise(this.modelClass.upsert(data, options));
499+
return this.toEntity(model);
500+
}
501+
496502
async createAll(entities: DataObject<T>[], options?: Options): Promise<T[]> {
497503
// perform persist hook
498504
const data = await Promise.all(

packages/repository/src/repositories/repository.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ export interface CrudRepository<
7979
*/
8080
create(dataObject: DataObject<T>, options?: Options): Promise<T>;
8181

82+
/**
83+
* Update an existing entity or Create if it does not exist
84+
* @param modelClass - The model class
85+
* @param entity - The entity instance or data
86+
* @param options - Options for the operation
87+
* @returns A promise of the entity created
88+
*/
89+
upsert(dataObject: DataObject<T>, options?: Options): Promise<T>;
90+
8291
/**
8392
* Create all records
8493
* @param dataObjects - An array of data to be created
@@ -284,6 +293,12 @@ export class CrudRepositoryImpl<T extends Entity, ID>
284293
);
285294
}
286295

296+
upsert(entity: DataObject<T>, options?: Options): Promise<T> {
297+
return this.toModel(
298+
this.connector.upsert(this.entityClass, entity, options),
299+
);
300+
}
301+
287302
createAll(entities: DataObject<T>[], options?: Options): Promise<T[]> {
288303
return this.toModels(
289304
this.connector.createAll!(this.entityClass, entities, options),

packages/rest-crud/src/crud-rest.controller.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export interface CrudRestControllerOptions {
105105
* Whether to generate readonly APIs
106106
*/
107107
readonly?: boolean;
108+
upsert?: boolean;
108109
}
109110

110111
/**
@@ -276,14 +277,42 @@ export function defineCrudRestController<
276277
}
277278
}
278279

280+
@api({basePath: options.basePath, paths: {}})
281+
class CrudRestControllerWithUpsertImpl extends CrudRestControllerImpl {
282+
constructor(
283+
public readonly repository: EntityCrudRepository<T, IdType, Relations>,
284+
) {
285+
super(repository);
286+
}
287+
@post('/upsert', {
288+
...response.model(200, `${modelName} instance created`, modelCtor),
289+
})
290+
async upsert(
291+
@body(modelCtor, {
292+
title: `New${modelName}`,
293+
exclude: modelCtor.getIdProperties() as (keyof T)[],
294+
})
295+
data: Omit<T, IdName>,
296+
): Promise<T> {
297+
return this.repository.upsert(
298+
// FIXME(bajtos) Improve repository API to support this use case
299+
// with no explicit type-casts required
300+
data as DataObject<T>,
301+
);
302+
}
303+
}
304+
279305
const controllerName = modelName + 'Controller';
280306
const defineNamedController = new Function(
281307
'controllerClass',
282308
`return class ${controllerName} extends controllerClass {}`,
283309
);
284-
const controller = defineNamedController(
285-
options.readonly ? ReadonlyRestControllerImpl : CrudRestControllerImpl,
286-
);
310+
let controllerImplementation = ReadonlyRestControllerImpl;
311+
if (options.readonly) controllerImplementation = ReadonlyRestControllerImpl;
312+
if (options.upsert)
313+
controllerImplementation = CrudRestControllerWithUpsertImpl;
314+
315+
const controller = defineNamedController(controllerImplementation);
287316
assert.equal(controller.name, controllerName);
288317
return controller;
289318
}

0 commit comments

Comments
 (0)