diff --git a/.travis.yml b/.travis.yml index b43c2030d..07353e6f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ node_js: - '9' - '10' - '11' + - '12' before_script: - psql -U postgres -c "CREATE USER objection SUPERUSER" diff --git a/README.md b/README.md index d3e9db88c..c474987a6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/Vincit/objection.js.svg?branch=master)](https://travis-ci.org/Vincit/objection.js) [![Coverage Status](https://coveralls.io/repos/github/Vincit/objection.js/badge.svg?branch=master&service=github)](https://coveralls.io/github/Vincit/objection.js?branch=master) [![Join the chat at https://gitter.im/Vincit/objection.js](https://badges.gitter.im/Vincit/objection.js.svg)](https://gitter.im/Vincit/objection.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://travis-ci.org/Vincit/objection.js.svg?branch=master)](https://travis-ci.org/Vincit/objection.js) [![Coverage Status](https://coveralls.io/repos/github/Vincit/objection.js/badge.svg?branch=master&u=1)](https://coveralls.io/github/Vincit/objection.js?branch=master) [![Join the chat at https://gitter.im/Vincit/objection.js](https://badges.gitter.im/Vincit/objection.js.svg)](https://gitter.im/Vincit/objection.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # [Objection.js](https://vincit.github.io/objection.js) diff --git a/doc/.vuepress/theme/components/AlgoliaSearchBox.vue b/doc/.vuepress/theme/components/AlgoliaSearchBox.vue new file mode 100644 index 000000000..c2d443055 --- /dev/null +++ b/doc/.vuepress/theme/components/AlgoliaSearchBox.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/doc/.vuepress/theme/index.js b/doc/.vuepress/theme/index.js new file mode 100644 index 000000000..c77d3dedf --- /dev/null +++ b/doc/.vuepress/theme/index.js @@ -0,0 +1,24 @@ +const path = require('path'); + +module.exports = (_, ctx) => ({ + // MODIFICATION_FROM_THEME - this alias method is imported without change + // to point to updated AlgoliaSearchBox.vue + alias() { + const { themeConfig, siteConfig } = ctx; + + // resolve algolia + const isAlgoliaSearch = + themeConfig.algolia || + Object.keys((siteConfig.locales && themeConfig.locales) || {}).some( + base => themeConfig.locales[base].algolia + ); + + return { + '@AlgoliaSearchBox': isAlgoliaSearch + ? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue') + : path.resolve(__dirname, 'noopModule.js') + }; + }, + + extend: '@vuepress/theme-default' +}); diff --git a/doc/changelog/README.md b/doc/changelog/README.md index f72eca1a0..85f74efaf 100644 --- a/doc/changelog/README.md +++ b/doc/changelog/README.md @@ -1,5 +1,17 @@ # Changelog +## 1.6.11 + + * Fixes [#1471](https://github.com/Vincit/objection.js/issues/1471) + * Fixes [#1470](https://github.com/Vincit/objection.js/issues/1470) + * Fixes [#1364](https://github.com/Vincit/objection.js/issues/1364) + * Fixes [#1458](https://github.com/Vincit/objection.js/issues/1458) + * Fixes [#1467](https://github.com/Vincit/objection.js/issues/1467) + +## 1.6.10 + + * Fixes [#1455](https://github.com/Vincit/objection.js/issues/1455) + ## 1.6.9 * Revert fix for [#1089](https://github.com/Vincit/objection.js/issues/1089). It was causing more bugs than it fixed. #1089 will be addressed in 2.0. diff --git a/lib/model/Model.js b/lib/model/Model.js index 86a47935e..7027e1fe5 100644 --- a/lib/model/Model.js +++ b/lib/model/Model.js @@ -725,6 +725,14 @@ class Model { } } +Object.defineProperties(Model, { + isObjectionModelClass: { + enumerable: false, + writable: false, + value: true + } +}); + Object.defineProperties(Model.prototype, { $isObjectionModel: { enumerable: false, diff --git a/lib/queryBuilder/QueryBuilder.js b/lib/queryBuilder/QueryBuilder.js index c20834d72..ff6a2fd38 100644 --- a/lib/queryBuilder/QueryBuilder.js +++ b/lib/queryBuilder/QueryBuilder.js @@ -442,7 +442,7 @@ class QueryBuilder extends QueryBuilderBase { } clone() { - const builder = new this.constructor(this.modelClass()); + const builder = this.emptyInstance(); // Call the super class's clone implementation. this.baseCloneInto(builder); @@ -455,6 +455,12 @@ class QueryBuilder extends QueryBuilderBase { builder._allowedUpsertExpression = this._allowedUpsertExpression; builder._findOperationOptions = this._findOperationOptions; + return builder; + } + + emptyInstance() { + const builder = new this.constructor(this.modelClass()); + builder._findOperationFactory = this._findOperationFactory; builder._insertOperationFactory = this._insertOperationFactory; builder._updateOperationFactory = this._updateOperationFactory; @@ -701,12 +707,15 @@ class QueryBuilder extends QueryBuilderBase { modelClass = null; } - // Turn the properties into a hash for performance. - properties = properties.reduce((obj, prop) => { - obj[prop] = true; - return obj; - }, {}); - + if (Array.isArray(properties)) { + // Turn the properties into a hash for performance. + properties = properties.reduce((obj, prop) => { + obj[prop] = true; + return obj; + }, {}); + } else { + properties = { [properties]: true }; + } return this.traverse(modelClass, model => { model.$omit(properties); }); @@ -982,7 +991,7 @@ class QueryBuilder extends QueryBuilderBase { } range(...args) { - return this.addOperation(new RangeOperation('range'), args); + return this.clear(RangeOperation).addOperation(new RangeOperation('range'), args); } first(...args) { @@ -1190,9 +1199,7 @@ function parseRelationExpression(modelClass, exp) { if (err instanceof DuplicateRelationError) { throw modelClass.createValidationError({ type: ValidationErrorType.RelationExpression, - message: `Duplicate relation name "${ - err.relationName - }" in relation expression "${exp}". Use "a.[b, c]" instead of "[a.b, a.c]".` + message: `Duplicate relation name "${err.relationName}" in relation expression "${exp}". Use "a.[b, c]" instead of "[a.b, a.c]".` }); } else { throw modelClass.createValidationError({ diff --git a/lib/queryBuilder/QueryBuilderOperationSupport.js b/lib/queryBuilder/QueryBuilderOperationSupport.js index 760f3a5d6..793599bfe 100644 --- a/lib/queryBuilder/QueryBuilderOperationSupport.js +++ b/lib/queryBuilder/QueryBuilderOperationSupport.js @@ -2,9 +2,7 @@ const promiseUtils = require('../utils/promiseUtils'); -const { isSubclassOf } = require('../utils/classUtils'); const { isString, isFunction, isRegExp, last } = require('../utils/objectUtils'); -const { QueryBuilderOperation } = require('./operations/QueryBuilderOperation'); const { QueryBuilderContextBase } = require('./QueryBuilderContextBase'); const { QueryBuilderUserContext } = require('./QueryBuilderUserContext'); @@ -468,7 +466,10 @@ function buildFunctionForOperationSelector(operationSelector) { return op => operationSelector.test(op.name); } else if (isString(operationSelector)) { return op => op.name === operationSelector; - } else if (isSubclassOf(operationSelector, QueryBuilderOperation)) { + } else if ( + isFunction(operationSelector) && + operationSelector.isObjectionQueryBuilderOperationClass + ) { return op => op.is(operationSelector); } else if (isFunction(operationSelector)) { return operationSelector; diff --git a/lib/queryBuilder/graph/GraphUpsert.js b/lib/queryBuilder/graph/GraphUpsert.js index 7a2a9a84d..143b66c06 100644 --- a/lib/queryBuilder/graph/GraphUpsert.js +++ b/lib/queryBuilder/graph/GraphUpsert.js @@ -93,8 +93,28 @@ function pruneRelatedBranches(graph, currentGraph, graphOptions) { function pruneDeletedBranches(graph, currentGraph) { const deleteNodes = currentGraph.nodes.filter(currentNode => !graph.nodeForNode(currentNode)); + const roots = findRoots(deleteNodes); + + // Don't delete relations the current graph doesn't even mention. + // So if the parent node doesn't even have the relation, it's not + // supposed to be deleted. + const rootsNotInRelation = roots.filter(deleteRoot => { + if (!deleteRoot.parentNode) { + return false; + } + + const { relation } = deleteRoot.parentEdge; + const parentNode = graph.nodeForNode(deleteRoot.parentNode); + + if (!parentNode) { + return false; + } - removeBranchesFromGraph(findRoots(deleteNodes), currentGraph); + return parentNode.obj[relation.name] === undefined; + }); + + removeBranchesFromGraph(roots, currentGraph); + removeNodesFromGraph(new Set(rootsNotInRelation), currentGraph); } function findRoots(nodes) { @@ -125,6 +145,10 @@ function removeBranchesFromGraph(branchRoots, graph) { ) ); + removeNodesFromGraph(nodesToRemove, graph); +} + +function removeNodesFromGraph(nodesToRemove, graph) { const edgesToRemove = new Set(); for (const node of nodesToRemove) { diff --git a/lib/queryBuilder/operations/QueryBuilderOperation.js b/lib/queryBuilder/operations/QueryBuilderOperation.js index e5655f983..ed3f06c38 100644 --- a/lib/queryBuilder/operations/QueryBuilderOperation.js +++ b/lib/queryBuilder/operations/QueryBuilderOperation.js @@ -252,6 +252,14 @@ class QueryBuilderOperation { } } +Object.defineProperties(QueryBuilderOperation, { + isObjectionQueryBuilderOperationClass: { + enumerable: false, + writable: false, + value: true + } +}); + module.exports = { QueryBuilderOperation }; diff --git a/lib/queryBuilder/operations/UpdateAndFetchOperation.js b/lib/queryBuilder/operations/UpdateAndFetchOperation.js index fa398a494..6d0eb9d6c 100644 --- a/lib/queryBuilder/operations/UpdateAndFetchOperation.js +++ b/lib/queryBuilder/operations/UpdateAndFetchOperation.js @@ -26,11 +26,11 @@ class UpdateAndFetchOperation extends DelegateOperation { } onBuild(builder) { - super.onBuild(builder); - if (!this.skipIdWhere) { builder.findById(this.id); } + + super.onBuild(builder); } onAfter2(builder, numUpdated) { @@ -39,11 +39,16 @@ class UpdateAndFetchOperation extends DelegateOperation { return afterReturn(super.onAfter2(builder, numUpdated), undefined); } + // Clone `this` query builder so that we get the correct + // operation factories in case of `$relatedQuery` etc. return builder - .modelClass() - .query() + .emptyInstance() .childQueryOf(builder) - .findById(this.id) + .modify(builder => { + if (!this.skipIdWhere) { + builder.findById(this.id); + } + }) .castTo(builder.resultModelClass()) .then(fetched => { let retVal = undefined; diff --git a/lib/queryBuilder/operations/eager/WhereInEagerOperation.js b/lib/queryBuilder/operations/eager/WhereInEagerOperation.js index 5f212fb07..92f55df16 100644 --- a/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +++ b/lib/queryBuilder/operations/eager/WhereInEagerOperation.js @@ -3,7 +3,7 @@ const promiseUtils = require('../../../utils/promiseUtils'); const { EagerOperation } = require('./EagerOperation'); -const { isMsSql, isOracle } = require('../../../utils/knexUtils'); +const { isMsSql, isOracle, isSqlite } = require('../../../utils/knexUtils'); const { asArray, flatten, chunk } = require('../../../utils/objectUtils'); const { ValidationErrorType } = require('../../../model/ValidationError'); const { createModifier } = require('../../../utils/createModifier'); @@ -25,6 +25,9 @@ class WhereInEagerOperation extends EagerOperation { return 2000; } else if (isOracle(knex)) { return 1000; + } else if (isSqlite(knex)) { + // SQLITE_MAX_VARIABLE_NUMBER is 999 by default + return 999; } else { // I'm sure there is some kind of limit for other databases too, but let's lower // this if someone ever hits those limits. diff --git a/lib/queryBuilder/parsers/relationExpressionParser.js b/lib/queryBuilder/parsers/relationExpressionParser.js index de561e703..4c09e01e1 100644 --- a/lib/queryBuilder/parsers/relationExpressionParser.js +++ b/lib/queryBuilder/parsers/relationExpressionParser.js @@ -999,9 +999,7 @@ function peg$parse(input, options) { function assertDuplicateRelation(node, expr) { if (expr.$name in node) { console.warn( - `Duplicate relation "${ - expr.$name - }" in a relation expression. You should use "a.[b, c]" instead of "[a.b, a.c]". This will cause an error in objection 2.0` + `Duplicate relation "${expr.$name}" in a relation expression. You should use "a.[b, c]" instead of "[a.b, a.c]". This will cause an error in objection 2.0` ); // TODO: enable for v2.0. diff --git a/lib/relations/Relation.js b/lib/relations/Relation.js index 282590844..609a7fb9c 100644 --- a/lib/relations/Relation.js +++ b/lib/relations/Relation.js @@ -1,7 +1,6 @@ 'use strict'; const { RelationProperty } = require('./RelationProperty'); -const getModel = () => require('../model/Model').Model; const { RelationFindOperation } = require('./RelationFindOperation'); const { RelationUpdateOperation } = require('./RelationUpdateOperation'); @@ -9,7 +8,6 @@ const { RelationDeleteOperation } = require('./RelationDeleteOperation'); const { RelationSubqueryOperation } = require('./RelationSubqueryOperation'); const { ref } = require('../queryBuilder/ReferenceBuilder'); -const { isSubclassOf } = require('../utils/classUtils'); const { resolveModel } = require('../utils/resolveModel'); const { get, isFunction } = require('../utils/objectUtils'); const { mapAfterAllReturn } = require('../utils/promiseUtils'); @@ -248,6 +246,14 @@ class Relation { } } +Object.defineProperties(Relation, { + isObjectionRelationClass: { + enumerable: false, + writable: false, + value: true + } +}); + Object.defineProperties(Relation.prototype, { isObjectionRelation: { enumerable: false, @@ -267,7 +273,7 @@ function checkForbiddenProperties(ctx) { } function checkOwnerModelClass(ctx) { - if (!isSubclassOf(ctx.ownerModelClass, getModel())) { + if (!isFunction(ctx.ownerModelClass) || !ctx.ownerModelClass.isObjectionModelClass) { throw ctx.createError(`Relation's owner is not a subclass of Model`); } @@ -303,7 +309,7 @@ function checkRelation(ctx) { throw ctx.createError('relation is not defined'); } - if (!isSubclassOf(ctx.mapping.relation, Relation)) { + if (!isFunction(ctx.mapping.relation) || !ctx.mapping.relation.isObjectionRelationClass) { throw ctx.createError('relation is not a subclass of Relation'); } @@ -337,9 +343,7 @@ function createJoinProperties(ctx) { if (ownerProp.props.some(it => it === ctx.name)) { throw ctx.createError( - `join: relation name and join property '${ - ctx.name - }' cannot have the same name. If you cannot change one or the other, you can use $parseDatabaseJson and $formatDatabaseJson methods to convert the column name.` + `join: relation name and join property '${ctx.name}' cannot have the same name. If you cannot change one or the other, you can use $parseDatabaseJson and $formatDatabaseJson methods to convert the column name.` ); } @@ -358,9 +362,7 @@ function createRelationProperty(ctx, refString, propName) { } catch (err) { if (err instanceof RelationProperty.ModelNotFoundError) { throw ctx.createError( - `join: either \`from\` or \`to\` must point to the owner model table and the other one to the related table. It might be that specified table '${ - err.tableName - }' is not correct` + `join: either \`from\` or \`to\` must point to the owner model table and the other one to the related table. It might be that specified table '${err.tableName}' is not correct` ); } else if (err instanceof RelationProperty.InvalidReferenceError) { throw ctx.createError( diff --git a/lib/relations/RelationProperty.js b/lib/relations/RelationProperty.js index ec5d914b2..6dbcd6169 100644 --- a/lib/relations/RelationProperty.js +++ b/lib/relations/RelationProperty.js @@ -42,7 +42,7 @@ class RelationProperty { this._cols = refs.map(it => it.column); this._propGetters = paths.map(it => createGetter(it.path)); this._propSetters = paths.map(it => createSetter(it.path)); - this._patchers = refs.map(it => createPatcher(it)); + this._patchers = refs.map((it, i) => createPatcher(it, paths[i].path)); } static get ModelNotFoundError() { @@ -227,9 +227,9 @@ function createSetter(path) { } } -function createPatcher(ref) { +function createPatcher(ref, path) { if (ref.isPlainColumnRef) { - return (patch, value) => (patch[ref.column] = value); + return (patch, value) => (patch[path[0]] = value); } else { // Objection `patch`, `update` etc. methods understand field expressions. return (patch, value) => (patch[ref.expression] = value); diff --git a/lib/transaction.js b/lib/transaction.js index ce5a35c4f..f6f7d74a0 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -1,9 +1,7 @@ 'use strict'; const Bluebird = require('bluebird'); -const { Model } = require('./model/Model'); const promiseUtils = require('./utils/promiseUtils'); -const { isSubclassOf } = require('./utils/classUtils'); const { isFunction } = require('./utils/objectUtils'); function transaction() { @@ -18,7 +16,7 @@ function transaction() { let args = Array.from(arguments); - if (!isSubclassOf(args[0], Model) && isFunction(args[0].transaction)) { + if (!isModelClass(args[0]) && isFunction(args[0].transaction)) { let knex = args[0]; args = args.slice(1); @@ -35,7 +33,7 @@ function transaction() { let i; for (i = 0; i < modelClasses.length; ++i) { - if (!isSubclassOf(modelClasses[i], Model)) { + if (!isModelClass(modelClasses[i])) { return Bluebird.reject( new Error('objection.transaction: all but the last argument should be Model subclasses') ); @@ -77,7 +75,7 @@ function transaction() { transaction.start = function(modelClassOrKnex) { let knex = modelClassOrKnex; - if (isSubclassOf(modelClassOrKnex, Model)) { + if (isModelClass(modelClassOrKnex)) { knex = modelClassOrKnex.knex(); } @@ -104,6 +102,10 @@ function isGenerator(fn) { return fn && fn.constructor && fn.constructor.name === 'GeneratorFunction'; } +function isModelClass(maybeModel) { + return isFunction(maybeModel) && maybeModel.isObjectionModelClass; +} + module.exports = { transaction }; diff --git a/lib/utils/assert.js b/lib/utils/assert.js index 09701ce9b..72a73d659 100644 --- a/lib/utils/assert.js +++ b/lib/utils/assert.js @@ -6,9 +6,7 @@ function assertHasId(model) { const ids = modelClass.getIdColumnArray().join(', '); throw new Error( - `one of the identifier columns [${ids}] is null or undefined. Have you specified the correct identifier column for the model '${ - modelClass.name - }' using the 'idColumn' property?` + `one of the identifier columns [${ids}] is null or undefined. Have you specified the correct identifier column for the model '${modelClass.name}' using the 'idColumn' property?` ); } } diff --git a/lib/utils/classUtils.js b/lib/utils/classUtils.js index 03121c14b..2a38f8e8a 100644 --- a/lib/utils/classUtils.js +++ b/lib/utils/classUtils.js @@ -1,23 +1,5 @@ 'use strict'; -const { isFunction } = require('./objectUtils'); - -function isSubclassOf(Constructor, SuperConstructor) { - if (!isFunction(SuperConstructor)) { - return false; - } - - while (isFunction(Constructor)) { - if (Constructor === SuperConstructor) { - return true; - } - - Constructor = Object.getPrototypeOf(Constructor); - } - - return false; -} - function inherit(Constructor, BaseConstructor) { Constructor.prototype = Object.create(BaseConstructor.prototype); Constructor.prototype.constructor = BaseConstructor; @@ -27,6 +9,5 @@ function inherit(Constructor, BaseConstructor) { } module.exports = { - isSubclassOf, inherit }; diff --git a/lib/utils/resolveModel.js b/lib/utils/resolveModel.js index 324d6224e..753cb1845 100644 --- a/lib/utils/resolveModel.js +++ b/lib/utils/resolveModel.js @@ -1,10 +1,8 @@ 'use strict'; const path = require('path'); -const { once, isString, isFunction } = require('../utils/objectUtils'); -const { isSubclassOf } = require('../utils/classUtils'); +const { isString, isFunction } = require('../utils/objectUtils'); -const getModel = once(() => require('../model/Model').Model); class ResolveError extends Error {} function resolveModel(modelRef, modelPaths, errorPrefix) { @@ -16,11 +14,11 @@ function resolveModel(modelRef, modelPaths, errorPrefix) { return requireUsingModelPaths(modelRef, modelPaths); } } else { - if (isFunction(modelRef) && !isSubclassOf(modelRef, getModel())) { + if (isFunction(modelRef) && !isModelClass(modelRef)) { modelRef = modelRef(); } - if (!isSubclassOf(modelRef, getModel())) { + if (!isModelClass(modelRef)) { throw new ResolveError( `is not a subclass of Model or a file path to a module that exports one. You may be dealing with a require loop. See the documentation section about require loops.` ); @@ -58,7 +56,6 @@ function requireUsingModelPaths(modelRef, modelPaths) { } function requireModel(modelPath) { - const Model = getModel(); /** * Wrap path string in template literal to prevent * warnings about Objection.JS being an expression @@ -68,16 +65,16 @@ function requireModel(modelPath) { let mod = require(`${path.resolve(modelPath)}`); let modelClass = null; - if (isSubclassOf(mod, Model)) { + if (isModelClass(mod)) { modelClass = mod; - } else if (isSubclassOf(mod.default, Model)) { + } else if (isModelClass(mod.default)) { // Babel 6 style of exposing default export. modelClass = mod.default; } else { Object.keys(mod).forEach(exportName => { const exp = mod[exportName]; - if (isSubclassOf(exp, Model)) { + if (isModelClass(exp)) { if (modelClass !== null) { throw new ResolveError( `path ${modelPath} exports multiple models. Don't know which one to choose.` @@ -89,7 +86,7 @@ function requireModel(modelPath) { }); } - if (!isSubclassOf(modelClass, Model)) { + if (!isModelClass(modelClass)) { throw new ResolveError(`${modelPath} is an invalid file path to a model class`); } @@ -100,6 +97,10 @@ function isAbsolutePath(pth) { return path.normalize(pth + '/') === path.normalize(path.resolve(pth) + '/'); } +function isModelClass(maybeModel) { + return isFunction(maybeModel) && maybeModel.isObjectionModelClass; +} + module.exports = { resolveModel }; diff --git a/package.json b/package.json index 82eb5410c..9781e9653 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "objection", - "version": "1.6.9", + "version": "1.6.11", "description": "An SQL-friendly ORM for Node.js", "main": "lib/objection.js", "license": "MIT", @@ -70,26 +70,26 @@ }, "devDependencies": { "@babel/polyfill": "^7.4.4", - "@types/node": "^10.14.7", + "@types/node": "^12.7.5", "babel-eslint": "^10.0.1", "chai": "^4.2.0", "chai-subset": "^1.6.0", "coveralls": "^3.0.3", - "cross-env": "^5.2.0", + "cross-env": "^6.0.0", "eslint": "^5.16.0", "eslint-plugin-prettier": "^3.1.0", "expect.js": "^0.3.1", - "fs-extra": "^7.0.1", + "fs-extra": "^8.1.0", "glob": "^7.1.3", - "knex": "0.17.0", - "mocha": "^5.2.0", + "knex": "^0.17.0", + "mocha": "^6.2.0", "mysql": "^2.17.1", "nyc": "^14.1.1", "pg": "^7.11.0", - "prettier": "1.17.1", + "prettier": "1.18.2", "sqlite3": "^4.0.8", "typescript": "^3.4.5", - "vuepress": "0.14.11" + "vuepress": "1.0.4" }, "nyc": { "description": "test coverage", diff --git a/tests/integration/find.js b/tests/integration/find.js index 81450fde8..edafd5ef0 100644 --- a/tests/integration/find.js +++ b/tests/integration/find.js @@ -102,6 +102,18 @@ module.exports = session => { }); }); + it('calling page twice should override the previous call', () => { + return Model2.query() + .page(1, 2) + .page(0, 2) + .orderBy('model2_prop2', 'desc') + .then(result => { + expect(result.results[0]).to.be.a(Model2); + expect(result.total === 3).to.equal(true); + expect(_.map(result.results, 'model2Prop2')).to.eql([30, 20]); + }); + }); + describe('query builder methods', () => { it('.select()', () => { return Model2.query() diff --git a/tests/integration/misc/#1455.js b/tests/integration/misc/#1455.js new file mode 100644 index 000000000..eee8fae26 --- /dev/null +++ b/tests/integration/misc/#1455.js @@ -0,0 +1,166 @@ +const { Model, transaction } = require('../../../'); +const expect = require('expect.js'); + +module.exports = session => { + describe('UpsertGraph deletes rows for relation which is not mentioned in graph #1455', () => { + let knex = session.knex; + let Role; + + beforeEach(() => { + const { knex } = session; + + return knex.schema + .dropTableIfExists('roles') + .then(() => knex.schema.dropTableIfExists('sets')) + .then(() => knex.schema.dropTableIfExists('setsAttributes')) + .then(() => { + return knex.schema.createTable('roles', table => { + table.increments(); + table.string('name').notNullable(); + }); + }) + .then(() => { + return knex.schema.createTable('sets', table => { + table.increments(); + table.string('name').notNullable(); + table + .integer('roleId') + .unsigned() + .notNullable(); + }); + }) + .then(() => { + return knex.schema.createTable('setsAttributes', table => { + table.increments(); + table.string('name').notNullable(); + table + .integer('setId') + .unsigned() + .notNullable(); + }); + }); + }); + + afterEach(() => { + return knex.schema + .dropTableIfExists('roles') + .then(() => knex.schema.dropTableIfExists('sets')) + .then(() => knex.schema.dropTableIfExists('setsAttributes')); + }); + + beforeEach(() => { + const { knex } = session; + + class BaseModel extends Model { + static get useLimitInFirst() { + return true; + } + } + + class SetAttribute extends BaseModel { + static get tableName() { + return 'setsAttributes'; + } + } + + class Set extends BaseModel { + static get tableName() { + return 'sets'; + } + + static get relationMappings() { + return { + setAttributes: { + relation: BaseModel.HasManyRelation, + modelClass: SetAttribute, + join: { from: 'sets.id', to: 'setsAttributes.setId' } + } + }; + } + } + + Role = class Role extends BaseModel { + static get tableName() { + return 'roles'; + } + + static get relationMappings() { + return { + sets: { + relation: BaseModel.HasManyRelation, + modelClass: Set, + join: { from: 'roles.id', to: 'sets.roleId' } + } + }; + } + }; + + BaseModel.knex(knex); + }); + + it('test', () => { + return transaction(Role.knex(), trx => + Role.query(trx).insertGraph({ + name: 'First Role', + sets: [ + { + name: 'First Set', + setAttributes: [{ name: 'First SetAttribute' }, { name: 'Second SetAttribute' }] + } + ] + }) + ) + .then(role => { + return transaction(Role.knex(), trx => + Role.query(trx).upsertGraph({ + id: role.id, + sets: [ + { id: role.sets[0].id }, + { + name: 'Second Set', + setAttributes: [{ name: 'First SetAttribute' }, { name: 'Second SetAttribute' }] + } + ] + }) + ); + }) + .then(() => { + return Role.query() + .first() + .eager('sets(orderById).setAttributes(orderById)', { + orderById(query) { + query.orderBy('id'); + } + }); + }) + .then(setsAfterUpsertGraph => { + expect(setsAfterUpsertGraph).to.eql({ + id: 1, + name: 'First Role', + sets: [ + { + id: 1, + name: 'First Set', + roleId: 1, + + setAttributes: [ + { id: 1, name: 'First SetAttribute', setId: 1 }, + { id: 2, name: 'Second SetAttribute', setId: 1 } + ] + }, + { + id: 2, + name: 'Second Set', + roleId: 1, + + setAttributes: [ + { id: 3, name: 'First SetAttribute', setId: 2 }, + { id: 4, name: 'Second SetAttribute', setId: 2 } + ] + } + ] + }); + }); + }); + }); +}; diff --git a/tests/integration/misc/#1467.js b/tests/integration/misc/#1467.js new file mode 100644 index 000000000..daa92d974 --- /dev/null +++ b/tests/integration/misc/#1467.js @@ -0,0 +1,213 @@ +const { Model, snakeCaseMappers } = require('../../../'); +const chai = require('chai'); + +module.exports = session => { + describe('Not CamelCasing ref.column #1467', () => { + let knex = session.knex; + let Campaign, Deliverable; + + beforeEach(() => { + const { knex } = session; + + return Promise.resolve() + .then(() => knex.schema.dropTableIfExists('cogs')) + .then(() => knex.schema.dropTableIfExists('campaigns')) + .then(() => knex.schema.dropTableIfExists('deliverables')) + .then(() => { + return knex.schema + .createTable('campaigns', table => { + table.increments('id').primary(); + }) + .createTable('deliverables', table => { + table.increments('id').primary(); + }) + .createTable('cogs', table => { + table.increments('id').primary(); + table + .integer('campaign_id') + .unsigned() + .references('id') + .inTable('campaigns'); + table + .integer('deliverable_id') + .unsigned() + .references('id') + .inTable('deliverables'); + }); + }); + }); + + afterEach(() => { + return Promise.resolve() + .then(() => knex.schema.dropTableIfExists('cogs')) + .then(() => knex.schema.dropTableIfExists('campaigns')) + .then(() => knex.schema.dropTableIfExists('deliverables')); + }); + + beforeEach(() => { + Campaign = class Campaign extends Model { + static get tableName() { + return 'campaigns'; + } + + static get columnNameMappers() { + return snakeCaseMappers(); + } + + static get jsonSchema() { + return { + type: 'object', + properties: { + id: { type: 'integer' } + }, + additionalProperties: false + }; + } + + static get relationMappings() { + return { + cogs: { + relation: Model.HasManyRelation, + modelClass: Cog, + join: { + from: 'campaigns.id', + to: 'cogs.campaign_id' + } + } + }; + } + }; + + class Cog extends Model { + static get tableName() { + return 'cogs'; + } + + static get columnNameMappers() { + return snakeCaseMappers(); + } + + static get jsonSchema() { + return { + type: 'object', + properties: { + id: { type: 'integer' }, + campaignId: { type: ['integer', 'null'] }, + deliverableId: { type: ['integer', 'null'] } + }, + additionalProperties: false + }; + } + + static get relationMappings() { + return {}; + } + } + + Deliverable = class Deliverable extends Model { + static get tableName() { + return 'deliverables'; + } + + static get columnNameMappers() { + return snakeCaseMappers(); + } + + static get jsonSchema() { + return { + type: 'object', + properties: { + id: { type: 'integer' } + }, + additionalProperties: false + }; + } + + static get relationMappings() { + return { + cogs: { + relation: Model.HasManyRelation, + modelClass: Cog, + join: { + from: 'deliverables.id', + to: 'cogs.deliverable_id' + } + } + }; + } + }; + + Campaign.knex(session.knex); + Deliverable.knex(session.knex); + }); + + it('test', () => { + return Promise.resolve() + .then(() => { + return Promise.all([ + Campaign.query().insertGraph({}), + Deliverable.query().insertGraph({}) + ]); + }) + .then(([campaign, deliverable]) => { + return Promise.resolve() + .then(() => { + return Campaign.query().upsertGraph( + { id: campaign.id, cogs: [{ deliverableId: deliverable.id }] }, + { relate: ['cogs'], unrelate: ['cogs'] } + ); + }) + .then(() => { + return Campaign.query() + .findOne({}) + .eager('cogs'); + }) + .then(c1 => { + chai.expect(c1.cogs.length).to.equal(1); + }) + .then(() => { + return Campaign.query().upsertGraph( + { id: campaign.id, cogs: [] }, + { relate: ['cogs'], unrelate: ['cogs'] } + ); + }) + .then(() => { + return Campaign.query() + .findOne({}) + .eager('cogs'); + }) + .then(c2 => { + chai.expect(c2.cogs.length).to.equal(0); + }) + .then(() => { + return Deliverable.query().upsertGraph( + { id: deliverable.id, cogs: [{ campaignId: campaign.id }] }, + { relate: ['cogs'], unrelate: ['cogs'] } + ); + }) + .then(() => { + return Deliverable.query() + .findOne({}) + .eager('cogs'); + }) + .then(d1 => { + chai.expect(d1.cogs.length).to.equal(1); + }) + .then(() => { + return Deliverable.query().upsertGraph( + { id: deliverable.id, cogs: [] }, + { relate: ['cogs'], unrelate: ['cogs'] } + ); + }) + .then(() => { + return Deliverable.query() + .findOne({}) + .eager('cogs'); + }) + .then(d2 => { + chai.expect(d2.cogs.length).to.equal(0); + }); + }); + }); + }); +}; diff --git a/tests/integration/patch.js b/tests/integration/patch.js index 479c6ff64..06d9e9124 100644 --- a/tests/integration/patch.js +++ b/tests/integration/patch.js @@ -1123,6 +1123,71 @@ module.exports = session => { }); }); + it('should patch a related object with extras using patchAndFetchById', () => { + return Model1.query() + .findById(1) + .then(parent => { + return parent.$relatedQuery('model1Relation3').patchAndFetchById(4, { + model2Prop1: 'iam updated', + extra1: 'updated extra 1', + // Test query properties. sqlite doesn't have `concat` function. Use a literal for it. + extra2: isSqlite(session.knex) + ? 'updated extra 2' + : raw(`CONCAT('updated extra ', '2')`) + }); + }) + .then(result => { + chai.expect(result).to.containSubset({ + model2Prop1: 'iam updated', + extra1: 'updated extra 1', + extra2: 'updated extra 2', + idCol: 4 + }); + + return Model1.query() + .findById(1) + .eager('model1Relation3'); + }) + .then(model1 => { + chai.expect(model1).to.containSubset({ + id: 1, + model1Id: null, + model1Prop1: 'hello 1', + model1Prop2: null, + model1Relation3: [ + { + idCol: 5, + model1Id: null, + model2Prop1: 'foo 3', + model2Prop2: null, + extra1: 'extra 13', + extra2: 'extra 23', + $afterGetCalled: 1 + }, + { + idCol: 4, + model1Id: null, + model2Prop1: 'iam updated', + model2Prop2: null, + extra1: 'updated extra 1', + extra2: 'updated extra 2', + $afterGetCalled: 1 + }, + { + idCol: 3, + model1Id: null, + model2Prop1: 'foo 1', + model2Prop2: null, + extra1: 'extra 11', + extra2: 'extra 21', + $afterGetCalled: 1 + } + ], + $afterGetCalled: 1 + }); + }); + }); + it('should patch a related object with extras', () => { return Model1.query() .findById(1) diff --git a/tests/unit/queryBuilder/QueryBuilder.js b/tests/unit/queryBuilder/QueryBuilder.js index 7bba0aac8..1a895799b 100644 --- a/tests/unit/queryBuilder/QueryBuilder.js +++ b/tests/unit/queryBuilder/QueryBuilder.js @@ -2464,6 +2464,33 @@ describe('QueryBuilder', () => { }); }); }); + + describe('omit', () => { + it('omit properties from model when ommiting an array', done => { + mockKnexQueryResults = [[{ a: 1 }, { b: 2 }, { c: 3 }]]; + TestModel.query() + .omit(['a', 'b']) + .then(result => { + expect(result[0]).to.not.have.property('a'); + expect(result[1]).to.not.have.property('b'); + expect(result[2]).to.have.property('c'); + expect(result[2].c).to.be.equal(3); + done(); + }); + }); + + it('omit properties from model', done => { + mockKnexQueryResults = [[{ a: 1 }, { b: 2 }]]; + TestModel.query() + .omit('b') + .then(result => { + expect(result[0]).to.have.property('a'); + expect(result[0].a).to.be.equal(1); + expect(result[1]).to.not.have.property('b'); + done(); + }); + }); + }); }); const operationBuilder = QueryBuilder.forClass(Model); diff --git a/tests/unit/relations/ManyToManyRelation.js b/tests/unit/relations/ManyToManyRelation.js index f5b9bb1be..60fe1b021 100644 --- a/tests/unit/relations/ManyToManyRelation.js +++ b/tests/unit/relations/ManyToManyRelation.js @@ -3,7 +3,7 @@ const Knex = require('knex'); const expect = require('expect.js'); const Promise = require('bluebird'); const objection = require('../../../'); -const classUtils = require('../../../lib/utils/classUtils'); +const { isFunction } = require('../../../lib/utils/objectUtils'); const knexMocker = require('../../../testUtils/mockKnex'); const Model = objection.Model; @@ -140,7 +140,7 @@ describe('ManyToManyRelation', () => { expect(relation.joinTable).to.equal('JoinModel'); expect(relation.joinTableOwnerProp.cols).to.eql(['ownerId']); expect(relation.joinTableRelatedProp.props).to.eql(['relatedId']); - expect(classUtils.isSubclassOf(relation.joinModelClass, JoinModel)).to.equal(true); + expect(isSubclassOf(relation.joinModelClass, JoinModel)).to.equal(true); }); it('should accept an absolute file path to a join model in join.through object', () => { @@ -163,9 +163,7 @@ describe('ManyToManyRelation', () => { expect(relation.joinTable).to.equal('JoinModel'); expect(relation.joinTableOwnerProp.cols).to.eql(['ownerId']); expect(relation.joinTableRelatedProp.cols).to.eql(['relatedId']); - expect(classUtils.isSubclassOf(relation.joinModelClass, require('./files/JoinModel'))).to.equal( - true - ); + expect(isSubclassOf(relation.joinModelClass, require('./files/JoinModel'))).to.equal(true); }); it('should accept a composite keys in join.through object (1)', () => { @@ -252,7 +250,7 @@ describe('ManyToManyRelation', () => { } }); }).to.throwException(err => { - expect(err.message).to.equal( + expect(err.message).to.contain( "OwnerModel.relationMappings.testRelation: Cannot find module '/not/a/path/to/a/model'" ); }); @@ -274,7 +272,7 @@ describe('ManyToManyRelation', () => { } }); }).to.throwException(err => { - expect(err.message).to.equal( + expect(err.message).to.contain( 'OwnerModel.relationMappings.testRelation: join.through must be an object that describes the join table. For example: {from: "JoinTable.someId", to: "JoinTable.someOtherId"}' ); }); @@ -296,7 +294,7 @@ describe('ManyToManyRelation', () => { } }); }).to.throwException(err => { - expect(err.message).to.equal( + expect(err.message).to.contain( 'OwnerModel.relationMappings.testRelation: join.through must be an object that describes the join table. For example: {from: "JoinTable.someId", to: "JoinTable.someOtherId"}' ); }); @@ -319,7 +317,7 @@ describe('ManyToManyRelation', () => { } }); }).to.throwException(err => { - expect(err.message).to.equal( + expect(err.message).to.contain( 'OwnerModel.relationMappings.testRelation: join.through.from must have format JoinTable.columnName. For example "JoinTable.someId" or in case of composite key ["JoinTable.a", "JoinTable.b"].' ); }); @@ -342,7 +340,7 @@ describe('ManyToManyRelation', () => { } }); }).to.throwException(err => { - expect(err.message).to.equal( + expect(err.message).to.contain( 'OwnerModel.relationMappings.testRelation: join.through.to must have format JoinTable.columnName. For example "JoinTable.someId" or in case of composite key ["JoinTable.a", "JoinTable.b"].' ); }); @@ -365,7 +363,7 @@ describe('ManyToManyRelation', () => { } }); }).to.throwException(err => { - expect(err.message).to.equal( + expect(err.message).to.contain( 'OwnerModel.relationMappings.testRelation: join.through `from` and `to` must point to the same join table.' ); }); @@ -1480,3 +1478,19 @@ describe('ManyToManyRelation', () => { }); } }); + +function isSubclassOf(Constructor, SuperConstructor) { + if (!isFunction(SuperConstructor)) { + return false; + } + + while (isFunction(Constructor)) { + if (Constructor === SuperConstructor) { + return true; + } + + Constructor = Object.getPrototypeOf(Constructor); + } + + return false; +} diff --git a/tests/unit/utils.js b/tests/unit/utils.js index 13128e1ae..7a49eb515 100644 --- a/tests/unit/utils.js +++ b/tests/unit/utils.js @@ -15,26 +15,6 @@ const { map } = require('../../lib/utils/promiseUtils'); const { jsonEquals } = require('../../lib/utils/objectUtils'); describe('utils', () => { - describe('isSubclassOf', () => { - class A {} - class B extends A {} - class C extends B {} - - it('should return true for subclass constructor', () => { - expect(classUtils.isSubclassOf(B, A)).to.equal(true); - expect(classUtils.isSubclassOf(C, B)).to.equal(true); - expect(classUtils.isSubclassOf(C, A)).to.equal(true); - expect(classUtils.isSubclassOf(A, B)).to.equal(false); - expect(classUtils.isSubclassOf(B, C)).to.equal(false); - expect(classUtils.isSubclassOf(A, C)).to.equal(false); - }); - - it('should return false if one of the inputs is not a constructor', () => { - expect(classUtils.isSubclassOf(function() {}, {})).to.equal(false); - expect(classUtils.isSubclassOf({}, function() {})).to.equal(false); - }); - }); - describe('mixin', () => { it('should mixin rest of the arguments to the first argument', () => { class X {} diff --git a/typings/objection/index.d.ts b/typings/objection/index.d.ts index 3038d7165..4e07e790e 100644 --- a/typings/objection/index.d.ts +++ b/typings/objection/index.d.ts @@ -347,7 +347,7 @@ declare namespace Objection { } interface JoinRelation { - (relationName: string, opt?: RelationOptions): QueryBuilder; + (expr: RelationExpression, opt?: RelationOptions): QueryBuilder; } type JsonObjectOrFieldExpression = object | object[] | FieldExpression; @@ -722,7 +722,7 @@ declare namespace Objection { } type PartialUpdate = { - [P in keyof QM]?: QM[P] | Raw | Reference | QueryBuilder + [P in keyof QM]?: QM[P] | Raw | Reference | QueryBuilder; }; interface QueryBuilderBase extends QueryInterface { @@ -1556,7 +1556,7 @@ declare namespace Objection { */ type?: string | string[]; /** - * fallback raw string for custom formats, + * fallback raw string for custom formats, * or formats that aren't in the standard yet */ format?: JsonSchemaFormatType | string;