@loopback/repository
Version:
Define and implement a common set of interfaces for interacting with databases
474 lines • 19.8 kB
JavaScript
"use strict";
// 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
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultTransactionalRepository = exports.DefaultCrudRepository = exports.ensurePromise = exports.bindModel = exports.juggler = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const loopback_datasource_juggler_1 = tslib_1.__importDefault(require("loopback-datasource-juggler"));
const errors_1 = require("../errors");
const model_1 = require("../model");
const relations_1 = require("../relations");
const type_resolver_1 = require("../type-resolver");
var juggler;
(function (juggler) {
/* eslint-disable @typescript-eslint/no-unused-vars */
juggler.DataSource = loopback_datasource_juggler_1.default.DataSource;
juggler.ModelBase = loopback_datasource_juggler_1.default.ModelBase;
juggler.PersistedModel = loopback_datasource_juggler_1.default.PersistedModel;
juggler.KeyValueModel = loopback_datasource_juggler_1.default.KeyValueModel;
// eslint-disable-next-line @typescript-eslint/no-shadow
juggler.IsolationLevel = loopback_datasource_juggler_1.default.IsolationLevel;
})(juggler = exports.juggler || (exports.juggler = {}));
function isModelClass(propertyType) {
return (!(0, type_resolver_1.isTypeResolver)(propertyType) &&
typeof propertyType === 'function' &&
typeof propertyType.definition === 'object' &&
propertyType.toString().startsWith('class '));
}
/**
* This is a bridge to the legacy DAO class. The function mixes DAO methods
* into a model class and attach it to a given data source
* @param modelClass - Model class
* @param ds - Data source
* @returns {} The new model class with DAO (CRUD) operations
*/
function bindModel(modelClass, ds) {
const BoundModelClass = class extends modelClass {
};
BoundModelClass.attachTo(ds);
return BoundModelClass;
}
exports.bindModel = bindModel;
/**
* Ensure the value is a promise
* @param p - Promise or void
*/
function ensurePromise(p) {
if (p && p instanceof Promise) {
return p;
}
else {
return Promise.reject(new Error('The value should be a Promise: ' + p));
}
}
exports.ensurePromise = ensurePromise;
/**
* Default implementation of CRUD repository using legacy juggler model
* and data source
*/
class DefaultCrudRepository {
/**
* Constructor of DefaultCrudRepository
* @param entityClass - LoopBack 4 entity class
* @param dataSource - Legacy juggler data source
*/
constructor(
// entityClass should have type "typeof T", but that's not supported by TSC
entityClass, dataSource) {
this.entityClass = entityClass;
this.dataSource = dataSource;
this.inclusionResolvers = new Map();
const definition = entityClass.definition;
(0, assert_1.default)(!!definition, `Entity ${entityClass.name} must have valid model definition.`);
(0, assert_1.default)(definition.idProperties().length > 0, `Entity ${entityClass.name} must have at least one id/pk property.`);
this.modelClass = this.ensurePersistedModel(entityClass);
}
// Create an internal legacy Model attached to the datasource
ensurePersistedModel(entityClass) {
const definition = entityClass.definition;
(0, assert_1.default)(!!definition, `Entity ${entityClass.name} must have valid model definition.`);
const dataSource = this.dataSource;
const model = dataSource.getModel(definition.name);
if (model) {
// The backing persisted model has been already defined.
return model;
}
return this.definePersistedModel(entityClass);
}
/**
* Creates a legacy persisted model class, attaches it to the datasource and
* returns it. This method can be overridden in sub-classes to acess methods
* and properties in the generated model class.
* @param entityClass - LB4 Entity constructor
*/
definePersistedModel(entityClass) {
const dataSource = this.dataSource;
const definition = entityClass.definition;
// To handle circular reference back to the same model,
// we create a placeholder model that will be replaced by real one later
dataSource.getModel(definition.name, true /* forceCreate */);
// We need to convert property definitions from PropertyDefinition
// to plain data object because of a juggler limitation
const properties = {};
// We need to convert PropertyDefinition into the definition that
// the juggler understands
Object.entries(definition.properties).forEach(([key, value]) => {
// always clone value so that we do not modify the original model definition
// ensures that model definitions can be reused with multiple datasources
if (value.type === 'array' || value.type === Array) {
value = Object.assign({}, value, {
type: [value.itemType && this.resolvePropertyType(value.itemType)],
});
delete value.itemType;
}
else {
value = Object.assign({}, value, {
type: this.resolvePropertyType(value.type),
});
}
properties[key] = Object.assign({}, value);
});
const modelClass = dataSource.createModel(definition.name, properties, Object.assign(
// settings that users can override
{ strict: true },
// user-defined settings
definition.settings,
// settings enforced by the framework
{ strictDelete: false }));
modelClass.attachTo(dataSource);
return modelClass;
}
resolvePropertyType(type) {
const resolved = (0, type_resolver_1.resolveType)(type);
return isModelClass(resolved)
? this.ensurePersistedModel(resolved)
: resolved;
}
/**
* @deprecated
* Function to create a constrained relation repository factory
*
* Use `this.createHasManyRepositoryFactoryFor()` instead
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
*/
_createHasManyRepositoryFactoryFor(relationName, targetRepositoryGetter) {
return this.createHasManyRepositoryFactoryFor(relationName, targetRepositoryGetter);
}
/**
* Function to create a constrained relation repository factory
*
* @example
* ```ts
* class CustomerRepository extends DefaultCrudRepository<
* Customer,
* typeof Customer.prototype.id,
* CustomerRelations
* > {
* public readonly orders: HasManyRepositoryFactory<Order, typeof Customer.prototype.id>;
*
* constructor(
* protected db: juggler.DataSource,
* orderRepository: EntityCrudRepository<Order, typeof Order.prototype.id>,
* ) {
* super(Customer, db);
* this.orders = this._createHasManyRepositoryFactoryFor(
* 'orders',
* orderRepository,
* );
* }
* }
* ```
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
*/
createHasManyRepositoryFactoryFor(relationName, targetRepositoryGetter) {
const meta = this.entityClass.definition.relations[relationName];
return (0, relations_1.createHasManyRepositoryFactory)(meta, targetRepositoryGetter);
}
/**
* Function to create a constrained hasManyThrough relation repository factory
*
* @example
* ```ts
* class CustomerRepository extends DefaultCrudRepository<
* Customer,
* typeof Customer.prototype.id,
* CustomerRelations
* > {
* public readonly cartItems: HasManyRepositoryFactory<CartItem, typeof Customer.prototype.id>;
*
* constructor(
* protected db: juggler.DataSource,
* cartItemRepository: EntityCrudRepository<CartItem, typeof, CartItem.prototype.id>,
* throughRepository: EntityCrudRepository<Through, typeof Through.prototype.id>,
* ) {
* super(Customer, db);
* this.cartItems = this.createHasManyThroughRepositoryFactoryFor(
* 'cartItems',
* cartItemRepository,
* );
* }
* }
* ```
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
* @param throughRepo - Through repository instance
*/
createHasManyThroughRepositoryFactoryFor(relationName, targetRepositoryGetter, throughRepositoryGetter) {
const meta = this.entityClass.definition.relations[relationName];
return (0, relations_1.createHasManyThroughRepositoryFactory)(meta, targetRepositoryGetter, throughRepositoryGetter);
}
/**
* @deprecated
* Function to create a belongs to accessor
*
* Use `this.createBelongsToAccessorFor()` instead
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
*/
_createBelongsToAccessorFor(relationName, targetRepositoryGetter) {
return this.createBelongsToAccessorFor(relationName, targetRepositoryGetter);
}
/**
* Function to create a belongs to accessor
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
*/
createBelongsToAccessorFor(relationName, targetRepositoryGetter) {
const meta = this.entityClass.definition.relations[relationName];
return (0, relations_1.createBelongsToAccessor)(meta, targetRepositoryGetter, this);
}
/**
* @deprecated
* Function to create a constrained hasOne relation repository factory
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
*/
_createHasOneRepositoryFactoryFor(relationName, targetRepositoryGetter) {
return this.createHasOneRepositoryFactoryFor(relationName, targetRepositoryGetter);
}
/**
* Function to create a constrained hasOne relation repository factory
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
*/
createHasOneRepositoryFactoryFor(relationName, targetRepositoryGetter) {
const meta = this.entityClass.definition.relations[relationName];
return (0, relations_1.createHasOneRepositoryFactory)(meta, targetRepositoryGetter);
}
/**
* @deprecated
* Function to create a references many accessor
*
* Use `this.createReferencesManyAccessorFor()` instead
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
*/
_createReferencesManyAccessorFor(relationName, targetRepoGetter) {
return this.createReferencesManyAccessorFor(relationName, targetRepoGetter);
}
/**
* Function to create a references many accessor
*
* @param relationName - Name of the relation defined on the source model
* @param targetRepo - Target repository instance
*/
createReferencesManyAccessorFor(relationName, targetRepoGetter) {
const meta = this.entityClass.definition.relations[relationName];
return (0, relations_1.createReferencesManyAccessor)(meta, targetRepoGetter, this);
}
async create(entity, options) {
// perform persist hook
const data = this.entityToData(entity, options);
const model = await ensurePromise(this.modelClass.create(data, options));
return this.toEntity(model);
}
async createAll(entities, options) {
// perform persist hook
const data = entities.map(e => this.entityToData(e, options));
const models = await ensurePromise(this.modelClass.createAll(data, options));
return this.toEntities(models);
}
async save(entity, options) {
const id = this.entityClass.getIdOf(entity);
if (id == null) {
return this.create(entity, options);
}
else {
await this.replaceById(id, entity, options);
return new this.entityClass(entity.toObject());
}
}
async find(filter, options) {
const include = filter === null || filter === void 0 ? void 0 : filter.include;
const models = await ensurePromise(this.modelClass.find(this.normalizeFilter(filter), options));
const entities = this.toEntities(models);
return this.includeRelatedModels(entities, include, options);
}
async findOne(filter, options) {
const model = await ensurePromise(this.modelClass.findOne(this.normalizeFilter(filter), options));
if (!model)
return null;
const entity = this.toEntity(model);
const include = filter === null || filter === void 0 ? void 0 : filter.include;
const resolved = await this.includeRelatedModels([entity], include, options);
return resolved[0];
}
async findById(id, filter, options) {
const include = filter === null || filter === void 0 ? void 0 : filter.include;
const model = await ensurePromise(this.modelClass.findById(id, this.normalizeFilter(filter), options));
if (!model) {
throw new errors_1.EntityNotFoundError(this.entityClass, id);
}
const entity = this.toEntity(model);
const resolved = await this.includeRelatedModels([entity], include, options);
return resolved[0];
}
update(entity, options) {
return this.updateById(entity.getId(), entity, options);
}
async delete(entity, options) {
// perform persist hook
this.entityToData(entity, options);
return this.deleteById(entity.getId(), options);
}
async updateAll(data, where, options) {
where = where !== null && where !== void 0 ? where : {};
const persistedData = this.entityToData(data, options);
const result = await ensurePromise(this.modelClass.updateAll(where, persistedData, options));
return { count: result.count };
}
async updateById(id, data, options) {
if (id === undefined) {
throw new Error('Invalid Argument: id cannot be undefined');
}
const idProp = this.modelClass.definition.idName();
const where = {};
where[idProp] = id;
const result = await this.updateAll(data, where, options);
if (result.count === 0) {
throw new errors_1.EntityNotFoundError(this.entityClass, id);
}
}
async replaceById(id, data, options) {
try {
const payload = this.entityToData(data, options);
await ensurePromise(this.modelClass.replaceById(id, payload, options));
}
catch (err) {
if (err.statusCode === 404) {
throw new errors_1.EntityNotFoundError(this.entityClass, id);
}
throw err;
}
}
async deleteAll(where, options) {
const result = await ensurePromise(this.modelClass.deleteAll(where, options));
return { count: result.count };
}
async deleteById(id, options) {
const result = await ensurePromise(this.modelClass.deleteById(id, options));
if (result.count === 0) {
throw new errors_1.EntityNotFoundError(this.entityClass, id);
}
}
async count(where, options) {
const result = await ensurePromise(this.modelClass.count(where, options));
return { count: result };
}
exists(id, options) {
return ensurePromise(this.modelClass.exists(id, options));
}
async execute(...args) {
return ensurePromise(this.dataSource.execute(...args));
}
toEntity(model) {
return new this.entityClass(model.toObject());
}
toEntities(models) {
return models.map(m => this.toEntity(m));
}
/**
* Register an inclusion resolver for the related model name.
*
* @param relationName - Name of the relation defined on the source model
* @param resolver - Resolver function for getting related model entities
*/
registerInclusionResolver(relationName, resolver) {
this.inclusionResolvers.set(relationName, resolver);
}
/**
* Returns model instances that include related models of this repository
* that have a registered resolver.
*
* @param entities - An array of entity instances or data
* @param include -Inclusion filter
* @param options - Options for the operations
*/
async includeRelatedModels(entities, include, options) {
return (0, relations_1.includeRelatedModels)(this, entities, include, options);
}
/**
* This function works as a persist hook.
* It converts an entity from the CRUD operations' caller
* to a persistable data that can will be stored in the
* back-end database.
*
* User can extend `DefaultCrudRepository` then override this
* function to execute custom persist hook.
* @param entity The entity passed from CRUD operations' caller.
* @param options
*/
entityToData(entity, options = {}) {
return this.ensurePersistable(entity, options);
}
/** Converts an entity object to a JSON object to check if it contains navigational property.
* Throws an error if `entity` contains navigational property.
*
* @param entity The entity passed from CRUD operations' caller.
* @param options
*/
ensurePersistable(entity, options = {}) {
// FIXME(bajtos) Ideally, we should call toJSON() to convert R to data object
// Unfortunately that breaks replaceById for MongoDB connector, where we
// would call replaceId with id *argument* set to ObjectID value but
// id *property* set to string value.
/*
const data: AnyObject =
typeof entity.toJSON === 'function' ? entity.toJSON() : {...entity};
*/
const data = new this.entityClass(entity);
(0, model_1.rejectNavigationalPropertiesInData)(this.entityClass, data);
return data;
}
/**
* Removes juggler's "include" filter as it does not apply to LoopBack 4
* relations.
*
* @param filter - Query filter
*/
normalizeFilter(filter) {
if (!filter)
return undefined;
return { ...filter, include: undefined };
}
}
exports.DefaultCrudRepository = DefaultCrudRepository;
/**
* Default implementation of CRUD repository using legacy juggler model
* and data source with beginTransaction() method for connectors which
* support Transactions
*/
class DefaultTransactionalRepository extends DefaultCrudRepository {
async beginTransaction(options) {
const dsOptions = options !== null && options !== void 0 ? options : {};
// juggler.Transaction still has the Promise/Callback variants of the
// Transaction methods
// so we need it cast it back
return (await this.dataSource.beginTransaction(dsOptions));
}
}
exports.DefaultTransactionalRepository = DefaultTransactionalRepository;
//# sourceMappingURL=legacy-juggler-bridge.js.map