UNPKG

objection

Version:
952 lines (761 loc) 24.1 kB
'use strict'; const { clone } = require('./modelClone'); const { bindKnex } = require('./modelBindKnex'); const { validate } = require('./modelValidate'); const { isMsSql } = require('../utils/knexUtils'); const { deprecate } = require('../utils/deprecate'); const { omit, pick } = require('./modelFilter'); const { visitModels } = require('./modelVisitor'); const { hasId, getSetId } = require('./modelId'); const { map: promiseMap } = require('../utils/promiseUtils'); const { toJson, toDatabaseJson } = require('./modelToJson'); const { values, propKey, hasProps } = require('./modelValues'); const { defineNonEnumerableProperty } = require('./modelUtils'); const { parseRelationsIntoModelInstances } = require('./modelParseRelations'); const { fetchTableMetadata, tableMetadata } = require('./modelTableMetadata'); const { asArray, isFunction, isString, asSingle } = require('../utils/objectUtils'); const { setJson, setFast, setRelated, appendRelated, setDatabaseJson } = require('./modelSet'); const { getJsonAttributes, parseJsonAttributes, formatJsonAttributes, } = require('./modelJsonAttributes'); const { columnNameToPropertyName, propertyNameToColumnName } = require('./modelColPropMap'); const { raw } = require('../queryBuilder/RawBuilder'); const { ref } = require('../queryBuilder/ReferenceBuilder'); const { fn } = require('../queryBuilder/FunctionBuilder'); const { AjvValidator } = require('./AjvValidator'); const { QueryBuilder } = require('../queryBuilder/QueryBuilder'); const { NotFoundError } = require('./NotFoundError'); const { ValidationError } = require('./ValidationError'); const { ModifierNotFoundError } = require('./ModifierNotFoundError'); const { RelationProperty } = require('../relations/RelationProperty'); const { RelationOwner } = require('../relations/RelationOwner'); const { HasOneRelation } = require('../relations/hasOne/HasOneRelation'); const { HasManyRelation } = require('../relations/hasMany/HasManyRelation'); const { ManyToManyRelation } = require('../relations/manyToMany/ManyToManyRelation'); const { BelongsToOneRelation } = require('../relations/belongsToOne/BelongsToOneRelation'); const { HasOneThroughRelation } = require('../relations/hasOneThrough/HasOneThroughRelation'); const { InstanceFindOperation } = require('../queryBuilder/operations/InstanceFindOperation'); const { InstanceInsertOperation } = require('../queryBuilder/operations/InstanceInsertOperation'); const { InstanceUpdateOperation } = require('../queryBuilder/operations/InstanceUpdateOperation'); const { InstanceDeleteOperation } = require('../queryBuilder/operations/InstanceDeleteOperation'); class Model { $id(maybeId) { return getSetId(this, maybeId); } $hasId() { return hasId(this); } $hasProps(props) { return hasProps(this, props); } $query(trx) { return instanceQuery({ instance: this, transaction: trx, }); } $relatedQuery(relationName, trx) { return relatedQuery({ modelClass: this.constructor, relationName, transaction: trx, alwaysReturnArray: false, }).for(this); } $loadRelated(relationExpression, modifiers, transaction) { deprecate('$loadRelated is deprected and will be removed in 3.0. Use $fetchGraph instead'); return this.$fetchGraph(relationExpression, { transaction }).modifiers(modifiers); } $fetchGraph(relationExpression, options) { return this.constructor.fetchGraph(this, relationExpression, options); } $beforeValidate(jsonSchema, json, options) { /* istanbul ignore next */ return jsonSchema; } $validate(json, options) { return validate(this, json, options); } $afterValidate(json, options) { // Do nothing by default. } $parseDatabaseJson(json) { const columnNameMappers = this.constructor.getColumnNameMappers(); if (columnNameMappers) { json = columnNameMappers.parse(json); } return parseJsonAttributes(json, this.constructor); } $formatDatabaseJson(json) { const columnNameMappers = this.constructor.getColumnNameMappers(); json = formatJsonAttributes(json, this.constructor); if (columnNameMappers) { json = columnNameMappers.format(json); } return json; } $parseJson(json, options) { return json; } $formatJson(json) { return json; } $setJson(json, options) { return setJson(this, json, options); } $setDatabaseJson(json) { return setDatabaseJson(this, json); } $set(obj) { return setFast(this, obj); } $setRelated(relation, models) { return setRelated(this, relation, models); } $appendRelated(relation, models) { return appendRelated(this, relation, models); } $toJson(opt) { return toJson(this, opt); } toJSON(opt) { return this.$toJson(opt); } $toDatabaseJson(builder) { return toDatabaseJson(this, builder); } $beforeInsert(queryContext) { // Do nothing by default. } $afterInsert(queryContext) { // Do nothing by default. } $beforeUpdate(opt, queryContext) { // Do nothing by default. } $afterUpdate(opt, queryContext) { // Do nothing by default. } // TODO: Deprecate & remove in 3.0 $afterGet(queryContext) { // Do nothing by default. } $afterFind(queryContext) { // Do nothing by default. } $beforeDelete(queryContext) { // Do nothing by default. } $afterDelete(queryContext) { // Do nothing by default. } $omit(...args) { deprecate('Model#$omit is deprected and will be removed in 3.0.'); return omit(this, args); } $pick(...args) { deprecate('Model#$pick is deprected and will be removed in 3.0.'); return pick(this, args); } $values(props) { return values(this, props); } $propKey(props) { return propKey(this, props); } $idKey() { return this.$propKey(this.constructor.getIdPropertyArray()); } $clone(opt) { return clone(this, !!(opt && opt.shallow), false); } $traverse(filterConstructor, callback) { if (callback === undefined) { callback = filterConstructor; filterConstructor = null; } this.constructor.traverse(filterConstructor, this, callback); return this; } $traverseAsync(filterConstructor, callback) { if (callback === undefined) { callback = filterConstructor; filterConstructor = null; } return this.constructor.traverseAsync(filterConstructor, this, callback); } $omitFromJson(props) { if (arguments.length === 0) { return this.$$omitFromJson; } else { if (!this.hasOwnProperty('$$omitFromJson')) { defineNonEnumerableProperty(this, '$$omitFromJson', props); } else { this.$$omitFromJson = this.$$omitFromJson.concat(props); } } } $omitFromDatabaseJson(props) { if (arguments.length === 0) { return this.$$omitFromDatabaseJson; } else { if (!this.hasOwnProperty('$$omitFromDatabaseJson')) { defineNonEnumerableProperty(this, '$$omitFromDatabaseJson', props); } else { this.$$omitFromDatabaseJson = this.$$omitFromDatabaseJson.concat(props); } } } $knex() { return this.constructor.knex(); } $transaction(...args) { return this.constructor.transaction(...args); } get $ref() { return this.constructor.ref; } static get objectionModelClass() { return Model; } static fromJson(json, options) { const model = new this(); model.$setJson(json || {}, options); return model; } static fromDatabaseJson(json) { const model = new this(); model.$setDatabaseJson(json || {}); return model; } static onCreateQuery(builder) { // Do nothing by default. } static beforeFind(args) { // Do nothing by default. } static afterFind(args) { // Do nothing by default. } static beforeInsert(args) { // Do nothing by default. } static afterInsert(args) { // Do nothing by default. } static beforeUpdate(args) { // Do nothing by default. } static afterUpdate(args) { // Do nothing by default. } static beforeDelete(args) { // Do nothing by default. } static afterDelete(args) { // Do nothing by default. } static omitImpl(obj, prop) { delete obj[prop]; } static joinTableAlias(relationPath) { return `${relationPath}_join`; } static createValidator() { return new AjvValidator({ onCreateAjv: (ajv) => { /* Do Nothing by default */ }, options: { allErrors: true, validateSchema: false, ownProperties: true, v5: true, }, }); } static modifierNotFound(builder, modifier) { throw new this.ModifierNotFoundError(modifier); } static createNotFoundError(ctx, props) { return new this.NotFoundError(props); } static createValidationError(props) { return new this.ValidationError(props); } static getTableName() { let tableName = this.tableName; if (isFunction(tableName)) { tableName = this.tableName(); } if (!isString(tableName)) { throw new Error(`Model ${this.name} must have a static property tableName`); } return tableName; } static getIdColumn() { let idColumn = this.idColumn; if (isFunction(idColumn)) { idColumn = this.idColumn(); } return idColumn; } static getValidator() { return cachedGet(this, '$$validator', getValidator); } static getJsonSchema() { return cachedGet(this, '$$jsonSchema', getJsonSchema); } static getJsonAttributes() { return cachedGet(this, '$$jsonAttributes', getJsonAttributes); } static getColumnNameMappers() { return cachedGet(this, '$$columnNameMappers', getColumnNameMappers); } static getConcurrency(knex) { const DEFAULT_CONCURRENCY = 4; if (this.concurrency === null) { if (!knex) { return DEFAULT_CONCURRENCY; } // The mssql driver is shit, and we cannot have concurrent queries. if (isMsSql(knex)) { return 1; } else { return DEFAULT_CONCURRENCY; } } else { if (isFunction(this.concurrency)) { return this.concurrency(); } else { return this.concurrency; } } } static getModifiers() { return this.modifiers || this.namedFilters || {}; } static columnNameToPropertyName(columnName) { let colToProp = cachedGet(this, '$$colToProp', () => new Map()); let propertyName = colToProp.get(columnName); if (!propertyName) { propertyName = columnNameToPropertyName(this, columnName); colToProp.set(columnName, propertyName); } return propertyName; } static propertyNameToColumnName(propertyName) { let propToCol = cachedGet(this, '$$propToCol', () => new Map()); let columnName = propToCol.get(propertyName); if (!columnName) { columnName = propertyNameToColumnName(this, propertyName); propToCol.set(propertyName, columnName); } return columnName; } static getReadOnlyAttributes() { return cachedGet(this, '$$readOnlyAttributes', getReadOnlyAttributes); } static getIdRelationProperty() { return cachedGet(this, '$$idRelationProperty', getIdRelationProperty); } static getIdColumnArray() { return this.getIdRelationProperty().cols; } static getIdPropertyArray() { return this.getIdRelationProperty().props; } static getIdProperty() { const idProps = this.getIdPropertyArray(); if (idProps.length === 1) { return idProps[0]; } else { return idProps; } } static getRelationMappings() { return cachedGet(this, '$$relationMappings', getRelationMappings); } static getRelations() { const relations = Object.create(null); for (const relationName of this.getRelationNames()) { relations[relationName] = this.getRelation(relationName); } return relations; } static getRelationNames() { return cachedGet(this, '$$relationNames', getRelationNames); } static getVirtualAttributes() { return cachedGet(this, '$$virtualAttributes', getVirtualAttributes); } static getDefaultGraphOptions() { return this.defaultGraphOptions || this.defaultEagerOptions; } static getRelatedFindQueryMutates() { if (this.relatedFindQueryMutates) { deprecate('Model.relatedFindQueryMutates is deprected and will be removed in 3.0.'); } return this.relatedFindQueryMutates; } static getRelatedInsertQueryMutates() { if (this.relatedInsertQueryMutates) { deprecate('Model.relatedInsertQueryMutates is deprected and will be removed in 3.0.'); } return this.relatedInsertQueryMutates; } static query(trx) { const query = this.QueryBuilder.forClass(this).transacting(trx); this.onCreateQuery(query); return query; } static relatedQuery(relationName, trx) { return relatedQuery({ modelClass: this, relationName, transaction: trx, alwaysReturnArray: true, }); } static fetchTableMetadata(opt) { return fetchTableMetadata(this, opt); } static tableMetadata(opt) { return tableMetadata(this, opt); } static knex(...args) { if (args.length) { defineNonEnumerableProperty(this, '$$knex', args[0]); } else { return this.$$knex; } } static transaction(knexOrTrx, cb) { if (!cb) { cb = knexOrTrx; knexOrTrx = null; } return (knexOrTrx || this.knex()).transaction(cb); } static startTransaction(knexOrTrx) { const { transaction } = require('../transaction'); return transaction.start(knexOrTrx || this.knex()); } static get raw() { return raw; } static get ref() { return (...args) => { return ref(...args).model(this); }; } static get fn() { return fn; } static knexQuery() { return this.knex().table(this.getTableName()); } static uniqueTag() { if (this.name) { return `${this.getTableName()}_${this.name}`; } else { return this.getTableName(); } } static bindKnex(knex) { return bindKnex(this, knex); } static bindTransaction(trx) { return bindKnex(this, trx); } static ensureModel(model, options) { const modelClass = this; if (!model) { return null; } if (model instanceof modelClass) { return parseRelationsIntoModelInstances(model, model, options); } else { return modelClass.fromJson(model, options); } } static ensureModelArray(input, options) { if (!input) { return []; } if (Array.isArray(input)) { const models = new Array(input.length); for (let i = 0, l = input.length; i < l; ++i) { models[i] = this.ensureModel(input[i], options); } return models; } else { return [this.ensureModel(input, options)]; } } static getRelationUnsafe(name) { const mapping = this.getRelationMappings()[name]; if (!mapping) { return null; } if (!this.hasOwnProperty('$$relations')) { defineNonEnumerableProperty(this, '$$relations', Object.create(null)); } if (!this.$$relations[name]) { this.$$relations[name] = new mapping.relation(name, this); this.$$relations[name].setMapping(mapping); } return this.$$relations[name]; } static getRelation(name) { const relation = this.getRelationUnsafe(name); if (!relation) { throw new Error(`A model class ${this.name} doesn't have relation ${name}`); } return relation; } static loadRelated(models, expression, modifiers, transaction) { deprecate('loadRelated is deprected and will be removed in 3.0. Use fetchGraph instead'); return this.fetchGraph(models, expression, { transaction }).modifiers(modifiers); } static fetchGraph($models, expression, options = {}) { return this.query(options.transaction) .resolve(this.ensureModelArray($models)) .findOptions({ dontCallFindHooks: true }) .withGraphFetched(expression, options) .runAfter((models) => (Array.isArray($models) ? models : models[0])); } static traverse(...args) { const { traverser, models, filterConstructor } = getTraverseArgs(...args); if (!asSingle(models)) { return; } const modelClass = asSingle(models).constructor; visitModels(models, modelClass, (model, _, parent, relation) => { if (!filterConstructor || model instanceof filterConstructor) { traverser(model, parent, relation && relation.name); } }); return this; } static traverseAsync(...args) { const { traverser, models, filterConstructor } = getTraverseArgs(...args); if (!asSingle(models)) { return Promise.resolve(); } const modelClass = asSingle(models).constructor; const promises = []; visitModels(models, modelClass, (model, _, parent, relation) => { if (!filterConstructor || model instanceof filterConstructor) { const maybePromise = traverser(model, parent, relation && relation.name); promises.push(maybePromise); } }); return promiseMap(promises, (it) => it, { concurrency: this.getConcurrency(this.knex()) }); } } Object.defineProperties(Model, { isObjectionModelClass: { enumerable: false, writable: false, value: true, }, }); Object.defineProperties(Model.prototype, { $isObjectionModel: { enumerable: false, writable: false, value: true, }, $objectionModelClass: { enumerable: false, writable: false, value: Model, }, }); Model.QueryBuilder = QueryBuilder; Model.HasOneRelation = HasOneRelation; Model.HasManyRelation = HasManyRelation; Model.ManyToManyRelation = ManyToManyRelation; Model.BelongsToOneRelation = BelongsToOneRelation; Model.HasOneThroughRelation = HasOneThroughRelation; Model.JoinEagerAlgorithm = 'JoinEagerAlgorithm'; Model.NaiveEagerAlgorithm = 'NaiveEagerAlgorithm'; Model.WhereInEagerAlgorithm = 'WhereInEagerAlgorithm'; Model.ValidationError = ValidationError; Model.NotFoundError = NotFoundError; Model.ModifierNotFoundError = ModifierNotFoundError; Model.tableName = null; Model.jsonSchema = null; Model.idColumn = 'id'; Model.uidProp = '#id'; Model.uidRefProp = '#ref'; Model.dbRefProp = '#dbRef'; Model.propRefRegex = /#ref{([^\.]+)\.([^}]+)}/g; Model.jsonAttributes = null; Model.cloneObjectAttributes = true; Model.virtualAttributes = null; Model.relationMappings = null; Model.modelPaths = []; Model.pickJsonSchemaProperties = false; // Deprecated. Model.defaultEagerAlgorithm = Model.WhereInEagerAlgorithm; // Deprecated. Model.defaultEagerOptions = Object.freeze({ minimize: false, separator: ':', aliases: {} }); Model.defaultGraphOptions = null; Model.defaultFindOptions = Object.freeze({}); Model.modifiers = null; // Deprecated. Model.namedFilters = null; Model.useLimitInFirst = false; Model.columnNameMappers = null; // Deprecated. Model.relatedFindQueryMutates = false; // Deprecated. Model.relatedInsertQueryMutates = false; Model.concurrency = null; function instanceQuery({ instance, transaction }) { const modelClass = instance.constructor; return modelClass .query(transaction) .findOperationFactory(() => { return new InstanceFindOperation('find', { instance }); }) .insertOperationFactory(() => { return new InstanceInsertOperation('insert', { instance }); }) .updateOperationFactory(() => { return new InstanceUpdateOperation('update', { instance }); }) .patchOperationFactory(() => { return new InstanceUpdateOperation('patch', { instance, modelOptions: { patch: true }, }); }) .deleteOperationFactory(() => { return new InstanceDeleteOperation('delete', { instance }); }) .relateOperationFactory(() => { throw new Error('`relate` makes no sense in this context'); }) .unrelateOperationFactory(() => { throw new Error('`unrelate` makes no sense in this context'); }); } function relatedQuery({ modelClass, relationName, transaction, alwaysReturnArray } = {}) { const relation = modelClass.getRelation(relationName); const relatedModelClass = relation.relatedModelClass; return relatedModelClass .query(transaction) .findOperationFactory((builder) => { const isSubQuery = !builder.for(); const owner = isSubQuery ? RelationOwner.createParentReference(builder, relation) : RelationOwner.create(builder.for()); const operation = relation.find(builder, owner); operation.assignResultToOwner = modelClass.getRelatedFindQueryMutates(); operation.alwaysReturnArray = alwaysReturnArray; operation.alias = isSubQuery ? relation.name : null; return operation; }) .insertOperationFactory((builder) => { const owner = RelationOwner.create(builder.for()); const operation = relation.insert(builder, owner); operation.assignResultToOwner = modelClass.getRelatedInsertQueryMutates(); return operation; }) .updateOperationFactory((builder) => { const owner = RelationOwner.create(builder.for()); return relation.update(builder, owner); }) .patchOperationFactory((builder) => { const owner = RelationOwner.create(builder.for()); return relation.patch(builder, owner); }) .deleteOperationFactory((builder) => { const owner = RelationOwner.create(builder.for()); return relation.delete(builder, owner); }) .relateOperationFactory((builder) => { const owner = RelationOwner.create(builder.for()); return relation.relate(builder, owner); }) .unrelateOperationFactory((builder) => { const owner = RelationOwner.create(builder.for()); return relation.unrelate(builder, owner); }); } function cachedGet(target, hiddenPropertyName, creator) { if (!target.hasOwnProperty(hiddenPropertyName)) { defineNonEnumerableProperty(target, hiddenPropertyName, creator(target)); } return target[hiddenPropertyName]; } function getValidator(modelClass) { return modelClass.createValidator(); } function getJsonSchema(modelClass) { return modelClass.jsonSchema; } function getColumnNameMappers(modelClass) { return modelClass.columnNameMappers; } function getIdRelationProperty(modelClass) { const idColumn = asArray(modelClass.getIdColumn()); return new RelationProperty( idColumn.map((idCol) => `${modelClass.getTableName()}.${idCol}`), () => modelClass ); } function getReadOnlyAttributes(modelClass) { return [...new Set(getReadOnlyAttributesRecursively(modelClass))]; } function getReadOnlyAttributesRecursively(modelClass) { if (modelClass === Model) { // Stop recursion to the model class. return []; } const propertyNames = Object.getOwnPropertyNames(modelClass.prototype); return [ ...getReadOnlyAttributes(Object.getPrototypeOf(modelClass)), ...propertyNames.filter((propName) => { const desc = Object.getOwnPropertyDescriptor(modelClass.prototype, propName); return (desc.get && !desc.set) || desc.writable === false || isFunction(desc.value); }), ]; } function getRelationMappings(modelClass) { let relationMappings = modelClass.relationMappings; if (isFunction(relationMappings)) { relationMappings = relationMappings.call(modelClass); } return relationMappings || {}; } function getRelationNames(modelClass) { return Object.keys(modelClass.getRelationMappings()); } function getVirtualAttributes(modelClass) { return modelClass.virtualAttributes || []; } function getTraverseArgs(filterConstructor, models, traverser) { filterConstructor = filterConstructor || null; if (traverser === undefined) { traverser = models; models = filterConstructor; filterConstructor = null; } if (!isFunction(traverser)) { throw new Error('traverser must be a function'); } return { traverser, models, filterConstructor, }; } module.exports = { Model, };