diff --git a/doc/includes/API.md b/doc/includes/API.md index 9ddb97155..3c03e756e 100644 --- a/doc/includes/API.md +++ b/doc/includes/API.md @@ -1,5 +1,9 @@ # API reference +__NOTE__: Everything not mentioned in the API documentation is considered private implementation +and shouldn't be relied upon. Private implemementation can change without any notice even between +patch versions. Public API described here follows [semantic versioning](https://semver.org/). + @@ -5668,11 +5672,6 @@ Shortcut for `Person.knex().fn` -#### formatter - -Shortcut for `Person.knex().client.formatter()` - - #### knexQuery @@ -6086,6 +6085,70 @@ Object.<string, [`Relation`](#relation)>|Object whose keys are relation na +#### columnNameToPropertyName + +```js +const propertyName = Person.columnNameToPropertyName(columnName); +``` + +> For example, if you have defined `columnNameMappers = snakeCaseMappers()` for your model: + +```js +const propName = Person.columnNameToPropertyName('foo_bar'); +console.log(propName); // --> 'fooBar' +``` + +Runs the property through possible `columnNameMappers` and `$parseDatabaseJson` hooks to apply +any possible conversion for the column name. + +##### Arguments + +Argument|Type|Description +--------|----|------------------- +columnName|string|A column name + +##### Return value + +Type|Description +----|----------------------------- +string|The property name + + + + + +#### propertyNameToColumnName + +```js +const columnName = Person.propertyNameToColumnName(propertyName); +``` + +> For example, if you have defined `columnNameMappers = snakeCaseMappers()` for your model: + +```js +const columnName = Person.propertyNameToColumnName('fooBar'); +console.log(columnName); // --> 'foo_bar' +``` + +Runs the property through possible `columnNameMappers` and `$formatDatabaseJson` hooks to apply +any possible conversion for the property name. + +##### Arguments + +Argument|Type|Description +--------|----|------------------- +propertyName|string|A property name + +##### Return value + +Type|Description +----|----------------------------- +string|The column name + + + + + ### Instance methods diff --git a/doc/includes/CHANGELOG.md b/doc/includes/CHANGELOG.md index 4553c7c63..41ecea01a 100644 --- a/doc/includes/CHANGELOG.md +++ b/doc/includes/CHANGELOG.md @@ -6,15 +6,17 @@ * The static [`relatedQuery`](#relatedquery) method. * New reflection methods: - [`isFind`](http://vincit.github.io/objection.js/#isfind), - [`isInsert`](http://vincit.github.io/objection.js/#isinsert), - [`isUpdate`](http://vincit.github.io/objection.js/#isupdate), - [`isDelete`](http://vincit.github.io/objection.js/#isdelete), - [`isRelate`](http://vincit.github.io/objection.js/#isrelate), - [`isUnrelate`](http://vincit.github.io/objection.js/#isunrelate), - [`hasWheres`](http://vincit.github.io/objection.js/#haswheres), - [`hasSelects`](http://vincit.github.io/objection.js/#hasselects), - [`hasEager`](http://vincit.github.io/objection.js/#haseager). + [`isFind`](#isfind), + [`isInsert`](#isinsert), + [`isUpdate`](#isupdate), + [`isDelete`](#isdelete), + [`isRelate`](#isrelate), + [`isUnrelate`](#isunrelate), + [`hasWheres`](#haswheres), + [`hasSelects`](#hasselects), + [`hasEager`](#haseager), + [`columnNameToPropertyName`](#columnnametopropertyname), + [`propertyNameToColumnName`](#propertynametocolumnname). ### Breaking changes diff --git a/doc/includes/RECIPES.md b/doc/includes/RECIPES.md index d8c72788d..c2c033462 100644 --- a/doc/includes/RECIPES.md +++ b/doc/includes/RECIPES.md @@ -706,6 +706,147 @@ select "Tweet".*, (select count(*) from "Like" where "Like"."tweetId" = "Tweet". Naturally you can add as many subquery selects as you like. For example you could also get the count of retweets in the same query. [`relatedQuery`](#relatedquery) method works with all relations and not just `HasManyRelation`. +## Error handling + +> An example error handler function that handles all possible errors. This example uses the [`objection-db-errors`](https://github.com/Vincit/objection-db-errors) library. + +```js +const { + ValidationError, + NotFoundError +} = require('objection'); + +const { + DbError, + ConstraintViolationError, + UniqueViolationError, + NotNullViolationError, + ForeignKeyViolationError, + CheckViolationError, + DataError +} = require('objection-db-errors'); + +function errorHandler(err, res) { + if (err instanceof ValidationError) { + switch (err.type) { + case 'ModelValidation': + res.status(400).send({ + message: err.message, + type: 'ModelValidation', + data: err.data + }); + break; + case 'RelationExpression': + res.status(400).send({ + message: err.message, + type: 'InvalidRelationExpression', + data: {} + }); + break; + case 'UnallowedRelation': + res.status(400).send({ + message: err.message, + type: 'UnallowedRelation', + data: {} + }); + break; + case 'InvalidGraph': + res.status(400).send({ + message: err.message, + type: 'InvalidGraph', + data: {} + }); + break; + default: + res.status(400).send({ + message: err.message, + type: 'UnknownValidationError', + data: {} + }); + break; + } + } else if (err instanceof NotFoundError) { + res.status(404).send({ + message: err.message, + type: 'NotFound', + data: {} + }); + } else if (err instanceof UniqueViolationError) { + res.status(409).send({ + message: err.message, + type: 'UniqueViolation', + data: { + columns: err.columns, + table: err.table, + constraint: err.constraint + } + }); + } else if (err instanceof NotNullViolationError) { + res.status(400).send({ + message: err.message, + type: 'NotNullViolation', + data: { + column: err.column, + table: err.table, + } + }); + } else if (err instanceof ForeignKeyViolationError) { + res.status(409).send({ + message: err.message, + type: 'ForeignKeyViolation', + data: { + table: err.table, + constraint: err.constraint + } + }); + } else if (err instanceof CheckViolationError) { + res.status(400).send({ + message: err.message, + type: 'CheckViolation', + data: { + table: err.table, + constraint: err.constraint + } + }); + } else if (err instanceof DataError) { + res.status(400).send({ + message: err.message, + type: 'InvalidData', + data: {} + }); + } else if (err instanceof DbError) { + res.status(500).send({ + message: err.message, + type: 'UnknownDatabaseError', + data: {} + }); + } else { + res.status(500).send({ + message: err.message, + type: 'UnknownError', + data: {} + }); + } +} +``` + +Objection throws four kinds of errors: + +1. [`ValidationError`](#validationerror) when an input that could come from the outside world is invalid. These inputs + include model instances and POJO's, eager expressions object graphs etc. `ValidationError` has a `type` property + that can be used to distinguish the different error types. + +2. [`NotFoundError`](#notfounderror) when [`throwIfNotFound`](#throwifnotfound) was called for a query and no + results were found. + +3. Database errors (unique violation error etc.) are thrown by the database client libraries and the error types depend on the + library. You can use the [`objection-db-errors`](https://github.com/Vincit/objection-db-errors) plugin to handle these. + +4. A basic javascript `Error` when a programming or logic error is detected. In these cases there is nothing the users + can do and the only correct way to handle the error is to send a 500 response to the user and to fix the program. + +See the example error handler that handles each error type. + ## Indexing PostgreSQL JSONB columns Good reading on the subject: diff --git a/lib/model/DbMetadata.js b/lib/model/DbMetadata.js new file mode 100644 index 000000000..a87729a74 --- /dev/null +++ b/lib/model/DbMetadata.js @@ -0,0 +1,15 @@ +class DbMetadata { + constructor(columnInfo) { + this.columns = Object.keys(columnInfo); + } + + static fetch({ modelClass, parentBuilder = null, knex = null } = {}) { + return modelClass + .query(knex) + .childQueryOf(parentBuilder) + .columnInfo() + .then(columnInfo => new DbMetadata(columnInfo)); + } +} + +module.exports = DbMetadata; diff --git a/lib/model/Model.js b/lib/model/Model.js index e7153c3c5..13e9c04b3 100644 --- a/lib/model/Model.js +++ b/lib/model/Model.js @@ -20,6 +20,7 @@ const { propertyNameToColumnName } = require('./modelColPropMap'); +const DbMetadata = require('./DbMetadata'); const AjvValidator = require('./AjvValidator'); const QueryBuilder = require('../queryBuilder/QueryBuilder'); const NotFoundError = require('./NotFoundError'); @@ -523,6 +524,30 @@ class Model { return modelClass.query().findOperationFactory(builder => relation.subQuery(builder)); } + static dbMetadata({ parentBuilder = null, knex = null } = {}) { + // Memoize metadata but only for this class. The hasOwnProperty check + // will fail for subclasses and the value gets recreated. + if (!this.hasOwnProperty('$$dbMetadata')) { + defineNonEnumerableProperty(this, '$$dbMetadata', Object.create(null)); + } + + // The table isn't necessarily same as `this.getTableName()` for example if + // a view is queried instead. + const table = (parentBuilder && parentBuilder.tableNameFor(this)) || this.getTableName(); + + if (this.$$dbMetadata[table]) { + return Promise.resolve(this.$$dbMetadata[table]); + } else { + const promise = DbMetadata.fetch({ modelClass: this, parentBuilder, knex }).then(metadata => { + this.$$dbMetadata[table] = metadata; + return metadata; + }); + + this.$$dbMetadata[table] = promise; + return promise; + } + } + static knex() { if (arguments.length) { defineNonEnumerableProperty(this, '$$knex', arguments[0]); diff --git a/lib/model/modelSet.js b/lib/model/modelSet.js index 2b6625d19..bc9718db0 100644 --- a/lib/model/modelSet.js +++ b/lib/model/modelSet.js @@ -100,8 +100,9 @@ function setDatabaseJson(model, json) { function setFast(model, obj) { if (obj) { - // Don't try to set read-only virtual properties. They can easily get here through `fromJson` - // when parsing an object that was previously serialized from a model instance. + // Don't try to set read-only virtual properties. They can easily get here + // through `fromJson` when parsing an object that was previously serialized + // from a model instance. const readOnlyVirtuals = model.constructor.getReadOnlyVirtualAttributes(); const keys = Object.keys(obj); diff --git a/lib/model/modelUtils.js b/lib/model/modelUtils.js index 36dce6c7f..e4c003d6e 100644 --- a/lib/model/modelUtils.js +++ b/lib/model/modelUtils.js @@ -12,7 +12,8 @@ const staticHiddenProps = [ '$$relations', '$$relationArray', '$$jsonAttributes', - '$$columnNameMappers' + '$$columnNameMappers', + '$$dbMetadata' ]; function defineNonEnumerableProperty(obj, prop, value) { diff --git a/lib/model/modelValidate.js b/lib/model/modelValidate.js index 4abf8a49b..58d79cc1f 100644 --- a/lib/model/modelValidate.js +++ b/lib/model/modelValidate.js @@ -1,19 +1,20 @@ const clone = require('./modelClone').clone; const { defineNonEnumerableProperty } = require('./modelUtils'); -function validate(model, json, options) { +function validate(model, json, options = {}) { json = json || model; - options = options || {}; + const inputJson = json; + const validatingModelInstance = inputJson && inputJson.$isObjectionModel; - if (json && json.$isObjectionModel) { + if (options.skipValidation) { + return json; + } + + if (validatingModelInstance) { // Strip away relations and other internal stuff. json = clone(json, true, true); // We can mutate `json` now that we took a copy of it. - options.mutable = true; - } - - if (options.skipValidation) { - return json; + options = Object.assign({}, options, { mutable: true }); } const ModelClass = model.constructor; @@ -29,7 +30,12 @@ function validate(model, json, options) { json = validator.validate(args); validator.afterValidate(args); - return json; + if (validatingModelInstance) { + // If we cloned `json`, we need to copy the possible default values. + return inputJson.$set(json); + } else { + return json; + } } module.exports = { diff --git a/lib/queryBuilder/QueryBuilder.js b/lib/queryBuilder/QueryBuilder.js index d611edb1b..7786e7638 100644 --- a/lib/queryBuilder/QueryBuilder.js +++ b/lib/queryBuilder/QueryBuilder.js @@ -33,7 +33,7 @@ const FindByIdOperation = require('./operations/FindByIdOperation'); const FindByIdsOperation = require('./operations/FindByIdsOperation'); const OnBuildOperation = require('./operations/OnBuildOperation'); const OnErrorOperation = require('./operations/OnErrorOperation'); -const SelectOperation = require('./operations/SelectOperation'); +const SelectOperation = require('./operations/select/SelectOperation'); const EagerOperation = require('./operations/eager/EagerOperation'); const RangeOperation = require('./operations/RangeOperation'); const FirstOperation = require('./operations/FirstOperation'); @@ -145,9 +145,11 @@ class QueryBuilder extends QueryBuilderBase { for (let i = 0, l = arguments.length; i < l; ++i) { const name = arguments[i]; const filter = namedFilters[name]; + if (typeof filter !== 'function') { throw new Error(`Could not find filter "${name}".`); } + filter(this); } @@ -643,7 +645,7 @@ class QueryBuilder extends QueryBuilderBase { const op = this._operations[i]; if (op.constructor === SelectOperation) { - const selectionObj = op.findSelection(table, selection); + const selectionObj = op.findSelection(selection, table); noSelectStatements = false; if (selectionObj) { diff --git a/lib/queryBuilder/QueryBuilderBase.js b/lib/queryBuilder/QueryBuilderBase.js index 22313d801..6430c20d0 100644 --- a/lib/queryBuilder/QueryBuilderBase.js +++ b/lib/queryBuilder/QueryBuilderBase.js @@ -2,7 +2,7 @@ const QueryBuilderOperationSupport = require('./QueryBuilderOperationSupport'); const { isSqlite, isMsSql } = require('../utils/knexUtils'); const KnexOperation = require('./operations/KnexOperation'); -const SelectOperation = require('./operations/SelectOperation'); +const SelectOperation = require('./operations/select/SelectOperation'); const ReturningOperation = require('./operations/ReturningOperation'); const WhereCompositeOperation = require('./operations/WhereCompositeOperation'); const WhereInCompositeOperation = require('./operations/whereInComposite/WhereInCompositeOperation'); diff --git a/lib/queryBuilder/operations/RangeOperation.js b/lib/queryBuilder/operations/RangeOperation.js index cd9f94d6e..d75e8efc9 100644 --- a/lib/queryBuilder/operations/RangeOperation.js +++ b/lib/queryBuilder/operations/RangeOperation.js @@ -3,7 +3,7 @@ const QueryBuilderOperation = require('./QueryBuilderOperation'); class RangeOperation extends QueryBuilderOperation { constructor(name, opt) { super(name, opt); - this.resultSizePromise = null; + this.resultSizeBuilder = null; } onAdd(builder, args) { @@ -19,23 +19,17 @@ class RangeOperation extends QueryBuilderOperation { return true; } - onBefore1(builder) { - // Don't return the promise so that it is executed - // in parallel with the actual query. - this.resultSizePromise = builder.resultSize().reflect(); - return null; + onBefore1(builder, result) { + this.resultSizeBuilder = builder.clone(); + return super.onBefore1(builder, result); } - onAfter3(builder, result) { - return this.resultSizePromise.then(res => { - if (res.isFulfilled()) { - return { - results: result, - total: parseInt(res.value(), 10) - }; - } else { - return Promise.reject(res.reason()); - } + onAfter3(builder, results) { + return this.resultSizeBuilder.resultSize().then(resultSize => { + return { + results, + total: parseInt(resultSize, 10) + }; }); } } diff --git a/lib/queryBuilder/operations/SelectOperation.js b/lib/queryBuilder/operations/SelectOperation.js deleted file mode 100644 index 4d641acfc..000000000 --- a/lib/queryBuilder/operations/SelectOperation.js +++ /dev/null @@ -1,117 +0,0 @@ -const flatten = require('lodash/flatten'); -const ObjectionToKnexConvertingOperation = require('./ObjectionToKnexConvertingOperation'); - -const ALIAS_REGEX = /\s+as\s+/i; -const COUNT_REGEX = /count/i; - -class SelectOperation extends ObjectionToKnexConvertingOperation { - constructor(name, opt) { - super(name, opt); - - this.selections = []; - } - - static get Selection() { - return Selection; - } - - static parseSelection(selection) { - let dotIdx, table, column; - let alias = null; - - if (typeof selection !== 'string') { - return null; - } - - if (ALIAS_REGEX.test(selection)) { - const parts = selection.split(ALIAS_REGEX); - - selection = parts[0].trim(); - alias = parts[1].trim(); - } - - dotIdx = selection.lastIndexOf('.'); - - if (dotIdx !== -1) { - table = selection.substr(0, dotIdx); - column = selection.substr(dotIdx + 1); - } else { - table = null; - column = selection; - } - - return new this.Selection(table, column, alias); - } - - onAdd(builder, args) { - const selections = flatten(args); - - // Don't add an empty selection. Empty list is accepted for `count`, `countDistinct` - // etc. because knex apparently supports it. - if (selections.length === 0 && !COUNT_REGEX.test(this.name)) { - return false; - } - - const ret = super.onAdd(builder, selections); - - for (let i = 0, l = selections.length; i < l; ++i) { - const selection = SelectOperation.parseSelection(selections[i]); - - if (selection) { - this.selections.push(selection); - } - } - - return ret; - } - - onBuildKnex(knexBuilder) { - knexBuilder[this.name].apply(knexBuilder, this.args); - } - - findSelection(fromTable, selection) { - let testSelect = SelectOperation.parseSelection(selection); - - if (!testSelect) { - return null; - } - - if (!testSelect.table) { - testSelect.table = fromTable; - } - - for (let i = 0, l = this.selections.length; i < l; ++i) { - if (this.selections[i].selects(testSelect)) { - return this.selections[i]; - } - } - - return null; - } -} - -class Selection { - constructor(table, column, alias) { - this.table = table || null; - this.column = column || null; - this.alias = alias || null; - } - - static get SelectAll() { - return SELECT_ALL; - } - - get name() { - return this.alias || this.column; - } - - selects(that) { - const tablesMatch = that.table === this.table || this.table === null || that.table === null; - const colsMatch = this.column === that.column || this.column === '*'; - return tablesMatch && colsMatch; - } -} - -const SELECT_ALL = new Selection(null, '*'); - -module.exports = SelectOperation; diff --git a/lib/queryBuilder/operations/eager/RelationJoinBuilder.js b/lib/queryBuilder/operations/eager/RelationJoinBuilder.js index ddf6f075f..6ba45cc9b 100644 --- a/lib/queryBuilder/operations/eager/RelationJoinBuilder.js +++ b/lib/queryBuilder/operations/eager/RelationJoinBuilder.js @@ -36,33 +36,14 @@ class RelationJoinBuilder { * without this since it doesn't build selects. */ fetchColumnInfo(builder) { - const columnInfo = RelationJoinBuilder.columnInfo; const allModelClasses = findAllModels(this.expression, this.rootModelClass); return Promise.map( allModelClasses, - ModelClass => { - const table = builder.tableNameFor(ModelClass); - - if (columnInfo[table]) { - return columnInfo[table]; - } else { - columnInfo[table] = ModelClass.query() - .childQueryOf(builder) - .columnInfo() - .then(info => { - const result = { - columns: Object.keys(info) - }; - - columnInfo[table] = result; - return result; - }); - - return columnInfo[table]; - } - }, - { concurrency: this.rootModelClass.concurrency } + ModelClass => ModelClass.dbMetadata({ parentBuilder: builder }), + { + concurrency: this.rootModelClass.concurrency + } ); } @@ -316,17 +297,18 @@ class RelationJoinBuilder { const selects = []; const idCols = modelClass.getIdColumnArray(); const rootTable = builder.tableRefFor(this.rootModelClass); - const table = builder.tableNameFor(modelClass); let columns = selectFilterQuery.findAllSelections().map(it => it.name); const selectAllIndex = columns.indexOf('*'); // If there are no explicit selects, or there is a select all item, // we need to select all columns using the schema information - // in `RelationJoinBuilder.columnInfo`. + // in `modelClass.$$dbMetadata`. if (columns.length === 0 || selectAllIndex !== -1) { + const table = builder.tableNameFor(modelClass); + columns.splice(selectAllIndex, 1); - columns = RelationJoinBuilder.columnInfo[table].columns.concat(columns); + columns = modelClass.$$dbMetadata[table].columns.concat(columns); } // Id columns always need to be selected so that we are able to construct @@ -707,6 +689,4 @@ class OneToOnePathInfo extends PathInfo { } } -RelationJoinBuilder.columnInfo = Object.create(null); - module.exports = RelationJoinBuilder; diff --git a/lib/queryBuilder/operations/select/SelectOperation.js b/lib/queryBuilder/operations/select/SelectOperation.js new file mode 100644 index 000000000..00bebc754 --- /dev/null +++ b/lib/queryBuilder/operations/select/SelectOperation.js @@ -0,0 +1,64 @@ +const flatten = require('lodash/flatten'); +const Selection = require('./Selection'); +const ObjectionToKnexConvertingOperation = require('../ObjectionToKnexConvertingOperation'); + +const COUNT_REGEX = /count/i; + +class SelectOperation extends ObjectionToKnexConvertingOperation { + constructor(name, opt) { + super(name, opt); + this.selections = []; + } + + static get Selection() { + return Selection; + } + + static parseSelection(selection, table) { + return this.Selection.create(selection, table); + } + + onAdd(builder, args) { + const selections = flatten(args); + + // Don't add an empty selection. Empty list is accepted for `count`, `countDistinct` + // etc. because knex apparently supports it. + if (selections.length === 0 && !COUNT_REGEX.test(this.name)) { + return false; + } + + const ret = super.onAdd(builder, selections); + + for (let i = 0, l = selections.length; i < l; ++i) { + const selection = SelectOperation.parseSelection(selections[i]); + + if (selection) { + this.selections.push(selection); + } + } + + return ret; + } + + onBuildKnex(knexBuilder) { + knexBuilder[this.name].apply(knexBuilder, this.args); + } + + findSelection(selection, table) { + let testSelect = this.constructor.parseSelection(selection, table); + + if (!testSelect) { + return null; + } + + for (let i = 0, l = this.selections.length; i < l; ++i) { + if (this.selections[i].includes(testSelect)) { + return this.selections[i]; + } + } + + return null; + } +} + +module.exports = SelectOperation; diff --git a/lib/queryBuilder/operations/select/Selection.js b/lib/queryBuilder/operations/select/Selection.js new file mode 100644 index 000000000..7898e7f90 --- /dev/null +++ b/lib/queryBuilder/operations/select/Selection.js @@ -0,0 +1,57 @@ +const ALIAS_REGEX = /\s+as\s+/i; + +class Selection { + constructor(table, column, alias) { + this.table = table || null; + this.column = column || null; + this.alias = alias || null; + } + + static create(selection, table = null) { + let dotIdx, column; + let alias = null; + + if (typeof selection !== 'string') { + return null; + } + + if (ALIAS_REGEX.test(selection)) { + const parts = selection.split(ALIAS_REGEX); + + selection = parts[0].trim(); + alias = parts[1].trim(); + } + + dotIdx = selection.lastIndexOf('.'); + + if (dotIdx !== -1) { + table = selection.substr(0, dotIdx); + column = selection.substr(dotIdx + 1); + } else { + column = selection; + } + + return new this(table, column, alias); + } + + static get SelectAll() { + return SELECT_ALL; + } + + get name() { + return this.alias || this.column; + } + + // Test if this selection "includes" another selection. + // For example `foo.*` includes `foo.bar`. + includes(that) { + const tablesMatch = that.table === this.table || this.table === null; + const colsMatch = this.column === that.column || this.column === '*'; + + return tablesMatch && colsMatch; + } +} + +const SELECT_ALL = new Selection(null, '*'); + +module.exports = Selection; diff --git a/package.json b/package.json index 00959041d..8f6b327fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "objection", - "version": "1.0.0-rc.4", + "version": "1.0.0-rc.5", "description": "An SQL-friendly ORM for Node.js", "main": "lib/objection.js", "license": "MIT", diff --git a/tests/integration/eager.js b/tests/integration/eager.js index 7687e30f0..19d1def5a 100644 --- a/tests/integration/eager.js +++ b/tests/integration/eager.js @@ -1019,6 +1019,16 @@ module.exports = session => { }); }); + it('range should work with joinEager', () => { + return Model1.query() + .where('Model1.id', 1) + .joinEager('model1Relation1') + .range(0, 0) + .then(res => { + expect(res.results[0].model1Relation1.id).to.equal(2); + }); + }); + describe('JoinEagerAlgorithm', () => { it('select should work', () => { return Model1.query() diff --git a/tests/ts/examples.ts b/tests/ts/examples.ts index d89e5fc00..85e82c95c 100644 --- a/tests/ts/examples.ts +++ b/tests/ts/examples.ts @@ -1,6 +1,8 @@ // tslint:disable:no-unused-variable import * as knex from 'knex'; + import * as objection from '../../typings/objection'; +import { RelationMappings } from '../../typings/objection'; const { lit, raw, ref } = objection; @@ -16,21 +18,21 @@ class Person extends objection.Model { firstName: string; lastName: string; mom: Person; + children: Person[]; + pets: Animal[]; comments: Comment[]; + movies: Movie[]; static columnNameMappers = objection.snakeCaseMappers(); examplePersonMethod = (arg: string) => 1; - // $relatedQuery can either take a cast, if you don't want to add the field - // to your model: petsWithId(petId: number): Promise { - return this.$relatedQuery('pets').where('id', petId); + return this.$relatedQuery('pets').where('id', petId); } - // Or, if you add the field, this.$relatedQuery just works: - fetchMom(): Promise { - return this.$relatedQuery('mom'); + fetchMom(): Promise { + return this.$relatedQuery('mom').first(); } async $beforeInsert(queryContext: objection.QueryContext) { @@ -126,13 +128,14 @@ async () => { class Movie extends objection.Model { title: string; actors: Person[]; + director: Person; /** * This static field instructs Objection how to hydrate and persist * relations. By making relationMappings a thunk, we avoid require loops * caused by other class references. */ - static relationMappings = () => ({ + static relationMappings: RelationMappings = { actors: { relation: objection.Model.ManyToManyRelation, modelClass: Person, @@ -143,16 +146,34 @@ class Movie extends objection.Model { to: ref('Actors.personId').castInt() }, to: [ref('Person.id1'), 'Person.id2'] + }, + filter: qb => qb.orderByRaw('coalesce(title, id)') + }, + director: { + relation: objection.Model.BelongsToOneRelation, + modelClass: Person, + join: { + from: 'Movie.directorId', + to: 'Person.id' } } - }); + }; } async () => { // Another example of strongly-typed $relatedQuery without a cast: takesPeople(await new Movie().$relatedQuery('actors')); + takesPerson(await new Movie().$relatedQuery('director')); + + // If you need to do subsequent changes to $relatedQuery, though, you need + // to cast: :\ + takesMaybePerson(await new Movie().$relatedQuery('actors').first()); + takesMaybePerson(await new Movie().$relatedQuery('director').where("age", ">", 32)); }; +const relatedPersons: Promise = new Person().$relatedQuery('children'); +const relatedMovies: Promise = new Movie().$relatedQuery('actors'); + class Animal extends objection.Model { species: string; @@ -284,9 +305,23 @@ qb = qb.where(raw('random()', 1, '2')); qb = qb.where(Person.raw('random()', 1, '2', raw('3'))); qb = qb.alias('someAlias'); -// query builder hooks -qb = qb.runBefore(async (result: any, builder: objection.QueryBuilder) => {}); -qb = qb.runAfter(async (result: Person[], builder: objection.QueryBuilder) => {}); +// Query builder hooks. runBefore() and runAfter() don't immediately affect the result. + +const runBeforePerson: Promise = qb + .first() + .throwIfNotFound() + .runBefore(async (result: any, builder: objection.QueryBuilder) => 88); +const runBeforePersons: Promise = qb.runBefore( + async (result: any, builder: objection.QueryBuilder) => 88 +); + +const runAfterPerson: Promise = qb + .first() + .throwIfNotFound() + .runAfter(async (result: any, builder: objection.QueryBuilder) => 88); +const runAfterPersons: Promise = qb.runAfter( + async (result: any, builder: objection.QueryBuilder) => 88 +); // signature-changing QueryBuilder methods: @@ -355,6 +390,15 @@ const rowsPage: Promise<{ const rowsRange: Promise> = Person.query().range(1, 10); +const rowsPageRunAfter: Promise> = Person.query() + .page(1, 10) + .runAfter( + async ( + result: objection.Page, + builder: objection.QueryBuilder> + ) => {} + ); + // `retuning` should change the return value from number to T[] const rowsUpdateReturning: Promise = Person.query() .update({}) @@ -584,6 +628,132 @@ Person.query().where(ref('Model.jsonColumn:details'), '=', lit({ name: 'Jennifer Person.query().where('age', '>', lit(10)); Person.query().where('firstName', lit('Jennifer').castText()); +// Preserving result type after result type changing methods. + +qb = Person.query(); + +const findByIdSelect: Promise = qb.findById(32).select('firstName'); +const findByIdSelectThrow: Promise = qb + .findById(32) + .select('firstName') + .throwIfNotFound(); +const findByIdJoin: Promise = qb + .findById(32) + .join('tablename', 'column1', '=', 'column2'); +const findByIdJoinThrow: Promise = qb + .findById(32) + .join('tablename', 'column1', '=', 'column2') + .throwIfNotFound(); +const findByIdJoinRaw: Promise = qb.findById(32).joinRaw('raw sql'); +const findByIdJoinRawThrow: Promise = qb + .findById(32) + .joinRaw('raw sql') + .throwIfNotFound(); +const findOneWhere: Promise = qb + .findOne({ firstName: 'Mo' }) + .where('lastName', 'like', 'Mac%'); +const findOneWhereThrow: Promise = qb + .findOne({ firstName: 'Mo' }) + .where('lastName', 'like', 'Mac%') + .throwIfNotFound(); +const findOneSelect: Promise = qb + .findOne({ firstName: 'Mo' }) + .select('firstName'); +const findOneSelectThrow: Promise = qb + .findOne({ firstName: 'Mo' }) + .select('firstName') + .throwIfNotFound(); +const findOneWhereIn: Promise = qb + .findOne({ firstName: 'Mo' }) + .whereIn('status', ['active', 'pending']); +const findOneWhereInThrow: Promise = qb + .findOne({ firstName: 'Mo' }) + .whereIn('status', ['active', 'pending']) + .throwIfNotFound(); +const findOneWhereJson: Promise = qb + .findOne({ firstName: 'Mo' }) + .whereJsonSupersetOf('x:y', 'abc'); +const findOneWhereJsonThrow: Promise = qb + .findOne({ firstName: 'Mo' }) + .whereJsonSupersetOf('x:y', 'abc') + .throwIfNotFound(); +const findOneWhereJsonIsArray: Promise = qb + .findOne({ firstName: 'Mo' }) + .whereJsonIsArray('x:y'); +const findOneWhereJsonIsArrayThrow: Promise = qb + .findOne({ firstName: 'Mo' }) + .whereJsonIsArray('x:y') + .throwIfNotFound(); +const patchWhere: Promise = qb.patch({ firstName: 'Mo' }).where('id', 32); +const patchWhereIn: Promise = qb.patch({ firstName: 'Mo' }).whereIn('id', [1, 2, 3]); +const patchWhereJson: Promise = qb + .patch({ firstName: 'Mo' }) + .whereJsonSupersetOf('x:y', 'abc'); +const patchWhereJsonIsArray: Promise = qb + .patch({ firstName: 'Mo' }) + .whereJsonIsArray('x:y'); +const patchThrow: Promise = qb.patch({ firstName: 'Mo' }).throwIfNotFound(); +const updateWhere: Promise = qb.update({ firstName: 'Mo' }).where('id', 32); +const updateWhereIn: Promise = qb.update({ firstName: 'Mo' }).whereIn('id', [1, 2, 3]); +const updateWhereJson: Promise = qb + .update({ firstName: 'Mo' }) + .whereJsonSupersetOf('x:y', 'abc'); +const updateWhereJsonIsArray: Promise = qb + .update({ firstName: 'Mo' }) + .whereJsonIsArray('x:y'); +const updateThrow: Promise = qb.update({ firstName: 'Mo' }).throwIfNotFound(); +const deleteWhere: Promise = qb.delete().where('lastName', 'like', 'Mac%'); +const deleteWhereIn: Promise = qb.delete().whereIn('id', [1, 2, 3]); +const deleteThrow: Promise = qb.delete().throwIfNotFound(); +const deleteByIDThrow: Promise = qb.deleteById(32).throwIfNotFound(); + +// The location of `first` doesn't matter. + +const whereFirst: Promise = qb.where({ firstName: 'Mo' }).first(); +const firstWhere: Promise = qb.first().where({ firstName: 'Mo' }); + +// Returning restores the result to Model or Model[]. + +const whereInsertRet: Promise = qb + .where({ lastName: 'MacMoo' }) + .insert({ firstName: 'Mo' }) + .returning('dbGeneratedColumn'); +const whereMultiInsertRet: Promise = qb + .where({ lastName: 'MacMoo' }) + .insert([{ firstName: 'Mo' }, { firstName: 'Bob' }]) + .returning('dbGeneratedColumn'); +const whereUpdateRet: Promise = qb + .where({ lastName: 'MacMoo' }) + .update({ firstName: 'Bob' }) + .returning('dbGeneratedColumn'); +const wherePatchRet: Promise = qb + .where({ lastName: 'MacMoo' }) + .patch({ firstName: 'Mo' }) + .returning('age'); +const whereDelRetFirstWhere: Promise = qb + .delete() + .returning('lastName') + .first() + .where({ firstName: 'Mo' }); + +// Verify that Model.query() and model.$query() return the same type of query builder. +// Confirming this prevent us from having to duplicate the tests for each. + +async function checkQueryEquivalence() { + // Confirm that every $query() type is a query() type + + let staticQB = Person.query() + .first() + .throwIfNotFound(); + const person = await staticQB; + staticQB = person.$query(); + + // Confirm that every query() type is a $query() type + + let instanceQB = person.$query(); + instanceQB = staticQB; +} + // .query, .$query, and .$relatedQuery can take a Knex instance to support // multitenancy diff --git a/tests/unit/model/Model.js b/tests/unit/model/Model.js index f267213a0..542bfa647 100644 --- a/tests/unit/model/Model.js +++ b/tests/unit/model/Model.js @@ -407,6 +407,31 @@ describe('Model', () => { expect(model.c).not.to.equal(obj); }); + it('should merge default values from jsonSchema when validating a model instance', () => { + let obj = { a: 100, b: 200 }; + + Model1.jsonSchema = { + required: ['a'], + properties: { + a: { type: 'string', default: 'default string' }, + b: { type: 'number', default: 666 }, + c: { type: 'object', default: obj } + } + }; + + let model = Model1.fromJson({ a: 'str' }, { skipValidation: true }); + + expect(model.b).to.equal(undefined); + expect(model.c).to.equal(undefined); + + model.$validate(); + + expect(model.a).to.equal('str'); + expect(model.b).to.equal(666); + expect(model.c).to.eql(obj); + expect(model.c).not.to.equal(obj); + }); + // regression introduced in 0.6 // https://github.com/Vincit/objection.js/issues/205 it('should not throw TypeError when jsonSchema.properties == undefined', () => { @@ -616,6 +641,42 @@ describe('Model', () => { expect(json).to.have.property('relation2'); }); + it('should parse relations into Model instances if source that is being parsed is already a Model instance', () => { + let Model2 = modelClass('Model2'); + + Model1.relationMappings = { + relation1: { + relation: Model.HasManyRelation, + modelClass: Model2, + join: { + from: 'Model1.id', + to: 'Model2.model1Id' + } + }, + relation2: { + relation: Model.BelongsToOneRelation, + modelClass: Model1, + join: { + from: 'Model1.id', + to: 'Model1.model1Id' + } + } + }; + + let model = Model1.fromJson({ + id: 10, + model1Id: 13 + }); + model.relation1 = [{ id: 11, model1Id: 10 }, { id: 12, model1Id: 10 }]; + model.relation2 = { id: 13, model1Id: null }; + + let modelWithRelationships = Model1.fromJson(model); + + expect(modelWithRelationships.relation1[0]).to.be.a(Model2); + expect(modelWithRelationships.relation1[1]).to.be.a(Model2); + expect(modelWithRelationships.relation2).to.be.a(Model1); + }); + it('should NOT parse relations into Model instances if skipParseRelations option is given', () => { let Model2 = modelClass('Model2'); diff --git a/tests/unit/queryBuilder/QueryBuilder.js b/tests/unit/queryBuilder/QueryBuilder.js index b099908e3..ba1d6ac41 100644 --- a/tests/unit/queryBuilder/QueryBuilder.js +++ b/tests/unit/queryBuilder/QueryBuilder.js @@ -723,7 +723,7 @@ describe('QueryBuilder', () => { }); it('range should return a range and the total count', done => { - mockKnexQueryResults = [[{ count: 123 }], [{ a: 1 }]]; + mockKnexQueryResults = [[{ a: 1 }], [{ count: 123 }]]; QueryBuilder.forClass(TestModel) .where('test', 100) .orderBy('order') @@ -731,8 +731,8 @@ describe('QueryBuilder', () => { .then(res => { expect(executedQueries).to.have.length(2); expect(executedQueries).to.eql([ - 'select count(*) as "count" from (select "Model".* from "Model" where "test" = 100) as "temp"', - 'select "Model".* from "Model" where "test" = 100 order by "order" asc limit 101 offset 100' + 'select "Model".* from "Model" where "test" = 100 order by "order" asc limit 101 offset 100', + 'select count(*) as "count" from (select "Model".* from "Model" where "test" = 100) as "temp"' ]); expect(res.total).to.equal(123); expect(res.results).to.eql([{ a: 1 }]); @@ -742,7 +742,7 @@ describe('QueryBuilder', () => { }); it('page should return a page and the total count', done => { - mockKnexQueryResults = [[{ count: 123 }], [{ a: 1 }]]; + mockKnexQueryResults = [[{ a: 1 }], [{ count: 123 }]]; QueryBuilder.forClass(TestModel) .where('test', 100) .orderBy('order') @@ -750,8 +750,8 @@ describe('QueryBuilder', () => { .then(res => { expect(executedQueries).to.have.length(2); expect(executedQueries).to.eql([ - 'select count(*) as "count" from (select "Model".* from "Model" where "test" = 100) as "temp"', - 'select "Model".* from "Model" where "test" = 100 order by "order" asc limit 100 offset 1000' + 'select "Model".* from "Model" where "test" = 100 order by "order" asc limit 100 offset 1000', + 'select count(*) as "count" from (select "Model".* from "Model" where "test" = 100) as "temp"' ]); expect(res.total).to.equal(123); expect(res.results).to.eql([{ a: 1 }]); diff --git a/typings/objection/index.d.ts b/typings/objection/index.d.ts index 9991c2379..bfb456053 100644 --- a/typings/objection/index.d.ts +++ b/typings/objection/index.d.ts @@ -1,6 +1,18 @@ -// Type definitions for objection -// Project: Objection.js -// Definitions by: Matthew McEachen & Drew R. +// Type definitions for Objection.js +// Project: +// Contributions by: +// * Matthew McEachen +// * Sami Koskimäki +// * Mikael Lepistö +// * Joseph T Lapp +// * Drew R. +// * Karl Blomster +// * And many others: See + +// PLEASE NOTE, the generic type symbols in this file follow this definition: +// QM - queried model +// RM - candidate result model or model array +// RV - actual result value /// import * as knex from 'knex'; @@ -86,9 +98,9 @@ declare namespace Objection { postProcessResponse(response: any): any; } - export interface Page { + export interface Page { total: number; - results: T[]; + results: QM[]; } export interface ModelOptions { @@ -182,8 +194,8 @@ declare namespace Objection { relation: Relation; modelClass: ModelClass | string; join: RelationJoin; - modify?: (queryBuilder: QueryBuilder) => QueryBuilder; - filter?: (queryBuilder: QueryBuilder) => QueryBuilder; + modify?: (queryBuilder: QueryBuilder) => QueryBuilder; + filter?: (queryBuilder: QueryBuilder) => QueryBuilder; } export interface EagerAlgorithm { @@ -221,16 +233,16 @@ declare namespace Objection { */ type RelationExpression = string; - interface FilterFunction { - (queryBuilder: QueryBuilder): void; + interface FilterFunction { + (queryBuilder: QueryBuilder): void; } - interface FilterExpression { - [namedFilter: string]: FilterFunction; + interface FilterExpression { + [namedFilter: string]: FilterFunction; } - interface RelationExpressionMethod { - (relationExpression: RelationExpression): QueryBuilder; + interface RelationExpressionMethod { + (relationExpression: RelationExpression): QueryBuilder; } interface TraverserFunction { @@ -254,39 +266,39 @@ declare namespace Objection { } interface JoinRelation { - (relationName: string, opt?: RelationOptions): QueryBuilder; + (relationName: string, opt?: RelationOptions): QueryBuilder; } type JsonObjectOrFieldExpression = object | object[] | FieldExpression; - interface WhereJson { + interface WhereJson { ( fieldExpression: FieldExpression, jsonObjectOrFieldExpression: JsonObjectOrFieldExpression - ): QueryBuilder; + ): QueryBuilder; } - interface WhereFieldExpression { - (fieldExpression: FieldExpression): QueryBuilder; + interface WhereFieldExpression { + (fieldExpression: FieldExpression): QueryBuilder; } - interface WhereJsonExpression { - (fieldExpression: FieldExpression, keys: string | string[]): QueryBuilder; + interface WhereJsonExpression { + (fieldExpression: FieldExpression, keys: string | string[]): QueryBuilder; } - interface WhereJsonField { + interface WhereJsonField { ( fieldExpression: FieldExpression, operator: string, value: boolean | number | string | null - ): QueryBuilder; + ): QueryBuilder; } - interface ModifyEager { - ( + interface ModifyEager { + ( relationExpression: string | RelationExpression, - modifier: (builder: QueryBuilder) => void - ): QueryBuilder; + modifier: (builder: QueryBuilder) => void + ): QueryBuilder; } interface BluebirdMapper { @@ -297,8 +309,8 @@ declare namespace Objection { (err: any, result?: any): void; } - interface Filters { - [filterName: string]: (queryBuilder: QueryBuilder) => void; + interface Filters { + [filterName: string]: (queryBuilder: QueryBuilder) => void; } interface Properties { @@ -339,9 +351,9 @@ declare namespace Objection { ManyToManyRelation: Relation; HasOneThroughRelation: Relation; - query(trxOrKnex?: Transaction | knex): QueryBuilder; + query(trxOrKnex?: Transaction | knex): QueryBuilder; // This can only be used as a subquery so the result model type is irrelevant. - relatedQuery(relationName: string): QueryBuilder; + relatedQuery(relationName: string): QueryBuilder; knex(knex?: knex): knex; knexQuery(): knex.QueryBuilder; @@ -405,29 +417,29 @@ declare namespace Objection { static WhereInEagerAlgorithm: EagerAlgorithm; static NaiveEagerAlgorithm: EagerAlgorithm; - static query(this: Constructor, trxOrKnex?: Transaction | knex): QueryBuilder; + static query(this: Constructor, trxOrKnex?: Transaction | knex): QueryBuilder; // This can only be used as a subquery so the result model type is irrelevant. - static relatedQuery(relationName: string): QueryBuilder; + static relatedQuery(relationName: string): QueryBuilder; static knex(knex?: knex): knex; static knexQuery(): knex.QueryBuilder; - static bindKnex(this: T, knex: knex): T; - static bindTransaction(this: T, transaction: Transaction): T; + static bindKnex(this: M, knex: knex): M; + static bindTransaction(this: M, transaction: Transaction): M; static createValidator(): Validator; static createValidationError(args: CreateValidationErrorArgs): Error; // fromJson and fromDatabaseJson both return an instance of Model, not a Model class: - static fromJson(this: Constructor, json: Pojo, opt?: ModelOptions): T; - static fromDatabaseJson(this: Constructor, row: Pojo): T; + static fromJson(this: Constructor, json: Pojo, opt?: ModelOptions): M; + static fromDatabaseJson(this: Constructor, row: Pojo): M; static omitImpl(f: (obj: object, prop: string) => void): void; - static loadRelated( - this: Constructor, - models: (T | object)[], + static loadRelated( + this: Constructor, + models: (QM | object)[], expression: RelationExpression, - filters?: Filters, + filters?: Filters, trxOrKnex?: Transaction | knex - ): Promise; + ): Promise; static traverse( filterConstructor: typeof Model, @@ -452,13 +464,13 @@ declare namespace Objection { $formatJson(json: Pojo): Pojo; $setJson(json: Pojo, opt?: ModelOptions): this; $setDatabaseJson(json: Pojo): this; - $setRelated( + $setRelated( relation: String | Relation, - related: M | M[] | null | undefined + related: RelatedM | RelatedM[] | null | undefined ): this; - $appendRelated( + $appendRelated( relation: String | Relation, - related: M | M[] | null | undefined + related: RelatedM | RelatedM[] | null | undefined ): this; $set(obj: Pojo): this; @@ -469,34 +481,35 @@ declare namespace Objection { /** * AKA `reload` in ActiveRecord parlance */ - $query(trxOrKnex?: Transaction | knex): QueryBuilderSingle; + $query(trxOrKnex?: Transaction | knex): QueryBuilder; /** - * If you add model relations as fields, $relatedQuery works - * automatically: + * If you add fields to your model, you get $relatedQuery typings for + * free. + * + * Note that if you make any chained calls to the QueryBuilder, + * though, you should apply a cast, which will make your code use not this + * signatue, but the following signature. */ - $relatedQuery( + $relatedQuery( relationName: K, trxOrKnex?: Transaction | knex - ): QueryBuilderSingle; - - // PLEASE NOTE: `$relatedQuery` MUST BE DEFINED - // BEFORE `$relatedQuery`! + ): QueryBuilder; /** * If you don't want to add the fields to your model, you can cast the * call to the expected Model subclass (`$relatedQuery('pets')`). */ - $relatedQuery( - relationName: string, + $relatedQuery( + relationName: keyof this | string, trxOrKnex?: Transaction | knex - ): QueryBuilder; + ): QueryBuilder; - $loadRelated( + $loadRelated( expression: keyof this | RelationExpression, - filters?: Filters, + filters?: Filters, trxOrKnex?: Transaction | knex - ): QueryBuilderSingle; + ): QueryBuilder; $traverse(traverser: TraverserFunction): void; $traverse(filterConstructor: this, traverser: TraverserFunction): void; @@ -513,164 +526,96 @@ declare namespace Objection { $afterDelete(queryContext: QueryContext): Promise | void; } - export class QueryBuilder { + export class QueryBuilder { static forClass(modelClass: ModelClass): QueryBuilder; } - export interface ThrowIfNotFound { - throwIfNotFound(): this; + export interface Executable extends Promise { + execute(): Promise; } - export interface Executable extends Promise { - execute(): Promise; + export interface QueryBuilder + extends QueryBuilderBase, + Executable { + throwIfNotFound(): QueryBuilder; } - /** - * QueryBuilder with one expected result - */ - export interface QueryBuilderSingle - extends QueryBuilderBase, - ThrowIfNotFound, - Executable { - runAfter(fn: (result: T, builder: this) => any): this; - } + export interface QueryBuilderYieldingOneOrNone extends QueryBuilder {} - /** - * Query builder for update operations - */ - export interface QueryBuilderUpdate - extends QueryBuilderBase, - ThrowIfNotFound, - Executable { - returning(columns: string | string[]): QueryBuilder; - runAfter(fn: (result: number, builder: this) => any): this; - } - - /** - * Query builder for delete operations - */ - export interface QueryBuilderDelete - extends QueryBuilderBase, - ThrowIfNotFound, + export interface QueryBuilderYieldingCount + extends QueryBuilderBase, Executable { - returning(columns: string | string[]): QueryBuilder; - runAfter(fn: (result: number, builder: this) => any): this; - } - - /** - * Query builder for batch insert operations - */ - export interface QueryBuilderInsert - extends QueryBuilderBase, - ThrowIfNotFound, - Executable { - returning(columns: string | string[]): this; - runAfter(fn: (result: T[], builder: this) => any): this; - } - - /** - * Query builder for single insert operations - */ - export interface QueryBuilderInsertSingle - extends QueryBuilderBase, - ThrowIfNotFound, - Executable { - returning(columns: string | string[]): this; - runAfter(fn: (result: T, builder: this) => any): this; - } - - /** - * QueryBuilder with zero or one expected result - * (Using the Scala `Option` terminology) - */ - export interface QueryBuilderOption extends QueryBuilderBase, Executable { - throwIfNotFound(): QueryBuilderSingle; - runAfter(fn: (result: T | undefined, builder: this) => any): this; - } - - /** - * QueryBuilder with zero or more expected results - */ - export interface QueryBuilder extends QueryBuilderBase, ThrowIfNotFound, Executable { - runAfter(fn: (result: T[], builder: this) => any): this; + throwIfNotFound(): this; } - /** - * QueryBuilder with a page result. - */ - export interface QueryBuilderPage - extends QueryBuilderBase, - ThrowIfNotFound, - Executable> {} - - interface Insert { - (modelsOrObjects?: Partial[]): QueryBuilderInsert; - (modelOrObject?: Partial): QueryBuilderInsertSingle; + interface Insert { + (modelsOrObjects?: Partial[]): QueryBuilder; + (modelOrObject?: Partial): QueryBuilder; (): this; } - interface InsertGraph { - (modelsOrObjects?: Partial[], options?: InsertGraphOptions): QueryBuilderInsert; - (modelOrObject?: Partial, options?: InsertGraphOptions): QueryBuilderInsertSingle; + interface InsertGraph { + (modelsOrObjects?: Partial[], options?: InsertGraphOptions): QueryBuilder; + (modelOrObject?: Partial, options?: InsertGraphOptions): QueryBuilder; (): this; } - interface Upsert { - (modelsOrObjects?: Partial[], options?: UpsertOptions): QueryBuilder; - (modelOrObject?: Partial, options?: UpsertOptions): QueryBuilderSingle; + interface Upsert { + (modelsOrObjects?: Partial[], options?: UpsertOptions): QueryBuilder; + (modelOrObject?: Partial, options?: UpsertOptions): QueryBuilder; } - interface InsertGraphAndFetch { - (modelsOrObjects?: Partial, options?: InsertGraphOptions): QueryBuilderInsertSingle; - (modelsOrObjects?: Partial[], options?: InsertGraphOptions): QueryBuilderInsert; + interface InsertGraphAndFetch { + (modelsOrObjects?: Partial, options?: InsertGraphOptions): QueryBuilder; + (modelsOrObjects?: Partial[], options?: InsertGraphOptions): QueryBuilder; } - interface QueryBuilderBase extends QueryInterface { + interface QueryBuilderBase extends QueryInterface { modify(func: (builder: this) => void): this; modify(namedFilter: string): this; - findById(id: Id): QueryBuilderOption; + findById(id: Id): QueryBuilderYieldingOneOrNone; findById(idOrIds: IdOrIds): this; findByIds(ids: Id[] | Id[][]): this; /** findOne is shorthand for .where(...whereArgs).first() */ - findOne: FindOne; + findOne: FindOne; - insert: Insert; - insertAndFetch(modelOrObject: Partial): QueryBuilderInsertSingle; - insertAndFetch(modelsOrObjects?: Partial[]): QueryBuilderInsert; + insert: Insert; + insertAndFetch(modelOrObject: Partial): QueryBuilder; + insertAndFetch(modelsOrObjects?: Partial[]): QueryBuilder; - insertGraph: InsertGraph; - insertGraphAndFetch: InsertGraphAndFetch; + insertGraph: InsertGraph; + insertGraphAndFetch: InsertGraphAndFetch; /** * insertWithRelated is an alias for insertGraph. */ - insertWithRelated: InsertGraph; - insertWithRelatedAndFetch: InsertGraphAndFetch; + insertWithRelated: InsertGraph; + insertWithRelatedAndFetch: InsertGraphAndFetch; /** * @return a Promise of the number of updated rows */ - update(modelOrObject: Partial): QueryBuilderUpdate; - updateAndFetch(modelOrObject: Partial): QueryBuilderSingle; - updateAndFetchById(id: Id, modelOrObject: Partial): QueryBuilderSingle; + update(modelOrObject: Partial): QueryBuilderYieldingCount; + updateAndFetch(modelOrObject: Partial): QueryBuilder; + updateAndFetchById(id: Id, modelOrObject: Partial): QueryBuilder; /** * @return a Promise of the number of patched rows */ - patch(modelOrObject: Partial): QueryBuilderUpdate; - patchAndFetchById(id: Id, modelOrObject: Partial): QueryBuilderSingle; - patchAndFetch(modelOrObject: Partial): QueryBuilderSingle; + patch(modelOrObject: Partial): QueryBuilderYieldingCount; + patchAndFetchById(id: Id, modelOrObject: Partial): QueryBuilder; + patchAndFetch(modelOrObject: Partial): QueryBuilder; - upsertGraph: Upsert; - upsertGraphAndFetch: Upsert; + upsertGraph: Upsert; + upsertGraphAndFetch: Upsert; /** * @return a Promise of the number of deleted rows */ - deleteById(idOrIds: IdOrIds): QueryBuilderDelete; + deleteById(idOrIds: IdOrIds): QueryBuilderYieldingCount; - relate(ids: IdOrIds | Partial | Partial[]): this; + relate(ids: IdOrIds | Partial | Partial[]): this; unrelate(): this; forUpdate(): this; @@ -695,41 +640,49 @@ declare namespace Objection { // The Objection documentation incorrectly states this returns a QueryBuilder. columnInfo(column?: string): Promise; - whereComposite(column: ColumnRef, value: Value | QueryBuilder): this; - whereComposite(column: ColumnRef[], value: Value[] | QueryBuilder): this; - whereComposite(column: ColumnRef, operator: string, value: Value | QueryBuilder): this; - whereComposite(column: ColumnRef[], operator: string, value: Value[] | QueryBuilder): this; - whereInComposite(column: ColumnRef, values: Value[] | QueryBuilder): this; + whereComposite(column: ColumnRef, value: Value | QueryBuilder): this; + whereComposite(column: ColumnRef[], value: Value[] | QueryBuilder): this; + whereComposite( + column: ColumnRef, + operator: string, + value: Value | QueryBuilder + ): this; + whereComposite( + column: ColumnRef[], + operator: string, + value: Value[] | QueryBuilder + ): this; + whereInComposite(column: ColumnRef, values: Value[] | QueryBuilder): this; - whereJsonSupersetOf: WhereJson; - orWhereJsonSupersetOf: WhereJson; + whereJsonSupersetOf: WhereJson; + orWhereJsonSupersetOf: WhereJson; - whereJsonNotSupersetOf: WhereJson; - orWhereJsonNotSupersetOf: WhereJson; + whereJsonNotSupersetOf: WhereJson; + orWhereJsonNotSupersetOf: WhereJson; - whereJsonSubsetOf: WhereJson; - orWhereJsonSubsetOf: WhereJson; + whereJsonSubsetOf: WhereJson; + orWhereJsonSubsetOf: WhereJson; - whereJsonNotSubsetOf: WhereJson; - orWhereJsonNotSubsetOf: WhereJson; + whereJsonNotSubsetOf: WhereJson; + orWhereJsonNotSubsetOf: WhereJson; - whereJsonIsArray: WhereFieldExpression; - orWhereJsonIsArray: WhereFieldExpression; + whereJsonIsArray: WhereFieldExpression; + orWhereJsonIsArray: WhereFieldExpression; - whereJsonNotArray: WhereFieldExpression; - orWhereJsonNotArray: WhereFieldExpression; + whereJsonNotArray: WhereFieldExpression; + orWhereJsonNotArray: WhereFieldExpression; - whereJsonIsObject: WhereFieldExpression; - orWhereJsonIsObject: WhereFieldExpression; + whereJsonIsObject: WhereFieldExpression; + orWhereJsonIsObject: WhereFieldExpression; - whereJsonNotObject: WhereFieldExpression; - orWhereJsonNotObject: WhereFieldExpression; + whereJsonNotObject: WhereFieldExpression; + orWhereJsonNotObject: WhereFieldExpression; - whereJsonHasAny: WhereJsonExpression; - orWhereJsonHasAny: WhereJsonExpression; + whereJsonHasAny: WhereJsonExpression; + orWhereJsonHasAny: WhereJsonExpression; - whereJsonHasAll: WhereJsonExpression; - orWhereJsonHasAll: WhereJsonExpression; + whereJsonHasAll: WhereJsonExpression; + orWhereJsonHasAll: WhereJsonExpression; // Non-query methods: @@ -751,27 +704,28 @@ declare namespace Objection { hasSelects(): boolean; hasEager(): boolean; - runBefore(fn: (result: any, builder: this) => any): this; + runBefore(fn: (result: any, builder: QueryBuilder) => any): this; + runAfter(fn: (result: any, builder: QueryBuilder) => any): this; onBuild(fn: (builder: this) => void): this; onError(fn: (error: Error, builder: this) => any): this; eagerAlgorithm(algo: EagerAlgorithm): this; - eager(relationExpression: RelationExpression, filters?: FilterExpression): this; - mergeEager(relationExpression: RelationExpression, filters?: FilterExpression): this; + eager(relationExpression: RelationExpression, filters?: FilterExpression): this; + mergeEager(relationExpression: RelationExpression, filters?: FilterExpression): this; - joinEager(relationExpression: RelationExpression, filters?: FilterExpression): this; - mergeJoinEager(relationExpression: RelationExpression, filters?: FilterExpression): this; + joinEager(relationExpression: RelationExpression, filters?: FilterExpression): this; + mergeJoinEager(relationExpression: RelationExpression, filters?: FilterExpression): this; - naiveEager(relationExpression: RelationExpression, filters?: FilterExpression): this; - mergeNaiveEager(relationExpression: RelationExpression, filters?: FilterExpression): this; + naiveEager(relationExpression: RelationExpression, filters?: FilterExpression): this; + mergeNaiveEager(relationExpression: RelationExpression, filters?: FilterExpression): this; - allowEager: RelationExpressionMethod; - modifyEager: ModifyEager; - filterEager: ModifyEager; + allowEager: RelationExpressionMethod; + modifyEager: ModifyEager; + filterEager: ModifyEager; - allowInsert: RelationExpressionMethod; - allowUpsert: RelationExpressionMethod; + allowInsert: RelationExpressionMethod; + allowUpsert: RelationExpressionMethod; modelClass(): typeof Model; @@ -789,19 +743,19 @@ declare namespace Objection { map(mapper: BluebirdMapper): Promise; return(returnValue: V): Promise; - bind(context: any): Promise; - reflect(): Promise; + bind(context: any): Promise; + reflect(): Promise; - asCallback(callback: NodeStyleCallback): Promise; + asCallback(callback: NodeStyleCallback): Promise; - nodeify(callback: NodeStyleCallback): Promise; + nodeify(callback: NodeStyleCallback): Promise; resultSize(): Promise; - page(page: number, pageSize: number): QueryBuilderPage; - range(start: number, end: number): QueryBuilderPage; + page(page: number, pageSize: number): QueryBuilder>; + range(start: number, end: number): QueryBuilder>; pluck(propertyName: string): this; - first(): QueryBuilderOption; + first(): QueryBuilderYieldingOneOrNone; alias(alias: string): this; tableRefFor(modelClass: ModelClass): string; @@ -814,6 +768,8 @@ declare namespace Objection { omit(modelClass: typeof Model, properties: string[]): this; omit(properties: string[]): this; + + returning(columns: string | string[]): QueryBuilder; } export interface transaction { @@ -911,101 +867,101 @@ declare namespace Objection { | Buffer | Raw | Literal; - type ColumnRef = string | Raw | Reference | QueryBuilder; - type TableName = string | Raw | Reference | QueryBuilder; - - interface QueryInterface { - select: Select; - as: As; - columns: Select; - column: Select; - from: Table; - into: Table; - table: Table; - distinct: Distinct; + type ColumnRef = string | Raw | Reference | QueryBuilder; + type TableName = string | Raw | Reference | QueryBuilder; + + interface QueryInterface { + select: Select; + as: As; + columns: Select; + column: Select; + from: Table; + into: Table; + table: Table; + distinct: Distinct; // Joins - join: Join; - joinRaw: JoinRaw; - innerJoin: Join; - leftJoin: Join; - leftOuterJoin: Join; - rightJoin: Join; - rightOuterJoin: Join; - outerJoin: Join; - fullOuterJoin: Join; - crossJoin: Join; + join: Join; + joinRaw: JoinRaw; + innerJoin: Join; + leftJoin: Join; + leftOuterJoin: Join; + rightJoin: Join; + rightOuterJoin: Join; + outerJoin: Join; + fullOuterJoin: Join; + crossJoin: Join; // Withs - with: With; - withRaw: WithRaw; - withWrapped: WithWrapped; + with: With; + withRaw: WithRaw; + withWrapped: WithWrapped; // Wheres - where: Where; - andWhere: Where; - orWhere: Where; - whereNot: Where; - andWhereNot: Where; - orWhereNot: Where; - whereRaw: WhereRaw; - orWhereRaw: WhereRaw; - andWhereRaw: WhereRaw; - whereWrapped: WhereWrapped; - havingWrapped: WhereWrapped; - whereExists: WhereExists; - orWhereExists: WhereExists; - whereNotExists: WhereExists; - orWhereNotExists: WhereExists; - whereIn: WhereIn; - orWhereIn: WhereIn; - whereNotIn: WhereIn; - orWhereNotIn: WhereIn; - whereNull: WhereNull; - orWhereNull: WhereNull; - whereNotNull: WhereNull; - orWhereNotNull: WhereNull; - whereBetween: WhereBetween; - orWhereBetween: WhereBetween; - andWhereBetween: WhereBetween; - whereNotBetween: WhereBetween; - orWhereNotBetween: WhereBetween; - andWhereNotBetween: WhereBetween; + where: Where; + andWhere: Where; + orWhere: Where; + whereNot: Where; + andWhereNot: Where; + orWhereNot: Where; + whereRaw: WhereRaw; + orWhereRaw: WhereRaw; + andWhereRaw: WhereRaw; + whereWrapped: WhereWrapped; + havingWrapped: WhereWrapped; + whereExists: WhereExists; + orWhereExists: WhereExists; + whereNotExists: WhereExists; + orWhereNotExists: WhereExists; + whereIn: WhereIn; + orWhereIn: WhereIn; + whereNotIn: WhereIn; + orWhereNotIn: WhereIn; + whereNull: WhereNull; + orWhereNull: WhereNull; + whereNotNull: WhereNull; + orWhereNotNull: WhereNull; + whereBetween: WhereBetween; + orWhereBetween: WhereBetween; + andWhereBetween: WhereBetween; + whereNotBetween: WhereBetween; + orWhereNotBetween: WhereBetween; + andWhereNotBetween: WhereBetween; // Group by - groupBy: GroupBy; - groupByRaw: RawMethod; + groupBy: GroupBy; + groupByRaw: RawMethod; // Order by - orderBy: OrderBy; - orderByRaw: RawMethod; + orderBy: OrderBy; + orderByRaw: RawMethod; // Union - union: Union; + union: Union; unionAll(callback: () => void): this; // Having - having: Where; - andHaving: Where; - orHaving: Where; - havingRaw: WhereRaw; - orHavingRaw: WhereRaw; - havingIn: WhereIn; - orHavingIn: WhereIn; - havingNotIn: WhereIn; - orHavingNotIn: WhereIn; - havingNull: WhereNull; - orHavingNull: WhereNull; - havingNotNull: WhereNull; - orHavingNotNull: WhereNull; - havingExists: WhereExists; - orHavingExists: WhereExists; - havingNotExists: WhereExists; - orHavingNotExists: WhereExists; - havingBetween: WhereBetween; - orHavingBetween: WhereBetween; - havingNotBetween: WhereBetween; - orHavingNotBetween: WhereBetween; + having: Where; + andHaving: Where; + orHaving: Where; + havingRaw: WhereRaw; + orHavingRaw: WhereRaw; + havingIn: WhereIn; + orHavingIn: WhereIn; + havingNotIn: WhereIn; + orHavingNotIn: WhereIn; + havingNull: WhereNull; + orHavingNull: WhereNull; + havingNotNull: WhereNull; + orHavingNotNull: WhereNull; + havingExists: WhereExists; + orHavingExists: WhereExists; + havingNotExists: WhereExists; + orHavingNotExists: WhereExists; + havingBetween: WhereBetween; + orHavingBetween: WhereBetween; + havingNotBetween: WhereBetween; + orHavingNotBetween: WhereBetween; // Clear clearSelect(): this; @@ -1030,8 +986,8 @@ declare namespace Objection { debug(enabled?: boolean): this; pluck(column: string): this; - del(): QueryBuilderDelete; - delete(): QueryBuilderDelete; + del(): QueryBuilderYieldingCount; + delete(): QueryBuilderYieldingCount; truncate(): this; transacting(trx: Transaction): this; @@ -1040,145 +996,159 @@ declare namespace Objection { clone(): this; } - interface As { - (alias: string): QueryBuilder; + interface As { + (alias: string): QueryBuilder; } - interface Select extends ColumnNamesMethod {} + interface Select extends ColumnNamesMethod {} - interface Table { - (tableName: TableName): QueryBuilder; - (callback: (queryBuilder: QueryBuilder) => void): QueryBuilder; + interface Table { + (tableName: TableName): QueryBuilder; + (callback: (queryBuilder: QueryBuilder) => void): QueryBuilder; } - interface Distinct extends ColumnNamesMethod {} + interface Distinct extends ColumnNamesMethod {} - interface Join { - (raw: Raw): QueryBuilder; + interface Join { + (raw: Raw): QueryBuilder; ( tableName: TableName, clause: (this: knex.JoinClause, join: knex.JoinClause) => void - ): QueryBuilder; + ): QueryBuilder; ( tableName: TableName, columns: { [key: string]: string | number | Raw | Reference } - ): QueryBuilder; - (tableName: TableName, raw: Raw): QueryBuilder; - (tableName: TableName, column1: ColumnRef, column2: ColumnRef): QueryBuilder; + ): QueryBuilder; + (tableName: TableName, raw: Raw): QueryBuilder; + (tableName: TableName, column1: ColumnRef, column2: ColumnRef): QueryBuilder; (tableName: TableName, column1: ColumnRef, operator: string, column2: ColumnRef): QueryBuilder< - T + QM, + RM, + RV >; } - interface JoinRaw { - (sql: string, bindings?: any): QueryBuilder; + interface JoinRaw { + (sql: string, bindings?: any): QueryBuilder; } - interface With extends WithRaw, WithWrapped {} + interface With extends WithRaw, WithWrapped {} - interface WithRaw { - (alias: string, raw: Raw): QueryBuilder; + interface WithRaw { + (alias: string, raw: Raw): QueryBuilder; join: knex.JoinClause; - (alias: string, sql: string, bindings?: any): QueryBuilder; + (alias: string, sql: string, bindings?: any): QueryBuilder; } - interface WithWrapped { - (alias: string, callback: (queryBuilder: QueryBuilder) => any): QueryBuilder; + interface WithWrapped { + (alias: string, callback: (queryBuilder: QueryBuilder) => any): QueryBuilder< + QM, + RM, + RV + >; } - interface Where extends WhereRaw { - (callback: (queryBuilder: QueryBuilder) => void): QueryBuilder; - (object: object): QueryBuilder; - (column: ColumnRef, value: Value | Reference | QueryBuilder): QueryBuilder; + interface Where extends WhereRaw { + (callback: (queryBuilder: QueryBuilder) => void): QueryBuilder; + (object: object): QueryBuilder; ( - column: ColumnRef, + column: keyof QM | ColumnRef, + value: Value | Reference | QueryBuilder + ): QueryBuilder; + ( + column: keyof QM | ColumnRef, operator: string, - value: Value | Reference | QueryBuilder - ): QueryBuilder; + value: Value | Reference | QueryBuilder + ): QueryBuilder; ( column: ColumnRef, - callback: (this: QueryBuilder, queryBuilder: QueryBuilder) => void - ): QueryBuilder; + callback: (this: QueryBuilder, queryBuilder: QueryBuilder) => void + ): QueryBuilder; } - interface FindOne { - (condition: boolean): QueryBuilderOption; - (callback: (queryBuilder: QueryBuilder) => void): QueryBuilderOption; - (object: object): QueryBuilderOption; - (sql: string, ...bindings: any[]): QueryBuilderOption; - (sql: string, bindings: any): QueryBuilderOption; - (column: ColumnRef, value: Value | Reference | QueryBuilder): QueryBuilderOption; + interface FindOne { + (condition: boolean): QueryBuilderYieldingOneOrNone; + (callback: (queryBuilder: QueryBuilder) => void): QueryBuilderYieldingOneOrNone; + (object: object): QueryBuilderYieldingOneOrNone; + (sql: string, ...bindings: any[]): QueryBuilderYieldingOneOrNone; + (sql: string, bindings: any): QueryBuilderYieldingOneOrNone; + ( + column: ColumnRef, + value: Value | Reference | QueryBuilder + ): QueryBuilderYieldingOneOrNone; ( column: ColumnRef, operator: string, - value: Value | Reference | QueryBuilder - ): QueryBuilderOption; + value: Value | Reference | QueryBuilder + ): QueryBuilderYieldingOneOrNone; ( column: ColumnRef, - callback: (this: QueryBuilder, queryBuilder: QueryBuilder) => void - ): QueryBuilderOption; + callback: (this: QueryBuilder, queryBuilder: QueryBuilder) => void + ): QueryBuilderYieldingOneOrNone; } - interface WhereRaw extends RawMethod { - (condition: boolean): QueryBuilder; + interface WhereRaw extends RawMethod { + (condition: boolean): QueryBuilder; } - interface WhereWrapped { - (callback: (queryBuilder: QueryBuilder) => void): QueryBuilder; + interface WhereWrapped { + (callback: (queryBuilder: QueryBuilder) => void): QueryBuilder; } - interface WhereNull { - (column: ColumnRef): QueryBuilder; + interface WhereNull { + (column: ColumnRef): QueryBuilder; } - interface WhereIn { - (column: ColumnRef, values: Value[]): QueryBuilder; + interface WhereIn { + (column: ColumnRef, values: Value[]): QueryBuilder; ( column: ColumnRef, - callback: (this: QueryBuilder, queryBuilder: QueryBuilder) => void - ): QueryBuilder; - (column: ColumnRef, query: QueryBuilder): QueryBuilder; + callback: (this: QueryBuilder, queryBuilder: QueryBuilder) => void + ): QueryBuilder; + (column: ColumnRef, query: QueryBuilder): QueryBuilder; } - interface WhereBetween { - (column: ColumnRef, range: [Value, Value]): QueryBuilder; + interface WhereBetween { + (column: ColumnRef, range: [Value, Value]): QueryBuilder; } - interface WhereExists { - (callback: (this: QueryBuilder, queryBuilder: QueryBuilder) => void): QueryBuilder; - (query: QueryBuilder): QueryBuilder; - (raw: Raw): QueryBuilder; + interface WhereExists { + ( + callback: (this: QueryBuilder, queryBuilder: QueryBuilder) => void + ): QueryBuilder; + (query: QueryBuilder): QueryBuilder; + (raw: Raw): QueryBuilder; } - interface GroupBy extends RawMethod, ColumnNamesMethod {} + interface GroupBy extends RawMethod, ColumnNamesMethod {} - interface OrderBy { - (column: ColumnRef, direction?: 'ASC' | 'DESC' | 'asc' | 'desc'): QueryBuilder; + interface OrderBy { + (column: ColumnRef, direction?: string): QueryBuilder; } - interface Union { - (callback: () => void, wrap?: boolean): QueryBuilder; - (callbacks: (() => void)[], wrap?: boolean): QueryBuilder; - (...callbacks: (() => void)[]): QueryBuilder; + interface Union { + (callback: () => void, wrap?: boolean): QueryBuilder; + (callbacks: (() => void)[], wrap?: boolean): QueryBuilder; + (...callbacks: (() => void)[]): QueryBuilder; } // commons - interface ColumnNamesMethod { - (...columnNames: ColumnRef[]): QueryBuilder; - (columnNames: ColumnRef[]): QueryBuilder; + interface ColumnNamesMethod { + (...columnNames: ColumnRef[]): QueryBuilder; + (columnNames: ColumnRef[]): QueryBuilder; } - interface RawMethod { - (sql: string, ...bindings: any[]): QueryBuilder; - (sql: string, bindings: any): QueryBuilder; - (raw: Raw): QueryBuilder; + interface RawMethod { + (sql: string, ...bindings: any[]): QueryBuilder; + (sql: string, bindings: any): QueryBuilder; + (raw: Raw): QueryBuilder; } interface Transaction extends knex { savepoint(transactionScope: (trx: Transaction) => any): Promise; - commit(value?: any): Promise; - rollback(error?: Error): Promise; + commit(value?: any): Promise; + rollback(error?: Error): Promise; } // The following is from https://gist.github.com/enriched/c84a2a99f886654149908091a3183e15