@loopback/repository
Version:
Define and implement a common set of interfaces for interacting with databases
442 lines (400 loc) • 13.7 kB
text/typescript
// 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);
}
}