objection
Version:
An SQL-friendly ORM for Node.js
952 lines (761 loc) • 24.1 kB
JavaScript
;
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,
};