Model.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. const { clone } = require('./modelClone');
  2. const { asArray } = require('../utils/objectUtils');
  3. const { bindKnex } = require('./modelBindKnex');
  4. const { validate } = require('./modelValidate');
  5. const { isPromise } = require('../utils/promiseUtils');
  6. const { omit, pick } = require('./modelFilter');
  7. const { visitModels } = require('./modelVisitor');
  8. const { hasId, getSetId } = require('./modelId');
  9. const { toJson, toDatabaseJson } = require('./modelToJson');
  10. const { values, propKey, hasProps } = require('./modelValues');
  11. const { defineNonEnumerableProperty } = require('./modelUtils');
  12. const { parseRelationsIntoModelInstances } = require('./modelParseRelations');
  13. const { fetchTableMetadata, tableMetadata } = require('./modelTableMetadata');
  14. const { setJson, setFast, setRelated, appendRelated, setDatabaseJson } = require('./modelSet');
  15. const {
  16. getJsonAttributes,
  17. parseJsonAttributes,
  18. formatJsonAttributes
  19. } = require('./modelJsonAttributes');
  20. const {
  21. idColumnToIdProperty,
  22. columnNameToPropertyName,
  23. propertyNameToColumnName
  24. } = require('./modelColPropMap');
  25. const AjvValidator = require('./AjvValidator');
  26. const QueryBuilder = require('../queryBuilder/QueryBuilder');
  27. const NotFoundError = require('./NotFoundError');
  28. const ValidationError = require('./ValidationError');
  29. const RelationProperty = require('../relations/RelationProperty');
  30. const Relation = require('../relations/Relation');
  31. const HasOneRelation = require('../relations/hasOne/HasOneRelation');
  32. const HasManyRelation = require('../relations/hasMany/HasManyRelation');
  33. const ManyToManyRelation = require('../relations/manyToMany/ManyToManyRelation');
  34. const BelongsToOneRelation = require('../relations/belongsToOne/BelongsToOneRelation');
  35. const HasOneThroughRelation = require('../relations/hasOneThrough/HasOneThroughRelation');
  36. const InstanceFindOperation = require('../queryBuilder/operations/InstanceFindOperation');
  37. const InstanceInsertOperation = require('../queryBuilder/operations/InstanceInsertOperation');
  38. const InstanceUpdateOperation = require('../queryBuilder/operations/InstanceUpdateOperation');
  39. const InstanceDeleteOperation = require('../queryBuilder/operations/InstanceDeleteOperation');
  40. const JoinEagerOperation = require('../queryBuilder/operations/eager/JoinEagerOperation');
  41. const NaiveEagerOperation = require('../queryBuilder/operations/eager/NaiveEagerOperation');
  42. const WhereInEagerOperation = require('../queryBuilder/operations/eager/WhereInEagerOperation');
  43. const JoinEagerAlgorithm = () => {
  44. return new JoinEagerOperation('eager');
  45. };
  46. const NaiveEagerAlgorithm = () => {
  47. return new NaiveEagerOperation('eager');
  48. };
  49. const WhereInEagerAlgorithm = () => {
  50. return new WhereInEagerOperation('eager');
  51. };
  52. class Model {
  53. $id(maybeId) {
  54. return getSetId(this, maybeId);
  55. }
  56. $hasId() {
  57. return hasId(this);
  58. }
  59. $hasProps(props) {
  60. return hasProps(this, props);
  61. }
  62. $query(trx) {
  63. const ModelClass = this.constructor;
  64. return ModelClass.query(trx)
  65. .findOperationFactory(() => {
  66. return new InstanceFindOperation('find', { instance: this });
  67. })
  68. .insertOperationFactory(() => {
  69. return new InstanceInsertOperation('insert', { instance: this });
  70. })
  71. .updateOperationFactory(() => {
  72. return new InstanceUpdateOperation('update', { instance: this });
  73. })
  74. .patchOperationFactory(() => {
  75. return new InstanceUpdateOperation('patch', {
  76. instance: this,
  77. modelOptions: { patch: true }
  78. });
  79. })
  80. .deleteOperationFactory(() => {
  81. return new InstanceDeleteOperation('delete', { instance: this });
  82. })
  83. .relateOperationFactory(() => {
  84. throw new Error('`relate` makes no sense in this context');
  85. })
  86. .unrelateOperationFactory(() => {
  87. throw new Error('`unrelate` makes no sense in this context');
  88. });
  89. }
  90. $relatedQuery(relationName, trx) {
  91. const ModelClass = this.constructor;
  92. const relation = ModelClass.getRelation(relationName);
  93. const RelatedModelClass = relation.relatedModelClass;
  94. return RelatedModelClass.query(trx)
  95. .findOperationFactory(builder => {
  96. const operation = relation.find(builder, [this]);
  97. operation.assignResultToOwner = this.constructor.relatedFindQueryMutates;
  98. return operation;
  99. })
  100. .insertOperationFactory(builder => {
  101. const operation = relation.insert(builder, this);
  102. operation.assignResultToOwner = this.constructor.relatedInsertQueryMutates;
  103. return operation;
  104. })
  105. .updateOperationFactory(builder => {
  106. return relation.update(builder, this);
  107. })
  108. .patchOperationFactory(builder => {
  109. return relation.patch(builder, this);
  110. })
  111. .deleteOperationFactory(builder => {
  112. return relation.delete(builder, this);
  113. })
  114. .relateOperationFactory(builder => {
  115. return relation.relate(builder, this);
  116. })
  117. .unrelateOperationFactory(builder => {
  118. return relation.unrelate(builder, this);
  119. });
  120. }
  121. $loadRelated(relationExpression, filters, trx) {
  122. return this.constructor.loadRelated(this, relationExpression, filters, trx);
  123. }
  124. $beforeValidate(jsonSchema, json, options) {
  125. /* istanbul ignore next */
  126. return jsonSchema;
  127. }
  128. $validate(json, options) {
  129. return validate(this, json, options);
  130. }
  131. $afterValidate(json, options) {
  132. // Do nothing by default.
  133. }
  134. $parseDatabaseJson(json) {
  135. const columnNameMappers = this.constructor.getColumnNameMappers();
  136. if (columnNameMappers) {
  137. json = columnNameMappers.parse(json);
  138. }
  139. return parseJsonAttributes(json, this.constructor);
  140. }
  141. $formatDatabaseJson(json) {
  142. const columnNameMappers = this.constructor.getColumnNameMappers();
  143. json = formatJsonAttributes(json, this.constructor);
  144. if (columnNameMappers) {
  145. json = columnNameMappers.format(json);
  146. }
  147. return json;
  148. }
  149. $parseJson(json, options) {
  150. return json;
  151. }
  152. $formatJson(json) {
  153. return json;
  154. }
  155. $setJson(json, options) {
  156. return setJson(this, json, options);
  157. }
  158. $setDatabaseJson(json) {
  159. return setDatabaseJson(this, json);
  160. }
  161. $set(obj) {
  162. return setFast(this, obj);
  163. }
  164. $setRelated(relation, models) {
  165. return setRelated(this, relation, models);
  166. }
  167. $appendRelated(relation, models) {
  168. return appendRelated(this, relation, models);
  169. }
  170. $toJson(opt) {
  171. return toJson(this, opt);
  172. }
  173. toJSON(opt) {
  174. return this.$toJson(opt);
  175. }
  176. $toDatabaseJson(knex) {
  177. return toDatabaseJson(this, knex);
  178. }
  179. $beforeInsert(queryContext) {
  180. // Do nothing by default.
  181. }
  182. $afterInsert(queryContext) {
  183. // Do nothing by default.
  184. }
  185. $beforeUpdate(opt, queryContext) {
  186. // Do nothing by default.
  187. }
  188. $afterUpdate(opt, queryContext) {
  189. // Do nothing by default.
  190. }
  191. $afterGet(queryContext) {
  192. // Do nothing by default.
  193. }
  194. $beforeDelete(queryContext) {
  195. // Do nothing by default.
  196. }
  197. $afterDelete(queryContext) {
  198. // Do nothing by default.
  199. }
  200. $omit() {
  201. return omit(this, arguments);
  202. }
  203. $pick() {
  204. return pick(this, arguments);
  205. }
  206. $values(props) {
  207. return values(this, props);
  208. }
  209. $propKey(props) {
  210. return propKey(this, props);
  211. }
  212. $clone(opt) {
  213. return clone(this, !!(opt && opt.shallow), false);
  214. }
  215. $traverse(filterConstructor, callback) {
  216. if (callback === undefined) {
  217. callback = filterConstructor;
  218. filterConstructor = null;
  219. }
  220. this.constructor.traverse(filterConstructor, this, callback);
  221. return this;
  222. }
  223. $omitFromJson(props) {
  224. if (arguments.length === 0) {
  225. return this.$$omitFromJson;
  226. } else {
  227. if (!this.hasOwnProperty('$$omitFromJson')) {
  228. defineNonEnumerableProperty(this, '$$omitFromJson', props);
  229. } else {
  230. this.$$omitFromJson = this.$$omitFromJson.concat(props);
  231. }
  232. }
  233. }
  234. $omitFromDatabaseJson(props) {
  235. if (arguments.length === 0) {
  236. return this.$$omitFromDatabaseJson;
  237. } else {
  238. if (!this.hasOwnProperty('$$omitFromDatabaseJson')) {
  239. defineNonEnumerableProperty(this, '$$omitFromDatabaseJson', props);
  240. } else {
  241. this.$$omitFromDatabaseJson = this.$$omitFromDatabaseJson.concat(props);
  242. }
  243. }
  244. }
  245. $knex() {
  246. return this.constructor.knex();
  247. }
  248. $transaction() {
  249. return this.constructor.transaction();
  250. }
  251. static get objectionModelClass() {
  252. return Model;
  253. }
  254. static fromJson(json, options) {
  255. const model = new this();
  256. model.$setJson(json || {}, options);
  257. return model;
  258. }
  259. static fromDatabaseJson(json) {
  260. const model = new this();
  261. model.$setDatabaseJson(json || {});
  262. return model;
  263. }
  264. static omitImpl(obj, prop) {
  265. delete obj[prop];
  266. }
  267. static createValidator() {
  268. return new AjvValidator({
  269. onCreateAjv: ajv => {
  270. /* Do Nothing by default */
  271. },
  272. options: {
  273. allErrors: true,
  274. validateSchema: false,
  275. ownProperties: true,
  276. v5: true
  277. }
  278. });
  279. }
  280. static createNotFoundError(ctx) {
  281. return new this.NotFoundError();
  282. }
  283. static createValidationError(props) {
  284. return new this.ValidationError(props);
  285. }
  286. static getTableName() {
  287. let tableName = this.tableName;
  288. if (typeof tableName === 'function') {
  289. tableName = this.tableName();
  290. }
  291. if (typeof tableName !== 'string') {
  292. throw new Error(`Model ${this.name} must have a static property tableName`);
  293. }
  294. return tableName;
  295. }
  296. static getIdColumn() {
  297. let idColumn = this.idColumn;
  298. if (typeof idColumn === 'function') {
  299. idColumn = this.idColumn();
  300. }
  301. return idColumn;
  302. }
  303. static getValidator() {
  304. // Memoize the validator but only for this class. The hasOwnProperty check
  305. // will fail for subclasses and the validator gets recreated.
  306. if (!this.hasOwnProperty('$$validator')) {
  307. defineNonEnumerableProperty(this, '$$validator', this.createValidator());
  308. }
  309. return this.$$validator;
  310. }
  311. static getJsonSchema() {
  312. // Memoize the jsonSchema but only for this class. The hasOwnProperty check
  313. // will fail for subclasses and the value gets recreated.
  314. if (!this.hasOwnProperty('$$jsonSchema')) {
  315. // this.jsonSchema is often a getter that returns a new object each time. We need
  316. // memoize it to make sure we get the same instance each time.
  317. defineNonEnumerableProperty(this, '$$jsonSchema', this.jsonSchema);
  318. }
  319. return this.$$jsonSchema;
  320. }
  321. static getJsonAttributes() {
  322. // Memoize the jsonAttributes but only for this class. The hasOwnProperty check
  323. // will fail for subclasses and the value gets recreated.
  324. if (!this.hasOwnProperty('$$jsonAttributes')) {
  325. defineNonEnumerableProperty(this, '$$jsonAttributes', getJsonAttributes(this));
  326. }
  327. return this.$$jsonAttributes;
  328. }
  329. static getColumnNameMappers() {
  330. // Memoize the columnNameMappers but only for this class. The hasOwnProperty check
  331. // will fail for subclasses and the value gets recreated.
  332. if (!this.hasOwnProperty('$$columnNameMappers')) {
  333. defineNonEnumerableProperty(this, '$$columnNameMappers', this.columnNameMappers);
  334. }
  335. return this.$$columnNameMappers;
  336. }
  337. static columnNameToPropertyName(columnName) {
  338. // Memoize the converted names but only for this class. The hasOwnProperty check
  339. // will fail for subclasses and the value gets recreated.
  340. if (!this.hasOwnProperty('$$colToProp')) {
  341. defineNonEnumerableProperty(this, '$$colToProp', new Map());
  342. }
  343. let propertyName = this.$$colToProp.get(columnName);
  344. if (!propertyName) {
  345. propertyName = columnNameToPropertyName(this, columnName);
  346. this.$$colToProp.set(columnName, propertyName);
  347. }
  348. return propertyName;
  349. }
  350. static propertyNameToColumnName(propertyName) {
  351. // Memoize the converted names but only for this class. The hasOwnProperty check
  352. // will fail for subclasses and the value gets recreated.
  353. if (!this.hasOwnProperty('$$propToCol')) {
  354. defineNonEnumerableProperty(this, '$$propToCol', new Map());
  355. }
  356. let columnName = this.$$propToCol.get(propertyName);
  357. if (!columnName) {
  358. columnName = propertyNameToColumnName(this, propertyName);
  359. this.$$propToCol.set(propertyName, columnName);
  360. }
  361. return columnName;
  362. }
  363. static getReadOnlyVirtualAttributes() {
  364. // Memoize getReadOnlyVirtualAttributes but only for this class. The hasOwnProperty check
  365. // will fail for subclasses and the value gets recreated.
  366. if (!this.hasOwnProperty('$$readOnlyVirtualAttributes')) {
  367. defineNonEnumerableProperty(
  368. this,
  369. '$$readOnlyVirtualAttributes',
  370. getReadOnlyVirtualAttributes(this)
  371. );
  372. }
  373. return this.$$readOnlyVirtualAttributes;
  374. }
  375. static getIdRelationProperty() {
  376. // Memoize getIdRelationProperty but only for this class. The hasOwnProperty check
  377. // will fail for subclasses and the value gets recreated.
  378. if (!this.hasOwnProperty('$$idRelationProperty')) {
  379. defineNonEnumerableProperty(this, '$$idRelationProperty', getIdRelationProperty(this));
  380. }
  381. return this.$$idRelationProperty;
  382. }
  383. static getIdColumnArray() {
  384. return this.getIdRelationProperty().cols;
  385. }
  386. static getIdPropertyArray() {
  387. return this.getIdRelationProperty().props;
  388. }
  389. static getIdProperty() {
  390. const idProps = this.getIdPropertyArray();
  391. if (idProps.length === 1) {
  392. return idProps[0];
  393. } else {
  394. return idProps;
  395. }
  396. }
  397. static getRelations() {
  398. // Memoize relations but only for this class. The hasOwnProperty check
  399. // will fail for subclasses and the value gets recreated.
  400. if (!this.hasOwnProperty('$$relations')) {
  401. let relationMappings = this.relationMappings;
  402. if (typeof relationMappings === 'function') {
  403. relationMappings = relationMappings.call(this);
  404. }
  405. const relations = Object.keys(relationMappings || {}).reduce((relations, relationName) => {
  406. const mapping = relationMappings[relationName];
  407. relations[relationName] = new mapping.relation(relationName, this);
  408. relations[relationName].setMapping(mapping);
  409. return relations;
  410. }, Object.create(null));
  411. defineNonEnumerableProperty(this, '$$relations', relations);
  412. }
  413. return this.$$relations;
  414. }
  415. static getRelationArray() {
  416. // Memoize relation array but only for this class. The hasOwnProperty check
  417. // will fail for subclasses and the value gets recreated.
  418. if (!this.hasOwnProperty('$$relationArray')) {
  419. defineNonEnumerableProperty(this, '$$relationArray', getRelationArray(this));
  420. }
  421. return this.$$relationArray;
  422. }
  423. static query(trx) {
  424. return this.QueryBuilder.forClass(this).transacting(trx);
  425. }
  426. static relatedQuery(relationName) {
  427. const relation = this.getRelation(relationName);
  428. const modelClass = relation.relatedModelClass;
  429. return modelClass
  430. .query()
  431. .alias(relation.name)
  432. .findOperationFactory(builder => relation.subQuery(builder));
  433. }
  434. static fetchTableMetadata(opt) {
  435. return fetchTableMetadata(this, opt);
  436. }
  437. static tableMetadata(opt) {
  438. return tableMetadata(this, opt);
  439. }
  440. static knex() {
  441. if (arguments.length) {
  442. defineNonEnumerableProperty(this, '$$knex', arguments[0]);
  443. } else {
  444. return this.$$knex;
  445. }
  446. }
  447. static transaction() {
  448. return this.knex();
  449. }
  450. static raw() {
  451. const knex = this.knex();
  452. return knex.raw.apply(knex, arguments);
  453. }
  454. static fn() {
  455. const knex = this.knex();
  456. return knex.fn;
  457. }
  458. static knexQuery() {
  459. return this.knex().table(this.getTableName());
  460. }
  461. static uniqueTag() {
  462. if (this.name) {
  463. return `${this.getTableName()}_${this.name}`;
  464. } else {
  465. return this.getTableName();
  466. }
  467. }
  468. static bindKnex(knex) {
  469. return bindKnex(this, knex);
  470. }
  471. static bindTransaction(trx) {
  472. return bindKnex(this, trx);
  473. }
  474. static ensureModel(model, options) {
  475. const ModelClass = this;
  476. if (!model) {
  477. return null;
  478. }
  479. if (model instanceof ModelClass) {
  480. return parseRelationsIntoModelInstances(model, model, options);
  481. } else {
  482. return ModelClass.fromJson(model, options);
  483. }
  484. }
  485. static ensureModelArray(input, options) {
  486. if (!input) {
  487. return [];
  488. }
  489. if (Array.isArray(input)) {
  490. let models = new Array(input.length);
  491. for (let i = 0, l = input.length; i < l; ++i) {
  492. models[i] = this.ensureModel(input[i], options);
  493. }
  494. return models;
  495. } else {
  496. return [this.ensureModel(input, options)];
  497. }
  498. }
  499. static getRelation(name) {
  500. const relation = this.getRelations()[name];
  501. if (!relation) {
  502. throw new Error(`A model class ${this.name} doesn't have relation ${name}`);
  503. }
  504. return relation;
  505. }
  506. static loadRelated($models, expression, filters, trx) {
  507. return this.query(trx)
  508. .resolve(this.ensureModelArray($models))
  509. .findOptions({ dontCallAfterGet: true })
  510. .eager(expression, filters)
  511. .runAfter(models => (Array.isArray($models) ? models : models[0]));
  512. }
  513. static traverse(filterConstructor, models, traverser) {
  514. filterConstructor = filterConstructor || null;
  515. if (traverser === undefined) {
  516. traverser = models;
  517. models = filterConstructor;
  518. filterConstructor = null;
  519. }
  520. if (typeof traverser !== 'function') {
  521. throw new Error('traverser must be a function');
  522. }
  523. if (!models || (Array.isArray(models) && !models.length)) {
  524. return this;
  525. }
  526. const modelClass = Array.isArray(models) ? models[0].constructor : models.constructor;
  527. visitModels(models, modelClass, (model, modelClass, parent, relation) => {
  528. if (!filterConstructor || model instanceof filterConstructor) {
  529. traverser(model, parent, relation && relation.name);
  530. }
  531. });
  532. return this;
  533. }
  534. }
  535. Object.defineProperties(Model.prototype, {
  536. $isObjectionModel: {
  537. enumerable: false,
  538. writable: false,
  539. value: true
  540. },
  541. $objectionModelClass: {
  542. enumerable: false,
  543. writable: false,
  544. value: Model
  545. }
  546. });
  547. Model.QueryBuilder = QueryBuilder;
  548. Model.HasOneRelation = HasOneRelation;
  549. Model.HasManyRelation = HasManyRelation;
  550. Model.ManyToManyRelation = ManyToManyRelation;
  551. Model.BelongsToOneRelation = BelongsToOneRelation;
  552. Model.HasOneThroughRelation = HasOneThroughRelation;
  553. Model.JoinEagerAlgorithm = JoinEagerAlgorithm;
  554. Model.NaiveEagerAlgorithm = NaiveEagerAlgorithm;
  555. Model.WhereInEagerAlgorithm = WhereInEagerAlgorithm;
  556. Model.ValidationError = ValidationError;
  557. Model.NotFoundError = NotFoundError;
  558. Model.tableName = null;
  559. Model.jsonSchema = null;
  560. Model.idColumn = 'id';
  561. Model.uidProp = '#id';
  562. Model.uidRefProp = '#ref';
  563. Model.dbRefProp = '#dbRef';
  564. Model.propRefRegex = /#ref{([^\.]+)\.([^}]+)}/g;
  565. Model.jsonAttributes = null;
  566. Model.virtualAttributes = null;
  567. Model.relationMappings = null;
  568. Model.modelPaths = [];
  569. Model.pickJsonSchemaProperties = false;
  570. Model.defaultEagerAlgorithm = WhereInEagerAlgorithm;
  571. Model.defaultEagerOptions = Object.freeze({ minimize: false, separator: ':', aliases: {} });
  572. Model.defaultFindOptions = Object.freeze({});
  573. Model.namedFilters = null;
  574. Model.useLimitInFirst = false;
  575. Model.columnNameMappers = null;
  576. Model.relatedFindQueryMutates = true;
  577. Model.relatedInsertQueryMutates = true;
  578. Model.concurrency = 1;
  579. function getIdRelationProperty(ModelClass) {
  580. const idColumn = asArray(ModelClass.getIdColumn());
  581. return new RelationProperty(
  582. idColumn.map(idCol => `${ModelClass.getTableName()}.${idCol}`),
  583. () => ModelClass
  584. );
  585. }
  586. function getReadOnlyVirtualAttributes(ModelClass) {
  587. const virtuals = ModelClass.virtualAttributes;
  588. if (!Array.isArray(virtuals)) {
  589. return null;
  590. }
  591. return virtuals.filter(virtual => {
  592. const desc = Object.getOwnPropertyDescriptor(ModelClass.prototype, virtual);
  593. if (!desc) {
  594. return false;
  595. }
  596. return (desc.get && !desc.set) || desc.writable === false || typeof desc.value === 'function';
  597. });
  598. }
  599. function getRelationArray(ModelClass) {
  600. const relations = ModelClass.getRelations();
  601. return Object.keys(relations).map(key => relations[key]);
  602. }
  603. module.exports = Model;