UNPKG

@loopback/repository

Version:

Define and implement a common set of interfaces for interacting with databases

442 lines (400 loc) 13.7 kB
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. // Node module: @loopback/repository // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import {Filter, FilterExcludingWhere, Where} from '@loopback/filter'; import { AnyObject, Command, Count, DataObject, NamedParameters, Options, PositionalParameters, } from '../common-types'; import {CrudConnector} from '../connectors'; import {DataSource} from '../datasource'; import {EntityNotFoundError} from '../errors'; import {Entity, Model, ValueObject} from '../model'; import {InclusionResolver} from '../relations/relation.types'; import {IsolationLevel, Transaction} from '../transaction'; /* eslint-disable @typescript-eslint/no-unused-vars */ export interface Repository<T extends Model> {} export interface ExecutableRepository<T extends Model> extends Repository<T> { /** * Execute a query with the given parameter object or an array of parameters * @param command - The query string or command object * @param parameters - The object with name/value pairs or an array of parameter * values * @param options - Options */ execute( command: Command, parameters: NamedParameters | PositionalParameters, options?: Options, ): Promise<AnyObject>; } /** * A type for CRUD repositories that are backed by IDs and support * Transactions */ export type TransactionalEntityRepository< T extends Entity, ID, Relations extends object = {}, > = TransactionalRepository<T> & EntityCrudRepository<T, ID>; /** * Repository Interface for Repositories that support Transactions * * @typeParam T Generic type for the Entity */ export interface TransactionalRepository<T extends Entity> extends Repository<T> { /** * Begin a new Transaction * @param options - Options for the operations * @returns Promise<Transaction> Promise that resolves to a new Transaction * object */ beginTransaction(options?: IsolationLevel | Options): Promise<Transaction>; } /** * Basic CRUD operations for ValueObject and Entity. No ID is required. */ export interface CrudRepository< T extends ValueObject | Entity, Relations extends object = {}, > extends Repository<T> { /** * Create a new record * @param dataObject - The data to be created * @param options - Options for the operations * @returns A promise of record created */ create(dataObject: DataObject<T>, options?: Options): Promise<T>; /** * Create all records * @param dataObjects - An array of data to be created * @param options - Options for the operations * @returns A promise of an array of records created */ createAll(dataObjects: DataObject<T>[], options?: Options): Promise<T[]>; /** * Find matching records * @param filter - Query filter * @param options - Options for the operations * @returns A promise of an array of records found */ find(filter?: Filter<T>, options?: Options): Promise<(T & Relations)[]>; /** * Updating matching records with attributes from the data object * @param dataObject - The data to be updated * @param where - Matching criteria * @param options - Options for the operations * @returns A promise of number of records updated */ updateAll( dataObject: DataObject<T>, where?: Where<T>, options?: Options, ): Promise<Count>; /** * Delete matching records * @param where - Matching criteria * @param options - Options for the operations * @returns A promise of number of records deleted */ deleteAll(where?: Where<T>, options?: Options): Promise<Count>; /** * Count matching records * @param where - Matching criteria * @param options - Options for the operations * @returns A promise of number of records matched */ count(where?: Where<T>, options?: Options): Promise<Count>; } /** * Base interface for a repository of entities */ export interface EntityRepository<T extends Entity, ID> extends ExecutableRepository<T> {} /** * CRUD operations for a repository of entities */ export interface EntityCrudRepository< T extends Entity, ID, Relations extends object = {}, > extends EntityRepository<T, ID>, CrudRepository<T, Relations> { // entityClass should have type "typeof T", but that's not supported by TSC entityClass: typeof Entity & {prototype: T}; inclusionResolvers: Map<string, InclusionResolver<T, Entity>>; /** * Save an entity. If no id is present, create a new entity * @param entity - Entity to be saved * @param options - Options for the operations * @returns A promise that will be resolve if the operation succeeded or will * be rejected if the entity was not found. */ save(entity: DataObject<T>, options?: Options): Promise<T>; /** * Update an entity * @param entity - Entity to be updated * @param options - Options for the operations * @returns A promise that will be resolve if the operation succeeded or will * be rejected if the entity was not found. */ update(entity: DataObject<T>, options?: Options): Promise<void>; /** * Delete an entity * @param entity - Entity to be deleted * @param options - Options for the operations * @returns A promise that will be resolve if the operation succeeded or will * be rejected if the entity was not found. */ delete(entity: DataObject<T>, options?: Options): Promise<void>; /** * Find an entity by id, return a rejected promise if not found. * * @remarks * * The rationale behind findById is to find an instance by its primary key * (id). No other search criteria than id should be used. If a client wants * to use a `where` clause beyond id, use `find` or `findOne` instead. * * @param id - Value for the entity id * @param filter - Additional query options. E.g. `filter.include` configures * which related models to fetch as part of the database query (or queries). * @param options - Options for the operations * @returns A promise of an entity found for the id */ findById( id: ID, filter?: FilterExcludingWhere<T>, options?: Options, ): Promise<T & Relations>; /** * Update an entity by id with property/value pairs in the data object * @param id - Value for the entity id * @param data - Data attributes to be updated * @param options - Options for the operations * @returns A promise that will be resolve if the operation succeeded or will * be rejected if the entity was not found. */ updateById(id: ID, data: DataObject<T>, options?: Options): Promise<void>; /** * Replace an entity by id * @param id - Value for the entity id * @param data - Data attributes to be replaced * @param options - Options for the operations * @returns A promise that will be resolve if the operation succeeded or will * be rejected if the entity was not found. */ replaceById(id: ID, data: DataObject<T>, options?: Options): Promise<void>; /** * Delete an entity by id * @param id - Value for the entity id * @param options - Options for the operations * @returns A promise that will be resolve if the operation succeeded or will * be rejected if the entity was not found. */ deleteById(id: ID, options?: Options): Promise<void>; /** * Check if an entity exists for the given id * @param id - Value for the entity id * @param options - Options for the operations * @returns Promise<true> if an entity exists for the id, otherwise * Promise<false> */ exists(id: ID, options?: Options): Promise<boolean>; } /** * Repository implementation * * @example * * User can import `CrudRepositoryImpl` and call its functions like: * `CrudRepositoryImpl.find(somefilters, someoptions)` * * Or extend class `CrudRepositoryImpl` and override its functions: * ```ts * export class TestRepository extends CrudRepositoryImpl<Test> { * constructor(dataSource: DataSource, model: Test) { * super(dataSource, Customer); * } * * // Override `deleteAll` to disable the operation * deleteAll(where?: Where, options?: Options) { * return Promise.reject(new Error('deleteAll is disabled')); * } * } * ``` */ export class CrudRepositoryImpl<T extends Entity, ID> implements EntityCrudRepository<T, ID> { private connector: CrudConnector; public readonly inclusionResolvers: Map< string, InclusionResolver<T, Entity> > = new Map(); constructor( public dataSource: DataSource, // model should have type "typeof T", but that's not supported by TSC public entityClass: typeof Entity & {prototype: T}, ) { this.connector = dataSource.connector as CrudConnector; } private toModels(data: Promise<DataObject<Entity>[]>): Promise<T[]> { return data.then(items => items.map(i => new this.entityClass(i) as T)); } private toModel(data: Promise<DataObject<Entity>>): Promise<T> { return data.then(d => new this.entityClass(d) as T); } create(entity: DataObject<T>, options?: Options): Promise<T> { return this.toModel( this.connector.create(this.entityClass, entity, options), ); } createAll(entities: DataObject<T>[], options?: Options): Promise<T[]> { return this.toModels( this.connector.createAll!(this.entityClass, entities, options), ); } async save(entity: DataObject<T>, options?: Options): Promise<T> { if (typeof this.connector.save === 'function') { return this.toModel( this.connector.save(this.entityClass, entity, options), ); } else { const id = this.entityClass.getIdOf(entity); if (id != null) { await this.replaceById(id, entity, options); return this.toModel(Promise.resolve(entity)); } else { return this.create(entity, options); } } } find(filter?: Filter<T>, options?: Options): Promise<T[]> { return this.toModels( this.connector.find(this.entityClass, filter, options), ); } async findById( id: ID, filter?: FilterExcludingWhere<T>, options?: Options, ): Promise<T> { if (typeof this.connector.findById === 'function') { return this.toModel( this.connector.findById(this.entityClass, id, options), ); } const where = this.entityClass.buildWhereForId(id); const entities = await this.toModels( this.connector.find(this.entityClass, {where: where}, options), ); if (!entities.length) { throw new EntityNotFoundError(this.entityClass, id); } return entities[0]; } update(entity: DataObject<T>, options?: Options): Promise<void> { return this.updateById(this.entityClass.getIdOf(entity), entity, options); } delete(entity: DataObject<T>, options?: Options): Promise<void> { return this.deleteById(this.entityClass.getIdOf(entity), options); } updateAll( data: DataObject<T>, where?: Where<T>, options?: Options, ): Promise<Count> { return this.connector.updateAll(this.entityClass, data, where, options); } async updateById( id: ID, data: DataObject<T>, options?: Options, ): Promise<void> { let success: boolean; if (typeof this.connector.updateById === 'function') { success = await this.connector.updateById( this.entityClass, id, data, options, ); } else { const where = this.entityClass.buildWhereForId(id); const result = await this.updateAll(data, where, options); success = result.count > 0; } if (!success) { throw new EntityNotFoundError(this.entityClass, id); } } async replaceById( id: ID, data: DataObject<T>, options?: Options, ): Promise<void> { let success: boolean; if (typeof this.connector.replaceById === 'function') { success = await this.connector.replaceById( this.entityClass, id, data, options, ); } else { // FIXME: populate inst with all properties const inst = data; const where = this.entityClass.buildWhereForId(id); const result = await this.updateAll(data, where, options); success = result.count > 0; } if (!success) { throw new EntityNotFoundError(this.entityClass, id); } } deleteAll(where?: Where<T>, options?: Options): Promise<Count> { return this.connector.deleteAll(this.entityClass, where, options); } async deleteById(id: ID, options?: Options): Promise<void> { let success: boolean; if (typeof this.connector.deleteById === 'function') { success = await this.connector.deleteById(this.entityClass, id, options); } else { const where = this.entityClass.buildWhereForId(id); const result = await this.deleteAll(where, options); success = result.count > 0; } if (!success) { throw new EntityNotFoundError(this.entityClass, id); } } count(where?: Where<T>, options?: Options): Promise<Count> { return this.connector.count(this.entityClass, where, options); } exists(id: ID, options?: Options): Promise<boolean> { if (typeof this.connector.exists === 'function') { return this.connector.exists(this.entityClass, id, options); } else { const where = this.entityClass.buildWhereForId(id); return this.count(where, options).then(result => result.count > 0); } } execute( command: Command, parameters: NamedParameters | PositionalParameters, options?: Options, ): Promise<AnyObject> { if (typeof this.connector.execute !== 'function') { throw new Error('Not implemented'); } return this.connector.execute(command, parameters, options); } }