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 @@
-[](https://travis-ci.org/Vincit/objection.js) [](https://coveralls.io/github/Vincit/objection.js?branch=master) [](https://gitter.im/Vincit/objection.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://travis-ci.org/Vincit/objection.js) [](https://coveralls.io/github/Vincit/objection.js?branch=master) [](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;