Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion packages/cli/generators/rest-crud/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ const config: ModelCrudRestApiConfig = {
dataSource: '<%= dataSourceName %>',
basePath: '<%= basePath %>',
readonly: <%= readonly %>,
upsert: <%= upsert %>,
};
module.exports = config;
13 changes: 13 additions & 0 deletions packages/repository/src/connectors/crud.connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export interface CrudConnector extends Connector {
options?: Options,
): Promise<EntityData>;

/**
* 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>,
entity: EntityData,
options?: Options,
): Promise<EntityData>;

/**
* Create multiple entities
* @param modelClass - The model class
Expand Down
6 changes: 6 additions & 0 deletions packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,12 @@ export class DefaultCrudRepository<
return this.toEntity(model);
}

async upsert(entity: DataObject<T>, options?: Options): Promise<T> {
const data = await this.entityToData(entity, options);
const model = await ensurePromise(this.modelClass.upsert(data, options));
return this.toEntity(model);
}

async createAll(entities: DataObject<T>[], options?: Options): Promise<T[]> {
// perform persist hook
const data = await Promise.all(
Expand Down
15 changes: 15 additions & 0 deletions packages/repository/src/repositories/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ export interface CrudRepository<
*/
create(dataObject: DataObject<T>, options?: Options): Promise<T>;

/**
* 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<T>, options?: Options): Promise<T>;

/**
* Create all records
* @param dataObjects - An array of data to be created
Expand Down Expand Up @@ -284,6 +293,12 @@ export class CrudRepositoryImpl<T extends Entity, ID>
);
}

upsert(entity: DataObject<T>, options?: Options): Promise<T> {
return this.toModel(
this.connector.upsert(this.entityClass, entity, options),
);
}

createAll(entities: DataObject<T>[], options?: Options): Promise<T[]> {
return this.toModels(
this.connector.createAll!(this.entityClass, entities, options),
Expand Down
35 changes: 32 additions & 3 deletions packages/rest-crud/src/crud-rest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface CrudRestControllerOptions {
* Whether to generate readonly APIs
*/
readonly?: boolean;
upsert?: boolean;
}

/**
Expand Down Expand Up @@ -276,14 +277,42 @@ export function defineCrudRestController<
}
}

@api({basePath: options.basePath, paths: {}})
class CrudRestControllerWithUpsertImpl extends CrudRestControllerImpl {
constructor(
public readonly repository: EntityCrudRepository<T, IdType, Relations>,
) {
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<T, IdName>,
): Promise<T> {
return this.repository.upsert(
// FIXME(bajtos) Improve repository API to support this use case
// with no explicit type-casts required
data as DataObject<T>,
);
}
}

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;
}
Expand Down
Loading