"use strict"; const Q = require("q"); const _ = require("underscore"); const BookshelfRepository = require("./BookshelfRepository"); const BookshelfModelWrapper = require("./BookshelfModelWrapper"); const BookshelfDeepOperation = require("./BookshelfDeepOperation"); const { required } = require("./Annotations"); /** * Abstraction on top of BookshelfRepository, Bookshelf and Knex. Provies basic CRUD operations for a specific type. */ class EntityRepository { /** * @param {Class | Function} Entity - Class or constructor function. Entities from this repository will be instances of this Type * @param {BookshelfMapping} Mapping - {@link DBMappingRegistry#compile Compiled Mapping} which describes this type and its relations */ constructor(Entity, Mapping) { this.Entity = Entity; this.Mapping = Mapping; this.wrapper = new BookshelfModelWrapper(Mapping, Entity); this.repository = new BookshelfRepository(Mapping); } /** * Create new instance of Entity. * @param {object} [flatModel] - Simple object representation of Entity, e.g. after deserializing JSON. Properties missing in Mapping are dropped * @returns {Entity} - Instance of Entity, with given properties if any */ newEntity(flatModel) { return this.wrapper.createNew(flatModel); } /** * Fetch one Entity from this Repository * @param {ID} id - Identifier of Entity, specified in Mapping by "identifiedBy" * @param {object} [options] - Bookshelf fetch options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @param {Array<string>} [options.exclude] - Relation names to exclude, deep relations in dot notation. Specify wildcards using "*" * @returns {Promise<Entity|null>} - Returns Promise resolved with entity, or null if not found */ findOne(id, options = null) { return this.repository.findOne(id, options).then((item) => this.wrapper.wrap(item)); } /** * Fetch all Entities, or Entities with given Ids from this Repository * @param {Array<ID>} ids - Identifiers of Entities, specified in Mapping by "identifiedBy" * @param {object} [options] - Bookshelf fetch options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @param {Array<string>} [options.exclude] - Relation names to exclude, deep relations in dot notation. Specify wildcards using "*" * @returns {Promise<Array<Entity>>} - Returns Promise resolved with array of entities, or empty list if not found. * If ids were specified, Entities are sorted statically by given ids */ findAll(ids, options = null) { return this.repository.findAll(ids, options).then((item) => this.wrapper.wrap(item)); } /** * Fetch Entities using a query * @param {Function} q - Callback, used as Bookshelf/Knex where condition. * @param {object} [options] - Bookshelf fetch options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @param {Array<string>} [options.exclude] - Relation names to exclude, deep relations in dot notation. Specify wildcards using "*" * @returns {Promise<Array<Entity>>} - Returns Promise resolved with array of entities, or empty list if not found. */ findAllWhere(q, options = null) { return this.repository.findWhere(q, options).then((items) => { return items.length ? this.wrapper.wrap(items) : []; }); } /** * Fetch Entity using a query * @param {Function} q - Callback, used as Bookshelf/Knex where condition. * @param {object} [options] - Bookshelf fetch options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @param {Array<string>} [options.exclude] - Relation names to exclude, deep relations in dot notation. Specify wildcards using "*" * @returns {Promise<Entity|null>} - Returns Promise resolved with entity, or null if not found */ findWhere(q, options = null) { return this.repository.findWhere(q, options).then((items) => { if (items.length) { return this.wrapper.wrap(items.pop()); } else { return null; } }); } findByConditions(conditions, options = null) { return this.repository.findByConditions(conditions, options).then((items) => { return items.length ? this.wrapper.wrap(items) : []; }); } /** * Save one or multiple Entities to this Repository * @param {Entity | Array<Entity>} entity - Entity or Entities to save * @param {object} [options] - Bookshelf save options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @param {string} [options.method] - Specify "update" or "insert". Defaults to "update", or "insert" if Id is null * @returns {Promise<Entity | Array<Entity>>} - Returns Promise resolved with saved entity, or array of saved entities */ save(entity, options = null) { if (Array.isArray(entity)) { return Q.all(entity.map((entity) => this.save(entity, options))); } return this.executeTransactional(() => { return this.repository.save(this.wrapper.unwrap(entity), options).then((item) => this.wrapper.wrap(item)); }, options).tap((entity) => { this.afterSave(entity[this.Mapping.identifiedBy]); }); } /** * Hook, is called once after every successful save operation * @param {ID} id - Identifier of saved Entity */ afterSave() { } /** * Remove one or multiple Entities from this Repository * @param {Entity | Array<Entity> | ID | Array<ID>} entity - Entity or Entities, Id or Ids to remove * @param {object} [options] - Bookshelf save options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @returns {Promise<Void>} - Returns Promise resolved after removal */ remove(entity, options = null) { if (Array.isArray(entity)) { return Q.all(entity.map((entity) => this.remove(entity, options))); } else if (entity instanceof this.Entity) { entity = this.wrapper.unwrap(entity); } return this.executeTransactional(() => { return this.repository.remove(entity, options); }, options).tap(() => { const id = _.isObject(entity) ? entity[this.Mapping.identifiedBy] : +entity; if (id) { this.afterRemove(id); } }); } /** * Hook, is called once after every successful remove operation * @param {ID} id - Identifier of removed Entity */ afterRemove() { } /** * Execute an operation in a running or new transaction * @param {Function} operation - Callback to execute in a transaction * @param {object} options - Bookshelf options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @returns {Promise<*>} - Promise resolved with result of operation. If operation fails, Promise is rejected */ executeTransactional(operation, options = null) { if (options && options.transactional && !options.transacting) { return this.Mapping.startTransaction((t) => { options.transacting = t; return Q.try(operation).then(t.commit).catch(t.rollback); }); } else { return Q.try(operation); } } /** * Add an already started transaction to given query. If not yet started, no transaction will be added * @param {Transaction} [options.transacting] - Add Transaction object to given query * @param {KnexQuery} query - Add transaction to query, if one was started. * @param {object} options - Bookshelf options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @returns {KnexQuery} query - Returns KnexQuery for chaining */ addTransactionToQuery(query, options = required("options")) { return BookshelfDeepOperation.addTransactionToQuery(query, options); } /** * Returns whether an Entity with the given Identifier exists. * @param {ID} id - Identifier * @param {object} [options] - Bookshelf save options * @param {Transaction} [options.transacting] - Run in given transaction * @param {boolean} [options.transactional] - Run in a transaction, start new one if not already transacting * @returns {Promise<boolean>} - Returns Promise resolved with flag indicating whether an Entity with the given Identifier exists */ exists(id, options = null) { if (!id) { return Q.when(false); } options = _.extend({}, options, { exclude: ["*"] }); return this.findOne(id, options).then((entity) => !!entity); } } module.exports = EntityRepository;