Repository: Vincit/objection.js
Branch: main
Commit: a7784ded683c
Files: 410
Total size: 2.8 MB
Directory structure:
gitextract_1w8nr7ov/
├── .eslintrc.json
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── doc/
│ ├── .prettierrc.json
│ ├── .vuepress/
│ │ ├── config.js
│ │ ├── styles/
│ │ │ ├── index.styl
│ │ │ └── palette.styl
│ │ └── theme/
│ │ ├── components/
│ │ │ └── AlgoliaSearchBox.vue
│ │ └── index.js
│ ├── README.md
│ ├── api/
│ │ ├── README.md
│ │ ├── model/
│ │ │ ├── instance-methods.md
│ │ │ ├── instance-properties.md
│ │ │ ├── overview.md
│ │ │ ├── static-methods.md
│ │ │ └── static-properties.md
│ │ ├── objection/
│ │ │ └── README.md
│ │ ├── query-builder/
│ │ │ ├── README.md
│ │ │ ├── eager-methods.md
│ │ │ ├── find-methods.md
│ │ │ ├── join-methods.md
│ │ │ ├── mutate-methods.md
│ │ │ ├── other-methods.md
│ │ │ └── static-methods.md
│ │ └── types/
│ │ └── README.md
│ ├── guide/
│ │ ├── contributing.md
│ │ ├── documents.md
│ │ ├── getting-started.md
│ │ ├── hooks.md
│ │ ├── installation.md
│ │ ├── models.md
│ │ ├── plugins.md
│ │ ├── query-examples.md
│ │ ├── relations.md
│ │ ├── transactions.md
│ │ └── validation.md
│ ├── recipes/
│ │ ├── composite-keys.md
│ │ ├── custom-id-column.md
│ │ ├── custom-query-builder.md
│ │ ├── custom-validation.md
│ │ ├── default-values.md
│ │ ├── error-handling.md
│ │ ├── extra-properties.md
│ │ ├── indexing-postgresql-jsonb-columns.md
│ │ ├── joins.md
│ │ ├── json-queries.md
│ │ ├── modifiers.md
│ │ ├── multitenancy-using-multiple-databases.md
│ │ ├── paging.md
│ │ ├── plugins.md
│ │ ├── polymorphic-associations.md
│ │ ├── precedence-and-parentheses.md
│ │ ├── raw-queries.md
│ │ ├── relation-subqueries.md
│ │ ├── returning-tricks.md
│ │ ├── snake-case-to-camel-case-conversion.md
│ │ ├── subqueries.md
│ │ ├── ternary-relationships.md
│ │ └── timestamps.md
│ └── release-notes/
│ ├── changelog.md
│ └── migration.md
├── docker-compose.yml
├── examples/
│ ├── koa/
│ │ ├── .prettierrc.json
│ │ ├── README.md
│ │ ├── api.js
│ │ ├── app.js
│ │ ├── client.js
│ │ ├── knexfile.js
│ │ ├── migrations/
│ │ │ └── 20150613161239_initial_schema.js
│ │ ├── models/
│ │ │ ├── Animal.js
│ │ │ ├── Movie.js
│ │ │ └── Person.js
│ │ └── package.json
│ ├── koa-ts/
│ │ ├── .prettierrc.json
│ │ ├── README.md
│ │ ├── api.ts
│ │ ├── app.ts
│ │ ├── client.js
│ │ ├── knexfile.js
│ │ ├── migrations/
│ │ │ └── 20150613161239_initial_schema.js
│ │ ├── models/
│ │ │ ├── Animal.ts
│ │ │ ├── Movie.ts
│ │ │ └── Person.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── minimal/
│ │ ├── README.md
│ │ ├── app.js
│ │ ├── knexfile.js
│ │ ├── migrations/
│ │ │ └── 20190330121219_initial_schema.js
│ │ ├── models/
│ │ │ └── Person.js
│ │ └── package.json
│ ├── plugin/
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── package.json
│ │ └── tests.js
│ └── plugin-with-options/
│ ├── README.md
│ ├── index.js
│ ├── package.json
│ └── tests.js
├── lib/
│ ├── .eslintrc.json
│ ├── initialize.js
│ ├── model/
│ │ ├── AjvValidator.js
│ │ ├── Model.js
│ │ ├── ModifierNotFoundError.js
│ │ ├── NotFoundError.js
│ │ ├── RelationDoesNotExistError.js
│ │ ├── ValidationError.js
│ │ ├── Validator.js
│ │ ├── getModel.js
│ │ ├── graph/
│ │ │ ├── ModelGraph.js
│ │ │ ├── ModelGraphBuilder.js
│ │ │ ├── ModelGraphEdge.js
│ │ │ └── ModelGraphNode.js
│ │ ├── inheritModel.js
│ │ ├── modelBindKnex.js
│ │ ├── modelClone.js
│ │ ├── modelColPropMap.js
│ │ ├── modelId.js
│ │ ├── modelJsonAttributes.js
│ │ ├── modelParseRelations.js
│ │ ├── modelQueryProps.js
│ │ ├── modelSet.js
│ │ ├── modelTableMetadata.js
│ │ ├── modelToJson.js
│ │ ├── modelUtils.js
│ │ ├── modelValidate.js
│ │ ├── modelValues.js
│ │ └── modelVisitor.js
│ ├── objection.js
│ ├── queryBuilder/
│ │ ├── FunctionBuilder.js
│ │ ├── InternalOptions.js
│ │ ├── JoinBuilder.js
│ │ ├── QueryBuilder.js
│ │ ├── QueryBuilderBase.js
│ │ ├── QueryBuilderContext.js
│ │ ├── QueryBuilderContextBase.js
│ │ ├── QueryBuilderOperationSupport.js
│ │ ├── QueryBuilderUserContext.js
│ │ ├── RawBuilder.js
│ │ ├── ReferenceBuilder.js
│ │ ├── RelationExpression.js
│ │ ├── StaticHookArguments.js
│ │ ├── ValueBuilder.js
│ │ ├── graph/
│ │ │ ├── GraphAction.js
│ │ │ ├── GraphData.js
│ │ │ ├── GraphFetcher.js
│ │ │ ├── GraphNodeDbExistence.js
│ │ │ ├── GraphOperation.js
│ │ │ ├── GraphOptions.js
│ │ │ ├── GraphUpsert.js
│ │ │ ├── delete/
│ │ │ │ ├── GraphDelete.js
│ │ │ │ └── GraphDeleteAction.js
│ │ │ ├── insert/
│ │ │ │ ├── GraphInsert.js
│ │ │ │ ├── GraphInsertAction.js
│ │ │ │ └── JoinRowGraphInsertAction.js
│ │ │ ├── patch/
│ │ │ │ ├── GraphPatch.js
│ │ │ │ └── GraphPatchAction.js
│ │ │ └── recursiveUpsert/
│ │ │ ├── GraphRecursiveUpsert.js
│ │ │ └── GraphRecursiveUpsertAction.js
│ │ ├── join/
│ │ │ ├── JoinResultColumn.js
│ │ │ ├── JoinResultParser.js
│ │ │ ├── RelationJoiner.js
│ │ │ ├── TableNode.js
│ │ │ ├── TableTree.js
│ │ │ └── utils.js
│ │ ├── operations/
│ │ │ ├── DelegateOperation.js
│ │ │ ├── DeleteOperation.js
│ │ │ ├── FindByIdOperation.js
│ │ │ ├── FindByIdsOperation.js
│ │ │ ├── FindOperation.js
│ │ │ ├── FirstOperation.js
│ │ │ ├── FromOperation.js
│ │ │ ├── InsertAndFetchOperation.js
│ │ │ ├── InsertGraphAndFetchOperation.js
│ │ │ ├── InsertGraphOperation.js
│ │ │ ├── InsertOperation.js
│ │ │ ├── InstanceDeleteOperation.js
│ │ │ ├── InstanceFindOperation.js
│ │ │ ├── InstanceInsertOperation.js
│ │ │ ├── InstanceUpdateOperation.js
│ │ │ ├── JoinRelatedOperation.js
│ │ │ ├── KnexOperation.js
│ │ │ ├── MergeOperation.js
│ │ │ ├── ObjectionToKnexConvertingOperation.js
│ │ │ ├── OnBuildKnexOperation.js
│ │ │ ├── OnBuildOperation.js
│ │ │ ├── OnErrorOperation.js
│ │ │ ├── QueryBuilderOperation.js
│ │ │ ├── RangeOperation.js
│ │ │ ├── RelateOperation.js
│ │ │ ├── ReturningOperation.js
│ │ │ ├── RunAfterOperation.js
│ │ │ ├── RunBeforeOperation.js
│ │ │ ├── UnrelateOperation.js
│ │ │ ├── UpdateAndFetchOperation.js
│ │ │ ├── UpdateOperation.js
│ │ │ ├── UpsertGraphAndFetchOperation.js
│ │ │ ├── UpsertGraphOperation.js
│ │ │ ├── WhereCompositeOperation.js
│ │ │ ├── eager/
│ │ │ │ ├── EagerOperation.js
│ │ │ │ ├── JoinEagerOperation.js
│ │ │ │ ├── NaiveEagerOperation.js
│ │ │ │ └── WhereInEagerOperation.js
│ │ │ ├── jsonApi/
│ │ │ │ ├── WhereJsonHasPostgresOperation.js
│ │ │ │ ├── WhereJsonNotObjectPostgresOperation.js
│ │ │ │ ├── WhereJsonPostgresOperation.js
│ │ │ │ └── postgresJsonApi.js
│ │ │ ├── select/
│ │ │ │ ├── SelectOperation.js
│ │ │ │ └── Selection.js
│ │ │ └── whereInComposite/
│ │ │ ├── WhereInCompositeMsSqlOperation.js
│ │ │ ├── WhereInCompositeOperation.js
│ │ │ └── WhereInCompositeSqliteOperation.js
│ │ ├── parsers/
│ │ │ ├── jsonFieldExpressionParser.js
│ │ │ ├── jsonFieldExpressionParser.pegjs
│ │ │ ├── relationExpressionParser.js
│ │ │ └── relationExpressionParser.pegjs
│ │ └── transformations/
│ │ ├── CompositeQueryTransformation.js
│ │ ├── QueryTransformation.js
│ │ ├── WrapMysqlModifySubqueryTransformation.js
│ │ └── index.js
│ ├── relations/
│ │ ├── Relation.js
│ │ ├── RelationDeleteOperation.js
│ │ ├── RelationFindOperation.js
│ │ ├── RelationInsertOperation.js
│ │ ├── RelationOwner.js
│ │ ├── RelationProperty.js
│ │ ├── RelationUpdateOperation.js
│ │ ├── belongsToOne/
│ │ │ ├── BelongsToOneDeleteOperation.js
│ │ │ ├── BelongsToOneInsertOperation.js
│ │ │ ├── BelongsToOneRelateOperation.js
│ │ │ ├── BelongsToOneRelation.js
│ │ │ └── BelongsToOneUnrelateOperation.js
│ │ ├── hasMany/
│ │ │ ├── HasManyInsertOperation.js
│ │ │ ├── HasManyRelateOperation.js
│ │ │ ├── HasManyRelation.js
│ │ │ └── HasManyUnrelateOperation.js
│ │ ├── hasOne/
│ │ │ └── HasOneRelation.js
│ │ ├── hasOneThrough/
│ │ │ └── HasOneThroughRelation.js
│ │ └── manyToMany/
│ │ ├── ManyToManyModifyMixin.js
│ │ ├── ManyToManyRelation.js
│ │ ├── ManyToManySqliteModifyMixin.js
│ │ ├── delete/
│ │ │ ├── ManyToManyDeleteOperation.js
│ │ │ ├── ManyToManyDeleteOperationBase.js
│ │ │ └── ManyToManyDeleteSqliteOperation.js
│ │ ├── find/
│ │ │ └── ManyToManyFindOperation.js
│ │ ├── insert/
│ │ │ └── ManyToManyInsertOperation.js
│ │ ├── relate/
│ │ │ └── ManyToManyRelateOperation.js
│ │ ├── unrelate/
│ │ │ ├── ManyToManyUnrelateOperation.js
│ │ │ ├── ManyToManyUnrelateOperationBase.js
│ │ │ └── ManyToManyUnrelateSqliteOperation.js
│ │ └── update/
│ │ ├── ManyToManyUpdateOperation.js
│ │ ├── ManyToManyUpdateOperationBase.js
│ │ └── ManyToManyUpdateSqliteOperation.js
│ ├── transaction.js
│ └── utils/
│ ├── assert.js
│ ├── buildUtils.js
│ ├── classUtils.js
│ ├── clone.js
│ ├── createModifier.js
│ ├── deprecate.js
│ ├── identifierMapping.js
│ ├── internalPropUtils.js
│ ├── knexUtils.js
│ ├── mixin.js
│ ├── normalizeIds.js
│ ├── objectUtils.js
│ ├── parseFieldExpression.js
│ ├── promiseUtils/
│ │ ├── after.js
│ │ ├── afterReturn.js
│ │ ├── index.js
│ │ ├── isPromise.js
│ │ ├── map.js
│ │ ├── mapAfterAllReturn.js
│ │ └── try.js
│ ├── resolveModel.js
│ └── tmpColumnUtils.js
├── package.json
├── publish-docs.sh
├── reproduction-template.js
├── setup-test-db.js
├── testUtils/
│ ├── TestSession.js
│ ├── mockKnex.js
│ └── testUtils.js
├── tests/
│ ├── integration/
│ │ ├── compositeKeys.js
│ │ ├── crossDb/
│ │ │ ├── index.js
│ │ │ └── mysql.js
│ │ ├── delete.js
│ │ ├── find.js
│ │ ├── graph/
│ │ │ └── GraphInsert.js
│ │ ├── index.js
│ │ ├── insert.js
│ │ ├── insertGraph.js
│ │ ├── jsonQueries.js
│ │ ├── jsonRelations.js
│ │ ├── knexIdentifierMapping.js
│ │ ├── knexSnakeCase.js
│ │ ├── misc/
│ │ │ ├── #1074.js
│ │ │ ├── #1202.js
│ │ │ ├── #1215.js
│ │ │ ├── #1223.js
│ │ │ ├── #1227.js
│ │ │ ├── #1265.js
│ │ │ ├── #1455.js
│ │ │ ├── #1467.js
│ │ │ ├── #1489.js
│ │ │ ├── #1627.js
│ │ │ ├── #1718.js
│ │ │ ├── #1757.js
│ │ │ ├── #2105.js
│ │ │ ├── #292.js
│ │ │ ├── #325.js
│ │ │ ├── #403.js
│ │ │ ├── #517.js
│ │ │ ├── #712.js
│ │ │ ├── #733.js
│ │ │ ├── #760.js
│ │ │ ├── #844.js
│ │ │ ├── #909.js
│ │ │ ├── aggregateMethodsWithRelations.js
│ │ │ ├── concurrency.js
│ │ │ ├── defaultModelFieldValues.js
│ │ │ ├── generatedId.js
│ │ │ ├── hasOneTree.js
│ │ │ ├── index.js
│ │ │ ├── modelWithLengthProperty.js
│ │ │ ├── multipleResultsWithOneToOneRelation.js
│ │ │ ├── mysqlBinaryColumns.js
│ │ │ ├── nonMutatingRelatedQuery.js
│ │ │ ├── refAttack.js
│ │ │ ├── relatedQueryErrors.js
│ │ │ ├── relationHooks.js
│ │ │ ├── tableMetadata.js
│ │ │ ├── unhandledRejectionErrors.js
│ │ │ ├── usingUnboundModelsByPassingKnex.js
│ │ │ └── zeroValueInRelationColumn.js
│ │ ├── modifiers.js
│ │ ├── nonPrimaryKeyRelations.js
│ │ ├── patch.js
│ │ ├── queryContext.js
│ │ ├── relate.js
│ │ ├── relationModify.js
│ │ ├── schema.js
│ │ ├── snakeCase.js
│ │ ├── staticHooks.js
│ │ ├── toKnexQuery.js
│ │ ├── transactions.js
│ │ ├── unrelate.js
│ │ ├── update.js
│ │ ├── upsertGraph.js
│ │ ├── viewsAndAliases.js
│ │ └── withGraph.js
│ ├── main.js
│ ├── ts/
│ │ ├── custom-query-builder.ts
│ │ ├── documents.ts
│ │ ├── examples.ts
│ │ ├── fixtures/
│ │ │ ├── animal.ts
│ │ │ ├── movie.ts
│ │ │ ├── person.ts
│ │ │ └── review.ts
│ │ ├── model/
│ │ │ └── instance-methods.ts
│ │ ├── model-class.ts
│ │ ├── query-builder-api/
│ │ │ ├── eager-loading-methods.ts
│ │ │ ├── find-methods.ts
│ │ │ ├── join-methods.ts
│ │ │ ├── mutating-methods.ts
│ │ │ └── other-methods.ts
│ │ ├── query-examples/
│ │ │ ├── basic-queries/
│ │ │ │ ├── delete.ts
│ │ │ │ ├── find.ts
│ │ │ │ ├── insert.ts
│ │ │ │ └── update.ts
│ │ │ ├── eager-loading.ts
│ │ │ ├── graph-inserts.ts
│ │ │ ├── graph-upserts.ts
│ │ │ └── relation-queries/
│ │ │ ├── delete.ts
│ │ │ ├── find.ts
│ │ │ ├── insert.ts
│ │ │ ├── relate.ts
│ │ │ ├── unrelate.ts
│ │ │ └── update.ts
│ │ ├── transactions/
│ │ │ ├── creating.ts
│ │ │ └── using.ts
│ │ └── validation.ts
│ └── unit/
│ ├── model/
│ │ ├── AjvValidator.js
│ │ └── Model.js
│ ├── queryBuilder/
│ │ ├── JoinBuilder.js
│ │ ├── QueryBuilder.js
│ │ ├── ReferenceBuilder.js
│ │ ├── RelationExpression.js
│ │ ├── ValueBuilder.js
│ │ └── jsonFieldExpressionParser.js
│ ├── relations/
│ │ ├── BelongsToOneRelation.js
│ │ ├── HasManyRelation.js
│ │ ├── ManyToManyRelation.js
│ │ ├── Relation.js
│ │ └── files/
│ │ ├── InvalidModel.js
│ │ ├── InvalidModelManyNamedModels.js
│ │ ├── JoinModel.js
│ │ ├── ModelWithARandomError.js
│ │ ├── OwnerModel.js
│ │ ├── RelatedModel.js
│ │ └── RelatedModelNamedExport.js
│ ├── utils/
│ │ └── resolveModel.js
│ └── utils.js
├── tsconfig.json
└── typings/
└── objection/
└── index.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"parserOptions": {
"ecmaVersion": "latest"
},
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": "error",
"no-undef": "error"
},
"env": {
"es6": true,
"node": true,
"mocha": true
}
}
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
Please read the instructions linked below before opening an issue.
https://vincit.github.io/objection.js/guide/contributing.html#issues
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
================================================
FILE: .github/workflows/test.yml
================================================
name: tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
run-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 20.x, 22.x, 24.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run docker compose
run: docker compose up -d
- name: Setup test db
run: node setup-test-db.js
- name: Run tests
run: npm test
================================================
FILE: .gitignore
================================================
node_modules
.history
.idea
scratchpad.js
testCoverage
examples/*/*.db
examples/*/dist
examples/**/package-lock.json
*.iml
*.DS_Store
profiling
npm-debug.log*
.vscode
/**/yarn.lock
/**/.vuepress/dist
================================================
FILE: .prettierrc.json
================================================
{
"printWidth": 100,
"singleQuote": true,
"bracketSpacing": true,
"semi": true
}
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Sami Koskimäki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[](https://stand-with-ukraine.pp.ua)
[](https://github.com/Vincit/objection.js)
[](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)
Objection.js is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) for [Node.js](https://nodejs.org/) that aims to stay out of your way and make it as easy as possible to use the full power of SQL and the underlying database engine while still making the common stuff easy and enjoyable.
Even though ORM is the best commonly known acronym to describe objection, a more accurate description is to call it **a relational query builder**. You get all the benefits of an SQL query builder but also a powerful set of tools for working with relations.
Objection.js is built on an SQL query builder called [knex](http://knexjs.org). All databases supported by knex are supported by objection.js. **SQLite3**, **Postgres** and **MySQL** are [thoroughly tested](https://github.com/Vincit/objection.js/actions).
What objection.js gives you:
- **An easy declarative way of [defining models](https://vincit.github.io/objection.js/guide/models.html) and relationships between them**
- **Simple and fun way to [fetch, insert, update and delete](https://vincit.github.io/objection.js/guide/query-examples.html) objects using the full power of SQL**
- **Powerful mechanisms for [eager loading](https://vincit.github.io/objection.js/guide/query-examples.html#eager-loading), [inserting](https://vincit.github.io/objection.js/guide/query-examples.html#graph-inserts) and [upserting](https://vincit.github.io/objection.js/guide/query-examples.html#graph-upserts) object graphs**
- **Easy to use [transactions](https://vincit.github.io/objection.js/guide/transactions.html)**
- **Official [TypeScript](https://github.com/Vincit/objection.js/blob/main/typings/objection/index.d.ts) support**
- **Optional [JSON schema](https://vincit.github.io/objection.js/guide/validation.html) validation**
- **A way to [store complex documents](https://vincit.github.io/objection.js/guide/documents.html) as single rows**
What objection.js **doesn't** give you:
- **A fully object oriented view of your database**
With objection you don't work with entities. You work with queries. Objection doesn't try to wrap every concept with an
object oriented equivalent. The best attempt to do that (IMO) is Hibernate, which is excellent, but it has 800k lines
of code and a lot more concepts to learn than SQL itself. The point is, writing a good traditional ORM is borderline
impossible. Objection attempts to provide a completely different way of working with SQL.
- **A custom query DSL. SQL is used as a query language.**
This doesn't mean you have to write SQL strings though. A query builder based on [knex](http://knexjs.org) is
used to build the SQL. However, if the query builder fails you for some reason, raw SQL strings can be easily
written using the [raw](https://vincit.github.io/objection.js/api/objection/#raw) helper function.
- **Automatic database schema creation and migration from model definitions.**
For simple things it is useful that the database schema is automatically generated from the model definitions,
but usually just gets in your way when doing anything non-trivial. Objection.js leaves the schema related things
to you. knex has a great [migration tool](https://knexjs.org/guide/migrations.html) that we recommend for this job. Check
out the [example project](https://github.com/Vincit/objection.js/tree/main/examples/koa-ts).
The best way to get started is to clone our [example project](https://github.com/Vincit/objection.js/tree/main/examples/koa) and start playing with it. There's also a [typescript version](https://github.com/Vincit/objection.js/tree/main/examples/koa-ts) available.
Check out [this issue](https://github.com/Vincit/objection.js/issues/1069) to see who is using objection and what they think about it.
Shortcuts:
- [Who uses objection.js](https://github.com/Vincit/objection.js/discussions/2464)
- [API reference](https://vincit.github.io/objection.js/api/query-builder/)
- [Example projects](https://github.com/Vincit/objection.js/tree/main/examples)
- [Changelog](https://vincit.github.io/objection.js/release-notes/changelog.html)
- [v1 -> v2 -> v3 migration guide](https://vincit.github.io/objection.js/release-notes/migration.html)
- [Contribution guide](https://vincit.github.io/objection.js/guide/contributing.html)
- [Plugins](https://vincit.github.io/objection.js/guide/plugins.html)
================================================
FILE: doc/.prettierrc.json
================================================
{
"printWidth": 80,
"singleQuote": true,
"bracketSpacing": true,
"semi": true
}
================================================
FILE: doc/.vuepress/config.js
================================================
module.exports = {
title: 'Objection.js',
description: 'An SQL friendly ORM for node.js',
base: '/objection.js/',
themeConfig: {
repo: 'vincit/objection.js',
repoLabel: 'GitHub',
algolia: {
apiKey: '8b9b4ac9f68d11c702e8102479760861',
indexName: 'vincit_objectionjs',
},
nav: [
{
text: 'Guide',
link: '/guide/',
},
{
text: 'API Reference',
items: [
{
text: 'Main Module',
link: '/api/objection/',
},
{
text: 'Query Builder',
link: '/api/query-builder/',
},
{
text: 'Model',
link: '/api/model/',
},
{
text: 'Types',
link: '/api/types/',
},
],
},
{
text: 'Recipe Book',
link: '/recipes/',
},
{
text: 'Release Notes',
items: [
{
text: 'Changelog',
link: '/release-notes/changelog.md',
},
{
text: 'Migration to 3.0',
link: '/release-notes/migration.md',
},
{
text: 'v2.x documentation',
link: 'https://github.com/Vincit/objection.js/tree/v2/doc',
},
{
text: 'v1.x documentation',
link: 'https://github.com/Vincit/objection.js/tree/v1/doc',
},
],
},
{
text: '⭐ Star',
link: 'https://github.com/vincit/objection.js',
},
],
sidebar: {
'/guide/': [
{
title: 'Guide',
collapsable: false,
children: [
'installation',
'getting-started',
'models',
'relations',
'query-examples',
'transactions',
'hooks',
'validation',
'documents',
'plugins',
'contributing',
],
},
],
'/api/model/': [
{
title: 'Model API Reference',
collapsable: false,
children: [
'overview',
'static-properties',
'static-methods',
'instance-methods',
'instance-properties',
],
},
],
'/api/objection/': [
{
title: 'Objection API Reference',
collapsable: false,
},
],
'/api/query-builder/': [
{
title: 'Query Builder API Reference',
collapsable: false,
children: [
'find-methods',
'mutate-methods',
'eager-methods',
'join-methods',
'other-methods',
'static-methods',
],
},
],
'/recipes/': [
{
title: 'Recipes',
collapsable: false,
children: [
'raw-queries',
'precedence-and-parentheses',
'subqueries',
'relation-subqueries',
'joins',
'modifiers',
'composite-keys',
'polymorphic-associations',
'json-queries',
'custom-id-column',
'extra-properties',
'custom-validation',
'snake-case-to-camel-case-conversion',
'paging',
'returning-tricks',
'timestamps',
'custom-query-builder',
'multitenancy-using-multiple-databases',
'default-values',
'error-handling',
'ternary-relationships',
'indexing-postgresql-jsonb-columns',
],
},
],
},
},
};
================================================
FILE: doc/.vuepress/styles/index.styl
================================================
$bgColor = #1E1E1E;
$mediumBgColor = #252525;
$lightBgColor = #383838;
$accentColor = #e0b24d;
$textColor = #DDDDDD;
$borderColor = #383838;
html, body {
background-color: $bgColor !important;
}
a:focus, button:focus {
// Remove ugly mozilla outline.
outline: none;
}
.theme-container {
background-color: $bgColor;
h2 {
padding-bottom: 1rem;
}
h5 {
font-size: 14px;
}
.page {
background-color: $bgColor;
}
.navbar {
background-color: $mediumBgColor;
.links {
background-color: $mediumBgColor;
.search-box {
input {
background-color: $lightBgColor;
}
.suggestions {
background-color: $mediumBgColor;
border-color: $lightBgColor;
.suggestion.focused {
background-color: $lightBgColor;
}
}
}
}
}
code {
background-color: lighten($bgColor, 6%);
}
.warning, .tip {
code {
background-color: rgba(0, 0, 0, 0.15);
}
}
.sidebar {
background-color: $mediumBgColor;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.dropdown-wrapper {
.nav-dropdown {
background-color: $mediumBgColor;
border-color: $lightBgColor;
}
}
.custom-block {
color: $textColor;
&.tip {
background-color: $lightBgColor;
}
}
.home {
.hero {
.action-button {
color: $mediumBgColor;
}
}
}
th, td {
border-color: #666666;
}
tr:nth-child(2n) {
background-color: lighten($bgColor, 2%);
}
.algolia-search-wrapper .algolia-autocomplete .ds-dropdown-menu {
background-color: $bgColor;
border-color: $lightBgColor;
.algolia-docsearch-suggestion--category-header {
background-color: $bgColor;
color: $textColor;
border: none;
border-radius: unset;
}
.algolia-docsearch-suggestion--content:before {
background-color: $lightBgColor;
}
[class*=ds-dataset-] {
background-color: $bgColor;
}
.algolia-docsearch-suggestion {
background-color: $borderColor;
}
.algolia-docsearch-suggestion--subcategory-column {
background-color: lighten($bgColor, 2%);
}
.algolia-docsearch-suggestion--content {
background-color: lighten($bgColor, 2%);
}
.algolia-docsearch-suggestion--highlight {
color: $accentColor;
background-color: initial;
}
}
.algolia-search-wrapper .algolia-autocomplete .ds-dropdown-menu .ds-cursor .algolia-docsearch-suggestion--content {
background-color: $lightBgColor !important;
}
.algolia-autocomplete .ds-dropdown-menu:before {
display: none;
}
}
================================================
FILE: doc/.vuepress/styles/palette.styl
================================================
$accentColor = #e0b24d;
$textColor = #DDDDDD;
$borderColor = #383838;
$codeBgColor = #252525;
================================================
FILE: doc/.vuepress/theme/components/AlgoliaSearchBox.vue
================================================
================================================
FILE: doc/.vuepress/theme/index.js
================================================
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',
});
================================================
FILE: doc/README.md
================================================
---
home: true
heroText: Objection.js
tagline: An SQL-friendly ORM for Node.js
actionText: Get Started →
actionLink: /guide/installation
footer: MIT Licensed | Copyright © 2015-present Sami Koskimäki
---
Objection.js is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) for [Node.js](https://nodejs.org/) that aims to stay out of your way and make it as easy as possible to use the full power of SQL and the underlying database engine while still making the common stuff easy and enjoyable.
Even though ORM is the best commonly known acronym to describe objection, a more accurate description is to call it **a relational query builder**. You get all the benefits of an SQL query builder but also a powerful set of tools for working with relations.
Objection.js is built on an SQL query builder called [knex](https://knexjs.org). All databases supported by knex are supported by objection.js. **SQLite3**, **Postgres** and **MySQL** are [thoroughly tested](https://github.com/Vincit/objection.js/actions).
What objection.js gives you:
- **An easy declarative way of [defining models](/guide/models.html) and relationships between them**
- **Simple and fun way to [fetch, insert, update and delete](/guide/query-examples.html) objects using the full power of SQL**
- **Powerful mechanisms for [eager loading](/guide/query-examples.html#eager-loading), [inserting](/guide/query-examples.html#graph-inserts) and [upserting](/guide/query-examples.html#graph-upserts) object graphs**
- **Easy to use [transactions](/guide/transactions.html)**
- **Official [TypeScript](https://github.com/Vincit/objection.js/blob/main/typings/objection/index.d.ts) support**
- **Optional [JSON schema](/guide/validation.html) validation**
- **A way to [store complex documents](/guide/documents.html) as single rows**
What objection.js **doesn't** give you:
- **A custom query DSL. SQL is used as a query language.**
This doesn't mean you have to write SQL strings though. A query builder based on [knex](https://knexjs.org) is
used to build the SQL. However, if the query builder fails you for some reason, raw SQL strings can be easily
written using the [raw](/api/objection/#raw) helper function.
- **Automatic database schema creation and migration from model definitions.**
For simple things it is useful that the database schema is automatically generated from the model definitions,
but usually just gets in your way when doing anything non-trivial. Objection.js leaves the schema related things
to you. knex has a great [migration tool](https://knexjs.org/guide/migrations.html) that we recommend for this job. Check
out the [example project](https://github.com/Vincit/objection.js/tree/main/examples/koa-ts).
The best way to get started is to clone our [example project](https://github.com/Vincit/objection.js/tree/main/examples/koa) and start playing with it. There's also a [typescript version](https://github.com/Vincit/objection.js/tree/main/examples/koa-ts) available.
Check out [this issue](https://github.com/Vincit/objection.js/issues/1069) to see who is using objection and what they think about it.
================================================
FILE: doc/api/README.md
================================================
# 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/).
================================================
FILE: doc/api/model/instance-methods.md
================================================
# Instance Methods
All instance methods start with the character `$` to prevent them from colliding with the database column names.
## $query()
```js
const queryBuilder = person.$query(transactionOrKnex);
```
Creates a query builder for this model instance.
All queries built using the returned builder only affect this instance.
##### Arguments
| Argument | Type | Description |
| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| transactionOrKnex | object | Optional transaction or knex instance for the query. This can be used to specify a transaction or even a different database for a query. Falsy values are ignored. |
##### Return value
| Type | Description |
| ----------------------------------- | ------------- |
| [QueryBuilder](/api/query-builder/) | query builder |
##### Examples
Re-fetch an item from the database:
```js
// If you need to refresh the same instance you can do this:
const reFetchedPerson = await person.$query();
// Note that `person` did not get modified by the fetch.
person.$set(reFetchedPerson);
```
Insert a new item to database:
```js
const jennifer = await Person.fromJson({ firstName: 'Jennifer' })
.$query()
.insert();
console.log(jennifer.id);
```
Patch an item:
```js
await person.$query().patch({ lastName: 'Cooper' });
console.log('person updated');
```
Delete an item.
```js
await person.$query().delete();
console.log('person deleted');
```
## $relatedQuery()
```js
const builder = person.$relatedQuery(relationName, transactionOrKnex);
```
Use this to build a query that only affects items related through a relation.
See the examples below and [here](/guide/query-examples.html#relation-queries).
::: tip
This methods is just a shortcut for this call to the static [relatedQuery](/api/model/static-methods.html#static-relatedquery) method:
```js
const builder = Person.relatedQuery(relationName, transactionOrKnex).for(
person
);
```
:::
##### Arguments
| Argument | Type | Description |
| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| relationName | string | The name of the relation to query. |
| transactionOrKnex | object | Optional transaction or knex instance for the query. This can be used to specify a transaction or even a different database for a query. Falsy values are ignored. |
##### Return value
| Type | Description |
| ----------------------------------- | --------------- |
| [QueryBuilder](/api/query-builder/) | A query builder |
##### Examples
Fetch all items related to an item through a relation:
```js
const pets = await jennifer.$relatedQuery('pets');
console.log('jennifer has', pets.length, 'pets');
```
The related query is just like any other query. All knex and objection query builder methods are available:
```js
const dogsAndCats = await jennifer
.$relatedQuery('pets')
.select('animals.*', 'persons.name as ownerName')
.where('species', '=', 'dog')
.orWhere('breed', '=', 'cat')
.innerJoin('persons', 'persons.id', 'animals.ownerId')
.orderBy('animals.name');
// All the dogs and cats have the owner's name "Jennifer"
// joined as the `ownerName` property.
console.log(dogsAndCats);
```
This inserts a new item to the database and binds it to the owner item as defined by the relation (by default):
```js
const waldo = await jennifer
.$relatedQuery('pets')
.insert({ species: 'dog', name: 'Fluffy' });
console.log(waldo.id);
```
To attach an existing item to a relation the `relate` method can be used. In this example the dog `fluffy` already exists in the database but it isn't related to `jennifer` through the `pets` relation. We can make the connection like this:
```js
await jennifer.$relatedQuery('pets').relate(fluffy.id);
console.log('fluffy is now related to jennifer through pets relation');
```
The connection can be removed using the `unrelate` method. Again, this doesn't delete the related model. Only the connection is removed. For example in the case of ManyToMany relation the join table entries are deleted.
```js
await jennifer
.$relatedQuery('pets')
.unrelate()
.where('id', fluffy.id);
console.log('jennifer no longer has fluffy as a pet');
```
Related items can be deleted using the delete method. Note that in the case of ManyToManyRelation the join table entries are not deleted. You should use `ON DELETE CASCADE` in your database migrations to make the database properly delete the join table rows when either end of the relation is deleted. Naturally the delete query can be chained with any query building methods.
```js
await jennifer
.$relatedQuery('pets')
.delete()
.where('species', 'cat');
console.log('jennifer no longer has any cats');
```
`update` and `patch` can be used to update related models. Only difference between the mentioned methods is that `update` validates the input objects using the related model class's full schema and `patch` ignores the `required` property of the schema. Use `update` when you want to update _all_ properties of a model and `patch` when only a subset should be updated.
```js
const updatedFluffy = await jennifer
.$relatedQuery('pets')
.update({ species: 'dog', name: 'Fluffy the great', vaccinated: false })
.where('id', fluffy.id);
console.log("fluffy's new name is", updatedFluffy.name);
// This query will be rejected assuming that `name` or `species`
// is a required property for an Animal.
await jennifer
.$relatedQuery('pets')
.update({ vaccinated: true })
.where('species', 'dog');
// This query will succeed.
await jennifer
.$relatedQuery('pets')
.patch({ vaccinated: true })
.where('species', 'dog');
console.log('jennifer just got all her dogs vaccinated');
```
## $beforeInsert()
```js
class Person extends Model {
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
await this.doPossiblyAsyncStuff();
}
}
```
Called before a model is inserted into the database.
You can return a promise from this function if you need to do asynchronous stuff. You can also throw an exception to abort the insert and reject the query. This can be useful if you need to do insert specific validation.
If you start a query from this hook, make sure you specify `queryContext.transaction` as it's connection to make sure the query takes part in the same transaction as the parent query. See the example below.
##### Arguments
| Argument | Type | Description |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------- |
| queryContext | Object | The context object of the insert query. See [context](/api/query-builder/other-methods.html#context). |
##### Return value
| Type | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------ |
| [Promise](http://bluebirdjs.com/docs/getting-started.html)
void | Promise or void depending whether your hook is async or not. |
##### Examples
The current query's transaction/knex instance can always be accessed through `queryContext.transaction`.
```js
class Person extends Model {
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
// This can always be done even if there is no running
// transaction. In that case `queryContext.transaction`
// returns the normal knex instance. This makes sure that
// the query is not executed outside the original query's
// transaction.
await SomeModel.query(queryContext.transaction).insert(whatever);
}
}
```
## $afterInsert()
```js
class Person extends Model {
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
await this.doPossiblyAsyncStuff();
}
}
```
Called after a model has been inserted into the database.
You can return a promise from this function if you need to do asynchronous stuff.
##### Arguments
| Argument | Type | Description |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------- |
| queryContext | Object | The context object of the insert query. See [context](/api/query-builder/other-methods.html#context). |
##### Return value
| Type | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------ |
| [Promise](http://bluebirdjs.com/docs/getting-started.html)
void | Promise or void depending whether your hook is async or not. |
##### Examples
The current query's transaction/knex instance can always be accessed through `queryContext.transaction`.
```js
class Person extends Model {
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
// This can always be done even if there is no running transaction. In that
// case `queryContext.transaction` returns the normal knex instance. This
// makes sure that the query is not executed outside the original query's
// transaction.
await SomeModel.query(queryContext.transaction).insert(whatever);
}
}
```
## $beforeUpdate()
```js
class Person extends Model {
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.doPossiblyAsyncStuff();
}
}
```
Called before a model instance is updated.
You can return a promise from this function if you need to do asynchronous stuff. You can also throw an exception to abort the update and reject the query. This can be useful if
you need to do update specific validation.
This method is also called before a model is patched. Therefore all the model's properties may not exist. You can check if the update operation is a patch by checking the `opt.patch` boolean.
Inside the hook, `this` contains the values to be updated. If (and only if) the query is started for an existing model instance using [\$query](/api/model/instance-methods.html#query), `opt.old` object contains the old values. The old values are never fetched from the database implicitly. For non-instance queries the `opt.old` object is `undefined`. See the examples.
##### Arguments
| Argument | Type | Description |
| ------------ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| opt | [ModelOptions](/api/types/#type-modeloptions) | Update options. |
| queryContext | Object | The context object of the update query. See [context](/api/query-builder/other-methods.html#context). |
##### Return value
| Type | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------ |
| [Promise](http://bluebirdjs.com/docs/getting-started.html)
void | Promise or void depending whether your hook is async or not. |
##### Examples
The current query's transaction/knex instance can always be accessed through `queryContext.transaction`.
```js
class Person extends Model {
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
// This can always be done even if there is no running transaction.
// In that case `queryContext.transaction` returns the normal knex
// instance. This makes sure that the query is not executed outside
// the original query's transaction.
await SomeModel.query(queryContext.transaction).insert(whatever);
}
}
```
Note that the `opt.old` object is only populated for instance queries started with `$query`:
```js
somePerson.$query().update(newValues);
```
For the following query `opt.old` is `undefined` because there is no old object in the JavaScript side. objection.js doesn't fetch the old values even if they existed in the database
for performance and simplicity reasons.
```js
Person.query()
.update(newValues)
.where('foo', 'bar');
```
## $afterUpdate()
```js
class Person extends Model {
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
await this.doPossiblyAsyncStuff();
}
}
```
Called after a model instance is updated.
You can return a promise from this function if you need to do asynchronous stuff.
This method is also called after a model is patched. Therefore all the model's properties may not exist. You can check if the update operation is a patch by checking the `opt.patch` boolean.
Inside the hook, `this` contains the values to be updated. If (and only if) the query is started for an existing model instance using [\$query](/api/model/instance-methods.html#query), `opt.old` object contains the old values. The old values are never fetched from the database implicitly. For non-instance queries the `opt.old` object is `undefined`. See the examples.
##### Arguments
| Argument | Type | Description |
| ------------ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| opt | [ModelOptions](/api/types/#type-modeloptions) | Update options. |
| queryContext | Object | The context object of the update query. See [context](/api/query-builder/other-methods.html#context). |
##### Return value
| Type | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------ |
| [Promise](http://bluebirdjs.com/docs/getting-started.html)
void | Promise or void depending whether your hook is async or not. |
##### Examples
The current query's transaction/knex instance can always be accessed through `queryContext.transaction`.
```js
class Person extends Model {
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
// This can always be done even if there is no running transaction.
// In that case `queryContext.transaction` returns the normal knex
// instance. This makes sure that the query is not executed
// outside the original query's transaction.
await SomeModel.query(queryContext.transaction).insert(whatever);
}
}
```
Note that the `opt.old` object is only populated for instance queries started with `$query`:
```js
somePerson.$query().update(newValues);
```
For the following query `opt.old` is `undefined` because there is no old object in the JavaScript side. objection.js doesn't fetch the old values even if they existed in the database for performance and simplicity reasons.
```js
Person.query()
.update(newValues)
.where('foo', 'bar');
```
## $beforeDelete()
```js
class Person extends Model {
async $beforeDelete(queryContext) {
await super.$beforeDelete(queryContext);
await doPossiblyAsyncStuff();
}
}
```
Called before a model is deleted.
You can return a promise from this function if you need to do asynchronous stuff.
::: warning
This method is only called for instance deletes started with [\$query()](/api/model/instance-methods.html#query) method. All hooks are instance methods. For deletes there is no instance for which to call the hook, except when [\$query()](/api/model/instance-methods.html#query) is used. Objection doesn't fetch the item just to call the hook for it to ensure predictable performance and prevent a whole class of concurrency bugs.
:::
##### Arguments
| Argument | Type | Description |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------- |
| queryContext | Object | The context object of the update query. See [context](/api/query-builder/other-methods.html#context). |
##### Return value
| Type | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------ |
| [Promise](http://bluebirdjs.com/docs/getting-started.html)
void | Promise or void depending whether your hook is async or not. |
##### Examples
The current query's transaction/knex instance can always be accessed through `queryContext.transaction`.
```js
class Person extends Model {
async $beforeDelete(queryContext) {
await super.$beforeDelete(queryContext);
// This can always be done even if there is no running transaction.
// In that case `queryContext.transaction` returns the normal knex
// instance. This makes sure that the query is not executed outside
// the original query's transaction.
await SomeModel.query(queryContext.transaction).insert(whatever);
}
}
```
## $afterDelete()
```js
class Person extends Model {
async $afterDelete(queryContext) {
await super.$afterDelete(queryContext);
await this.doPossiblyAsyncStuff();
}
}
```
Called after a model is deleted.
You can return a promise from this function if you need to do asynchronous stuff.
::: warning
This method is only called for instance deletes started with [\$query()](/api/model/instance-methods.html#query) method. All hooks are instance methods. For deletes there is no instance for which to call the hook, except when [\$query()](/api/model/instance-methods.html#query) is used. Objection doesn't fetch the item just to call the hook for it to ensure predictable performance and prevent a whole class of concurrency bugs.
:::
##### Arguments
| Argument | Type | Description |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------- |
| queryContext | Object | The context object of the update query. See [context](/api/query-builder/other-methods.html#context). |
##### Return value
| Type | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------ |
| [Promise](http://bluebirdjs.com/docs/getting-started.html)
void | Promise or void depending whether your hook is async or not. |
##### Examples
The current query's transaction/knex instance can always be accessed through `queryContext.transaction`.
```js
class Person extends Model {
async $afterDelete(queryContext) {
await super.$afterDelete(queryContext);
// This can always be done even if there is no running transaction. In that
// case `queryContext.transaction` returns the normal knex instance. This
// makes sure that the query is not executed outside the original query's
// transaction.
await SomeModel.query(queryContext.transaction).insert(whatever);
}
}
```
## $afterFind()
```js
class Person extends Model {
$afterFind(queryContext) {
return doPossiblyAsyncStuff();
}
}
```
Called after a model is fetched.
This method is _not_ called for insert, update or delete operations.
You can return a promise from this function if you need to do asynchronous stuff.
##### Arguments
| Argument | Type | Description |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------- |
| queryContext | Object | The context object of the update query. See [context](/api/query-builder/other-methods.html#context). |
##### Return value
| Type | Description |
| ------------------------------------------------------------------ | ------------------------------------------------------------ |
| [Promise](http://bluebirdjs.com/docs/getting-started.html)
void | Promise or void depending whether your hook is async or not. |
##### Examples
The current query's transaction/knex instance can always be accessed through `queryContext.transaction`.
```js
class Person extends Model {
$afterFind(queryContext) {
// This can always be done even if there is no running transaction.
// In that case `queryContext.transaction` returns the normal knex
// instance. This makes sure that the query is not executed outside
// the original query's transaction.
return SomeModel.query(queryContext.transaction).insert(whatever);
}
}
```
## $clone()
```js
const clone = modelInstance.$clone(options);
```
Returns a (deep) copy of a model instance.
If the item to be cloned has instances of [Model](/api/model/) as properties (or arrays of them) they are cloned using their `$clone()` method. A shallow copy without relations can be created by passing the `shallow: true` option.
##### Arguments
| Argument | Type | Description |
| -------- | --------------------------------------------- | ---------------- |
| opt | [CloneOptions](/api/types/#type-cloneoptions) | Optional options |
##### Return value
| Type | Description |
| -------------------- | -------------------- |
| [Model](/api/model/) | Deep clone of `this` |
##### Examples
```js
const shallowClone = modelInstance.$clone({ shallow: true });
```
## toJSON()
```js
const jsonObj = modelInstance.toJSON(opt);
```
Exports this model as a JSON object.
See [this section](/api/model/overview.html#model-data-lifecycle) for more information.
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------- | ---------------- |
| opt | [ToJsonOptions](/api/types/#type-tojsonoptions) | Optional options |
##### Return value
| Type | Description |
| ------ | ----------------------- |
| Object | Model as a JSON object. |
##### Examples
```js
const shallowObj = modelInstance.toJSON({ shallow: true, virtuals: false });
```
```js
const onlySomeVirtuals = modelInstance.toJSON({ virtuals: ['fullName'] });
```
## $toJson()
Alias for [toJSON](/api/model/instance-methods.html#tojson)
## $toDatabaseJson()
```js
const row = modelInstance.$toDatabaseJson();
```
Exports this model as a database JSON object.
This method is called internally to convert a model into a database row.
See [this section](/api/model/overview.html#model-data-lifecycle) for more information.
##### Return value
| Type | Description |
| ------ | ------------- |
| Object | Database row. |
## $parseDatabaseJson()
```js
class Person extends Model {
$parseDatabaseJson(json) {
// Remember to call the super class's implementation.
json = super.$parseDatabaseJson(json);
// Do your conversion here.
return json;
}
}
```
This is called when a [Model](/api/model/) instance is created from a database JSON object. This method converts the JSON object from the database format to the internal format.
You can override this method to carry out whatever conversions you want for the data when it's fetched from the database, before it's converted into a model instance. See [this section](/api/model/overview.html#model-data-lifecycle) for more information.
There are a couple of requirements for the implementation:
1. This function must be pure. It shouldn't have any side effects because it is called from "unexpected" places (for example to determine if your model somehow transforms column names between db and code).
2. This function must be able to handle any subset of the model's properties coming in.You cannot assume that some column is present in the `json` object as it depends on the select statement. There can also be additional columns because of joins, aliases etc. This method must also be prepared for null values in _any_ property of the `json` object.
##### Arguments
| Argument | Type | Description |
| -------- | ------ | -------------------------------- |
| json | Object | The JSON POJO in database format |
##### Return value
| Type | Description |
| ------ | -------------------------------- |
| Object | The JSON POJO in internal format |
## $formatDatabaseJson()
```js
class Person extends Model {
$formatDatabaseJson(json) {
// Remember to call the super class's implementation.
json = super.$formatDatabaseJson(json);
// Do your conversion here.
return json;
}
}
```
This is called when a [Model](/api/model/) is converted to database format.
You can override this method to carry out whatever conversions you want for the data when it's being sent to the database driver. See [this section](/api/model/overview.html#model-data-lifecycle) for more information.
There are a couple of requirements for the implementation:
1. This function must be pure. It shouldn't have any side effects because it is called from "unexpected" places (for example to determine if your model somehow transforms column names between db and code).
2. This function must be able to handle any subset of the model's properties coming in. You cannot assume that some property is present in the `json` object. There can also be additional properties. This method must also be prepared for null values in _any_ property of the `json` object.
##### Arguments
| Argument | Type | Description |
| -------- | ------ | -------------------------------- |
| json | Object | The JSON POJO in internal format |
##### Return value
| Type | Description |
| ------ | -------------------------------- |
| Object | The JSON POJO in database format |
## $parseJson()
```js
class Person extends Model {
$parseJson(json, opt) {
// Remember to call the super class's implementation.
json = super.$parseJson(json, opt);
// Do your conversion here.
return json;
}
}
```
This is called when a [Model](/api/model/) is created from a JSON object. Converts the JSON object from the external format to the internal format.
You can override this method to carry out whatever conversions you want for the data when a model instance is being created from external data. See [this section](/api/model/overview.html#model-data-lifecycle) for more information.
There are a couple of requirements for the implementation:
1. This function must be pure. It shouldn't have any side effects because it is called from "unexpected" places (for example to determine if your model somehow transforms column names between db and code).
2. This function must be able to handle any subset of the model's properties coming in. You cannot assume that some property is present in the `json` object. There can also be additional properties. This method must also be prepared for null values in _any_ property of the `json` object.
##### Arguments
| Argument | Type | Description |
| -------- | --------------------------------------------- | -------------------------------- |
| json | Object | The JSON POJO in external format |
| opt | [ModelOptions](/api/types/#type-modeloptions) | Optional options |
##### Return value
| Type | Description |
| ------ | -------------------------------- |
| Object | The JSON POJO in internal format |
## $formatJson()
```js
class Person extends Model {
$formatJson(json) {
// Remember to call the super class's implementation.
json = super.$formatJson(json);
// Do your conversion here.
return json;
}
}
```
This is called when a [Model](/api/model/) is converted to JSON. Converts the JSON object from the internal format to the external format.
You can override this method to carry out whatever conversions you want for the data when a model instance is being converted into external representation. See [this section](/api/model/overview.html#model-data-lifecycle) for more information.
There are a couple of requirements for the implementation:
1. This function must be pure. It shouldn't have any side effects because it is called from "unexpected" places (for example to determine if your model somehow transforms column names between db and code).
2. This function must be able to handle any subset of the model's properties coming in. You cannot assume that some column is present in the `json` object as it depends on the select statement. There can also be additional columns because of joins, aliases etc. This method must also be prepared for null values in _any_ property of the `json` object.
##### Arguments
| Argument | Type | Description |
| -------- | ------ | -------------------------------- |
| json | Object | The JSON POJO in internal format |
##### Return value
| Type | Description |
| ------ | -------------------------------- |
| Object | The JSON POJO in external format |
## $setJson()
```js
modelInstance.$setJson(json, opt);
```
Sets the values from a JSON object.
Validates the JSON before setting values.
##### Arguments
| Argument | Type | Description |
| -------- | --------------------------------------------- | -------------------- |
| json | Object | The JSON POJO to set |
| opt | [ModelOptions](/api/types/#type-modeloptions) | Optional options |
##### Return value
| Type | Description |
| -------------------- | ------------------- |
| [Model](/api/model/) | `this` for chaining |
## $setDatabaseJson()
```js
modelInstance.$setDatabaseJson(json);
```
Sets the values from a JSON object in database format.
##### Arguments
| Argument | Type | Description |
| -------- | ------ | -------------------------------- |
| json | Object | The JSON POJO in database format |
##### Return value
| Type | Description |
| -------------------- | ------------------- |
| [Model](/api/model/) | `this` for chaining |
## $set()
```js
modelInstance.$set(json);
```
Sets the values from another model instance or object.
Unlike [\$setJson](/api/model/instance-methods.html#setjson), this doesn't call any [\$parseJson](/api/model/instance-methods.html#parsejson) hooks or validate the input. This simply sets each value in the object to this object.
##### Arguments
| Argument | Type | Description |
| -------- | ------ | ----------------- |
| obj | Object | The values to set |
##### Return value
| Type | Description |
| -------------------- | ------------------- |
| [Model](/api/model/) | `this` for chaining |
## $setRelated()
```js
modelInstance.$setRelated(relation, relatedModels);
```
Sets related models to a corresponding property in the object.
##### Arguments
| Argument | Type | Description |
| ------------- | -------------------------------------------------- | -------------------------------------------- |
| relation | string|[Relation](/api/types/#class-relation) | Relation name or a relation instance to set. |
| relatedModels | [Model](/api/model/)|[Model](/api/model/)[] | Models to set. |
##### Return value
| Type | Description |
| -------------------- | ------------------- |
| [Model](/api/model/) | `this` for chaining |
##### Examples
```js
person.$setRelated('parent', parent);
console.log(person.parent);
```
```js
person.$setRelated('children', children);
console.log(person.children[0]);
```
## $appendRelated()
```js
modelInstance.$appendRelated(relation, relatedModels);
```
Appends related models to a corresponding property in the object.
##### Arguments
| Argument | Type | Description |
| ------------- | -------------------------------------------------- | -------------------------------------------------- |
| relation | string|[Relation](/api/types/#class-relation) | Relation name or a relation instance to append to. |
| relatedModels | [Model](/api/model/)|[Model](/api/model/)[] | Models to append. |
##### Return value
| Type | Description |
| -------------------- | ------------------- |
| [Model](/api/model/) | `this` for chaining |
##### Examples
```js
person.$appendRelated('parent', parent);
console.log(person.parent);
```
```js
person.$appendRelated('children', child1);
person.$appendRelated('children', child2);
child1 = person.children[person.children.length - 1];
child2 = person.children[person.children.length - 2];
```
## $fetchGraph()
```js
const builder = person.$fetchGraph(expression, options);
```
Shortcut for [Person.fetchGraph(person, options)](/api/model/static-methods.html#static-fetchgraph)
## $traverse()
```js
person.$traverse(filterConstructor, callback);
```
Shortcut for [Model.traverse(filterConstructor, this, callback)](/api/model/static-methods.html#static-traverse).
## $traverseAsync()
```js
person.$traverseAsync(filterConstructor, callback);
```
Shortcut for [Model.traverseAsync(filterConstructor, this, callback)](/api/model/static-methods.html#static-traverseasync).
## $knex()
```js
const knex = person.$knex();
```
Shortcut for [Person.knex()](/api/model/static-methods.html#static-knex).
## $transaction()
```js
const knex = person.$transaction();
```
Shortcut for [Person.knex()](/api/model/static-methods.html#static-knex).
## $id()
```js
console.log(model.$id()); // -> 100
// Sets the id.
model.$id(100);
```
Returns or sets the identifier of a model instance.
The identifier property does not have to be accessed or set using this method.
If the identifier property is known it can be accessed or set just like any other property. You don't need to use this method to set the identifier. This method is mainly helpful when building plugins and other tools on top of objection.
##### Examples
Composite key
```js
console.log(model.$id()); // -> [100, 20, 30]
// Sets the id.
model.$id([100, 20, 30]);
```
## $beforeValidate()
```js
class Person extends Model {
$beforeValidate(jsonSchema, json, opt) {
return jsonSchema;
}
}
```
This is called before validation.
You can add any additional validation to this method. If validation fails, simply throw an exception and the query will be rejected. If you modify the `jsonSchema` argument and return it, that one will be used to validate the model.
`opt.old` object contains the old values while `json` contains the new values if validation is being done for an existing object.
##### Arguments
| Argument | Type | Description |
| ---------- | --------------------------------------------- | --------------------------------------- |
| jsonSchema | Object | A deep clone of this class's jsonSchema |
| json | Object | The JSON object to be validated |
| opt | [ModelOptions](/api/types/#type-modeloptions) | Optional options |
##### Return value
| Type | Description |
| ------ | ------------------------------------------------ |
| Object | The modified jsonSchema or the input jsonSchema. |
## $afterValidate()
```js
class Person extends Model {
$afterValidate(json, opt) {}
}
```
This is called after successful validation.
You can do further validation here and throw an error if something goes wrong.
`opt.old` object contains the old values while `json` contains the new values if validation is being done for an existing object.
##### Arguments
| Argument | Type | Description |
| -------- | --------------------------------------------- | ------------------------------- |
| json | Object | The JSON object to be validated |
| opt | [ModelOptions](/api/types/#type-modeloptions) | Optional options |
## $validate()
```js
modelInstance.$validate();
```
Validates the model instance.
Calls [\$beforeValidate](/api/model/instance-methods.html#beforevalidate) and [\$afterValidate](/api/model/instance-methods.html#aftervalidate) methods. This method is called automatically from [fromJson](/api/model/static-methods.html#static-fromjson) and [\$setJson](/api/model/instance-methods.html#setjson) methods. This method can also be
called explicitly when needed.
##### Throws
| Type | Description |
| ---------------------------------------------------- | -------------------- |
| [ValidationError](/api/types/#class-validationerror) | If validation fails. |
## $omitFromJson()
```js
modelInstance.$omitFromJson(props);
```
Omits a set of properties when converting the model to JSON.
##### Arguments
| Argument | Type | Description |
| -------- | -------------------------------------------------------- | ------------- |
| props | string
string[]
Object<string, boolean> | props to omit |
##### Return value
| Type | Description |
| -------------------- | ------------------- |
| [Model](/api/model/) | `this` for chaining |
##### Examples
```js
const json = person
.fromJson({ firstName: 'Jennifer', lastName: 'Lawrence', age: 24 })
.$omitFromJson('lastName')
.$toJson();
console.log(_.has(json, 'lastName')); // --> false
```
```js
const json = person
.fromJson({ firstName: 'Jennifer', lastName: 'Lawrence', age: 24 })
.$omitFromJson(['lastName'])
.$toJson();
console.log(_.has(json, 'lastName')); // --> false
```
```js
const json = person
.fromJson({ firstName: 'Jennifer', lastName: 'Lawrence', age: 24 })
.$omitFromJson({ lastName: true })
.$toJson();
console.log(_.has(json, 'lastName')); // --> false
```
## $omitFromDatabaseJson()
```js
modelInstance.$omitFromDatabaseJson(props);
```
Omits a set of properties when converting the model to database JSON.
##### Arguments
| Argument | Type | Description |
| -------- | -------------------------------------------------------- | ------------- |
| props | string
string[]
Object<string, boolean> | props to omit |
##### Return value
| Type | Description |
| -------------------- | ------------------- |
| [Model](/api/model/) | `this` for chaining |
##### Examples
```js
const json = person
.fromJson({ firstName: 'Jennifer', lastName: 'Lawrence', age: 24 })
.$omitFromDatabaseJson('lastName')
.$toDatabaseJson();
console.log(_.has(json, 'lastName')); // --> false
```
```js
const json = person
.fromJson({ firstName: 'Jennifer', lastName: 'Lawrence', age: 24 })
.$omitFromJson(['lastName'])
.$toDatabaseJson();
console.log(_.has(json, 'lastName')); // --> false
```
```js
const json = person
.fromJson({ firstName: 'Jennifer', lastName: 'Lawrence', age: 24 })
.$omitFromJson({ lastName: true })
.$toDatabaseJson();
console.log(_.has(json, 'lastName')); // --> false
```
================================================
FILE: doc/api/model/instance-properties.md
================================================
# Instance Properties
All instance properties start with the character `$` to prevent them from colliding with the database column names.
## $modelClass
```js
const modelClass = person.$modelClass;
```
Returns the class of the model instance. The return value is equal to `this.constructor`. This is mainly useful with typescript where the type is `ModelClass` which is more useful than the `Function` type of `this.constructor`.
##### Examples
```js
const person = Person.fromJson({ id: 1 });
console.log(person.$modelClass === Person); // prints true
```
================================================
FILE: doc/api/model/overview.md
================================================
# Overview
## Model data lifecycle
For the purposes of this explanation, let’s define three data layouts:
1. `database`: The data layout returned by the database.
2. `internal`: The data layout of a model instance.
3. `external`: The data layout after calling model.toJSON().
Whenever data is converted from one layout to another, converter methods are called:
1. `database` -> [\$parseDatabaseJson](/api/model/instance-methods.html#parsedatabasejson) -> `internal`
2. `internal` -> [\$formatDatabaseJson](/api/model/instance-methods.html#formatdatabasejson) -> `database`
3. `external` -> [\$parseJson](/api/model/instance-methods.html#parsejson) -> `internal`
4. `internal` -> [\$formatJson](/api/model/instance-methods.html#formatjson) -> `external`
So for example when the results of a query are read from the database the data goes through the [\$parseDatabaseJson](/api/model/instance-methods.html#parsedatabasejson) method. When data is written to database it goes through the [\$formatDatabaseJson](/api/model/instance-methods.html#formatdatabasejson) method.
Similarly when you give data for a query (for example [`query().insert(req.body)`](/api/query-builder/mutate-methods.html#insert)) or create a model explicitly using [`Model.fromJson(obj)`](/api/model/static-methods.html#static-fromjson) the [\$parseJson](/api/model/instance-methods.html#parsejson) method is invoked. When you call [`model.toJSON()`](/api/model/instance-methods.html#tojson) or [`model.$toJson()`](/api/model/instance-methods.html#tojson) the [\$formatJson](/api/model/instance-methods.html#formatjson) is called.
Note: Most libraries like [express](https://expressjs.com/en/index.html) and [koa](https://koajs.com/) automatically call the [toJSON](/api/model/instance-methods.html#tojson) method when you pass the model instance to methods like `response.json(model)`. You rarely need to call [toJSON()](/api/model/instance-methods.html#tojson) or [\$toJson()](/api/model/instance-methods.html#tojson) explicitly.
By overriding the lifecycle methods, you can have different layouts for the data in database and when exposed to the outside world.
All instance methods of models are prefixed with `$` letter so that they won’t overlap with database properties. All properties that start with `$` are also removed from `database` and `external` layouts.
In addition to these data formatting hooks, Model also has query lifecycle hooks
- [\$beforeUpdate](/api/model/instance-methods.html#beforeupdate)
- [\$afterUpdate](/api/model/instance-methods.html#afterupdate)
- [\$beforeInsert](/api/model/instance-methods.html#beforeinsert)
- [\$afterInsert](/api/model/instance-methods.html#afterinsert)
- [\$beforeDelete](/api/model/instance-methods.html#beforedelete)
- [\$afterDelete](/api/model/instance-methods.html#afterdelete)
- [\$afterFind](/api/model/instance-methods.html#afterfind)
================================================
FILE: doc/api/model/static-methods.md
================================================
# Static Methods
## `static` query()
```js
const queryBuilder = Person.query(transactionOrKnex);
```
Creates a query builder for the model's table.
All query builders are created using this function, including `$query`, `relatedQuery` and `$relatedQuery`. That means you can modify each query by overriding this method for your model class.
See the [query examples](/guide/query-examples.html) section for more examples.
#### Arguments
| Argument | Type | Description |
| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| transactionOrKnex | object | Optional transaction or knex instance for the query. This can be used to specify a transaction or even a different database. for a query. Falsy values are ignored. |
#### Return value
| Type | Description |
| ----------------------------------- | ------------------------- |
| [QueryBuilder](/api/query-builder/) | The created query builder |
#### Examples
Read models from the database:
```js
// Get all rows.
const people = await Person.query();
console.log('there are', people.length, 'people in the database');
// Example of a more complex WHERE clause. This generates:
// SELECT "persons".*
// FROM "persons"
// WHERE ("firstName" = 'Jennifer' AND "age" < 30)
// OR ("firstName" = 'Mark' AND "age" > 30)
const marksAndJennifers = await Person.query()
.where(builder => {
builder.where('firstName', 'Jennifer').where('age', '<', 30);
})
.orWhere(builder => {
builder.where('firstName', 'Mark').where('age', '>', 30);
});
console.log(marksAndJennifers);
// Get a subset of rows and fetch related models
// for each row.
const oldPeople = await Person.query()
.where('age', '>', 60)
.withGraphFetched('children.children.movies');
console.log(
"some old person's grand child has appeared in",
oldPeople[0].children[0].children[0].movies.length,
'movies'
);
```
Insert models to the database:
```js
const sylvester = await Person.query().insert({
firstName: 'Sylvester',
lastName: 'Stallone'
});
console.log(sylvester.fullName());
// --> 'Sylvester Stallone'.
// Batch insert. This only works on Postgresql as it is
// the only database that returns the identifiers of
// _all_ inserted rows. If you need to do batch inserts
// on other databases useknex* directly.
// (See .knexQuery() method).
const inserted = await Person.query().insert([
{ firstName: 'Arnold', lastName: 'Schwarzenegger' },
{ firstName: 'Sylvester', lastName: 'Stallone' }
]);
console.log(inserted[0].fullName()); // --> 'Arnold Schwarzenegger'
```
`update` and `patch` can be used to update models. Only difference between the mentioned methods is that `update` validates the input objects using the model class's full jsonSchema and `patch` ignores the `required` property of the schema. Use `update` when you want to update _all_ properties of a model and `patch` when only a subset should be updated.
```js
const numUpdatedRows = await Person.query()
.update({ firstName: 'Jennifer', lastName: 'Lawrence', age: 35 })
.where('id', jennifer.id);
console.log(numUpdatedRows);
// This will throw assuming that `firstName` or `lastName`
// is a required property for a Person.
await Person.query().update({ age: 100 });
// This will _not_ throw.
await Person.query().patch({ age: 100 });
console.log('Everyone is now 100 years old');
```
Models can be deleted using the delete method. Naturally the delete query can be chained with any knex\* methods:
```js
await Person.query()
.delete()
.where('age', '>', 90);
console.log('anyone over 90 is now removed from the database');
```
## `static` relatedQuery()
```js
const queryBuilder = Person.relatedQuery(relationName, transactionOrKnex);
```
Creates a query builder that can be used to query a relation of an item (or items).
This method is best explained through examples. See the examples below and the following sections:
- [relation queries](/guide/query-examples.html#relation-queries)
- [relation subqueries recipe](/recipes/relation-subqueries.html)
##### Arguments
| Argument | Type | Description |
| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| relationName | string | The name of the relation to query. |
| transactionOrKnex | object | Optional transaction or knex instance for the query. This can be used to specify a transaction or even a different database for a query. Falsy values are ignored. |
##### Return value
| Type | Description |
| ----------------------------------- | ------------------------- |
| [QueryBuilder](/api/query-builder/) | The created query builder |
##### Examples
This example fetches `pets` for a person with id 1. `pets` is the name of the relation defined in [relationMappings](/api/model/static-properties.html#static-relationmappings).
```js
const personId = 1;
const pets = await Person.relatedQuery('pets').for(personId);
```
```sql
select "animals".* from "animals"
where "animals"."ownerId" = 1
```
Just like to any query, you can chain any methods. The following example only fetches dogs and sorts them by name:
```js
const dogs = await Person.relatedQuery('pets')
.for(1)
.where('species', 'dog')
.orderBy('name');
```
```sql
select "animals".* from "animals"
where "species" = 'dog'
and "animals"."ownerId" = 1
order by "name" asc
```
If you want to fetch dogs of multiple people in one query, you can pass an array of identifiers to the [for](/api/query-builder/other-methods.html#for) method like this:
```js
const dogs = await Person.relatedQuery('pets')
.for([1, 2])
.where('species', 'dog')
.orderBy('name');
```
```sql
select "animals".* from "animals"
where "species" = 'dog'
and "animals"."ownerId" in (1, 2)
order by "name" asc
```
You can even give it a subquery! The following example fetches all dogs of all people named Jennifer.
```js
// Note that there is no `await` here. This query does not get executed.
const jennifers = Person.query().where('name', 'Jennifer');
// This is the only executed query in this example.
const allDogsOfAllJennifers = await Person.relatedQuery('pets')
.for(jennifers)
.where('species', 'dog')
.orderBy('name');
```
```sql
select "animals".* from "animals"
where "species" = 'dog'
and "animals"."ownerId" in (
select "persons"."id"
from "persons"
where "name" = 'Jennifer'
)
order by "name" asc
```
`relatedQuery` also works with `relate` , `unrelate`, `delete` and all other query methods. The following example relates a person with id 100 to a movie with id 200 for the many-to-many relation `movies`:
```js
await Person.relatedQuery('movies')
.for(100)
.relate(200);
```
```sql
insert into "persons_movies" ("personId", "movieId") values (100, 200)
```
See more examples [here](/guide/query-examples.html#relation-queries).
`relatedQuery` can also be used as a subquery when `for` is omitted. The next example selects the count of a relation and the maximum value of another one:
```js
const people = await Person.query().select([
'persons.*',
Person.relatedQuery('pets')
.count()
.where('species', 'dog')
.as('dogCount'),
Person.relatedQuery('movies')
.max('createdAt')
.as('mostRecentMovieDate')
]);
console.log(people[0].id);
console.log(people[0].dogCount);
console.log(people[0].mostRecentMovieDate);
```
Find models that have at least one item in a relation:
```js
const peopleThatHavePets = await Person.query().whereExists(
Person.relatedQuery('pets')
);
```
Generates something like this:
```sql
select "persons".*
from "persons"
where exists (
select "pets".*
from "animals" as "pets"
where "pets"."ownerId" = "persons"."id"
)
```
## `static` knex()
Get/Set the knex instance for a model class.
Subclasses inherit the connection. A system-wide knex instance can thus be set by calling `objection.Model.knex(knex)`. This works even after subclasses have been created.
If you want to use multiple databases, you can instead pass the knex instance to each individual query or use the [bindKnex](/api/model/static-methods.html#static-bindknex) method.
##### Examples
Set a knex instance:
```js
const knex = require('knex')({
client: 'sqlite3',
connection: {
filename: 'database.db'
}
});
Model.knex(knex);
```
Get the knex instance:
```js
const knex = Person.knex();
```
## `static` transaction()
```js
const result = await Person.transaction(callback);
const result = await Person.transaction(trxOrKnex, callback);
```
Shortcut for `Person.knex().transaction(callback)`.
See the [transaction guide](/guide/transactions.html).
##### Arguments
| Argument | Type | Description |
| --------- | ------------------ | ----------------------------------------------- |
| callback | function |
| trxOrKnex | knex or Transation | Optional existing transaction or knex instance. |
##### Examples
```js
try {
const scrappy = await Person.transaction(async trx => {
const jennifer = await Person.query(trx).insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
const scrappy = await jennifer
.$relatedQuery('pets', trx)
.insert({ name: 'Scrappy' });
return scrappy;
});
console.log('Great success! Both Jennifer and Scrappy were inserted');
} catch (err) {
console.log(
'Something went wrong. Neither Jennifer nor Scrappy were inserted'
);
}
```
## `static` startTransaction()
```js
const trx = await Person.startTransaction(trxOrKnex);
```
Shortcut for `objection.transaction.start(Model1.knex())`.
See the [transaction guide](/guide/transactions.html).
##### Arguments
| Argument | Type | Description |
| --------- | ------------------ | ----------------------------------------------- |
| trxOrKnex | knex or Transation | Optional existing transaction or knex instance. |
##### Examples
```js
const trx = await Person.startTransaction();
try {
await Person.query(trx).insert(person1);
await Person.query(trx).insert(person2);
await Person.query(trx)
.patch(person3)
.where('id', person3.id);
await trx.commit();
} catch (err) {
await trx.rollback();
throw err;
}
```
## `static` beforeFind()
```js
class Person extends Model {
static beforeFind(args) {}
}
```
A hook that is executed before find queries.
See these sections for more information:
- [static hooks guide](/guide/hooks.html#static-query-hooks)
- [documentation for the arguments](/api/types/#type-statichookarguments)
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------- | ------------- |
| args | [StaticHookArguments](/api/types/#type-statichookarguments) | The arguments |
##### Return value
| Type | Description |
| ---- | ----------------------------- |
| any | The return value is not used. |
## `static` afterFind()
```js
class Person extends Model {
static afterFind(args) {}
}
```
A hook that is executed after find queries.
See these sections for more information:
- [static hooks guide](/guide/hooks.html#static-query-hooks)
- [documentation for the arguments](/api/types/#type-statichookarguments)
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------- | ------------- |
| args | [StaticHookArguments](/api/types/#type-statichookarguments) | The arguments |
##### Return value
| Type | Description |
| ---- | ----------------------------------------------------------------------------------------- |
| any | If the return value is not `undefined`, it will be used as the return value of the query. |
## `static` beforeUpdate()
```js
class Person extends Model {
static beforeUpdate(args) {}
}
```
A hook that is executed before update and patch queries.
See these sections for more information:
- [static hooks guide](/guide/hooks.html#static-query-hooks)
- [documentation for the arguments](/api/types/#type-statichookarguments)
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------- | ------------- |
| args | [StaticHookArguments](/api/types/#type-statichookarguments) | The arguments |
##### Return value
| Type | Description |
| ---- | ----------------------------- |
| any | The return value is not used. |
## `static` afterUpdate()
```js
class Person extends Model {
static afterUpdate(args) {}
}
```
A hook that is executed after update and patch queries.
See these sections for more information:
- [static hooks guide](/guide/hooks.html#static-query-hooks)
- [documentation for the arguments](/api/types/#type-statichookarguments)
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------- | ------------- |
| args | [StaticHookArguments](/api/types/#type-statichookarguments) | The arguments |
##### Return value
| Type | Description |
| ---- | ----------------------------------------------------------------------------------------- |
| any | If the return value is not `undefined`, it will be used as the return value of the query. |
## `static` beforeInsert()
```js
class Person extends Model {
static beforeInsert(args) {}
}
```
A hook that is executed before insert queries.
See these sections for more information:
- [static hooks guide](/guide/hooks.html#static-query-hooks)
- [documentation for the arguments](/api/types/#type-statichookarguments)
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------- | ------------- |
| args | [StaticHookArguments](/api/types/#type-statichookarguments) | The arguments |
##### Return value
| Type | Description |
| ---- | ----------------------------- |
| any | The return value is not used. |
## `static` afterInsert()
```js
class Person extends Model {
static afterInsert(args) {}
}
```
A hook that is executed after insert queries.
See these sections for more information:
- [static hooks guide](/guide/hooks.html#static-query-hooks)
- [documentation for the arguments](/api/types/#type-statichookarguments)
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------- | ------------- |
| args | [StaticHookArguments](/api/types/#type-statichookarguments) | The arguments |
##### Return value
| Type | Description |
| ---- | ----------------------------------------------------------------------------------------- |
| any | If the return value is not `undefined`, it will be used as the return value of the query. |
## `static` beforeDelete()
```js
class Person extends Model {
static beforeDelete(args) {}
}
```
A hook that is executed before delete queries.
See these sections for more information:
- [static hooks guide](/guide/hooks.html#static-query-hooks)
- [documentation for the arguments](/api/types/#type-statichookarguments)
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------- | ------------- |
| args | [StaticHookArguments](/api/types/#type-statichookarguments) | The arguments |
##### Return value
| Type | Description |
| ---- | ----------------------------- |
| any | The return value is not used. |
## `static` afterDelete()
```js
class Person extends Model {
static afterDelete(args) {}
}
```
A hook that is executed after delete queries.
See these sections for more information:
- [static hooks guide](/guide/hooks.html#static-query-hooks)
- [documentation for the arguments](/api/types/#type-statichookarguments)
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------- | ------------- |
| args | [StaticHookArguments](/api/types/#type-statichookarguments) | The arguments |
##### Return value
| Type | Description |
| ---- | ----------------------------------------------------------------------------------------- |
| any | If the return value is not `undefined`, it will be used as the return value of the query. |
## `static` bindKnex()
```js
const BoundPerson = Person.bindKnex(transactionOrKnex);
```
Creates an anonymous model subclass class that is bound to the given knex instance or transaction.
This method can be used to bind a Model subclass to multiple databases for example in a multi-tenant system. See the [multi tenancy recipe](/recipes/multitenancy-using-multiple-databases.html) for more info.
Also check out the [model binding pattern for transactions](/guide/transactions.html#binding-models-to-a-transaction) which internally uses `bindKnex`.
##### Arguments
| Argument | Type | Description |
| ----------------- | ------ | ---------------------------------------------------- |
| transactionOrKnex | object | knex instance or a transaction to bind the model to. |
##### Return value
| Type | Description |
| ---------------------------- | -------------------------------------- |
| Constructor extends Model> | The created model subclass constructor |
##### Examples
```js
const knex1 = require('knex')({
client: 'sqlite3',
connection: {
filename: 'database1.db'
}
});
const knex2 = require('knex')({
client: 'sqlite3',
connection: {
filename: 'database2.db'
}
});
SomeModel.knex(null);
const BoundModel1 = SomeModel.bindKnex(knex1);
const BoundModel2 = SomeModel.bindKnex(knex2);
// Throws since the knex instance is null.
await SomeModel.query();
// Works.
const models = await BoundModel1.query();
console.log(models[0] instanceof SomeModel); // --> true
console.log(models[0] instanceof BoundModel1); // --> true
// Works.
const models = await BoundModel2.query();
console.log(models[0] instanceof SomeModel); // --> true
console.log(models[0] instanceof BoundModel2); // --> true
```
## `static` bindTransaction()
Alias for [bindKnex](/api/model/static-methods.html#static-bindknex).
##### Examples
```js
const { transaction } = require('objection');
const Person = require('./models/Person');
await transaction(Person.knex(), async trx => {
const TransactingPerson = Person.bindTransaction(trx);
await TransactingPerson.query().insert({ firstName: 'Jennifer' });
return TransactingPerson.query()
.patch({ lastName: 'Lawrence' })
.where('id', jennifer.id);
});
```
This is 100% equivalent to the example above:
```js
const { transaction } = require('objection');
const Person = require('./models/Person');
await transaction(Person, async TransactingPerson => {
await TransactingPerson.query().insert({ firstName: 'Jennifer' });
return TransactingPerson.query()
.patch({ lastName: 'Lawrence' })
.where('id', jennifer.id);
});
```
## `static` fromJson()
```js
const person = Person.fromJson(json, opt);
```
Creates a model instance from a POJO (Plain Old Javascript Object).
The object is checked against [jsonSchema](/api/model/static-properties.html#static-jsonschema) if a schema is provided and an exception is thrown on failure.
The `json` object is also passed through the [\$parseJson](/api/model/instance-methods.html#parsejson) hook before the model instance is created. See [this section](/api/model/overview.html#model-data-lifecycle) for more info.
##### Arguments
| Argument | Type | Description |
| -------- | --------------------------------------------- | ----------------------------------------------- |
| json | Object | The JSON object from which to create the model. |
| opt | [ModelOptions](/api/types/#type-modeloptions) | Update options. |
##### Return value
| Type | Description |
| -------------------- | -------------------------- |
| [Model](/api/model/) | The created model instance |
##### Examples
Create a model instance:
```js
const jennifer = Person.fromJson({ firstName: 'Jennifer' });
```
Create a model instance skipping validation:
```js
const jennifer = Person.fromJson(
{ firstName: 'Jennifer' },
{ skipValidation: true }
);
```
## `static` fromDatabaseJson()
```js
const person = Person.fromDatabaseJson(row);
```
Creates a model instance from a JSON object send by the database driver.
Unlike [fromJson](/api/model/static-methods.html#static-fromjson), this method doesn't validate the input. The input is expected to be in the database format as explained [here](/api/model/overview.html#model-data-lifecycle).
##### Arguments
| Argument | Type | Description |
| -------- | ------ | --------------- |
| row | Object | A database row. |
##### Return value
| Type | Description |
| -------------------- | -------------------------- |
| [Model](/api/model/) | The created model instance |
## `static` modifierNotFound()
```js
class BaseModel extends Model {
static modifierNotFound(builder, modifier) {
const { properties } = this.jsonSchema;
if (properties && modifier in properties) {
builder.select(modifier);
} else {
super.modifierNotFound(builder, modifier);
}
}
}
```
This method is called when an unknown modifier is used somewhere.
By default, the static `modifierNotFound()` hook throws a `ModifierNotFoundError` error. If a model class overrides the hook, it can decide to handle the modifer through the passed `builder` instance, or call the hook's definition in the super class to still throw the error.
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------- | ------------------------------------------------- |
| builder | [QueryBuilder](/api/query-builder/) | The query builder on which to apply the modifier. |
| modifier | string | The name of the unknown modifier. |
## `static` createValidator()
```js
class BaseModel extends Model {
static createValidator() {
return new MyCustomValidator();
}
}
```
Creates an instance of a [Validator](/api/types/#class-validator) that is used to do all validation related stuff. This method is called only once per model class.
You can override this method to return an instance of your custom validator. The custom validator doesn't need to be based on the `jsonSchema`. It can be anything at all as long as it implements the [Validator](/api/types/#class-validator) interface.
If you want to use the default json schema based [AjvValidator](/api/types/#class-ajvvalidator) but want to modify it, you can use the `objection.AjvValidator` constructor. See the default implementation example.
If you want to share the same validator instance between multiple models, that's completely fine too. Simply implement `createValidator` so that it always returns the same object instead of creating a new one.
##### Examples
Sharing the same validator between model classes is also possible:
```js
const validator = new MyCustomValidator();
class BaseModel extends Model {
static createValidator() {
return validator;
}
}
```
The default implementation:
```js
const AjvValidator = require('objection').AjvValidator;
class Model {
static createValidator() {
return new AjvValidator({
onCreateAjv: ajv => {
// Here you can modify the `Ajv` instance.
},
options: {
allErrors: true,
validateSchema: false,
ownProperties: true,
v5: true
}
});
}
}
```
## `static` createNotFoundError()
```js
class BaseModel extends Model {
static createNotFoundError(queryContext, props) {
return new MyCustomNotFoundError({ ...props, modelClass: this });
}
}
```
Creates an error thrown by [throwIfNotFound](/api/query-builder/other-methods.html#throwifnotfound) method. You can override this
to throw any error you want.
##### Arguments
| Argument | Type | Description |
| ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------- |
| queryContext | Object | The context object of query that produced the empty result. See [context](/api/query-builder/other-methods.html#context). |
| props | any | Data passed to the error class constructor.
##### Return value
| Type | Description |
| ------- | ------------------------------------------------------------------------------- |
| `Error` | The created error. [NotFoundError](/api/types/#class-notfounderror) by default. |
##### Examples
The default implementation:
```js
class Model {
static createNotFoundError(queryContext, props) {
return new this.NotFoundError({ ...props, modelClass: this });
}
}
```
## `static` createValidationError()
```js
class BaseModel extends Model {
static createValidationError({ type, message, data }) {
return new MyCustomValidationError({ type, message, data, modelClass: this });
}
}
```
Creates an error thrown when validation fails for a model. You can override this to throw any error you want. The errors created by this function don't have to implement any interface or have the same properties as `ValidationError`. Objection only throws errors created by this function an never catches them.
##### Return value
| Type | Description |
| ------- | ----------------------------------------------------------------------------------- |
| `Error` | The created error. [ValidationError](/api/types/#class-validationerror) by default. |
## `static` fetchGraph()
```js
const queryBuilder = Person.fetchGraph(models, expression, options);
```
Load related models for a set of models using a [RelationExpression](/api/types/#type-relationexpression).
##### Arguments
| Argument | Type | Description |
| ---------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| models | Array<[Model](/api/model/)|Object> | Model instances for which to fetch the relations. Can be an array of model instances, array of POJOs, a single model instance or a single POJO. |
| expression | string|[RelationExpression](/api/types/#type-relationexpression) | The relation expression |
| options | [FetchGraphOptions](/api/types/#type-fetchgraphoptions) | Optional options. |
##### Return value
| Type | Description |
| ----------------------------------- | ------------------------- |
| [QueryBuilder](/api/query-builder/) | The created query builder |
##### Examples
```js
const people = await Person.fetchGraph([person1, person2], 'children.pets');
const person1 = people[0];
const person2 = people[1];
```
Relations can be filtered by giving modifier functions as arguments for the relations:
```js
const people = await Person.fetchGraph(
[person1, person2],
`
children(orderByAge).[
pets(onlyDogs, orderByName),
movies
]
`
).modifiers({
orderByAge(builder) {
builder.orderBy('age');
},
orderByName(builder) {
builder.orderBy('name');
},
onlyDogs(builder) {
builder.where('species', 'dog');
}
});
console.log(people[1].children.pets[0]);
```
## `static` traverseAsync()
Traverses the relation tree of a model instance (or a list of model instances).
Calls the callback for each related model recursively. The callback is called also for the input models themselves.
In the second example the traverser function is only called for `Person` instances.
##### Arguments
| Argument | Type | Description |
| ----------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| filterConstructor | function | If this optional constructor is given, the `traverser` is only called for models for which `model instanceof filterConstructor` returns `true`. |
| models | [Model](/api/model/)|[Model](/api/model/)[] | The model(s) whose relation trees to traverse. |
| traverser | function([Model](/api/model/), string, string) | The traverser function that is called for each model. The first argument is the model itself. If the model is in a relation of some other model the second argument is the parent model and the third argument is the name of the relation. |
##### Examples
There are two ways to call this method:
```js
const models = await SomeModel.query();
await Model.traverseAsync(models, async (model, parentModel, relationName) => {
await doSomething(model);
});
```
and
```js
const persons = await Person.query();
Model.traverseAsync(
Person,
persons,
async (person, parentModel, relationName) => {
await doSomethingWithPerson(person);
}
);
```
Also works with a single model instance
```js
const person = await Person.query();
await Person.traverseAsync(person, async (model, parentModel, relationName) => {
await doSomething(model);
});
```
## `static` getRelations()
```js
const relations = Person.getRelations();
```
Returns a [Relation](/api/types/#class-relation) object for each relation defined in [relationMappings](/api/model/static-properties.html#static-relationmappings).
This method is mainly useful for plugin developers and for other generic usages.
##### Return value
| Type | Description |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Object<string, [Relation](/api/types/#class-relation)> | Object whose keys are relation names and values are [Relation](/api/types/#class-relation) instances. |
## `static` columnNameToPropertyName()
```js
const propertyName = Person.columnNameToPropertyName(columnName);
```
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 |
##### Examples
If you have defined `columnNameMappers = snakeCaseMappers()` for your model:
```js
const propName = Person.columnNameToPropertyName('foo_bar');
console.log(propName); // --> 'fooBar'
```
## `static` propertyNameToColumnName()
```js
const columnName = Person.propertyNameToColumnName(propertyName);
```
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 |
##### Examples
If you have defined `columnNameMappers = snakeCaseMappers()` for your model:
```js
const columnName = Person.propertyNameToColumnName('fooBar');
console.log(columnName); // --> 'foo_bar'
```
## `static` fetchTableMetadata()
```js
const metadata = await Person.fetchTableMetadata(opt);
```
Fetches and caches the table metadata.
Most of the time objection doesn't need this metadata, but some methods like [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) do. This method is called by objection when the metadata is needed. The result is cached and after the first call the cached promise is returned and no queries are executed.
Because objection uses this on demand, the first query that needs this information can have unpredicable performance. If that's a problem, you can call this method for each of your models during your app's startup.
If you've implemented [tableMetadata](/api/model/static-methods.html#static-tablemetadata) method to return a custom metadata object, this method doesn't execute database queries, but returns `Promise.resolve(this.tableMetadata())` instead.
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------------------- | ---------------- |
| opt | [TableMetadataFetchOptions](/api/types/#type-tablemetadatafetchoptions) | Optional options |
##### Return value
| Type | Description |
| -------------------------------------------------------------- | ------------------------- |
| Promise<[TableMetadata](/api/types/#type-tablemetadata)> | The table metadata object |
## `static` tableMetadata()
```js
const metadata = Person.tableMetadata(opt);
```
Synchronously returns the table metadata object from the cache.
You can override this method to return a custom object if you don't want objection to use
[fetchTableMetadata](/api/model/static-methods.html#static-fetchtablemetadata).
See [fetchTableMetadata](/api/model/static-methods.html#static-fetchtablemetadata) for more information.
##### Arguments
| Argument | Type | Description |
| -------- | ------------------------------------------------------------- | ---------------- |
| opt | [TableMetadataOptions](/api/types/#type-tablemetadataoptions) | Optional options |
##### Return value
| Type | Description |
| ----------------------------------------------- | ------------------------- |
| [TableMetadata](/api/types/#type-tablemetadata) | The table metadata object |
##### Examples
A custom override that uses the property information in `jsonSchema`.
```js
class Person extends Model {
static tableMetadata() {
return {
columns: Object.keys(this.jsonSchema.properties)
};
}
}
```
## `static` raw()
Shortcut for `Person.knex().raw(...args)`
## `static` ref()
Returns a [ReferenceBuilder](/api/types/#class-referencebuilder) instance that is bound to the model class. Any reference created using it will add the correct table name to the reference.
```js
const { ref } = Person;
await Person.query().where(ref('firstName'), 'Jennifer');
```
```sql
select "persons".* from "persons" where "persons"."firstName" = 'Jennifer'
```
`ref` uses the correct table name even when an alias has been given to the table.
```js
const { ref } = Person;
await Person.query()
.alias('p')
.where(ref('firstName'), 'Jennifer');
```
```sql
select "p".* from "persons" as "p" where "p"."firstName" = 'Jennifer'
```
Note that the following two ways to use `Model.ref` are completely equivalent:
```js
const { ref } = Person;
await Person.query().where(ref('firstName'), 'Jennifer');
```
```js
await Person.query().where(Person.ref('firstName'), 'Jennifer');
```
## `static` fn()
Shortcut for `Person.knex().fn`
## `static` knexQuery()
Shortcut for `Person.knex().table(Person.tableName)`
================================================
FILE: doc/api/model/static-properties.md
================================================
# Static Properties
## `static` tableName
```js
class Person extends Model {
static get tableName() {
return 'persons';
}
}
```
Name of the database table for this model.
Each model must set this.
## `static` relationMappings
This property defines the relations (relationships, associations) to other models.
`relationMappings` is an object (or a function/getter that returns an object) whose keys are relation names and values are [RelationMapping](/api/types/#type-relationmapping) instances. The `join` property in addition to the relation type define how the models are related to one another.
The `from` and `to` properties of the `join` object define the database columns through which the models are associated. Note that neither of these columns need to be primary keys. They can be any columns. In fact they can even be fields inside JSON columns (using the [ref](/api/objection/#ref) helper). In the case of ManyToManyRelation also the join table needs to be defined. This is done using the `through` object.
The `modelClass` passed to the relation mappings is the class of the related model. It can be one of the following:
1. A model class constructor
2. An absolute path to a module that exports a model class
3. A path relative to one of the paths in [modelPaths](/api/model/static-properties.html#static-modelpaths) array.
The file path versions are handy for avoiding require loops.
Further reading:
- [the relation guide](/guide/relations.html)
- [RelationMapping](/api/types/#type-relationmapping)
##### Examples
```js
const { Model, ref } = require('objection');
class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'persons.id',
to: 'animals.ownerId'
// Any of the `to` and `from` fields can also be
// references to nested fields (or arrays of references).
// Here the relation is created between `persons.id` and
// `animals.json.details.ownerId` properties. The reference
// must be cast to the same type as the other key.
//
// to: ref('animals.json:details.ownerId').castInt()
}
},
father: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'persons.fatherId',
to: 'persons.id'
}
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'persons.id',
through: {
from: 'persons_movies.actorId',
to: 'persons_movies.movieId'
// If you have a model class for the join table
// you can specify it like this:
//
// modelClass: PersonMovie,
// Columns listed here are automatically joined
// to the related models on read and written to
// the join table instead of the related table
// on insert/update.
//
// extra: ['someExtra']
},
to: 'movies.id'
}
}
};
}
}
```
## `static` idColumn
```js
class Person extends Model {
static get idColumn() {
return 'some_column_name';
}
}
```
Name of the primary key column in the database table.
Composite id can be specified by giving an array of column names.
Defaults to 'id'.
You can return `null` in order to tell objection there is no primary key. This may be useful in models of join tables.
## `static` jsonSchema
```js
class Person extends Model {
static get jsonSchema() {
return {
type: 'object',
required: ['name'],
properties: {
id: { type: 'integer' },
name: { type: 'string', minLength: 1, maxLength: 255 },
age: { type: 'number' } // optional
}
};
}
}
```
The optional schema against which the model is validated.
Must follow [JSON Schema](https://json-schema.org) specification. If unset, no validation is done.
##### Read more
- [\$validate](/api/model/instance-methods.html#validate)
- [jsonAttributes](/api/model/static-properties.html#static-jsonattributes)
- [custom validation recipe](/recipes/custom-validation.html)
##### Examples
```js
class Person extends Model {
static get jsonSchema() {
return {
type: 'object',
required: ['firstName', 'lastName'],
properties: {
id: { type: 'integer' },
parentId: { type: ['integer', 'null'] },
firstName: { type: 'string', minLength: 1, maxLength: 255 },
lastName: { type: 'string', minLength: 1, maxLength: 255 },
age: { type: 'number' },
// Properties defined as objects or arrays are
// automatically converted to JSON strings when
// writing to database and back to objects and arrays
// when reading from database. To override this
// behaviour, you can override the
// Person.jsonAttributes property.
address: {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
zipCode: { type: 'string' }
}
}
}
};
}
}
```
## `static` virtualAttributes
```js
class Person extends Model {
static get virtualAttributes() {
return ['fullName', 'isFemale'];
}
fullName() {
return `${this.firstName} ${this.lastName}`;
}
get isFemale() {
return this.gender === 'female';
}
}
```
Getters and methods listed here are serialized with real properties when [toJSON](/api/model/instance-methods.html#tojson) is called. Virtual attribute methods and getters must be synchronous.
The virtual values are not written to database. Only the "external" JSON format will contain them.
##### Examples
```js
class Person extends Model {
static get virtualAttributes() {
return ['fullName', 'isFemale'];
}
fullName() {
return `${this.firstName} ${this.lastName}`;
}
get isFemale() {
return this.gender === 'female';
}
}
const person = Person.fromJson({
firstName: 'Jennifer',
lastName: 'Aniston',
gender: 'female'
});
// Note that `toJSON` is always called automatically
// when an object is serialized to a JSON string using
// JSON.stringify. You very rarely need to call `toJSON`
// explicitly. koa, express and all other frameworks I'm
// aware of use JSON.stringify to serialize objects to JSON.
const pojo = person.toJSON();
console.log(pojo.fullName); // --> 'Jennifer Aniston'
console.log(pojo.isFemale); // --> true
```
You can also pass options to [toJSON](/api/model/instance-methods.html#tojson) to only serialize a subset of virtual attributes. In fact, when the `virtuals` option is used, the attributes don't even need to be listed in `virtualAttributes`.
```js
const pojo = person.toJSON({ virtuals: ['fullName'] });
```
## `static` modifiers
Reusable query building functions that can be used in any query using [modify](/api/query-builder/other-methods.html#modify) method and in many other places.
Also see the [modifier recipe](/recipes/modifiers.html) for more info and examples.
```js
class Movie extends Model {
static get modifiers() {
return {
goodMovies(builder) {
builder.where('stars', '>', 3);
},
orderByName(builder) {
builder.orderBy('name');
}
};
}
}
class Animal extends Model {
static get modifiers() {
return {
dogs(builder) {
builder.where('species', 'dog');
}
};
}
}
```
Modifiers can be used for relations in a `withGraphFetched` or `withGraphJoined` query.
```js
Person.query().withGraphFetched(
'[movies(goodMovies, orderByName).actors, pets(dogs)]'
);
```
Modifiers can also be used through [modifyGraph](/api/query-builder/other-methods.html#modifygraph):
```js
Person.query()
.withGraphFetched('[movies.actors, pets]')
.modifyGraph('movies', ['goodMovies', 'orderByName'])
.modifyGraph('pets', 'dogs');
```
## `static` modelPaths
```js
class Person extends Model {
static get modelPaths() {
return [__dirname];
}
}
```
A list of paths from which to search for models for relations.
A model class can be defined for a relation in [relationMappings](/api/model/static-properties.html#static-relationmappings) as
1. A model class constructor
2. An absolute path to a module that exports a model class
3. A path relative to one of the paths in `modelPaths` array.
You probably don't want to define `modelPaths` property for each model. Once again we
recommend that you create a `BaseModel` super class for all your models and define
shared configuration such as this there.
##### Examples
Using a shared `BaseModel` superclass:
```js
const { Model } = require('objection');
// models/BaseModel.js
class BaseModel extends Model {
static get modelPaths() {
return [__dirname];
}
}
module.exports = {
BaseModel
};
// models/Person.js
const { BaseModel } = require('./BaseModel');
class Person extends BaseModel {
...
}
```
## `static` concurrency
```js
class Person extends Model {
static get concurrency() {
return 10;
}
}
```
How many queries can be run concurrently per connection.
This doesn't limit the concurrency of the entire server. It only limits the number of concurrent queries that can be run on a single connection. By default knex connection pool size is 10, which means that the maximum number of concurrent queries started by objection is `Model.concurrency * 10`. You can also easily increase the knex pool size.
The default concurrency is 4 except for mssql, for which the default is 1. The mssql default is needed because of the buggy driver that only allows one query at a time per connection.
## `static` jsonAttributes
```js
class Person extends Model {
static get jsonAttributes() {
return ['someProp', 'someOtherProp'];
}
}
```
Properties that should be saved to database as JSON strings.
The properties listed here are serialized to JSON strings upon insertion/update to the database and parsed back to objects when models are read from the database. Combined with the postgresql's json or jsonb data type, this is a powerful way of representing documents as single database rows.
If this property is left unset all properties declared as objects or arrays in the [jsonSchema](/api/model/static-properties.html#static-jsonschema) are implicitly added to this list.
## `static` cloneObjectAttributes
```js
class Person extends Model {
static get cloneObjectAttributes() {
return false;
}
}
```
If `true` (the default) object attributes (for example jsonb columns) are cloned when `$toDatabaseJson`, `$toJson` or `toJSON` is called. If this is set to `false`, they are not cloned. Note that nested `Model` instances inside relations etc. are still effectively cloned, because `$toJson` is called for them recursively, but their jsonb columns, again,
are not :)
Usually you don't need to care about this setting, but if you have large object fields (for example large objects in jsonb columns) cloning the data can become slow and play a significant part in your server's performance. There's rarely a need to to clone this data, but since it has historically been copied, we cannot change the default behaviour
easily.
TLDR; Set this setting to `false` if you have large jsonb columns and you see that cloning that data takes a significant amount of time **when you profile the code**.
## `static` columnNameMappers
```js
const { Model, snakeCaseMappers } = require('objection');
class Person extends Model {
static get columnNameMappers() {
return snakeCaseMappers();
}
}
```
The mappers to use to convert column names to property names in code.
Further reading:
- [snakeCaseMappers](/api/objection/#snakecasemappers)
- [snake_case to camelCase conversion recipe](/recipes/snake-case-to-camel-case-conversion.html)
##### Examples
If your columns are UPPER_SNAKE_CASE:
```js
const { Model, snakeCaseMappers } = require('objection');
class Person extends Model {
static get columnNameMappers() {
return snakeCaseMappers({ upperCase: true });
}
}
```
The mapper signature:
```js
class Person extends Model {
static columnNameMappers = {
parse(obj) {
// database --> code
},
format(obj) {
// code --> database
}
};
}
```
## `static` uidProp
```js
class Person extends Model {
static get uidProp() {
return '#id';
}
}
```
Name of the property used to store a temporary non-db identifier for the model.
NOTE: You cannot use any of the model's properties as `uidProp`. For example if your model has a property `id`, you cannot set `uidProp = 'id'`.
Defaults to '#id'.
## `static` uidRefProp
```js
class Person extends Model {
static get uidRefProp() {
return '#ref';
}
}
```
Name of the property used to store a reference to a [uidProp](/api/model/static-properties.html#static-uidprop)
NOTE: You cannot use any of the model's properties as `uidRefProp`. For example if your model has a property `ref`, you cannot set `uidRefProp = 'ref'`.
Defaults to `'#ref'`.
## `static` dbRefProp
```js
class Person extends Model {
static get dbRefProp() {
return '#dbRef';
}
}
```
Name of the property used to point to an existing database row from an `insertGraph` graph.
NOTE: You cannot use any of the model's properties as `dbRefProp`. For example if your model has a property `id`, you cannot set `dbRefProp = 'id'`.
Defaults to '#dbRef'.
## `static` propRefRegex
```js
class Person extends Model {
static get propRefRegex() {
return /#ref{([^\.]+)\.([^}]+)}/g;
}
}
```
Regular expression for parsing a reference to a property.
Defaults to `/#ref{([^\.]+)\.([^}]+)}/g`.
## `static` pickJsonSchemaProperties
```js
class Person extends Model {
static get pickJsonSchemaProperties() {
return true;
}
}
```
If this is `true` only properties in `jsonSchema` are picked when inserting or updating a row in the database.
Defaults to `false`.
## `static` defaultGraphOptions
```js
class Person extends Model {
static get defaultGraphOptions() {
return {
minimize: true,
separator: '->',
aliases: {},
maxBatchSize: 10000
};
}
}
```
Sets the default options for [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) and [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined). See the possible
fields [here](/api/types/#type-graphoptions).
Defaults to `{ minimize: false, separator: ':', aliases: {}, maxBatchSize: 10000 }`.
## `static` useLimitInFirst
```js
class Animal extends Model {
static get useLimitInFirst() {
return true;
}
}
```
If `true`, `limit(1)` is added to the query when [first()](/api/query-builder/other-methods.html#first) is called. Defaults to `false` for legacy reasons.
## `static` QueryBuilder
```js
class Person extends Model {
static get QueryBuilder() {
return MyCustomQueryBuilder;
}
}
```
[QueryBuilder](/api/query-builder/) subclass to use for all queries created for this model.
This constructor is used whenever a query builder is created using [query](/api/model/static-methods.html#static-query), [\$query](/api/model/instance-methods.html#query), [\$relatedQuery](/api/model/instance-methods.html#relatedquery) or any other method that creates a query. You can override this to use your own [QueryBuilder](/api/query-builder/) subclass.
[Usage example](/recipes/custom-query-builder.html).
## `static` BelongsToOneRelation
```js
class Person extends Model {
static get relationMappings() {
parent: {
relation: Model.BelongsToOneRelation,
...
}
}
}
```
A relation type that can be used in [relationMappings](/api/model/static-properties.html#static-relationmappings) to create a belongs-to-one relationship. See [this section](/guide/relations.md) for more information about different relation types.
## `static` HasOneRelation
```js
class Person extends Model {
static get relationMappings() {
parent: {
relation: Model.HasOneRelation,
...
}
}
}
```
A relation type that can be used in [relationMappings](/api/model/static-properties.html#static-relationmappings) to create a has-one relationship. See [this section](/guide/relations.md) for more information about different relation types.
## `static` HasManyRelation
```js
class Person extends Model {
static get relationMappings() {
pets: {
relation: Model.HasManyRelation,
...
}
}
}
```
A relation type that can be used in [relationMappings](/api/model/static-properties.html#static-relationmappings) to create a has-many relationship. See [this section](/guide/relations.md) for more information about different relation types.
## `static` ManyToManyRelation
```js
class Person extends Model {
static get relationMappings() {
movies: {
relation: Model.ManyToManyRelation,
...
}
}
}
```
A relation type that can be used in [relationMappings](/api/model/static-properties.html#static-relationmappings) to create a many-to-many relationship. See [this section](/guide/relations.md) for more information about different relation types.
## `static` HasOneThroughRelation
```js
class Person extends Model {
static get relationMappings() {
favoriteMovie: {
relation: Model.HasOneThroughRelation,
...
}
}
}
```
A relation type that can be used in [relationMappings](/api/model/static-properties.html#static-relationmappings) to create a has-one-through relationship. See [this section](/guide/relations.md) for more information about different relation types.
================================================
FILE: doc/api/objection/README.md
================================================
---
sidebar: auto
---
# `module` objection
```js
const objection = require('objection');
const { Model, ref } = require('objection');
```
The objection module is what you get when you import objection. It has a bunch of properties that are listed below.
## Model
```js
const { Model } = require('objection');
```
[The model class](/api/model/)
## initialize
```js
const { initialize } = require('objection');
```
For some queries objection needs to perform asynchronous operations in preparation, like fetch table metadata from the db. Objection does these preparations on-demand the first time such query is executed. However, some methods like `toKnexQuery` need these preparations to have been made so that the query can be built synchronously. In these cases you can use `initialize` to "warm up" the models and do all needed async preparations needed. You only need to call this function once if you choose to use it.
Calling this function is completely optional. If some method requires this to have been called, they will throw a clear error message asking you to do so. These cases are extremely rare, but this function is here for those cases.
You can also call this function if you want to be in control of when these async preparation operations get executed. It can be helpful for example in tests.
##### Examples
```js
const { initialize } = require('objection');
await initialize(knex, [Person, Movie, Pet, SomeOtherModelClass]);
```
If knex has been installed for the `Model` globally, you can omit the first argument.
```js
const { initialize } = require('objection');
await initialize([Person, Movie, Pet, SomeOtherModelClass]);
```
## transaction
```js
const { transaction } = require('objection');
```
[The transaction function](/guide/transactions.html)
## ref
```js
const { ref } = require('objection');
```
Factory function that returns a [ReferenceBuilder](/api/types/#class-referencebuilder) instance, that makes it easier to refer to tables, columns, json attributes etc. [ReferenceBuilder](/api/types/#class-referencebuilder) can also be used to type cast and alias the references.
See [FieldExpression](/api/types/#type-fieldexpression) for more information about how to refer to json fields.
##### Examples
```js
const { ref } = require('objection');
await Model.query()
.select([
'id',
ref('Model.jsonColumn:details.name')
.castText()
.as('name'),
ref('Model.jsonColumn:details.age')
.castInt()
.as('age')
])
.join(
'OtherModel',
ref('Model.jsonColumn:details.name').castText(),
'=',
ref('OtherModel.name')
)
.where('age', '>', ref('OtherModel.ageLimit'));
```
`withGraphJoined` and `joinRelated` methods also use `:` as a separator which can lead to ambiquous queries when combined with json references. For example:
```
jsonColumn:details.name
```
Can mean two things:
1. column `name` of the relation `jsonColumn.details`
2. field `name` of the `details` object inside `jsonColumn` column
When used with `withGraphJoined` and `joinRelated` you can use the `from` method of the `ReferenceBuilder` to specify the table:
```js
await Person.query()
.withGraphJoined('children.children')
.where(ref('jsonColumn:details.name').from('children:children'), 'Jennifer');
```
## raw
```js
const { raw } = require('objection');
```
Factory function that returns a [RawBuilder](/api/types/#class-rawbuilder) instance. [RawBuilder](/api/types/#class-rawbuilder) is a wrapper for knex raw method that doesn't depend on knex. Instances of [RawBuilder](/api/types/#class-rawbuilder) are converted to knex raw instances lazily when the query is executed.
Also see [the raw query recipe](/recipes/raw-queries.html).
##### Examples
When using raw SQL segments in queries, it's a good idea to use placeholders instead of adding user input directly to the SQL to avoid injection errors. Placeholders are sent to the database engine which then takes care of interpolating the SQL safely.
You can use `??` as a placeholder for identifiers (column names, aliases etc.) and `?` for values.
```js
const { raw } = require('objection');
const result = await Person.query()
.select(raw('coalesce(sum(??), 0) as ??', ['age', 'ageSum']))
.where('age', '<', raw('? + ?', [50, 25]));
console.log(result[0].ageSum);
```
You can use `raw` in insert and update queries too:
```js
await Person.query().patch({
age: raw('age + ?', 10)
});
```
You can also use named placeholders. `:someName:` for identifiers (column names, aliases etc.) and `:someName` for values.
```js
await Person.query()
.select(
raw('coalesce(sum(:sumColumn:), 0) as :alias:', {
sumColumn: 'age',
alias: 'ageSum'
})
)
.where(
'age',
'<',
raw(':value1 + :value2', {
value1: 50,
value2: 25
})
);
```
You can nest `ref`, `raw`, `val` and query builders (both knex and objection) in `raw` calls
```js
const { val } = require('objection')
await Person
.query()
.select(raw('coalesce(:sumQuery, 0) as :alias:', {
sumQuery: Person.query().sum('age'),
alias: 'ageSum'
}))
.where('age', '<', raw(':value1 + :value2', {
value1: val(50)
value2: knex.raw('25')
}));
```
## val
```js
const { val } = require('objection');
```
Factory function that returns a [ValueBuilder](/api/types/#class-valuebuilder) instance. [ValueBuilder](/api/types/#class-valuebuilder) helps build values of different types.
##### Examples
```js
const { val, ref } = require('objection');
// Compare json objects
await Model.query().where(
ref('Model.jsonColumn:details'),
'=',
val({ name: 'Jennifer', age: 29 })
);
// Insert an array.
await Model.query().insert({
numbers: val([1, 2, 3])
.asArray()
.castTo('real[]')
});
```
## fn
```js
const { fn } = require('objection');
```
Factory function that returns a [FunctionBuilder](/api/types/#class-functionbuilder) instance. `fn` helps calling SQL functions. The signature is:
```js
const functionBuilder = fn(functionName, ...args);
```
For example:
```js
fn('coalesce', ref('age'), 0);
```
The `fn` function also has shortcuts for most common functions:
```js
fn.now();
fn.now(precision);
fn.coalesce(...args);
fn.concat(...args);
fn.sum(...args);
fn.avg(...args);
fn.min(...args);
fn.max(...args);
fn.count(...args);
fn.upper(...args);
fn.lower(...args);
```
All arguments are interpreted as values by default. Use `ref` to refer to columns. you can also pass `raw` instances, other `fn` instances, `QueryBuilders` knex builders, knex raw and anything else just like to any other objection method.
##### Examples
```js
const { fn, ref } = require('objection');
// Compare nullable numbers
await Model.query().where(fn('coalesce', ref('age'), 0), '>', 30);
// The same example using the fn.coalesce shortcut
await Model.query().where(fn.coalesce(ref('age'), 0), '>', 30);
```
Note that it can often be cleaner to use `raw` or `whereRaw`:
```js
await Model.query().whereRaw('coalesce(age, 0) > ?', 30);
```
## mixin
```js
const { mixin } = require('objection');
```
The mixin helper for applying multiple [plugins](/guide/plugins.html).
##### Examples
```js
const { mixin, Model } = require('objection');
class Person extends mixin(Model, [
SomeMixin,
SomeOtherMixin,
EvenMoreMixins,
LolSoManyMixins,
ImAMixinWithOptions({ foo: 'bar' })
]) {}
```
## compose
```js
const { compose } = require('objection');
```
The compose helper for applying multiple [plugins](/guide/plugins.html).
##### Examples
```js
const { compose, Model } = require('objection');
const mixins = compose(
SomeMixin,
SomeOtherMixin,
EvenMoreMixins,
LolSoManyMixins,
ImAMixinWithOptions({ foo: 'bar' })
);
class Person extends mixins(Model) {}
```
## snakeCaseMappers
```js
const { snakeCaseMappers } = require('objection');
```
Function for adding snake_case to camelCase conversion to objection models. Better documented [here](/recipes/snake-case-to-camel-case-conversion.html). The `snakeCaseMappers` function accepts an options object. The available options are:
| Option | Type | Default | Description |
| --------------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| upperCase | boolean | `false` | Set to `true` if your columns are UPPER_SNAKE_CASED. |
| underscoreBeforeDigits | boolean | `false` | When `true`, will place an underscore before digits (`foo1Bar2` becomes `foo_1_bar_2`). When `false`, `foo1Bar2` becomes `foo1_bar2`. |
| underscoreBetweenUppercaseLetters | boolean | `false` | When `true`, will place underscores between consecutive uppercase letters (`fooBAR` becomes `foo_b_a_r`). When `false`, `fooBAR` will become `foo_bar`. |
##### Examples
```js
const { Model, snakeCaseMappers } = require('objection');
class Person extends Model {
static get columnNameMappers() {
return snakeCaseMappers();
}
}
```
If your columns are UPPER_SNAKE_CASE
```js
const { Model, snakeCaseMappers } = require('objection');
class Person extends Model {
static get columnNameMappers() {
return snakeCaseMappers({ upperCase: true });
}
}
```
## knexSnakeCaseMappers
```js
const { knexSnakeCaseMappers } = require('objection');
```
Function for adding a snake_case to camelCase conversion to `knex`. Better documented [here](/recipes/snake-case-to-camel-case-conversion.html). The `knexSnakeCaseMappers` function accepts an options object. The available options are:
| Option | Type | Default | Description |
| --------------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| upperCase | boolean | `false` | Set to `true` if your columns are UPPER_SNAKE_CASED. |
| underscoreBeforeDigits | boolean | `false` | When `true`, will place an underscore before digits (`foo1Bar2` becomes `foo_1_bar_2`). When `false`, `foo1Bar2` becomes `foo1_bar2`. |
| underscoreBetweenUppercaseLetters | boolean | `false` | When `true`, will place underscores between consecutive uppercase letters (`fooBAR` becomes `foo_b_a_r`). When `false`, `fooBAR` will become `foo_bar`. |
##### Examples
```js
const { knexSnakeCaseMappers } = require('objection');
const Knex = require('knex');
const knex = Knex({
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test'
}
...knexSnakeCaseMappers()
});
```
If your columns are UPPER_SNAKE_CASE
```js
const { knexSnakeCaseMappers } = require('objection');
const Knex = require('knex');
const knex = Knex({
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test'
}
...knexSnakeCaseMappers({ upperCase: true })
});
```
For older nodes:
```js
const Knex = require('knex');
const knexSnakeCaseMappers = require('objection').knexSnakeCaseMappers;
const knex = Knex({
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test'
},
...knexSnakeCaseMappers()
});
```
## knexIdentifierMapping
```js
const { knexIdentifierMapping } = require('objection');
```
Like [knexSnakeCaseMappers](/api/objection/#knexsnakecasemappers), but can be used to make an arbitrary static mapping between column names and property names. In the examples, you would have identifiers `MyId`, `MyProp` and `MyAnotherProp` in the database and you would like to map them into `id`, `prop` and `anotherProp` in the code.
##### Examples
```js
const { knexIdentifierMapping } = require('objection');
const Knex = require('knex');
const knex = Knex({
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test'
}
...knexIdentifierMapping({
MyId: 'id',
MyProp: 'prop',
MyAnotherProp: 'anotherProp'
})
});
```
Note that you can pretty easily define the conversions in some static property of your model. In this example we have added a property `column` to jsonSchema and use that to create the mapping object.
```js
const { knexIdentifierMapping } = require('objection');
const Knex = require('knex');
const path = require('path')
const fs = require('fs');
// Path to your model folder.
const MODELS_PATH = path.join(__dirname, 'models');
const knex = Knex({
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test'
}
// Go through all models and add conversions using the custom property
// `column` in json schema.
...knexIdentifierMapping(fs.readdirSync(MODELS_PATH)
.filter(it => it.endsWith('.js'))
.map(it => require(path.join(MODELS_PATH, it)))
.reduce((mapping, modelClass) => {
const properties = modelClass.jsonSchema.properties;
return Object.keys(properties).reduce((mapping, propName) => {
mapping[properties[propName].column] = propName;
return mapping;
}, mapping);
}, {});
)
});
```
For older nodes:
```js
const Knex = require('knex');
const knexIdentifierMapping = require('objection').knexIdentifierMapping;
const knex = Knex({
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test'
},
...knexIdentifierMapping({
MyId: 'id',
MyProp: 'prop',
MyAnotherProp: 'anotherProp'
})
});
```
## ValidationError
```js
const { ValidationError } = require('objection');
```
The [ValidationError](/api/types/#class-validationerror) class.
## NotFoundError
```js
const { NotFoundError } = require('objection');
```
The [NotFoundError](/api/types/#class-notfounderror) class.
## DBError
```js
const { DBError } = require('objection');
```
The [DBError](https://github.com/Vincit/db-errors#dberror) from [db-errors](https://github.com/Vincit/db-errors) library.
## ConstraintViolationError
```js
const { ConstraintViolationError } = require('objection');
```
The [ConstraintViolationError](https://github.com/Vincit/db-errors#constraintviolationerror) from [db-errors](https://github.com/Vincit/db-errors) library.
## UniqueViolationError
```js
const { UniqueViolationError } = require('objection');
```
The [UniqueViolationError](https://github.com/Vincit/db-errors#uniqueviolationerror) from [db-errors](https://github.com/Vincit/db-errors) library.
## NotNullViolationError
```js
const { NotNullViolationError } = require('objection');
```
The [NotNullViolationError](https://github.com/Vincit/db-errors#notnullviolationerror) from [db-errors](https://github.com/Vincit/db-errors) library.
## ForeignKeyViolationError
```js
const { ForeignKeyViolationError } = require('objection');
```
The [ForeignKeyViolationError](https://github.com/Vincit/db-errors#foreignkeyviolationerror) from [db-errors](https://github.com/Vincit/db-errors) library.
## CheckViolationError
```js
const { CheckViolationError } = require('objection');
```
The [CheckViolationError](https://github.com/Vincit/db-errors#checkviolationerror) from [db-errors](https://github.com/Vincit/db-errors) library.
## DataError
```js
const { DataError } = require('objection');
```
The [DataError](https://github.com/Vincit/db-errors#dataerror) from [db-errors](https://github.com/Vincit/db-errors) library.
================================================
FILE: doc/api/query-builder/README.md
================================================
# `class` QueryBuilder
`QueryBuilder` is the most important component in objection. Every method that allows you to fetch or modify items in the database returns an instance of the `QueryBuilder`.
`QueryBuilder` is a wrapper around [knex QueryBuilder](https://knexjs.org/guide/query-builder.html). QueryBuilder has all the methods a knex QueryBuilder has and more. While knex QueryBuilder returns plain JavaScript objects, QueryBuilder returns Model subclass instances.
QueryBuilder is thenable, meaning that it can be used like a promise. You can `await` a query builder, and it will get executed. You can return query builder from a [then](/api/query-builder/other-methods.html#then) method of a promise and it gets chained just like a normal promise would.
See also
- [Custom query builder recipe](/recipes/custom-query-builder.html)
================================================
FILE: doc/api/query-builder/eager-methods.md
================================================
# Eager Loading Methods
## withGraphFetched()
```js
queryBuilder = queryBuilder.withGraphFetched(relationExpression, graphOptions);
```
Fetch a graph of related items for the result of any query (eager loading).
There are two methods that can be used to load relations eagerly: [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) and [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined). The main difference is that [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) uses multiple queries under the hood to fetch the result while [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) uses a single query and joins to fetch the results. Both methods allow you to do different things which we will go through in detail in the examples below and in the examples of the [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) method.
As mentioned, this method uses multiple queries to fetch the related objects. Objection performs one query per level in the relation expression tree. For example only two additional queries will be created for the expression `children.children` no matter how many children the item has or how many children each of the children have. This algorithm is explained in detail in [this blog post](https://www.vincit.fi/en/blog/nested-eager-loading-and-inserts-with-objection-js/) (note that `withGraphFetched` method used to be called `eager`).
**Limitations:**
- Relations cannot be referenced in the root query because they are not joined.
- `limit` and `page` methods will work incorrectly when applied to a relation using `modifyGraph` or `modifiers` because they will be applied on a query that fetches relations for multiple parents. You can use `limit` and `page` for the root query.
See the [eager loading](/guide/query-examples.html#eager-loading) section for more examples and [RelationExpression](/api/types/#type-relationexpression) for more info about the relation expression language.
See the [fetchGraph](/api/model/static-methods.html#static-fetchgraph) and [\$fetchGraph](/api/model/instance-methods.html#fetchgraph) methods if you want to load relations for items already loaded from the database.
**About performance:**
Note that while [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) sounds more performant than [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched), both methods have very similar performance in most cases and [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) is actually much much faster in some cases where the [relationExpression](/api/types/#type-relationexpression) contains multiple many-to-many or has-many relations. The flat record list the db returns for joins can have an incredible amount of duplicate information in some cases. Transferring + parsing that data from the db to node can be very costly, even though the actual joins in the db are very fast. You shouldn't select [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) blindly just because it sounds more peformant. The three rules of optimization apply here too: 1. Don't optimize 2. Don't optimize yet 3. Profile before optimizing. When you don't actually need joins, use [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched).
##### Arguments
| Argument | Type | Description |
| ------------------ | --------------------------------------------------------- | ------------------------------------------------------------ |
| relationExpression | [RelationExpression](/api/types/#type-relationexpression) | The relation expression describing which relations to fetch. |
| options | [GraphOptions](/api/types/#type-graphoptions) | Optional options. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
Fetches all `Persons` named Arnold with all their pets. `'pets'` is the name of the relation defined in [relationMappings](/api/model/static-properties.html#static-relationmappings).
```js
const people = await Person.query()
.where('firstName', 'Arnold')
.withGraphFetched('pets');
console.log(people[0].pets[0].name);
```
Fetch `children` relation for each result Person and `pets` and `movies`
relations for all the children.
```js
const people = await Person.query().withGraphFetched('children.[pets, movies]');
console.log(people[0].children[0].pets[0].name);
console.log(people[0].children[0].movies[0].id);
```
[Relation expressions](/api/types/#relationexpression-object-notation) can also be objects. This is equivalent to the previous example:
```js
const people = await Person.query().withGraphFetched({
children: {
pets: true,
movies: true
}
});
console.log(people[0].children[0].pets[0].name);
console.log(people[0].children[0].movies[0].id);
```
Relation results can be filtered and modified by giving modifier function names as arguments for the relations:
```js
const people = await Person.query()
.withGraphFetched(
'children(selectNameAndId).[pets(onlyDogs, orderByName), movies]'
)
.modifiers({
selectNameAndId(builder) {
builder.select('name', 'id');
},
orderByName(builder) {
builder.orderBy('name');
},
onlyDogs(builder) {
builder.where('species', 'dog');
}
});
console.log(people[0].children[0].pets[0].name);
console.log(people[0].children[0].movies[0].id);
```
Reusable modifiers can be defined for a model class using [modifiers](/api/model/static-properties.html#static-modifiers). Also see the [modifiers recipe](/recipes/modifiers.md).
```js
class Person extends Model {
static get modifiers() {
return {
// Note that this modifier takes an argument!
filterGender(builder, gender) {
builder.where('gender', gender);
},
defaultSelects(builder) {
builder.select('id', 'firstName', 'lastName');
},
orderByAge(builder) {
builder.orderBy('age');
}
};
}
}
class Animal extends Model {
static get modifiers() {
return {
orderByName(builder) {
builder.orderBy('name');
},
filterSpecies(builder, species) {
builder.where('species', species);
}
};
}
}
const people = await Person.query().modifiers({
// You can bind arguments to Model modifiers like this
filterFemale(builder) {
builder.modify('filterGender', 'female');
},
filterDogs(builder) {
builder.modify('filterSpecies', 'dog');
}
}).withGraphFetched(`
children(defaultSelects, orderByAge, filterFemale).[
pets(filterDogs, orderByName),
movies
]
`);
console.log(people[0].children[0].pets[0].name);
console.log(people[0].children[0].movies[0].id);
```
Filters can also be registered using the [modifyGraph](/api/query-builder/other-methods.html#modifygraph) method:
```js
const people = await Person.query()
.withGraphFetched('children.[pets, movies]')
.modifyGraph('children', builder => {
// Order children by age and only select id.
builder.orderBy('age').select('id');
})
.modifyGraph('children.[pets, movies]', builder => {
// Only select `pets` and `movies` whose id > 10 for the children.
builder.where('id', '>', 10);
});
console.log(people[0].children[0].pets[0].name);
console.log(people[0].children[0].movies[0].id);
```
Relations can be given aliases using the `as` keyword:
```js
const people = await Person.query().withGraphFetched(`[
children(orderByAge) as kids .[
pets(filterDogs) as dogs,
pets(filterCats) as cats
movies.[
actors
]
]
]`);
console.log(people[0].kids[0].dogs[0].name);
console.log(people[0].kids[0].movies[0].id);
```
Eager loading is optimized to avoid the N + 1 queries problem. Consider this query:
```js
const people = await Person.query()
.where('id', 1)
.withGraphFetched('children.children');
console.log(people[0].children.length); // --> 10
console.log(people[0].children[9].children.length); // --> 10
```
The person has 10 children and they all have 10 children. The query above will return 100 database rows but will generate only three database queries when using `withGraphFetched` and only one query when using `withGraphJoined`.
## withGraphJoined()
```js
queryBuilder = queryBuilder.withGraphJoined(relationExpression, graphOptions);
```
Join and fetch a graph of related items for the result of any query (eager loading).
There are two methods that can be used to load relations eagerly: [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) and [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined). The main difference is that [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) uses multiple queries under the hood to fetch the result while [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) uses a single query and joins to fetch the results. Both methods allow you to do different things which we will go through in detail in the examples below and in the examples of the [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) method.
As mentioned, this method uses [SQL joins](https://www.postgresql.org/docs/12/tutorial-join.html) to join all the relations defined in the `relationExpression` and then parses the result into a graph of model instances equal to the one you get from `withGraphFetched`. The main benefit of this is that you can filter the query based on the relations. See the examples.
By default left join is used but you can define the join type using the [joinOperation](/api/types/#type-eageroptions) option.
**Limitations:**
- `limit`, `page` and `range` methods will work incorrectly because they will limit the result set that contains all the result rows in a flattened format. For example the result set of the eager expression children.children will have 10 \* 10 \* 10 rows assuming that you fetched 10 models that all had 10 children that all had 10 children.
**About performance:**
Note that while [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) sounds more performant than [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched), both methods have very similar performance in most cases and [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) is actually much much faster in some cases where the [relationExpression](/api/types/#type-relationexpression) contains multiple many-to-many or has-many relations. The flat record list the db returns for joins can have an incredible amount of duplicate information in some cases. Transferring + parsing that data from the db to node can be very costly, even though the actual joins in the db are very fast. You shouldn't select [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) blindly just because it sounds more peformant. The three rules of optimization apply here too: 1. Don't optimize 2. Don't optimize yet 3. Profile before optimizing. When you don't actually need joins, use [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched).
##### Arguments
| Argument | Type | Description |
| ------------------ | --------------------------------------------------------- | ------------------------------------------------------------ |
| relationExpression | [RelationExpression](/api/types/#type-relationexpression) | The relation expression describing which relations to fetch. |
| options | [GraphOptions](/api/types/#type-graphoptions) | Optional options. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
All examples in [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) also work with `withGraphJoined`. Remember to also study those. The following examples are only about the cases that don't work with [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched)
Using `withGraphJoined` all the relations are joined to the main query and you can reference them in any query building method. Note that nested relations are named by concatenating relation names using `:` as a separator. See the next example:
```js
const people = await Person.query()
.withGraphJoined('children.[pets, movies]')
.whereIn('children.firstName', ['Arnold', 'Jennifer'])
.where('children:pets.name', 'Fluffy')
.where('children:movies.name', 'like', 'Terminator%');
console.log(people[0].children[0].pets[0].name);
console.log(people[0].children[0].movies[0].id);
```
Using [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) you can refer to columns only by their name because the column names are unique in the query. With `withGraphJoined` you often need to also mention the table name. Consider the following example. We join the relation `pets` to a `persons` query. Both tables have the `id` column. We need to use `where('persons.id', '>', 100)` instead of `where('id', '>', 100)` so that objection knows which `id` you mean. If you don't do this, you get an `ambiguous column name` error.
```js
const people = await Person.query()
.withGraphJoined('pets')
.where('persons.id', '>', 100);
```
## graphExpressionObject()
```js
const builder = Person.query().withGraphFetched('children.pets(onlyId)');
const expr = builder.graphExpressionObject();
console.log(expr.children.pets.$modify);
// prints ["onlyId"]
expr.children.movies = true;
// You can modify the object and pass it back to the `withGraphFetched` method.
builder.withGraphFetched(expr);
```
Returns the object representation of the relation expression passed to either `withGraphFetched` or `withGraphJoined`.
See [this section](/api/types/#relationexpression-object-notation) for more examples and information about the structure of the returned object.
##### Return value
| Type | Description |
| ------ | ------------------------------------------------------------------------------------------------------------------ |
| object | Object representation of the current relation expression passed to either `withGraphFetched` or `withGraphJoined`. |
## allowGraph()
```js
queryBuilder = queryBuilder.allowGraph(relationExpression);
```
Sets the allowed tree of relations to fetch, insert or upsert using [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched), [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined), [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) or [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) methods.
When using [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) or [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) the query is rejected and an error is thrown if the [expression](/api/types/#type-relationexpression) passed to the methods is not a subset of the [expression](/api/types/#type-relationexpression) passed to `allowGraph`. This method is useful when the relation expression comes from an untrusted source like query parameters of a http request.
If the model tree given to the [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) or the [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) method isn't a subtree of the given [expression](/api/types/#type-relationexpression), the query is rejected and and error is thrown.
See the examples.
##### Arguments
| Argument | Type | Description |
| ------------------ | --------------------------------------------------------- | ------------------------------- |
| relationExpression | [RelationExpression](/api/types/#type-relationexpression) | The allowed relation expression |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
This will throw because `actors` is not allowed.
```js
await Person.query()
.allowGraph('[children.pets, movies]')
.withGraphFetched('movies.actors');
```
This will not throw:
```js
await Person.query()
.allowGraph('[children.pets, movies]')
.withGraphFetched('children.pets');
```
Calling `allowGraph` multiple times merges the expressions. The following is equivalent to the previous example:
```js
await Person.query()
.allowGraph('children.pets')
.allowGraph('movies')
.withGraphFetched(req.query.eager);
```
Usage in `insertGraph` and `upsertGraph` works the same way. The following will not throw.
```js
const insertedPerson = await Person.query()
.allowGraph('[children.pets, movies]')
.insertGraph({
firstName: 'Sylvester',
children: [
{
firstName: 'Sage',
pets: [
{
name: 'Fluffy',
species: 'dog'
},
{
name: 'Scrappy',
species: 'dog'
}
]
}
]
});
```
This will throw because `cousins` is not allowed:
```js
const insertedPerson = await Person.query()
.allowGraph('[children.pets, movies]')
.upsertGraph({
firstName: 'Sylvester',
children: [
{
firstName: 'Sage',
pets: [
{
name: 'Fluffy',
species: 'dog'
},
{
name: 'Scrappy',
species: 'dog'
}
]
}
],
cousins: [sylvestersCousin]
});
```
You can use [clearAllowGraph](/api/query-builder/eager-methods.html#clearallowgraph) to clear any previous calls to `allowGraph`.
## clearAllowGraph()
Clears all calls to `allowGraph`.
## clearWithGraph()
Clears all calls to `withGraphFetched` and `withGraphJoined`.
## modifyGraph()
```js
queryBuilder = queryBuilder.modifyGraph(pathExpression, modifier);
```
Can be used to modify `withGraphFetched` and `withGraphJoined` queries.
The `pathExpression` is a relation expression that specifies the queries for which the modifier is given.
The following query would filter out the children's pets that are <= 10 years old:
##### Arguments
| Argument | Type | Description |
| -------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| pathExpression | [RelationExpression](/api/types/#type-relationexpression) | Expression that specifies the queries for which to give the filter. |
| modifier | function([QueryBuilder](/api/query-builder/) | string | string[] | A modifier function, [model modifier](/api/model/static-properties.html#static-modifiers) name or an array of model modifier names. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('children.pets', builder => {
builder.where('age', '>', 10);
});
```
The path expression can have multiple targets. The next example sorts both the pets and movies of the children by id:
```js
Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('children.[pets, movies]', builder => {
builder.orderBy('id');
});
```
This example only selects movies whose name contains the word 'Predator':
```js
Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('[children.movies, movies]', builder => {
builder.where('name', 'like', '%Predator%');
});
```
The modifier can also be a [Model modifier](/api/model/static-properties.html#static-modifiers) name, or an array of them:
```js
Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('children.movies', 'selectId');
```
================================================
FILE: doc/api/query-builder/find-methods.md
================================================
# Find Methods
## findById()
```js
queryBuilder = queryBuilder.findById(id);
```
Finds a single item by id.
##### Arguments
| Argument | Type | Description |
| -------- | -------------------------- | --------------- |
| id | any | any[] | The identifier. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const person = await Person.query().findById(1);
```
Composite key:
```js
const person = await Person.query().findById([1, '10']);
```
`findById` can be used together with `patch`, `delete` and any other query method. All it does is adds the needed `where` clauses to the query.
```js
await Person.query()
.findById(someId)
.patch({ firstName: 'Jennifer' });
```
## findByIds()
```js
queryBuilder = queryBuilder.findByIds(ids);
```
Finds a list of items. The order of the returned items is not guaranteed to be the same as the order of the inputs.
##### Arguments
| Argument | Type | Description |
| -------- | ----- | ---------------------- |
| ids | any[] | A List of identifiers. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const [person1, person2] = await Person.query().findByIds([1, 2]);
```
Composite key:
```js
const [person1, person2] = await Person.query().findByIds([
[1, '10'],
[2, '10']
]);
```
## findOne()
```js
queryBuilder = queryBuilder.findOne(...whereArgs);
```
Shorthand for `where(...whereArgs).first()`.
NOTE: [`.first()`](https://vincit.github.io/objection.js/api/query-builder/other-methods.html#first) doesn't add `limit 1` to the query by default. You can override the [Model.useLimitInFirst](/api/model/static-properties.html#static-uselimitinfirst) property to change this behaviour.
##### Arguments
| Argument | Type | Description |
| --------- | ------ | -------------------------------------------------------------------------------- |
| whereArgs | ...any | Anything the [where](/api/query-builder/find-methods.html#where) method accepts. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const person = await Person.query().findOne({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
```
```js
const person = await Person.query().findOne('age', '>', 20);
```
```js
const person = await Person.query().findOne(raw('random() < 0.5'));
```
## alias()
```js
queryBuilder = queryBuilder.alias(alias);
```
Give an alias for the table to be used in the query.
##### Arguments
| Argument | Type | Description |
| -------- | ------ | -------------------------- |
| alias | string | Table alias for the query. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
await Person.query()
.alias('p')
.where('p.id', 1)
.join('persons as parent', 'parent.id', 'p.parentId');
```
## aliasFor()
```js
queryBuilder = queryBuilder.aliasFor(tableNameOrModelClass, alias);
```
Give an alias for any table in the query.
##### Arguments
| Argument | Type | Description |
| --------------------- | ---------------------------------- | ---------------------------------- |
| tableNameOrModelClass | string | ModelClass | The table or model class to alias. |
| alias | string | The alias. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
// This query uses joinRelated to join a many-to-many relation which also joins
// the join table `persons_movies`. We specify that the `persons_movies` table
// should be called `pm` instead of the default `movies_join`.
await Person.query()
.aliasFor('persons_movies', 'pm')
.joinRelated('movies')
.where('pm.someProp', 100);
```
Model class can be used instead of table name
```js
await Person.query()
.aliasFor(Movie, 'm')
.joinRelated('movies')
.where('m.name', 'The Room');
```
## select()
See [knex documentation](https://knexjs.org/guide/query-builder.html#select)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## forUpdate()
See [knex documentation](https://knexjs.org/guide/query-builder.html#forupdate)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## forShare()
See [knex documentation](https://knexjs.org/guide/query-builder.html#forshare)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## forNoKeyUpdate()
See [knex documentation](https://knexjs.org/guide/query-builder.html#fornokeyupdate)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## forKeyShare()
See [knex documentation](https://knexjs.org/guide/query-builder.html#forkeyshare)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## skipLocked()
See [knex documentation](https://knexjs.org/guide/query-builder.html#skiplocked)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## noWait()
See [knex documentation](https://knexjs.org/guide/query-builder.html#nowait)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## as()
See [knex documentation](https://knexjs.org/guide/query-builder.html#as)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## columns()
See [knex documentation](https://knexjs.org/guide/query-builder.html#column)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## column()
See [knex documentation](https://knexjs.org/guide/query-builder.html#column)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## from()
See [knex documentation](https://knexjs.org/guide/query-builder.html#from)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## fromRaw()
See [knex documentation](https://knexjs.org/guide/query-builder.html#fromRaw)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## updateFrom()
See [knex documentation](https://knexjs.org/guide/query-builder.html#updatefrom)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## into()
See [knex documentation](https://knexjs.org/guide/query-builder.html)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## with()
See [knex documentation](https://knexjs.org/guide/query-builder.html#with)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## withMaterialized()
See [knex documentation](https://knexjs.org/guide/query-builder.html#withmaterialized)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## withNotMaterialized()
See [knex documentation](https://knexjs.org/guide/query-builder.html#withnotmaterialized)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## withSchema()
See [knex documentation](https://knexjs.org/guide/query-builder.html#withschema)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## table()
See [knex documentation](https://knexjs.org/guide/query-builder.html)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## distinct()
See [knex documentation](https://knexjs.org/guide/query-builder.html#distinct)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## distinctOn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#distincton)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## where()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## andWhere()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhere()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereNot()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenot)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereNot()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenot)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereRaw()
See [knex documentation](https://knexjs.org/guide/query-builder.html#whereraw)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereWrapped()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## havingWrapped()
See [knex documentation](https://knexjs.org/guide/query-builder.html#having)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereRaw()
See [knex documentation](https://knexjs.org/guide/query-builder.html#whereraw)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereExists()
See [knex documentation](https://knexjs.org/guide/query-builder.html#whereexists)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereExists()
See [knex documentation](https://knexjs.org/guide/query-builder.html#whereexists)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereNotExists()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenotexists)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereNotExists()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenotexists)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereIn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherein)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereIn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherein)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereNotIn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenotin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereNotIn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenotin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereNull()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenull)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereNull()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenull)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereNotNull()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenotnull)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereNotNull()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenotnull)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereBetween()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherebetween)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereNotBetween()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenotbetween)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereBetween()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherebetween)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereNotBetween()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherenotbetween)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereColumn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## andWhereColumn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereColumn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereNotColumn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## andWhereNotColumn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereNotColumn()
See [knex documentation](https://knexjs.org/guide/query-builder.html#where)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereLike()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherelike)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereILike()
See [knex documentation](https://knexjs.org/guide/query-builder.html#whereilike)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## groupBy()
See [knex documentation](https://knexjs.org/guide/query-builder.html#groupby)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## groupByRaw()
See [knex documentation](https://knexjs.org/guide/query-builder.html#groupbyraw)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orderBy()
See [knex documentation](https://knexjs.org/guide/query-builder.html#orderby)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orderByRaw()
See [knex documentation](https://knexjs.org/guide/query-builder.html#orderbyraw)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## union()
See [knex documentation](https://knexjs.org/guide/query-builder.html#union)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## unionAll()
See [knex documentation](https://knexjs.org/guide/query-builder.html#unionall)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## intersect()
See [knex documentation](https://knexjs.org/guide/query-builder.html#intersect)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## having()
See [knex documentation](https://knexjs.org/guide/query-builder.html#having)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## havingRaw()
See [knex documentation](https://knexjs.org/guide/query-builder.html#havingraw)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orHaving()
See [knex documentation](https://knexjs.org/guide/query-builder.html#having)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orHavingRaw()
See [knex documentation](https://knexjs.org/guide/query-builder.html#havingraw)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## offset()
See [knex documentation](https://knexjs.org/guide/query-builder.html#offset)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## limit()
See [knex documentation](https://knexjs.org/guide/query-builder.html#limit)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## count()
See [knex documentation](https://knexjs.org/guide/query-builder.html#count)
Also see the [resultSize](/api/query-builder/other-methods.md#resultsize) method for a cleaner way to just get the number of rows a query would create.
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## countDistinct()
See [knex documentation](https://knexjs.org/guide/query-builder.html#count)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## min()
See [knex documentation](https://knexjs.org/guide/query-builder.html#min)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## max()
See [knex documentation](https://knexjs.org/guide/query-builder.html#max)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## sum()
See [knex documentation](https://knexjs.org/guide/query-builder.html#sum)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## avg()
See [knex documentation](https://knexjs.org/guide/query-builder.html#avg)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## avgDistinct()
See [knex documentation](https://knexjs.org/guide/query-builder.html#avg)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## returning()
See [knex documentation](https://knexjs.org/guide/query-builder.html#returning)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## columnInfo()
See [knex documentation](https://knexjs.org/guide/query-builder.html#columninfo)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereComposite()
```js
queryBuilder = queryBuilder.whereComposite(columns, operator, values);
```
[where](/api/query-builder/find-methods.html#where) for (possibly) composite keys.
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
builder.whereComposite(['id', 'name'], '=', [1, 'Jennifer']);
```
This method also works with a single column - value pair:
```js
builder.whereComposite('id', 1);
```
## whereInComposite()
```js
queryBuilder = queryBuilder.whereInComposite(columns, values);
```
[whereIn](/api/query-builder/find-methods.html#wherein) for (possibly) composite keys.
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
builder.whereInComposite(
['a', 'b'],
[
[1, 2],
[3, 4],
[1, 4]
]
);
```
```js
builder.whereInComposite('a', [[1], [3], [1]]);
```
```js
builder.whereInComposite('a', [1, 3, 1]);
```
```js
builder.whereInComposite(['a', 'b'], SomeModel.query().select('a', 'b'));
```
## jsonExtract()
See [knex documentation](https://knexjs.org/guide/query-builder.html#jsonextract)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## jsonSet()
See [knex documentation](https://knexjs.org/guide/query-builder.html#jsonset)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## jsonInsert()
See [knex documentation](https://knexjs.org/guide/query-builder.html#jsoninsert)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## jsonRemove()
See [knex documentation](https://knexjs.org/guide/query-builder.html#jsonremove)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereJsonObject()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherejsonobject)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereNotJsonObject()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherejsonobject)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereJsonPath()
See [knex documentation](https://knexjs.org/guide/query-builder.html#wherejsonpath)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## whereJsonSupersetOf()
```js
queryBuilder = queryBuilder.whereJsonSupersetOf(
fieldExpression,
jsonObjectOrFieldExpression
);
```
Where left hand json field reference is a superset of the right hand json value or reference.
##### Arguments
| Argument | Type | Description |
| --------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| fieldExpression | [FieldExpression](/api/types/#type-fieldexpression) | Reference to column / json field, which is tested for being a superset |
| jsonObjectOrFieldExpression | Object | Array | [FieldExpression](/api/types/#type-fieldexpression) | To which to compare |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const people = await Person.query().whereJsonSupersetOf(
'additionalData:myDogs',
'additionalData:dogsAtHome'
);
// These people have all or some of their dogs at home. Person might have some
// additional dogs in their custody since myDogs is superset of dogsAtHome.
const people = await Person.query().whereJsonSupersetOf(
'additionalData:myDogs[0]',
{ name: 'peter' }
);
// These people's first dog name is "peter", but the dog might have
// additional attributes as well.
```
Object and array are always their own supersets.
For arrays this means that left side matches if it has all the elements listed in the right hand side. e.g.
```
[1,2,3] isSuperSetOf [2] => true
[1,2,3] isSuperSetOf [2,1,3] => true
[1,2,3] isSuperSetOf [2,null] => false
[1,2,3] isSuperSetOf [] => true
```
The `not` variants with jsonb operators behave in a way that they won't match rows, which don't have the referred json key referred in field expression. e.g. for table
```
id | jsonObject
----+--------------------------
1 | {}
2 | NULL
3 | {"a": 1}
4 | {"a": 1, "b": 2}
5 | {"a": ['3'], "b": ['3']}
```
this query:
```js
builder.whereJsonNotEquals('jsonObject:a', 'jsonObject:b');
```
Returns only the row `4` which has keys `a` and `b` and `a` != `b`, but it won't return any rows that don't have `jsonObject.a` or `jsonObject.b`.
## orWhereJsonSupersetOf()
See [whereJsonSupersetOf](/api/query-builder/find-methods.html#wherejsonsupersetof)
## whereJsonNotSupersetOf()
See [whereJsonSupersetOf](/api/query-builder/find-methods.html#wherejsonsupersetof)
## orWhereJsonNotSupersetOf()
See [whereJsonSupersetOf](/api/query-builder/find-methods.html#wherejsonsupersetof)
## whereJsonSubsetOf()
```js
queryBuilder = queryBuilder.whereJsonSubsetOf(
fieldExpression,
jsonObjectOrFieldExpression
);
```
Where left hand json field reference is a subset of the right hand json value or reference.
Object and array are always their own subsets.
See [whereJsonSupersetOf](/api/query-builder/find-methods.html#wherejsonsupersetof)
##### Arguments
| Argument | Type | Description |
| --------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| fieldExpression | [FieldExpression](/api/types/#type-fieldexpression) | Reference to column / json field, which is tested for being a superset |
| jsonObjectOrFieldExpression | Object | Array | [FieldExpression](/api/types/#type-fieldexpression) | To which to compare |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereJsonSubsetOf()
See [whereJsonSubsetOf](/api/query-builder/find-methods.html#wherejsonsubsetof)
## whereJsonNotSubsetOf()
See [whereJsonSubsetOf](/api/query-builder/find-methods.html#wherejsonsubsetof)
## orWhereJsonNotSubsetOf()
See [whereJsonSubsetOf](/api/query-builder/find-methods.html#wherejsonsubsetof)
## whereJsonIsArray()
```js
queryBuilder = queryBuilder.whereJsonIsArray(fieldExpression);
```
Where json field reference is an array.
##### Arguments
| Argument | Type | Description |
| --------------- | --------------------------------------------------- | ----------- |
| fieldExpression | [FieldExpression](/api/types/#type-fieldexpression) |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereJsonIsArray()
See [whereJsonIsArray](/api/query-builder/find-methods.html#wherejsonisarray)
## whereJsonNotArray()
See [whereJsonIsArray](/api/query-builder/find-methods.html#wherejsonisarray)
## orWhereJsonNotArray()
See [whereJsonIsArray](/api/query-builder/find-methods.html#wherejsonisarray)
## whereJsonIsObject()
```js
queryBuilder = queryBuilder.whereJsonIsObject(fieldExpression);
```
Where json field reference is an object.
##### Arguments
| Argument | Type | Description |
| --------------- | --------------------------------------------------- | ----------- |
| fieldExpression | [FieldExpression](/api/types/#type-fieldexpression) |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereJsonIsObject()
See [whereJsonIsObject](/api/query-builder/find-methods.html#wherejsonisobject)
## whereJsonNotObject()
See [whereJsonIsObject](/api/query-builder/find-methods.html#wherejsonisobject)
## orWhereJsonNotObject()
See [whereJsonIsObject](/api/query-builder/find-methods.html#wherejsonisobject)
## whereJsonHasAny()
```js
queryBuilder = queryBuilder.whereJsonHasAny(fieldExpression, keys);
```
Where any of given strings is found from json object keys.
::: tip
This doesn't work for arrays. If you want to check if an array contains an item, see [this](https://github.com/Vincit/objection.js/issues/415) and [this](https://github.com/Vincit/objection.js/issues/1133) issue.
:::
##### Arguments
| Argument | Type | Description |
| --------------- | --------------------------------------------------- | -------------------------------------------- |
| fieldExpression | [FieldExpression](/api/types/#type-fieldexpression) |
| keys | string | string[] | Strings that are looked from object or array |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereJsonHasAny()
See [whereJsonHasAny](/api/query-builder/find-methods.html#wherejsonhasany)
## whereJsonHasAll()
```js
queryBuilder = queryBuilder.whereJsonHasAll(fieldExpression, keys);
```
Where all of given strings are found from json object keys.
::: tip
This doesn't work for arrays. If you want to check if an array contains an item, see [this](https://github.com/Vincit/objection.js/issues/415) and [this](https://github.com/Vincit/objection.js/issues/1133) issue.
:::
##### Arguments
| Argument | Type | Description |
| --------------- | --------------------------------------------------- | -------------------------------------------- |
| fieldExpression | [FieldExpression](/api/types/#type-fieldexpression) |
| keys | string | string[] | Strings that are looked from object or array |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## orWhereJsonHasAll()
See [whereJsonHasAll](/api/query-builder/find-methods.html#wherejsonhasall)
================================================
FILE: doc/api/query-builder/join-methods.md
================================================
# Join Methods
## joinRelated()
```js
queryBuilder = queryBuilder.joinRelated(relationExpression, opt);
```
Joins a set of relations described by `relationExpression`. See the examples for more info.
##### Arguments
| Argument | Type | Description |
| ------------------ | --------------------------------------------------------- | ------------------------------------------------- |
| relationExpression | [RelationExpression](/api/types/#type-relationexpression) | An expression describing which relations to join. |
| opt | object | Optional options. See the examples. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
Join one relation:
```js
await Person.query()
.joinRelated('pets')
.where('pets.species', 'dog');
```
Give an alias for a single relation:
```js
await Person.query()
.joinRelated('pets', { alias: 'p' })
.where('p.species', 'dog');
```
Join two relations:
```js
await Person.query()
.joinRelated('[pets, parent]')
.where('pets.species', 'dog')
.where('parent.name', 'Arnold');
```
You can also use the [object notation](/api/types/#relationexpression-object-notation)
```js
await Person.query()
.joinRelated({
pets: true,
parent: true
})
.where('pets.species', 'dog')
.where('parent.name', 'Arnold');
```
Join multiple nested relations. Note that when referring to nested relations `:` must be used as a separator instead of `.`. This limitation comes from the way knex parses table references.
```js
await Person.query()
.select('persons.id', 'parent:parent.name as grandParentName')
.joinRelated('[pets, parent.[pets, parent]]')
.where('parent:pets.species', 'dog');
```
Give aliases for a bunch of relations:
```js
await Person.query()
.select('persons.id', 'pr:pr.name as grandParentName')
.joinRelated('[pets, parent.[pets, parent]]', {
aliases: {
parent: 'pr',
pets: 'pt'
}
})
.where('pr:pt.species', 'dog');
```
You can also give aliases using the relation expression:
```js
await Person.query()
.select('persons.id', 'pr:pr.name as grandParentName')
.joinRelated('[pets as pt, parent as pr.[pets as pt, parent as pr]]')
.where('pr:pt.species', 'dog');
```
## innerJoinRelated()
Alias for [joinRelated](/api/query-builder/join-methods.html#joinrelated).
## outerJoinRelated()
Outer join version of the [joinRelated](/api/query-builder/join-methods.html#joinrelated) method.
## leftJoinRelated()
Left join version of the [joinRelated](/api/query-builder/join-methods.html#joinrelated) method.
## leftOuterJoinRelated()
Left outer join version of the [joinRelated](/api/query-builder/join-methods.html#joinrelated) method.
## rightJoinRelated()
Right join version of the [joinRelated](/api/query-builder/join-methods.html#joinrelated) method.
## rightOuterJoinRelated()
Left outer join version of the [joinRelated](/api/query-builder/join-methods.html#joinrelated) method.
## fullOuterJoinRelated()
Full outer join version of the [joinRelated](/api/query-builder/join-methods.html#joinrelated) method.
## join()
See [knex documentation](https://knexjs.org/guide/query-builder.html#join)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## joinRaw()
See [knex documentation](https://knexjs.org/guide/query-builder.html#joinraw)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## innerJoin()
See [knex documentation](https://knexjs.org/guide/query-builder.html#innerjoin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## leftJoin()
See [knex documentation](https://knexjs.org/guide/query-builder.html#leftjoin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## leftOuterJoin()
See [knex documentation](https://knexjs.org/guide/query-builder.html#leftouterjoin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## rightJoin()
See [knex documentation](https://knexjs.org/guide/query-builder.html#rightjoin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## rightOuterJoin()
See [knex documentation](https://knexjs.org/guide/query-builder.html#rightouterjoin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## outerJoin()
See [knex documentation](https://knexjs.org/guide/query-builder.html#fullouterjoin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## fullOuterJoin()
See [knex documentation](https://knexjs.org/guide/query-builder.html#fullouterjoin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## crossJoin()
See [knex documentation](https://knexjs.org/guide/query-builder.html#crossjoin)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
================================================
FILE: doc/api/query-builder/mutate-methods.md
================================================
# Mutating Methods
## insert()
```js
queryBuilder = queryBuilder.insert(modelsOrObjects);
```
Creates an insert query.
The inserted objects are validated against the model's [jsonSchema](/api/model/static-properties.html#static-jsonschema). If validation fails
the Promise is rejected with a [ValidationError](/api/types/#class-validationerror).
NOTE: The return value of the insert query _only_ contains the properties given to the insert
method plus the identifier. This is because we don't make an additional fetch query after
the insert. Using postgres you can chain [returning('\*')](/api/query-builder/find-methods.html#returning) to the query to get all
properties - see [this recipe](/recipes/returning-tricks.html) for some examples. If you use
`returning(['only', 'some', 'props'])` note that the result object will still contain the input properies
**plus** the properties listed in `returning`. On other databases you can use the [insertAndFetch](/api/query-builder/mutate-methods.html#insertandfetch) method.
Batch inserts only work on Postgres because Postgres is the only database engine
that returns the identifiers of _all_ inserted rows. knex supports batch inserts on
other databases also, but you only get the id of the first (or last) inserted object
as a result. If you need batch insert on other databases you can use knex directly
through [knexQuery](/api/model/static-methods.html#static-knexquery).
##### Arguments
| Argument | Type | Description |
| --------------- | -------------------------------------------------------------------------------------------------------------- | ----------------- |
| modelsOrObjects | Object | [Model](/api/model/) | Object[] | [Model](/api/model/)[] | Objects to insert |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const jennifer = await Person.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
console.log(jennifer.id);
```
Batch insert (Only works on Postgres):
```js
const actors = await Movie.relatedQuery('actors')
.for(someMovie)
.insert([
{ firstName: 'Jennifer', lastName: 'Lawrence' },
{ firstName: 'Bradley', lastName: 'Cooper' }
]);
console.log(actors[0].firstName);
console.log(actors[1].firstName);
```
You can also give raw expressions and subqueries as values like this:
```js
const { raw } = require('objection');
await Person.query().insert({
age: Person.query().avg('age'),
firstName: raw("'Jenni' || 'fer'")
});
```
Fields marked as `extras` for many-to-many relations in [relationMappings](/api/model/static-properties.html#static-relationmappings) are automatically
written to the join table instead of the target table. The `someExtra` field in the following example is written
to the join table if the `extra` array of the relation mapping contains the string `'someExtra'`. See [this recipe](/recipes/extra-properties.html) for more info.
```js
const jennifer = await Movie.relatedQuery('actors')
.for(someMovie)
.insert({
firstName: 'Jennifer',
lastName: 'Lawrence',
someExtra: "I'll be written to the join table"
});
console.log(jennifer.someExtra);
```
## insertAndFetch()
```js
queryBuilder = queryBuilder.insertAndFetch(modelsOrObjects);
```
Just like [insert](/api/query-builder/mutate-methods.html#insert) but also fetches the item afterwards.
Note that on postgresql you can just chain [returning('\*')](/api/query-builder/find-methods.html#returning) to the normal insert query to get the same result without an additional query. See [this recipe](/recipes/returning-tricks.html) for some examples.
##### Arguments
| Argument | Type | Description |
| --------------- | -------------------------------------------------------------------------------------------------------------- | ----------------- |
| modelsOrObjects | Object | [Model](/api/model/) | Object[] | [Model](/api/model/)[] | Objects to insert |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## insertGraph()
```js
queryBuilder = queryBuilder.insertGraph(graph, options);
```
See the [section about graph inserts](/guide/query-examples.html#graph-inserts).
##### Arguments
| Argument | Type | Description |
| -------- | -------------------------------------------------------------------------------------------------------------- | ----------------- |
| graph | Object | [Model](/api/model/) | Object[] | [Model](/api/model/)[] | Objects to insert |
| options | [InsertGraphOptions](/api/types/#type-insertgraphoptions) | Optional options. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## insertGraphAndFetch()
Exactly like [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) but also fetches the graph from the db after insert. Note that on postgres, you can simply chain [returning('\*')](/api/query-builder/find-methods.html#returning) to the normal `insertGraph` query to get the same result without additional queries.
## patch()
```js
queryBuilder = queryBuilder.patch(modelOrObject);
```
Creates a patch query.
The patch object is validated against the model's [jsonSchema](/api/model/static-properties.html#static-jsonschema) (if one is defined) _but_ the `required` property of the [jsonSchema](/api/model/static-properties.html#static-jsonschema) is ignored. This way the properties in the patch object are still validated but an error isn't thrown if the patch object doesn't contain all required properties.
If validation fails the Promise is rejected with a [ValidationError](/api/types/#class-validationerror).
The return value of the query will be the number of affected rows. If you want to update a single row and retrieve the updated row as a result, you may want to use the [patchAndFetchById](/api/query-builder/mutate-methods.html#patchandfetchbyid) method or _take a look at [this recipe](/recipes/returning-tricks.html) if you're using Postgres_.
::: tip
This generates an SQL `update` query. While there's also the [update](/api/query-builder/mutate-methods.html#update) method, `patch` is what you want to use most of the time for updates. Read both methods' documentation carefully. If unsure or hate reading, use `patch` to update stuff :smile:
:::
::: warning
[raw](/api/objection/#raw), [lit](/api/objection/#lit), subqueries and other "query properties" in the patch object are not validated. Also fields specified using [FieldExpressions](/api/types/#type-fieldexpression) are not validated.
:::
##### Arguments
| Argument | Type | Description |
| ------------- | -------------------------------------------- | ---------------- |
| modelOrObject | Object | [Model](/api/model/) | The patch object |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
Patching a single row:
```js
const numberOfAffectedRows = await Person.query()
.patch({ age: 24 })
.findById(personId);
console.log(numberOfAffectedRows);
```
Patching multiple rows:
```js
const numberOfAffectedRows = await Person.query()
.patch({ age: 20 })
.where('age', '<', 50);
```
Increment a value atomically:
```js
const numberOfAffectedRows = await Person.query()
.patch({ age: raw('age + 1') })
.where('age', '<', 50);
```
You can also give raw expressions, subqueries and `ref()` as values and [FieldExpressions](/api/types/#type-fieldexpression) as keys. Note that none of these are validated. Objection cannot know what their values will be at the time the validation is done.
```js
const { ref, raw } = require('objection');
await Person.query().patch({
age: Person.query().avg('age'),
// You can use knex.raw instead of `raw()` if
// you prefer.
firstName: raw("'Jenni' || 'fer'"),
oldLastName: ref('lastName'),
// This updates a value nested deep inside a
// json column `detailsJsonColumn`.
'detailsJsonColumn:address.street': 'Elm street'
});
```
## patchAndFetchById()
```js
queryBuilder = queryBuilder.patchAndFetchById(id, modelOrObject);
```
Just like [patch](/api/query-builder/mutate-methods.html#patch) for a single item, but also fetches the updated row from the database afterwards.
##### Arguments
| Argument | Type | Description |
| ------------- | -------------------------------------------- | --------------------------------------------------------- |
| id | any | Identifier of the item to update. Can be a composite key. |
| modelOrObject | Object | [Model](/api/model/) | The patch object |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const updatedPerson = await Person.query().patchAndFetchById(134, { age: 24 });
console.log(updatedPerson.firstName);
```
## patchAndFetch()
```js
queryBuilder = queryBuilder.patchAndFetch(modelOrObject);
```
Just like [patchAndFetchById](/api/query-builder/mutate-methods.html#patchandfetchbyid) but can be used in an instance [\$query](/api/model/instance-methods.html#query) without the need to specify the id.
##### Arguments
| Argument | Type | Description |
| ------------- | -------------------------------------------- | ---------------- |
| modelOrObject | Object | [Model](/api/model/) | The patch object |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const jennifer = await Person.query().findOne({ firstName: 'Jennifer' });
const updatedJennifer = await jennifer.$query().patchAndFetch({ age: 24 });
console.log(updatedJennifer.firstName);
```
## update()
```js
queryBuilder = queryBuilder.update(modelOrObject);
```
Creates an update query.
The update object is validated against the model's [jsonSchema](/api/model/static-properties.html#static-jsonschema). If validation fails the Promise is rejected with a [ValidationError](/api/types/#class-validationerror).
Use `update` if you update the whole row with all its columns. Otherwise, using the [patch](/api/query-builder/mutate-methods.html#patch) method is recommended. When `update` method is used, the validation respects the schema's `required` properties and throws a [ValidationError](/api/types/#class-validationerror) if any of them are missing. [patch](/api/query-builder/mutate-methods.html#patch) ignores the `required` properties and only validates the ones that are found.
The return value of the query will be the number of affected rows. If you want to update a single row and retrieve the updated row as a result, you may want to use the [updateAndFetchById](/api/query-builder/mutate-methods.html#updateandfetchbyid) method or _take a look at [this recipe](/recipes/returning-tricks.html) if you're using Postgres_.
##### Arguments
| Argument | Type | Description |
| ------------- | -------------------------------------------- | ----------------- |
| modelOrObject | Object | [Model](/api/model/) | The update object |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const numberOfAffectedRows = await Person.query()
.update({ firstName: 'Jennifer', lastName: 'Lawrence', age: 24 })
.where('id', 134);
console.log(numberOfAffectedRows);
```
You can also give raw expressions, subqueries and `ref()` as values like this:
```js
const { raw, ref } = require('objection');
await Person.query().update({
firstName: raw("'Jenni' || 'fer'"),
lastName: 'Lawrence',
age: Person.query().avg('age'),
oldLastName: ref('lastName') // same as knex.raw('??', ['lastName'])
});
```
Updating single value inside json column and referring attributes inside json columns (only with postgres) etc.:
```js
await Person.query().update({
lastName: ref('someJsonColumn:mother.lastName').castText(),
'detailsJsonColumn:address.street': 'Elm street'
});
```
## updateAndFetchById()
```js
queryBuilder = queryBuilder.updateAndFetchById(id, modelOrObject);
```
Just like [update](/api/query-builder/mutate-methods.html#update) for a single item, but also fetches the updated row from the database afterwards.
##### Arguments
| Argument | Type | Description |
| ------------- | -------------------------------------------- | --------------------------------------------------------- |
| id | any | Identifier of the item to update. Can be a composite key. |
| modelOrObject | Object | [Model](/api/model/) | The update object |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const updatedPerson = await Person.query().updateAndFetchById(134, person);
console.log(updatedPerson.firstName);
```
## updateAndFetch()
```js
queryBuilder = queryBuilder.updateAndFetch(modelOrObject);
```
Just like [updateAndFetchById](/api/query-builder/mutate-methods.html#updateandfetchbyid) but can be used in an instance [\$query](/api/model/instance-methods.html#query) without the need to specify the id.
##### Arguments
| Argument | Type | Description |
| ------------- | -------------------------------------------- | ----------------- |
| modelOrObject | Object | [Model](/api/model/) | The update object |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const jennifer = await Person.query().findOne({ firstName: 'Jennifer' });
const updatedJennifer = await jennifer.$query().updateAndFetch({ age: 24 });
console.log(updatedJennifer.firstName);
```
## upsertGraph()
```js
queryBuilder = queryBuilder.upsertGraph(graph, options);
```
See the [section about graph upserts](/guide/query-examples.html#graph-upserts)
::: warning
WARNING!
Before you start using `upsertGraph` beware that it's not the silver bullet it seems to be. If you start using it because it seems to provide a "mongodb API" for a relational database, you are using it for a wrong reason!
Our suggestion is to first try to write any code without it and only use `upsertGraph` if it saves you **a lot** of code and makes things simpler. Over time you'll learn where `upsertGraph` helps and where it makes things more complicated. Don't use it by default for everything. You can search through the objection issues to see what kind of problems `upsertGraph` can cause if used too much.
For simple things `upsertGraph` calls are easy to understand and remain readable. When you start passing it a bunch of options it becomes increasingly difficult for other developers (and even yourself) to understand.
It's also really easy to create a server that doesn't work well with multiple users by overusing `upsertGraph`. That's because you can easily get into a situation where you override other user's changes if you always upsert large graphs at a time. Always try to update the minimum amount of rows and columns and you'll save yourself a lot of trouble in the long run.
:::
##### Arguments
| Argument | Type | Description |
| -------- | -------------------------------------------------------------------------------------------------------------- | ----------------- |
| graph | Object | [Model](/api/model/) | Object[] | [Model](/api/model/)[] | Objects to upsert |
| options | [UpsertGraphOptions](/api/types/#type-upsertgraphoptions) | Optional options. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## upsertGraphAndFetch()
Exactly like [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) but also fetches the graph from the db after the upsert operation.
## delete()
```js
queryBuilder = queryBuilder.delete();
```
Creates a delete query.
The return value of the query will be the number of deleted rows. if you're using Postgres
and want to get the deleted rows, _take a look at [this recipe](/recipes/returning-tricks.html)_.
Also see [deleteById](/api/query-builder/mutate-methods.html#deletebyid).
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const numberOfDeletedRows = await Person.query()
.delete()
.where('age', '>', 100);
console.log('removed', numberOfDeletedRows, 'people');
```
You can always use subqueries and all query building methods with `delete` queries, just like with every query in objection. With some databases, you cannot use joins with deletes (db restriction, not objection). You can replace joins with subqueries like this:
```js
// This query deletes all people that have a pet named "Fluffy".
await Person.query()
.delete()
.whereIn(
'id',
Person.query()
.select('persons.id')
.joinRelated('pets')
.where('pets.name', 'Fluffy')
);
// This is another way to implement the same query.
await Person.query()
.delete()
.whereExists(Person.relatedQuery('pets').where('pets.name', 'Fluffy'));
```
Delete can of course be used with [\$relatedQuery](/api/model/instance-methods.html#relatedquery) and [\$query](/api/model/instance-methods.html#query) too.
```js
const person = await Person.query().findById(personId);
// Delete all pets but cats and dogs of a person.
await person
.$relatedQuery('pets')
.delete()
.whereNotIn('species', ['cat', 'dog']);
// Delete all pets of a person.
await person.$relatedQuery('pets').delete();
```
## deleteById()
```js
queryBuilder = queryBuilder.deleteById(id);
```
Deletes an item by id.
The return value of the query will be the number of deleted rows. if you're using Postgres and want to get the deleted rows, _take a look at [this recipe](/recipes/returning-tricks.html)_.
##### Arguments
| Argument | Type | Description |
| -------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- |
| id | any | any[] | The id. Array for composite keys. This method doesn't accept multiple identifiers! See the examples below. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const numberOfDeletedRows = await Person.query().deleteById(1);
console.log('removed', numberOfDeletedRows, 'people');
```
Delete single item with a composite key:
```js
const numberOfDeletedRows = await Person.query().deleteById([10, '20', 46]);
console.log('removed', numberOfDeletedRows, 'people');
```
## relate()
```js
queryBuilder = queryBuilder.relate(ids);
```
Relate (attach) an existing item to another item through a relation.
This method doesn't create a new item but only updates the foreign keys. In
the case of a many-to-many relation, creates a join row to the join table.
On Postgres multiple items can be related by giving an array of identifiers.
The return value of the query is the number of affected items.
##### Arguments
| Argument | Type | Description |
| -------- | ----------------------------------------------------------------------------- | --------------------------------------- |
| ids | number | string | Array | Object | Identifier(s) of the model(s) to relate |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
In the following example we relate an actor to a movie. In this example the relation between `Person` and `Movie` is a many-to-many relation but `relate` also works for all other relation types.
```js
const actor = await Person.query().findById(100);
```
```sql
select "persons".* from "persons" where "persons"."id" = 100
```
```js
const movie = await Movie.query().findById(200);
```
```sql
select "movies".* from "movies" where "movies"."id" = 200
```
```js
await actor.$relatedQuery('movies').relate(movie);
```
```sql
insert into "persons_movies" ("personId", "movieId") values (100, 200)
```
You can also pass the id `200` directly to `relate` instead of passing a model instance. A more objectiony way of doing this would be to utilize the static [relatedQuery](/api/model/static-methods.html#static-relatedquery) method:
```js
await Person.relatedQuery('movies')
.for(100)
.relate(200);
```
```sql
insert into "persons_movies" ("personId", "movieId") values (100, 200)
```
The next example four movies to the first person whose first name Arnold. Note that this query only works on Postgres because on other databases it would require multiple queries.
```js
await Person.relatedQuery('movies')
.for(
Person.query()
.where('firstName', 'Arnold')
.limit(1)
)
.relate([100, 200, 300, 400]);
```
The `relate` method returns the amount of affected rows.
```js
const numRelatedRows = await person
.relatedQuery('movies')
.for(123)
.relate(50);
console.log('movie 50 is now related to person 123 through `movies` relation');
```
Relate multiple (only works with postgres)
```js
const numRelatedRows = await Person.relatedQuery('movies')
.for(123)
.relate([50, 60, 70]);
console.log(`${numRelatedRows} rows were related`);
```
Composite key can either be provided as an array of identifiers or using an object like this:
```js
const numRelatedRows = await Person.relatedQuery('movies')
.for(123)
.relate({ foo: 50, bar: 20, baz: 10 });
console.log(`${numRelatedRows} rows were related`);
```
Fields marked as [extras](/api/types/#type-relationthrough) for many-to-many relations in [relationMappings](/api/model/static-properties.html#static-relationmappings) are automatically written to the join table. The `someExtra` field in the following example is written to the join table if the `extra` array of the relation mapping contains the string `'someExtra'`.
```js
const numRelatedRows = await Movie.relatedQuery('actors')
.for(movieId)
.relate({
id: 50,
someExtra: "I'll be written to the join table"
});
console.log(`${numRelatedRows} rows were related`);
```
## unrelate()
```js
queryBuilder = queryBuilder.unrelate();
```
Remove (detach) a connection between two items.
Doesn't delete the items. Only removes the connection. For ManyToMany relations this
deletes the join row from the join table. For other relation types this sets the
join columns to null.
Note that, unlike for `relate`, you shouldn't pass arguments for the `unrelate` method.
Use `unrelate` like `delete` and filter the rows using the returned query builder.
The return value of the query is the number of affected items.
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const actor = await Person.query().findById(100);
```
```sql
select "persons".* from "persons" where "persons"."id" = 100
```
```js
await actor
.$relatedQuery('movies')
.unrelate()
.where('name', 'like', 'Terminator%');
```
```sql
delete from "persons_movies"
where "persons_movies"."personId" = 100
where "persons_movies"."movieId" in (
select "movies"."id" from "movies" where "name" like 'Terminator%'
)
```
The same using the static [relatedQuery](/api/model/static-methods.html#static-relatedquery) method:
```js
await Person.relatedQuery('movies')
.for(100)
.unrelate()
.where('name', 'like', 'Terminator%');
```
```sql
delete from "persons_movies"
where "persons_movies"."personId" = 100
and "persons_movies"."movieId" in (
select "movies"."id"
from "movies"
where "name" like 'Terminator%'
)
```
The next query removes all Terminator movies from Arnold Schwarzenegger:
```js
// Note that we don't await this query. This query is not executed.
// It's a placeholder that will be used to build a subquery when
// the `relatedQuery` gets executed.
const arnold = Person.query().findOne({
firstName: 'Arnold',
lastName: 'Schwarzenegger'
});
await Person.relatedQuery('movies')
.for(arnold)
.unrelate()
.where('name', 'like', 'Terminator%');
```
```sql
delete from "persons_movies"
where "persons_movies"."personId" in (
select "persons"."id"
from "persons"
where "firstName" = 'Arnold'
and "lastName" = 'Schwarzenegger'
)
and "persons_movies"."movieId" in (
select "movies"."id"
from "movies"
where "name" like 'Terminator%'
)
```
`unrelate` returns the number of affected rows.
```js
const person = await Person.query().findById(123);
const numUnrelatedRows = await person
.$relatedQuery('movies')
.unrelate()
.where('id', 50);
console.log(
'movie 50 is no longer related to person 123 through `movies` relation'
);
```
## increment()
See [knex documentation](https://knexjs.org/guide/query-builder.html#increment)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## decrement()
See [knex documentation](https://knexjs.org/guide/query-builder.html#decrement)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## truncate()
See [knex documentation](https://knexjs.org/guide/query-builder.html#truncate)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## onConflict()
See [knex documentation](https://knexjs.org/guide/query-builder.html#onconflict)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## ignore()
See [knex documentation](https://knexjs.org/guide/query-builder.html#ignore)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## merge()
See [knex documentation](https://knexjs.org/guide/query-builder.html#merge)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
================================================
FILE: doc/api/query-builder/other-methods.md
================================================
# Other Methods
## debug()
Chaining this method to any query will print all the executed SQL to console.
See [knex documentation](https://knexjs.org/guide/query-builder.html#debug)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## toKnexQuery()
```js
knexQueryBuilder = queryBuilder.toKnexQuery();
```
Compiles the query into a knex query and returns the knex query builder instance.
Some objection queries, like when `withGraphFetched` is used, actually execute multiple queries. In these cases, the query builer of the first query is returned
In some rare cases the knex query cannot be built synchronously. In these cases you will get a clear error message. These cases can be handled by calling the optional [initialize](/api/objection/#initialize) method once before the failing `toKnexQuery` method is called.
```js
const { initialize } = require('objection');
await initialize([Person, Pet, Movie, SomeOtherModelClass]);
```
## for()
```js
queryBuilder = queryBuilder.for(relationOwner);
```
This method can only be used in conjunction with the static [relatedQuery](/api/model/static-methods.html#static-relatedquery) method. See the [relatedQuery](/api/model/static-methods.html#static-relatedquery) documentation on how to use it.
This method takes one argument (the owner(s) of the relation) and it can have any of the following types:
- A single identifier (can be composite)
- An array of identifiers (can be composite)
- A [QueryBuilder](/api/query-builder/)
- A model instance.
- An array of model instances.
## context()
```js
queryBuilder = queryBuilder.context(queryContext);
```
Sets/gets the query context.
Some query builder methods create more than one query. The query context is an object that is shared with all queries started by a query builder.
The context is also passed to [\$beforeInsert](/api/model/instance-methods.html#beforeinsert), [\$afterInsert](/api/model/instance-methods.html#afterinsert), [\$beforeUpdate](/api/model/instance-methods.html#beforeupdate), [\$afterUpdate](/api/model/instance-methods.html#afterupdate), [\$beforeDelete](/api/model/instance-methods.html#beforedelete), [\$afterDelete](/api/model/instance-methods.html#afterdelete) and [\$afterFind](/api/model/instance-methods.html#afterfind) calls that the query creates.
In addition to properties added using this method the query context object always has a `transaction` property that holds the active transaction. If there is no active transaction the `transaction` property contains the normal knex instance. In both cases the value can be passed anywhere where a transaction object can be passed so you never need to check for the existence of the `transaction` property.
This method merges the given object with the current context. You can use `clearContext` to clear the context if needed.
See the methods [runBefore](/api/query-builder/other-methods.html#runbefore), [onBuild](/api/query-builder/other-methods.html#onbuild) and [runAfter](/api/query-builder/other-methods.html#runafter)
for more information about the hooks.
##### Arguments
| Argument | Type | Description |
| ------------ | ------ | ------------------------ |
| queryContext | Object | The query context object |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
You can set the context like this:
```js
await Person.query().context({ something: 'hello' });
```
and access the context like this:
```js
const context = builder.context();
```
You can set any data to the context object. You can also register QueryBuilder lifecycle methods for _all_ queries that share the context:
```js
Person.query().context({
runBefore(result, builder) {
return result;
},
runAfter(result, builder) {
return result;
},
onBuild(builder) {}
});
```
For example the `withGraphFetched` method causes multiple queries to be executed from a single query builder. If you wanted to make all of them use the same schema you could write this:
```js
Person.query()
.withGraphFetched('[movies, children.movies]')
.context({
onBuild(builder) {
builder.withSchema('someSchema');
}
});
```
## clearContext()
```js
queryBuilder = queryBuilder.clearContext();
```
Replaces the current context with an empty object.
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## tableNameFor()
```js
const tableName = queryBuilder.tableNameFor(modelClass);
```
Returns the table name for a given model class in the query. Usually the table name can be fetched through `Model.tableName` but if the source table has been changed for example using the [QueryBuilder#table](/api/query-builder/find-methods.html#table) method `tableNameFor` will return the correct value.
##### Arguments
| Argument | Type | Description |
| ---------- | -------- | -------------- |
| modelClass | function | A model class. |
##### Return value
| Type | Description |
| ------ | ------------------------------------------------- |
| string | The source table (or view) name for `modelClass`. |
## tableRefFor()
```js
const tableRef = queryBuilder.tableRefFor(modelClass);
```
Returns the name that should be used to refer to the `modelClass`'s table in the query.
Usually a table can be referred to using its name, but `tableRefFor` can return a different
value for example in case an alias has been given.
##### Arguments
| Argument | Type | Description |
| ---------- | -------- | -------------- |
| modelClass | function | A model class. |
##### Return value
| Type | Description |
| ------ | -------------------------------------------------------------- |
| string | The name that should be used to refer to a table in the query. |
## reject()
```js
queryBuilder = queryBuilder.reject(reason);
```
Skips the database query and "fakes" an error result.
##### Arguments
| Argument | Type | Description |
| -------- | ---- | -------------------- |
| reason | | The rejection reason |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## resolve()
```js
queryBuilder = queryBuilder.resolve(value);
```
Skips the database query and "fakes" a result.
##### Arguments
| Argument | Type | Description |
| -------- | ---- | ----------------- |
| value | | The resolve value |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## isExecutable()
```js
const isExecutable = queryBuilder.isExecutable();
```
Returns `false` if this query will never be executed.
This may be true in multiple cases:
1. The query is explicitly resolved or rejected using the [resolve](/api/query-builder/other-methods.html#resolve) or [reject](/api/query-builder/other-methods.html#reject) methods.
2. The query starts a different query when it is executed.
##### Return value
| Type | Description |
| ------- | ------------------------------------------ |
| boolean | `false` if the query will never be executed. |
## isFind()
```js
const isFind = queryBuilder.isFind();
```
Returns `true` if the query is read-only.
##### Return value
| Type | Description |
| ------- | ------------------------------- |
| boolean | `true` if the query is read-only. |
## isInsert()
```js
const isInsert = queryBuilder.isInsert();
```
Returns `true` if the query performs an insert operation.
##### Return value
| Type | Description |
| ------- | ----------------------------------------------- |
| boolean | `true` if the query performs an insert operation. |
## isUpdate()
```js
const isUpdate = queryBuilder.isUpdate();
```
Returns `true` if the query performs an update or patch operation.
##### Return value
| Type | Description |
| ------- | -------------------------------------------------------- |
| boolean | `true` if the query performs an update or patch operation. |
## isDelete()
```js
const isDelete = queryBuilder.isDelete();
```
Returns `true` if the query performs a delete operation.
##### Return value
| Type | Description |
| ------- | ---------------------------------------------- |
| boolean | `true` if the query performs a delete operation. |
## isRelate()
```js
const isRelate = queryBuilder.isRelate();
```
Returns `true` if the query performs a relate operation.
##### Return value
| Type | Description |
| ------- | ---------------------------------------------- |
| boolean | `true` if the query performs a relate operation. |
## isUnrelate()
```js
const isUnrelate = queryBuilder.isUnrelate();
```
Returns `true` if the query performs an unrelate operation.
##### Return value
| Type | Description |
| ------- | ------------------------------------------------- |
| boolean | `true` if the query performs an unrelate operation. |
## isInternal()
```js
const isInternal = queryBuilder.isInternal();
```
Returns `true` for internal "helper" queries that are not directly
part of the operation being executed. For example the `select` queries
performed by `upsertGraph` to get the current state of the graph are
internal queries.
##### Return value
| Type | Description |
| ------- | -------------------------------------------------------- |
| boolean | `true` if the query performs an internal helper operation. |
## hasWheres()
```js
const hasWheres = queryBuilder.hasWheres();
```
Returns `true` if the query contains where statements.
##### Return value
| Type | Description |
| ------- | -------------------------------------------- |
| boolean | `true` if the query contains where statements. |
## hasSelects()
```js
const hasSelects = queryBuilder.hasSelects();
```
Returns `true` if the query contains any specific select staments, such as:
`'select'`, `'columns'`, `'column'`, `'distinct'`, `'count'`, `'countDistinct'`, `'min'`, `'max'`, `'sum'`, `'sumDistinct'`, `'avg'`, `'avgDistinct'`
##### Return value
| Type | Description |
| ------- | -------------------------------------------------------- |
| boolean | `true` if the query contains any specific select staments. |
## hasWithGraph()
```js
const hasWithGraph = queryBuilder.hasWithGraph();
```
Returns `true` if `withGraphFetched` or `withGraphJoined` has been called for the query.
##### Return value
| Type | Description |
| ------- | ------------------------------------------------------------------------------ |
| boolean | `true` if `withGraphFetched` or `withGraphJoined` has been called for the query. |
## has()
```js
const has = queryBuilder.has(selector);
```
```js
console.log(
Person.query()
.range(0, 4)
.has('range')
);
```
Returns `true` if the query defines an operation that matches the given selector.
##### Arguments
| Argument | Type | Description |
| -------- | ------------------------------ | --------------------------------------------------------------------- |
| selector | string | RegExp | A name or regular expression to match all defined operations against. |
##### Return value
| Type | Description |
| ------- | ----------------------------------------------------------------------- |
| boolean | `true` if the query defines an operation that matches the given selector. |
## clear()
```js
queryBuilder = queryBuilder.clear(selector);
```
Removes all operations in the query that match the given selector.
##### Arguments
| Argument | Type | Description |
| -------- | ------------------------------ | ------------------------------------------------------------------------------------ |
| selector | string | regexp | A name or regular expression to match all operations that are to be removed against. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
console.log(
Person.query()
.orderBy('firstName')
.clear('orderBy')
.has('orderBy')
);
```
## runBefore()
```js
queryBuilder = queryBuilder.runBefore(runBefore);
```
Registers a function to be called before just the database query when the builder is executed. Multiple functions can be chained like `then` methods of a promise.
##### Arguments
| Argument | Type | Description |
| --------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| runBefore | function(result, [QueryBuilder](/api/query-builder/)) | The function to be executed. This function can be async. Note that it needs to return the result used for further processing in the chain of calls. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const query = Person.query();
query
.runBefore(async result => {
console.log('hello 1');
await Promise.delay(10);
console.log('hello 2');
return result;
})
.runBefore(result => {
console.log('hello 3');
return result;
});
await query;
// --> hello 1
// --> hello 2
// --> hello 3
```
## onBuild()
```js
queryBuilder = queryBuilder.onBuild(onBuild);
```
Functions registered with this method are called each time the query is built into an SQL string. This method is ran after [runBefore](/api/query-builder/other-methods.html#runbefore) methods but before [runAfter](/api/query-builder/other-methods.html#runafter) methods.
If you need to modify the SQL query at query build time, this is the place to do it. You shouldn't modify the query in any of the `run` methods.
Unlike the `run` methods (`runAfter`, `runBefore` etc.) these must be synchronous. Also you should not register any `run` methods from these. You should _only_ call the query building methods of the builder provided as a parameter.
##### Arguments
| Argument | Type | Description |
| -------- | --------------------------------------------- | -------------------------------------------- |
| onBuild | function([QueryBuilder](/api/query-builder/)) | The **synchronous** function to be executed. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Eamples
```js
const query = Person.query();
query
.onBuild(builder => {
builder.where('id', 1);
})
.onBuild(builder => {
builder.orWhere('id', 2);
});
```
## onBuildKnex()
```js
queryBuilder = queryBuilder.onBuildKnex(onBuildKnex);
```
Functions registered with this method are called each time the query is built into an SQL string. This method is ran after [onBuild](/api/query-builder/other-methods.html#onbuild) methods but before [runAfter](/api/query-builder/other-methods.html#runafter) methods.
If you need to modify the SQL query at query build time, this is the place to do it in addition to `onBuild`. The only difference between `onBuildKnex` and `onBuild` is that in `onBuild` you can modify the objection's query builder. In `onBuildKnex` the objection builder has been compiled into a knex query builder and any modifications to the objection builder will be ignored.
Unlike the `run` methods (`runAfter`, `runBefore` etc.) these must be synchronous. Also you should not register any `run` methods from these. You should _only_ call the query building methods of the **knexBuilder** provided as a parameter.
::: warning
You should never call any query building (or any other mutating) method on the `objectionBuilder` in this function. If you do, those calls will get ignored. At this point the query builder has been compiled into a knex query builder and you should only modify that. You can call non mutating methods like `hasSelects`, `hasWheres` etc. on the objection builder.
:::
##### Arguments
| Argument | Type | Description |
| ----------- | ---------------------------------------------------------------------- | ---------------------------- |
| onBuildKnex | function(`KnexQueryBuilder`, [QueryBuilder](/api/query-builder/)) | The function to be executed. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const query = Person.query();
query.onBuildKnex((knexBuilder, objectionBuilder) => {
knexBuilder.where('id', 1);
});
```
## runAfter()
```js
queryBuilder = queryBuilder.runAfter(runAfter);
```
Registers a function to be called when the builder is executed.
These functions are executed as the last thing before any promise handlers registered using the [then](/api/query-builder/other-methods.html#then) method. Multiple functions can be chained like [then](/api/query-builder/other-methods.html#then) methods of a promise.
##### Arguments
| Argument | Type | Description |
| -------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| runAfter | function(result, [QueryBuilder](/api/query-builder/)) | The function to be executed. This function can be async. Note that it needs to return the result used for further processing in the chain of calls. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const query = Person.query();
query
.runAfter(async (models, queryBuilder) => {
return models;
})
.runAfter(async (models, queryBuilder) => {
models.push(Person.fromJson({ firstName: 'Jennifer' }));
return models;
});
const models = await query;
```
## onError()
```js
queryBuilder = queryBuilder.onError(onError);
```
Registers an error handler. Just like `catch` but doesn't execute the query.
##### Arguments
| Argument | Type | Description |
| -------- | --------------------------------------------------------- | ------------------------------------- |
| onError | function(Error, [QueryBuilder](/api/query-builder/)) | The function to be executed on error. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const query = Person.query();
query
.onError(async (error, queryBuilder) => {
// Handle `SomeError` but let other errors go through.
if (error instanceof SomeError) {
// This will cause the query to be resolved with an object
// instead of throwing an error.
return { error: 'some error occurred' };
} else {
return Promise.reject(error);
}
})
.where('age', '>', 30);
```
## castTo()
```js
queryBuilder = queryBuilder.castTo(ModelClass);
```
```ts
queryBuilder = queryBuilder.castTo();
```
Sets the model class of the result rows or if no arguments are provided, simply casts the
result type to the provided type.
##### Return value
| Type | Description |
| ------------------------- | ----------------------------------- |
| [ModelClass](/api/model/) | The model class of the result rows. |
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
The following example creates a query through `Person`, joins a bunch of relations, selects
only the related `Animal`'s columns and returns the results as `Animal` instances instead
of `Person` instances.
```js
const animals = await Person.query()
.joinRelated('children.children.pets')
.select('children:children:pets.*')
.castTo(Animal);
```
If you don't provide any arguments for the method, but provide a generic type argument, the
result is not changed but its typescript type is cast to the given generic type.
```ts
interface Named {
name: string;
}
const result = await Person.query()
.select('firstName as name')
.castTo();
console.log(result[0].name);
```
## modelClass()
```js
const modelClass = queryBuilder.modelClass();
```
Gets the Model subclass this builder is bound to.
##### Return value
| Type | Description |
| -------------------- | ------------------------------------------- |
| [Model](/api/model/) | The Model subclass this builder is bound to |
## skipUndefined()
```js
queryBuilder = queryBuilder.skipUndefined();
```
If this method is called for a builder then undefined values passed to the query builder methods don't cause an exception but are ignored instead.
For example the following query will return all `Person` rows if `req.query.firstName` is `undefined`.
##### Return value
| Type | Description |
| ----------------------------------- | --------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining |
##### Examples
```js
Person.query()
.skipUndefined()
.where('firstName', req.query.firstName);
```
## transacting()
```js
queryBuilder = queryBuilder.transacting(transaction);
```
Sets the transaction for a query.
##### Arguments
| Argument | Type | Description |
| ----------- | ------ | -------------------- |
| transaction | object | A transaction object |
##### Return value
| Type | Description |
| ----------------------------------- | --------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining |
## clone()
```js
const clone = queryBuilder.clone();
```
Create a clone of this builder.
| Type | Description |
| ----------------------------------- | -------------------------- |
| [QueryBuilder](/api/query-builder/) | Clone of the query builder |
## execute()
```js
const promise = queryBuilder.execute();
```
Executes the query and returns a Promise.
##### Return value
| Type | Description |
| --------- | ---------------------------------------------------------- |
| `Promise` | Promise the will be resolved with the result of the query. |
## then()
```js
const promise = queryBuilder.then(successHandler, errorHandler);
```
Executes the query and returns a Promise.
##### Arguments
| Argument | Type | Default | Description |
| -------------- | -------- | -------- | ----------------------- |
| successHandler | function | identity | Promise success handler |
| errorHandler | function | identity | Promise error handler |
##### Return value
| Type | Description |
| --------- | ---------------------------------------------------------- |
| `Promise` | Promise the will be resolved with the result of the query. |
## catch()
```js
const promise = queryBuilder.catch(errorHandler);
```
Executes the query and calls `catch(errorHandler)` for the returned promise.
##### Arguments
| Argument | Type | Default | Description |
| ------------ | -------- | -------- | ------------- |
| errorHandler | function | identity | Error handler |
##### Return value
| Type | Description |
| --------- | ---------------------------------------------------------- |
| `Promise` | Promise the will be resolved with the result of the query. |
## bind()
```js
const promise = queryBuilder.bind(returnValue);
```
Executes the query and calls `bind(context)` for the returned promise.
##### Arguments
| Argument | Type | Default | Description |
| -------- | ---- | --------- | ------------ |
| context | | undefined | Bind context |
##### Return value
| Type | Description |
| --------- | ---------------------------------------------------------- |
| `Promise` | Promise the will be resolved with the result of the query. |
## resultSize()
```js
const promise = queryBuilder.resultSize();
```
Returns the amount of rows the current query would produce without [limit](/api/query-builder/find-methods.html#limit) and [offset](/api/query-builder/find-methods.html#offset) applied. Note that this executes a copy of the query and returns a Promise.
This method is often more convenient than `count` which returns an array of objects instead a single number.
##### Return value
| Type | Description |
| ----------------- | -------------------------------------------------- |
| `Promise` | Promise the will be resolved with the result size. |
##### Examples
```js
const query = Person.query().where('age', '>', 20);
const [total, models] = await Promise.all([
query.resultSize(),
query.offset(100).limit(50)
]);
```
## page()
```js
queryBuilder = queryBuilder.page(page, pageSize);
```
```js
const result = await Person.query()
.where('age', '>', 20)
.page(5, 100);
console.log(result.results.length); // --> 100
console.log(result.total); // --> 3341
```
Two queries are performed by this method: the actual query and a query to get the `total` count.
Mysql has the `SQL_CALC_FOUND_ROWS` option and `FOUND_ROWS()` function that can be used to calculate the result size, but according to my tests and [the interwebs](https://www.google.com/search?q=SQL_CALC_FOUND_ROWS+performance) the performance is significantly worse than just executing a separate count query.
Postgresql has window functions that can be used to get the total count like this `select count(*) over () as total`. The problem with this is that if the result set is empty, we don't get the total count either. (If someone can figure out a way around this, a PR is very welcome).
##### Arguments
| Argument | Type | Description |
| -------- | ------ | ------------------------------------------------------------------ |
| page | number | The index of the page to return. The index of the first page is 0. |
| pageSize | number | The page size |
##### Return value
| Type | Description |
| ----------------------------------- | --------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining |
## range()
```js
queryBuilder = queryBuilder.range(start, end);
```
Only returns the given range of results.
Two queries are performed by this method: the actual query and a query to get the `total` count.
Mysql has the `SQL_CALC_FOUND_ROWS` option and `FOUND_ROWS()` function that can be used to calculate the result size, but according to my tests and [the interwebs](https://www.google.com/search?q=SQL_CALC_FOUND_ROWS+performance) the performance is significantly worse than just executing a separate count query.
Postgresql has window functions that can be used to get the total count like this `select count(*) over () as total`. The problem with this is that if the result set is empty, we don't get the total count either. (If someone can figure out a way around this, a PR is very welcome).
##### Arguments
| Argument | Type | Description |
| -------- | ------ | ----------------------------------------- |
| start | number | The index of the first result (inclusive) |
| end | number | The index of the last result (inclusive) |
##### Return value
| Type | Description |
| ----------------------------------- | --------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining |
##### Examples
```js
const result = await Person.query()
.where('age', '>', 20)
.range(0, 100);
console.log(result.results.length); // --> 101
console.log(result.total); // --> 3341
```
`range` can be called without arguments if you want to specify the limit and offset explicitly:
```js
const result = await Person.query()
.where('age', '>', 20)
.limit(10)
.range();
console.log(result.results.length); // --> 101
console.log(result.total); // --> 3341
```
## first()
```js
queryBuilder = queryBuilder.first();
```
If the result is an array, selects the first item.
NOTE: This doesn't add `limit 1` to the query by default. You can override the [Model.useLimitInFirst](/api/model/static-properties.html#static-uselimitinfirst) property to change this behaviour.
Also see [findById](/api/query-builder/find-methods.html#findbyid) and [findOne](/api/query-builder/find-methods.html#findone) shorthand methods.
##### Return value
| Type | Description |
| ----------------------------------- | --------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining |
##### Examples
```js
const firstPerson = await Person.query().first();
console.log(firstPerson.age);
```
## throwIfNotFound()
```js
queryBuilder = queryBuilder.throwIfNotFound(data);
```
Causes a [Model.NotFoundError](/api/types/#class-notfounderror) to be thrown if the query result is empty.
You can replace `Model.NotFoundError` with your own error by implementing the static [Model.createNotFoundError(ctx)](/api/model/static-methods.html#static-createnotfounderror) method.
##### Arguments
| Argument | Type | Description |
| -------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| data | object | optional object with custom data may contain any property such as message, type. The object is returned in the error thrown under the `data` property. A special case is for the optional message property. This is used to set the title of the error. These extra properties can be leveraged in the error handling middleware |
##### Return value
| Type | Description |
| ----------------------------------- | --------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining |
##### Examples
```js
try {
await Language.query()
.where('name', 'Java')
.andWhere('isModern', true)
.throwIfNotFound({
message: `Custom message returned`,
type: `Custom type`
});
} catch (err) {
// No results found.
console.log(err instanceof Language.NotFoundError); // --> true
}
```
## timeout()
See [knex documentation](https://knexjs.org/guide/query-builder.html#debug)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## connection()
See [knex documentation](https://knexjs.org/guide/query-builder.html#connection)
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## modify()
Works like `knex`'s [modify](https://knexjs.org/guide/query-builder.html#modify) function but in addition you can specify a [modifier](/api/model/static-properties.html#static-modifiers) by providing modifier names.
See the [modifier](/recipes/modifiers.html) recipe for examples of the things you can do with modifiers.
##### Arguments
| Argument | Type | Description |
| ----------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| modifier | function([QueryBuilder](/api/query-builder/)) | string | string[] | The modify callback function, receiving the builder as its first argument, followed by the optional arguments. If a string or an array of strings is provided, the corresponding [modifier](/api/model/static-properties.html#static-modifiers) is executed instead. |
| \*arguments | ...any | The optional arguments passed to the modify function |
##### Examples
The first argument can be a name of a [model modifier](/api/model/static-properties.html#static-modifiers). The rest of the arguments are passed as arguments for the modifier.
```js
Person.query().modify('someModifier', 'foo', 1);
```
You can also pass an array of modifier names:
```js
Person.query().modify(['someModifier', 'someOtherModifier'], 'foo', 1);
```
The first argument can be a function:
```js
function modifierFunc(query, arg1, arg2) {
query.where(arg1, arg2);
}
Person.query().modify(modifierFunc, 'foo', 1);
```
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
## modifiers()
Registers modifiers for the query.
You can call this method without arguments to get the currently registered modifiers.
See the [modifier recipe](/recipes/modifiers.html) for more info and examples.
##### Return value
| Type | Description |
| ----------------------------------- | ---------------------------------- |
| [QueryBuilder](/api/query-builder/) | `this` query builder for chaining. |
##### Examples
```js
const people = await Person.query()
.modifiers({
selectFields: query => query.select('id', 'name'),
// In the following modifier, `filterGender` is a modifier
// registered in Person.modifiers object. Query modifiers
// can be used to bind arguments to model modifiers like this.
filterWomen: query => query.modify('filterGender', 'female')
})
.modify('selectFields')
.withGraphFetched('children(selectFields, filterWomen)');
```
You can get the currently registered modifiers by calling the method without arguments.
```js
const modifiers = query.modifiers();
```
================================================
FILE: doc/api/query-builder/static-methods.md
================================================
# Static Methods
## forClass()
```js
const builder = QueryBuilder.forClass(modelClass);
```
Create QueryBuilder for a Model subclass. You rarely need to call this. Query builders are created using the [Model.query()](/api/model/static-methods.html#query) and other query methods.
##### Arguments
| Argument | Type | Description |
| ---------- | ---------- | ------------------------- |
| modelClass | ModelClass | A Model class constructor |
##### Return value
| Type | Description |
| ----------------------------------- | ------------------------- |
| [QueryBuilder](/api/query-builder/) | The created query builder |
## parseRelationExpression()
```js
const exprObj = QueryBuilder.parseRelationExpression(expr);
```
Parses a string relation expression into the [object notation](/api/types/#relationexpression-object-notation).
##### Arguments
| Argument | Type | Description |
| -------- | --------------------------------------------------------- | --------------------------------------- |
| expr | [RelationExpression](/api/types/#type-relationexpression) | A relation expression string or object. |
##### Return value
| Type | Description |
| ------ | ------------------------------------------- |
| object | The relation expression in object notation. |
================================================
FILE: doc/api/types/README.md
================================================
---
sidebar: auto
---
# Types
This page contains the documentation of all other types and classes than [Model](/api/model/) and [QueryBuilder](/api/query-builder/). There are two types of items on this page:
1. `type`: A type is just a POJO (Plain Old Javascript Object) with a set of properties.
2. `class`: A class is a JavaScript class with properties and methods.
## `type` RelationMapping
| Property | Type | Description |
| ------------ | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| relation | function | The relation type. One of `Model.BelongsToOneRelation`, `Model.HasOneRelation`, `Model.HasManyRelation`, `Model.ManyToManyRelation` and `Model.HasOneThroughRelation`. |
| modelClass | [Model](/api/model/)
string | Constructor of the related model class, an absolute path to a module that exports one or a path relative to [modelPaths](/api/model/static-properties.html#static-modelpaths) that exports a model class. |
| join | [RelationJoin](/api/types/#type-relationjoin) | Describes how the models are related to each other. See [RelationJoin](/api/types/#type-relationjoin). |
| modify | function([QueryBuilder](/api/query-builder/))
string
object | Optional modifier for the relation query. If specified as a function, it will be called each time before fetching the relation. If specified as a string, modifier with specified name will be applied each time when fetching the relation. If specified as an object, it will be used as an additional query parameter - e. g. passing {name: 'Jenny'} would additionally narrow fetched rows to the ones with the name 'Jenny'. |
| filter | function([QueryBuilder](/api/query-builder/))
string
object | Alias for modify. |
| beforeInsert | function([Model](/api/model/), [QueryContext](/api/query-builder/other-methods.html#context)) | Optional insert hook that is called for each inserted model instance. This function can be async. |
## `type` RelationJoin
| Property | Type | Description |
| -------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| from | string
[ReferenceBuilder](/api/objection/#ref)
Array | The relation column in the owner table. Must be given with the table name. For example `persons.id`. Composite key can be specified using an array of columns e.g. `['persons.a', 'persons.b']`. Note that neither this nor `to` need to be foreign keys or primary keys. You can join any column to any column. You can even join nested json fields using the [ref](/api/objection/#ref) helper. |
| to | string
[ReferenceBuilder](/api/objection/#ref)
Array | The relation column in the related table. Must be given with the table name. For example `movies.id`. Composite key can be specified using an array of columns e.g. `['movies.a', 'movies.b']`. Note that neither this nor `from` need to be foreign keys or primary keys. You can join any column to any column. You can even join nested json fields using the [ref](/api/objection/#ref) helper. |
| through | [RelationThrough](/api/types/#type-relationthrough) | Describes the join table if the models are related through one. |
## `type` RelationThrough
| Property | Type | Description |
| ------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| from | string
[ReferenceBuilder](/api/objection/#ref)
Array | The column that is joined to `from` property of the `RelationJoin`. For example `Person_movies.actorId` where `Person_movies` is the join table. Composite key can be specified using an array of columns e.g. `['persons_movies.a', 'persons_movies.b']`. You can join nested json fields using the [ref](/api/objection/#ref) helper. |
| to | string
[ReferenceBuilder](/api/objection/#ref)
Array | The column that is joined to `to` property of the `RelationJoin`. For example `Person_movies.movieId` where `Person_movies` is the join table. Composite key can be specified using an array of columns e.g. `['persons_movies.a', 'persons_movies.b']`. You can join nested json fields using the [ref](/api/objection/#ref) helper. |
| modelClass | string
ModelClass | If you have a model class for the join table, you should specify it here. This is optional so you don't need to create a model class if you don't want to. |
| extra | string
string[]
Object | Join table columns listed here are automatically joined to the related objects when they are fetched and automatically written to the join table instead of the related table on insert and update. The values can be aliased by providing an object `{propertyName: 'columnName', otherPropertyName: 'otherColumnName'} instead of array` See [this recipe](/recipes/extra-properties.html) for more info. |
| modify | function([QueryBuilder](/api/query-builder/))
string
object | Optional modifier for the join table query. If specified as a function, it will be called each time before fetching the relation. If specified as a string, modifier with specified name will be applied each time when fetching the relation. If specified as an object, it will be used as an additional query parameter - e. g. passing {name: 'Jenny'} would additionally narrow fetched rows to the ones with the name 'Jenny'. |
| filter | function([QueryBuilder](/api/query-builder/))
string
object | Alias for modify. |
| beforeInsert | function([Model](/api/model/), [QueryContext](/api/query-builder/other-methods.html#context)) | Optional insert hook that is called for each inserted join table model instance. This function can be async. |
## `type` ModelOptions
| Property | Type | Description |
| -------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| patch | boolean | If `true` the json is treated as a patch and the `required` field of the json schema is ignored in the validation. This allows us to create models with a subset of required properties for patch operations. |
| skipValidation | boolean | If `true` the json schema validation is skipped |
| old | object | The old values for methods like `$beforeUpdate` and `$beforeValidate`. |
## `type` CloneOptions
| Property | Type | Description |
| -------- | ------- | ------------------------------ |
| shallow | boolean | If `true`, relations are ignored |
## `type` ToJsonOptions
| Property | Type | Description |
| -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| shallow | boolean | If `true`, relations are ignored. Default is `false`. |
| virtuals | boolean
string[] | If `false`, virtual attributes are omitted from the output. Default is `true`. You can also pass an array of property names and only those virtual properties get picked. You can even pass in property/function names that are not included in the static `virtualAttributes` array. |
## `type` GraphOptions
| Property | Type | Description |
| ------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| minimize | boolean | If `true` the aliases of the joined tables and columns created by `withGraphJoined` are minimized. This is sometimes needed because of identifier length limitations of some database engines. objection throws an exception when a query exceeds the length limit. You need to use this only in those cases. |
| separator | string | Separator between relations in nested `withGraphJoined` query. Defaults to `:`. Dot (`.`) cannot be used at the moment because of the way knex parses the identifiers. |
| aliases | Object | Aliases for relations in a `withGraphJoined` query. Defaults to an empty object. |
| joinOperation | string | Which join type to use `['leftJoin', 'innerJoin', 'rightJoin', ...]` or any other knex join method name. Defaults to `leftJoin`. |
| maxBatchSize | integer | For how many parents should a relation be fetched using a single query at a time. If you set this to `1` then a separate query is used for each parent to fetch a relation. For example if you want to fetch pets for 5 persons, you get five queries (one for each person). Setting this to `1` will allow you to use stuff like `limit` and aggregate functions in `modifyGraph` and other graph modifiers. This can be used to replace the `naiveEager` objection 1.x had. |
## `type` UpsertGraphOptions
| Property | Type | Description |
| ------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| relate | boolean
string[] | If `true`, relations are related instead of inserted. Relate functionality can be enabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| unrelate | boolean
string[] | If `true`, relations are unrelated instead of deleted. Unrelate functionality can be enabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| insertMissing | boolean
string[] | If `true`, models that have identifiers _and_ are not found in the database, are inserted. By default this is `false` and an error is thrown. This functionality can be enabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| update | boolean
string[] | If `true`, update operations are performed instead of patch when altering existing models, affecting the way the data is validated. With update operations, all required fields need to be present in the data provided. This functionality can be enabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| noInsert | boolean
string[] | If `true`, no inserts are performed. Inserts can be disabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| noUpdate | boolean
string[] | If `true`, no updates are performed. Updates can be disabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| noDelete | boolean
string[] | If `true`, no deletes are performed. Deletes can be disabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| noRelate | boolean
string[] | If `true`, no relates are performed. Relate operations can be disabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| noUnrelate | boolean
string[] | If `true`, no unrelate operations are performed. Unrelate operations can be disabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-upserts). |
| allowRefs | boolean | This needs to be `true` if you want to use `#ref` in your graphs. See [this section](/guide/query-examples.html#graph-inserts) for `#ref` usage examples. |
## `type` InsertGraphOptions
| Property | Type | Description |
| --------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| relate | boolean
string[] | If `true`, models with an `id` are related instead of inserted. Relate functionality can be enabled for a subset of relations of the graph by providing a list of relation expressions. See the examples [here](/guide/query-examples.html#graph-inserts). |
| allowRefs | boolean | This needs to be `true` if you want to use `#ref` in your graphs. See [this section](/guide/query-examples.html#graph-inserts) for `#ref` usage examples. |
## `type` FetchGraphOptions
| Property | Type | Description |
| ------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| transaction | knex
Transaction | Optional transaction or knex instance for the query. This can be used to specify a transaction or even a different database. |
| skipFetched | boolean | If `true`, only fetch relations that don't yet exist in the object. |
## `type` TableMetadataFetchOptions
| Property | Type | Description |
| -------- | ------------------- | ----------------------------------------------------------- |
| table | string | A custom table name. If not given, Model.tableName is used. |
| knex | knex
Transaction | A knex instance or a transaction |
## `type` TableMetadataOptions
| Property | Type | Description |
| -------- | ------ | ----------------------------------------------------------- |
| table | string | A custom table name. If not given, Model.tableName is used. |
## `type` TableMetadata
| Property | Type | Description |
| -------- | -------- | ------------------------------------ |
| columns | string[] | Names of all the columns in a table. |
## `type` StaticHookArguments
| Property | Type | Description |
| ----------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| items | Model[] | Items for which the query was started. For example in case of an instance query `person.$query()` or `person.$relatedQuery('pets')` `items` would equal `[person]`. In case of `Person.relatedQuery('pets').for([matt, jennifer])` `items` would equal `[matt, jennifer]`. In many cases like `Person.query()` or `Person.query().findById(1)` this array is empty. It's only populated when the query has been explicitly started for a set of model instances. |
| inputItems | Model[] | Items that were passed as an input for the query. For example in case of `Person.query().insert(person)` or `Person.query().patch(person)` `inputItems` would equal `[person]`. |
| asFindQuery | () => [QueryBuilder](/api/query-builder/) | A function that returns a query builder that can be used to fetch the items that were/would get affected by the query being executed. Modifying this query builder doesn't affect the query being executed. For example calling `await asFindQuery().select('id')` in a `beforeDelete` hook would get you the identifiers of all the items that will get deleted by the query. This query is automatically executed inside any existing transaction. This query builder always returns an array even if the query being executed would return an object, a number or something else. |
| transaction | knex
Transaction | If the query being executed has a transaction, this property will contain it. Otherwise this holds the knex instance installed for the query. Either way, this can and should be passed to any queries executed in the static hooks. |
| context | object | The context of the query. See [context](/api/query-builder/other-methods.html#context). |
| relation | [Relation](#class-relation) | If the query is for a relation, this property holds the [Relation](#class-relation) object. For example when you call `person.$relatedQuery('pets)` or `Person.relatedQuery('movies')` the `relation` will be a relation object for pets and movies relation of `Person` respectively. |
| cancelQuery | function(any) | Cancels the query being executed. You can pass an arugment for the function and that value will be the result of the query. |
| result | any[] | The result of the query. Only available in `after*` hooks. |
## `type` FieldExpression
Field expressions are strings that allow you to refer to JSONB fields inside columns.
Syntax: `[:]`
e.g. `persons.jsonColumnName:details.names[1]` would refer to value `'Second'` in column `persons.jsonColumnName` which has
`{ details: { names: ['First', 'Second', 'Last'] } }` object stored in it.
First part `` is compatible with column references used in knex e.g. `MyFancyTable.tributeToThBestColumnNameEver`.
Second part describes a path to an attribute inside the referred column. It is optional and it always starts with colon which follows directly with first path element. e.g. `Table.jsonObjectColumnName:jsonFieldName` or `Table.jsonArrayColumn:[321]`.
Syntax supports `[]` and `.` flavors of reference to json keys / array indexes:
e.g. both `Table.myColumn:[1][3]` and `Table.myColumn:1.3` would access correctly both of the following objects `[null, [null,null,null, "I was accessed"]]` and `{ "1": { "3" : "I was accessed" } }`
Caveats when using special characters in keys:
1. `objectColumn.key` This is the most common syntax, good if you are not using dots or square brackets `[]` in your json object key name.
2. Keys containing dots `objectColumn:[keywith.dots]` Column `{ "keywith.dots" : "I was referred" }`
3. Keys containing square brackets `column['[]']` `{ "[]" : "This is getting ridiculous..." }`
4. Keys containing square brackets and quotes `objectColumn:['Double."Quote".[]']` and `objectColumn:["Sinlge.'Quote'.[]"]` Column `{ "Double.\"Quote\".[]" : "I was referred", "Sinlge.'Quote'.[]" : "Mee too!" }`
5. Keys containing dots, square brackets, single quotes and double quotes in one json key is not currently supported
There are some special methods that accept `FieldExpression` strings directly, like [whereJsonSupersetOf](/api/query-builder/find-methods.html#wherejsonsupersetof) but you can use `FieldExpressions` anywhere with [ref](/api/objection/#ref). Here's an example:
```js
const { ref } = require('objection');
await Person.query()
.select([
'id',
ref('persons.jsonColumn:details.name').castText().as('name'),
ref('persons.jsonColumn:details.age').castInt().as('age'),
])
.join(
'someTable',
ref('persons.jsonColumn:details.name').castText(),
'=',
ref('someTable.name')
)
.where('age', '>', ref('someTable.ageLimit'));
```
In the above example, we assume `persons` table has a column named `jsonColumn` of type `jsonb` (only works on postgres).
## `type` RelationExpression
Relation expression is a simple DSL for expressing relation trees.
These strings are all valid relation expressions:
- `children`
- `children.movies`
- `[children, pets]`
- `[children.movies, pets]`
- `[children.[movies, pets], pets]`
- `[children.[movies.actors.[children, pets], pets], pets]`
- `[children as kids, pets(filterDogs) as dogs]`
There are two tokens that have special meaning: `*` and `^`. `*` means "all relations recursively" and `^` means "this relation recursively".
For example `children.*` means "relation `children` and all its relations, and all their relations and ...".
::: warning
The \* token must be used with caution or you will end up fetching your entire database.
:::
Expression `parent.^` is equivalent to `parent.parent.parent.parent...` up to the point a relation no longer has results for the `parent` relation. The recursion can be limited to certain depth by giving the depth after the `^` character. For example `parent.^3` is equal to `parent.parent.parent`.
Relations can be aliased using the `as` keyword.
For example the expression `children.[movies.actors.[pets, children], pets]` represents a tree:
```
children
(Person)
|
-----------------
| |
movies pets
(Movie) (Animal)
|
actors
(Person)
|
-----------
| |
pets children
(Animal) (Person)
```
The model classes are shown in parenthesis. When given to `withGraphFetched` method, this expression would fetch all relations as shown in the tree above:
```js
const people = await Person.query().withGraphFetched(
'children.[movies.actors.[pets, children], pets]'
);
// All persons have the given relation tree fetched.
console.log(people[0].children[0].movies[0].actors[0].pets[0].name);
```
Relation expressions can have arguments. Arguments are used to refer to modifier functions (either [global](/api/model/static-properties.html#static-modifiers) or [local](/api/query-builder/other-methods.md#modifiers). Arguments are listed in parenthesis after the relation names like this:
```js
Person.query().withGraphFetched(
`children(arg1, arg2).[movies.actors(arg3), pets]`
);
```
You can spread relation expressions to multiple lines and add whitespace:
```js
Person.query().withGraphFetched(`[
children.[
pets,
movies.actors.[
pets,
children
]
]
]`);
```
Relation expressions can be aliased using `as` keyword:
```js
Person.query().withGraphFetched(`[
children as kids.[
pets(filterDogs) as dogs,
pets(filterCats) as cats,
movies.actors.[
pets,
children as kids
]
]
]`);
```
### RelationExpression object notation
In addition to the string expressions, a more verbose object notation can also be used.
The string expression in the comment is equivalent to the object expression below it:
```js
// `children`
{
children: true;
}
```
```js
// `children.movies`
{
children: {
movies: true;
}
}
```
```js
// `[children, pets]`
{
children: true;
pets: true;
}
```
```js
// `[children.[movies, pets], pets]`
{
children: {
movies: true,
pets: true
}
pets: true
}
```
```js
// `parent.^`
{
parent: {
$recursive: true;
}
}
```
```js
// `parent.^5`
{
parent: {
$recursive: 5;
}
}
```
```js
// `parent.*`
{
parent: {
$allRecursive: true;
}
}
```
```js
// `[children as kids, pets(filterDogs) as dogs]`
{
kids: {
$relation: 'children'
},
dogs: {
$relation: 'pets',
$modify: ['filterDogs']
}
}
```
## `type` TransactionObject
This is nothing more than a knex transaction object. It can be used as a knex query builder, it can be [passed to objection queries](/guide/transactions.html#passing-around-a-transaction-object) and [models can be bound to it](/guide/transactions.html#binding-models-to-a-transaction)
See the section about [transactions](/guide/transactions.html) for more info and examples.
### Instance Methods
#### commit()
```js
const promise = trx.commit();
```
Call this method to commit the transaction. This only needs to be called if you use `transaction.start()` method.
#### rollback()
```js
const promise = trx.rollback(error);
```
Call this method to rollback the transaction. This only needs to be called if you use `transaction.start()` method. You need to pass the error to the method as the only argument.
## `class` ValidationError
```js
const { ValidationError } = require('objection');
throw new ValidationError({ type, message, data });
```
For each `key`, a list of errors is given. Each error contains the default `message` (as returned by the validator), an optional `keyword` string to identify the validation rule which didn't pass and a `param` object which optionally contains more details about the context of the validation error.
If `type` is anything else but `"ModelValidation"`, `data` can be any object that describes the error.
Error of this class is thrown by default if validation of any input fails. By input we mean any data that can come from the outside world, like model instances (or POJOs), relation expressions object graphs etc.
You can replace this error by overriding [Model.createValidationError()](/api/model/static-methods.html#static-createvalidationerror) method.
See the [error handling recipe](/recipes/error-handling.html) for more info.
| Property | Type | Description |
| ---------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| statusCode | number | HTTP status code for interop with express error handlers and other libraries that search for status code from errors. |
| type | string | One of "ModelValidation", "RelationExpression", "UnallowedRelation" and "InvalidGraph". This can be any string for your own custom errors. The listed values are used internally by objection. |
| data | object | The content of this property is documented below for "ModelValidation" errors. For other types, this can be any data. |
If `type` is `"ModelValidation"` then `data` object should follow this pattern:
```js
{
key1: [{
message: '...',
keyword: 'required',
params: null
}, {
message: '...',
keyword: '...',
params: {
...
}
}, ...],
key2: [{
message: '...',
keyword: 'minLength',
params: {
limit: 1,
...
}
}, ...],
...
}
```
## `class` NotFoundError
```js
const { NotFoundError } = require('objection');
throw new NotFoundError(data);
```
Error of this class is thrown by default by [throwIfNotFound()](/api/query-builder/other-methods.html#throwifnotfound)
You can replace this error by overriding [Model.createNotFoundError()](/api/model/static-methods.html#static-createnotfounderror) method.
See the [error handling recipe](/recipes/error-handling.html) for more info.
## `class` Relation
`Relation` is a parsed and normalized instance of a [RelationMapping](/api/types/#type-relationmapping). `Relation`s can be accessed using the [getRelations](/api/model/static-methods.html#static-getrelations) method.
`Relation` holds a [RelationProperty](/api/types/#class-relationproperty) instance for each property that is used to create the relationship between two tables.
`Relation` is actually a base class for all relation types `BelongsToOneRelation`, `HasManyRelation` etc. You can use `instanceof` to determine the type of the relations (see the example on the right). Note that `HasOneRelation` is a subclass of `HasManyRelation` and `HasOneThroughRelation` is a subclass of `ManyToManyRelation`. Arrange your `instanceof` checks accordingly.
| Property | Type | Description |
| -------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | Name of the relation. For example `pets` or `children`. |
| ownerModelClass | function | The model class that has defined the relation. |
| relatedModelClass | function | The model class of the related objects. |
| ownerProp | [RelationProperty](/api/types/#class-relationproperty) | The relation property in the `ownerModelClass`. |
| relatedProp | [RelationProperty](/api/types/#class-relationproperty) | The relation property in the `relatedModelClass`. |
| joinModelClass | function | The model class representing the join table. This class is automatically generated by Objection if none is provided in the `join.through.modelClass` setting of the relation mapping, see [RelationThrough](/api/types/#type-relationthrough). |
| joinTable | string | The name of the join table (only for `ManyToMany` and `HasOneThrough` relations). |
| joinTableOwnerProp | [RelationProperty](/api/types/#class-relationproperty) | The join table property pointing to `ownerProp` (only for `ManyToMany` and `HasOneThrough` relations). |
| joinTableRelatedProp | [RelationProperty](/api/types/#class-relationproperty) | The join table property pointing to `relatedProp` (only for `ManyToMany` and `HasOneThrough` relations). |
Note that `Relation` instances are actually instances of the relation classes used in `relationMappings`. For example:
```js
class Person extends Model {
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'persons.id',
to: 'animals.ownerId',
},
},
};
}
}
const relations = Person.getRelations();
console.log(relations.pets instanceof Model.HasManyRelation); // --> true
console.log(relations.pets.name); // --> pets
console.log(relations.pets.ownerProp.cols); // --> ['id']
console.log(relations.pets.relatedProp.cols); // --> ['ownerId']
```
## `class` RelationProperty
Represents a property that is used to create relationship between two tables. A single `RelationProperty` instance can represent
composite key. In addition to a table column, A `RelationProperty` can represent a nested field inside a column (for example a jsonb column).
### Properties
| Property | Type | Description |
| ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| size | number | The number of columns. In case of composite key, this is greater than one. |
| modelClass | function | The model class that owns the property. |
| props | string[] | The column names converted to "external" format. For example if `modelClass` defines a snake_case to camelCase conversion, these names are in camelCase. Note that a `RelationProperty` may actually point to a sub-properties of the columns in case they are of json or some other non-scalar type. This array always contains only the converted column names. Use `getProp(obj, idx)` method to get the actual value from an object. |
| cols | string[] | The column names in the database format. For example if `modelClass` defines a snake_case to camelCase conversion, these names are in snake_case. Note that a `RelationProperty` may actually point to a sub-properties of the columns in case they are of json or some other non-scalar type. This array always contains only the column names. |
### Methods
#### getProp()
```js
const value = property.getProp(obj, index);
```
Gets this property's index:th value from an object. For example if the property represents a composite key `[a, b.d.e, c]`
and obj is `{a: 1, b: {d: {e: 2}}, c: 3}` then `getProp(obj, 1)` would return `2`.
#### setProp()
```js
property.setProp(obj, index, value);
```
Sets this property's index:th value in an object. For example if the property represents a composite key `[a, b.d.e, c]`
and obj is `{a: 1, b: {d: {e: 2}}, c: 3}` then `setProp(obj, 1, 'foo')` would mutate `obj` into `{a: 1, b: {d: {e: 'foo'}}, c: 3}`.
#### fullCol()
```js
const col = property.fullCol(builder, index);
```
Returns the property's index:th column name with the correct table reference. Something like `"Table.column"`.
The first argument must be an objection [QueryBuilder](/api/types/#querybuilder) instance.
#### ref()
```js
const ref = property.ref(builder, index);
```
Allows you to do things like this:
```js
const builder = Person.query();
const ref = property.ref(builder, 0);
builder.where(ref, '>', 10);
```
Returns a [ReferenceBuilder](/api/objection/#ref) instance that points to the index:th column.
#### patch()
```js
property.patch(patchObj, index, value);
```
Allows you to do things like this:
```js
const builder = Person.query();
const patch = {};
property.patch(patch, 0, 'foo');
builder.patch(patch);
```
Appends an update operation for the index:th column into `patchObj` object.
## `class` ReferenceBuilder
An instance of this is returned from the [ref](/api/objection/#ref) helper function.
### Instance Methods
#### castText()
Cast reference to sql type `text`.
#### castInt()
Cast reference to sql type `integer`.
#### castBigInt()
Cast reference to sql type `bigint`.
#### castFloat()
Cast reference to sql type `float`.
#### castDecimal()
Cast reference to sql type `decimal`.
#### castReal()
Cast reference to sql type `real`.
#### castBool()
Cast reference to sql type `boolean`.
#### castTo()
Give custom type to which referenced value is cast to.
`.castTo('mytype') --> CAST(?? as mytype)`
#### castJson()
In addition to other casts wrap reference to_jsonb() function so that final value
reference will be json type.
#### as()
Gives an alias for the reference `.select(ref('age').as('yougness'))`
#### from()
Specifies that table of the reference.
See [this](/api/objection/#ref) for some examples.
## `class` ValueBuilder
An instance of this is returned from the [val](/api/objection/#val) helper function. If an object
is given as a value, it is cast to json by default.
### Instance Methods
#### castText()
Cast to sql type `text`.
#### castInt()
Cast to sql type `integer`.
#### castBigInt()
Cast to sql type `bigint`.
#### castFloat()
Cast to sql type `float`.
#### castDecimal()
Cast to sql type `decimal`.
#### castReal()
Cast to sql type `real`.
#### castBool()
Cast to sql type `boolean`.
#### castTo()
Give custom type to which referenced value is cast to.
`.castTo('mytype') --> CAST(?? as mytype)`
#### castJson()
Converts the value to json (jsonb in case of postgresql). The default
cast type for object values.
#### asArray()
Converts the value to an array.
`val([1, 2, 3]).asArray() --> ARRAY[?, ?, ?]`
Can be used in conjuction with `castTo`.
`val([1, 2, 3]).asArray().castTo('real[]') -> CAST(ARRAY[?, ?, ?] AS real[])`
#### as()
Gives an alias for the reference `.select(ref('age').as('yougness'))`
## `class` RawBuilder
An instance of this is returned from the [raw](/api/objection/#raw) helper function.
### Instance Methods
#### as()
Gives an alias for the raw expression `.select(raw('concat(foo, bar)').as('fooBar'))`.
You should use this instead of inserting the alias to the SQL to give objection more information about the query. Some edge cases, like using `raw` in `select` inside a `withGraphJoined` modifier won't work unless you use this method.
## `class` FunctionBuilder
An instance of this is returned from the [fn](/api/objection/#fn) helper function.
### Instance Methods
#### as()
Gives an alias for the raw expression `.select(fn('concat', 'foo', 'bar').as('fooBar'))`.
You should use this instead of inserting the alias to the SQL to give objection more information about the query. Some edge cases, like using `fn` in `select` inside a `withGraphJoined` modifier won't work unless you use this method.
## `class` Validator
```js
const { Validator } = require('objection');
```
Abstract class from which model validators must be inherited. See the example for explanation. Also check out the [createValidator](/api/model/static-methods.html#static-createvalidator) method.
#### Examples
```js
const { Validator } = require('objection');
class MyCustomValidator extends Validator {
validate(args) {
// The model instance. May be empty at this point.
const model = args.model;
// The properties to validate. After validation these values will
// be merged into `model` by objection.
const json = args.json;
// `ModelOptions` object. If your custom validator sets default
// values, you need to check the `opt.patch` boolean. If it is true
// we are validating a patch object and the defaults should not be set.
const opt = args.options;
// A context object shared between the validation methods. A new
// object is created for each validation operation. You can store
// any data here.
const ctx = args.ctx;
// Do your validation here and throw any exception if the
// validation fails.
doSomeValidationAndThrowIfFails(json);
// You need to return the (possibly modified) json.
return json;
}
beforeValidate(args) {
// Takes the same arguments as `validate`. Usually there is no need
// to override this.
return super.beforeValidate(args);
}
afterValidate(args) {
// Takes the same arguments as `validate`. Usually there is no need
// to override this.
return super.afterValidate(args);
}
}
const { Model } = require('objection');
// Override the `createValidator` method of a `Model` to use the
// custom validator.
class BaseModel extends Model {
static createValidator() {
return new MyCustomValidator();
}
}
```
## `class` AjvValidator
```js
const { AjvValidator } = require('objection');
```
The default [Ajv](https://github.com/epoberezkin/ajv) based json schema
validator. You can override the [createValidator](/api/model/static-methods.html#static-createvalidator)
method of [Model](/api/model/) like in the example to modify the validator.
#### Examples
```js
const { Model, AjvValidator } = require('objection');
class BaseModel extends Model {
static createValidator() {
return new AjvValidator({
onCreateAjv: (ajv) => {
// Here you can modify the `Ajv` instance.
},
options: {
allErrors: true,
validateSchema: false,
ownProperties: true,
v5: true,
},
});
}
}
```
================================================
FILE: doc/guide/contributing.md
================================================
# Contribution guide
## Issues
You can use [github issues](https://github.com/Vincit/objection.js/issues) to request features and file bug reports. An issue is also a good place to ask questions. We are happy to help out if you have reached a dead end, but please try to solve the problem yourself first. The [gitter chat](https://gitter.im/Vincit/objection.js) is also a good place to ask for help.
When creating an issue there are couple of things you need to remember:
1. **Update to the latest version of objection if possible and see if the problem remains.**
If updating is not an option you can still request critical bug fixes for older versions.
2. **Describe your problem.**
Answer the following questions: Which objection version are you using? What are you doing? What code are you running? What is happening? What are you expecting to happen instead? If you provide code examples (please do!), **use the actual code you are running**. People often leave out details or use made up examples because they think they are only leaving out irrelevant stuff. If you do that, you have already made an assumption about what the problem is and it's usually something else. Also provide all possible stack traces and error messages.
3. **If possible, provide an actual reproduction**
The fastest way to get your bug fixed or problem solved is to create a simple standalone app or a test case that demonstrates your problem. There's a file called [reproduction-template.js](https://github.com/Vincit/objection.js/blob/main/reproduction-template.js) you can use as a starting point for your reproduction.
Please bear in mind that objection has thousands of tests and if you run into a problem, say with `insert` method, it doesn't mean that `insert` is completely broken, but some small part of it you are using is. That's why enough context is necessary. It's not enough to say, "insert fails". You need to provide the code that fails and usually the models that are used too. And let's say this again: **don't provide made up code examples!** When you do, you only write the parts you think are relevant and usually leave out the useful information. Use the actual code that you have tested to fail.
## Pull requests
If you have found a bug or want to add a feature, pull requests are always welcome! It's better to create an issue first to open a discussion if the feature is something that should be added to objection. In case of bugfixes it's also a good idea to open an issue indicating that you are working on a fix.
For a pull request to get merged it needs to have the following things:
1. **A good description of what the PR fixes or adds. You can just add a link to the corresponding issue.**
2. **Tests that verify the fix/feature.** It's possible to create a PR without tests and ask for someone else to write them but in that case it may take a long time or forever until someone finds time to do it. _Untested code will never get merged!_
3. **For features you also need to write documentation.** See the [development setup](/guide/contributing.html#development-setup) section for instructions on how to write documentation.
## Development setup
1. **Fork objection in github**
2. **Clone objection**
```bash
git clone git@github.com:/objection.js.git objection
```
3. **Run `npm install` at the root of the repo**
4. **Run `docker compose up` at the root of the repo**
- If you have local databases running, shut them down or port binding will conflict.
5. **Create test users and databases by running `node setup-test-db` at the root of the repo**
6. **Run `npm test` in objection's root to see if everything works.**
7. **Run `npm run docs:dev` and goto http://localhost:8080 to see the generated documentation site when you change the markdown files in the `doc` folder.**
You can run the tests on a subset of databases by setting the `DATABASES` env variable
```bash
# Only run tests on sqlite. No need for docker compose.
DATABASES=sqlite3 npm test
```
Code and tests need to be written in ES2015 subset supported by node 8.0.0. The best way to make sure of this is to develop with the correct node version. [nvm](https://github.com/creationix/nvm) is a great tool for swapping between node versions.
[prettier](https://prettier.io/) is used to format the code. Remember to run `npm run prettier` before committing code.
================================================
FILE: doc/guide/documents.md
================================================
# Documents
Objection.js makes it easy to store non-flat documents as table rows. All properties of a model that are marked as objects or arrays in the model's [jsonSchema](/api/model/static-properties.html#static-jsonschema) are automatically converted to JSON strings in the database and back to objects when read from the database. The database columns for the object properties can be normal text columns. Postgresql has the `json` and `jsonb` data types that can be used instead for better performance and possibility to [query the documents](https://www.postgresql.org/docs/9.4/static/functions-json.html). If you don't want to use [jsonSchema](/api/model/static-properties.html#static-jsonschema) you can mark properties as objects using the [jsonAttributes](/api/model/static-properties.html#static-jsonattributes)
Model property.
The `address` property of the Person model is defined as an object in the [Person.jsonSchema](/api/model/static-properties.html#static-jsonschema):
```js
const jennifer = await Person.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence',
age: 24,
address: {
street: 'Somestreet 10',
zipCode: '123456',
city: 'Tampere'
}
});
const jenniferFromDb = await Person.query().findById(jennifer.id);
console.log(jennifer.address.city); // --> Tampere
console.log(jenniferFromDb.address.city); // --> Tampere
```
================================================
FILE: doc/guide/getting-started.md
================================================
# Getting started
To use objection.js all you need to do is [initialize knex](https://knexjs.org/guide/#node-js) and give the created knex instance to objection.js using [Model.knex(knex)](/api/model/static-methods.html#static-knex). Doing this installs the knex instance globally for all models (even the ones that have not been created yet). If you need to use multiple databases check out our [multi-tenancy recipe](/recipes/multitenancy-using-multiple-databases.html).
The next step is to create some migrations and models and start using objection.js. The best way to get started is to check out one of our example projects:
- [The minimal example](https://github.com/Vincit/objection.js/tree/main/examples/minimal) contains the bare minimum for you to start testing out things with objection.
```bash
git clone git@github.com:Vincit/objection.js.git objection
cd objection/examples/minimal
npm install
npm start
```
- [The koa example project](https://github.com/Vincit/objection.js/tree/main/examples/koa) is a simple [koa](https://koajs.com) server. The `client.js` file contains a bunch of http requests for you to start playing with the REST API.
```bash
git clone git@github.com:Vincit/objection.js.git objection
cd objection/examples/koa
npm install
npm start
```
We also have a [typescript version](https://github.com/Vincit/objection.js/tree/main/examples/koa-ts) of the example.
Also check out our [API reference](/api/query-builder/) and [recipe book](/recipes/raw-queries.html).
If installing the example project seems like too much work, here is a simple standalone example. Just copy this into a file and run it:
```js
// run the following command to install:
// npm install objection knex sqlite3
const { Model } = require('objection');
const Knex = require('knex');
// Initialize knex.
const knex = Knex({
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: 'example.db'
}
});
// Give the knex instance to objection.
Model.knex(knex);
// Person model.
class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'persons.id',
to: 'persons.parentId'
}
}
};
}
}
async function createSchema() {
if (await knex.schema.hasTable('persons')) {
return;
}
// Create database schema. You should use knex migration files
// to do this. We create it here for simplicity.
await knex.schema.createTable('persons', table => {
table.increments('id').primary();
table.integer('parentId').references('persons.id');
table.string('firstName');
});
}
async function main() {
// Create some people.
const sylvester = await Person.query().insertGraph({
firstName: 'Sylvester',
children: [
{
firstName: 'Sage'
},
{
firstName: 'Sophia'
}
]
});
console.log('created:', sylvester);
// Fetch all people named Sylvester and sort them by id.
// Load `children` relation eagerly.
const sylvesters = await Person.query()
.where('firstName', 'Sylvester')
.withGraphFetched('children')
.orderBy('id');
console.log('sylvesters:', sylvesters);
}
createSchema()
.then(() => main())
.then(() => knex.destroy())
.catch(err => {
console.error(err);
return knex.destroy();
});
```
================================================
FILE: doc/guide/hooks.md
================================================
# Hooks
Hooks are model methods that allow you too hook into different stages of objection queries. There are three different kinds of hooks
1. [instance query hooks](#instance-query-hooks)
2. [static query hooks](#static-query-hooks)
3. [model data lifecycle hooks](#model-data-lifecycle-hooks)
Each hook type serve a different purpose. We'll go through the different types in the following chapters.
## Instance query hooks
These hooks are executed in different stages of each query type (find, update, insert, delete) for **model instances**. The following hooks exist:
- [\$beforeInsert](/api/model/instance-methods.html#beforeinsert)
- [\$afterInsert](/api/model/instance-methods.html#afterinsert)
- [\$beforeUpdate](/api/model/instance-methods.html#beforeupdate)
- [\$afterUpdate](/api/model/instance-methods.html#afterupdate)
- [\$beforeDelete](/api/model/instance-methods.html#beforedelete)
- [\$afterDelete](/api/model/instance-methods.html#afterdelete)
- [\$afterFind](/api/model/instance-methods.html#afterfind)
All of these are instance methods on a model. Therefore you can access the model instance's properties through `this`.
```js
class Person extends Model {
$beforeInsert(context) {
this.createdAt = new Date().toISOString();
}
}
```
Instance hooks can be async. It allows you, among other things, to execute queries. If you do fire off queries from instance hooks, it's important to always make sure they are fired inside any existing transaction. You can access the transaction through the `context`:
```js
class Person extends Model {
async $beforeInsert(context) {
// Remember to attach any queries to the existing transaction. This works
// even if there's no transaction, so you should just always pass the
// `context.transaction` to queries.
await SomeModel.query(context.transaction).patch({ whatever: false });
}
}
```
::: warning
Warning!
Running queries and other async operations inside instance hooks can lead to very unpredictable performance because the instance hooks are run for each model instance. For example, consider a query that returns 1000 items. If you have an `$afterFind` hook that executes a query, you'd be executing 1000 queries! The [static query hooks](#static-query-hooks) are better suited for that kind of thing.
:::
Because the instance hooks are executed for model instances, they cannot be executed when there are no model instances available. Not all hooks can be implemented and some hooks are only executed in certain situations. There's no `$beforeFind` hook, because we don't have any model instances before the query is executed. There's nothing to call the hook for. `$beforeDelete` and `$afterDelete` are also a bit strange for this reason. They are **only** executed when you run an instance query like this:
```js
const arnold = await Person.query().findOne({ name: 'Arnold' });
// This will execute the delete hooks because here we have a model
// instance `arnold`.
await arnold.$query().delete();
// This will NOT execute the delete hooks because we don't have model
// instance for Arnold.
await Person.query()
.delete()
.where('name', 'Arnold');
```
Objection could fetch the items from the database and call the hooks for those model instances, but that would lead to unpredicable performance, and that's something objection tries to avoid. If you need to do something like this, you can do it using [the static query hooks](#static-query-hooks).
Another thing that may cause confusion with the instance hooks is that the insert and update hooks are always executed for the **input** items:
```js
class Person extends Model {
$beforeUpdate() {
console.log(this.firstName);
}
}
// This will print 'Jennifer'
await Person.query()
.patch({ firstName: 'Jennifer' })
.where('id', 1);
const arnold = await Person.query().findOne({ name: 'Arnold' });
// This will also print 'Jennifer' and NOT 'Arnold'.
await arnold.$query().patch({ firstName: 'Jennifer' });
```
Each instance hook is passed the [context](/api/query-builder/other-methods.html#context) object as the only argument. The context contains whatever data you have installed using either the [context](/api/query-builder/other-methods.html#context) or the [mergeContext](/api/query-builder/other-methods.html#mergecontext) method. In addition to that, it always contains the `transaction` property that holds the parent query's transaction.
```js
class Person extends Model {
$beforeUpdate(context) {
console.log(context.hello);
}
}
// This will print 'hello!'
await Person.query()
.mergeContext({ hello: 'hello!' })
.patch({ firstName: 'Jennifer' })
.where('id', 1);
```
::: tip
Note!
If you use 3rd party plugins, it's very usual that they use the hooks to perform their magic. If any plugins are used, it's a good idea to always call the `super` implementation if you implement any of the hooks:
```js
class Person extends SomePluginP(Model) {
async $beforeInsert(context) {
await super.$beforeInsert(context);
...
}
async $afterFind(context) {
const result = await super.$afterFind(context);
...
return result;
}
}
```
:::
## Static query hooks
Static hooks are executed in different stages of each query type (find, update, insert, delete). Unlike the [instance query hooks](#instance-query-hooks), static hooks are executed once per query. Static hooks are always executed and there are no corner cases like the `$beforeDelete`/`$afterDelete` issue with instance hooks. The following hooks are available:
- [beforeInsert](/api/model/static-methods.html#static-beforeinsert)
- [afterInsert](/api/model/static-methods.html#static-afterinsert)
- [beforeUpdate](/api/model/static-methods.html#static-beforeupdate)
- [afterUpdate](/api/model/static-methods.html#static-afterupdate)
- [beforeDelete](/api/model/static-methods.html#static-beforedelete)
- [afterDelete](/api/model/static-methods.html#static-afterdelete)
- [beforeFind](/api/model/static-methods.html#static-beforefind)
- [afterFind](/api/model/static-methods.html#static-afterfind)
The static hooks are passed one argument of type [StaticHookArguments](/api/types/#type-statichookarguments). The most interesting of all properties of that object is the `asFindQuery` parameter that allows you to fetch the items that were/would be affected by the query. For example the following example would fetch the identifiers of all people that would get deleted by the query being executed:
```js
class Person extends Model {
static async beforeDelete({ asFindQuery }) {
// This query will automatically be executed in the same transaction
// as the query we are hooking into.
const idsOfItemsToBeDeleted = await asFindQuery().select('id');
await doSomethingWithIds(idsOfItemsToBeDeleted);
}
}
```
The beauty of `asFindQuery` and the static hooks is that they work in all cases, no matter how complex your query is.
::: warning
Warning!
Even though the static hooks are only executed once per query, and `asFindQuery` only executes one additional query, it can still lead to bad performance. For example, consider this query that deletes all items in a table:
```js
await Person.query().delete();
```
If `Person` has a hook that uses `asFindQuery` to fetch all items that will get deleted, the hook ends up fetching the whole table! Even if you simply select the `id`, the amount of data can be huge.
Be careful with `asFindQuery`!
:::
Another interesting argument is the `cancelQuery` function. It allows you to cancel the query being executed. Used in conjunction with the `asFindQuery`, you can do stuff like this:
```js
class Person extends Model {
static async beforeDelete({ asFindQuery, cancelQuery }) {
// Even though `asFindQuery` returns a `select` query by default, you
// can turn it into an update, insert, delete or whatever you want.
const numAffectedItems = await asFindQuery().patch({ deleted: true });
// Cancel the query being executed with `numAffectedItems`
// as the return value. No need to `await` this one.
cancelQuery(numAffectedItems);
}
}
```
The example above turns all delete queries into updates that set the `deleted` property to `true`. These two lines implement a simple version of a "soft delete" feature.
You can also access the model instances for which the query is started, the input model instances and the relation. The next example should explain what each of them mean:
```js
class Person extends Model {
static beforeUpdate({ items, inputItems, relation }) {
console.log('items: ', items);
console.log('inputItems:', inputItems);
console.log('relation: ', relation ? relation.name : 'none');
}
static afterInsert({ items, inputItems, relation }) {
console.log('items: ', items);
console.log('inputItems:', inputItems);
console.log('relation: ', relation ? relation.name : 'none');
}
}
const jennifer = await Person.query().insert({ firstName: 'Jennifer' });
// items: []
// inputItems: [{ firstName: 'Jennifer' }]
// relation: none
await jennifer.$query().patch({ lastName: 'Aniston' });
// items: [{ id: 1, firstName: 'Jennifer' }]
// inputItems: [{ lastName: 'Aniston' }]
// relation: none
await jennifer.$relatedQuery('movies').insert({ name: "We're the Millers" });
// items: [{ id: 1, firstName: 'Jennifer' }]
// inputItems: [{ name: "We're the Millers" }]
// relation: movies
await Person.relatedQuery('pets')
.for([jennifer, brad])
.insert([{ name: 'Cato' }, { name: 'Doggo' }]);
// items: [{ id: 1, firstName: 'Jennifer' }, { id: 2, firstName: 'Brad' }]
// inputItems: [{ name: 'Cato' }, { name: 'Doggo' }]
// relation: pets
```
In `afterDelete`, `afterUpdate`, `afterInsert` and `afterFind` hooks, the `result` property of the input argument contains the result of the query. You can change the result by returning a non-undefined value from the hook:
```js
class Person extends Model {
static afterFind({ result }) {
return {
result,
success: true
};
}
}
const result = await Person.query();
console.log(result.success);
console.log(result.result.length);
```
::: tip
Note!
If you use 3rd party plugins, it's very usual that they use the hooks to perform their magic. If any plugins are used, it's a good idea to always call the `super` implementation if you implement any of the hooks:
```js
class Person extends SomePlugin(Model) {
static async beforeInsert(args) {
await super.beforeInsert(args);
const { asFindQuery, items } = args;
...
}
static async afterFind(args) {
const result = await super.afterFind(args);
const { asFindQuery, items } = args;
...
return result;
}
}
```
:::
## Model data lifecycle hooks
For the purposes of this explanation, let’s define three data layouts:
1. `database`: The data layout returned by the database.
2. `internal`: The data layout of a model instance.
3. `external`: The data layout after calling model.toJSON().
Whenever data is converted from one layout to another a data lifecycle hook is called:
1. `database` -> [\$parseDatabaseJson](/api/model/instance-methods.html#parsedatabasejson) -> `internal`
2. `internal` -> [\$formatDatabaseJson](/api/model/instance-methods.html#formatdatabasejson) -> `database`
3. `external` -> [\$parseJson](/api/model/instance-methods.html#parsejson) -> `internal`
4. `internal` -> [\$formatJson](/api/model/instance-methods.html#formatjson) -> `external`
So for example when the results of a query are read from the database the data goes through the [\$parseDatabaseJson](/api/model/instance-methods.html#parsedatabasejson) method. When data is written to database it goes through the [\$formatDatabaseJson](/api/model/instance-methods.html#formatdatabasejson) method.
Similarly when you give data for a query (for example [`query().insert(req.body)`](/api/query-builder/mutate-methods.html#insert)) or create a model explicitly using [`Model.fromJson(obj)`](/api/model/static-methods.html#static-fromjson) the [\$parseJson](/api/model/instance-methods.html#parsejson) method is invoked. When you call [`model.toJSON()`](/api/model/instance-methods.html#tojson) or [`model.$toJson()`](/api/model/instance-methods.html#tojson) the [\$formatJson](/api/model/instance-methods.html#formatjson) is called.
Note: Most libraries like [express](https://expressjs.com/en/index.html) and [koa](https://koajs.com/) automatically call the [toJSON](/api/model/instance-methods.html#tojson) method when you pass the model instance to methods like `response.json(model)`. You rarely need to call [toJSON()](/api/model/instance-methods.html#tojson) or [\$toJson()](/api/model/instance-methods.html#tojson) explicitly. This is because `JSON.stringify` calls the `toJSON` method and basically all libraries that create JSON strings use `JSON.stringify` under the hood.
Data lifecycle hooks are always synchronous. They cannot be `async` or return promises.
Fore example, here's a hook that converts date strings to `moment` instances when read from the database, and back to date strings when written to database:
```js
// We use a hardcoded list here. Note that you can store these fields
// for example as a static array in the model and access it through
// this.constructor in the hooks, or read the values from `jsonSchema`
// if you use it.
const dateColumns = ['dateOfBirth', 'dateOfDeath'];
class Person extends Model {
$parseDatabaseJson(json) {
// Remember to call the super implementation.
json = super.$parseDatabaseJson(json);
for (const dateColumn of dateColumns) {
// Remember to always check if the json object has the particular
// field. It may not exist if the user has used `select('id')`
// or any other select that excludes the field.
if (json[dateColumn] !== undefined) {
json[dateColumn] = moment(json[dateColumn]);
}
}
return json;
}
$formatDatabaseJson(json) {
for (const dateColumn of dateColumns) {
// Remember to always check if the json object has the particular field.
// It may not exist if the user updates or inserts a partial object.
if (json[dateColumn] !== undefined && moment.isMoment(json[dateColumn])) {
json[dateColumn] = json[dateColumn].toISOString();
}
}
// Remember to call the super implementation.
return super.$formatDatabaseJson(json);
}
}
```
::: warning
Be sure to read the special requirements for the data lifecycle hooks in their documentation.
[\$parseDatabaseJson](/api/model/instance-methods.html#parsedatabasejson)
[\$formatDatabaseJson](/api/model/instance-methods.html#formatdatabasejson)
[\$parseJson](/api/model/instance-methods.html#parsejson)
[\$formatJson](/api/model/instance-methods.html#formatjson)
:::
================================================
FILE: doc/guide/installation.md
================================================
# Installation
Objection.js can be installed using `npm` or `yarn`. Objection uses [knex](https://knexjs.org/) as its database access layer, so you also need to install it.
```bash
npm install objection knex
yarn add objection knex
```
You also need to install one of the following depending on the database you want to use:
```bash
npm install pg
npm install sqlite3
npm install mysql
npm install mysql2
```
You can use the `next` tag to install an alpha/beta/RC version:
```bash
npm install objection@next
```
================================================
FILE: doc/guide/models.md
================================================
# Models
A [Model](/api/model/) subclass represents a database table and instances of that class represent table rows. Models are created by inheriting from the [Model](/api/model/) class. A [Model](/api/model/) class can define [relationships](/guide/relations.html) (aka. relations, associations) to other models using the static [relationMappings](/api/model/static-properties.html#static-relationmappings) property.
Models can optionally define a [jsonSchema](/api/model/static-properties.html#static-jsonschema) object that is used for input validation. Every time a [Model](/api/model/) instance is created, it is validated against the [jsonSchema](/api/model/static-properties.html#static-jsonschema). Note that [Model](/api/model/) instances are implicitly created whenever you call [insert](/api/query-builder/mutate-methods.html#insert), [insertGraph](/api/query-builder/mutate-methods.html#insertgraph), [patch](/api/query-builder/mutate-methods.html#patch) or any other method that takes in model properties (no validation is done when reading from the database).
Each model must have an identifier column. The identifier column name can be set using the [idColumn](/api/model/static-properties.html#static-idcolumn) property. [idColumn](/api/model/static-properties.html#static-idcolumn) defaults to `"id"`. If your table's identifier is something else, you need to set [idColumn](/api/model/static-properties.html#static-idcolumn). A composite id can be set by giving an array of column names. Composite keys are first class citizens in objection.
In objection, all configuration is done through [Model](/api/model/) classes and there is no global configuration or state. There is no "objection instance". This allows you to create isolated components and for example to use multiple different databases with different configurations in one app. Most of the time you want the same configuration for all models and a good pattern is to create a `BaseModel` class and inherit all your models from that. You can then add all shared configuration to `BaseModel`. See the static properties in [API Reference / Model](/api/model/static-properties.html#static-tablename) section for all available configuration options.
Note that in addition to `idColumn`, you don't define properties, indexes or anything else related to database schema in the model. In objection, database schema is considered a separate concern and should be handled using [migrations](https://knexjs.org/guide/migrations.html). The reasoning is that every non-trivial project will need migrations anyway. Managing the schema in two places (model and migrations) only makes things more complex in the long run.
## Examples
A working model with minimal amount of code:
```js
const { Model } = require('objection');
class MinimalModel extends Model {
static get tableName() {
return 'someTableName';
}
}
module.exports = MinimalModel;
```
Model with custom methods, json schema validation and relations. This model is used in the examples:
```js
const { Model } = require('objection');
class Person extends Model {
// Table name is the only required property.
static get tableName() {
return 'persons';
}
// Each model must have a column (or a set of columns) that uniquely
// identifies the rows. The column(s) can be specified using the `idColumn`
// property. `idColumn` returns `id` by default and doesn't need to be
// specified unless the model's primary key is something else.
static get idColumn() {
return 'id';
}
// Methods can be defined for model classes just as you would for
// any JavaScript class. If you want to include the result of these
// methods in the output json, see `virtualAttributes`.
fullName() {
return this.firstName + ' ' + this.lastName;
}
// Optional JSON schema. This is not the database schema!
// No tables or columns are generated based on this. This is only
// used for input validation. Whenever a model instance is created
// either explicitly or implicitly it is checked against this schema.
// See https://json-schema.org/ for more info.
static get jsonSchema() {
return {
type: 'object',
required: ['firstName', 'lastName'],
properties: {
id: { type: 'integer' },
parentId: { type: ['integer', 'null'] },
firstName: { type: 'string', minLength: 1, maxLength: 255 },
lastName: { type: 'string', minLength: 1, maxLength: 255 },
age: { type: 'number' },
// Properties defined as objects or arrays are
// automatically converted to JSON strings when
// writing to database and back to objects and arrays
// when reading from database. To override this
// behaviour, you can override the
// Model.jsonAttributes property.
address: {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
zipCode: { type: 'string' }
}
}
}
};
}
// This object defines the relations to other models.
static get relationMappings() {
// Importing models here is one way to avoid require loops.
const Animal = require('./Animal');
const Movie = require('./Movie');
return {
pets: {
relation: Model.HasManyRelation,
// The related model. This can be either a Model
// subclass constructor or an absolute file path
// to a module that exports one. We use a model
// subclass constructor `Animal` here.
modelClass: Animal,
join: {
from: 'persons.id',
to: 'animals.ownerId'
}
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'persons.id',
// ManyToMany relation needs the `through` object
// to describe the join table.
through: {
// If you have a model class for the join table
// you need to specify it like this:
// modelClass: PersonMovie,
from: 'persons_movies.personId',
to: 'persons_movies.movieId'
},
to: 'movies.id'
}
},
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'persons.id',
to: 'persons.parentId'
}
},
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'persons.parentId',
to: 'persons.id'
}
}
};
}
}
```
================================================
FILE: doc/guide/plugins.md
================================================
# Plugins
A curated list of plugins and modules for objection. Only plugins that follow [the best practices](/guide/plugins.html#plugin-development-best-practices) are accepted on this list. Other modules like plugins for other frameworks and things that cannot be implemented following the best practices are an exception to this rule. If you are a developer of or otherwise know of a good plugin/module for objection, please create a pull request or an issue to get it added to this list.
## 3rd party plugins
- [objection-authorize](https://github.com/JaneJeon/objection-authorize) - integrate access control into Objection queries
- [objection-dynamic-finder](https://github.com/snlamm/objection-dynamic-finder) - dynamic finders for your models
- [objection-guid](https://github.com/seegno/objection-guid) - automatic guid for your models
- [objection-password](https://github.com/scoutforpets/objection-password) - automatic password hashing for your models
- [objection-soft-delete](https://github.com/griffinpp/objection-soft-delete) - Soft delete functionality with minimal configuration
- [objection-unique](https://github.com/seegno/objection-unique) - Unique validation for your models
- [objection-visibility](https://github.com/oscaroox/objection-visibility) - whitelist/blacklist your model properties
## Other 3rd party modules
- [objection-filter](https://github.com/tandg-digital/objection-filter) - API filtering on data and related models
- [objection-graphql](https://github.com/vincit/objection-graphql) - Automatically generates rich graphql schema for objection models
- [objectionjs-graphql](https://www.npmjs.com/package/objectionjs-graphql) - Automatically generates GraphQl schema for objection models compatible with Objection 3.x (Graph fetch support)
## Plugin development best practices
When possible, objection.js plugins should be implemented as [class mixins](http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/). A mixin is simply a function that takes a class as an argument and returns a subclass. Plugins should not modify [objection.Model](/api/model/), [objection.QueryBuilder](/api/query-builder/) or any other global variables directly. See the [example plugin](https://github.com/Vincit/objection.js/tree/main/examples/plugin) for more info. There is also [another example](https://github.com/Vincit/objection.js/tree/main/examples/plugin-with-options) that should be followed if your plugin takes options or configuration parameters.
Mixin is just a function that takes a class and returns an extended subclass.
```js
function SomeMixin(Model) {
// The returned class should have no name. That way
// the superclass's name gets inherited.
return class extends Model {
// Your modifications.
};
}
```
Mixins can be then applied like this:
```js
class Person extends SomeMixin(Model) {}
```
This **doesn't** work since mixins never modify the input:
```js
// This does absolutely nothing.
SomeMixin(Model);
class Person extends Model {}
```
Multiple mixins:
```js
class Person extends SomeMixin(SomeOtherMixin(Model)) {}
```
There are a couple of helpers in objection main module for applying multiple mixins.
```js
const { mixin, Model } = require('objection');
class Person extends mixin(Model, [
SomeMixin,
SomeOtherMixin,
EvenMoreMixins,
LolSoManyMixins,
ImAMixinWithOptions({ foo: 'bar' })
]) {}
```
```js
const { compose, Model } = require('objection');
const mixins = compose(
SomeMixin,
SomeOtherMixin,
EvenMoreMixins,
LolSoManyMixins,
ImAMixinWithOptions({ foo: 'bar' })
);
class Person extends mixins(Model) {}
```
Mixins can also be used as decorators:
```js
@SomeMixin
@MixinWithOptions({ foo: 'bar' })
class Person extends Model {}
```
================================================
FILE: doc/guide/query-examples.md
================================================
---
sidebarDepth: 3
---
# Query examples
The `Person` model used in the examples is defined [here](/guide/models.html#examples).
All queries are started with one of the [Model](/api/model/) methods [query](/api/model/static-methods.html#static-query), [\$query](/api/model/instance-methods.html#query), [relatedQuery](/api/model/static-methods.html#static-relatedquery) or [\$relatedQuery](/api/model/instance-methods.html#relatedquery). All these methods return a [QueryBuilder](/api/query-builder/) instance that can be used just like a [knex QueryBuilder](https://knexjs.org/guide/query-builder.html) but they also have a bunch of methods added by objection.
Note that you can chain [debug()](/api/query-builder/other-methods.html#debug) to any query to get the executed SQL printed to console.
## Basic queries
### Find queries
Find queries can be created by calling [Model.query()](/api/model/static-methods.html#static-query) and chaining query builder methods for the returned
[QueryBuilder](/api/query-builder/) instance.
In addition to the examples here, you can find more examples behind these links.
- [subqueries](/recipes/subqueries.html)
- [raw queries](/recipes/raw-queries.html)
- [precedence and parentheses](/recipes/precedence-and-parentheses.html)
There's also a large amount of examples in the [API documentation](/api/query-builder/).
##### Examples
Fetch an item by id:
```js
const person = await Person.query().findById(1);
console.log(person.firstName);
console.log(person instanceof Person); // --> true
```
```sql
select "persons".* from "persons" where "persons"."id" = 1
```
Fetch all people from the database:
```js
const people = await Person.query();
console.log(people[0] instanceof Person); // --> true
console.log('there are', people.length, 'People in total');
```
```sql
select "persons".* from "persons"
```
The return value of the [query](/api/model/static-methods.html#static-query) method is an instance of [QueryBuilder](/api/query-builder/) that has all the methods a [knex QueryBuilder](https://knexjs.org/#Builder) has and a lot more. Here is a simple example that uses some of them:
```js
const middleAgedJennifers = await Person.query()
.select('age', 'firstName', 'lastName')
.where('age', '>', 40)
.where('age', '<', 60)
.where('firstName', 'Jennifer')
.orderBy('lastName');
console.log('The last name of the first middle aged Jennifer is');
console.log(middleAgedJennifers[0].lastName);
```
```sql
select "age", "firstName", "lastName"
from "persons"
where "age" > 40
and "age" < 60
and "firstName" = 'Jennifer'
order by "lastName" asc
```
The next example shows how easy it is to build complex queries:
```js
const people = await Person.query()
.select('persons.*', 'parent.firstName as parentFirstName')
.innerJoin('persons as parent', 'persons.parentId', 'parent.id')
.where('persons.age', '<', Person.query().avg('persons.age'))
.whereExists(
Animal.query()
.select(1)
.whereColumn('persons.id', 'animals.ownerId')
)
.orderBy('persons.lastName');
console.log(people[0].parentFirstName);
```
```sql
select "persons".*, "parent"."firstName" as "parentFirstName"
from "persons"
inner join "persons"
as "parent"
on "persons"."parentId" = "parent"."id"
where "persons"."age" < (
select avg("persons"."age")
from "persons"
)
and exists (
select 1
from "animals"
where "persons"."id" = "animals"."ownerId"
)
order by "persons"."lastName" asc
```
In addition to knex methods, the [QueryBuilder](/api/query-builder/) has a lot of helpers for dealing with relations like the [joinRelated](/api/query-builder/join-methods.html#joinrelated) method:
```js
const people = await Person.query()
.select('parent:parent.name as grandParentName')
.joinRelated('parent.parent');
console.log(people[0].grandParentName);
```
```sql
select "parent:parent"."firstName" as "grandParentName"
from "persons"
inner join "persons"
as "parent"
on "parent"."id" = "persons"."parentId"
inner join "persons"
as "parent:parent"
on "parent:parent"."id" = "parent"."parentId"
```
Objection allows a bit more modern syntax with groupings and subqueries. Where knex requires you to use an old fashioned `function` an `this`, with objection you can use arrow functions:
```js
const nonMiddleAgedJennifers = await Person.query()
.where(builder => builder.where('age', '<', 40).orWhere('age', '>', 60))
.where('firstName', 'Jennifer')
.orderBy('lastName');
console.log('The last name of the first non middle aged Jennifer is');
console.log(nonMiddleAgedJennifers[0].lastName);
```
```sql
select "persons".* from "persons"
where ("age" < 40 or "age" > 60)
and "firstName" = 'Jennifer'
order by "lastName" asc
```
### Insert queries
Insert queries are created by chaining the [insert](/api/query-builder/mutate-methods.html#insert) method to the query. See the [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) method for inserting object graphs.
In addition to the examples here, you can find more examples behind these links.
- [insert API reference](/api/query-builder/mutate-methods.html#insert)
- [graph inserts](/guide/query-examples.html#graph-inserts)
##### Examples
```js
const jennifer = await Person.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
console.log(jennifer instanceof Person); // --> true
console.log(jennifer.firstName); // --> 'Jennifer'
console.log(jennifer.fullName()); // --> 'Jennifer Lawrence'
```
```sql
insert into "persons" ("firstName", "lastName") values ('Jennifer', 'Lawrence')
```
Just like with any query, you can mix in `raw` statements, subqueries, `knex.raw` instances etc.
```js
const jennifer = await Person.query().insert({
firstName: 'Average',
lastName: 'Person',
age: Person.query().avg('age')
});
```
### Update queries
Update queries are created by chaining the [update](/api/query-builder/mutate-methods.html#update) or [patch](/api/query-builder/mutate-methods.html#patch) method to the query. [patch](/api/query-builder/mutate-methods.html#patch) and [update](/api/query-builder/mutate-methods.html#update) return the number of updated rows. If you want the freshly updated item as a result you can use the helper method [patchAndFetchById](/api/query-builder/mutate-methods.html#patchandfetchbyid) and [updateAndFetchById](/api/query-builder/mutate-methods.html#updateandfetchbyid). On postgresql you can simply chain [.returning('\*')](/api/query-builder/find-methods.html#returning) or take a look at [this recipe](/recipes/returning-tricks.html) for more ideas. See [update](/api/query-builder/mutate-methods.html#update) and [patch](/api/query-builder/mutate-methods.html#patch) API documentation for discussion about their differences.
In addition to the examples here, you can find more examples behind these links.
- [patch API reference](/api/query-builder/mutate-methods.html#patch)
- [raw queries](/recipes/raw-queries.html)
##### Examples
Update an item by id:
```js
const numUpdated = await Person.query()
.findById(1)
.patch({
firstName: 'Jennifer'
});
```
```sql
update "persons" set "firstName" = 'Jennifer' where "id" = 1
```
Update multiple items:
```js
const numUpdated = await Person.query()
.patch({ lastName: 'Dinosaur' })
.where('age', '>', 60);
console.log('all people over 60 years old are now dinosaurs');
console.log(numUpdated, 'people were updated');
```
```sql
update "persons" set "lastName" = 'Dinosaur' where "age" > 60
```
Update and fetch an item:
```js
const updatedPerson = await Person.query().patchAndFetchById(246, {
lastName: 'Updated'
});
console.log(updatedPerson.lastName); // --> Updated.
```
```sql
update "persons" set "lastName" = 'Updated' where "id" = 246
select "persons".* from "persons" where "id" = 246
```
### Delete queries
Delete queries are created by chaining the [delete](/api/query-builder/mutate-methods.html#delete) method to the query.
NOTE: The return value of the query will be the number of deleted rows. _If you're using Postgres take a look at [this recipe](/recipes/returning-tricks.html) if you'd like the deleted rows to be returned as Model instances_.
##### Examples
Delete an item by id:
```js
const numDeleted = await Person.query().deleteById(1);
```
```sql
delete from "persons" where id = 1
```
Delete multiple items:
```js
const numDeleted = await Person.query()
.delete()
.where(raw('lower("firstName")'), 'like', '%ennif%');
console.log(numDeleted, 'people were deleted');
```
```sql
delete from "persons" where lower("firstName") like '%ennif%'
```
You can always use [subqueries](/recipes/subqueries.html), [raw](/api/objection/#raw), [ref](/api/objection/#ref), [lit](/api/objection/#lit) and all query building methods with [delete](/api/query-builder/mutate-methods.html#delete) queries, just like with every query in objection. With some databases, you cannot use joins with deletes (db restriction, not objection). You can replace joins with subqueries like this:
```js
// This query deletes all people that have a pet named "Fluffy".
await Person.query()
.delete()
.whereIn(
'id',
Person.query()
.select('persons.id')
.joinRelated('pets')
.where('pets.name', 'Fluffy')
);
```
```sql
delete from "persons"
where "id" in (
select "persons.id"
from "persons"
join "pets" on "pets.ownerId" = "persons.id"
where "pets.name" = 'Fluffy'
)
```
```js
// This is another way to implement the previous query.
await Person.query()
.delete()
.whereExists(Person.relatedQuery('pets').where('pets.name', 'Fluffy'));
```
```sql
delete from "persons"
where exists (
select "pets".*
from "pets"
where "pets.ownerId" = "persons.id"
and "pets.name" = 'Fluffy'
)
```
## Relation queries
While the static [query](/api/model/static-methods.html#static-query) method can be used to create a query to a whole table [relatedQuery](/api/model/static-methods.html#static-relatedquery) and its instance method counterpart [\$relatedQuery](/api/model/instance-methods.html#relatedquery) can be used to query items related to another item. Both of these methods return an instance of [QueryBuilder](/api/query-builder/) just like the [query](/api/model/static-methods.html#static-query) method.
### Relation find queries
Simply call [\$relatedQuery('relationName')](/api/model/instance-methods.html#relatedquery) for a model _instance_ to fetch a relation for it. The relation name is given as the only argument. The return value is a [QueryBuilder](/api/query-builder/) so you once again have all the query methods at your disposal. In many cases it's more convenient to use [eager loading](/guide/query-examples.html#eager-loading) to fetch relations. [\$relatedQuery](/api/model/instance-methods.html#relatedquery) is better when you only need one relation and you need to filter the query extensively.
The static method [relatedQuery](/api/model/static-methods.html#static-relatedquery) can be used to create related queries for multiple items using identifiers, model instances or even subqueries. This allows you to build complex queries by composing simple pieces.
In addition to the examples here, you can find more examples behind these links.
- [relation subqueries](/recipes/relation-subqueries.html)
- [relatedQuery](/api/model/static-methods.html#static-relatedquery)
##### Examples
This example fetches the person's pets. `'pets'` is the name of a relation defined in [relationMappings](/api/model/static-properties.html#static-relationmappings).
```js
const person = await Person.query().findById(1);
```
```sql
select "persons".* from "persons" where "persons"."id" = 1
```
```js
const dogs = await person
.$relatedQuery('pets')
.where('species', 'dog')
.orderBy('name');
```
```sql
select "animals".* from "animals"
where "species" = 'dog'
and "animals"."ownerId" = 1
order by "name" asc
```
The above example needed two queries to find pets of a person. You can do this with one single query using the static [relatedQuery](/api/model/static-methods.html#static-relatedquery) method:
```js
const dogs = await Person.relatedQuery('pets')
.for(1)
.where('species', 'dog')
.orderBy('name');
```
```sql
select "animals".* from "animals"
where "species" = 'dog'
and "animals"."ownerId" = 1
order by "name" asc
```
With `HasManyRelation`s and `BelongsToOneRelation`s the `relatedQuery` helper may just seem like unnecessary bloat. You can of course simply write the SQL directly. The following code should be clear to anyone even without any objection experience:
```js
const dogs = await Pet.query()
.where('species', 'dog')
.where('ownerId', 1)
.orderBy('name')
```
```sql
select "animals".* from "animals"
where "species" = 'dog'
and "ownerId" = 1
order by "name" asc
```
The `relatedQuery` helper comes in handy with `ManyToManyRelation` where the needed SQL is more complex. it also provides a unified API for all kinds of relations. You can write the same code regardless of the relation type. Or you may simply prefer the `relatedQuery` style. Now back to the examples :)
If you want to fetch dogs for multiple people in one query, you can pass an array of identifiers to the `for` method like this:
```js
const dogs = await Person.relatedQuery('pets')
.for([1, 2])
.where('species', 'dog')
.orderBy('name');
```
```sql
select "animals".* from "animals"
where "species" = 'dog'
and "animals"."ownerId" in (1, 2)
order by "name" asc
```
You can even give it a subquery! The following example fetches all dogs of all people named Jennifer using one single query:
```js
// Note that there is no `await` here. This query does not get executed.
// jennifersSubQuery is of type QueryBuilder.
const jennifersSubQuery = Person.query().where('name', 'Jennifer');
// This is the only executed query in this example.
const dogs = await Person.relatedQuery('pets')
.for(jennifersSubQuery)
.where('species', 'dog')
.orderBy('name');
```
```sql
select "animals".* from "animals"
where "species" = 'dog'
and "animals"."ownerId" in (
select "persons"."id"
from "persons"
where "name" = 'Jennifer'
)
order by "name" asc
```
### Relation insert queries
Chain the [insert](/api/query-builder/mutate-methods.html#insert) method to a [relatedQuery](/api/model/static-methods.html#static-relatedquery) or [\$relatedQuery](/api/model/instance-methods.html#relatedquery) call to insert a related object for an item. The query inserts a new object to the related table and updates the needed tables to create the relationship. In case of many-to-many relation a row is inserted to the join table etc. Also check out [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) method for an alternative way to insert related models.
##### Examples
Add a pet for a person:
```js
const person = await Person.query().findById(1);
```
```sql
select "persons".* from "persons" where "persons"."id" = 1
```
```js
const fluffy = await person.$relatedQuery('pets').insert({ name: 'Fluffy' });
```
```sql
insert into "animals" ("name", "ownerId") values ('Fluffy', 1)
```
Just like with [relation find queries](#relation-find-queries), you can save a query and add a pet for a person using one single query by utilizing the static `relatedQuery` method:
```js
const fluffy = await Person.relatedQuery('pets')
.for(1)
.insert({ name: 'Fluffy' });
```
```sql
insert into "animals" ("name", "ownerId") values ('Fluffy', 1)
```
If you want to write columns to the join table of a many-to-many relation you first need to specify the columns in the `extra` array of the `through` object in [relationMappings](/api/model/static-properties.html#static-relationmappings) (see the examples behind the link). For example, if you specified an array `extra: ['awesomeness']` in [relationMappings](/api/model/static-properties.html#static-relationmappings) then `awesomeness` is written to the join table in the following example:
```js
const movie = await Person.relatedQuery('movies')
.for(100)
.insert({ name: 'The room', awesomeness: 9001 });
console.log('best movie ever was added');
```
```sql
insert into "movies" ("name")
values ('The room')
insert into "persons_movies" ("movieId", "personId", "awesomeness")
values (14, 100, 9001)
```
See [this recipe](/recipes/extra-properties.html) for more information about `extra` properties.
### Relation relate queries
Relating means attaching a existing item to another item through a relationship defined in the [relationMappings](/api/model/static-properties.html#static-relationmappings).
In addition to the examples here, you can find more examples behind these links.
- [relate method](/api/query-builder/mutate-methods.html#relate)
##### Examples
In the following example we relate an actor to a movie. In this example the relation between `Person` and `Movie` is a many-to-many relation but `relate` also works for all other relation types.
```js
const actor = await Person.query().findById(100);
```
```sql
select "persons".* from "persons" where "persons"."id" = 100
```
```js
const movie = await Movie.query().findById(200);
```
```sql
select "movies".* from "movies" where "movies"."id" = 200
```
```js
await actor.$relatedQuery('movies').relate(movie);
```
```sql
insert into "persons_movies" ("personId", "movieId") values (100, 200)
```
You can also pass the id `200` directly to `relate` instead of passing a model instance. A more objectiony way of doing this would be to once again utilize the static [relatedQuery](/api/model/static-methods.html#static-relatedquery) method:
```js
await Person.relatedQuery('movies')
.for(100)
.relate(200);
```
```sql
insert into "persons_movies" ("personId", "movieId") values (100, 200)
```
Actually in this case, the cleanest way of all would be to just insert a row to the `persons_movies` table. Note that you can create models for pivot (join) tables too. There's nothing wrong with that.
Here's one more example that relates four movies to the first person whose first name Arnold. Note that this query only works on Postgres because on other databases it would require multiple queries.
```js
await Person.relatedQuery('movies')
.for(
Person.query()
.where('firstName', 'Arnold')
.limit(1)
)
.relate([100, 200, 300, 400]);
```
### Relation unrelate queries
Unrelating is the inverse of [relating](#relation-relate-queries). For example if an actor is related to a movie through a `movies` relation, unrelating them means removing this association, but neither the movie nor the actor get deleted from the database.
##### Examples
The first example `unrelates` all movies whose name starts with the string 'Terminator' from an actor.
```js
const actor = await Person.query().findById(100);
```
```sql
select "persons".* from "persons" where "persons"."id" = 100
```
```js
await actor
.$relatedQuery('movies')
.unrelate()
.where('name', 'like', 'Terminator%');
```
```sql
delete from "persons_movies"
where "persons_movies"."personId" = 100
where "persons_movies"."movieId" in (
select "movies"."id" from "movies" where "name" like 'Terminator%'
)
```
The same using the static [relatedQuery](/api/model/static-methods.html#static-relatedquery) method:
```js
await Person.relatedQuery('movies')
.for(100)
.unrelate()
.where('name', 'like', 'Terminator%');
```
```sql
delete from "persons_movies"
where "persons_movies"."personId" = 100
and "persons_movies"."movieId" in (
select "movies"."id"
from "movies"
where "name" like 'Terminator%'
)
```
The next query removes all Terminator movies from Arnold Schwarzenegger:
```js
// Once again, note that we don't await this query. This query
// is not executed. It's a placeholder that will be used to build
// a subquery when the `relatedQuery` gets executed.
const arnold = Person.query().findOne({
firstName: 'Arnold',
lastName: 'Schwarzenegger'
});
await Person.relatedQuery('movies')
.for(arnold)
.unrelate()
.where('name', 'like', 'Terminator%');
```
```sql
delete from "persons_movies"
where "persons_movies"."personId" in (
select "persons"."id"
from "persons"
where "firstName" = 'Arnold'
and "lastName" = 'Schwarzenegger'
)
and "persons_movies"."movieId" in (
select "movies"."id"
from "movies"
where "name" like 'Terminator%'
)
```
### Relation update queries
Relation update queries work just like the normal update queries, but the query is automatically filtered so that only the related items are affected.
See the [API documentation](/api/query-builder/mutate-methods.html#update) of `update` method.
##### Examples
```js
await Person.relatedQuery('pets')
.for([1, 2])
.patch({ name: raw(`concat(name, ' the doggo')`) })
.where('species', 'dog');
```
```sql
update "animals"
set "name" = concat(name, ' the doggo')
where "animals"."ownerId" in (1, 2)
and "species" = 'dog'
```
### Relation delete queries
Relation delete queries work just like the normal delete queries, but the query is automatically filtered so that only the related items are affected.
See the [API documentation](/api/query-builder/mutate-methods.html#delete) of `delete` method.
##### Examples
```js
await Person.relatedQuery('pets')
.for([1, 2])
.delete()
.where('species', 'dog');
```
```sql
delete from "animals"
where "animals"."ownerId" in (1, 2)
and "species" = 'dog'
```
## Eager loading
You can fetch an arbitrary graph of relations for the results of any query by chaining the [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) or [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) method. Both methods take a [relation expression](/api/types/#type-relationexpression) as the first argument. In addition to making your life easier, eager loading avoids the "N+1 selects" problem and provide a great performance.
Because the relation expressions are strings (there's also an optional [object notation](/api/types/#relationexpression-object-notation)) they can be easily passed, for example, as a query parameter of an HTTP request. However, allowing the client to execute expressions like this without any limitations is not very secure. Therefore the [QueryBuilder](/api/query-builder/) has the [allowGraph](/api/query-builder/eager-methods.html#allowgraph) method. [allowGraph](/api/query-builder/eager-methods.html#allowgraph) can be used to limit the allowed relation expression to a certain subset.
By giving the expression `[pets, children.pets]` for [allowGraph](/api/query-builder/eager-methods.html#allowgraph) the value passed to [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) is allowed to be one of:
- `'pets'`
- `'children'`
- `'children.pets'`
- `'[pets, children]'`
- `'[pets, children.pets]'`
Examples of expressions that would cause an error:
- `'movies'`
- `'children.children'`
- `'[pets, children.children]'`
- `'notEvenAnExistingRelation'`
In addition to the [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) and [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) methods, relations can be fetched using the [fetchGraph](/api/model/static-properties.html#static-fetchgraph) and
[\$fetchGraph](/api/model/instance-methods.html#fetchgraph) methods.
[withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) uses multiple queries to load the related items. Note that [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) used to be called `eager`.). [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) uses joins and only performs one single query to fetch the whole relation graph. This doesn't mean that `withGraphJoined` is faster though. See the performance discussion [here](/api/query-builder/eager-methods.html#withgraphfetched). You should only use `withGraphJoined` if you actually need the joins to be able to reference the nested tables. When in doubt use [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched).
##### Examples
Fetch the `pets` relation for all results of a query:
```js
const people = await Person.query().withGraphFetched('pets');
// Each person has the `pets` property populated with Animal objects related
// through the `pets` relation.
console.log(people[0].pets[0].name);
console.log(people[0].pets[0] instanceof Animal); // --> true
```
Fetch multiple relations on multiple levels:
```js
const people = await Person.query().withGraphFetched(
'[pets, children.[pets, children]]'
);
// Each person has the `pets` property populated with Animal objects related
// through the `pets` relation. The `children` property contains the Person's
// children. Each child also has the `pets` and `children` relations eagerly
// fetched.
console.log(people[0].pets[0].name);
console.log(people[1].children[2].pets[1].name);
console.log(people[1].children[2].children[0].name);
```
Here's the previous query using the [object notation](/api/types/#relationexpression-object-notation)
```js
const people = await Person.query().withGraphFetched({
pets: true,
children: {
pets: true,
children: true
}
});
```
Fetch one relation recursively:
```js
const people = await Person.query().withGraphFetched('[pets, children.^]');
// The children relation is from Person to Person. If we want to fetch the whole
// descendant tree of a person we can just say "fetch this relation recursively"
// using the `.^` notation.
console.log(
people[0].children[0].children[0].children[0].children[0].firstName
);
```
Limit recursion to 3 levels:
```js
const people = await Person.query().withGraphFetched('[pets, children.^3]');
console.log(people[0].children[0].children[0].children[0].firstName);
```
Relations can be modified using the [modifyGraph](/api/query-builder/other-methods.html#modifygraph) method:
```js
const people = await Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('children.pets', builder => {
// Only select pets older than 10 years old for children
// and only return their names.
builder.where('age', '>', 10).select('name');
});
```
Relations can also be modified using modifiers like this:
```js
const people = await Person.query()
.withGraphFetched(
'[pets(selectName, onlyDogs), children(orderByAge).[pets, children]]'
)
.modifiers({
selectName: builder => {
builder.select('name');
},
orderByAge: builder => {
builder.orderBy('age');
},
onlyDogs: builder => {
builder.where('species', 'dog');
}
});
console.log(people[0].children[0].pets[0].name);
console.log(people[0].children[0].movies[0].id);
```
Reusable modifiers can be defined for models using [modifiers](/api/model/static-properties.html#static-modifiers)
```js
// Person.js
class Person extends Model {
static get modifiers() {
return {
defaultSelects(builder) {
builder.select('id', 'firstName');
},
orderByAge(builder) {
builder.orderBy('age');
}
};
}
}
// Animal.js
class Animal extends Model {
static get modifiers() {
return {
orderByName(builder) {
builder.orderBy('name');
},
// Note that this modifier takes an argument.
onlySpecies(builder, species) {
builder.where('species', species);
}
};
}
}
// somewhereElse.js
const people = await Person.query().modifiers({
// This way you can bind arguments to modifiers.
onlyDogs: query => query.modify('onlySpecies', 'dog')
}).withGraphFetched(`
children(defaultSelects, orderByAge).[
pets(onlyDogs, orderByName),
movies
]
`);
console.log(people[0].children[0].pets[0].name);
console.log(people[0].children[0].movies[0].id);
```
Relations can be aliased using `as` keyword:
```js
const people = await Person.query().withGraphFetched(`[
children(orderByAge) as kids .[
pets(filterDogs) as dogs,
pets(filterCats) as cats
movies.[
actors
]
]
]`);
console.log(people[0].kids[0].dogs[0].name);
console.log(people[0].kids[0].movies[0].id);
```
Example usage for [allowGraph](/api/query-builder/eager-methods.html#allowgraph) in an express route:
```js
expressApp.get('/people', async (req, res) => {
const people = await Person.query()
.allowGraph('[pets, children.pets]')
.withGraphFetched(req.query.eager);
res.send(people);
});
```
[withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) can be used just like [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched). In addition you can refer to the related items from the root query because they are all joined:
```js
const people = await Person.query()
.withGraphJoined('[pets, children.pets]')
.where('pets.age', '>', 10)
.where('children:pets.age', '>', 10);
```
## Graph inserts
Arbitrary relation graphs can be inserted using the [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) method. This is best explained using examples, so check them out.
See the [allowGraph](/api/query-builder/eager-methods.html#allowgraph) method if you need to limit which relations can be inserted using [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) method to avoid security issues.
If you are using Postgres the inserts are done in batches for maximum performance. On other databases the rows need to be inserted one at a time. This is because postgresql is the only database engine that returns the identifiers of all inserted rows and not just the first or the last one.
[insertGraph](/api/query-builder/mutate-methods.html#insertgraph) operation is **not** atomic by default! You need to start a transaction and pass it to the query using any of the supported ways. See the section about [transactions](/guide/transactions.html) for more information.
You can read more about graph inserts from [this blog post](https://www.vincit.fi/en/blog/nested-eager-loading-and-inserts-with-objection-js/).
##### Examples
```js
// The return value of `insertGraph` is the input graph converted into
// model instances. Inserted objects have ids added to them and related
// rows have foreign keys set, but no other columns get fetched from
// the database. You can use `insertGraphAndFetch` for that.
const graph = await Person.query().insertGraph({
firstName: 'Sylvester',
lastName: 'Stallone',
children: [
{
firstName: 'Sage',
lastName: 'Stallone',
pets: [
{
name: 'Fluffy',
species: 'dog'
}
]
}
]
});
```
The query above will insert 'Sylvester', 'Sage' and 'Fluffy' into db and create relationships between them as defined in the [relationMappings](/api/model/static-properties.html#static-relationmappings) of the models. Technically [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) builds a dependency graph from the object graph and inserts the models that don't depend on any other models until the whole graph is inserted.
If you need to refer to the same model in multiple places you can use the special properties `#id` and `#ref` like this:
```js
await Person.query().insertGraph(
[
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
'#id': 'silverLiningsPlaybook',
name: 'Silver Linings Playbook',
duration: 122
}
]
},
{
firstName: 'Bradley',
lastName: 'Cooper',
movies: [
{
'#ref': 'silverLiningsPlaybook'
}
]
}
],
{ allowRefs: true }
);
```
Note that you need to also set the `allowRefs` option to `true` for this to work.
The query above will insert only one movie (the 'Silver Linings Playbook') but both 'Jennifer' and 'Bradley' will have the movie related to them through the many-to-many relation `movies`. The `#id` can be any string. There are no format or length requirements for them. It is quite easy to create circular dependencies using `#id` and `#ref`. Luckily [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) detects them and rejects the query with a clear error message.
You can refer to the properties of other models anywhere in the graph using expressions of format `#ref{.}` as long as the reference doesn't create a circular dependency. For example:
```js
await Person
.query()
.insertGraph([
{
"#id": 'jenni',
firstName: 'Jennifer',
lastName: 'Lawrence',
pets: [{
name: "I am the dog of #ref{jenni.firstName} whose id is #ref{jenni.id}",
species: 'dog'
}
],
{ allowRefs: true }
}]);
```
Again, make sure you set the `allowRefs` option to `true`.
The query above will insert a pet named `I am the dog of Jennifer whose id is 523` for Jennifer. If `#ref{}` is used within a string, the references are replaced with the referred values inside the string. If the reference string contains nothing but the reference, the referred value is copied to its place preserving its type.
Existing rows can be related to newly inserted rows by using the `relate` option. `relate` can be `true` in which case all models in the graph that have an identifier get related. `relate` can also be an array of relation paths like `['children', 'children.movies.actors']` in which case only objects in those paths get related even if they have an idetifier.
```js
await Person.query().insertGraph(
[
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
id: 2636
}
]
}
],
{
relate: true
}
);
```
The query above would create a new person `Jennifer Lawrence` and add an existing movie (id = 2636) to its `movies` relation. The next query would do the same:
```js
await Person.query().insertGraph(
[
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
id: 2636
}
]
}
],
{
relate: ['movies']
}
);
```
The `relate` option can also contain nested relations:
```js
await Person.query().insertGraph(
[
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
name: 'Silver Linings Playbook',
duration: 122,
actors: [
{
id: 2516
}
]
}
]
}
],
{
relate: ['movies.actors']
}
);
```
If you need to mix inserts and relates inside a single relation, you can use the special property `#dbRef`
```js
await Person.query().insertGraph([
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
'#dbRef': 2636
},
{
// This will be inserted with an id.
id: 100,
name: 'New movie'
}
]
}
]);
```
## Graph upserts
Arbitrary relation graphs can be upserted (insert + update + delete) using the [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) method. This is best explained using examples, so check them out.
By default [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) method updates the objects that have an id, inserts objects that don't have an id and deletes all objects that are not present. This functionality can be modified in many ways by providing [UpsertGraphOptions](/api/types/#type-upsertgraphoptions) object as the second argument.
The [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) method works a little different than the other update and patch methods. When using [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) any `where` or `having` methods are ignored. The models are updated based on the id properties in the graph. This is also clarified in the examples.
[upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) uses [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) under the hood for inserts. That means that you can insert object graphs for relations and use all [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) features like `#ref` references.
[upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) operation is **not** atomic by default! You need to start a transaction and pass it to the query using any of the supported ways. See the section about [transactions](/guide/transactions.html) for more information.
See the [allowGraph](/api/query-builder/eager-methods.html#allowgraph) method if you need to limit which relations can be modified using [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) method to avoid security issues.
::: warning
WARNING!
Before you start using `upsertGraph` beware that it's not the silver bullet it seems to be. If you start using it because it seems to provide a "mongodb API" for a relational database, you are using it for a wrong reason!
Our suggestion is to first try to write any code without it and only use `upsertGraph` if it saves you **a lot** of code and makes things simpler. Over time you'll learn where `upsertGraph` helps and where it makes things more complicated. Don't use it by default for everything. You can search through the objection issues to see what kind of problems `upsertGraph` can cause if used too much.
For simple things `upsertGraph` calls are easy to understand and remain readable. When you start passing it a bunch of options it becomes increasingly difficult for other developers (and even yourself) to understand.
It's also really easy to create a server that doesn't work well with multiple users by overusing `upsertGraph`. That's because you can easily get into a situation where you override other user's changes if you always upsert large graphs at a time. Always try to update the minimum amount of rows and columns and you'll save yourself a lot of trouble in the long run.
:::
##### Examples
For the following examples, assume this is the content of the database:
```js
[{
id: 1,
firstName: 'Jennifer',
lastName: 'Aniston',
// This is a BelongsToOneRelation
parent: {
id: 2,
firstName: 'Nancy',
lastName: 'Dow'
},
// This is a HasManyRelation
pets: [{
id: 1,
name: 'Doggo',
species: 'Dog',
}, {
id: 2,
name: 'Kat',
species: 'Cat',
}],
// This is a ManyToManyRelation
movies: [{
id: 1,
name: 'Horrible Bosses',
reviews: [{
id: 1,
title: 'Meh',
stars: 3,
text: 'Meh'
}]
}, {
id: 2
name: 'Wanderlust',
reviews: [{
id: 2,
title: 'Brilliant',
stars: 5,
text: 'Makes me want to travel'
}]
}]
}]
```
By default [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) method updates the objects that have an id, inserts objects that don't have an id and deletes all objects that are not present. Of course the delete only applies to relations and not the root. Here's a basic example:
```js
// The return value of `upsertGraph` is the input graph converted into
// model instances. Inserted objects have ids added to them related
// rows have foreign keys set but no other columns get fetched from
// the database. You can use `upsertGraphAndFetch` for that.
const graph = await Person.query().upsertGraph({
// This updates the `Jennifer Aniston` person since the id property is present.
id: 1,
firstName: 'Jonnifer',
parent: {
// This also gets updated since the id property is present. If no id was given
// here, Nancy Dow would get deleted, a new Person John Aniston would
// get inserted and related to Jennifer.
id: 2,
firstName: 'John',
lastName: 'Aniston'
},
// Notice that Kat the Cat is not listed in `pets`. It will get deleted.
pets: [
{
// Jennifer just got a new pet. Insert it and relate it to Jennifer. Notice
// that there is no id!
name: 'Wolfgang',
species: 'Dog'
},
{
// It turns out Doggo is a cat. Update it.
id: 1,
species: 'Cat'
}
],
// Notice that Wanderlust is missing from the list. It will get deleted.
// It is also worth mentioning that the Wanderlust's `reviews` or any
// other relations are NOT recursively deleted (unless you have
// defined `ON DELETE CASCADE` or other hooks in the db).
movies: [
{
id: 1,
// Upsert graphs can be arbitrarily deep. This modifies the
// reviews of "Horrible Bosses".
reviews: [
{
// Update a review.
id: 1,
stars: 2,
text: 'Even more Meh'
},
{
// And insert another one.
stars: 5,
title: 'Loved it',
text: 'Best movie ever'
},
{
// And insert a third one.
stars: 4,
title: '4 / 5',
text: 'Would see again'
}
]
}
]
});
```
By giving `relate: true` and/or `unrelate: true` options as the second argument, you can change the behaviour so that instead of inserting and deleting rows, they are related and/or unrelated. Rows with no id still get inserted, but rows that have an id and are not currently related, get related.
```js
const options = {
relate: true,
unrelate: true
};
await Person.query().upsertGraph(
{
// This updates the `Jennifer Aniston` person since the id property is present.
id: 1,
firstName: 'Jonnifer',
// Unrelate the parent. This doesn't delete it.
parent: null,
// Notice that Kat the Cat is not listed in `pets`. It will get unrelated.
pets: [
{
// Jennifer just got a new pet. Insert it and relate it to Jennifer. Notice
// that there is no id!
name: 'Wolfgang',
species: 'Dog'
},
{
// It turns out Doggo is a cat. Update it.
id: 1,
species: 'Cat'
}
],
// Notice that Wanderlust is missing from the list. It will get unrelated.
movies: [
{
id: 1,
// Upsert graphs can be arbitrarily deep. This modifies the
// reviews of "Horrible Bosses".
reviews: [
{
// Update a review.
id: 1,
stars: 2,
text: 'Even more Meh'
},
{
// And insert another one.
stars: 5,
title: 'Loved it',
text: 'Best movie ever'
}
]
},
{
// This is some existing movie that isn't currently related to Jennifer.
// It will get related.
id: 1253
}
]
},
options
);
```
`relate` and `unrelate` (and all other [options](/api/types/#type-upsertgraphoptions) can also be lists of relation paths. In that case the option is only applied for the listed relations.
```js
const options = {
// Only enable `unrelate` functionality for these two paths.
unrelate: ['pets', 'movies.reviews'],
// Only enable `relate` functionality for 'movies' relation.
relate: ['movies'],
// Disable deleting for movies.
noDelete: ['movies']
};
await Person.query().upsertGraph(
{
id: 1,
// This gets deleted since `unrelate` list doesn't have 'parent' in it
// and deleting is the default behaviour.
parent: null,
// Notice that Kat the Cat is not listed in `pets`. It will get unrelated.
pets: [
{
// It turns out Doggo is a cat. Update it.
id: 1,
species: 'Cat'
}
],
// Notice that Wanderlust is missing from the list. It will NOT get unrelated
// or deleted since `unrelate` list doesn't contain `movies` and `noDelete`
// list does.
movies: [
{
id: 1,
// Upsert graphs can be arbitrarily deep. This modifies the
// reviews of "Horrible Bosses".
reviews: [
{
// Update a review.
id: 1,
stars: 2,
text: 'Even more Meh'
},
{
// And insert another one.
stars: 5,
title: 'Loved it',
text: 'Best movie ever'
}
]
},
{
// This is some existing movie that isn't currently related to Jennifer.
// It will get related.
id: 1253
}
]
},
options
);
```
You can disable updates, inserts, deletes etc. for the whole [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph) operation or for individual relations by using the `noUpdate`, `noInsert`, `noDelete` etc. options. See [UpsertGraphOptions](/api/types/#type-upsertgraphoptions) docs for more info.
================================================
FILE: doc/guide/relations.md
================================================
# Relations
We already went through how to create relationships (aka. relations, associations) in the [models](/guide/models.html) section's examples but here's a list of all the available relation types in a nicely searchable place. See [this](/api/types/#type-relationmapping) API doc section for full documentation of the relation mapping parameters.
Relationships are a very basic concept in relational databases and if you aren't familiar with it, you should spend some time googling it first. Basically there are three ways to create a relationship between two tables `A` and `B`:
1. Table `A` has a column that holds table `B`'s id. This relationship is called a `BelongsToOneRelation` in objection.
We can say that `A` belongs to one `B`.
2. Table `B` has a column that holds table `A`'s id. This relationship is called a `HasManyRelation` in objection.
We can say that `A` has many `B`'s.
3. Table `C` has columns for both `A` and `B` tables' identifiers. This relationship is called `ManyToManyRelation` in objection.
Each row in `C` joins one `A` with one `B`. Therefore an `A` row can be related to multiple `B` rows and a `B` row can be related to
multiple `A` rows through table `C`.
While relations are usually created between the primary key of one table and a foreign key reference of another table, objection has no such limitations. You can create relationship using any two columns (or any sets of columns). You can even create relation using values nested deep inside json columns.
If you've used other ORMs you may notice that objection's [relationMappings](/api/model/static-properties.html#static-relationmappings) are pretty verbose. There are couple of reasons for that:
1. For a new user, this style underlines what is happening, and which columns and tables are involved.
2. You only need to define relations once. Writing a couple of lines more for clarity shouldn't impact your productivity.
## Examples
Vocabulary for the relation descriptions:
- source model: The model for which you are writing the `relationMapping` for.
- related model: The model at the other end of the relation.
`BelongsToOneRelation`: Use this relation when the source model has the foreign key
```js
class Animal extends Model {
static tableName = 'animals';
static relationMappings = {
owner: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'animals.ownerId',
to: 'persons.id'
}
}
};
}
```
`HasManyRelation`: Use this relation when the related model has the foreign key
```js
class Person extends Model {
static tableName = 'persons';
static relationMappings = {
animals: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'persons.id',
to: 'animals.ownerId'
}
}
};
}
```
`HasOneRelation`: Just like `HasManyRelation` but for one related row
```js
class Person extends Model {
static tableName = 'persons';
static relationMappings = {
animal: {
relation: Model.HasOneRelation,
modelClass: Animal,
join: {
from: 'persons.id',
to: 'animals.ownerId'
}
}
};
}
```
`ManyToManyRelation`: Use this relation when the model is related to a list of other models through a join table
```js
class Person extends Model {
static tableName = 'persons';
static relationMappings = {
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'persons.id',
through: {
// persons_movies is the join table.
from: 'persons_movies.personId',
to: 'persons_movies.movieId'
},
to: 'movies.id'
}
}
};
}
```
`HasOneThroughRelation`: Use this relation when the model is related to a single model through a join table
```js
class Person extends Model {
static tableName = 'persons';
static relationMappings = {
movie: {
relation: Model.HasOneThroughRelation,
modelClass: Movie,
join: {
from: 'persons.id',
through: {
// persons_movies is the join table.
from: 'persons_movies.personId',
to: 'persons_movies.movieId'
},
to: 'movies.id'
}
}
};
}
```
## Require loops (non ECMAScript modules only)
Require loops (circular dependencies, circular requires) are a very common problem when defining relations. Whenever a module `A` imports module `B` that immediately (synchronously) imports module `A`, you create a require loop that node.js or objection cannot solve automatically. A require loop usually leads to the other imported value to be an empty object which causes all kinds of problems. Objection attempts to detect these situations and mention the words `require loop` in the thrown error. Objection offers multiple solutions to this problem. See the circular dependency solutions examples in this section. In addition to objection's solutions, you can always organize your code so that such loops are not created.
If you are using [ECMAScript modules](https://nodejs.org/api/esm.html), circular imports are not a problem. You can just do:
```js
import Animal from "./Animal.js";
class Person extends Model {
static get tableName() {
return "persons";
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: "persons.id",
to: "animals.ownerId",
},
},
};
}
}
```
However if you are not using ECMAScript modules, solutions to require loops are:
```js
class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
// Solution 1:
//
// relationMappings getter is accessed lazily when you execute
// your first query that needs it. Therefore if you `require`
// your models inside the getter, you don't end up with a require loop.
// Note that only one end of the relation needs to be required like
// this, not both. `relationMappings` can also be a method or
// a thunk if you prefer those instead of getters.
const Animal = require('./Animal');
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'persons.id',
to: 'animals.ownerId'
}
},
movies: {
relation: Model.ManyToManyRelation,
// Solution 2:
//
// Absolute file path to a module that exports the model class.
// This is similar to solution 1, but objection calls `require`
// under the hood. The downside here is that you need to give
// an absolute file path because of the way `require` works.
modelClass: path.join(__dirname, 'Movie'),
join: {
from: 'persons.id',
through: {
// persons_movies is the join table.
from: 'persons_movies.personId',
to: 'persons_movies.movieId'
},
to: 'movies.id'
}
},
movies: {
relation: Model.ManyToManyRelation,
// Solution 3:
//
// Use only a module name and define a `modelPaths` property
// for your model (or a superclass of your model). Search for
// `modelPaths` from the docs for more info.
modelClass: 'Movie',
join: {
from: 'persons.id',
through: {
from: 'persons_movies.personId',
to: 'persons_movies.movieId'
},
to: 'movies.id'
}
}
};
}
}
```
================================================
FILE: doc/guide/transactions.md
================================================
# Transactions
Transactions are atomic and isolated units of work in relational databases. If you are not familiar with transactions, I suggest you read up on them. [The wikipedia article](https://en.wikipedia.org/wiki/Database_transaction) is a good place to start.
## Creating a transaction
In objection, a transaction can be started by calling the [Model.transaction](/api/model/static-methods.html#static-transaction) function:
```js
try {
const returnValue = await Person.transaction(async trx => {
// Here you can use the transaction.
// Whatever you return from the transaction callback gets returned
// from the `transaction` function.
return 'the return value of the transaction';
});
// Here the transaction has been committed.
} catch (err) {
// Here the transaction has been rolled back.
}
```
The first argument is a callback that gets called with the transaction object as an argument once the transaction has been successfully started. The transaction object is actually just a [knex transaction object](https://knexjs.org/guide/transactions.html) and you can start the transaction just as well using [knex.transaction](https://knexjs.org/#Transactions) function.
The transaction is committed if the promise returned from the callback is resolved successfully. If the returned Promise is rejected or an error is thrown inside the callback the transaction is rolled back.
The above example works if you have installed a knex instance globally using the `Model.knex()` method. If you haven't, you can pass the knex instance as the first argument to the `transaction` method
```js
const returnValue = await Person.transaction(knex, async trx => { ... })
```
Or just simply use `knex.transaction`
```js
const returnValue = await knex.transaction(async trx => { ... })
```
::: tip
Note: Even if you start a transaction using `Person.transaction` it doesn't mean that the transaction is just for `Persons`. It's just a normal knex transaction, no matter what model you use to start it. You can even use the `Model` base class if you want.
:::
An alternative way to start a transaction is to use the [Model.startTransaction()](/api/model/static-methods.html#static-starttransaction) method:
```js
const { transaction } = require('objection');
const trx = await Person.startTransaction();
try {
// If you created the transaction using `Model.startTransaction`, you need
// commit or rollback the transaction manually.
await trx.commit();
} catch (err) {
await trx.rollback();
throw err;
}
```
There's also a third way to use transactions, which is described in detail [later](#binding-models-to-a-transaction).
## Using a transaction
After you have created a transaction, you need to tell objection which queries should be executed inside that transaction. There are two ways to do that:
1. [By passing the transaction object to each query](/guide/transactions.html#passing-around-a-transaction-object)
2. [By binding models to the transaction](/guide/transactions.html#binding-models-to-a-transaction)
### Passing around a transaction object
The most straightforward way to use a transaction is to explicitly give it to each query you start. [query](/api/model/static-methods.html#static-query), [\$query](/api/model/instance-methods.html#query) and [\$relatedQuery](/api/model/instance-methods.html#relatedquery) accept a transaction as their last argument.
```js
try {
const scrappy = await Person.transaction(async trx => {
const jennifer = await Person.query(trx).insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
const scrappy = await jennifer
.$relatedQuery('pets', trx)
.insert({ name: 'Scrappy' });
return scrappy;
});
console.log('Great success! Both Jennifer and Scrappy were inserted');
} catch (err) {
console.log(
'Something went wrong. Neither Jennifer nor Scrappy were inserted'
);
}
```
Note that you can pass either a normal knex instance or a transaction to [query](/api/model/static-methods.html#static-query), [\$relatedQuery](/api/model/instance-methods.html#relatedquery) etc. allowing you to build helper functions and services that can be used with or without a transaction. When a transaction is not wanted, just pass in the normal knex instance (or nothing at all if you have installed the knex object globally using [Model.knex(knex)](/api/model/static-methods.html#static-knex)):
```js
// `db` can be either a transaction or a knex instance or even
// `null` or `undefined` if you have globally set the knex
// instance using `Model.knex(knex)`.
async function insertPersonAndPet(person, pet, db) {
const person = await Person.query(db).insert(person);
return person.$relatedQuery('pets', db).insert(pet);
}
// All following four ways to call insertPersonAndPet work:
// 1.
const trx = await Person.startTransaction();
await insertPersonAndPet(person, pet, trx);
await trx.commit();
// 2.
await Person.transaction(async trx => {
await insertPersonAndPet(person, pet, trx);
});
// 3.
await insertPersonAndPet(person, pet, Person.knex());
// 4.
await insertPersonAndPet(person, pet);
```
### Binding models to a transaction
The second way to use transactions avoids passing around a transaction object by "binding" model classes to a transaction. You pass all models you want to bind as arguments to the [objection.transaction](/api/objection/#transaction) method and as the last argument you provide a callback that receives **copies** of the models that have been bound to a newly started transaction. All queries started through the bound copies take part in the transaction and you don't need to pass around a transaction object. Note that the models passed to the callback are actual copies of the models passed as arguments to [objection.transaction](/api/objection/#transaction) and starting a query through any other object will **not** be executed inside a transaction.
```js
const { transaction } = require('objection');
try {
const scrappy = await transaction(Person, Animal, async (Person, Animal) => {
// Person and Animal inside this function are bound to a newly
// created transaction. The Person and Animal outside this function
// are not! Even if you do `require('./models/Person')` inside this
// function and start a query using the required `Person` it will
// NOT take part in the transaction. Only the actual objects passed
// to this function are bound to the transaction.
await Person.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
return Animal.query().insert({ name: 'Scrappy' });
});
} catch (err) {
console.log(
'Something went wrong. Neither Jennifer nor Scrappy were inserted'
);
}
```
You only need to give the [objection.transaction](/api/objection/#transaction) function the model classes you use explicitly. All the related model classes are implicitly bound to the same transaction:
```js
const { transaction } = require('objection');
try {
const scrappy = await transaction(Person, async Person => {
const jennifer = await Person.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
// This creates a query using the `Animal` model class but we
// don't need to give `Animal` as one of the arguments for the
// transaction function because `jennifer` is an instance of
// the `Person` that is bound to a transaction.
return jennifer.$relatedQuery('pets').insert({ name: 'Scrappy' });
});
} catch (err) {
console.log(
'Something went wrong. Neither Jennifer nor Scrappy were inserted'
);
}
```
The only way you can mess up with the transactions is if you _explicitly_ start a query using a model class that is not bound to the transaction:
```js
const { transaction } = require('objection');
const Person = require('./models/Person');
const Animal = require('./models/Animal');
await transaction(Person, async BoundPerson => {
// This will be executed inside the transaction.
const jennifer = await BoundPerson.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
// OH NO! This query is executed outside the transaction
// since the `Animal` class is not bound to the transaction.
await Animal.query().insert({ name: 'Scrappy' });
// OH NO! This query is executed outside the transaction
// since the `Person` class is not bound to the transaction.
// BoundPerson !== Person.
await Person.query().insert({ firstName: 'Bradley' });
});
```
The transaction object is always passed as the last argument to the callback:
```js
const { transaction } = require('objection');
await transaction(Person, async (Person, trx) => {
// `trx` is the knex transaction object.
// It can be passed to `transacting`, `query` etc.
// methods, or used as a knex query builder.
const jennifer = await trx('persons').insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
const scrappy = await Animal.query(trx).insert({
name: 'Scrappy'
});
const fluffy = await Animal.query()
.transacting(trx)
.insert({
name: 'Fluffy'
});
return {
jennifer,
scrappy,
fluffy
};
});
```
Originally we advertised this way of doing transactions as a remedy to the transaction passing plague but it has turned out to be pretty error-prone. This approach is handy for single inline functions that do a handful of operations, but becomes tricky when you have to call services and helper methods that also perform database queries. To get the helpers and service functions to participate in the transaction you need to pass around the bound copies of the model classes. If you `require` the same models in the helpers and start queries through them, they will **not** be executed in the transaction since the required models are not the bound copies, but the original models from which the copies were taken.
## Setting the isolation level
You can use `raw` to set the isolation level (among other things):
```js
try {
const scrappy = await Person.transaction(async trx => {
await trx.raw('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
const jennifer = await Person.query(trx).insert({
firstName: 'Jennifer',
lastName: 'Lawrence'
});
const scrappy = await jennifer
.$relatedQuery('pets', trx)
.insert({ name: 'Scrappy' });
return scrappy;
});
console.log('Great success! Both Jennifer and Scrappy were inserted');
} catch (err) {
console.log(
'Something went wrong. Neither Jennifer nor Scrappy were inserted'
);
}
```
================================================
FILE: doc/guide/validation.md
================================================
# Validation
[JSON schema](https://json-schema.org/) validation can be enabled by setting the [jsonSchema](/api/model/static-properties.html#static-jsonschema) property of a model class. The validation is ran each time a [Model](/api/model/) instance is created.
You rarely need to call [\$validate](/api/model/instance-methods.html#validate) method explicitly, but you can do it when needed. If validation fails a [ValidationError](/api/types/#class-validationerror) will be thrown. Since we use Promises, this usually means that a promise will be rejected with an instance of [ValidationError](/api/types/#class-validationerror).
See [the recipe book](/recipes/custom-validation.html) for instructions if you want to use some other validation library.
## Examples
All these will trigger the validation:
```js
Person.fromJson({ firstName: 'jennifer', lastName: 'Lawrence' });
await Person.query().insert({ firstName: 'jennifer', lastName: 'Lawrence' });
await Person.query()
.update({ firstName: 'jennifer', lastName: 'Lawrence' })
.where('id', 10);
// Patch operation ignores the `required` property of the schema
// and only validates the given properties. This allows a subset
// of model's properties to be updated.
await Person.query()
.patch({ age: 24 })
.where('age', '<', 24);
await Person.query().insertGraph({
firstName: 'Jennifer',
pets: [
{
name: 'Fluffy'
}
]
});
await Person.query().upsertGraph({
id: 1,
pets: [
{
name: 'Fluffy II'
}
]
});
```
Validation errors provide detailed error message:
```js
try {
await Person.query().insert({ firstName: 'jennifer' });
} catch (err) {
console.log(err instanceof objection.ValidationError); // --> true
console.log(err.data); // --> {lastName: [{message: 'required property missing', ...}]}
}
```
Error parameters returned by [ValidationError](/api/types/#class-validationerror) could be used to provide custom error messages:
```js
try {
await Person.query().insert({ firstName: 'jennifer' });
} catch (err) {
let lastNameErrors = err.data.lastName;
for (let i = 0; i < lastNameErrors.length; ++i) {
let lastNameError = lastNameErrors[i];
if (lastNameError.keyword === 'required') {
console.log('This field is required!');
} else if (lastNameError.keyword === 'minLength') {
console.log('Must be longer than ' + lastNameError.params.limit);
} else {
console.log(lastNameError.message); // Fallback to default error message
}
}
}
```
================================================
FILE: doc/recipes/composite-keys.md
================================================
# Composite keys
Composite (compound) keys are fully supported. Just give an array of columns where you would normally give a single column name. Composite primary key can be specified by setting an array of column names to the [idColumn](/api/model/static-properties.html#static-idcolumn) of a model class.
Here's a list of methods that may help working with composite keys:
- [whereComposite](/api/query-builder/find-methods.html#wherecomposite)
- [whereInComposite](/api/query-builder/find-methods.html#whereincomposite)
- [findById](/api/query-builder/find-methods.html#findbyid)
- [findByIds](/api/query-builder/find-methods.html#findbyids)
- [deleteById](/api/query-builder/mutate-methods.html#deletebyid)
- [updateAndFetchById](/api/query-builder/mutate-methods.html#updateandfetchbyid)
- [patchAndFetchById](/api/query-builder/mutate-methods.html#patchandfetchbyid)
- [\$id](/api/model/instance-methods.html#id)
## Examples
Specifying a composite primary key for a model:
```js
class Person extends Model {
static get idColumn() {
return ['firstName', 'lastName', 'dateOfBirth'];
}
}
```
Specifying a relation using a composite primary key and a composite foreign key:
```js
class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
pets: {
relation: Model.BelongsToOneRelation,
modelClass: Animal,
join: {
from: [
'persons.firstName',
'persons.lastName',
'persons.dateOfBirth'
],
to: [
'animals.ownerFirstName',
'animals.ownerLastName',
'animals.ownerDateOfBirth'
]
}
}
};
}
}
```
[findById](/api/query-builder/find-methods.html#findbyid):
```js
await Person.query().findById([1, 'something', 7]);
```
[whereComposite](/api/query-builder/find-methods.html#wherecomposite):
```js
await Person.query().whereComposite(['foo', 'bar'], [1, 'barValue']);
```
[whereInComposite](/api/query-builder/find-methods.html#whereincomposite):
```js
await Person.query().whereInComposite(
['foo', 'bar'],
[
[1, 'barValue1'],
[2, 'barValue2']
]
);
```
================================================
FILE: doc/recipes/custom-id-column.md
================================================
# Custom id column
Name of the identifier column can be changed by setting the static [idColumn](/api/model/static-properties.html#static-idcolumn) property of a model class. Composite key can be defined by using an array of column names.
```js
class Person extends Model {
static get idColumn() {
return 'person_id';
}
}
```
Composite key:
```js
class Person extends Model {
static get idColumn() {
return ['someColumn', 'someOtherColumn'];
}
}
```
================================================
FILE: doc/recipes/custom-query-builder.md
================================================
# Custom query builder (extending the query builder)
You can extend the [QueryBuilder](/api/query-builder/) returned by [query()](/api/model/static-methods.html#static-query), [relatedQuery()](/api/model/static-methods.html#static-relatedquery), [\$relatedQuery()](/api/model/instance-methods.html#relatedquery) and [\$query()](/api/model/instance-methods.html#query) methods (and all other methods that create a [QueryBuilder](/api/query-builder/)) by setting the model class's static [QueryBuilder](/api/model/static-methods.html#static-querybuilder) property.
```js
// MyQueryBuilder.js
const { QueryBuilder } = require('objection');
class MyQueryBuilder extends QueryBuilder {
myCustomMethod(something) {
doSomething(something);
return this;
}
}
// Person.js
const { MyQueryBuilder } = require('./MyQueryBuilder');
class Person extends Model {
static get QueryBuilder() {
return MyQueryBuilder;
}
}
```
Now you can do this:
```js
await Person.query().where('id', 1).myCustomMethod(1).where('foo', 'bar');
```
If you want to set the custom query builder for all model classes you can just set the [QueryBuilder](/api/model/static-methods.html#static-querybuilder) property of the [Model](/api/model/) base class. A cleaner option would be to create your own Model subclass, set its [QueryBuilder](/api/query-builder/) property and inherit all your models from the custom Model class.
```js
// BaseModel.js
const { MyQueryBuilder } = require('./MyQueryBuilder');
class BaseModel extends Model {
static get QueryBuilder() {
return MyQueryBuilder;
}
}
// Person.js
const { BaseModel } = require('./BaseModel');
// Person now uses MyQueryBuilder
class Person extends BaseModel {}
```
Whenever a [QueryBuilder](/api/query-builder/) instance is created it is done by calling the static [query()](/api/model/static-methods.html#static-query) method. If you don't need to add methods, but simply modify the query, you can override the [query()](/api/model/static-methods.html#static-query).
```js
class BaseModel extends Model {
static query(...args) {
const query = super.query(...args);
// Somehow modify the query.
return query.runAfter((result) => {
console.log(this.name, 'got result', result);
return result;
});
}
}
```
::: tip
TIP: Consider using [modifiers](/recipes/modifiers.html#usage-in-a-query) instead of extending the query builder. You can often achieve the same flexibility with both.
:::
# Extending the query builder in typescript
With typescript, you need to add some extra type properties for the custom query builder. These are necessary until typescript fully supports our use case. The good news is that you only need to define them once for the shared `BaseModel`. If you don't already have one, it's time to create it.
```ts
import { Model, Page } from 'objection';
class MyQueryBuilder extends QueryBuilder {
// These are necessary. You can just copy-paste them and change the
// name of the query builder class.
ArrayQueryBuilderType!: MyQueryBuilder;
SingleQueryBuilderType!: MyQueryBuilder;
MaybeSingleQueryBuilderType!: MyQueryBuilder;
NumberQueryBuilderType!: MyQueryBuilder;
PageQueryBuilderType!: MyQueryBuilder>;
myCustomMethod(something: number): this {
doSomething(something);
return this;
}
}
class BaseModel extends Model {
// Both of these are needed.
QueryBuilderType!: MyQueryBuilder;
static QueryBuilder = MyQueryBuilder;
}
```
Now all models you inherit from `BaseModel` use `MyQueryBuilder` as a query builder.
```js
class Person extends BaseModel {
static tableName = 'persons';
}
await Person.query().where('id', 1).myCustomMethod(1).where('foo', 'bar');
```
::: tip
TIP: Consider using [modifiers](/recipes/modifiers.html#usage-in-a-query) instead of extending the query builder. You can often achieve the same flexibility with both.
:::
================================================
FILE: doc/recipes/custom-validation.md
================================================
# Custom validation
If you want to use the json schema validation but add some custom validation on top of it you can override the [\$beforeValidate](/api/model/instance-methods.html#beforevalidate) or [\$afterValidate](/api/model/instance-methods.html#aftervalidate) method.
If you need to do validation on insert or update you can throw exceptions from the [\$beforeInsert](/api/model/instance-methods.html#beforeinsert) and [\$beforeUpdate](/api/model/instance-methods.html#beforeupdate) methods.
If you don't want to use the built-in json schema validation, you can just ignore the [jsonSchema](/api/model/instance-methods.html#jsonschema) property. It is completely optional. If you want to use some other validation library you need to implement a custom [Validator](/api/types/#class-validator) (see the example).
## Examples
Additional validation:
```js
class Person extends Model {
$beforeInsert() {
if (this.id) {
throw new objection.ValidationError({
message: 'identifier should not be defined before insert',
type: 'MyCustomError',
data: someObjectWithSomeData
});
}
}
}
```
Modifying the [Ajv](https://github.com/epoberezkin/ajv) based JSON schema validation:
```js
const AjvValidator = require('objection').AjvValidator;
class Model {
static createValidator() {
return new AjvValidator({
onCreateAjv: ajv => {
// Here you can modify the `Ajv` instance.
},
options: {
allErrors: true,
validateSchema: false,
ownProperties: true,
v5: true
}
});
}
}
```
Replace JSON schema validation with any other validation scheme by implementing a custom [Validator](/api/types/#class-validator):
```js
// MyCustomValidator.js
const { Validator } = require('objection');
class MyCustomValidator extends Validator {
validate(args) {
// The model instance. May be empty at this point.
const model = args.model;
// The properties to validate. After validation these values will
// be merged into `model` by objection.
const json = args.json;
// `ModelOptions` object. If your custom validator sets default
// values or has the concept of required properties, you need to
// check the `opt.patch` boolean. If it is `true` we are validating
// a patch object (an object with a subset of model's properties).
const opt = args.options;
// A context object shared between the validation methods. A new
// object is created for each validation operation. You can store
// whatever you need in this object.
const ctx = args.ctx;
// Do your validation here and throw any exception if the
// validation fails.
doSomeValidationAndThrowIfFails(json);
// You need to return the (possibly modified) json.
return json;
}
beforeValidate(args) {
// Takes the same arguments as `validate`. Usually there is no need
// to override this.
return super.beforeValidate(args);
}
afterValidate(args) {
// Takes the same arguments as `validate`. Usually there is no need
// to override this.
return super.afterValidate(args);
}
}
// BaseModel.js
const Model = require('objection').Model;
// Override the `createValidator` method of a `Model` to use the
// custom validator.
class BaseModel extends Model {
static createValidator() {
return new MyCustomValidator();
}
}
```
================================================
FILE: doc/recipes/default-values.md
================================================
# Default values
You can set the default values for properties using the `default` property in [jsonSchema](/api/model/static-properties.html#static-jsonschema).
```js
class Person extends Model {
static get jsonSchema() {
return {
type: 'object',
properties: {
gender: {
type: 'string',
enum: ['Male', 'Female', 'Other'],
default: 'Female'
}
}
};
}
}
```
Note that you can also set default values in the database. See the documentation of knex and the appropriate database engine for more info. If you need to set dynamic default values, you can use the [\$beforeInsert](/api/model/instance-methods.html#beforeinsert) hook.
================================================
FILE: doc/recipes/error-handling.md
================================================
# Error handling
Objection throws four kinds of errors:
1. [ValidationError](/api/types/#class-validationerror) when an input that could come from the outside world is invalid. These inputs
include model instances and POJOs, relation expressions, object graphs etc. [ValidationError](/api/types/#class-validationerror) has
a `type` property that can be used to distinguish between the different error types.
2. [NotFoundError](/api/types/#class-notfounderror) when [throwIfNotFound](/api/query-builder/other-methods.html#throwifnotfound) was called for a query and no
results were found.
3. Database errors as defined by the [db-errors library](https://github.com/Vincit/db-errors). You can access the error classes through objection. See the example.
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.
## Examples
An example error handler function that handles all possible errors. Note that you should never send the errors directly to the client as they may contain SQL and other information that reveals too much about the inner workings of your app.
```js
const {
ValidationError,
NotFoundError,
DBError,
ConstraintViolationError,
UniqueViolationError,
NotNullViolationError,
ForeignKeyViolationError,
CheckViolationError,
DataError
} = require('objection');
// In this example `res` is an express response object.
function errorHandler(err, res) {
if (err instanceof ValidationError) {
switch (err.type) {
case 'ModelValidation':
res.status(400).send({
message: err.message,
type: err.type,
data: err.data
});
break;
case 'RelationExpression':
res.status(400).send({
message: err.message,
type: 'RelationExpression',
data: {}
});
break;
case 'UnallowedRelation':
res.status(400).send({
message: err.message,
type: err.type,
data: {}
});
break;
case 'InvalidGraph':
res.status(400).send({
message: err.message,
type: err.type,
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: {}
});
}
}
```
================================================
FILE: doc/recipes/extra-properties.md
================================================
# Join table extra properties
Sometimes when you have a many-to-many relationship, you want to store some properties in the join (pivot) table and still join them with the related objects. In objection, these properties can be defined as `extra` properties of many-to-many relationship.
Let's consider a schema like this:
```js
exports.up = knex => {
return knex.schema
.createTable('actors', table => {
table.increments('id').primary();
table.string('name');
})
.createTable('movies', table => {
table.increments('id').primary();
table.string('name');
})
.createTable('actors_movies', table => {
table.integer('actorId').references('actors.id');
table.integer('movieId').references('movies.id');
// The actor's character's name in the movie.
table.string('characterName');
});
};
```
In this schema, `characterName` is the `extra` property. When we fetch movies for an actor, we want the movie objects to contain the `characterName` in addition to normal movie properties.
You can define your [relationMapping](/api/model/static-properties.html#static-relationmappings) like this:
```js
class Actor extends Model {
static get relationMappings() {
return {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'actors.id',
through: {
from: 'actors_movies.actorId',
to: 'actors_movies.movieId',
extra: ['characterName']
},
to: 'movies.id'
}
};
}
}
```
You can give a different name for the property in the result by providing an object:
```js
class Actor extends Model {
static get relationMappings() {
return {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'actors.id',
through: {
from: 'actors_movies.actorId',
to: 'actors_movies.movieId',
extra: {
// Here `character` is the name that will appear in the model object
// and 'characterName' is the name of the column in the db.
character: 'characterName'
}
},
to: 'movies.id'
}
};
}
}
```
`extra` properties automatically work with all objection operations:
```js
const linda = await Actor.query().findOne({ name: 'Linda Hamilton' });
// Fetch a movie.
const someMovie = await linda.$relatedQuery('movies').first();
console.log(
"Linda's character's name in the movie",
someMovie.name,
'is',
someMovie.characterName
);
// Insert a movie with a `characterName`.
await linda.$relatedQuery('movies').insert({
name: 'Terminator',
characterName: 'Sarah Connor'
});
// Relate an existing movie with a `characterName`.
await linda.$relatedQuery('movies').relate({
id: 23452,
characterName: 'Sarah Connor'
});
// Update a movie and change `characterName`
await linda
.$relatedQuery('movies')
.patch({ characterName: 'Florence' })
.where('movies.name', 'Curvature');
```
`extra` properties also work with [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) [insertGraph](/api/query-builder/mutate-methods.html#insertgraph) and [upsertGraph](/api/query-builder/mutate-methods.html#upsertgraph).
================================================
FILE: doc/recipes/indexing-postgresql-jsonb-columns.md
================================================
# Indexing PostgreSQL JSONB columns
Good reading on the subject:
- [JSONB type performance in PostgreSQL 9.4](https://www.enterprisedb.com/blog/jsonb-type-performance-postgresql-94) and
- [Postgres 9.4 feature highlight - Indexing JSON data with jsonb data type](http://paquier.xyz/postgresql-2/postgres-9-4-feature-highlight-indexing-jsonb/).
## General Inverted Indexes a.k.a. GIN
This is the index type which makes all JSONB set operations fast. All `isSuperset` / `isSubset` / `hasKeys` / `hasValues` etc. queries can use this index to speed ’em up. Usually this is the index you want and it may take around 30% extra space on the DB server.
If one likes to use only the subset/superset operators with faster and smaller index one can give an extra `path_ops` parameter when creating the index: [“The path_ops index supports only the search path operator `@>` (see below), but produces a smaller and faster index for these kinds of searches.”](https://wiki.postgresql.org/wiki/What's_new_in_PostgreSQL_9.4). According to Marco Nenciarini’s post the speed up can be over 600% compared to full GIN index and the size of the index is reduced from ~30% -> ~20%.
Full GIN index to speed up all type of json queries:
```js
.raw('CREATE INDEX on ?? USING GIN (??)', ['Hero', 'details'])
```
Partial GIN index to speed up all subset / superset type of json queries:
```js
.raw('CREATE INDEX on ?? USING GIN (?? jsonb_path_ops)', ['Place', 'details'])
```
## Index on Expression
Another type of index one may use for JSONB field is to create an expression index for example for a certain JSON field inside a column.
You might want to use these if you are using lots of `.where(ref('jsonColumn:details.name').castText(), 'marilyn')` type of queries, which cannot be sped up with GIN index.
Use of these indexes are more limited, but they are also somewhat faster than using GIN and querying e.g. `{ field: value }` with subset operator. GIN indices also takes a lot of space in compared to expression index for certain field. So if you want to make just certain query to go extra fast you may consider using index on expression.
An expression index referring an internal `details.name` attribute of an object stored in `jsonColumn`:
```js
.raw("CREATE INDEX on ?? ((??#>>'{details,name}'))", ['Hero', 'jsonColumn'])
```
## Complete Migration Example and Created Tables / Indexes
Complete example how to try out different index choices.
Migration:
```js
exports.up = knex => {
return knex.schema
.createTable('Hero', table => {
table.increments('id').primary();
table.string('name');
table.jsonb('details');
table
.integer('homeId')
.unsigned()
.references('id')
.inTable('Place');
})
.raw('CREATE INDEX on ?? USING GIN (??)', ['Hero', 'details'])
.raw("CREATE INDEX on ?? ((??#>>'{type}'))", ['Hero', 'details'])
.createTable('Place', table => {
table.increments('id').primary();
table.string('name');
table.jsonb('details');
})
.raw('CREATE INDEX on ?? USING GIN (?? jsonb_path_ops)', [
'Place',
'details'
]);
};
```
Results following schema:
```sql
objection-jsonb-example=# \d "Hero"
Table "public.Hero"
Column | Type
---------+------------------------
id | integer
name | character varying(255)
details | jsonb
homeId | integer
Indexes:
"Hero_pkey" PRIMARY KEY, btree (id)
"Hero_details_idx" gin (details)
"Hero_expr_idx" btree ((details #>> '{type}'::text[]))
objection-jsonb-example=# \d "Place"
Table "public.Place"
Column | Type
---------+------------------------
id | integer
name | character varying(255)
details | jsonb
Indexes:
"Place_pkey" PRIMARY KEY, btree (id)
"Place_details_idx" gin (details jsonb_path_ops)
```
Expression index is used for example for following query:
```sql
explain select * from "Hero" where details#>>'{type}' = 'Hero';
QUERY PLAN
----------------------------------------------------------------
Index Scan using "Hero_expr_idx" on "Hero"
Index Cond: ((details #>> '{type}'::text[]) = 'Hero'::text)
```
================================================
FILE: doc/recipes/joins.md
================================================
# Joins
Again, [do as you would with a knex query builder](https://knexjs.org/guide/query-builder.html#join):
```js
const people = await Person.query()
.select('persons.*', 'parent.firstName as parentName')
.join('persons as parent', 'persons.parentId', 'parent.id');
console.log(people[0].parentName);
```
Objection also has helpers like the [joinRelated](/api/query-builder/join-methods.html#joinrelated) method family:
```js
const people = await Person.query()
.select('parent:parent.name as grandParentName')
.joinRelated('parent.parent');
console.log(people[0].grandParentName);
```
================================================
FILE: doc/recipes/json-queries.md
================================================
# JSON queries
You can use the [ref](/api/objection/#ref) function from the main module to refer to json columns in queries. There is also a bunch of query building methods that have `Json` in their names. Check them out too.
See [FieldExpression](/api/types/#type-fieldexpression) for more information about how to refer to json fields.
Json queries currently only work with postgres.
```js
const { ref } = require('objection');
await Person.query()
.select([
'id',
ref('jsonColumn:details.name')
.castText()
.as('name'),
ref('jsonColumn:details.age')
.castInt()
.as('age')
])
.join(
'animals',
ref('persons.jsonColumn:details.name').castText(),
'=',
ref('animals.name')
)
.where('age', '>', ref('animals.jsonData:details.ageLimit'));
```
Individual json fields can be updated like this:
```js
await Person.query().patch({
'jsonColumn:details.name': 'Jennifer',
'jsonColumn:details.age': 29
});
```
`withGraphJoined` and `joinRelated` methods also use `:` as a separator which can lead to ambiquous queries when combined with json references. For example:
```
jsonColumn:details.name
```
Can mean two things:
1. column `name` of the relation `jsonColumn.details`
2. field `name` of the `details` object inside `jsonColumn` column
When used with `withGraphJoined` and `joinRelated` you can use the `from` method of the `ReferenceBuilder` to specify the table:
```js
await Person.query()
.withGraphJoined('children.children')
.where(ref('jsonColumn:details.name').from('children:children'), 'Jennifer');
```
================================================
FILE: doc/recipes/modifiers.md
================================================
# Modifiers
Modifiers allow you to easily reuse snippets of query logic. A modifier is simply a function that takes a [QueryBuilder](/api/query-builder/) as the first argument and optionally any number of arguments after that. Modifier functions can then mutate the query passed in, but they must always be synchronous. Here's a contrived example:
```js
function filterGender(query, gender) {
query.where('gender', gender);
}
```
Model classes have the static [modifiers](/api/model/static-properties.md#static-modifiers) property that can be used to store these modifiers:
```js
class Person extends Model {
static modifiers = {
defaultSelects(query) {
query.select('id', 'firstName');
},
filterGender(query, gender) {
query.where('gender', gender);
}
};
}
```
Modifiers defined in the [modifiers](/api/model/static-properties.md#static-modifiers) object can then be used in many ways.
## Usage in a query
You can apply any modifier using the [modify](/api/query-builder/other-methods.md#modify) method:
```js
const women = await Person.query()
.modify('defaultSelects')
.modify('filterGender', 'female');
```
## Usage with eager loading
You can pass modifier names as "arguments" to the relation names in [relation expressions](/api/types/#type-relationexpression). See the [withGraphFetched](/api/query-builder/eager-methods.html#withgraphfetched) and [withGraphJoined](/api/query-builder/eager-methods.html#withgraphjoined) methods' docs for more info and examples.
```js
const people = await Person.query().withGraphFetched(
'children(defaultSelects)'
);
```
Note that you can only use modifiers registered for the relation's model class. In the previous example `children` is of class `Person`, so you can use `defaultSelects` that was registered for the `Person` model. In the following example, `filterDogs` must have been specified in `Pet` model's `modifiers` object.
```js
const people = await Person.query().withGraphFetched(
'[children(defaultSelects), pets(filterDogs)]'
);
```
You can register new modifiers for a query using the [modifiers](/api/query-builder/other-methods.md#modifiers) query builder method. This also allows you to bind arguments to existing modifiers like this
```js
const people = await Person.query()
.withGraphFetched('children(defaultSelects, filterWomen)')
.modifiers({
filterWomen: query => query.modify('filterGender', 'female')
});
```
## Usage in joinRelated
```js
const women = await Person.query().joinRelated('children(defaultSelects)');
```
Query builder [modifiers](/api/query-builder/other-methods.md#modifiers) can also be used with `joinRelated` just like with `withGraphFetched` and `withGraphJoined`.
## Other usages
- Relation mappings' [modify](/api/types/#type-relationmapping) properties.
## Modifier best practices
You can refer to column names using strings just like we did in the previous example and you won't run into trouble in most cases. In some cases however modifiers may be used in contexts where it's not clear to which table the column names refer and objection doesn't (and in many cases could not) deduce that information from the query. If you want to play it safe, and make the modifiers usable in as many situations as possible, you can use the [Model.ref](/api/model/static-methods.md#static-ref) helper refer to columns. Like this:
```js
class Person extends Model {
static modifiers = {
defaultSelects(query) {
const { ref } = Person;
query.select(ref('id'), ref('firstName'));
},
filterGender(query, gender) {
const { ref } = Person;
query.where(ref('gender'), gender);
}
};
}
```
When `ref` is used, the modifiers work even when you have specified an alias for a table in a query.
================================================
FILE: doc/recipes/multitenancy-using-multiple-databases.md
================================================
# Multitenancy using multiple databases
By default, the examples guide you to setup the database connection by calling [Model.knex(knex)](/api/model/static-methods.html#static-knex). This doesn't fly if you want to select the database based on the request as it sets the connection globally. There are (at least) two patterns for dealing with this kind of setup:
_NOTE:_ The following patterns don't work if you have a large amount of tenants since we need to create a knex instance for each of them. In those cases you probably shouldn't be creating a separate database for each tenant anyway.
## Model binding pattern
If you have a different database for each tenant, a useful pattern is to add a middleware that adds the models to `req.models` hash and then _always_ use the models through `req.models` instead of requiring them directly. What [bindKnex](/api/model/static-properties.html#static-bindknex) method actually does is that it creates an anonymous subclass of the model class and sets its knex connection. That way the database connection doesn't change for the other requests that are currently being executed.
```js
const Knex = require('knex');
const knexCache = new Map();
app.use((req, res, next) => {
// Function that parses the tenant id from path, header, query parameter etc.
// and returns an instance of knex. You should cache the knex instances and
// not create a new one for each query. Knex takes care of connection pooling.
const knex = getKnexForRequest(req, knexCache);
req.models = {
Person: Person.bindKnex(knex),
Movie: Movie.bindKnex(knex),
Animal: Animal.bindKnex(knex)
};
next();
});
function getKnexForRequest(req, knexCache) {
// If you pass the tenantIs a query parameter, you would do something
// like this.
let tenantId = req.query.tenantId;
let knex = knexCache.get(tenantId);
if (!knex) {
knex = Knex(knexConfigForTenant(tenantId));
knexCache.set(tenantId, knex);
}
return knex;
}
function knexConfigForTenant(tenantId) {
return {
// The correct knex config object for the given tenant.
};
}
app.get('/people', async (req, res) => {
const { Person } = req.models;
const people = await Person.query().findById(req.params.id);
res.send(people);
});
```
## Knex passing pattern
Another option is to add the knex instance to the request using a middleware and not bind models at all (not even using [Model.knex()](/api/model/static-methods.html#static-knex)). The knex instance can be passed to [query](/api/model/static-methods.html#static-query), [\$query](/api/model/instance-methods.html#query), and [\$relatedQuery](/api/model/instance-methods.html#relatedquery) methods as the last argument. This pattern forces you to design your services and helper methods in a way that you always need to pass in a knex instance. A great thing about this is that you can pass a transaction object instead. (the knex/objection transaction object is a query builder just like the normal knex instance). This gives you a fine grained control over your transactions.
```js
app.use((req, res, next) => {
// This function is defined in the previous example.
req.knex = getKnexForRequest(req);
next();
});
app.get('/people', async (req, res) => {
const people = await Person.query(req.knex).findById(req.params.id);
res.send(people);
});
```
================================================
FILE: doc/recipes/paging.md
================================================
# Paging
Most of the queries can be paged using the [page](/api/query-builder/other-methods.html#page) or [range](/api/query-builder/other-methods.html#range) method.
```js
const result = await Person.query()
.where('age', '>', 20)
.page(5, 100);
console.log(result.results.length); // --> 100
console.log(result.total); // --> 3341
```
There are some cases where [page](/api/query-builder/other-methods.html#page) and [range](/api/query-builder/other-methods.html#range) don't work.
1. In [modifyGraph](/api/query-builder/other-methods.html#modifygraph) or modifiers:
```js
// This doesn't work because the query `qb` fetches the
// `children` for all parents at once. Paging that query
// will have not fetch 10 results for all parents, but
// instead 10 results in total.
const result = await Person.query()
.withGraphFetched('children')
.modifyGraph('children', qb => qb.page(0, 10));
```
2. When `withGraphJoined` is used:
```js
// This doesn't work because of the way SQL joins work.
// Databases return the nested relations as a flattened
// list of records. Paging the query will page the
// flattened results which has alot more rows than
// the root query.
const result = await Person.query()
.withGraphJoined('children')
.page(0, 10);
```
================================================
FILE: doc/recipes/plugins.md
================================================
# Plugins
## TypeScript Example
```ts
export function Mixin(options = {}) {
return function(Base: T) {
return class extends Base {
mixinMethod() {}
};
};
}
// Usage
class Person extends Model {}
const MixinPerson = Mixin(Person);
// Or as a decorator:
@Mixin
class Person extends Model {}
```
## TypeScript Example with Custom QueryBuilder
```ts
class CustomQueryBuilder extends QueryBuilder {
ArrayQueryBuilderType!: CustomQueryBuilder;
SingleQueryBuilderType!: CustomQueryBuilder;
NumberQueryBuilderType!: CustomQueryBuilder;
PageQueryBuilderType!: CustomQueryBuilder>;
someCustomMethod(): this {
return this;
}
}
export function CustomQueryBuilderMixin(options = {}) {
return function(Base: T) {
return class extends Base {
static QueryBuilder = QueryBuilder;
QueryBuilderType: CustomQueryBuilder;
mixinMethod() {}
};
};
}
// Usage
class Person extends Model {}
const MixinPerson = CustomQueryBuilderMixin(Person);
// Or as a decorator:
@CustomQueryBuilderMixin
class Person extends Model {}
async () => {
const z = await MixinPerson.query()
.whereIn('id', [1, 2])
.someCustomMethod()
.where('foo', 1)
.someCustomMethod();
z[0].mixinMethod();
};
```
================================================
FILE: doc/recipes/polymorphic-associations.md
================================================
# Polymorphic associations
Let's assume we have tables `Comment`, `Issue` and `PullRequest`. Both `Issue` and `PullRequest` can have a list of comments. `Comment` has a column `commentableId` to hold the foreign key and `commentableType` to hold the related model type.
```js
class Comment extends Model {
static get tableName() {
return 'comments';
}
}
class Issue extends Model {
static get tableName() {
return 'issues';
}
static get relationMappings() {
return {
comments: {
relation: Model.HasManyRelation,
modelClass: Comment,
filter(builder) {
builder.where('commentableType', 'Issue');
},
beforeInsert(model) {
model.commentableType = 'Issue';
},
join: {
from: 'issues.id',
to: 'comments.commentableId'
}
}
};
}
}
class PullRequest extends Model {
static get tableName() {
return 'pullrequests';
}
static get relationMappings() {
return {
comments: {
relation: Model.HasManyRelation,
modelClass: Comment,
filter(builder) {
builder.where('commentableType', 'PullRequest');
},
beforeInsert(model) {
model.commentableType = 'PullRequest';
},
join: {
from: 'pullrequests.id',
to: 'comments.commentableId'
}
}
};
}
}
```
The `where('commentableType', 'Type')` filter adds a `WHERE "commentableType" = 'Type'` clause to the relation fetch query. The `beforeInsert` hook takes care of setting the type on insert.
This kind of associations don't have referential integrity and should be avoided if possible. Instead, consider using the _exclusive arc table_ pattern discussed [here](https://github.com/Vincit/objection.js/issues/19#issuecomment-291621442).
================================================
FILE: doc/recipes/precedence-and-parentheses.md
================================================
# Precedence and parentheses
You can add parentheses to queries by passing a function to any of the [where\*](/api/query-builder/find-methods.html#where) methods.
```js
await Person.query()
.where('stuff', 1)
.where(builder => {
builder.where('foo', 2).orWhere('bar', 3);
});
```
The generated SQL:
```sql
select * from "persons" where "stuff" = 1 and ("foo" = 2 or "bar" = 3)
```
================================================
FILE: doc/recipes/raw-queries.md
================================================
# Raw queries
To mix raw SQL with queries, use the [raw](/api/objection/#raw) function from the main module. [raw](/api/objection/#raw) works just like the [knex's raw method](https://knexjs.org/guide/raw.html) but in addition, supports objection queries, [raw](/api/objection/#raw), [ref](/api/objection/#ref), [val](/api/objection/#val) and all other objection types. You can also use [knex.raw()](https://knexjs.org/guide/raw.html).
[raw](/api/objection/#raw) is handy when you want to mix SQL in objection queries, but if you want to fire off a completely custom query, you need to use [knex.raw](https://knexjs.org/guide/raw.html).
There are also some helper methods such as [whereRaw](/api/query-builder/find-methods.html#whereraw) in the [QueryBuilder](/api/query-builder/).
## Examples
```js
const { raw } = require('objection');
const ageToAdd = 10;
await Person.query().patch({
age: raw('age + ?', ageToAdd),
});
```
```js
const { raw } = require('objection');
const childAgeSums = await Person.query()
.select(raw('coalesce(sum(??), 0)', 'age').as('childAgeSum'))
.where(
raw(`?? || ' ' || ??`, 'firstName', 'lastName'),
'Arnold Schwarzenegger'
)
.orderBy(raw('random()'));
console.log(childAgeSums[0].childAgeSum);
```
Also see the [fn](/api/objection/#fn) helper for calling SQL functions. The following example is equivalent the previous one.
```js
const { fn, ref } = require('objection');
const childAgeSums = await Person.query()
.select(fn.coalesce(fn.sum(ref('age')), 0).as('childAgeSum'))
.where(
fn.concat(ref('firstName'), ' ', ref('lastName')),
'Arnold Schwarzenegger'
)
.orderBy(fn('random'));
console.log(childAgeSums[0].childAgeSum);
```
Binding arguments can be other [raw](/api/objection/#raw) instances, [QueryBuilders](/api/query-builder/) or pretty much anything you can think of.
```js
const { raw, ref } = require('objection');
const people = await Person
.query()
.alias('p')
.select(raw('array(?) as childIds', [
Person.query()
.select('id')
.where('id', ref('p.parentId'))
]);
console.log('child identifiers:', people[0].childIds)
```
Completely custom raw query using knex:
```js
const knex = Person.knex();
await knex.raw('SELECT 1');
```
================================================
FILE: doc/recipes/relation-subqueries.md
================================================
# Relation subqueries
Let's say you have a `Tweet` model and a `Like` model. `Tweet` has a `HasManyRelation` named `likes` to `Like` table. Now let's assume you'd like to fetch a list of `Tweet`s and get the number of likes for each of them without fetching the actual `Like` rows. This cannot be easily achieved using `withGraphFetched` because of the way the queries are optimized (you can read more [here](/api/query-builder/eager-methods.html#withgraphfetched)). You can leverage SQL's subqueries and the [relatedQuery](/api/model/static-methods.html#static-relatedquery) helper:
```js
const tweets = await Tweet.query().select(
'Tweet.*',
Tweet.relatedQuery('likes')
.count()
.as('numberOfLikes')
);
console.log(tweets[4].numberOfLikes);
```
The generated SQL is something like this:
```sql
select "Tweet".*, (
select count(*)
from "Like"
where "Like"."tweetId" = "Tweet"."id"
) as "numberOfLikes"
from "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](/api/model/static-methods.html#static-relatedquery) method works with all relations and not just `HasManyRelation`.
Another common use case for subqueries is selecting `Tweet`s that have one or more likes. That could also be achieved using joins, but it's often simpler to use a subquery. There should be no performance difference between the two methods on modern database engines.
```js
const tweets = await Tweet.query().whereExists(Tweet.relatedQuery('likes'));
```
The generated SQL is something like this:
```sql
select "Tweet".*
from "Tweet"
where exists (
select "Like".*
from "Like"
where "Like"."tweetId" = "Tweet"."id"
)
```
You can even use the common `select 1` optimization if you want (I'm fairly sure it's useless nowadays though):
```js
const tweets = await Tweet.query().whereExists(
Tweet.relatedQuery('likes').select(1)
);
```
The generated SQL is something like this:
```sql
select "Tweet".*
from "Tweet"
where exists (
select 1
from "Like"
where "Like"."tweetId" = "Tweet"."id"
)
```
================================================
FILE: doc/recipes/returning-tricks.md
================================================
# PostgreSQL "returning" tricks
Because PostgreSQL (and some others) support [returning('\*')](/api/query-builder/find-methods.html#returning) chaining, you can actually `insert` a row, or `update` / `patch` / `delete` existing rows, **and** receive the affected rows as Model instances in a single query, thus improving efficiency. See the examples for more clarity.
# Examples
Insert and return a Model instance in 1 query:
```js
const jennifer = await Person.query()
.insert({ firstName: 'Jennifer', lastName: 'Lawrence' })
.returning('*');
console.log(jennifer.createdAt); // NOW()-ish
console.log(jennifer.id); // Sequence ID
```
Update a single row by ID and return the updated Model instance in 1 query:
```js
const jennifer = await Person.query()
.patch({ firstName: 'Jenn', lastName: 'Lawrence' })
.where('id', 1234)
.returning('*')
.first();
console.log(jennifer.updatedAt); // NOW()-ish
console.log(jennifer.firstName); // "Jenn"
```
Patch a Model instance and receive DB updates to Model instance in 1 query:
```js
const updateJennifer = await jennifer
.$query()
.patch({ firstName: 'J.', lastName: 'Lawrence' })
.returning('*');
console.log(updateJennifer.updatedAt); // NOW()-ish
console.log(updateJennifer.firstName); // "J."
```
Delete all Persons named Jennifer and return the deleted rows as Model instances in 1 query:
```js
const deletedJennifers = await Person.query()
.delete()
.where({ firstName: 'Jennifer' })
.returning('*');
console.log(deletedJennifers.length); // How many Jennifers there were
console.log(deletedJennifers[0].lastName); // Maybe "Lawrence"
```
Delete all of Jennifer's dogs and return the deleted Model instances in 1 query:
```js
const jennsDeletedDogs = await jennifer
.$relatedQuery('pets')
.delete()
.where({ species: 'dog' })
.returning('*');
console.log(jennsDeletedDogs.length); // How many dogs Jennifer had
console.log(jennsDeletedDogs[0].name); // Maybe "Fido"
```
================================================
FILE: doc/recipes/snake-case-to-camel-case-conversion.md
================================================
# Snake case to camel case conversion
You may want to use snake_cased names in database and camelCased names in code. There are two ways to achieve this:
1. _Conversion in knex using [knexSnakeCaseMappers](/api/objection/#knexsnakecasemappers)_. When the conversion is done on knex level **everything** is converted to camel case including properties and identifiers in [relationMappings](/api/model/static-properties.html#static-relationmappings) and queries. When this method is used, objection has no idea the database is defined in snake case. All it ever sees is camel cased properties, identifiers and tables. This is because the conversion is done using knex's `postProcessResponse` and `wrapIdentifier` hooks which are executed by knex before objection receives the data.
2. _Conversion in objection using [snakeCaseMappers](/api/objection/#snakecasemappers)_. When the conversion is done on objection level only database columns of the returned rows (model instances) are convered to camel case. You still need to use snake case in [relationMappings](/api/model/static-properties.html#static-relationmappings) and queries. Note that [insert](/api/query-builder/mutate-methods.html#insert), [patch](/api/query-builder/mutate-methods.html#patch), [update](/api/query-builder/mutate-methods.html#update) and their variants still take objects in camel case. The reasoning is that objects passed to those methods usually come from the client that also uses camel case.
Let's assume this is our schema:
```js
exports.up = knex => {
return knex.schema.createTable('persons_table', table => {
table.increments('id_column').primary();
table.string('first_name');
table.string('last_name');
table.integer('parent_id').references('persons_table.id_column');
});
};
exports.down = knex => {
return knex.schema.dropTableIfExists('persons_table');
};
```
**knexSnakeCaseMappers:**
See [here](/api/objection/#knexsnakecasemappers) for the full list of options that can be passed to `knexSnakeCaseMappers`.
```js
const Knex = require('knex');
const { Model, knexSnakeCaseMappers } = require('objection');
const knex = Knex({
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test'
}
// If your columns are UPPER_SNAKE_CASE you can use
// knexSnakeCaseMappers({ upperCase: true })
...knexSnakeCaseMappers()
});
...
// When `knexSnakeCaseMappers` is used, you need to define tables,
// columns and relation mappings using camelCase.
class Person extends Model {
static get tableName() {
return 'personsTable';
}
static get idColumn() {
return 'idColumn';
}
static get relationMappings() {
return {
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'personsTable.parentId',
to: 'personsTable.idColumn'
}
}
};
}
}
...
// All column names in queries need to be camel case too.
await Person.query().where('firstName', 'Jennifer');
```
**snakeCaseMappers:**
See [here](/api/objection/#snakecasemappers) for the full list of options that can be passed to `snakeCaseMappers`.
```js
const { Model, snakeCaseMappers } = require('objection');
// When `snakeCaseMappers` is used, you still define tables,
// columns and relation mappings using snake_case.
class Person extends Model {
static get columnNameMappers() {
// If your columns are UPPER_SNAKE_CASE you can
// use snakeCaseMappers({ upperCase: true })
return snakeCaseMappers();
}
static get tableName() {
return 'persons_table';
}
static get idColumn() {
return 'id_column';
}
static get relationMappings() {
return {
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'persons_table.parent_id',
to: 'persons_table.id_column'
}
}
};
}
}
...
// Queries need to use the database casing.
await Person.query().where('first_name', 'Jennifer');
```
================================================
FILE: doc/recipes/subqueries.md
================================================
# Subqueries
Subqueries can be written just like in knex: by passing a function in place of a value. A bunch of query building methods accept a function. See the knex.js documentation or just try it out. A function is accepted in most places you would expect. You can also pass [QueryBuilder](/api/query-builder/) instances or knex queries instead of functions.
Using a function:
```js
const peopleOlderThanAverage = await Person.query().where(
'age',
'>',
builder => {
builder.avg('age').from('persons');
}
);
console.log(peopleOlderThanAverage);
```
Using a [QueryBuilder](/api/query-builder/):
```js
const peopleOlderThanAverage = await Person.query().where(
'age',
'>',
Person.query().avg('age')
);
console.log(peopleOlderThanAverage);
```
You can use [ref](/api/objection/#ref) to reference the parent query in subqueries:
```js
const { ref } = require('objection');
const peopleWithPetCount = await Person.query().select([
'persons.*',
Pet.query()
.where('ownerId', ref('persons.id'))
.count()
.as('petCount')
]);
console.log(peopleWithPetCount[4].petCount);
```
The above query can also be written using the [relatedQuery](/api/model/static-methods.html#static-relatedquery) (assuming a relation `pets` has been defined for `Person`):
```js
const peopleWithPetCount = await Person.query().select([
'persons.*',
Person.relatedQuery('pets')
.count()
.as('petCount')
]);
console.log(peopleWithPetCount[4].petCount);
```
================================================
FILE: doc/recipes/ternary-relationships.md
================================================
# Ternary relationships
Assume we have the following Models:
1. user `(id, first_name, last_name)`
1. group `(id, name)`
1. permission `(id, label)`
1. user_group_permission `(user_id, group_id, permission_id, extra_attribute)`
Here's how you could create your models:
```js
// User.js
const { Model } = require('objection');
class User extends Model {
static get tableName() {
return 'user';
}
static get relationMappings() {
return {
groups: {
relation: Model.ManyToManyRelation,
modelClass: require('./Group'),
join: {
from: 'user.id',
through: {
from: 'user_group_permission.user_id',
extra: ['extra_attribute'],
to: 'user_group_permission.group_id'
},
to: 'group.id'
}
},
permissions: {
relation: Model.ManyToManyRelation,
modelClass: require('./Permission'),
join: {
from: 'user.id',
through: {
from: 'user_group_permission.user_id',
extra: ['extra_attribute'],
to: 'user_group_permission.permission_id'
},
to: 'permission.id'
}
}
};
}
}
module.exports = User;
```
```js
// Group.js
const { Model } = require('objection');
class Group extends Model {
static get tableName() {
return 'group';
}
static get relationMappings() {
return {
users: {
relation: Model.ManyToManyRelation,
modelClass: require('./User'),
join: {
from: 'group.id',
through: {
from: 'user_group_permission.group_id',
extra: ['extra_attribute'],
to: 'user_group_permission.user_id'
},
to: 'user.id'
}
},
permissions: {
relation: Model.ManyToManyRelation,
modelClass: require('./Permission'),
join: {
from: 'group.id',
through: {
from: 'user_group_permission.group_id',
extra: ['extra_attribute'],
to: 'user_group_permission.permission_id'
},
to: 'permission.id'
}
}
};
}
}
module.exports = Group;
```
```js
// Permission.js
const { Model } = require('objection');
class Permission extends Model {
static get tableName() {
return 'permission';
}
static get relationMappings() {
return {
users: {
relation: Model.ManyToManyRelation,
modelClass: require('./User'),
join: {
from: 'permission.id',
through: {
from: 'user_group_permission.permission_id',
extra: ['extra_attribute'],
to: 'user_group_permission.user_id'
},
to: 'user.id'
}
},
groups: {
relation: Model.ManyToManyRelation,
modelClass: require('./Group'),
join: {
from: 'permission.id',
through: {
from: 'user_group_permission.permission_id',
extra: ['extra_attribute'],
to: 'user_group_permission.group_id'
},
to: 'group.id'
}
}
};
}
}
module.exports = Permission;
```
```js
// UserGroupPermission.js
const { Model } = require('objection');
class UserGroupPermission extends Model {
static get tableName() {
return 'user_group_permission';
}
static get idColumn() {
return ['user_id', 'group_id', 'permission_id'];
}
static get relationMappings() {
return {
user: {
relation: Model.BelongsToOneRelation,
modelClass: require('./User'),
join: {
from: 'user_group_permission.user_id',
to: 'user.id'
}
},
group: {
relation: Model.BelongsToOneRelation,
modelClass: require('./Group'),
join: {
from: 'user_group_permission.group_id',
to: 'group.id'
}
},
permission: {
relation: Model.BelongsToOneRelation,
modelClass: require('./Permission'),
join: {
from: 'user_group_permission.permission_id',
to: 'permission.id'
}
}
};
}
}
module.exports = UserGroupPermission;
```
Here's how you can query your models:
- `.*JoinRelated()`
```js
UserGroupPermission.query()
.select('first_name', 'last_name', 'label', 'extra_attribute')
.joinRelated('[user, permission]')
.where('group_id', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
/*
{
first_name: ... ,
last_name: ... ,
label: ... ,
extra_attribute: ...
}
*/
```
- `.withGraphFetched()`
```js
UserGroupPermission.query()
.withGraphFetched('[user, permission]')
.where('group_id', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
/*
{
user: {
first_name: ... ,
last_name: ...
},
group: {
name: ...
},
permission: {
label: ...
},
extra_attribute: ...
}
*/
```
Read more about ternary relationships on [this issue](https://github.com/Vincit/objection.js/issues/179).
================================================
FILE: doc/recipes/timestamps.md
================================================
# Timestamps
You can implement the [\$beforeInsert](/api/model/instance-methods.html#beforeinsert) and [\$beforeUpdate](/api/model/instance-methods.html#beforeupdate) methods to set the timestamps. If you want to do this for all your models, you can simply create common base class that implements these methods.
```js
class Person extends Model {
$beforeInsert() {
this.created_at = new Date().toISOString();
}
$beforeUpdate() {
this.updated_at = new Date().toISOString();
}
}
```
================================================
FILE: doc/release-notes/changelog.md
================================================
# Changelog
## 3.1.5
### What's new
- Types: Fix generic static `this` [#2533](https://github.com/Vincit/objection.js/pull/2533)
- Types: Add `fromRaw` to `FromSelector regex [#2628](https://github.com/Vincit/objection.js/issues/2628)
- Types: Fix argument types of `onConflict()` [#2635](https://github.com/Vincit/objection.js/pull/2635)
- Types: Make `trx` optional for `Model.transaction(trx, cb)` [#2694](https://github.com/Vincit/objection.js/pull/2694)
## 3.1.4
### What's new
- Fix `upsertGraph()` `$beforeUpdate()` calls on empty relates [#2605](https://github.com/Vincit/objection.js/issues/2605)
- Don't call `onError()` with internal exceptions [#2603](https://github.com/Vincit/objection.js/issues/2603)
- Remove docs and typings for nonexistent `$pick()`
- Make `$omitFromJson()` + `$omitFromDatabaseJson()` compatible with old `$omit()` syntax
## 3.1.3
### What's new
- Revert generic constructor type change [#2531](https://github.com/Vincit/objection.js/issues/2531), [#2399](https://github.com/Vincit/objection.js/pull/2399)
### What's new
- Patch Validator: Prevent recursion on inner properties [#2520](https://github.com/Vincit/objection.js/pull/2520)
## 3.1.2
### What's new
- Patch Validator: Prevent recursion on inner properties [#2520](https://github.com/Vincit/objection.js/pull/2520)
## 3.1.1
### What's new
- Only add Ajv formats if they weren't added in user-land already [#2482](https://github.com/Vincit/objection.js/pull/2482)
## 3.1.0
### What's new
- Support `$beforeUpdate()` mutations in `upsertGraph()` [#2233](https://github.com/Vincit/objection.js/issues/2233)
- Remove deprecated `$afterGet()` hook [#2477](https://github.com/Vincit/objection.js/pull/2477)
- Drop support for Node v12 [#2478](https://github.com/Vincit/objection.js/pull/2478)
## 3.0.5
### What's new
- Fixes [#2183](https://github.com/Vincit/objection.js/pull/2183)
- Fixes [#2257](https://github.com/Vincit/objection.js/pull/2257)
- Fixes [#2276](https://github.com/Vincit/objection.js/pull/2276)
- Fixes [#2453](https://github.com/Vincit/objection.js/pull/2453)
- Fixes [#2476](https://github.com/Vincit/objection.js/pull/2476)
## 3.0.4
### What's new
- Fixes [#2447](https://github.com/Vincit/objection.js/issues/2447)
## 3.0.3
### What's new
- Fixes [#1673](https://github.com/Vincit/objection.js/issues/1673)
- Fixes [#1957](https://github.com/Vincit/objection.js/issues/1957)
- Fixes [#2105](https://github.com/Vincit/objection.js/issues/2105)
- Fixes [#2132](https://github.com/Vincit/objection.js/issues/2132)
- Fixes [#2251](https://github.com/Vincit/objection.js/issues/2251)
- Fixes [#2262](https://github.com/Vincit/objection.js/issues/2262)
- Fixes [#2271](https://github.com/Vincit/objection.js/issues/2271)
- Fixes [#2277](https://github.com/Vincit/objection.js/issues/2277)
- Fixes [#2308](https://github.com/Vincit/objection.js/pull/2308)
- Fixes [#2311](https://github.com/Vincit/objection.js/issues/2311)
- Fixes [#2311](https://github.com/Vincit/objection.js/pull/2311)
- Fixes [#2311](https://github.com/Vincit/objection.js/pull/2311)
- Fixes [#2332](https://github.com/Vincit/objection.js/issues/2332)
- Fixes [#2337](https://github.com/Vincit/objection.js/issues/2337)
- Fixes [#2372](https://github.com/Vincit/objection.js/pull/2372)
- Fixes [#2379](https://github.com/Vincit/objection.js/pull/2379)
- Fixes [#2383](https://github.com/Vincit/objection.js/pull/2383)
- Fixes [#2399](https://github.com/Vincit/objection.js/pull/2399)
- Fixes [#2404](https://github.com/Vincit/objection.js/pull/2404)
- Fixes [#2405](https://github.com/Vincit/objection.js/pull/2405)
- Fixes [#2408](https://github.com/Vincit/objection.js/pull/2408)
- Fixes [#2409](https://github.com/Vincit/objection.js/pull/2409)
- Fixes [#2423](https://github.com/Vincit/objection.js/pull/2423)
## 3.0.2
### What's new
- Fixes [#1356](https://github.com/Vincit/objection.js/issues/1356)
- Fixes [#1957](https://github.com/Vincit/objection.js/issues/1957)
- Fixes [#2192](https://github.com/Vincit/objection.js/issues/2192)
- Fixes [#2247](https://github.com/Vincit/objection.js/pull/2247)
- Fixes [#2307](https://github.com/Vincit/objection.js/pull/2307)
- Fixes [#2308](https://github.com/Vincit/objection.js/pull/2308)
- Fixes [#2311](https://github.com/Vincit/objection.js/pull/2311)
- Fixes [#2323](https://github.com/Vincit/objection.js/pull/2323)
- Fixes [#2337](https://github.com/Vincit/objection.js/issues/2337)
- Fixes [#2362](https://github.com/Vincit/objection.js/pull/2362)
## 3.0.1
### What's new
- Fixes [#2123](https://github.com/Vincit/objection.js/issues/2123)
- Fixes [#2150](https://github.com/Vincit/objection.js/issues/2150)
- Fixes [#2179](https://github.com/Vincit/objection.js/pull/2179)
## 3.0.0
### What's new
- Fixes [#1986](https://github.com/Vincit/objection.js/issues/1986)
- Fixes [#1987](https://github.com/Vincit/objection.js/issues/1987)
- Fixes [#1954](https://github.com/Vincit/objection.js/issues/1954)
- Fixes [#1993](https://github.com/Vincit/objection.js/issues/1993)
- Fixes [#1688](https://github.com/Vincit/objection.js/issues/1688)
- Fixes [#1651](https://github.com/Vincit/objection.js/issues/1651)
- Fixes [#2135](https://github.com/Vincit/objection.js/issues/2135)
- Fixes [#1936](https://github.com/Vincit/objection.js/issues/1936)
- Fixes [#1905](https://github.com/Vincit/objection.js/issues/1905)
- Fixes [#1997](https://github.com/Vincit/objection.js/issues/1997)
- Fixes [#2024](https://github.com/Vincit/objection.js/issues/2024)
### Breaking changes
See the [migration guide](/release-notes/migration.md).
## 2.2.10
### What's new
- Add `modelClass` property for `ValidationError` and `NotFoundError`.
## 2.2.9
### What's new
- Add `noWait` query builder method.
## 2.2.8
### What's new
- Fixes [#1982](https://github.com/Vincit/objection.js/issues/1982)
- Fixes [#1983](https://github.com/Vincit/objection.js/issues/1983)
## 2.2.7
### What's new
- `QueryBuilder.castTo` can now be used to cast query results to any typescript type.
## 2.2.6
### What's new
- Fixes [#1964](https://github.com/Vincit/objection.js/issues/1964)
## 2.2.5
### What's new
- Fixes [#1855](https://github.com/Vincit/objection.js/issues/1855)
## 2.2.4
### What's new
- Add support for onConflict, merge and ignore knex methods.
## 2.2.2
### What's new
- [#1722](https://github.com/Vincit/objection.js/issues/1722)
## 2.2.1
### What's new
- Fixes [#1757](https://github.com/Vincit/objection.js/issues/1757)
- Fixes [#1729](https://github.com/Vincit/objection.js/issues/1729)
## 2.2.0
### What's new
- Fixes [#1770](https://github.com/Vincit/objection.js/issues/1770)
- Fixes [#1699](https://github.com/Vincit/objection.js/issues/1699)
- Fixes [#1703](https://github.com/Vincit/objection.js/issues/1703)
- Fixes [#1675](https://github.com/Vincit/objection.js/issues/1675)
- Fixes [#1708](https://github.com/Vincit/objection.js/issues/1708)
- Fixes [#1743](https://github.com/Vincit/objection.js/issues/1743)
- Fixes [#1731](https://github.com/Vincit/objection.js/issues/1731)
- Fixes [#1761](https://github.com/Vincit/objection.js/issues/1761)
## 2.1.4
### What's new
- Fixes [#1750](https://github.com/Vincit/objection.js/issues/1750)
## 2.1.3
### What's new
- Add `underscoreBetweenUppercaseLetters` option for snake case mappers. [#1676](https://github.com/Vincit/objection.js/issues/1676)
## 2.1.2
### What's new
- Fix `startTransaction` typings.
## 2.1.1
### What's new
- Fixes [#1489](https://github.com/Vincit/objection.js/issues/1489)
## 2.1.0
### What's new
- Fixes [#1638](https://github.com/Vincit/objection.js/issues/1638)
- Fixes [#1636](https://github.com/Vincit/objection.js/issues/1636)
- Fixes [#1615](https://github.com/Vincit/objection.js/issues/1615)
# Changelog
## 2.0.10
### What's new
- Fixes [#1630](https://github.com/Vincit/objection.js/issues/1630)
## 2.0.9
### What's new
- Fixes [#1606](https://github.com/Vincit/objection.js/issues/1606)
## 2.0.8
### What's new
- Fixes [#1627](https://github.com/Vincit/objection.js/issues/1627)
## 2.0.7
### What's new
- Fixes [#1607](https://github.com/Vincit/objection.js/issues/1607)
## 2.0.6
### What's new
- Fixes [#1603](https://github.com/Vincit/objection.js/issues/1603)
## 2.0.5
### What's new
- Fixes `upsertGraph` bug where composite keys were not selected correctly. See the fix [here](https://github.com/Vincit/objection.js/commit/0e58cf010348efc33e5459c055eea141f62f7561).
## 2.0.4
### What's new
- New `skipFetched` option for `fetchGraph` and `$fetchGraph`
## 2.0.3
### What's new
- Fixes [#1585](https://github.com/Vincit/objection.js/issues/1585)
- Fixes [#1361](https://github.com/Vincit/objection.js/issues/1361)
- Fixes [#1488](https://github.com/Vincit/objection.js/issues/1488)
## 2.0.0
### What's new
- Cleaner and more consistent API. A lot of methods have been renamed, removed combined and cleaned up. Most of the old methods still exist, but print a deprecation warning when first used. Some examples:
- `eager` -> `withGraphFetched`
- `joinEager` -> `withGraphJoined`
- removed `eagerAlgorithm` (you must explicitly use either `withGraphFetched` or `withGraphJoined`)
- merged `allowEager`, `allowInsert` and `allowUpsert` into one method `allowGraph`
- `$loadRelated` -> `$fetchGraph`
- `joinRelation` -> `joinRelated`
- `$relatedQuery` no longer mutates the receiving model instances
- New [static hook API](/guide/hooks.html#static-query-hooks). The old instance hooks are still around.
- `relatedQuery` can now be used for more than just subqueries. See the examples [here](/guide/query-examples.html#relation-queries).
- modifiers can now take arguments and are a lot more useful. See [this recipe](https://vincit.github.io/objection.js/recipes/modifiers.html) for more info.
- Objection now uses the [db-errors](https://github.com/Vincit/db-errors) library by default to wrap the database errors.
- `insertMissing` `upsertGraph` option now works as expected with `relate: true`: items that are not found in the database are inserted.
- Brand new typings written from scratch with many improvements and finally a support for [custom query builders](/recipes/custom-query-builder.html#custom-query-builder)
- A bunch of improvements and bug fixes for `upsertGraph`, including a huge speedup in some cases due to less data fetching.
- A brand new [fn](/api/objection/#fn) helper for calling SQL functions.
- Objection now uses native promises instead of bluebird.
- Objection is now leaner as we dropped a bunch of dependencies like `bluebird` and `lodash`.
- In addition to all of this, a huge number of bugs has been squashed!
### Breaking changes
See the [migration guide](/release-notes/migration.md).
## 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.
- Typings updates
## 1.6.8
- Fix [#1287](https://github.com/Vincit/objection.js/issues/1287)
## 1.6.7
- A bunch of regression bug fixes.
## 1.6.3
- Fixes: [#1227](https://github.com/Vincit/objection.js/issues/1227)
## 1.6.2
- Add `as` method for `raw` making it possible to use `raw` expressions in `joinEager` modifiers (as long as you give names to your raw expressions using `as`).
## 1.6.1
- Fix some very rare upsertGraph edge cases.
## 1.6.0
- Add `Model.traverseAsync` and `modelInstance.$traverseAsync` methods.
- Fixes: [#842](https://github.com/Vincit/objection.js/issues/842) and [#1205](https://github.com/Vincit/objection.js/issues/1205). This bug is about subqueries "inheriting" parent query table name and alias. This bug has been around a long time and there is a small chance that people have started accidentally or on purpose use it as a feature. If you get weird reference errors from subqueries (relation not found, table not found etc.) you may need to explicitly give an alias or use `from` in your subqueries after this update. This is a borderline breaking change, but since 2.0 is still pretty far away, I wanted to get this out faster. If I'm wrong and people are heavily depending on this bug, I'll revert the change.
- Fixes: [#1215](https://github.com/Vincit/objection.js/issues/1215)
- Fixes: [#1206](https://github.com/Vincit/objection.js/issues/1206)
## 1.5.3
### What's new
- Fixes [#1204](https://github.com/Vincit/objection.js/issues/1204)
## 1.5.1
### What's new
- Relations are now loaded lazily [#1202](https://github.com/Vincit/objection.js/issues/1202)
- `relationMappings.modelClass` can now be a function that returns a model class.
## 1.5.0
### What's new
- fix [#1131](https://github.com/Vincit/objection.js/issues/1131)
- fix [#1114](https://github.com/Vincit/objection.js/issues/1114)
- fix [#1185](https://github.com/Vincit/objection.js/issues/1185)
- fix [#1109](https://github.com/Vincit/objection.js/issues/1109)
- fix [#1110](https://github.com/Vincit/objection.js/issues/1110)
- add eagerObject and eagerModifiers accessors to QueryBuilder.
- complete rewrite of `insertGraph` and `upsertGraph` code. The rewrite brought a bunch of small performance optimizations and makes future development easier. No breaking changes.
- Chaining `returning('*')` to `insertGraph` or `upsertGraph` now propagates the call to all insert, update and delete operations.
- Code using objectio can now be transpilsed to ES5. No need to add babel workarounds anymore.
## 1.4.0
### What's new
- Add `modifierNotFound` hook [#1120](https://github.com/Vincit/objection.js/issues/1120)
- fix [#1121](https://github.com/Vincit/objection.js/issues/1121)
- fix [#1126](https://github.com/Vincit/objection.js/issues/1126)
## 1.3.0
### What's new
- Use `objection.raw` instead of `knex.raw` in `Model.raw`. [#1077](https://github.com/Vincit/objection.js/issues/1077)
- Allow modifiers (namedFilters) to be used in `modifyEager` too.
- Add `underscoreBeforeDigits` option for snake case converters. [#1025](https://github.com/Vincit/objection.js/issues/1025)
- fix [#1074](https://github.com/Vincit/objection.js/issues/1074)
- Typing fixes
## 1.2.3
### What's new
- fix [#1007](https://github.com/Vincit/objection.js/issues/1007)
- fix [#1008](https://github.com/Vincit/objection.js/issues/1008)
- fix [#1047](https://github.com/Vincit/objection.js/issues/1047)
## 1.2.2
### What's new
- Improve reference cycle detection in `upsertGraph`
## 1.2.1
### What's new
- fix [#1009](https://github.com/Vincit/objection.js/issues/1009)
## 1.2.0
### What's new
- fix [#919](https://github.com/Vincit/objection.js/issues/919)
- fix [#964](https://github.com/Vincit/objection.js/issues/964)
- Add `aliasFor` method to public API
- Prevent bluebird warnings
- UPPER_SNAKE_CASE support for `snakeCaseMappers` and `knexSnakeCaseMappers`
## 1.1.10
### What's new
- Nothing! the npm release was somehow borked. This was just a rerelease of 1.1.9.
## 1.1.9
### What's new
- fix [#782](https://github.com/Vincit/objection.js/issues/782)
## 1.1.8
### What's new
- fix [#909](https://github.com/Vincit/objection.js/issues/909)
## 1.1.7
### What's new
- fix [#884](https://github.com/Vincit/objection.js/issues/884)
## 1.1.6
### What's new
- Add typings for fetchTableMetadata, tableMetadata and onbuildknex
## 1.1.5
### What's new
- Make [Model.fetchTableMetadata](#fetchtablemetadata) and [Model.tableMetadata](#tablemetadata) methods public. [#871](https://github.com/Vincit/objection.js/issues/871)
- Add [onBuildKnex](#onbuildknex) query builder hook. [#807](https://github.com/Vincit/objection.js/issues/807)
## 1.1.4
### What's new
- fix subquery bug causing incompatibility with knex 0.14.5 and sqlite3
## 1.1.3
### What's new
- fix regression in 1.1.2 (sorry about this) [#869](https://github.com/Vincit/objection.js/issues/869)
## 1.1.2
### What's new
- Add `virtuals` option for `toJSON` and `$toJson` [#866](https://github.com/Vincit/objection.js/issues/866)
- fix [#868](https://github.com/Vincit/objection.js/issues/868)
## 1.1.1
### What's new
- fix [#865](https://github.com/Vincit/objection.js/issues/865)
- fix bug where the static `Model.relatedQuery` didn't use the relation name as an alias for the table. This may break
code if you have explicitly referenced the subquery table. [#859](https://github.com/Vincit/objection.js/issues/859)
## 1.1.0
### What's new
- Optional [object notation](#relationexpression-object-notation) for relation expressions.
- fix [#855](https://github.com/Vincit/objection.js/issues/855)
- fix [#858](https://github.com/Vincit/objection.js/issues/858)
## 1.0.1
### What's new
- Added public [Relation.joinModelClass](#relation) accessor
- Don't call `returning` on sqlite (prevents a warning message added in knex 0.14.4)
- fix [#844](https://github.com/Vincit/objection.js/issues/844)
- Small documentation updates
- Small typing fixes end updates
## 1.0.0 🎉
### What's new
- The static [`relatedQuery`](#relatedquery) method.
- New reflection methods:
[`isFind`](#isfind),
[`isInsert`](#isinsert),
[`isUpdate`](#isupdate),
[`isDelete`](#isdelete),
[`isRelate`](#isrelate),
[`isUnrelate`](#isunrelate),
[`hasWheres`](#haswheres),
[`hasSelects`](#hasselects),
[`hasEager`](#haseager),
[`has`](#has).
[`clear`](#clear).
[`columnNameToPropertyName`](#columnnametopropertyname),
[`propertyNameToColumnName`](#propertynametocolumnname).
- `ManyToMany` extras now work consistently in queries and filters. [#760](https://github.com/Vincit/objection.js/issues/760)
### Breaking changes
- `modelInstance.$query().delete().returning(something)` now returns a single instance instead of an array. [#659](https://github.com/Vincit/objection.js/issues/659)
- Node 6.0.0 is now the minimum. Objection will not work on node < 6.0.0.
- [`ValidationError`](#validationerror) overhaul. This is a big one, so read this carefully! There are three things to check when you migrate to 1.0:
1. The [`createValidationError`](#createvalidationerror) and [`ValidationError`](#validationerror) interfaces have changed.
If you have overridden the `createValidationError` method in your project, or you create custom `ValidationError` instances
you need migrate to the interfaces.
2. The model validation errors (jsonSchema violations) have remained pretty much the same but there are couple of differences. Before, the
keys of `error.data` were property names even when a nested object in a graph failed a validation. Now the keys for nested
validation errors are key paths like `foo.bar[2].spam`. Another tiny difference is the order of validation errors for each key in
`error.data`. Let's say a property `spam` failed for your model and `error.data.spam` contains an array of objects that describe
the failures. Before, the first failed validation was the last item in the array, now it is the first item.
3. All [`ValidationErrors`](#validationerror) now have a `type` field. Before all [`ValidationErrors`](#validationerror) but the model
validation errors (errors like "invalid relation expression", or "cyclic model graph") had no type, and could only be identified
based on the existence of some weird key in `error.data`. The `error.data` is now removed from those errors and the `type` should be
used instead. The message from the data is now stored in `error.message`.
- Removed deprecated methods `whereRef`, `whereJsonField` and `whereJsonEquals`. The [`ref`](#ref) helper can be used to replace the
`whereRef` calls. [`ref`](#ref) and [`lit`](#lit) can be used to replace the removed json methods.
- `ManyToMany` extras now work consistently in queries and filters. [#760](https://github.com/Vincit/objection.js/issues/760). This is not
a breaking change per se, but can cause some queries to fail with a "ambiguous identifier" error because the join table is now joined
in places where it previously wasn't. You need to explicitly specify the table for those failing columns using `Table.theColumn` syntax.
### Changes
- `isFindQuery` is renamed to [`isFind`](https://vincit.github.io/objection.js/#isfind) and deprecated.
## 0.9.4
### What's new
- Fixed [#627](https://github.com/Vincit/objection.js/issues/627)
- Fixed [#671](https://github.com/Vincit/objection.js/issues/671)
- Fixed [#672](https://github.com/Vincit/objection.js/issues/672)
- Fixed [#674](https://github.com/Vincit/objection.js/issues/674)
## 0.9.3
### What's new
- Add beforeInsert hook for relations. [#649](https://github.com/Vincit/objection.js/issues/649) [#19](https://github.com/Vincit/objection.js/issues/19)
- Add [`relatedFindQueryMutates`](#relatedfindquerymutates) and [`relatedInsertQueryMutates`](#relatedinsertquerymutates) configs as well as [`$setRelated`](#_s_setrelated) and [`$appendRelated`](#_s_appendrelated) helpers. [#599](https://github.com/Vincit/objection.js/issues/599)
- Fixed [#648](https://github.com/Vincit/objection.js/issues/648)
## 0.9.2
### What's new
- Fix regression: `from` fails with a subquery.
## 0.9.1
### What's new
- [`castTo`](https://vincit.github.io/objection.js/#castto) method for setting the model class of query result rows.
- [`onError`](https://vincit.github.io/objection.js/#onerror) `QueryBuilder` method.
- [`knexSnakeCaseMappers`](https://vincit.github.io/objection.js/#objection-knexsnakecasemappers) and [`snakeCaseMappers`](https://vincit.github.io/objection.js/#objection-snakecasemappers) for snake_case to camelCase conversions.
## 0.9.0
### What's new
- Relations can now be defined using keys inside JSON columns. See the examples [here](https://vincit.github.io/objection.js/#relationmappings).
- [`lit`](https://vincit.github.io/objection.js/#lit) helper function [#275](https://github.com/Vincit/objection.js/issues/275)
- Fixes for [`upsertGraph`](https://vincit.github.io/objection.js/#upsertgraph) when using composite keys. [#517](https://github.com/Vincit/objection.js/issues/517)
- Added `noDelete`, `noUpdate`, `noInsert`, `noRelate` and `noUnrelate` options for `upsertGraph`. See [UpsertGraphOptions docs](#upsertgraphoptions) for more info.
- `insertGraph` now accepts an options object just like `upsertGraph`. `relate` option can be used instead of `#dbRef`. [#586](https://github.com/Vincit/objection.js/issues/586)
### Breaking changes
- Instance update/patch with `returning` now return a single object instead of an array. [#423](https://github.com/Vincit/objection.js/issues/423)
- Because of the support for JSON relations [the `Relation` class](https://vincit.github.io/objection.js/#relation)
has changed a bit.
## 0.8.8
### What's new
- Typing updates: [#489](https://github.com/Vincit/objection.js/issues/489) [#487](https://github.com/Vincit/objection.js/issues/487)
- Improve `resultSize` method. [#213](https://github.com/Vincit/objection.js/issues/213)
- Avoid unnecessary updates in upsertGraph [#480](https://github.com/Vincit/objection.js/issues/480)
## 0.8.7
### What's new
- `throwIfNotFound` now also throws when update or delete doesn't change any rows.
- [`mixin`](#mixin) and [`compose`](#compose) helpers for applying multiple plugins. [#475](https://github.com/Vincit/objection.js/issues/475) [#473](https://github.com/Vincit/objection.js/issues/473)
- Typing updates [#474](https://github.com/Vincit/objection.js/issues/474) [#479](https://github.com/Vincit/objection.js/issues/479)
- `upsertGraph` now validates patched models correctly. [#477](https://github.com/Vincit/objection.js/issues/477)
## 0.8.6
### What's new
- Finally: the first version of [`upsertGraph`](#graph-upserts) method! Please open issues about bugs, WTFs and missing features.
- Strip readonly virtual properties in fromJson & friends [#432](https://github.com/Vincit/objection.js/issues/432)
- Fixed [#439](https://github.com/Vincit/objection.js/issues/439)
## 0.8.5
### What's new
- Add [`Model.useLimitInFirst`](https://vincit.github.io/objection.js/#uselimitinfirst) configuration flag.
## 0.8.4
### What's new
- New shorthand methods [`joinEager`](https://vincit.github.io/objection.js/#joineager), [`naiveEager`](https://vincit.github.io/objection.js/#naiveeager),
[`mergeJoinEager`](https://vincit.github.io/objection.js/#mergejoineager) and [`mergeNaiveEager`](https://vincit.github.io/objection.js/#mergenaiveeager).
- New shorthand method [`findOne`](https://vincit.github.io/objection.js/#findone)
- New reflection method [`isFindQuery`](https://vincit.github.io/objection.js/#isfind)
- ManyToMany extra properties can now be updated [#413](https://github.com/Vincit/objection.js/issues/413)
## 0.8.3
### What's new
- [`NaiveEagerAlogrithm`](https://vincit.github.io/objection.js/#eager)
- [Aliases in relation expressions](https://vincit.github.io/objection.js/#relationexpression) [#402](https://github.com/Vincit/objection.js/issues/402)
- New lazily evaluated `raw` function. [#275](https://github.com/Vincit/objection.js/issues/275)
## 0.8.2
### What's new
- [`Model.namedFilters`](https://vincit.github.io/objection.js/#namedfilters) object for defining shared filters that can be used by name in eager expressions.
- Full support for views and table aliases in eager, join, joinRelation etc. [#181](https://github.com/Vincit/objection.js/issues/181)
- Fix `bindTransaction` bug with `ManyToManyRelation` junction tables [#395](https://github.com/Vincit/objection.js/issues/395)
## 0.8.1
### What's new
- [`throwIfNotFound`](https://vincit.github.io/objection.js/#throwifnotfound) method for making empty query results throw an exception.
- fix error when passing model instance to a `where` method. [#387](https://github.com/Vincit/objection.js/issues/387)
## 0.8.0
### What's new
- All query methods now call `Model.query` to create a `QueryBuilder` instance [#346](https://github.com/Vincit/objection.js/issues/346)
- Objection is no longer transpiled. One of the implications is that you can use a github
link in package.json to test experimental versions.
- `count` can now be called without arguments [#364](https://github.com/Vincit/objection.js/issues/364)
- A new [`getRelations`](#getrelations) method for plugin development and other reflection greatness.
### Breaking changes
> Old model definition
```js
function Person() {
Model.apply(this, arguments);
}
Model.extend(Person);
Person.tableName = 'Person';
Person.prototype.fullName = function () {
return this.firstName + ' ' + this.lastName;
};
// More static and prototype methods.
```
> Easiest way to migrate to `class` and `extends` keywords
```js
class Person extends Model {}
Person.tableName = 'Person';
Person.prototype.fullName = function () {
return this.firstName + ' ' + this.lastName;
};
// More static and prototype methods.
```
- Support for node versions below 4.0.0 has been removed. With it the support for legacy class inheritance using `Model.extend` method
has also been removed. This means that you need to change your model definitions to use the `class` and `extends` keywords.
To achieve this with the minimum amount of changes you can simply swap the constructor function and `Model.extend` to
a class definition. You can still define all static and prototype methods and properties the old way. See the example on the right -->
Note that this also affects Babel transpilation. You cannot (or need to) use `babel-plugin-transform-es2015-classes` anymore.
See the [ESNext example project](https://github.com/Vincit/objection.js/tree/0.8.0/examples/express-es7) as an example of
how to setup babel.
- The default value of [`pickJsonSchemaProperties`](#pickjsonschemaproperties) was changed to `false`. Before, all properties that
were not listed in `jsonSchema` were removed before `insert`, `patch` or `update` (if `jsonSchma` was defined). Starting from
this version you need to explicitly set the value to `true`. You may have been used this feature by accident.
If you have weird problems after the update, try setting `objection.Model.pickJsonSchemaProperties = true;` to see
if it helps.
- [`relate`](#pickjsonschemaproperties) and [`unrelate`](#pickjsonschemaproperties) methods now return the result of the
underlying query (`patch` in case of `HasManyRelation`, `HasOneRelation`, and `BelongsToOneRelation`. `insert` otherwise).
Before the method input was always returned.
- `Model.RelatedQueryBuilder` is removed. `Model.QueryBuilder` is now used to create all query builders for the model.
This only affects you if you have defined custom query builders.
## 0.7.12
### What's new
- fix [#345](https://github.com/Vincit/objection.js/issues/345)
## 0.7.11
### What's new
- fix [#339](https://github.com/Vincit/objection.js/issues/339)
- fix [#341](https://github.com/Vincit/objection.js/issues/341)
## 0.7.10
### What's new
- fix bugs that prevented using `$relatedQuery` and `eager` together with `JoinEagerAlgorithm`
- typing updates
## 0.7.9
### What's new
- [`joinRelation`](https://vincit.github.io/objection.js/#joinrelation) now accepts [`RelationExpressions`](https://vincit.github.io/objection.js/#relationexpression) and can join multiple and nested relations.
## 0.7.6
### What's new
- `range` and `page` methods now use a window function and only generate one query on postgresql [#62](https://github.com/Vincit/objection.js/issues/62)
- fix MSSQL 2100 parameter limit in eager queries [#311](https://github.com/Vincit/objection.js/issues/311)
## 0.7.5
### What's new
- fix [#327](https://github.com/Vincit/objection.js/issues/327)
- fix [#256](https://github.com/Vincit/objection.js/issues/256)
## 0.7.4
### What's new
- automatically select columns needed for relations [#309](https://github.com/Vincit/objection.js/issues/309)
- fix an issue where `$formatJson` was called inside `insertGraph` [#326](https://github.com/Vincit/objection.js/issues/326)
## 0.7.3
### What's new
- fix [#325](https://github.com/Vincit/objection.js/issues/325)
- fix an issue where `select` had to be used in addition to `distinct` in some cases
## 0.7.2
### What's new
- `HasOneThroughRelation` relation type.
## 0.7.1
### What's new
- fix `JoinEagerAlgorithm` NPE bug
## 0.7.0
### What's new
- `jsonSchema` without `properties` now works. [#205](https://github.com/Vincit/objection.js/issues/205)
- `relationMappings` can now be a function. [#227](https://github.com/Vincit/objection.js/issues/227)
- many to many extras can now be aliased. [#223](https://github.com/Vincit/objection.js/issues/223)
- zero values are now allowed in relation columns. [#228](https://github.com/Vincit/objection.js/issues/228)
- active transaction can now be accessed in `$before/$after` hooks through `queryContext.transaction` property.
- Validation can now be easily modified through a new [`Validator`](#validator) interface. [#241](https://github.com/Vincit/objection.js/issues/241) [#199](https://github.com/Vincit/objection.js/issues/199)
- fix a `JoinEager` problem where an empty result for a relation caused the following relations to be empty. [#292](https://github.com/Vincit/objection.js/issues/292)
- `ref(fieldExpression)` syntax to reduce need for knex.raw and updating single attribute inside JSON column. [#270](https://github.com/Vincit/objection.js/issues/270)
- [mergeEager](https://vincit.github.io/objection.js/#mergeeager) method.
### Breaking changes
- `$relatedQuery` now returns a single model instead of an array for belongsToOne and hasOne relations. [#155](https://github.com/Vincit/objection.js/issues/155)
- identifier of a model can now be updated. Be careful with this one! Before if you forgot a wrong id in an `update`/`patch` operation, it would simply get ignored. Now the id is also updated just like any other column [#100](https://github.com/Vincit/objection.js/issues/100)
- `Table.*` is now selected by default in all queries instead of `*`. This will break some join queries that don't have an explicit select clause. [#161](https://github.com/Vincit/objection.js/issues/161)
- `ValidationError.data` is now an object including, for each key, a list of errors with context info. [#283](https://github.com/Vincit/objection.js/issues/283)
## 0.6.2
### What's new
- `relationMappings` can now be a function [#227](https://github.com/Vincit/objection.js/issues/227)
## 0.6.1
### What's new
- fix bug [#205](https://github.com/Vincit/objection.js/issues/205)
## 0.6.0
### What's new
- Eager loading can now be done using joins and zero extra queries. See [`eagerAlgorithm`](#eageralgorithm), [`defaultEagerAlgorithm`](#defaulteageralgorithm) and [`eager`](#eager) for more info.
- `#ref` in graph inserts can now contain extra properties for many-to-many relations [#156](https://github.com/Vincit/objection.js/issues/156)
- `#dbRef` can now be used to refer to existing rows from a `insertWithRelated` graph.
- [`modelPaths`](#modelpaths) attribute for cleaner way to point to models in relationMappings.
- [`pickJsonSchemaProperties`](#pickjsonschemaproperties) config parameter [#110](https://github.com/Vincit/objection.js/issues/110)
- [`insertGraphAndFetch`](#insertgraphandfetch) with `insertWithRelatedAndFetch` alias. [#172](https://github.com/Vincit/objection.js/issues/172)
- Added [`$beforeDelete`](#_s_beforedelete) and [`$afterDelete`](#_s_afterdelete) hooks [#112](https://github.com/Vincit/objection.js/issues/112)
- Old values can now be accessed from `$beforeUpdate`, `$afterUpdate`, `$beforeValidate` and `$afterValidate` hooks [#185](https://github.com/Vincit/objection.js/issues/185)
- Support length property [#168](https://github.com/Vincit/objection.js/issues/168)
- Make sure operations are executed in the order they are called [#180](https://github.com/Vincit/objection.js/issues/180)
- Fetch nothing if the `where` clauses hit no rows in `update/patchAndFetchById` methods [#189](https://github.com/Vincit/objection.js/issues/189)
- Lots of performance tweaks.
- `$loadRelated` and `loadRelated` now return a `QueryBuilder`.
### Breaking changes
- Undefined values as query method arguments now throw an exception. Before they were just silently ignored
and for example `delete().where('id', undefined)` caused the entire table to be deleted. [skipUndefined](https://vincit.github.io/objection.js/#skipundefined)
method can be called for a query builder to handle the undefined values the old way.
- Deprecated method `dumpSql` is now removed.
- `$loadRelated` and `loadRelated` now return a `QueryBuilder`. This may break your code is some rare cases
where you have called a non-standard promise method like `reflect` for the return value of these functions.
## 0.5.5
### What's new
- [Virtual attributes](#virtualattributes)
## 0.5.4
### What's new
- bugfix: insertWithRelated now works with `additionalProperties = false` in `jsonSchema`
- Add updateAndFetch and patchAndFetch methods for `$query`
- bugfix: afterGet was not called for nested models in eager query
- Use ajv instad of tv4 for json schema validation
## 0.5.3
### What's new
- ES6 promise compatibility fixes.
## 0.5.1
### What's new
- [\$afterGet](#afterget) hook.
## 0.5.0
### What's new
- [joinRelation](#joinrelation) family of query builder methods.
- `HasOneRelation` for creating inverse one-to-one relations.
- Relations have been renamed `OneToOneRelation` --> `BelongsToOneRelation`, `OneToManyRelation` --> `HasManyRelation`.
The old names work, but have been deprecated.
- [withSchema](#withschema) now works as expected and sets the schema of all queries executed by the query builder the
method is called for.
- [filterEager](#filtereager) method for better eager query filtering.
- [extra properties](#relationmappings) feature for selecting/inserting columns from/to the join table in many-to-many relations.
- Eager query recursion depth can be controlled like so: `parent.^5`.
## 0.4.0
### What's new
- Query context feature. See [#51](https://github.com/Vincit/objection.js/issues/51) and [these docs](#context) for more info.
- Composite key support.
- [findById](#findbyid), [deleteById](#deletebyid), [whereComposite](#wherecomposite) and
[whereInComposite](#whereincomposite) query builder methods.
### Breaking changes
There shouldn't be any major breaking changes. We moved from ES5 to ES7 + babel in this version so there are big changes
in the codebase. If something comes up, please open an issue.
There are a few known corner cases that may break:
- You can now define a model for the join table of `ManyToMany` relations in `relationMappings`. This is optional,
but may be needed if you already have a model for a `ManyToMany` relation _and_ you use `snake_case`
to `camelCase` conversion for the column names. See the documentation on the [through](#relationthrough)
property of [relationMappings](#relationmappings).
- The repo no longer contains the actual built javascript. Only the ES7 code that is transpiled when the code is
published to npm. Therefore you can no longer specify a git hash to package.json to use for example the
HEAD version. We will start to publish alpha and RC versions to npm when something new and experimental
is added to the library.
## 0.3.3
### What's new
- fix regression: QueryBuilder.from is broken.
## 0.3.2
### What's new
- Improved relation expression whitespace handling.
## 0.3.1
### What's new
- `whereJson*` methods can now be used inside functions given to `where` methods.
- Added multiple missing knex methods to `QueryBuilder`.
## 0.3.0
### What's new
- [insertWithRelated](https://vincit.github.io/objection.js/QueryBuilder.html#insertWithRelated) method for
inserting model trees
- [insertAndFetch](https://vincit.github.io/objection.js/QueryBuilder.html#insertAndFetch),
[updateAndFetchById](https://vincit.github.io/objection.js/QueryBuilder.html#updateAndFetchById) and
[patchAndFetchById](https://vincit.github.io/objection.js/QueryBuilder.html#patchAndFetchById) helper methods
- Filters for [eager expressions](#eager-queries)
- [New alternative way to use transactions](#transaction-object)
- Many performance updates related to cloning, serializing and deserializing model trees.
### Breaking changes
- QueryBuilder methods `update`, `patch` and `delete` now return the number of affected rows.
The new methods `updateAndFetchById` and `patchAndFetchById` may help with the migration
- `modelInstance.$query()` instance method now returns a single model instead of an array
- Removed `Model.generateId()` method. `$beforeInsert` can be used instead
## 0.2.8
### What's new
- ES6 inheritance support
- generator function support for transactions
- traverse,pick and omit methods for Model and QueryBuilder
- bugfix: issue #38
## 0.2.7
### What's new
- bugfix: fix #37 also for `$query()`.
- Significant `toJson`/`fromJson` performance boost.
## 0.2.6
### What's new
- bugfix: fix regression bug that broke dumpSql.
## 0.2.5
### What's new
- bugfix: fix regression bug that prevented values assigned to `this` in `$before` callbacks from getting into
the actual database query
## 0.2.4
### What's new
- bugfix: many-to-many relations didn't work correctly with a snake_case to camelCase conversion
in the related model class.
## 0.2.3
### What's new
- Promise constructor is now exposed through `require('objection').Promise`.
## 0.2.2
### What's new
- $beforeUpdate, $afterUpdate, \$beforeInsert etc. are now asynchronous and you can return promises from them.
- Added `Model.fn()` shortcut to `knex.fn`.
- Added missing `asCallback` and `nodeify` methods for `QueryBuilder`.
## 0.2.1
### What's new
- bugfix: Chaining `insert` with `returning` now returns all listed columns.
## 0.2.0
### What's new
- New name `objection.js`.
- `$beforeInsert`, `$afterInsert`, `$beforeUpdate` and `$afterUpdate` hooks for `Model`.
- Postgres jsonb query methods: `whereJsonEquals`, `whereJsonSupersetOf`, `whereJsonSubsetOf` and friends.
- `whereRef` query method.
- Expose `knex.raw()` through `Model.raw()`.
- Expose `knex.client.formatter()` through `Model.formatter()`.
- `QueryBuilder` can be used to make sub queries just like knex's `QueryBuilder`.
- Possibility to use a custom `QueryBuilder` subclass by overriding `Model.QueryBuilder`.
- Filter queries/objects for relations.
- A pile of bug fixes.
### Breaking changes
- Project was renamed to objection.js. Migrate simply by replacing `moron` with `objection`.
## 0.1.0
First release.
================================================
FILE: doc/release-notes/migration.md
================================================
# Migration from objection 2.x to 3.0
This document guides you through each breaking change in objection 3.0 and attempts to provide clear steps to follow. If you find something missing, please open an issue and we'll fix this guide ASAP.
Here's a list of the breaking changes
- [Dropped support for node < 12](#dropped-support-for-node-12)
- [Dropped support for knex < 0.95](#dropped-support-for-knex-0-95)
- [typescript: Methods like `findById` and `findOne` that return a single item are now typed correctly](#typescript-methods-like-findbyid-and-findone-that-return-a-single-item-are-now-typed-correctly)
- [typescript: `QueryBuilder` now inherits `PromiseLike` instead of `Promise`](#typescript-querybuilder-now-inherits-promiselike-instead-of-promise)
- [Dropped a bunch of deprecated methods and features](#dropped-a-bunch-of-deprecated-methods-and-features)
## Dropped support for node < 12
Objection 3.0 needs at least node 12 to work.
## Dropped support for knex < 0.95
Objection needs at least knex 0.95.0 to work.
This is because knex introduced some breaking changes in 0.95.0.
## typescript: Methods like `findById` and `findOne` that return a single item are now typed correctly
In 2.0 methods like `findById`, `findOne` and `first` that return one item or undefined were incorrectly typed
to always return a value. Now the return type of those methods is `SomeModel | undefined`.
You'll get compilation errors in places where you've trusted the old typings. All you need to do is to add a
check for undefined values to narrow down the type or chain the `throwIfNotFound` method that also narrows
down the type back to `SomeModel`.
Before:
```ts
const person = await Person.query().findById(id)
console.log(person.id)
```
After:
```ts
const person = await Person.query().findById(id)
if (!person) {
throw new Error('Person not found')
}
console.log(person.id)
```
Or
```ts
const person = await Person.query().findById(id).throwIfNotFound()
console.log(person.id)
```
## typescript: `QueryBuilder` now inherits `PromiseLike` instead of `Promise`
In 2.0 and before, the `QueryBuilder` class inherited `Promise` in the typings which made code like this possible
```ts
function findPerson(id: number): Promise {
return Person.query().findById(id);
}
```
However, `QueryBuilder` doesn't actually inherit `Promise` but instead is "thenable". Typescript has the type
`PromiseLike` for thenable objects which objection now correctly uses. There are couple of ways to fix the
compilation errors this change causes:
1. Chain `execute` to the query:
```ts
function findPerson(id: number): Promise {
return Person.query().findById(id).execute();
}
```
2. Use `PromiseLike`:
```ts
function findPerson(id: number): PromiseLike {
return Person.query().findById(id);
}
```
3. Make the functions `async`:
```ts
async function findPerson(id: number): Promise {
return Person.query().findById(id);
}
```
## Dropped a bunch of deprecated methods and features
All the methods that were marked deprecated (and printed a deprection message during runtime) in 2.0 have
now been removed.
# Migration from objection 1.x to 2.0
Objection 2.0 brought a lot of great new features, but the main focus was in cleaning the API, which lead to a bunch of breaking changes. This document guides you through each change and attempts to provide clear steps to follow. If you find something missing, please open an issue and we'll fix this guide ASAP.
Here's a list of the breaking changes
- [Node 6 and 7 are no longer supported](#node-6-and-7-are-no-longer-supported)
- [modify method signature change](#modify-method-signature-change)
- [Bluebird and lodash have been removed](#bluebird-and-lodash-have-been-removed)
- [Database errors now come from db-errors library](#database-errors-now-come-from-the-db-errors-library)
- [#ref references in insertGraph and upsertGraph now require the allowRefs: true option](#ref-references-in-insertgraph-and-upsertgraph-now-require-the-allowrefs-true-option)
- [relate method now always returns the number of affected rows](#relate-method-now-always-returns-the-number-of-affected-rows)
- [\$relatedQuery no longer mutates](#relatedquery-no-longer-mutates)
- [context now acts like mergeContext](#context-now-acts-like-mergecontext)
- [Model.raw and Model.fn now return objection raw and fn](#model-raw-and-model-fn-now-return-objection-raw-and-fn)
- [QueryBuilder.toString and QueryBuilder.toSql have been removed](#querybuilder-tostring-and-querybuilder-tosql-have-been-removed)
- [Rewritten typings](#rewritten-typings)
In addition to these, **a lot** of methods were deprecated and replaced by a new method. The old methods still work, but they print a warning (once per process) when you use them. The warning message tells which method you should be using in the future and you can slowly replace the methods as you get annoyed by the warnings.
Most of the methods have simply been renamed, but in some cases the replacing methods work a little differently. Make sure to read the documentation of the new method.
## Node 6 and 7 are no longer supported
Objection now needs at least node 8 to work.
## `modify` method signature change
Before, you were able to provide multiple modifiers or modifier names to `modify` by providing multiple arguments like this:
```js
Person.query().modify('foo', 'bar');
```
Now only the first argument is used to specify modifiers and all the rest are arguments for the modifiers. The first argument can be an array so you can simply wrap the modifiers in an array if there are more than one of them:
```js
Person.query().modify(['foo', 'bar']);
```
## Bluebird and lodash have been removed
Before, all async operations returned a bluebird promise. Now the bluebird dependency has been dropped and the native `Promise` is used instead. This also means that all bluebird-specific methods
- `map`
- `reduce`
- `reflect`
- `bind`
- `spread`
- `asCallback`
- `nodeify`
have been removed from the `QueryBuilder`.
You need to go through your code and make sure you don't use any bluebird methods or trust that objection returns a bluebird promise.
Objection also used to export `Promise` and `lodash` properties like this:
```js
import { Promise, lodash } from 'objection';
```
which is not true anymore. Both of those exports have been removed.
## Database errors now come from the db-errors library
Before, when a database operation failed, objection simply passed through the native error thrown by the database client. In 2.0 the errors are wrapped using the [db-errors](https://github.com/Vincit/db-errors) library.
The `db-errors` library errors expose a `nativeError` property. If you rely on the properties of the old errors, you can simply change code like this
```js
try {
await Person.query().where('foo', 'bar')
} catch (err) {
if (err.code === 13514) {
...
}
}
```
into this:
```js
try {
await Person.query().where('foo', 'bar')
} catch (err) {
err = err.nativeError || err
if (err.code === 13514) {
...
}
}
```
A preferred way to handle this would be to use the new `db-error` classes as described [here](/recipes/error-handling.html#error-handling), but the fastest migration path is to do the above trick.
## #ref references in insertGraph and upsertGraph now require the allowRefs: true option
The usage of `'#ref': 'someId'` and `#ref{someId.someProp}` now requires an explicit `allowRefs: true` option to be passed to the called method:
```js
await Person.query().insertGraph(graphWithRefs, { allowRefs: true });
```
This change was made for security reasons. An attacker could, in theory, use a `#ref{someId.someProperty}` reference to access for example the password hash of a user:
```js
const graphUpsertSentByTheAttacker = {
user: {
id: 13431,
'#id': 'user',
},
movie: {
name: '#ref{user.passwordHash}',
},
};
```
and then the attacker could just take the password hash out of the movies's name in a client of some sort.
For this attack to work, the attacker must already have an access to the API that modifies the user's information. Additionally and more importantly, the graph described above (and all other graphs I could think of) would only yield the password hash in the movie's name **if the program sets the hash to the graph before the `upsertGraph` call is executed**. This is a highly unlikely scenario and in case of passowords, would require the attacker to be able to access a route that changes the user's password. The reference can never access the property in the database, only in the object itself.
Even though there's very little chance this kind of attack could be carried out at the moment, I'd advice you to never use `upsertGraph` with `{ allowRefs: true }` and unvalidated user input with references!
## relate method now always returns the number of affected rows
`relate` used to return the inserted pivot table row in case of `ManyToManyRelation` and the number of updated rows in case of other relations. Now an integer indicating the number of affected rows is always returned.
You need to go through your code and check how the return values of `relate` queries are used.
## \$relatedQuery no longer mutates
With objection 1.x, doing this
```js
await somePerson.$relatedQuery('pets');
```
added a property `pets` for `somePerson` and saved the result there. This no longer happens with objection 2. Also inserting a new item using
```js
await somePerson.$relatedQuery('pets').insert(pet);
```
would previously add the inserted pet to the `pets` array of `somePerson`. This also no longer happens.
You can use `withGraphFetched` and `fetchGraph` methods if you want to populate the relations. Also, nothing prevents you from doing this:
```js
somePerson.pets = await somePerson.$relatedQuery('pets');
```
You can also use the `Model.relatedInsertQueryMutates` and `Model.relatedFindQueryMutates` properties to revert back to 1.x behavior. Note that those properties are now deprecated and will be removed in 3.0.
## context() now acts like mergeContext()
The `context` method of `QueryBuilder` now merges the given object with the current context object instead of replacing it. You can use `clearContext` to clear the context if you need the old behaviour.
## Model.raw and Model.fn now return objection raw and fn
Previously `Model.raw` returned a knex raw builder and `Model.fn` returned a knex `FunctionHelper` instance. In 2.0 objection's [raw](/api/objection/#raw) and [fn](/api/objection/#fn) helpers are returned. To get a knex `raw` builder, you need to use `knex.raw` directly.
## QueryBuilder.toString and QueryBuilder.toSql have been removed
You can use `QueryBuilder.toKnexQuery().toSQL()` instead.
## Rewritten typings
The typings have been completely rewritten in 2.0 and many of the types have changed names. By default you shouldn't get that many errors, but whenever you have explicitly defined an objection type for something other than `Model`, you may need to adjust that type. For example the `QueryBuilder` no longer takes three generic arguments, but two.
For this breaking change we can't easily provide clear migration steps, because we don't know how much you have trusted the typescript type inference, and how much you have used explicit types.
One notable change is that you should no longer define your relations properties using `Partial` or `Partial[]`. You can simply use `Model` and `Model[]` and methods like `insertGraph` and `upsertGraph` will just work.
================================================
FILE: docker-compose.yml
================================================
version: '3'
services:
postgres:
image: 'postgres'
container_name: 'objection_postgres'
command: postgres -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c random_page_cost=1.0
ports:
- '5432:5432'
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
mysql:
image: 'mysql:5'
container_name: 'objection_mysql'
ports:
- '3306:3306'
environment:
- 'MYSQL_ALLOW_EMPTY_PASSWORD=yes'
================================================
FILE: examples/koa/.prettierrc.json
================================================
{
"printWidth": 100,
"semi": false,
"singleQuote": true
}
================================================
FILE: examples/koa/README.md
================================================
# Koa example project
This is an example project that targets node 8.0.0 and up. The project is a simple Koa server with a REST API that demonstrates the basic functionalities of objection like models, queries, relations, eager loading and graph inserts.
Note that this is not an example of how to build a web server. It's an example of how to use objection in a web server. All other aspects are kept as simple and minimal as possible.
# Install and run
```sh
git clone git@github.com:Vincit/objection.js.git objection
cd objection/examples/koa
npm install
npm start
node client.js
```
================================================
FILE: examples/koa/api.js
================================================
'use strict'
const Person = require('./models/Person')
const Movie = require('./models/Movie')
module.exports = (router) => {
/**
* Create a new Person.
*
* Because we use `insertGraph` you can pass relations with the person and they
* also get inserted and related to the person. If all you want to do is insert
* a single person, `insertGraph` and `allowGraph` can be replaced by
* `insert(ctx.request.body)`.
*/
router.post('/persons', async (ctx) => {
// insertGraph can run multiple queries. It's a good idea to
// run it inside a transaction.
const insertedGraph = await Person.transaction(async (trx) => {
const insertedGraph = await Person.query(trx)
// For security reasons, limit the relations that can be inserted.
.allowGraph('[pets, children.[pets, movies], movies, parent]')
.insertGraph(ctx.request.body)
return insertedGraph
})
ctx.body = insertedGraph
})
/**
* Fetch multiple Persons.
*
* The result can be filtered using various query parameters:
*
* select: a list of fields to select for the Persons
* name: fuzzy search by name
* hasPets: only select Persons that have one or more pets
* isActor: only select Persons that are actors in one or more movies
* withGraph: return a graph of relations with the results
* orderBy: sort the result using this field.
*
* withPetCount: return Persons with a `petCount` column that holds the
* number of pets the person has.
*
* withMovieCount: return Persons with a `movieCount` column that holds the
* number of movies the person has acted in.
*/
router.get('/persons', async (ctx) => {
const query = Person.query()
if (ctx.query.select) {
query.select(ctx.query.select)
}
if (ctx.query.name) {
// The fuzzy name search has been defined as a reusable
// modifier. See the Person model.
query.modify('searchByName', ctx.query.name)
}
if (ctx.query.hasPets) {
query.whereExists(Person.relatedQuery('pets'))
}
if (ctx.query.isActor) {
query.whereExists(Person.relatedQuery('movies'))
}
if (ctx.query.withGraph) {
query
// For security reasons, limit the relations that can be fetched.
.allowGraph('[pets, parent, children.[pets, movies.actors], movies.actors.pets]')
.withGraphFetched(ctx.query.withGraph)
}
if (ctx.query.orderBy) {
query.orderBy(ctx.query.orderBy)
}
if (ctx.query.withPetCount) {
query.select(Person.relatedQuery('pets').count().as('petCount'))
}
if (ctx.query.withMovieCount) {
query.select(Person.relatedQuery('movies').count().as('movieCount'))
}
// You can uncomment the next line to see the SQL that gets executed.
// query.debug();
ctx.body = await query
})
/**
* Update a single Person.
*/
router.patch('/persons/:id', async (ctx) => {
const numUpdated = await Person.query().findById(ctx.params.id).patch(ctx.request.body)
ctx.body = {
success: numUpdated == 1,
}
})
/**
* Delete a person.
*/
router.delete('/persons/:id', async (ctx) => {
const numDeleted = await Person.query().findById(ctx.params.id).delete()
ctx.body = {
success: numDeleted == 1,
}
})
/**
* Insert a new child for a person.
*/
router.post('/persons/:id/children', async (ctx) => {
const personId = parseInt(ctx.params.id)
const child = await Person.relatedQuery('children').for(personId).insert(ctx.request.body)
ctx.body = child
})
/**
* Get a Person's children.
*
* The result can be filtered using various query parameters:
*
* select: a list of fields to select for the children
* name: fuzzy search by name
*
* actorInMovie: only return children that are actors in this movie.
* Provide the name of the movie.
*/
router.get('/persons/:id/children', async (ctx) => {
const query = Person.relatedQuery('children').for(ctx.params.id)
if (ctx.query.select) {
query.select(ctx.query.select)
}
if (ctx.query.name) {
// The fuzzy name search has been defined as a reusable
// modifier. See the Person model.
query.modify('searchByName', ctx.query.name)
}
if (ctx.query.actorInMovie) {
// Here's an example of a more complex query. We only select children
// that are actors in the movie with name `ctx.query.actorInMovie`.
// We could also achieve this using joins, but subqueries are often
// easier to deal with than joins since they don't interfere with
// the rest of the query.
const movieSubquery = Person.relatedQuery('movies').where('name', ctx.query.actorInMovie)
query.whereExists(movieSubquery)
}
ctx.body = await query
})
/**
* Insert a new pet for a Person.
*/
router.post('/persons/:id/pets', async (ctx) => {
const personId = parseInt(ctx.params.id)
const pet = await Person.relatedQuery('pets').for(personId).insert(ctx.request.body)
ctx.body = pet
})
/**
* Get a Person's pets. The result can be filtered using query parameters
*
* The result can be filtered using the following query parameters:
*
* name: the name of the pets to fetch
* species: the species of the pets to fetch
*/
router.get('/persons/:id/pets', async (ctx) => {
const query = Person.relatedQuery('pets').for(ctx.params.id)
if (ctx.query.name) {
query.where('name', 'like', ctx.query.name)
}
if (ctx.query.species) {
query.where('species', ctx.query.species)
}
const pets = await query
ctx.body = pets
})
/**
* Insert a new movie.
*/
router.post('/movies', async (ctx) => {
const movie = await Movie.query().insert(ctx.request.body)
ctx.body = movie
})
/**
* Add existing Person as an actor to a movie.
*/
router.post('/movies/:movieId/actors/:personId', async (ctx) => {
const numRelated = await Movie.relatedQuery('actors')
.for(ctx.params.movieId)
.relate(ctx.params.personId)
ctx.body = {
success: numRelated == 1,
}
})
/**
* Remove a connection between a movie and an actor. Doesn't delete
* the movie or the actor. Just removes their connection.
*/
router.delete('/movies/:movieId/actors/:personId', async (ctx) => {
const numUnrelated = await Movie.relatedQuery('actors')
.for(ctx.params.movieId)
.unrelate()
.where('persons.id', ctx.params.personId)
ctx.body = {
success: numUnrelated == 1,
}
})
/**
* Get Movie's actors.
*/
router.get('/movies/:id/actors', async (ctx) => {
const actors = await Movie.relatedQuery('actors').for(ctx.params.id)
ctx.body = actors
})
}
================================================
FILE: examples/koa/app.js
================================================
const Koa = require('koa')
const KoaRouter = require('koa-router')
const bodyParser = require('koa-bodyparser')
const Knex = require('knex')
const knexConfig = require('./knexfile')
const registerApi = require('./api')
const { Model, ForeignKeyViolationError, ValidationError } = require('objection')
// Initialize knex.
const knex = Knex(knexConfig.development)
// Bind all Models to a knex instance. If you only have one database in
// your server this is all you have to do. For multi database systems, see
// the Model.bindKnex() method.
Model.knex(knex)
const router = new KoaRouter()
const app = new Koa()
// Register our REST API.
registerApi(router)
app.use(errorHandler)
app.use(bodyParser())
app.use(router.routes())
app.use(router.allowedMethods())
const server = app.listen(8641, () => {
console.log('Example app listening at port %s', server.address().port)
})
// Error handling.
//
// NOTE: This is not a good error handler, this is a simple one. See the error handing
// recipe for a better handler: http://vincit.github.io/objection.js/recipes/error-handling.html
async function errorHandler(ctx, next) {
try {
await next()
} catch (err) {
if (err instanceof ValidationError) {
ctx.status = 400
ctx.body = {
error: 'ValidationError',
errors: err.data,
}
} else if (err instanceof ForeignKeyViolationError) {
ctx.status = 409
ctx.body = {
error: 'ForeignKeyViolationError',
}
} else {
ctx.status = 500
ctx.body = {
error: 'InternalServerError',
message: err.message || {},
}
}
}
}
================================================
FILE: examples/koa/client.js
================================================
'use strict'
/**
* This file contains a bunch of HTTP requests that use the
* API defined in api.js.
*/
const axios = require('axios')
const qs = require('querystring')
const req = axios.create({
baseURL: 'http://localhost:8641/',
paramsSerializer: qs.stringify,
})
;(async () => {
const matt = await insertPersonWithRelations()
await fetchPeople()
await updatePerson(matt, { age: 41 })
await deletePerson(matt.children[0])
const isabella = await insertChildForPerson(matt, {
firstName: 'Isabella',
lastName: 'Damon',
age: 13,
})
await insertChildForPerson(matt.parent, {
firstName: 'Kyle',
lastName: 'Damon',
age: 52,
})
await fetchChildren(matt.parent)
await insertPetForPerson(isabella, { name: 'Chewy', species: 'hamster' })
await fetchPersonsHamsters(isabella)
const departed = await insertMovie({ name: 'The Departed' })
await addPersonToMovieAsActor(departed, matt)
await removePersonFromMovie(departed, matt)
})().catch((err) => {
console.error('error:', err.response.status, err.response.data)
})
async function insertPersonWithRelations() {
console.log(`
////////////////////////////////////////////////
// Insert a person with relations //
////////////////////////////////////////////////
`)
const { data: matt } = await req.post('persons', {
firstName: 'Matt',
lastName: 'Damon',
age: 43,
parent: {
firstName: 'Kent',
lastName: 'Damon',
age: 70,
},
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Kat',
species: 'cat',
},
],
movies: [
{
name: 'The Martian',
},
{
name: 'Good Will Hunting',
},
],
children: [
{
firstName: 'Isabella',
lastName: 'Damon',
age: 13,
},
],
})
console.dir(matt, { depth: null })
return matt
}
async function fetchPeople() {
console.log(`
////////////////////////////////////////////////
// Fetch people using some filters //
////////////////////////////////////////////////
`)
const { data: allPeople } = await req.get('persons', {
params: {
select: ['firstName', 'lastName'],
// Fuzzy name search. This should match to all the Damons.
name: 'damo',
withMovieCount: true,
withGraph: '[pets, children]',
},
})
console.dir(allPeople, { depth: null })
}
async function updatePerson(person, patch) {
console.log(`
////////////////////////////////////////////////
// Update a person //
////////////////////////////////////////////////
`)
const { data } = await req.patch(`persons/${person.id}`, patch)
console.dir(data)
}
async function deletePerson(person) {
console.log(`
////////////////////////////////////////////////
// Delete a person //
////////////////////////////////////////////////
`)
const { data } = await req.delete(`persons/${person.id}`)
console.dir(data)
}
async function insertChildForPerson(person, child) {
console.log(`
////////////////////////////////////////////////
// Add a child for a person //
////////////////////////////////////////////////
`)
const { data } = await req.post(`persons/${person.id}/children`, child)
console.dir(data)
return data
}
async function fetchChildren(person) {
console.log(`
////////////////////////////////////////////////
// Fetch a person's children //
////////////////////////////////////////////////
`)
const { data } = await req.get(`persons/${person.id}/children`, {
params: {
actorInMovie: 'Good Will Hunting',
},
})
console.dir(data)
}
async function insertPetForPerson(person, pet) {
console.log(`
////////////////////////////////////////////////
// Add a pet for a person //
////////////////////////////////////////////////
`)
const { data } = await req.post(`persons/${person.id}/pets`, pet)
console.dir(data)
}
async function fetchPersonsHamsters(person) {
console.log(`
////////////////////////////////////////////////
// Fetch a person's pets //
////////////////////////////////////////////////
`)
const { data } = await req.get(`persons/${person.id}/pets`, {
params: {
species: 'hamster',
},
})
console.dir(data)
}
async function insertMovie(movie) {
console.log(`
////////////////////////////////////////////////
// Insert a new movie //
////////////////////////////////////////////////
`)
const { data } = await req.post(`movies`, movie)
console.dir(data)
return data
}
async function addPersonToMovieAsActor(movie, actor) {
console.log(`
////////////////////////////////////////////////
// Connect a movie and an actor //
////////////////////////////////////////////////
`)
const { data } = await req.post(`movies/${movie.id}/actors/${actor.id}`)
console.dir(data)
}
async function removePersonFromMovie(movie, actor) {
console.log(`
////////////////////////////////////////////////
// Disconnect a movie and an actor //
////////////////////////////////////////////////
`)
const { data } = await req.delete(`movies/${movie.id}/actors/${actor.id}`)
console.dir(data)
}
================================================
FILE: examples/koa/knexfile.js
================================================
module.exports = {
development: {
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: './example.db',
},
pool: {
afterCreate: (conn, cb) => {
conn.run('PRAGMA foreign_keys = ON', cb)
},
},
},
production: {
client: 'postgresql',
connection: {
database: 'example',
},
pool: {
min: 2,
max: 10,
},
},
}
================================================
FILE: examples/koa/migrations/20150613161239_initial_schema.js
================================================
exports.up = (knex) => {
return knex.schema
.createTable('persons', (table) => {
table.increments('id').primary()
table
.integer('parentId')
.unsigned()
.references('id')
.inTable('persons')
.onDelete('SET NULL')
.index()
table.string('firstName')
table.string('lastName')
table.integer('age')
table.json('address')
})
.createTable('movies', (table) => {
table.increments('id').primary()
table.string('name')
})
.createTable('animals', (table) => {
table.increments('id').primary()
table
.integer('ownerId')
.unsigned()
.references('id')
.inTable('persons')
.onDelete('SET NULL')
.index()
table.string('name')
table.string('species')
})
.createTable('persons_movies', (table) => {
table.increments('id').primary()
table
.integer('personId')
.unsigned()
.references('id')
.inTable('persons')
.onDelete('CASCADE')
.index()
table
.integer('movieId')
.unsigned()
.references('id')
.inTable('movies')
.onDelete('CASCADE')
.index()
})
}
exports.down = (knex) => {
return knex.schema
.dropTableIfExists('persons_movies')
.dropTableIfExists('animals')
.dropTableIfExists('movies')
.dropTableIfExists('persons')
}
================================================
FILE: examples/koa/models/Animal.js
================================================
'use strict'
const { Model } = require('objection')
class Animal extends Model {
// Table name is the only required property.
static get tableName() {
return 'animals'
}
// Optional JSON schema. This is not the database schema! Nothing is generated
// based on this. This is only used for validation. Whenever a model instance
// is created it is checked against this schema. http://json-schema.org/.
static get jsonSchema() {
return {
type: 'object',
required: ['name'],
properties: {
id: { type: 'integer' },
ownerId: { type: ['integer', 'null'] },
name: { type: 'string', minLength: 1, maxLength: 255 },
species: { type: 'string', minLength: 1, maxLength: 255 },
},
}
}
// This object defines the relations to other models.
static get relationMappings() {
// One way to prevent circular references
// is to require the model classes here.
const Person = require('./Person')
return {
owner: {
relation: Model.BelongsToOneRelation,
// The related model. This can be either a Model subclass constructor or an
// absolute file path to a module that exports one.
modelClass: Person,
join: {
from: 'animals.ownerId',
to: 'persons.id',
},
},
}
}
}
module.exports = Animal
================================================
FILE: examples/koa/models/Movie.js
================================================
'use strict'
const { Model } = require('objection')
class Movie extends Model {
// Table name is the only required property.
static get tableName() {
return 'movies'
}
// Optional JSON schema. This is not the database schema! Nothing is generated
// based on this. This is only used for validation. Whenever a model instance
// is created it is checked against this schema. http://json-schema.org/.
static get jsonSchema() {
return {
type: 'object',
required: ['name'],
properties: {
id: { type: 'integer' },
name: { type: 'string', minLength: 1, maxLength: 255 },
},
}
}
static get relationMappings() {
// One way to prevent circular references
// is to require the model classes here.
const Person = require('./Person')
return {
actors: {
relation: Model.ManyToManyRelation,
// The related model. This can be either a Model subclass constructor or an
// absolute file path to a module that exports one.
modelClass: Person,
join: {
from: 'movies.id',
// ManyToMany relation needs the `through` object to describe the join table.
through: {
from: 'persons_movies.movieId',
to: 'persons_movies.personId',
},
to: 'persons.id',
},
},
}
}
}
module.exports = Movie
================================================
FILE: examples/koa/models/Person.js
================================================
'use strict'
const { Model } = require('objection')
class Person extends Model {
// Table name is the only required property.
static get tableName() {
return 'persons'
}
// Optional JSON schema. This is not the database schema! Nothing is generated
// based on this. This is only used for validation. Whenever a model instance
// is created it is checked against this schema. http://json-schema.org/.
static get jsonSchema() {
return {
type: 'object',
required: ['firstName', 'lastName'],
properties: {
id: { type: 'integer' },
parentId: { type: ['integer', 'null'] },
firstName: { type: 'string', minLength: 1, maxLength: 255 },
lastName: { type: 'string', minLength: 1, maxLength: 255 },
age: { type: 'number' },
address: {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
zipCode: { type: 'string' },
},
},
},
}
}
// Modifiers are reusable query snippets that can be used in various places.
static get modifiers() {
return {
// Our example modifier is a a semi-dumb fuzzy name match. We split the
// name into pieces using whitespace and then try to partially match
// each of those pieces to both the `firstName` and the `lastName`
// fields.
searchByName(query, name) {
// This `where` simply creates parentheses so that other `where`
// statements don't get mixed with the these.
query.where((query) => {
for (const namePart of name.trim().split(/\s+/)) {
for (const column of ['firstName', 'lastName']) {
query.orWhereRaw('lower(??) like ?', [column, namePart.toLowerCase() + '%'])
}
}
})
},
}
}
// This object defines the relations to other models.
static get relationMappings() {
// One way to prevent circular references
// is to require the model classes here.
const Animal = require('./Animal')
const Movie = require('./Movie')
return {
pets: {
relation: Model.HasManyRelation,
// The related model. This can be either a Model subclass constructor or an
// absolute file path to a module that exports one.
modelClass: Animal,
join: {
from: 'persons.id',
to: 'animals.ownerId',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'persons.id',
// ManyToMany relation needs the `through` object to describe the join table.
through: {
from: 'persons_movies.personId',
to: 'persons_movies.movieId',
},
to: 'movies.id',
},
},
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'persons.id',
to: 'persons.parentId',
},
},
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'persons.parentId',
to: 'persons.id',
},
},
}
}
}
module.exports = Person
================================================
FILE: examples/koa/package.json
================================================
{
"name": "objection-example-koa",
"version": "3.0.0",
"description": "Objection.js koa example",
"main": "app.js",
"scripts": {
"migrate": "knex migrate:latest",
"start": "npm run migrate && node app"
},
"engines": {
"node": ">=8.0.0"
},
"author": "Sami Koskimäki",
"license": "MIT",
"dependencies": {
"axios": "^0.19.0",
"knex": "^0.95.13",
"koa": "^2.11.0",
"koa-bodyparser": "^4.2.1",
"koa-router": "^7.4.0",
"objection": "3.0.0-rc.4",
"sqlite3": "^5.0.2"
}
}
================================================
FILE: examples/koa-ts/.prettierrc.json
================================================
{
"printWidth": 100,
"semi": false,
"singleQuote": true
}
================================================
FILE: examples/koa-ts/README.md
================================================
# Koa typescript example project
This is an example project that targets node 8.0.0 and up. The project is a simple Koa server with a REST API that demonstrates the basic functionalities of objection like models, queries, relations, eager loading and graph inserts.
Note that this is not an example of how to build a web server. It's an example of how to use objection in a web server. All other aspects are kept as simple and minimal as possible.
# Install and run
```sh
git clone git@github.com:Vincit/objection.js.git objection
cd objection/examples/koa-ts
npm install
npm start
node client.js
```
================================================
FILE: examples/koa-ts/api.ts
================================================
import Person from './models/Person'
import Movie from './models/Movie'
import KoaRouter from 'koa-router'
export default (router: KoaRouter) => {
/**
* Create a new Person.
*
* Because we use `insertGraph` you can pass relations with the person and they
* also get inserted and related to the person. If all you want to do is insert
* a single person, `insertGraph` and `allowGraph` can be replaced by
* `insert(ctx.request.body)`.
*/
router.post('/persons', async (ctx) => {
// insertGraph can run multiple queries. It's a good idea to
// run it inside a transaction.
const insertedGraph = await Person.transaction(async (trx) => {
const insertedGraph = await Person.query(trx)
// For security reasons, limit the relations that can be inserted.
.allowGraph('[pets, children.[pets, movies], movies, parent]')
.insertGraph(ctx.request.body)
return insertedGraph
})
ctx.body = insertedGraph
})
/**
* Fetch multiple Persons.
*
* The result can be filtered using various query parameters:
*
* select: a list of fields to select for the Persons
* name: fuzzy search by name
* hasPets: only select Persons that have one or more pets
* isActor: only select Persons that are actors in one or more movies
* withGraph: return a graph of relations with the results
* orderBy: sort the result using this field.
*
* withPetCount: return Persons with a `petCount` column that holds the
* number of pets the person has.
*
* withMovieCount: return Persons with a `movieCount` column that holds the
* number of movies the person has acted in.
*/
router.get('/persons', async (ctx) => {
const query = Person.query()
if (ctx.query.select) {
query.select(ctx.query.select)
}
if (ctx.query.name) {
// The fuzzy name search has been defined as a reusable
// modifier. See the Person model.
query.modify('searchByName', ctx.query.name)
}
if (ctx.query.hasPets) {
query.whereExists(Person.relatedQuery('pets'))
}
if (ctx.query.isActor) {
query.whereExists(Person.relatedQuery('movies'))
}
if (ctx.query.withGraph) {
query
// For security reasons, limit the relations that can be fetched.
.allowGraph('[pets, parent, children.[pets, movies.actors], movies.actors.pets]')
.withGraphFetched(ctx.query.withGraph)
}
if (ctx.query.orderBy) {
query.orderBy(takeFirst(ctx.query.orderBy))
}
if (ctx.query.withPetCount) {
query.select(Person.relatedQuery('pets').count().as('petCount'))
}
if (ctx.query.withMovieCount) {
query.select(Person.relatedQuery('movies').count().as('movieCount'))
}
// You can uncomment the next line to see the SQL that gets executed.
// query.debug();
ctx.body = await query
})
/**
* Update a single Person.
*/
router.patch('/persons/:id', async (ctx) => {
const numUpdated = await Person.query().findById(ctx.params.id).patch(ctx.request.body)
ctx.body = {
success: numUpdated == 1,
}
})
/**
* Delete a person.
*/
router.delete('/persons/:id', async (ctx) => {
const numDeleted = await Person.query().findById(ctx.params.id).delete()
ctx.body = {
success: numDeleted == 1,
}
})
/**
* Insert a new child for a person.
*/
router.post('/persons/:id/children', async (ctx) => {
const personId = parseInt(ctx.params.id)
const child = await Person.relatedQuery('children').for(personId).insert(ctx.request.body)
ctx.body = child
})
/**
* Get a Person's children.
*
* The result can be filtered using various query parameters:
*
* select: a list of fields to select for the children
* name: fuzzy search by name
*
* actorInMovie: only return children that are actors in this movie.
* Provide the name of the movie.
*/
router.get('/persons/:id/children', async (ctx) => {
const query = Person.relatedQuery('children').for(ctx.params.id)
if (ctx.query.select) {
query.select(ctx.query.select)
}
if (ctx.query.name) {
// The fuzzy name search has been defined as a reusable
// modifier. See the Person model.
query.modify('searchByName', ctx.query.name)
}
if (ctx.query.actorInMovie) {
// Here's an example of a more complex query. We only select children
// that are actors in the movie with name `ctx.query.actorInMovie`.
// We could also achieve this using joins, but subqueries are often
// easier to deal with than joins since they don't interfere with
// the rest of the query.
const movieSubquery = Person.relatedQuery('movies').where('name', ctx.query.actorInMovie)
query.whereExists(movieSubquery)
}
ctx.body = await query
})
/**
* Insert a new pet for a Person.
*/
router.post('/persons/:id/pets', async (ctx) => {
const personId = parseInt(ctx.params.id)
const pet = await Person.relatedQuery('pets').for(personId).insert(ctx.request.body)
ctx.body = pet
})
/**
* Get a Person's pets. The result can be filtered using query parameters
*
* The result can be filtered using the following query parameters:
*
* name: the name of the pets to fetch
* species: the species of the pets to fetch
*/
router.get('/persons/:id/pets', async (ctx) => {
const query = Person.relatedQuery('pets').for(ctx.params.id)
if (ctx.query.name) {
query.where('name', 'like', ctx.query.name)
}
if (ctx.query.species) {
query.where('species', ctx.query.species)
}
const pets = await query
ctx.body = pets
})
/**
* Insert a new movie.
*/
router.post('/movies', async (ctx) => {
const movie = await Movie.query().insert(ctx.request.body)
ctx.body = movie
})
/**
* Add existing Person as an actor to a movie.
*/
router.post('/movies/:movieId/actors/:personId', async (ctx) => {
const numRelated = await Movie.relatedQuery('actors')
.for(ctx.params.movieId)
.relate(ctx.params.personId)
ctx.body = {
success: numRelated === 1,
}
})
/**
* Remove a connection between a movie and an actor. Doesn't delete
* the movie or the actor. Just removes their connection.
*/
router.delete('/movies/:movieId/actors/:personId', async (ctx) => {
const numUnrelated = await Movie.relatedQuery('actors')
.for(ctx.params.movieId)
.unrelate()
.where('persons.id', ctx.params.personId)
ctx.body = {
success: numUnrelated === 1,
}
})
/**
* Get Movie's actors.
*/
router.get('/movies/:id/actors', async (ctx) => {
const actors = await Movie.relatedQuery('actors').for(ctx.params.id)
ctx.body = actors
})
}
function takeFirst(item: T | ReadonlyArray): T {
return Array.isArray(item) ? item[0] : item
}
================================================
FILE: examples/koa-ts/app.ts
================================================
import Koa, { Context } from 'koa'
import KoaRouter from 'koa-router'
import bodyParser from 'koa-bodyparser'
import Knex from 'knex'
import knexConfig from './knexfile'
import registerApi from './api'
import { Model, ForeignKeyViolationError, ValidationError } from 'objection'
// Initialize knex.
const knex = Knex(knexConfig.development)
// Bind all Models to a knex instance. If you only have one database in
// your server this is all you have to do. For multi database systems, see
// the Model.bindKnex() method.
Model.knex(knex)
const router = new KoaRouter()
const app = new Koa()
// Register our REST API.
registerApi(router)
app.use(errorHandler)
app.use(bodyParser())
app.use(router.routes())
app.use(router.allowedMethods())
const port = 8641
app.listen(port, () => {
console.log('Example app listening at port %s', port)
})
// Error handling.
//
// NOTE: This is not a good error handler, this is a simple one. See the error handing
// recipe for a better handler: http://vincit.github.io/objection.js/recipes/error-handling.html
async function errorHandler(ctx: Context, next: () => Promise) {
try {
await next()
} catch (err: any) {
if (err instanceof ValidationError) {
ctx.status = 400
ctx.body = {
error: 'ValidationError',
errors: err.data,
}
} else if (err instanceof ForeignKeyViolationError) {
ctx.status = 409
ctx.body = {
error: 'ForeignKeyViolationError',
}
} else {
ctx.status = 500
ctx.body = {
error: 'InternalServerError',
message: err.message || {},
}
}
}
}
================================================
FILE: examples/koa-ts/client.js
================================================
'use strict'
/**
* This file contains a bunch of HTTP requests that use the
* API defined in api.js.
*/
const axios = require('axios')
const qs = require('querystring')
const req = axios.create({
baseURL: 'http://localhost:8641/',
paramsSerializer: qs.stringify,
})
;(async () => {
const matt = await insertPersonWithRelations()
await fetchPeople()
await updatePerson(matt, { age: 41 })
await deletePerson(matt.children[0])
const isabella = await insertChildForPerson(matt, {
firstName: 'Isabella',
lastName: 'Damon',
age: 13,
})
await insertChildForPerson(matt.parent, {
firstName: 'Kyle',
lastName: 'Damon',
age: 52,
})
await fetchChildren(matt.parent)
await insertPetForPerson(isabella, { name: 'Chewy', species: 'hamster' })
await fetchPersonsHamsters(isabella)
const departed = await insertMovie({ name: 'The Departed' })
await addPersonToMovieAsActor(departed, matt)
await removePersonFromMovie(departed, matt)
})().catch((err) => {
if (err.response) {
console.error('error:', err.response.status, err.response.data)
} else {
console.error('error:', err)
}
})
async function insertPersonWithRelations() {
console.log(`
////////////////////////////////////////////////
// Insert a person with relations //
////////////////////////////////////////////////
`)
const { data: matt } = await req.post('persons', {
firstName: 'Matt',
lastName: 'Damon',
age: 43,
parent: {
firstName: 'Kent',
lastName: 'Damon',
age: 70,
},
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Kat',
species: 'cat',
},
],
movies: [
{
name: 'The Martian',
},
{
name: 'Good Will Hunting',
},
],
children: [
{
firstName: 'Isabella',
lastName: 'Damon',
age: 13,
},
],
})
console.dir(matt, { depth: null })
return matt
}
async function fetchPeople() {
console.log(`
////////////////////////////////////////////////
// Fetch people using some filters //
////////////////////////////////////////////////
`)
const { data: allPeople } = await req.get('persons', {
params: {
select: ['firstName', 'lastName'],
// Fuzzy name search. This should match to all the Damons.
name: 'damo',
withMovieCount: true,
withGraph: '[pets, children]',
},
})
console.dir(allPeople, { depth: null })
}
async function updatePerson(person, patch) {
console.log(`
////////////////////////////////////////////////
// Update a person //
////////////////////////////////////////////////
`)
const { data } = await req.patch(`persons/${person.id}`, patch)
console.dir(data)
}
async function deletePerson(person) {
console.log(`
////////////////////////////////////////////////
// Delete a person //
////////////////////////////////////////////////
`)
const { data } = await req.delete(`persons/${person.id}`)
console.dir(data)
}
async function insertChildForPerson(person, child) {
console.log(`
////////////////////////////////////////////////
// Add a child for a person //
////////////////////////////////////////////////
`)
const { data } = await req.post(`persons/${person.id}/children`, child)
console.dir(data)
return data
}
async function fetchChildren(person) {
console.log(`
////////////////////////////////////////////////
// Fetch a person's children //
////////////////////////////////////////////////
`)
const { data } = await req.get(`persons/${person.id}/children`, {
params: {
actorInMovie: 'Good Will Hunting',
},
})
console.dir(data)
}
async function insertPetForPerson(person, pet) {
console.log(`
////////////////////////////////////////////////
// Add a pet for a person //
////////////////////////////////////////////////
`)
const { data } = await req.post(`persons/${person.id}/pets`, pet)
console.dir(data)
}
async function fetchPersonsHamsters(person) {
console.log(`
////////////////////////////////////////////////
// Fetch a person's pets //
////////////////////////////////////////////////
`)
const { data } = await req.get(`persons/${person.id}/pets`, {
params: {
species: 'hamster',
},
})
console.dir(data)
}
async function insertMovie(movie) {
console.log(`
////////////////////////////////////////////////
// Insert a new movie //
////////////////////////////////////////////////
`)
const { data } = await req.post(`movies`, movie)
console.dir(data)
return data
}
async function addPersonToMovieAsActor(movie, actor) {
console.log(`
////////////////////////////////////////////////
// Connect a movie and an actor //
////////////////////////////////////////////////
`)
const { data } = await req.post(`movies/${movie.id}/actors/${actor.id}`)
console.dir(data)
}
async function removePersonFromMovie(movie, actor) {
console.log(`
////////////////////////////////////////////////
// Disconnect a movie and an actor //
////////////////////////////////////////////////
`)
const { data } = await req.delete(`movies/${movie.id}/actors/${actor.id}`)
console.dir(data)
}
================================================
FILE: examples/koa-ts/knexfile.js
================================================
module.exports = {
development: {
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: './example.db',
},
pool: {
afterCreate: (conn, cb) => {
conn.run('PRAGMA foreign_keys = ON', cb)
},
},
},
production: {
client: 'postgresql',
connection: {
database: 'example',
},
pool: {
min: 2,
max: 10,
},
},
}
================================================
FILE: examples/koa-ts/migrations/20150613161239_initial_schema.js
================================================
exports.up = (knex) => {
return knex.schema
.createTable('persons', (table) => {
table.increments('id').primary()
table
.integer('parentId')
.unsigned()
.references('id')
.inTable('persons')
.onDelete('SET NULL')
.index()
table.string('firstName')
table.string('lastName')
table.integer('age')
table.json('address')
})
.createTable('movies', (table) => {
table.increments('id').primary()
table.string('name')
})
.createTable('animals', (table) => {
table.increments('id').primary()
table
.integer('ownerId')
.unsigned()
.references('id')
.inTable('persons')
.onDelete('SET NULL')
.index()
table.string('name')
table.string('species')
})
.createTable('persons_movies', (table) => {
table.increments('id').primary()
table
.integer('personId')
.unsigned()
.references('id')
.inTable('persons')
.onDelete('CASCADE')
.index()
table
.integer('movieId')
.unsigned()
.references('id')
.inTable('movies')
.onDelete('CASCADE')
.index()
})
}
exports.down = (knex) => {
return knex.schema
.dropTableIfExists('persons_movies')
.dropTableIfExists('animals')
.dropTableIfExists('movies')
.dropTableIfExists('persons')
}
================================================
FILE: examples/koa-ts/models/Animal.ts
================================================
import { Model } from 'objection'
import Person from './Person'
export default class Animal extends Model {
id!: number
name!: string
owner?: Person
// Table name is the only required property.
static tableName = 'animals'
// Optional JSON schema. This is not the database schema! Nothing is generated
// based on this. This is only used for validation. Whenever a model instance
// is created it is checked against this schema. http://json-schema.org/.
static jsonSchema = {
type: 'object',
required: ['name'],
properties: {
id: { type: 'integer' },
ownerId: { type: ['integer', 'null'] },
name: { type: 'string', minLength: 1, maxLength: 255 },
species: { type: 'string', minLength: 1, maxLength: 255 },
},
}
// This object defines the relations to other models. The relationMappings
// property can be a thunk to prevent circular dependencies.
static relationMappings = () => ({
owner: {
relation: Model.BelongsToOneRelation,
// The related model.
modelClass: Person,
join: {
from: 'animals.ownerId',
to: 'persons.id',
},
},
})
}
================================================
FILE: examples/koa-ts/models/Movie.ts
================================================
import { Model } from 'objection'
import Person from './Person'
export default class Movie extends Model {
id!: number
name!: string
actors!: Person[]
// Table name is the only required property.
static tableName = 'movies'
// Optional JSON schema. This is not the database schema! Nothing is generated
// based on this. This is only used for validation. Whenever a model instance
// is created it is checked against this schema. http://json-schema.org/.
static jsonSchema = {
type: 'object',
required: ['name'],
properties: {
id: { type: 'integer' },
name: { type: 'string', minLength: 1, maxLength: 255 },
},
}
// This object defines the relations to other models. The relationMappings
// property can be a thunk to prevent circular dependencies.
static relationMappings = () => ({
actors: {
relation: Model.ManyToManyRelation,
// The related model.
modelClass: Person,
join: {
from: 'movies.id',
// ManyToMany relation needs the `through` object to describe the join table.
through: {
from: 'persons_movies.movieId',
to: 'persons_movies.personId',
},
to: 'persons.id',
},
},
})
}
================================================
FILE: examples/koa-ts/models/Person.ts
================================================
import { Model, Modifiers } from 'objection'
import Movie from './Movie'
import Animal from './Animal'
export default class Person extends Model {
id!: number
firstName!: string
lastName!: string
age!: number
pets?: Animal[]
movies?: Movie[]
children?: Person[]
parent?: Person
// Table name is the only required property.
static tableName = 'persons'
// Optional JSON schema. This is not the database schema! Nothing is generated
// based on this. This is only used for validation. Whenever a model instance
// is created it is checked against this schema. http://json-schema.org/.
static jsonSchema = {
type: 'object',
required: ['firstName', 'lastName'],
properties: {
id: { type: 'integer' },
parentId: { type: ['integer', 'null'] },
firstName: { type: 'string', minLength: 1, maxLength: 255 },
lastName: { type: 'string', minLength: 1, maxLength: 255 },
age: { type: 'number' },
address: {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
zipCode: { type: 'string' },
},
},
},
}
// Modifiers are reusable query snippets that can be used in various places.
static modifiers: Modifiers = {
// Our example modifier is a a semi-dumb fuzzy name match. We split the
// name into pieces using whitespace and then try to partially match
// each of those pieces to both the `firstName` and the `lastName`
// fields.
searchByName(query, name) {
// This `where` simply creates parentheses so that other `where`
// statements don't get mixed with the these.
query.where((query) => {
for (const namePart of name.trim().split(/\s+/)) {
for (const column of ['firstName', 'lastName']) {
query.orWhereRaw('lower(??) like ?', [column, namePart.toLowerCase() + '%'])
}
}
})
},
}
// This object defines the relations to other models. The relationMappings
// property can be a thunk to prevent circular dependencies.
static relationMappings = () => ({
pets: {
relation: Model.HasManyRelation,
// The related model. This can be either a Model subclass constructor or an
// absolute file path to a module that exports one.
modelClass: Animal,
join: {
from: 'persons.id',
to: 'animals.ownerId',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'persons.id',
// ManyToMany relation needs the `through` object to describe the join table.
through: {
from: 'persons_movies.personId',
to: 'persons_movies.movieId',
},
to: 'movies.id',
},
},
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'persons.id',
to: 'persons.parentId',
},
},
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'persons.parentId',
to: 'persons.id',
},
},
})
}
================================================
FILE: examples/koa-ts/package.json
================================================
{
"name": "objection-example-koa-ts",
"version": "3.0.0",
"description": "Objection.js koa typescript example",
"main": "app.js",
"scripts": {
"migrate": "knex migrate:latest",
"start": "npm run migrate && rm -rf dist && tsc && node dist/app.js"
},
"engines": {
"node": ">=8.0.0"
},
"author": "Sami Koskimäki",
"license": "MIT",
"dependencies": {
"axios": "^0.19.0",
"knex": "^0.95.13",
"koa": "^2.11.0",
"koa-bodyparser": "^4.2.1",
"koa-router": "^7.4.0",
"objection": "^3.0.0-rc.4",
"sqlite3": "^5.0.2"
},
"devDependencies": {
"@types/koa": "^2.11.0",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa-router": "^7.0.42",
"typescript": "4.4.4"
}
}
================================================
FILE: examples/koa-ts/tsconfig.json
================================================
{
"compilerOptions": {
"allowUnreachableCode": false,
"alwaysStrict": true,
"module": "commonjs",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"strict": true,
"strictNullChecks": true,
"esModuleInterop": true,
"allowJs": true,
"target": "esnext",
"rootDir": "./",
"outDir": "dist"
}
}
================================================
FILE: examples/minimal/README.md
================================================
# Minimal example project.
This example has the bare minimum to get you running queries and testing out things with objection. If you want to see a more realistic example, with multiple models, relations, a REST API etc. check out the [koa example](https://github.com/Vincit/objection.js/tree/main/examples/koa)
# Install and run
```sh
git clone git@github.com:Vincit/objection.js.git objection
cd objection/examples/minimal
npm install
npm start
```
================================================
FILE: examples/minimal/app.js
================================================
'use strict';
const Knex = require('knex');
const knexConfig = require('./knexfile');
const { Model } = require('objection');
const { Person } = require('./models/Person');
// Initialize knex.
const knex = Knex(knexConfig.development);
// Bind all Models to the knex instance. You only
// need to do this once before you use any of
// your model classes.
Model.knex(knex);
async function main() {
// Delete all persons from the db.
await Person.query().delete();
// Insert one row to the database.
await Person.query().insert({
firstName: 'Jennifer',
lastName: 'Aniston',
});
// Read all rows from the db.
const people = await Person.query();
console.log(people);
}
main()
.then(() => knex.destroy())
.catch((err) => {
console.error(err);
return knex.destroy();
});
================================================
FILE: examples/minimal/knexfile.js
================================================
module.exports = {
development: {
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: './example.db',
},
pool: {
afterCreate: (conn, cb) => {
conn.run('PRAGMA foreign_keys = ON', cb);
},
},
},
production: {
client: 'postgresql',
connection: {
database: 'example',
},
pool: {
min: 2,
max: 10,
},
},
};
================================================
FILE: examples/minimal/migrations/20190330121219_initial_schema.js
================================================
'use strict';
exports.up = (knex) => {
return knex.schema.createTable('persons', (table) => {
table.increments('id').primary();
table.string('firstName');
table.string('lastName');
});
};
exports.down = (knex) => {
return knex.schema.dropTableIfExists('persons');
};
================================================
FILE: examples/minimal/models/Person.js
================================================
'use strict';
const { Model } = require('objection');
class Person extends Model {
// Table name is the only required property.
static get tableName() {
return 'persons';
}
}
module.exports = {
Person,
};
================================================
FILE: examples/minimal/package.json
================================================
{
"name": "objection-example-minimal",
"version": "2.0.0",
"description": "Objection.js minimal example project",
"main": "app.js",
"scripts": {
"migrate": "knex migrate:latest",
"start": "npm run migrate && node app"
},
"engines": {
"node": ">=8.0.0"
},
"author": "Sami Koskimäki",
"license": "MIT",
"dependencies": {
"knex": "^0.14.6",
"objection": "^1.6.6",
"sqlite3": "^4.0.4"
}
}
================================================
FILE: examples/plugin/README.md
================================================
# Objection.js example plugin
This project serves as the best practices example of an objection.js plugin.
The plugin adds a `session` method for `QueryBuilder` and extends a model
so that it sets `modifiedAt`, `modifiedBy`, `createdAt` and `createdBy` properties
automatically based on the given session.
Usage example:
```js
const Model = require('objection').Model;
const Session = require('path/to/this/example');
class Person extends Session(Model) {
static get tableName() {
return 'Person';
}
}
module.exports = Person;
```
```js
// expressjs route.
router.post('/persons', (req, res) => {
return (
Person.query()
// The following method was added by our plugin.
.session(req.session)
.insert(req.body)
.then(person => {
// Our plugin set the following properties.
console.log(person.createdAt);
console.log(person.createdBy);
res.send(person);
})
);
});
```
================================================
FILE: examples/plugin/index.js
================================================
'use strict';
// Objection.js plugins are class mixins. Read this excellent article for detailed description:
// http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
//
// A plugin should be a function that takes a model class as an argument. A plugin then needs to
// extends that model and return it. A plugin should never modify the model directly!
module.exports = (Model) => {
// If your plugin extends the QueryBuilder, you need to extend `Model.QueryBuilder`
// since it may have already been extended by other plugins.
class SessionQueryBuilder extends Model.QueryBuilder {
// Add a custom method that stores a session object to the query context. In this example
// plugin a session represents the logged-in user as in passport.js session.
session(session) {
// Save the session to the query context so that it will be available in all
// queries created by this builder and also in the model hooks. `session` is
// not a reserved word or some objection.js concept. You can store any data
// to the query context.
return this.mergeContext({
session: session,
});
}
}
// A Plugin always needs to return the extended model class.
//
// IMPORTANT: Don't give a name for the returned class! This way the returned
// class inherits the super class's name (starting from node 8).
return class extends Model {
// Make our model use the extended QueryBuilder.
static get QueryBuilder() {
return SessionQueryBuilder;
}
$beforeUpdate(opt, context) {
// If you extend existing methods like this one, always remember to call the
// super implementation. Check the documentation to see if the function can be
// async and prepare for that also.
const maybePromise = super.$beforeUpdate(opt, context);
return Promise.resolve(maybePromise).then(() => {
if (context.session) {
this.modifiedAt = new Date().toISOString();
this.modifiedBy = context.session.userId;
}
});
}
$beforeInsert(context) {
// If you extend existing methods like this one, always remember to call the
// super implementation. Check the documentation to see if the function can be
// async and prepare for that also.
const maybePromise = super.$beforeInsert(context);
return Promise.resolve(maybePromise).then(() => {
if (context.session) {
this.createdAt = new Date().toISOString();
this.createdBy = context.session.userId;
}
});
}
};
};
================================================
FILE: examples/plugin/package.json
================================================
{
"name": "objection-plugin-example",
"version": "1.0.0",
"description": "An example plugin for objection",
"main": "index.js",
"scripts": {
"test": "mocha --slow 10 --timeout 15000 --reporter spec tests.js"
},
"peerDependencies": {
"objection": "0.x"
},
"author": "Sami Koskimäki",
"license": "MIT",
"devDependencies": {
"expect.js": "^0.3.1",
"knex": "^0.14.6",
"mocha": "^5.1.1",
"objection": "^1.4.0",
"sqlite3": "^4.0.0"
}
}
================================================
FILE: examples/plugin/tests.js
================================================
'use strict';
const sessionPlugin = require('./');
const expect = require('expect.js');
const { Model } = require('objection');
const Knex = require('knex');
const ISO_DATE_REGEX = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
describe('example plugin tests', () => {
let knex;
before(() => {
knex = Knex({
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: './test.db',
},
});
});
before(() => {
return knex.schema.createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
table.string('createdBy');
table.string('createdAt');
table.string('modifiedBy');
table.string('modifiedAt');
});
});
after(() => {
return knex.schema.dropTable('Person');
});
after(() => {
return knex.destroy();
});
beforeEach(() => {
return knex('Person').delete();
});
it('should add `createdBy` and `createdAt` properties automatically on insert', () => {
const session = {
userId: 'foo',
};
class Person extends sessionPlugin(Model) {
static get tableName() {
return 'Person';
}
}
return Person.query(knex)
.session(session)
.insert({ name: 'Jennifer' })
.then((jennifer) => {
expect(jennifer.createdBy).to.equal(session.userId);
expect(jennifer.createdAt).to.match(ISO_DATE_REGEX);
});
});
it('should add `modifiedBy` and `modifiedAt` properties automatically on update', () => {
class Person extends sessionPlugin(Model) {
static get tableName() {
return 'Person';
}
}
return Person.query(knex)
.session({ userId: 'foo' })
.insert({ name: 'Jennifer' })
.then((jennifer) => {
return jennifer.$query(knex).session({ userId: 'bar' }).patchAndFetch({ name: 'Jonnifer' });
})
.then((jonnifer) => {
expect(jonnifer.createdBy).to.equal('foo');
expect(jonnifer.createdAt).to.match(ISO_DATE_REGEX);
expect(jonnifer.modifiedBy).to.equal('bar');
expect(jonnifer.modifiedAt).to.match(ISO_DATE_REGEX);
});
});
});
================================================
FILE: examples/plugin-with-options/README.md
================================================
# Objection.js example plugin with options
This project serves as the best practices example of an objection.js plugin that takes options.
The plugin adds a `session` method for `QueryBuilder` and extends a model
so that it sets `modifiedAt`, `modifiedBy`, `createdAt` and `createdBy` properties
automatically based on the given session.
This example is exactly the same as the [plugin](https://github.com/Vincit/objection.js/tree/main/examples/plugin)
example but this one accepts options. The only difference is that the main module is a factory method that accepts options
and returns a mixin.
Usage example:
```js
const Model = require('objection').Model;
const Session = require('path/to/this/example')({
setCreatedBy: false,
setModifiedBy: false
});
class Person extends Session(Model) {
static get tableName() {
return 'Person';
}
}
module.exports = Person;
```
```js
// expressjs route.
router.post('/persons', (req, res) => {
return (
Person.query()
// The following method was added by our plugin.
.session(req.session)
.insert(req.body)
.then(person => {
// Our plugin set the following property.
console.log(person.createdAt);
// This wasn't set because of the `setCreatedBy: false` option.
console.log(person.createdBy); // --> undefined
res.send(person);
})
);
});
```
================================================
FILE: examples/plugin-with-options/index.js
================================================
'use strict';
// Objection.js plugins are class mixins. Read this excellent article for detailed description:
// http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
//
// A plugin should be a function that takes a model class as an argument. A plugin then needs to
// extends that model and return it. A plugin should never modify the model directly!
//
// If the plugin takes options the main module should be a factory function that returns a
// mixin. This plugin is exactly the same as the `plugin` example, but adds a couple of options.
module.exports = (options) => {
// Provide good defaults for the options if possible.
options = Object.assign(
{
setModifiedBy: true,
setModifiedAt: true,
setCreatedBy: true,
setCreatedAt: true,
},
options,
);
// Return the mixin. If your plugin doesn't take options, you can simply export
// the mixin. The factory function is not needed.
return (Model) => {
// If your plugin extends the QueryBuilder, you need to extend `Model.QueryBuilder`
// since it may have already been extended by other plugins.
class SessionQueryBuilder extends Model.QueryBuilder {
// Add a custom method that stores a session object to the query context. In this example
// plugin a session represents the logged-in user as in passport.js session.
session(session) {
// Save the session to the query context so that it will be available in all
// queries created by this builder and also in the model hooks. `session` is
// not a reserved word or some objection.js concept. You can store any data
// to the query context.
return this.mergeContext({
session: session,
});
}
}
// A Plugin always needs to return the extended model class.
//
// IMPORTANT: Don't give a name for the returned class! This way the returned
// class inherits the super class's name (starting from node 8).
return class extends Model {
// Make our model use the extended QueryBuilder.
static get QueryBuilder() {
return SessionQueryBuilder;
}
$beforeUpdate(opt, context) {
// If you extend existing methods like this one, always remember to call the
// super implementation. Check the documentation to see if the function can be
// async and prepare for that also.
const maybePromise = super.$beforeUpdate(opt, context);
return Promise.resolve(maybePromise).then(() => {
if (context.session) {
if (options.setModifiedAt) {
this.modifiedAt = new Date().toISOString();
}
if (options.setModifiedBy) {
this.modifiedBy = context.session.userId;
}
}
});
}
$beforeInsert(context) {
// If you exetend existing methods like this one, always remember to call the
// super implementation. Check the documentation to see if the function can be
// async and prepare for that also.
const maybePromise = super.$beforeInsert(context);
return Promise.resolve(maybePromise).then(() => {
if (context.session) {
if (options.setCreatedAt) {
this.createdAt = new Date().toISOString();
}
if (options.setCreatedBy) {
this.createdBy = context.session.userId;
}
}
});
}
};
};
};
================================================
FILE: examples/plugin-with-options/package.json
================================================
{
"name": "objection-plugin-example",
"version": "1.0.0",
"description": "An example plugin for objection",
"main": "index.js",
"scripts": {
"test": "mocha --slow 10 --timeout 15000 --reporter spec tests.js"
},
"peerDependencies": {
"objection": "0.x"
},
"author": "Sami Koskimäki",
"license": "MIT",
"devDependencies": {
"expect.js": "^0.3.1",
"knex": "^0.14.6",
"mocha": "^5.1.1",
"objection": "^1.4.0",
"sqlite3": "^4.0.4"
}
}
================================================
FILE: examples/plugin-with-options/tests.js
================================================
'use strict';
const sessionPluginFactory = require('./');
const expect = require('expect.js');
const { Model } = require('objection');
const Knex = require('knex');
const ISO_DATE_REGEX = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
describe('example plugin tests', () => {
let knex;
before(() => {
knex = Knex({
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: './test.db',
},
});
});
before(() => {
return knex.schema.createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
table.string('createdBy');
table.string('createdAt');
table.string('modifiedBy');
table.string('modifiedAt');
});
});
after(() => {
return knex.schema.dropTable('Person');
});
after(() => {
return knex.destroy();
});
beforeEach(() => {
return knex('Person').delete();
});
it('should add `createdBy` and `createdAt` properties automatically on insert', () => {
const sessionPlugin = sessionPluginFactory();
const session = {
userId: 'foo',
};
class Person extends sessionPlugin(Model) {
static get tableName() {
return 'Person';
}
}
return Person.query(knex)
.session(session)
.insert({ name: 'Jennifer' })
.then((jennifer) => {
expect(jennifer.createdBy).to.equal(session.userId);
expect(jennifer.createdAt).to.match(ISO_DATE_REGEX);
});
});
it('should add `modifiedBy` and `modifiedAt` properties automatically on update', () => {
const sessionPlugin = sessionPluginFactory();
class Person extends sessionPlugin(Model) {
static get tableName() {
return 'Person';
}
}
return Person.query(knex)
.session({ userId: 'foo' })
.insert({ name: 'Jennifer' })
.then((jennifer) => {
return jennifer.$query(knex).session({ userId: 'bar' }).patchAndFetch({ name: 'Jonnifer' });
})
.then((jonnifer) => {
expect(jonnifer.createdBy).to.equal('foo');
expect(jonnifer.createdAt).to.match(ISO_DATE_REGEX);
expect(jonnifer.modifiedBy).to.equal('bar');
expect(jonnifer.modifiedAt).to.match(ISO_DATE_REGEX);
});
});
it('should not add `modifiedBy` or `createdBy` if `options.setModifiedBy` and `options.setCreatedBy` are false', () => {
const sessionPlugin = sessionPluginFactory({
setModifiedBy: false,
setCreatedBy: false,
});
class Person extends sessionPlugin(Model) {
static get tableName() {
return 'Person';
}
}
return Person.query(knex)
.session({ userId: 'foo' })
.insert({ name: 'Jennifer' })
.then((jennifer) => {
return jennifer.$query(knex).session({ userId: 'bar' }).patchAndFetch({ name: 'Jonnifer' });
})
.then((jonnifer) => {
expect(jonnifer.createdBy).to.equal(null);
expect(jonnifer.createdAt).to.match(ISO_DATE_REGEX);
expect(jonnifer.modifiedBy).to.equal(null);
expect(jonnifer.modifiedAt).to.match(ISO_DATE_REGEX);
});
});
});
================================================
FILE: lib/.eslintrc.json
================================================
{
"parserOptions": {
"sourceType": "script"
},
"rules": {
"strict": "error"
}
}
================================================
FILE: lib/initialize.js
================================================
'use strict';
async function initialize(knex, modelClasses) {
if (!modelClasses) {
modelClasses = knex;
knex = modelClasses[0].knex();
}
await Promise.all(modelClasses.map((modelClass) => modelClass.fetchTableMetadata({ knex })));
}
module.exports = {
initialize,
};
================================================
FILE: lib/model/AjvValidator.js
================================================
'use strict';
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const { Validator } = require('./Validator');
const { ValidationErrorType } = require('../model/ValidationError');
const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
class AjvValidator extends Validator {
static init(self, conf) {
super.init(self, conf);
self.ajvOptions = Object.assign({}, conf.options, {
allErrors: true,
});
// Create a normal Ajv instance.
self.ajv = new Ajv(
Object.assign(
{
useDefaults: true,
},
self.ajvOptions,
),
);
// Create an instance that doesn't set default values. We need this one
// to validate `patch` objects (objects that have a subset of properties).
self.ajvNoDefaults = new Ajv(
Object.assign({}, self.ajvOptions, {
useDefaults: false,
}),
);
// A cache for the compiled validator functions.
self.cache = new Map();
const setupAjv = (ajv) => {
if (conf.onCreateAjv) {
conf.onCreateAjv(ajv);
}
// Only add Ajv formats if they weren't added in user-space already
if (!ajv.formats['date-time']) {
addFormats(ajv);
}
};
setupAjv(self.ajv);
setupAjv(self.ajvNoDefaults);
}
beforeValidate({ json, model, options, ctx }) {
ctx.jsonSchema = model.constructor.getJsonSchema();
// Objection model's have a `$beforeValidate` hook that is allowed to modify the schema.
// We need to clone the schema in case the function modifies it. We only do this in the
// rare case that the given model has implemented the hook.
if (model.$beforeValidate !== model.$objectionModelClass.prototype.$beforeValidate) {
ctx.jsonSchema = cloneDeep(ctx.jsonSchema);
const ret = model.$beforeValidate(ctx.jsonSchema, json, options);
if (ret !== undefined) {
ctx.jsonSchema = ret;
}
}
}
validate({ json, model, options, ctx }) {
if (!ctx.jsonSchema) {
return json;
}
const modelClass = model.constructor;
const validator = this.getValidator(modelClass, ctx.jsonSchema, !!options.patch);
// We need to clone the input json if we are about to set default values.
if (!options.mutable && !options.patch && setsDefaultValues(ctx.jsonSchema)) {
json = cloneDeep(json);
}
validator.call(model, json);
const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
if (error) {
throw error;
}
return json;
}
getValidator(modelClass, jsonSchema, isPatchObject) {
// Use the Ajv custom serializer if provided.
const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
// Optimization for the common case where jsonSchema is never modified.
// In that case we don't need to call the costly createCacheKey function.
const cacheKey =
jsonSchema === modelClass.getJsonSchema()
? modelClass.uniqueTag()
: createCacheKey(jsonSchema);
let validators = this.cache.get(cacheKey);
let validator = null;
if (!validators) {
validators = {
// Validator created for the schema object without `required` properties
// using the Ajv instance that doesn't set default values.
patchValidator: null,
// Validator created for the unmodified schema.
normalValidator: null,
};
this.cache.set(cacheKey, validators);
}
if (isPatchObject) {
validator = validators.patchValidator;
if (!validator) {
validator = this.compilePatchValidator(jsonSchema);
validators.patchValidator = validator;
}
} else {
validator = validators.normalValidator;
if (!validator) {
validator = this.compileNormalValidator(jsonSchema);
validators.normalValidator = validator;
}
}
return validator;
}
compilePatchValidator(jsonSchema) {
jsonSchema = jsonSchemaWithoutRequired(jsonSchema);
// We need to use the ajv instance that doesn't set the default values.
return this.ajvNoDefaults.compile(jsonSchema);
}
compileNormalValidator(jsonSchema) {
return this.ajv.compile(jsonSchema);
}
}
function parseValidationError(errors, modelClass, options, ajvOptions) {
if (!errors) {
return null;
}
let relationNames = modelClass.getRelationNames();
let errorHash = {};
let numErrors = 0;
for (const error of errors) {
// If additionalProperties = false, relations can pop up as additionalProperty
// errors. Skip those.
if (
error.params &&
error.params.additionalProperty &&
relationNames.includes(error.params.additionalProperty)
) {
continue;
}
let path = error.instancePath.replace(/\//g, '.');
if (error.params) {
if (error.params.missingProperty) {
path += `.${error.params.missingProperty}`;
} else if (error.params.additionalProperty) {
path += `.${error.params.additionalProperty}`;
}
}
const key = `${options.dataPath || ''}${path}`.substring(1);
// More than one error can occur for the same key in Ajv, merge them in the array:
const array = errorHash[key] || (errorHash[key] = []);
// Prepare error object
const errorObj = {
message: error.message,
keyword: error.keyword,
params: error.params,
};
// Add data if verbose enabled
if (ajvOptions.verbose) {
errorObj.data = error.data;
}
// Use unshift instead of push so that the last error ends up at [0],
// preserving previous behavior where only the last error was stored.
array.unshift(errorObj);
++numErrors;
}
if (numErrors === 0) {
return null;
}
return modelClass.createValidationError({
type: ValidationErrorType.ModelValidation,
data: errorHash,
});
}
function cloneDeep(obj) {
if (isObject(obj) && obj.$isObjectionModel) {
return obj.$clone();
} else {
return lodashCloneDeep(obj);
}
}
function setsDefaultValues(jsonSchema) {
return jsonSchema && jsonSchema.properties && hasDefaults(jsonSchema.properties);
}
function hasDefaults(obj) {
if (Array.isArray(obj)) {
return arrayHasDefaults(obj);
} else {
return objectHasDefaults(obj);
}
}
function arrayHasDefaults(arr) {
for (let i = 0, l = arr.length; i < l; ++i) {
const val = arr[i];
if (isObject(val) && hasDefaults(val)) {
return true;
}
}
return false;
}
function objectHasDefaults(obj) {
const keys = Object.keys(obj);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
if (key === 'default') {
return true;
} else {
const val = obj[key];
if (isObject(val) && hasDefaults(val)) {
return true;
}
}
}
return false;
}
function jsonSchemaWithoutRequired(jsonSchema) {
const subSchemaProps = ['anyOf', 'oneOf', 'allOf', 'not', 'then', 'else', 'properties'];
return Object.assign(
omit(jsonSchema, ['required', ...subSchemaProps]),
...subSchemaProps.map((prop) => subSchemaWithoutRequired(jsonSchema, prop)),
jsonSchema && jsonSchema.definitions && Object.keys(jsonSchema.definitions).length > 0
? {
definitions: Object.assign(
...Object.keys(jsonSchema.definitions).map((prop) => ({
[prop]: jsonSchemaWithoutRequired(jsonSchema.definitions[prop]),
})),
),
}
: {},
jsonSchema.discriminator && jsonSchema.discriminator.propertyName
? { required: [jsonSchema.discriminator.propertyName] }
: {},
);
}
function subSchemaWithoutRequired(jsonSchema, prop) {
if (jsonSchema[prop]) {
if (Array.isArray(jsonSchema[prop])) {
const schemaArray = jsonSchemaArrayWithoutRequired(jsonSchema[prop]);
if (schemaArray.length !== 0) {
return {
[prop]: schemaArray,
};
} else {
return {};
}
} else if (jsonSchema.type === 'object' && prop === 'properties') {
return {
[prop]: Object.fromEntries(
Object.entries(jsonSchema[prop]).map(([key, schema]) => [
key,
jsonSchemaWithoutRequired(schema),
]),
),
};
} else {
return {
[prop]: jsonSchemaWithoutRequired(jsonSchema[prop]),
};
}
} else {
return {};
}
}
function jsonSchemaArrayWithoutRequired(jsonSchemaArray) {
return jsonSchemaArray.map(jsonSchemaWithoutRequired).filter(isNotEmptyObject);
}
function isNotEmptyObject(obj) {
return Object.keys(obj).length !== 0;
}
module.exports = {
AjvValidator,
};
================================================
FILE: lib/model/Model.js
================================================
'use strict';
const { clone } = require('./modelClone');
const { bindKnex } = require('./modelBindKnex');
const { validate } = require('./modelValidate');
const { isMsSql } = require('../utils/knexUtils');
const { visitModels } = require('./modelVisitor');
const { hasId, getSetId } = require('./modelId');
const { map: promiseMap } = require('../utils/promiseUtils');
const { toJson, toDatabaseJson } = require('./modelToJson');
const { values, propKey, hasProps } = require('./modelValues');
const { defineNonEnumerableProperty } = require('./modelUtils');
const { parseRelationsIntoModelInstances } = require('./modelParseRelations');
const { fetchTableMetadata, tableMetadata } = require('./modelTableMetadata');
const { asArray, isFunction, isString, asSingle } = require('../utils/objectUtils');
const { setJson, setFast, setRelated, appendRelated, setDatabaseJson } = require('./modelSet');
const {
getJsonAttributes,
parseJsonAttributes,
formatJsonAttributes,
} = require('./modelJsonAttributes');
const { columnNameToPropertyName, propertyNameToColumnName } = require('./modelColPropMap');
const { raw } = require('../queryBuilder/RawBuilder');
const { ref } = require('../queryBuilder/ReferenceBuilder');
const { fn } = require('../queryBuilder/FunctionBuilder');
const { AjvValidator } = require('./AjvValidator');
const { QueryBuilder } = require('../queryBuilder/QueryBuilder');
const { NotFoundError } = require('./NotFoundError');
const { ValidationError } = require('./ValidationError');
const { ModifierNotFoundError } = require('./ModifierNotFoundError');
const { RelationProperty } = require('../relations/RelationProperty');
const { RelationOwner } = require('../relations/RelationOwner');
const { HasOneRelation } = require('../relations/hasOne/HasOneRelation');
const { HasManyRelation } = require('../relations/hasMany/HasManyRelation');
const { ManyToManyRelation } = require('../relations/manyToMany/ManyToManyRelation');
const { BelongsToOneRelation } = require('../relations/belongsToOne/BelongsToOneRelation');
const { HasOneThroughRelation } = require('../relations/hasOneThrough/HasOneThroughRelation');
const { InstanceFindOperation } = require('../queryBuilder/operations/InstanceFindOperation');
const { InstanceInsertOperation } = require('../queryBuilder/operations/InstanceInsertOperation');
const { InstanceUpdateOperation } = require('../queryBuilder/operations/InstanceUpdateOperation');
const { InstanceDeleteOperation } = require('../queryBuilder/operations/InstanceDeleteOperation');
class Model {
get $modelClass() {
return this.constructor;
}
$id(maybeId) {
return getSetId(this, maybeId);
}
$hasId() {
return hasId(this);
}
$hasProps(props) {
return hasProps(this, props);
}
$query(trx) {
return instanceQuery({
instance: this,
transaction: trx,
});
}
$relatedQuery(relationName, trx) {
return relatedQuery({
modelClass: this.constructor,
relationName,
transaction: trx,
alwaysReturnArray: false,
}).for(this);
}
$fetchGraph(relationExpression, options) {
return this.constructor.fetchGraph(this, relationExpression, options);
}
$beforeValidate(jsonSchema, json, options) {
/* istanbul ignore next */
return jsonSchema;
}
$validate(json, options) {
return validate(this, json, options);
}
$afterValidate(json, options) {
// Do nothing by default.
}
$parseDatabaseJson(json) {
const columnNameMappers = this.constructor.getColumnNameMappers();
if (columnNameMappers) {
json = columnNameMappers.parse(json);
}
return parseJsonAttributes(json, this.constructor);
}
$formatDatabaseJson(json) {
const columnNameMappers = this.constructor.getColumnNameMappers();
json = formatJsonAttributes(json, this.constructor);
if (columnNameMappers) {
json = columnNameMappers.format(json);
}
return json;
}
$parseJson(json, options) {
return json;
}
$formatJson(json) {
return json;
}
$setJson(json, options) {
return setJson(this, json, options);
}
$setDatabaseJson(json) {
return setDatabaseJson(this, json);
}
$set(obj) {
return setFast(this, obj);
}
$setRelated(relation, models) {
return setRelated(this, relation, models);
}
$appendRelated(relation, models) {
return appendRelated(this, relation, models);
}
$toJson(opt) {
return toJson(this, opt);
}
toJSON(opt) {
return this.$toJson(opt);
}
$toDatabaseJson(builder) {
return toDatabaseJson(this, builder);
}
$beforeInsert(queryContext) {
// Do nothing by default.
}
$afterInsert(queryContext) {
// Do nothing by default.
}
$beforeUpdate(opt, queryContext) {
// Do nothing by default.
}
$afterUpdate(opt, queryContext) {
// Do nothing by default.
}
$afterFind(queryContext) {
// Do nothing by default.
}
$beforeDelete(queryContext) {
// Do nothing by default.
}
$afterDelete(queryContext) {
// Do nothing by default.
}
$values(props) {
return values(this, props);
}
$propKey(props) {
return propKey(this, props);
}
$idKey() {
return this.$propKey(this.constructor.getIdPropertyArray());
}
$clone(opt) {
return clone(this, !!(opt && opt.shallow), false);
}
$traverse(filterConstructor, callback) {
if (callback === undefined) {
callback = filterConstructor;
filterConstructor = null;
}
this.constructor.traverse(filterConstructor, this, callback);
return this;
}
$traverseAsync(filterConstructor, callback) {
if (callback === undefined) {
callback = filterConstructor;
filterConstructor = null;
}
return this.constructor.traverseAsync(filterConstructor, this, callback);
}
$omitFromJson(...props) {
if (arguments.length === 0) {
return this.$$omitFromJson;
} else {
if (!this.hasOwnProperty('$$omitFromJson')) {
defineNonEnumerableProperty(this, '$$omitFromJson', []);
}
this.$$omitFromJson = this.$$omitFromJson.concat(asPropsArray(props));
return this;
}
}
$omitFromDatabaseJson(...props) {
if (arguments.length === 0) {
return this.$$omitFromDatabaseJson;
} else {
if (!this.hasOwnProperty('$$omitFromDatabaseJson')) {
defineNonEnumerableProperty(this, '$$omitFromDatabaseJson', []);
}
this.$$omitFromDatabaseJson = this.$$omitFromDatabaseJson.concat(asPropsArray(props));
return this;
}
}
$knex() {
return this.constructor.knex();
}
$transaction(...args) {
return this.constructor.transaction(...args);
}
get $ref() {
return this.constructor.ref;
}
static get objectionModelClass() {
return Model;
}
static fromJson(json, options) {
const model = new this();
model.$setJson(json || {}, options);
return model;
}
static fromDatabaseJson(json) {
const model = new this();
model.$setDatabaseJson(json || {});
return model;
}
static onCreateQuery(builder) {
// Do nothing by default.
}
static beforeFind(args) {
// Do nothing by default.
}
static afterFind(args) {
// Do nothing by default.
}
static beforeInsert(args) {
// Do nothing by default.
}
static afterInsert(args) {
// Do nothing by default.
}
static beforeUpdate(args) {
// Do nothing by default.
}
static afterUpdate(args) {
// Do nothing by default.
}
static beforeDelete(args) {
// Do nothing by default.
}
static afterDelete(args) {
// Do nothing by default.
}
static omitImpl(obj, prop) {
delete obj[prop];
}
static joinTableAlias(relationPath) {
return `${relationPath}_join`;
}
static createValidator() {
return new AjvValidator({
options: {
allErrors: true,
validateSchema: false,
ownProperties: true,
v5: true,
},
});
}
static modifierNotFound(builder, modifier) {
throw new this.ModifierNotFoundError(modifier);
}
static createNotFoundError(queryContext, props) {
return new this.NotFoundError({ ...props, modelClass: this });
}
static createValidationError(props) {
return new this.ValidationError({ ...props, modelClass: this });
}
static getTableName() {
let tableName = this.tableName;
if (isFunction(tableName)) {
tableName = this.tableName();
}
if (!isString(tableName)) {
throw new Error(`Model ${this.name} must have a static property tableName`);
}
return tableName;
}
static getIdColumn() {
let idColumn = this.idColumn;
if (isFunction(idColumn)) {
idColumn = this.idColumn();
}
return idColumn;
}
static getValidator() {
return cachedGet(this, '$$validator', getValidator);
}
static getJsonSchema() {
return cachedGet(this, '$$jsonSchema', getJsonSchema);
}
static getJsonAttributes() {
return cachedGet(this, '$$jsonAttributes', getJsonAttributes);
}
static getColumnNameMappers() {
return cachedGet(this, '$$columnNameMappers', getColumnNameMappers);
}
static getConcurrency(knex) {
const DEFAULT_CONCURRENCY = 4;
if (this.concurrency === null) {
if (!knex) {
return DEFAULT_CONCURRENCY;
}
// The mssql driver is shit, and we cannot have concurrent queries.
if (isMsSql(knex)) {
return 1;
} else {
return DEFAULT_CONCURRENCY;
}
} else {
if (isFunction(this.concurrency)) {
return this.concurrency();
} else {
return this.concurrency;
}
}
}
static getModifiers() {
return this.modifiers || {};
}
static columnNameToPropertyName(columnName) {
let colToProp = cachedGet(this, '$$colToProp', () => new Map());
let propertyName = colToProp.get(columnName);
if (!propertyName) {
propertyName = columnNameToPropertyName(this, columnName);
colToProp.set(columnName, propertyName);
}
return propertyName;
}
static propertyNameToColumnName(propertyName) {
let propToCol = cachedGet(this, '$$propToCol', () => new Map());
let columnName = propToCol.get(propertyName);
if (!columnName) {
columnName = propertyNameToColumnName(this, propertyName);
propToCol.set(propertyName, columnName);
}
return columnName;
}
static getReadOnlyAttributes() {
return cachedGet(this, '$$readOnlyAttributes', getReadOnlyAttributes);
}
static getIdRelationProperty() {
return cachedGet(this, '$$idRelationProperty', getIdRelationProperty);
}
static getIdColumnArray() {
return this.getIdRelationProperty().cols;
}
static getIdPropertyArray() {
return this.getIdRelationProperty().props;
}
static getIdProperty() {
const idProps = this.getIdPropertyArray();
if (idProps.length === 1) {
return idProps[0];
} else {
return idProps;
}
}
static getRelationMappings() {
return cachedGet(this, '$$relationMappings', getRelationMappings);
}
static getRelations() {
const relations = Object.create(null);
for (const relationName of this.getRelationNames()) {
relations[relationName] = this.getRelation(relationName);
}
return relations;
}
static getRelationNames() {
return cachedGet(this, '$$relationNames', getRelationNames);
}
static getVirtualAttributes() {
return cachedGet(this, '$$virtualAttributes', getVirtualAttributes);
}
static getDefaultGraphOptions() {
return this.defaultGraphOptions;
}
static getRelatedFindQueryMutates() {
return this.relatedFindQueryMutates;
}
static getRelatedInsertQueryMutates() {
return this.relatedInsertQueryMutates;
}
static query(trx) {
const query = this.QueryBuilder.forClass(this).transacting(trx);
this.onCreateQuery(query);
return query;
}
static relatedQuery(relationName, trx) {
return relatedQuery({
modelClass: this,
relationName,
transaction: trx,
alwaysReturnArray: true,
});
}
static fetchTableMetadata(opt) {
return fetchTableMetadata(this, opt);
}
static tableMetadata(opt) {
return tableMetadata(this, opt);
}
static knex(...args) {
if (args.length) {
defineNonEnumerableProperty(this, '$$knex', args[0]);
} else {
return this.$$knex;
}
}
static transaction(knexOrTrx, cb) {
if (!cb) {
cb = knexOrTrx;
knexOrTrx = null;
}
return (knexOrTrx || this.knex()).transaction(cb);
}
static startTransaction(knexOrTrx) {
const { transaction } = require('../transaction');
return transaction.start(knexOrTrx || this.knex());
}
static get raw() {
return raw;
}
static get ref() {
return (...args) => {
return ref(...args).model(this);
};
}
static get fn() {
return fn;
}
static knexQuery() {
return this.knex().table(this.getTableName());
}
static uniqueTag() {
if (this.name) {
return `${this.getTableName()}_${this.name}`;
} else {
return this.getTableName();
}
}
static bindKnex(knex) {
return bindKnex(this, knex);
}
static bindTransaction(trx) {
return bindKnex(this, trx);
}
static ensureModel(model, options) {
const modelClass = this;
if (!model) {
return null;
}
if (model instanceof modelClass) {
return parseRelationsIntoModelInstances(model, model, options);
} else {
return modelClass.fromJson(model, options);
}
}
static ensureModelArray(input, options) {
if (!input) {
return [];
}
if (Array.isArray(input)) {
const models = new Array(input.length);
for (let i = 0, l = input.length; i < l; ++i) {
models[i] = this.ensureModel(input[i], options);
}
return models;
} else {
return [this.ensureModel(input, options)];
}
}
static getRelationUnsafe(name) {
const mapping = this.getRelationMappings()[name];
if (!mapping) {
return null;
}
if (!this.hasOwnProperty('$$relations')) {
defineNonEnumerableProperty(this, '$$relations', Object.create(null));
}
if (!this.$$relations[name]) {
this.$$relations[name] = new mapping.relation(name, this);
this.$$relations[name].setMapping(mapping);
}
return this.$$relations[name];
}
static getRelation(name) {
const relation = this.getRelationUnsafe(name);
if (!relation) {
throw new Error(`A model class ${this.name} doesn't have relation ${name}`);
}
return relation;
}
static fetchGraph($models, expression, options = {}) {
return this.query(options.transaction)
.resolve(this.ensureModelArray($models))
.findOptions({ dontCallFindHooks: true })
.withGraphFetched(expression, options)
.runAfter((models) => (Array.isArray($models) ? models : models[0]));
}
static traverse(...args) {
const { traverser, models, filterConstructor } = getTraverseArgs(...args);
if (!asSingle(models)) {
return;
}
const modelClass = asSingle(models).constructor;
visitModels(models, modelClass, (model, _, parent, relation) => {
if (!filterConstructor || model instanceof filterConstructor) {
traverser(model, parent, relation && relation.name);
}
});
return this;
}
static traverseAsync(...args) {
const { traverser, models, filterConstructor } = getTraverseArgs(...args);
if (!asSingle(models)) {
return Promise.resolve();
}
const modelClass = asSingle(models).constructor;
const promises = [];
visitModels(models, modelClass, (model, _, parent, relation) => {
if (!filterConstructor || model instanceof filterConstructor) {
const maybePromise = traverser(model, parent, relation && relation.name);
promises.push(maybePromise);
}
});
return promiseMap(promises, (it) => it, { concurrency: this.getConcurrency(this.knex()) });
}
}
Object.defineProperties(Model, {
isObjectionModelClass: {
enumerable: false,
writable: false,
value: true,
},
});
Object.defineProperties(Model.prototype, {
$isObjectionModel: {
enumerable: false,
writable: false,
value: true,
},
$objectionModelClass: {
enumerable: false,
writable: false,
value: Model,
},
});
Model.QueryBuilder = QueryBuilder;
Model.HasOneRelation = HasOneRelation;
Model.HasManyRelation = HasManyRelation;
Model.ManyToManyRelation = ManyToManyRelation;
Model.BelongsToOneRelation = BelongsToOneRelation;
Model.HasOneThroughRelation = HasOneThroughRelation;
Model.JoinEagerAlgorithm = 'JoinEagerAlgorithm';
Model.NaiveEagerAlgorithm = 'NaiveEagerAlgorithm';
Model.WhereInEagerAlgorithm = 'WhereInEagerAlgorithm';
Model.ValidationError = ValidationError;
Model.NotFoundError = NotFoundError;
Model.ModifierNotFoundError = ModifierNotFoundError;
Model.tableName = null;
Model.jsonSchema = null;
Model.idColumn = 'id';
Model.uidProp = '#id';
Model.uidRefProp = '#ref';
Model.dbRefProp = '#dbRef';
Model.propRefRegex = /#ref{([^\.]+)\.([^}]+)}/g;
Model.jsonAttributes = null;
Model.cloneObjectAttributes = true;
Model.virtualAttributes = null;
Model.relationMappings = null;
Model.modelPaths = [];
Model.pickJsonSchemaProperties = false;
Model.defaultGraphOptions = null;
Model.defaultFindOptions = Object.freeze({});
Model.modifiers = null;
Model.useLimitInFirst = false;
Model.columnNameMappers = null;
Model.relatedFindQueryMutates = false;
Model.relatedInsertQueryMutates = false;
Model.concurrency = null;
function instanceQuery({ instance, transaction }) {
const modelClass = instance.constructor;
return modelClass
.query(transaction)
.findOperationFactory(() => {
return new InstanceFindOperation('find', { instance });
})
.insertOperationFactory(() => {
return new InstanceInsertOperation('insert', { instance });
})
.updateOperationFactory(() => {
return new InstanceUpdateOperation('update', { instance });
})
.patchOperationFactory(() => {
return new InstanceUpdateOperation('patch', {
instance,
modelOptions: { patch: true },
});
})
.deleteOperationFactory(() => {
return new InstanceDeleteOperation('delete', { instance });
})
.relateOperationFactory(() => {
throw new Error('`relate` makes no sense in this context');
})
.unrelateOperationFactory(() => {
throw new Error('`unrelate` makes no sense in this context');
});
}
function relatedQuery({ modelClass, relationName, transaction, alwaysReturnArray } = {}) {
const relation = modelClass.getRelation(relationName);
const relatedModelClass = relation.relatedModelClass;
return relatedModelClass
.query(transaction)
.findOperationFactory((builder) => {
const isSubQuery = !builder.for();
const owner = isSubQuery
? RelationOwner.createParentReference(builder, relation)
: RelationOwner.create(builder.for());
const operation = relation.find(builder, owner);
operation.assignResultToOwner = modelClass.getRelatedFindQueryMutates();
operation.alwaysReturnArray = alwaysReturnArray;
operation.alias = isSubQuery ? relation.name : null;
return operation;
})
.insertOperationFactory((builder) => {
const owner = RelationOwner.create(builder.for());
const operation = relation.insert(builder, owner);
operation.assignResultToOwner = modelClass.getRelatedInsertQueryMutates();
return operation;
})
.updateOperationFactory((builder) => {
const owner = RelationOwner.create(builder.for());
return relation.update(builder, owner);
})
.patchOperationFactory((builder) => {
const owner = RelationOwner.create(builder.for());
return relation.patch(builder, owner);
})
.deleteOperationFactory((builder) => {
const owner = RelationOwner.create(builder.for());
return relation.delete(builder, owner);
})
.relateOperationFactory((builder) => {
const owner = RelationOwner.create(builder.for());
return relation.relate(builder, owner);
})
.unrelateOperationFactory((builder) => {
const owner = RelationOwner.create(builder.for());
return relation.unrelate(builder, owner);
});
}
function cachedGet(target, hiddenPropertyName, creator) {
if (!target.hasOwnProperty(hiddenPropertyName)) {
defineNonEnumerableProperty(target, hiddenPropertyName, creator(target));
}
return target[hiddenPropertyName];
}
function getValidator(modelClass) {
return modelClass.createValidator();
}
function getJsonSchema(modelClass) {
return modelClass.jsonSchema;
}
function getColumnNameMappers(modelClass) {
return modelClass.columnNameMappers;
}
function getIdRelationProperty(modelClass) {
const idColumn = asArray(modelClass.getIdColumn());
return new RelationProperty(
idColumn.map((idCol) => `${modelClass.getTableName()}.${idCol}`),
() => modelClass,
);
}
function getReadOnlyAttributes(modelClass) {
return [...new Set(getReadOnlyAttributesRecursively(modelClass))];
}
function getReadOnlyAttributesRecursively(modelClass) {
if (modelClass === Model || modelClass.prototype == undefined) {
// Stop recursion to the model class or its prototype is null or undefined.
return [];
}
const propertyNames = Object.getOwnPropertyNames(modelClass.prototype);
return [
...getReadOnlyAttributes(Object.getPrototypeOf(modelClass)),
...propertyNames.filter((propName) => {
const desc = Object.getOwnPropertyDescriptor(modelClass.prototype, propName);
return (desc.get && !desc.set) || desc.writable === false || isFunction(desc.value);
}),
];
}
function getRelationMappings(modelClass) {
let relationMappings = modelClass.relationMappings;
if (isFunction(relationMappings)) {
relationMappings = relationMappings.call(modelClass);
}
return relationMappings || {};
}
function getRelationNames(modelClass) {
return Object.keys(modelClass.getRelationMappings());
}
function getVirtualAttributes(modelClass) {
return modelClass.virtualAttributes || [];
}
function getTraverseArgs(filterConstructor, models, traverser) {
filterConstructor = filterConstructor || null;
if (traverser === undefined) {
traverser = models;
models = filterConstructor;
filterConstructor = null;
}
if (!isFunction(traverser)) {
throw new Error('traverser must be a function');
}
return {
traverser,
models,
filterConstructor,
};
}
function asPropsArray(props) {
if (props.length === 1) {
const arg = props[0];
if (Array.isArray(arg)) {
return arg;
} else if (arg && typeof arg === 'object') {
return Object.entries(arg)
.filter(([, value]) => value)
.map(([key]) => key);
}
}
return props;
}
module.exports = {
Model,
};
================================================
FILE: lib/model/ModifierNotFoundError.js
================================================
'use strict';
class ModifierNotFoundError extends Error {
constructor(modifierName) {
super(`Unable to determine modify function from provided value: "${modifierName}".`);
this.modifierName = modifierName;
}
}
module.exports = {
ModifierNotFoundError,
};
================================================
FILE: lib/model/NotFoundError.js
================================================
'use strict';
class NotFoundError extends Error {
constructor({ modelClass, data = {}, statusCode = 404, ...rest } = {}) {
super(rest.message || 'NotFoundError');
this.type = 'NotFound';
this.name = this.constructor.name;
this.data = { ...rest, ...data };
this.statusCode = statusCode;
// Add as non-enumerable in case people are passing instances of
// this error directly to `JSON.stringify`.
Object.defineProperty(this, 'modelClass', {
value: modelClass,
enumerable: false,
});
}
}
module.exports = {
NotFoundError,
};
================================================
FILE: lib/model/RelationDoesNotExistError.js
================================================
'use strict';
class RelationDoesNotExistError extends Error {
constructor(relationName) {
super(`unknown relation "${relationName}" in a relation expression`);
this.name = this.constructor.name;
this.relationName = relationName;
}
}
module.exports = {
RelationDoesNotExistError,
};
================================================
FILE: lib/model/ValidationError.js
================================================
'use strict';
const { asArray, isString } = require('../utils/objectUtils');
const ValidationErrorType = {
ModelValidation: 'ModelValidation',
RelationExpression: 'RelationExpression',
UnallowedRelation: 'UnallowedRelation',
InvalidGraph: 'InvalidGraph',
};
class ValidationError extends Error {
static get Type() {
return ValidationErrorType;
}
constructor({ type, message, modelClass, data = {}, statusCode = 400 }) {
super(message || errorsToMessage(data));
this.name = this.constructor.name;
this.type = type;
this.data = data;
this.statusCode = statusCode;
// Add as non-enumerable in case people are passing instances of
// this error directly to `JSON.stringify`.
Object.defineProperty(this, 'modelClass', {
value: modelClass,
enumerable: false,
});
}
}
function errorsToMessage(data) {
return Object.keys(data)
.reduce((messages, key) => {
messages.push(`${key}: ${asArray(data[key]).map(message).join(', ')}`);
return messages;
}, [])
.join(', ');
}
function message(it) {
if (isString(it)) {
return it;
} else {
return it.message;
}
}
module.exports = {
ValidationError,
ValidationErrorType,
};
================================================
FILE: lib/model/Validator.js
================================================
'use strict';
class Validator {
constructor(...args) {
this.constructor.init(this, ...args);
}
static init() {}
beforeValidate({ model, json, options }) {
model.$beforeValidate(null, json, options);
}
validate() {
/* istanbul ignore next */
throw new Error('not implemented');
}
afterValidate({ model, json, options }) {
model.$afterValidate(json, options);
}
}
module.exports = {
Validator,
};
================================================
FILE: lib/model/getModel.js
================================================
'use strict';
// A small helper method for cached lazy-importing of the Model class.
let Model;
const getModel = () => Model || (Model = require('./Model').Model);
module.exports = {
getModel,
};
================================================
FILE: lib/model/graph/ModelGraph.js
================================================
'use strict';
const { ModelGraphBuilder } = require('./ModelGraphBuilder');
const NOT_CALCULATED = {};
class ModelGraph {
constructor(nodes, edges) {
this.nodes = nodes;
this.edges = edges;
// These are calculated lazily.
this._nodesByObjects = NOT_CALCULATED;
this._nodesByIdPathKeys = NOT_CALCULATED;
}
static create(rootModelClass, roots) {
const builder = ModelGraphBuilder.buildGraph(rootModelClass, roots);
return new ModelGraph(builder.nodes, builder.edges);
}
static createEmpty() {
return new ModelGraph([], []);
}
get rootObjects() {
return this.nodes.filter((node) => !node.parentEdge).map((node) => node.obj);
}
nodeForObject(obj) {
if (!obj) {
return null;
}
if (this._nodesByObjects === NOT_CALCULATED) {
this._nodesByObjects = createNodesByObjectsMap(this.nodes);
}
return this._nodesByObjects.get(obj) || null;
}
nodeForNode(node) {
if (!node) {
return null;
}
if (this._nodesByIdPathKeys === NOT_CALCULATED) {
this._nodesByIdPathKeys = createNodesByIdPathKeysMap(this.nodes);
}
return this._nodesByIdPathKeys.get(node.idPathKey) || null;
}
}
function createNodesByObjectsMap(nodes) {
const nodesByObjects = new Map();
for (const node of nodes) {
nodesByObjects.set(node.obj, node);
}
return nodesByObjects;
}
function createNodesByIdPathKeysMap(nodes) {
const nodesByIdPathKeys = new Map();
for (const node of nodes) {
const idPathKey = node.idPathKey;
if (idPathKey !== null) {
nodesByIdPathKeys.set(idPathKey, node);
}
}
return nodesByIdPathKeys;
}
module.exports = {
ModelGraph,
};
================================================
FILE: lib/model/graph/ModelGraphBuilder.js
================================================
'use strict';
const { isObject, isString, asArray, asSingle } = require('../../utils/objectUtils');
const { ValidationErrorType } = require('../../model/ValidationError');
const { ModelGraphNode } = require('./ModelGraphNode');
const { ModelGraphEdge } = require('./ModelGraphEdge');
class ModelGraphBuilder {
constructor() {
this.nodes = [];
this.edges = [];
}
static buildGraph(rootModelClass, roots) {
const builder = new this();
builder._buildGraph(rootModelClass, roots);
return builder;
}
_buildGraph(rootModelClass, roots) {
if (roots) {
if (Array.isArray(roots)) {
this._buildNodes(rootModelClass, roots);
} else if (isObject(roots)) {
this._buildNode(rootModelClass, roots);
} else {
throw createNotModelError(rootModelClass, roots);
}
}
this._buildReferences();
}
_buildNodes(modelClass, objs, parentNode = null, relation = null) {
objs = asArray(objs);
objs.forEach((obj, index) => {
this._buildNode(modelClass, obj, parentNode, relation, index);
});
}
_buildNode(modelClass, obj, parentNode = null, relation = null, index = null) {
obj = asSingle(obj);
if (!isObject(obj) || !obj.$isObjectionModel) {
throw createNotModelError(modelClass, obj);
}
const node = new ModelGraphNode(modelClass, obj);
this.nodes.push(node);
if (parentNode) {
const edge = new ModelGraphEdge(
ModelGraphEdge.Type.Relation,
parentNode,
node,
relation,
index,
);
node.parentEdge = edge;
this._addEdge(parentNode, node, edge);
}
this._buildRelationNodes(node);
}
_buildRelationNodes(node) {
for (const relationName of node.modelClass.getRelationNames()) {
const relatedObjects = node.obj[relationName];
if (!relatedObjects) {
continue;
}
const relation = node.modelClass.getRelation(relationName);
if (relation.isOneToOne()) {
this._buildNode(relation.relatedModelClass, relatedObjects, node, relation);
} else {
this._buildNodes(relation.relatedModelClass, relatedObjects, node, relation);
}
}
}
_buildReferences() {
const nodesByUid = this._nodesByUid();
this._buildObjectReferences(nodesByUid);
this._buildPropertyReferences(nodesByUid);
}
_nodesByUid() {
const nodesByUid = new Map();
for (const node of this.nodes) {
const uid = node.uid;
if (uid === undefined) {
continue;
}
nodesByUid.set(uid, node);
}
return nodesByUid;
}
_buildObjectReferences(nodesByUid) {
for (const node of this.nodes) {
const ref = node.reference;
if (ref === undefined) {
continue;
}
const refNode = nodesByUid.get(ref);
if (!refNode) {
throw createReferenceNotFoundError(ref);
}
const edge = new ModelGraphEdge(ModelGraphEdge.Type.Reference, node, refNode);
edge.refType = ModelGraphEdge.ReferenceType.Object;
this._addEdge(node, refNode, edge);
}
}
_buildPropertyReferences(nodesByUid) {
for (const node of this.nodes) {
forEachPropertyReference(node.obj, ({ path, refMatch, ref, refPath }) => {
const refNode = nodesByUid.get(ref);
if (!refNode) {
throw createReferenceNotFoundError(ref);
}
const edge = new ModelGraphEdge(ModelGraphEdge.Type.Reference, node, refNode);
edge.refType = ModelGraphEdge.ReferenceType.Property;
edge.refMatch = refMatch;
edge.refOwnerDataPath = path.slice();
edge.refRelatedDataPath = refPath;
this._addEdge(node, refNode, edge);
});
}
}
_addEdge(ownerNode, relatedNode, edge) {
this.edges.push(edge);
ownerNode.edges.push(edge);
relatedNode.edges.push(edge);
if (edge.type === ModelGraphEdge.Type.Reference) {
ownerNode.refEdges.push(edge);
relatedNode.refEdges.push(edge);
}
}
}
function forEachPropertyReference(obj, callback) {
const modelClass = obj.constructor;
const relationNames = modelClass.getRelationNames();
for (const prop of Object.keys(obj)) {
if (relationNames.includes(prop)) {
continue;
}
visitStrings(obj[prop], [prop], (str, path) => {
forEachMatch(modelClass.propRefRegex, str, (match) => {
const [refMatch, ref, refPath] = match;
callback({ path, refMatch, ref, refPath: refPath.trim().split('.') });
});
});
}
}
function visitStrings(value, path, visit) {
if (Array.isArray(value)) {
visitStringsInArray(value, path, visit);
} else if (isObject(value) && !Buffer.isBuffer(value)) {
visitStringsInObject(value, path, visit);
} else if (isString(value)) {
visit(value, path);
}
}
function visitStringsInArray(value, path, visit) {
for (let i = 0; i < value.length; ++i) {
path.push(i);
visitStrings(value[i], path, visit);
path.pop();
}
}
function visitStringsInObject(value, path, visit) {
for (const prop of Object.keys(value)) {
path.push(prop);
visitStrings(value[prop], path, visit);
path.pop();
}
}
function forEachMatch(regex, str, cb) {
let matchResult = regex.exec(str);
while (matchResult) {
cb(matchResult);
matchResult = regex.exec(str);
}
}
function createReferenceNotFoundError(ref) {
return new Error(
[
`could not resolve reference ${ref} in a graph.`,
`If you are sure the #id exist in the same graph,`,
`this may be due to a limitation that a subgraph under a related item`,
`cannot reference an item in a subgraph under another related item.`,
`If you run into this limitation, please open an issue in objection github.`,
].join(' '),
);
}
function createNotModelError(modelClass, value) {
throw modelClass.createValidationError({
type: ValidationErrorType.InvalidGraph,
message: `expected value "${value}" to be an instance of ${modelClass.name}`,
});
}
module.exports = {
ModelGraphBuilder,
createNotModelError,
forEachPropertyReference,
};
================================================
FILE: lib/model/graph/ModelGraphEdge.js
================================================
'use strict';
const Type = {
Relation: 'Relation',
Reference: 'Reference',
};
const ReferenceType = {
Object: 'Object',
Property: 'Property',
};
class ModelGraphEdge {
constructor(type, ownerNode, relatedNode, relation = null, relationIndex = null) {
this.type = type;
this.ownerNode = ownerNode;
this.relatedNode = relatedNode;
this.relation = relation;
this.relationIndex = relationIndex;
this.refType = null;
this.refMatch = null;
this.refOwnerDataPath = null;
this.refRelatedDataPath = null;
}
static get Type() {
return Type;
}
static get ReferenceType() {
return ReferenceType;
}
getOtherNode(node) {
return this.isOwnerNode(node) ? this.relatedNode : this.ownerNode;
}
isOwnerNode(node) {
return node === this.ownerNode;
}
isRelatedNode(node) {
return node === this.relatedNode;
}
}
module.exports = {
ModelGraphEdge,
};
================================================
FILE: lib/model/graph/ModelGraphNode.js
================================================
'use strict';
const { ModelGraphEdge } = require('./ModelGraphEdge');
const { isNumber } = require('../../utils/objectUtils');
const NOT_CALCULATED = {};
class ModelGraphNode {
constructor(modelClass, obj) {
this.modelClass = modelClass;
this.obj = obj;
this.edges = [];
this.userData = {};
this.hasId = obj.$hasId();
this.uid = obj[modelClass.uidProp];
// These are also included in `edges`. These are simply
// shortcuts for commonly used edges.
this.refEdges = [];
this.parentEdge = null;
// These are calculated lazily.
this._relationPath = NOT_CALCULATED;
this._relationPathKey = NOT_CALCULATED;
this._dataPath = NOT_CALCULATED;
this._dataPathKey = NOT_CALCULATED;
this._idPath = NOT_CALCULATED;
this._idPathKey = NOT_CALCULATED;
}
get isReference() {
return this.reference !== undefined;
}
get isDbReference() {
return this.dbReference !== undefined;
}
get reference() {
return this.obj[this.modelClass.uidRefProp];
}
get dbReference() {
return this.obj[this.modelClass.dbRefProp];
}
get parentNode() {
if (this.parentEdge) {
return this.parentEdge.ownerNode;
} else {
return null;
}
}
get indexInRelation() {
if (this.parentEdge) {
return this.parentEdge.relationIndex;
} else {
return null;
}
}
get relationName() {
if (this.parentEdge) {
return this.parentEdge.relation.name;
} else {
return null;
}
}
get relationPath() {
if (this._relationPath === NOT_CALCULATED) {
this._relationPath = this._createRelationPath();
}
return this._relationPath;
}
get relationPathKey() {
if (this._relationPathKey === NOT_CALCULATED) {
this._relationPathKey = this._createRelationPathKey();
}
return this._relationPathKey;
}
get dataPath() {
if (this._dataPath === NOT_CALCULATED) {
this._dataPath = this._createDataPath();
}
return this._dataPath;
}
get dataPathKey() {
if (this._dataPathKey === NOT_CALCULATED) {
this._dataPathKey = this._createDataPathKey();
}
return this._dataPathKey;
}
get idPath() {
if (this._idPath === NOT_CALCULATED) {
this._idPath = this._createIdPath();
}
return this._idPath;
}
get idPathKey() {
if (this._idPathKey === NOT_CALCULATED) {
this._idPathKey = this._createIdPathKey();
}
return this._idPathKey;
}
/**
* If this node is a reference, returns the referred node.
*/
get referencedNode() {
for (const edge of this.refEdges) {
if (edge.refType === ModelGraphEdge.ReferenceType.Object && edge.isOwnerNode(this)) {
return edge.relatedNode;
}
}
return null;
}
/**
* Returns all nodes that are references to this node.
*/
get referencingNodes() {
const nodes = [];
for (const edge of this.refEdges) {
if (edge.refType === ModelGraphEdge.ReferenceType.Object && edge.isRelatedNode(this)) {
nodes.push(edge.ownerNode);
}
}
return nodes;
}
get descendantRelationNodes() {
return this._collectDescendantRelationNodes([]);
}
removeEdge(edge) {
// Don't allow removing parent edges for now. It would
// cause all kinds of cache invalidation.
if (edge === this.parentEdge) {
throw new Error('cannot remove parent edge');
}
this.edges = this.edges.filter((it) => it !== edge);
this.refEdges = this.refEdges.filter((it) => it !== edge);
}
_collectDescendantRelationNodes(nodes) {
for (const edge of this.edges) {
if (edge.type === ModelGraphEdge.Type.Relation && edge.isOwnerNode(this)) {
nodes.push(edge.relatedNode);
edge.relatedNode._collectDescendantRelationNodes(nodes);
}
}
return nodes;
}
_createRelationPath() {
if (this.parentNode === null) {
return [];
} else {
return [...this.parentNode.relationPath, this.relationName];
}
}
_createRelationPathKey() {
return this.relationPath.join('.');
}
_createDataPath() {
if (this.parentEdge === null) {
return [];
} else if (this.parentEdge.relation.isOneToOne()) {
return [...this.parentNode.dataPath, this.relationName];
} else {
return [...this.parentNode.dataPath, this.relationName, this.indexInRelation];
}
}
_createDataPathKey() {
const dataPathKey = this.dataPath.reduce((key, it) => {
if (isNumber(it)) {
return `${key}[${it}]`;
} else {
return key ? `${key}.${it}` : it;
}
}, '');
return dataPathKey ? '.' + dataPathKey : dataPathKey;
}
_createIdPath() {
if (!this.obj.$hasId()) {
return null;
}
if (this.parentEdge === null) {
return [this.obj.$idKey()];
} else {
const path = this.parentNode.idPath;
if (path === null) {
return null;
}
return [...path, this.relationName, this.obj.$idKey()];
}
}
_createIdPathKey() {
const idPath = this.idPath;
if (idPath) {
return this.idPath.join('.');
} else {
return null;
}
}
}
module.exports = {
ModelGraphNode,
};
================================================
FILE: lib/model/inheritModel.js
================================================
'use strict';
const cache = new Map();
function inheritModel(modelClass) {
let inherit = cache.get(modelClass.name);
if (!inherit) {
inherit = createClassInheritor(modelClass.name);
cache.set(modelClass.name, inherit);
}
return inherit(modelClass);
}
function createClassInheritor(className) {
return new Function(
'BaseClass',
`
'use strict';
return class ${className} extends BaseClass {}
`,
);
}
module.exports = {
inheritModel,
};
================================================
FILE: lib/model/modelBindKnex.js
================================================
'use strict';
const { inheritModel } = require('./inheritModel');
const { staticHiddenProps } = require('./modelUtils');
const { defineNonEnumerableProperty } = require('./modelUtils');
function bindKnex(modelClass, knex) {
let BoundModelClass = getBoundModelFromCache(modelClass, knex);
if (BoundModelClass === null) {
BoundModelClass = inheritModel(modelClass);
BoundModelClass = copyHiddenProperties(modelClass, BoundModelClass);
BoundModelClass.knex(knex);
BoundModelClass = putBoundModelToCache(modelClass, BoundModelClass, knex);
BoundModelClass = bindRelations(modelClass, BoundModelClass, knex);
}
return BoundModelClass;
}
function getBoundModelFromCache(modelClass, knex) {
const cache = getCache(knex);
const cacheKey = modelClass.uniqueTag();
return cache.get(cacheKey) || null;
}
function getCache(knex) {
if (!knex.$$objection) {
createCache(knex);
}
return knex.$$objection.boundModels;
}
function createCache(knex) {
defineNonEnumerableProperty(knex, '$$objection', {
boundModels: new Map(),
});
}
function copyHiddenProperties(modelClass, BoundModelClass) {
for (const prop of staticHiddenProps) {
// $$relations and $$relationArray are handled in separately.
if (modelClass.hasOwnProperty(prop) && prop !== '$$relations' && prop !== '$$relationArray') {
defineNonEnumerableProperty(BoundModelClass, prop, modelClass[prop]);
}
}
return BoundModelClass;
}
function putBoundModelToCache(modelClass, BoundModelClass, knex) {
const cache = getCache(knex);
const cacheKey = modelClass.uniqueTag();
cache.set(cacheKey, BoundModelClass);
return BoundModelClass;
}
function bindRelations(modelClass, BoundModelClass, knex) {
const boundRelations = Object.create(null);
const boundRelationArray = [];
for (const relationName of modelClass.getRelationNames()) {
const relation = modelClass.getRelation(relationName);
const boundRelation = relation.bindKnex(knex);
boundRelations[relation.name] = boundRelation;
boundRelationArray.push(boundRelation);
}
defineNonEnumerableProperty(BoundModelClass, '$$relations', boundRelations);
defineNonEnumerableProperty(BoundModelClass, '$$relationArray', boundRelationArray);
return BoundModelClass;
}
module.exports = {
bindKnex,
};
================================================
FILE: lib/model/modelClone.js
================================================
'use strict';
const { isObject, cloneDeep } = require('../utils/objectUtils');
const { hiddenProps } = require('./modelUtils');
const { defineNonEnumerableProperty } = require('./modelUtils');
const { isInternalProp } = require('../utils/internalPropUtils');
function clone(model, shallow, stripInternal) {
let clone = null;
const omitFromJson = model.$omitFromJson();
const omitFromDatabaseJson = model.$omitFromDatabaseJson();
if (!shallow && !stripInternal) {
clone = cloneSimple(model);
} else {
clone = cloneWithOpt(model, shallow, stripInternal);
}
if (omitFromJson) {
clone.$omitFromJson(omitFromJson);
}
if (omitFromDatabaseJson) {
clone.$omitFromDatabaseJson(omitFromDatabaseJson);
}
clone = copyHiddenProps(model, clone);
return clone;
}
function cloneSimple(model) {
const clone = new model.constructor();
const keys = Object.keys(model);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
const value = model[key];
if (isObject(value)) {
clone[key] = cloneObject(value);
} else {
clone[key] = value;
}
}
return clone;
}
function cloneWithOpt(model, shallow, stripInternal) {
const clone = new model.constructor();
const keys = Object.keys(model);
const relationNames = model.constructor.getRelationNames();
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
const value = model[key];
if (shallow && relationNames.includes(key)) {
// The constructor may have given default values for relations.
delete clone[key];
continue;
}
if (stripInternal && isInternalProp(key)) {
continue;
}
if (isObject(value)) {
clone[key] = cloneObject(value);
} else {
clone[key] = value;
}
}
return clone;
}
function cloneObject(value) {
if (Array.isArray(value)) {
return cloneArray(value);
} else if (value.$isObjectionModel) {
return clone(value, false, false);
} else if (Buffer.isBuffer(value)) {
return new Buffer(value);
} else {
return cloneDeep(value);
}
}
function cloneArray(value) {
const ret = new Array(value.length);
for (let i = 0, l = ret.length; i < l; ++i) {
const item = value[i];
if (isObject(item)) {
ret[i] = cloneObject(item);
} else {
ret[i] = item;
}
}
return ret;
}
function copyHiddenProps(model, clone) {
for (let i = 0, l = hiddenProps.length; i < l; ++i) {
const prop = hiddenProps[i];
if (model.hasOwnProperty(prop)) {
defineNonEnumerableProperty(clone, prop, model[prop]);
}
}
return clone;
}
module.exports = {
clone,
};
================================================
FILE: lib/model/modelColPropMap.js
================================================
'use strict';
const { difference } = require('../utils/objectUtils');
function columnNameToPropertyName(modelClass, columnName) {
const model = new modelClass();
const addedProps = Object.keys(model.$parseDatabaseJson({}));
const row = {};
row[columnName] = null;
const props = Object.keys(model.$parseDatabaseJson(row));
const propertyName = difference(props, addedProps)[0];
return propertyName || columnName;
}
function propertyNameToColumnName(modelClass, propertyName) {
const model = new modelClass();
const addedCols = Object.keys(model.$formatDatabaseJson({}));
const obj = {};
obj[propertyName] = null;
const cols = Object.keys(model.$formatDatabaseJson(obj));
const columnName = difference(cols, addedCols)[0];
return columnName || propertyName;
}
module.exports = {
columnNameToPropertyName,
propertyNameToColumnName,
};
================================================
FILE: lib/model/modelId.js
================================================
'use strict';
function getSetId(model, maybeId) {
if (maybeId !== undefined) {
return setId(model, maybeId);
} else {
return getId(model);
}
}
function hasId(model) {
return model.$hasProps(model.constructor.getIdPropertyArray());
}
function setId(model, id) {
const idProp = model.constructor.getIdProperty();
const isCompositeId = Array.isArray(idProp);
if (Array.isArray(id)) {
if (isCompositeId) {
if (id.length !== idProp.length) {
throw new Error('trying to set an invalid identifier for a model');
}
for (let i = 0; i < id.length; ++i) {
model[idProp[i]] = id[i];
}
} else {
if (id.length !== 1) {
throw new Error('trying to set an invalid identifier for a model');
}
model[idProp] = id[0];
}
} else {
if (isCompositeId) {
if (idProp.length > 1) {
throw new Error('trying to set an invalid identifier for a model');
}
model[idProp[0]] = id;
} else {
model[idProp] = id;
}
}
}
function getId(model) {
const idProp = model.constructor.getIdProperty();
const isCompositeId = Array.isArray(idProp);
if (isCompositeId) {
return model.$values(idProp);
} else {
return model[idProp];
}
}
module.exports = {
getSetId,
hasId,
};
================================================
FILE: lib/model/modelJsonAttributes.js
================================================
'use strict';
const { asArray, isObject, flatten, isString } = require('../utils/objectUtils');
function parseJsonAttributes(json, modelClass) {
const jsonAttr = modelClass.getJsonAttributes();
if (jsonAttr.length) {
// JSON attributes may be returned as strings depending on the database and
// the database client. Convert them to objects here.
for (let i = 0, l = jsonAttr.length; i < l; ++i) {
const attr = jsonAttr[i];
const value = json[attr];
if (isString(value)) {
const parsed = tryParseJson(value);
// tryParseJson returns undefined if parsing failed.
if (parsed !== undefined) {
json[attr] = parsed;
}
}
}
}
return json;
}
function formatJsonAttributes(json, modelClass) {
const jsonAttr = modelClass.getJsonAttributes();
if (jsonAttr.length) {
// All database clients want JSON columns as strings. Do the conversion here.
for (let i = 0, l = jsonAttr.length; i < l; ++i) {
const attr = jsonAttr[i];
const value = json[attr];
if (value != null) {
json[attr] = JSON.stringify(value);
}
}
}
return json;
}
function getJsonAttributes(modelClass) {
let jsonAttributes = modelClass.jsonAttributes;
if (Array.isArray(jsonAttributes)) {
return jsonAttributes;
}
jsonAttributes = [];
if (modelClass.getJsonSchema()) {
const props = modelClass.getJsonSchema().properties || {};
for (const propName of Object.keys(props)) {
const prop = props[propName];
let types = asArray(prop.type).filter((it) => !!it);
if (types.length === 0 && Array.isArray(prop.anyOf)) {
types = flatten(prop.anyOf.map((it) => it.type));
}
if (types.length === 0 && Array.isArray(prop.oneOf)) {
types = flatten(prop.oneOf.map((it) => it.type));
}
if (types.indexOf('object') !== -1 || types.indexOf('array') !== -1) {
jsonAttributes.push(propName);
}
}
}
return jsonAttributes;
}
function tryParseJson(maybeJsonStr) {
try {
return JSON.parse(maybeJsonStr);
} catch (err) {
return undefined;
}
}
module.exports = {
parseJsonAttributes,
formatJsonAttributes,
getJsonAttributes,
};
================================================
FILE: lib/model/modelParseRelations.js
================================================
'use strict';
const { isObject } = require('../utils/objectUtils');
function parseRelationsIntoModelInstances(model, json, options = {}) {
if (!options.cache) {
options = Object.assign({}, options, {
cache: new Map(),
});
}
options.cache.set(json, model);
for (const relationName of model.constructor.getRelationNames()) {
const relationJson = json[relationName];
if (relationJson !== undefined) {
const relation = model.constructor.getRelation(relationName);
const relationModel = parseRelation(relationJson, relation, options);
if (relationModel !== relationJson) {
model[relation.name] = relationModel;
}
}
}
return model;
}
function parseRelation(json, relation, options) {
if (Array.isArray(json)) {
return parseRelationArray(json, relation, options);
} else if (json) {
return parseRelationObject(json, relation, options);
} else {
return null;
}
}
function parseRelationArray(json, relation, options) {
const models = new Array(json.length);
let didChange = false;
for (let i = 0, l = json.length; i < l; ++i) {
const model = parseRelationObject(json[i], relation, options);
if (model !== json[i]) {
didChange = true;
}
models[i] = model;
}
if (didChange) {
return models;
} else {
return json;
}
}
function parseRelationObject(json, relation, options) {
if (isObject(json)) {
const modelClass = relation.relatedModelClass;
let model = options.cache.get(json);
if (model === undefined) {
if (json instanceof modelClass) {
model = parseRelationsIntoModelInstances(json, json, options);
} else {
model = modelClass.fromJson(json, options);
}
}
return model;
} else {
return json;
}
}
module.exports = {
parseRelationsIntoModelInstances,
};
================================================
FILE: lib/model/modelQueryProps.js
================================================
'use strict';
const { isObject, isFunction } = require('../utils/objectUtils');
const { defineNonEnumerableProperty } = require('./modelUtils');
const { isKnexRaw, isKnexQueryBuilder } = require('../utils/knexUtils');
const QUERY_PROPS_PROPERTY = '$$queryProps';
// Removes query properties from `json` and stores them into a hidden property
// inside `model` so that they can be later merged back to `json`.
function splitQueryProps(model, json) {
const keys = Object.keys(json);
if (hasQueryProps(json, keys)) {
const queryProps = {};
const modelProps = {};
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
const value = json[key];
if (isQueryProp(value)) {
queryProps[key] = value;
} else {
modelProps[key] = value;
}
}
defineNonEnumerableProperty(model, QUERY_PROPS_PROPERTY, queryProps);
return modelProps;
} else {
return json;
}
}
function hasQueryProps(json, keys) {
for (let i = 0, l = keys.length; i < l; ++i) {
if (isQueryProp(json[keys[i]])) {
return true;
}
}
return false;
}
function isQueryProp(value) {
if (!isObject(value)) {
return false;
}
return (
isKnexQueryBuilder(value) ||
isKnexRaw(value) ||
isKnexRawConvertable(value) ||
value.isObjectionQueryBuilderBase
);
}
// Merges and converts `model`'s query properties into `json`.
function mergeQueryProps(model, json, omitProps, builder) {
json = convertExistingQueryProps(json, builder);
json = convertAndMergeHiddenQueryProps(model, json, omitProps, builder);
return json;
}
// Converts the query properties in `json` to knex raw instances.
// `json` may have query properties even though we removed them.
// For example they may have been added in lifecycle hooks.
function convertExistingQueryProps(json, builder) {
const keys = Object.keys(json);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
const value = json[key];
if (isQueryProp(value)) {
json[key] = queryPropToKnexRaw(value, builder);
}
}
return json;
}
// Converts and merges the query props that were split from the model
// and stored into QUERY_PROPS_PROPERTY.
function convertAndMergeHiddenQueryProps(model, json, omitProps, builder) {
const queryProps = model[QUERY_PROPS_PROPERTY];
if (!queryProps) {
// The model has no query properties.
return json;
}
const modelClass = model.constructor;
const keys = Object.keys(queryProps);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
if (!omitProps || !omitProps.includes(key)) {
const queryProp = queryPropToKnexRaw(queryProps[key], builder);
json[modelClass.propertyNameToColumnName(key)] = queryProp;
}
}
return json;
}
// Converts a query property into a knex `raw` instance.
function queryPropToKnexRaw(queryProp, builder) {
if (!queryProp) {
return queryProp;
}
if (queryProp.isObjectionQueryBuilderBase) {
return buildObjectionQueryBuilder(queryProp, builder);
} else if (isKnexRawConvertable(queryProp)) {
return buildKnexRawConvertable(queryProp, builder);
} else {
return queryProp;
}
}
function buildObjectionQueryBuilder(builder, parentBuilder) {
return builder.subqueryOf(parentBuilder).toKnexQuery();
}
function buildKnexRawConvertable(convertable, builder) {
if (!builder) {
throw new Error(
'toDatabaseJson called without a query builder instance for a model with query properties',
);
}
return convertable.toKnexRaw(builder);
}
function isKnexRawConvertable(queryProp) {
return isFunction(queryProp.toKnexRaw);
}
module.exports = {
splitQueryProps,
mergeQueryProps,
};
================================================
FILE: lib/model/modelSet.js
================================================
'use strict';
const { isInternalProp } = require('../utils/internalPropUtils');
const { splitQueryProps } = require('./modelQueryProps');
const { isFunction, isString, isPlainObject } = require('../utils/objectUtils');
const { parseRelationsIntoModelInstances } = require('./modelParseRelations');
function setJson(model, json, options) {
json = json || {};
options = options || {};
if (Object.prototype.toString.call(json) !== '[object Object]') {
throw new Error(
'You should only pass objects to $setJson method. ' +
'$setJson method was given an invalid value ' +
json,
);
}
if (!json.$isObjectionModel) {
// json can contain "query properties" like `raw` instances, query builders etc.
// We take them out of `json` and store them to a hidden property $$queryProps
// in the model instance for later use.
json = splitQueryProps(model, json);
}
json = model.$parseJson(json, options);
json = model.$validate(json, options);
model.$set(json);
if (!options.skipParseRelations) {
parseRelationsIntoModelInstances(model, json, options);
}
return model;
}
function setDatabaseJson(model, json) {
json = model.$parseDatabaseJson(json);
if (json) {
const keys = Object.keys(json);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
model[key] = json[key];
}
}
return model;
}
function setFast(model, obj) {
if (obj) {
// Don't try to set read-only properties. They can easily get here
// through `fromJson` when parsing an object that was previously
// serialized from a model instance.
const readOnlyAttr = model.constructor.getReadOnlyAttributes();
const keys = Object.keys(obj);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
const value = obj[key];
if (
(value !== undefined || isPlainObject(obj)) &&
!isInternalProp(key) &&
!isFunction(value) &&
!readOnlyAttr.includes(key)
) {
model[key] = value;
}
}
}
return model;
}
function setRelated(model, relation, models) {
relation = ensureRelation(model, relation);
if (relation.isOneToOne()) {
if (Array.isArray(models)) {
if (models.length === 0) {
model[relation.name] = null;
} else {
model[relation.name] = models[0] || null;
}
} else {
model[relation.name] = models || null;
}
} else {
if (!models) {
model[relation.name] = [];
} else if (Array.isArray(models)) {
model[relation.name] = models;
} else {
model[relation.name] = [models];
}
}
return model;
}
function appendRelated(model, relation, models) {
relation = ensureRelation(model, relation);
if (!model[relation.name] || relation.isOneToOne()) {
return model.$setRelated(relation, models);
} else {
if (Array.isArray(models)) {
models.forEach((it) => model[relation.name].push(it));
} else if (models) {
model[relation.name].push(models);
}
}
return model;
}
function ensureRelation(model, relation) {
if (isString(relation)) {
return model.constructor.getRelation(relation);
} else {
return relation;
}
}
module.exports = {
setFast,
setJson,
setDatabaseJson,
setRelated,
appendRelated,
};
================================================
FILE: lib/model/modelTableMetadata.js
================================================
'use strict';
const { defineNonEnumerableProperty } = require('./modelUtils');
const { isPromise } = require('../utils/promiseUtils');
const TABLE_METADATA = '$$tableMetadata';
function fetchTableMetadata(
modelClass,
{ parentBuilder = null, knex = null, force = false, table = null } = {},
) {
// The table isn't necessarily same as `modelClass.getTableName()` for example if
// a view is queried instead.
if (!table) {
if (parentBuilder) {
table = parentBuilder.tableNameFor(modelClass);
} else {
table = modelClass.getTableName();
}
}
// Call tableMetadata first instead of accessing the cache directly beause
// tableMetadata may have been overriden.
let metadata = modelClass.tableMetadata({ table });
if (!force && metadata) {
return Promise.resolve(metadata);
}
// Memoize metadata but only for modelClass. The hasOwnProperty check
// will fail for subclasses and the value gets recreated.
if (!modelClass.hasOwnProperty(TABLE_METADATA)) {
defineNonEnumerableProperty(modelClass, TABLE_METADATA, new Map());
}
// The cache needs to be checked in addition to calling tableMetadata
// because the cache may contain a temporary promise in which case
// tableMetadata returns null.
metadata = modelClass[TABLE_METADATA].get(table);
if (!force && metadata) {
return Promise.resolve(metadata);
} else {
const promise = modelClass
.query(knex)
.childQueryOf(parentBuilder)
.columnInfo({ table })
.then((columnInfo) => {
const metadata = {
columns: Object.keys(columnInfo),
};
modelClass[TABLE_METADATA].set(table, metadata);
return metadata;
})
.catch((err) => {
modelClass[TABLE_METADATA].delete(table);
throw err;
});
modelClass[TABLE_METADATA].set(table, promise);
return promise;
}
}
function tableMetadata(modelClass, { table } = {}) {
if (modelClass.hasOwnProperty(TABLE_METADATA)) {
const metadata = modelClass[TABLE_METADATA].get(table || modelClass.getTableName());
if (isPromise(metadata)) {
return null;
} else {
return metadata;
}
} else {
return null;
}
}
module.exports = {
fetchTableMetadata,
tableMetadata,
};
================================================
FILE: lib/model/modelToJson.js
================================================
'use strict';
const { isInternalProp } = require('../utils/internalPropUtils');
const { mergeQueryProps } = require('./modelQueryProps');
const { isObject, cloneDeep, isFunction } = require('../utils/objectUtils');
const EMPTY_ARRAY = [];
function toJson(model, optIn) {
const modelClass = model.constructor;
const opt = {
virtuals: getVirtuals(optIn),
shallow: isShallow(optIn),
omit: getOmit(optIn, modelClass),
pick: null,
omitFromJson: model.$omitFromJson() || null,
cloneObjects: modelClass.cloneObjectAttributes,
};
let json = toExternalJsonImpl(model, opt);
json = model.$formatJson(json);
return json;
}
function toDatabaseJson(model, builder) {
const modelClass = model.constructor;
const opt = {
virtuals: false,
shallow: true,
omit: modelClass.getRelationNames(),
pick: getPick(modelClass),
omitFromJson: model.$omitFromDatabaseJson() || null,
cloneObjects: modelClass.cloneObjectAttributes,
};
let json = toDatabaseJsonImpl(model, opt);
json = model.$formatDatabaseJson(json);
return mergeQueryProps(model, json, opt.omitFromJson, builder);
}
function getVirtuals(opt) {
if (!opt) {
return true;
} else if (Array.isArray(opt.virtuals)) {
return opt.virtuals;
} else {
return opt.virtuals !== false;
}
}
function isShallow(opt) {
return !!opt && !!opt.shallow;
}
function getOmit(opt, modelClass) {
return isShallow(opt) ? modelClass.getRelationNames() : null;
}
function getPick(modelClass) {
const jsonSchema = modelClass.getJsonSchema();
return (jsonSchema && modelClass.pickJsonSchemaProperties && jsonSchema.properties) || null;
}
function toExternalJsonImpl(model, opt) {
const json = {};
const keys = Object.keys(model);
const vAttr = getVirtualAttributes(model, opt);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
const value = model[key];
assignJsonValue(json, key, value, opt);
}
if (vAttr.length !== 0) {
assignVirtualAttributes(json, model, vAttr, opt);
}
return json;
}
function getVirtualAttributes(model, opt) {
if (Array.isArray(opt.virtuals)) {
return opt.virtuals;
} else if (opt.virtuals === true) {
return model.constructor.getVirtualAttributes();
} else {
return EMPTY_ARRAY;
}
}
function toDatabaseJsonImpl(model, opt) {
const json = {};
const keys = Object.keys(model);
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
const value = model[key];
assignJsonValue(json, key, value, opt);
}
return json;
}
function assignJsonValue(json, key, value, opt) {
const type = typeof value;
if (
type !== 'function' &&
type !== 'undefined' &&
!isInternalProp(key) &&
!shouldOmit(opt, key) &&
shouldPick(opt, key)
) {
if (isObject(value)) {
json[key] = toJsonObject(value, opt);
} else {
json[key] = value;
}
}
}
function shouldOmit(opt, key) {
return (
(opt.omit !== null && opt.omit.includes(key)) ||
(opt.omitFromJson !== null && opt.omitFromJson.includes(key))
);
}
function shouldPick(opt, key) {
return opt.pick === null || key in opt.pick;
}
function assignVirtualAttributes(json, model, vAttr, opt) {
for (let i = 0, l = vAttr.length; i < l; ++i) {
const key = vAttr[i];
let value = model[key];
if (isFunction(value)) {
value = value.call(model);
}
assignJsonValue(json, key, value, opt);
}
}
function toJsonObject(value, opt) {
if (Array.isArray(value)) {
return toJsonArray(value, opt);
} else if (value.$isObjectionModel) {
// No branch for $toDatabaseJson here since there is never a need
// to have nested models in database rows.
return value.$toJson(opt);
} else if (Buffer.isBuffer(value)) {
return value;
} else if (opt.cloneObjects) {
return cloneDeep(value);
} else {
return value;
}
}
function toJsonArray(value, opt) {
const ret = new Array(value.length);
for (let i = 0, l = ret.length; i < l; ++i) {
const item = value[i];
if (isObject(item)) {
ret[i] = toJsonObject(item, opt);
} else {
ret[i] = item;
}
}
return ret;
}
module.exports = {
toJson,
toDatabaseJson,
};
================================================
FILE: lib/model/modelUtils.js
================================================
'use strict';
const hiddenProps = ['$$queryProps'];
const staticHiddenProps = [
'$$knex',
'$$validator',
'$$jsonSchema',
'$$colToProp',
'$$propToCol',
'$$relationMappings',
'$$relations',
'$$relationNames',
'$$jsonAttributes',
'$$columnNameMappers',
'$$tableMetadata',
'$$readOnlyAttributes',
'$$idRelationProperty',
'$$virtualAttributes',
];
function defineNonEnumerableProperty(obj, prop, value) {
Object.defineProperty(obj, prop, {
enumerable: false,
writable: true,
configurable: true,
value,
});
}
function keyByProps(models, props) {
const map = new Map();
for (let i = 0, l = models.length; i < l; ++i) {
const model = models[i];
map.set(model.$propKey(props), model);
}
return map;
}
module.exports = {
hiddenProps,
staticHiddenProps,
defineNonEnumerableProperty,
keyByProps,
};
================================================
FILE: lib/model/modelValidate.js
================================================
'use strict';
const { clone } = require('./modelClone');
function validate(model, json, options = {}) {
json = json || model;
const inputJson = json;
const validatingModelInstance = inputJson && inputJson.$isObjectionModel;
if (options.skipValidation) {
return json;
}
if (validatingModelInstance) {
// Strip away relations and other internal stuff.
// TODO 1: This should use `json.$toJson()` since we always validate the input representation!
// TODO 2: This should also keep the relations in the object because some validators may
// depend on the relations.
json = clone(json, true, true);
// We can mutate `json` now that we took a copy of it.
options = Object.assign({}, options, { mutable: true });
}
const modelClass = model.constructor;
const validator = modelClass.getValidator();
const args = {
options,
model,
json,
ctx: Object.create(null),
};
validator.beforeValidate(args);
json = validator.validate(args);
validator.afterValidate(args);
if (validatingModelInstance) {
// If we cloned `json`, we need to copy the possible default values.
return inputJson.$set(json);
} else {
return json;
}
}
module.exports = {
validate,
};
================================================
FILE: lib/model/modelValues.js
================================================
'use strict';
const { isObject } = require('../utils/objectUtils');
// Property keys needs to be prefixed with a non-numeric character so that
// they are not considered indexes when used as object keys.
const PROP_KEY_PREFIX = 'k_';
function values(model, args) {
switch (args.length) {
case 1:
return values1(model, args);
case 2:
return values2(model, args);
case 3:
return values3(model, args);
default:
return valuesN(model, args);
}
}
function propKey(model, props) {
switch (props.length) {
case 1:
return propKey1(model, props);
case 2:
return propKey2(model, props);
case 3:
return propKey3(model, props);
default:
return propKeyN(model, props);
}
}
function hasProps(model, props) {
for (let i = 0; i < props.length; ++i) {
const value = model[props[i]];
if (isNullOrUndefined(value)) {
return false;
}
}
return true;
}
function values1(model, args) {
return [model[args[0]]];
}
function values2(model, args) {
return [model[args[0]], model[args[1]]];
}
function values3(model, args) {
return [model[args[0]], model[args[1]], model[args[2]]];
}
function valuesN(model, args) {
const ret = new Array(args.length);
for (let i = 0, l = args.length; i < l; ++i) {
ret[i] = model[args[i]];
}
return ret;
}
function propKey1(model, props) {
return PROP_KEY_PREFIX + propToStr(model[props[0]]);
}
function propKey2(model, props) {
return PROP_KEY_PREFIX + propToStr(model[props[0]]) + ',' + propToStr(model[props[1]]);
}
function propKey3(model, props) {
return (
PROP_KEY_PREFIX +
propToStr(model[props[0]]) +
',' +
propToStr(model[props[1]]) +
',' +
propToStr(model[props[2]])
);
}
function propKeyN(model, props) {
let key = PROP_KEY_PREFIX;
for (let i = 0, l = props.length; i < l; ++i) {
key += propToStr(model[props[i]]);
if (i < l - 1) {
key += ',';
}
}
return key;
}
function propToStr(value) {
if (value === null) {
return 'null';
} else if (value === undefined) {
return 'undefined';
} else if (Buffer.isBuffer(value)) {
return value.toString('hex');
} else if (isObject(value)) {
return JSON.stringify(value);
} else {
return `${value}`;
}
}
function isNullOrUndefined(val) {
return val === null || val === undefined;
}
module.exports = {
PROP_KEY_PREFIX,
propToStr,
values,
hasProps,
propKey,
};
================================================
FILE: lib/model/modelVisitor.js
================================================
'use strict';
function visitModels(models, modelClass, visitor) {
doVisit(models, modelClass, null, null, visitor);
}
function doVisit(models, modelClass, parent, rel, visitor) {
if (Array.isArray(models)) {
visitMany(models, modelClass, parent, rel, visitor);
} else if (models) {
visitOne(models, modelClass, parent, rel, visitor);
}
}
function visitMany(models, modelClass, parent, rel, visitor) {
for (let i = 0, l = models.length; i < l; ++i) {
visitOne(models[i], modelClass, parent, rel, visitor);
}
}
function visitOne(model, modelClass, parent, rel, visitor) {
if (model) {
visitor(model, modelClass, parent, rel);
}
const relationNames = modelClass.getRelationNames();
for (let i = 0, l = relationNames.length; i < l; ++i) {
const relationName = relationNames[i];
const relatedObj = model[relationName];
if (relatedObj) {
const relation = modelClass.getRelation(relationName);
doVisit(relatedObj, relation.relatedModelClass, model, relation, visitor);
}
}
}
module.exports = {
visitModels,
};
================================================
FILE: lib/objection.js
================================================
'use strict';
const {
DBError,
UniqueViolationError,
NotNullViolationError,
ForeignKeyViolationError,
ConstraintViolationError,
CheckViolationError,
DataError,
} = require('db-errors');
const { Model: NativeModel } = require('./model/Model');
const { QueryBuilder: NativeQueryBuilder } = require('./queryBuilder/QueryBuilder');
const { QueryBuilderBase } = require('./queryBuilder/QueryBuilderBase');
const { QueryBuilderOperation } = require('./queryBuilder/operations/QueryBuilderOperation');
const { RelationExpression } = require('./queryBuilder/RelationExpression');
const { ValidationError } = require('./model/ValidationError');
const { NotFoundError } = require('./model/NotFoundError');
const { AjvValidator: NativeAjvValidator } = require('./model/AjvValidator');
const { Validator: NativeValidator } = require('./model/Validator');
const { Relation } = require('./relations/Relation');
const { HasOneRelation } = require('./relations/hasOne/HasOneRelation');
const { HasManyRelation } = require('./relations/hasMany/HasManyRelation');
const { BelongsToOneRelation } = require('./relations/belongsToOne/BelongsToOneRelation');
const { HasOneThroughRelation } = require('./relations/hasOneThrough/HasOneThroughRelation');
const { ManyToManyRelation } = require('./relations/manyToMany/ManyToManyRelation');
const { transaction } = require('./transaction');
const { initialize } = require('./initialize');
const {
snakeCaseMappers,
knexSnakeCaseMappers,
knexIdentifierMapping,
} = require('./utils/identifierMapping');
const { compose, mixin } = require('./utils/mixin');
const { ref } = require('./queryBuilder/ReferenceBuilder');
const { val } = require('./queryBuilder/ValueBuilder');
const { raw } = require('./queryBuilder/RawBuilder');
const { fn } = require('./queryBuilder/FunctionBuilder');
const { inherit } = require('../lib/utils/classUtils');
// We need to wrap the classes, that people can inherit, with ES5 classes
// so that babel is able to use ES5 inheritance. sigh... Maybe people
// should stop transpiling node apps to ES5 in the year 2019? Node 6
// with full class support was released three years ago.
function Model() {
// Nothing to do here.
}
function QueryBuilder(...args) {
NativeQueryBuilder.init(this, ...args);
}
function Validator(...args) {
NativeValidator.init(this, ...args);
}
function AjvValidator(...args) {
NativeAjvValidator.init(this, ...args);
}
inherit(Model, NativeModel);
inherit(QueryBuilder, NativeQueryBuilder);
inherit(Validator, NativeValidator);
inherit(AjvValidator, NativeAjvValidator);
Model.QueryBuilder = QueryBuilder;
module.exports = {
Model,
QueryBuilder,
QueryBuilderBase,
QueryBuilderOperation,
RelationExpression,
ValidationError,
NotFoundError,
AjvValidator,
Validator,
Relation,
HasOneRelation,
HasManyRelation,
BelongsToOneRelation,
HasOneThroughRelation,
ManyToManyRelation,
transaction,
initialize,
compose,
mixin,
ref,
val,
raw,
fn,
snakeCaseMappers,
knexSnakeCaseMappers,
knexIdentifierMapping,
DBError,
UniqueViolationError,
NotNullViolationError,
ForeignKeyViolationError,
ConstraintViolationError,
CheckViolationError,
DataError,
};
================================================
FILE: lib/queryBuilder/FunctionBuilder.js
================================================
'use strict';
const { RawBuilder, normalizeRawArgs } = require('./RawBuilder');
const { asSingle, isNumber } = require('../utils/objectUtils');
class FunctionBuilder extends RawBuilder {}
function fn(...argsIn) {
const { sql, args } = normalizeRawArgs(argsIn);
return new FunctionBuilder(`${sql}(${args.map(() => '?').join(', ')})`, args);
}
for (const func of ['coalesce', 'concat', 'sum', 'avg', 'min', 'max', 'count', 'upper', 'lower']) {
fn[func] = (...args) => fn(func.toUpperCase(), args);
}
fn.now = (precision) => {
precision = parseInt(asSingle(precision), 10);
if (isNaN(precision) || !isNumber(precision)) {
precision = 6;
}
// We need to use a literal precision instead of a binding here
// for the CURRENT_TIMESTAMP to work. This is okay here since we
// make sure `precision` is a number. There's no chance of SQL
// injection here.
return new FunctionBuilder(`CURRENT_TIMESTAMP(${precision})`, []);
};
module.exports = {
FunctionBuilder,
fn,
};
================================================
FILE: lib/queryBuilder/InternalOptions.js
================================================
'use strict';
class InternalOptions {
constructor() {
this.skipUndefined = false;
this.keepImplicitJoinProps = false;
this.returnImmediatelyValue = undefined;
this.isInternalQuery = false;
this.debug = false;
this.schema = undefined;
}
clone() {
const copy = new this.constructor();
copy.skipUndefined = this.skipUndefined;
copy.keepImplicitJoinProps = this.keepImplicitJoinProps;
copy.returnImmediatelyValue = this.returnImmediatelyValue;
copy.isInternalQuery = this.isInternalQuery;
copy.debug = this.debug;
copy.schema = this.schema;
return copy;
}
}
module.exports = {
InternalOptions,
};
================================================
FILE: lib/queryBuilder/JoinBuilder.js
================================================
'use strict';
const { QueryBuilderOperationSupport } = require('./QueryBuilderOperationSupport');
const { KnexOperation } = require('./operations/KnexOperation');
class JoinBuilder extends QueryBuilderOperationSupport {
using(...args) {
return this.addOperation(new KnexOperation('using'), args);
}
on(...args) {
return this.addOperation(new KnexOperation('on'), args);
}
orOn(...args) {
return this.addOperation(new KnexOperation('orOn'), args);
}
onBetween(...args) {
return this.addOperation(new KnexOperation('onBetween'), args);
}
onNotBetween(...args) {
return this.addOperation(new KnexOperation('onNotBetween'), args);
}
orOnBetween(...args) {
return this.addOperation(new KnexOperation('orOnBetween'), args);
}
orOnNotBetween(...args) {
return this.addOperation(new KnexOperation('orOnNotBetween'), args);
}
onIn(...args) {
return this.addOperation(new KnexOperation('onIn'), args);
}
onNotIn(...args) {
return this.addOperation(new KnexOperation('onNotIn'), args);
}
orOnIn(...args) {
return this.addOperation(new KnexOperation('orOnIn'), args);
}
orOnNotIn(...args) {
return this.addOperation(new KnexOperation('orOnNotIn'), args);
}
onNull(...args) {
return this.addOperation(new KnexOperation('onNull'), args);
}
orOnNull(...args) {
return this.addOperation(new KnexOperation('orOnNull'), args);
}
onNotNull(...args) {
return this.addOperation(new KnexOperation('onNotNull'), args);
}
orOnNotNull(...args) {
return this.addOperation(new KnexOperation('orOnNotNull'), args);
}
onExists(...args) {
return this.addOperation(new KnexOperation('onExists'), args);
}
orOnExists(...args) {
return this.addOperation(new KnexOperation('orOnExists'), args);
}
onNotExists(...args) {
return this.addOperation(new KnexOperation('onNotExists'), args);
}
orOnNotExists(...args) {
return this.addOperation(new KnexOperation('orOnNotExists'), args);
}
type(...args) {
return this.addOperation(new KnexOperation('type'), args);
}
andOn(...args) {
return this.addOperation(new KnexOperation('andOn'), args);
}
andOnIn(...args) {
return this.addOperation(new KnexOperation('andOnIn'), args);
}
andOnNotIn(...args) {
return this.addOperation(new KnexOperation('andOnNotIn'), args);
}
andOnNull(...args) {
return this.addOperation(new KnexOperation('andOnNull'), args);
}
andOnNotNull(...args) {
return this.addOperation(new KnexOperation('andOnNotNull'), args);
}
andOnExists(...args) {
return this.addOperation(new KnexOperation('andOnExists'), args);
}
andOnNotExists(...args) {
return this.addOperation(new KnexOperation('andOnNotExists'), args);
}
andOnBetween(...args) {
return this.addOperation(new KnexOperation('andOnBetween'), args);
}
andOnNotBetween(...args) {
return this.addOperation(new KnexOperation('andOnNotBetween'), args);
}
andOnJsonPathEquals(...args) {
return this.addOperation(new KnexOperation('andOnJsonPathEquals'), args);
}
onVal(...args) {
return this.addOperation(new KnexOperation('onVal'), args);
}
andOnVal(...args) {
return this.addOperation(new KnexOperation('andOnVal'), args);
}
orOnVal(...args) {
return this.addOperation(new KnexOperation('orOnVal'), args);
}
}
module.exports = {
JoinBuilder,
};
================================================
FILE: lib/queryBuilder/QueryBuilder.js
================================================
'use strict';
const { wrapError } = require('db-errors');
const { raw } = require('./RawBuilder');
const { createModifier } = require('../utils/createModifier');
const { ValidationErrorType } = require('../model/ValidationError');
const { isObject, isString, isFunction, last, flatten } = require('../utils/objectUtils');
const { RelationExpression, DuplicateRelationError } = require('./RelationExpression');
const { assertIdNotUndefined } = require('../utils/assert');
const { Selection } = require('./operations/select/Selection');
const { QueryBuilderContext } = require('./QueryBuilderContext');
const { QueryBuilderBase } = require('./QueryBuilderBase');
const { FindOperation } = require('./operations/FindOperation');
const { DeleteOperation } = require('./operations/DeleteOperation');
const { UpdateOperation } = require('./operations/UpdateOperation');
const { InsertOperation } = require('./operations/InsertOperation');
const { RelateOperation } = require('./operations/RelateOperation');
const { UnrelateOperation } = require('./operations/UnrelateOperation');
const { JoinEagerOperation } = require('./operations/eager/JoinEagerOperation');
const { NaiveEagerOperation } = require('./operations/eager/NaiveEagerOperation');
const { WhereInEagerOperation } = require('./operations/eager/WhereInEagerOperation');
const { InsertGraphAndFetchOperation } = require('./operations/InsertGraphAndFetchOperation');
const { UpsertGraphAndFetchOperation } = require('./operations/UpsertGraphAndFetchOperation');
const { InsertAndFetchOperation } = require('./operations/InsertAndFetchOperation');
const { UpdateAndFetchOperation } = require('./operations/UpdateAndFetchOperation');
const { JoinRelatedOperation } = require('./operations/JoinRelatedOperation');
const { OnBuildKnexOperation } = require('./operations/OnBuildKnexOperation');
const { InsertGraphOperation } = require('./operations/InsertGraphOperation');
const { UpsertGraphOperation } = require('./operations/UpsertGraphOperation');
const { RunBeforeOperation } = require('./operations/RunBeforeOperation');
const { RunAfterOperation } = require('./operations/RunAfterOperation');
const { FindByIdOperation } = require('./operations/FindByIdOperation');
const { FindByIdsOperation } = require('./operations/FindByIdsOperation');
const { OnBuildOperation } = require('./operations/OnBuildOperation');
const { OnErrorOperation } = require('./operations/OnErrorOperation');
const { SelectOperation } = require('./operations/select/SelectOperation');
const { EagerOperation } = require('./operations/eager/EagerOperation');
const { RangeOperation } = require('./operations/RangeOperation');
const { FirstOperation } = require('./operations/FirstOperation');
const { FromOperation } = require('./operations/FromOperation');
const { KnexOperation } = require('./operations/KnexOperation');
class QueryBuilder extends QueryBuilderBase {
static init(self, modelClass) {
super.init(self, modelClass);
self._resultModelClass = null;
self._explicitRejectValue = null;
self._explicitResolveValue = null;
self._modifiers = {};
self._allowedGraphExpression = null;
self._findOperationOptions = modelClass.defaultFindOptions;
self._relatedQueryFor = null;
self._findOperationFactory = findOperationFactory;
self._insertOperationFactory = insertOperationFactory;
self._updateOperationFactory = updateOperationFactory;
self._patchOperationFactory = patchOperationFactory;
self._relateOperationFactory = relateOperationFactory;
self._unrelateOperationFactory = unrelateOperationFactory;
self._deleteOperationFactory = deleteOperationFactory;
}
static get QueryBuilderContext() {
return QueryBuilderContext;
}
static parseRelationExpression(expr) {
return RelationExpression.create(expr).toPojo();
}
tableNameFor(modelClassOrTableName, newTableName) {
return super.tableNameFor(getTableName(modelClassOrTableName), newTableName);
}
tableName(newTableName) {
return this.tableNameFor(this.modelClass().getTableName(), newTableName);
}
tableRef() {
return this.tableRefFor(this.modelClass().getTableName());
}
aliasFor(modelClassOrTableName, alias) {
return super.aliasFor(getTableName(modelClassOrTableName), alias);
}
alias(alias) {
return this.aliasFor(this.modelClass().getTableName(), alias);
}
fullIdColumnFor(modelClass) {
const tableName = this.tableRefFor(modelClass);
const idColumn = modelClass.getIdColumn();
if (Array.isArray(idColumn)) {
return idColumn.map((col) => `${tableName}.${col}`);
} else {
return `${tableName}.${idColumn}`;
}
}
fullIdColumn() {
return this.fullIdColumnFor(this.modelClass());
}
modifiers(modifiers) {
if (arguments.length === 0) {
return { ...this._modifiers };
} else {
this._modifiers = { ...this._modifiers, ...modifiers };
return this;
}
}
modify(modifier, ...args) {
if (!modifier) {
return this;
}
modifier = createModifier({
modifier,
modelClass: this.modelClass(),
modifiers: this._modifiers,
});
modifier(this, ...args);
return this;
}
reject(error) {
this._explicitRejectValue = error;
return this;
}
resolve(value) {
this._explicitResolveValue = value;
return this;
}
isExplicitlyResolvedOrRejected() {
return !!(this._explicitRejectValue || this._explicitResolveValue);
}
isExecutable() {
return !this.isExplicitlyResolvedOrRejected() && !findQueryExecutorOperation(this);
}
findOperationFactory(factory) {
this._findOperationFactory = factory;
return this;
}
insertOperationFactory(factory) {
this._insertOperationFactory = factory;
return this;
}
updateOperationFactory(factory) {
this._updateOperationFactory = factory;
return this;
}
patchOperationFactory(factory) {
this._patchOperationFactory = factory;
return this;
}
deleteOperationFactory(factory) {
this._deleteOperationFactory = factory;
return this;
}
relateOperationFactory(factory) {
this._relateOperationFactory = factory;
return this;
}
unrelateOperationFactory(factory) {
this._unrelateOperationFactory = factory;
return this;
}
withGraphFetched(exp, options = {}) {
return this._withGraph(exp, options, getWhereInEagerAlgorithm(this));
}
withGraphJoined(exp, options = {}) {
return this._withGraph(exp, options, getJoinEagerAlgorithm(this));
}
_withGraph(exp, options, algorithm) {
const eagerOp = ensureEagerOperation(this, algorithm);
const parsedExp = parseRelationExpression(this.modelClass(), exp);
eagerOp.expression = eagerOp.expression.merge(parsedExp);
eagerOp.graphOptions = { ...eagerOp.graphOptions, ...options };
checkEager(this);
return this;
}
allowGraph(exp) {
const currentExpr = this._allowedGraphExpression || RelationExpression.create();
this._allowedGraphExpression = currentExpr.merge(
parseRelationExpression(this.modelClass(), exp),
);
checkEager(this);
return this;
}
allowedGraphExpression() {
return this._allowedGraphExpression;
}
graphExpressionObject() {
const eagerOp = this.findOperation(EagerOperation);
if (eagerOp && !eagerOp.expression.isEmpty) {
return eagerOp.expression.toPojo();
} else {
return null;
}
}
graphModifiersAtPath() {
const eagerOp = this.findOperation(EagerOperation);
if (eagerOp && !eagerOp.expression.isEmpty) {
return eagerOp.modifiersAtPath.map((it) => Object.assign({}, it));
} else {
return [];
}
}
modifyGraph(path, modifier) {
const eagerOp = ensureEagerOperation(this);
eagerOp.modifiersAtPath.push({ path, modifier });
return this;
}
findOptions(opt) {
if (arguments.length !== 0) {
this._findOperationOptions = Object.assign({}, this._findOperationOptions, opt);
return this;
} else {
return this._findOperationOptions;
}
}
resultModelClass() {
return this._resultModelClass || this.modelClass();
}
isFind() {
return !(
this.isInsert() ||
this.isUpdate() ||
this.isDelete() ||
this.isRelate() ||
this.isUnrelate()
);
}
isInsert() {
return this.has(InsertOperation);
}
isUpdate() {
return this.has(UpdateOperation);
}
isDelete() {
return this.has(DeleteOperation);
}
isRelate() {
return this.has(RelateOperation);
}
isUnrelate() {
return this.has(UnrelateOperation);
}
hasWheres() {
const queryWithoutGraph = this.clone().clearWithGraph();
return prebuildQuery(queryWithoutGraph).has(QueryBuilderBase.WhereSelector);
}
hasSelects() {
return this.has(QueryBuilderBase.SelectSelector);
}
hasWithGraph() {
const eagerOp = this.findOperation(EagerOperation);
return !!eagerOp && !eagerOp.expression.isEmpty;
}
isSelectAll() {
if (this._operations.length === 0) {
return true;
}
const tableRef = this.tableRef();
const tableName = this.tableName();
return this.everyOperation((op) => {
if (op.constructor === SelectOperation) {
// SelectOperations with zero selections are the ones that only have
// raw items or other non-trivial selections.
return (
op.selections.length > 0 &&
op.selections.every((select) => {
return (!select.table || select.table === tableRef) && select.column === '*';
})
);
} else if (op.constructor === FromOperation) {
return op.table === tableName;
} else if (op.name === 'as' || op.is(FindOperation) || op.is(OnErrorOperation)) {
return true;
} else {
return false;
}
});
}
toString() {
return '[object QueryBuilder]';
}
clone() {
const builder = this.emptyInstance();
// Call the super class's clone implementation.
this.baseCloneInto(builder);
builder._resultModelClass = this._resultModelClass;
builder._explicitRejectValue = this._explicitRejectValue;
builder._explicitResolveValue = this._explicitResolveValue;
builder._modifiers = { ...this._modifiers };
builder._allowedGraphExpression = this._allowedGraphExpression;
builder._findOperationOptions = this._findOperationOptions;
builder._relatedQueryFor = this._relatedQueryFor;
return builder;
}
emptyInstance() {
const builder = new this.constructor(this.modelClass());
builder._findOperationFactory = this._findOperationFactory;
builder._insertOperationFactory = this._insertOperationFactory;
builder._updateOperationFactory = this._updateOperationFactory;
builder._patchOperationFactory = this._patchOperationFactory;
builder._relateOperationFactory = this._relateOperationFactory;
builder._unrelateOperationFactory = this._unrelateOperationFactory;
builder._deleteOperationFactory = this._deleteOperationFactory;
builder._relatedQueryFor = this._relatedQueryFor;
return builder;
}
clearWithGraph() {
this.clear(EagerOperation);
return this;
}
clearWithGraphFetched() {
this.clear(WhereInEagerOperation);
return this;
}
clearAllowGraph() {
this._allowedGraphExpression = null;
return this;
}
clearModifiers() {
this._modifiers = {};
return this;
}
clearReject() {
this._explicitRejectValue = null;
return this;
}
clearResolve() {
this._explicitResolveValue = null;
return this;
}
castTo(modelClass) {
this._resultModelClass = modelClass || null;
return this;
}
then(...args) {
const promise = this.execute();
return promise.then(...args);
}
catch(...args) {
const promise = this.execute();
return promise.catch(...args);
}
async resultSize() {
const knex = this.knex();
const builder = this.clone().clear(/orderBy|offset|limit/);
const countQuery = knex.count('* as count').from((knexBuilder) => {
builder.toKnexQuery(knexBuilder).as('temp');
});
if (this.internalOptions().debug) {
countQuery.debug();
}
const result = await countQuery;
return result[0] && result[0].count ? parseInt(result[0].count) : 0;
}
toKnexQuery(knexBuilder = this.knex().queryBuilder()) {
const prebuiltQuery = prebuildQuery(this.clone());
return buildKnexQuery(prebuiltQuery, knexBuilder);
}
async execute() {
// Take a clone so that we don't modify this instance during execution.
const builder = this.clone();
try {
await beforeExecute(builder);
const result = await doExecute(builder);
return await afterExecute(builder, result);
} catch (error) {
return await handleExecuteError(builder, error);
}
}
throwIfNotFound(data = {}) {
return this.runAfter((result) => {
if (
(Array.isArray(result) && result.length === 0) ||
result === null ||
result === undefined ||
result === 0
) {
throw this.modelClass().createNotFoundError(this.context(), data);
} else {
return result;
}
});
}
findSelection(selection, explicit = false) {
let noSelectStatements = true;
let selectionInstance = null;
this.forEachOperation(true, (op) => {
if (op.constructor === SelectOperation) {
selectionInstance = op.findSelection(this, selection);
noSelectStatements = false;
if (selectionInstance) {
return false;
}
}
});
if (selectionInstance) {
return selectionInstance;
}
if (noSelectStatements && !explicit) {
const selectAll = new Selection(this.tableRef(), '*');
if (Selection.doesSelect(this, selectAll, selection)) {
return selectAll;
} else {
return null;
}
} else {
return null;
}
}
findAllSelections() {
let allSelections = [];
this.forEachOperation(true, (op) => {
if (op.constructor === SelectOperation) {
allSelections = allSelections.concat(op.selections);
}
});
return allSelections;
}
hasSelection(selection, explicit) {
return this.findSelection(selection, explicit) !== null;
}
hasSelectionAs(selection, alias, explicit) {
selection = Selection.create(selection);
const foundSelection = this.findSelection(selection, explicit);
if (foundSelection === null) {
return false;
} else {
if (foundSelection.column === '*') {
// * selects the columns with their column names as aliases.
return selection.column === alias;
} else {
return foundSelection.name === alias;
}
}
}
traverse(modelClass, traverser) {
if (typeof traverser === 'undefined') {
traverser = modelClass;
modelClass = null;
}
return this.runAfter((result) => {
this.resultModelClass().traverse(modelClass, result, traverser);
return result;
});
}
page(page, pageSize) {
return this.range(+page * +pageSize, (+page + 1) * +pageSize - 1);
}
columnInfo({ table = null } = {}) {
table = table || this.tableName();
const knex = this.knex();
const tableParts = table.split('.');
const columnInfoQuery = knex(last(tableParts)).columnInfo();
const schema = this.internalOptions().schema;
if (schema) {
columnInfoQuery.withSchema(schema);
} else if (tableParts.length > 1) {
columnInfoQuery.withSchema(tableParts[0]);
}
if (this.internalOptions().debug) {
columnInfoQuery.debug();
}
return columnInfoQuery;
}
withSchema(schema) {
this.internalOptions().schema = schema;
this.internalContext().onBuild.push((builder) => {
if (!builder.has(/withSchema/)) {
// Need to push this operation to the front because knex doesn't use the
// schema for operations called before `withSchema`.
builder.addOperationToFront(new KnexOperation('withSchema'), [schema]);
}
});
return this;
}
debug /* istanbul ignore next */() {
this.internalOptions().debug = true;
this.internalContext().onBuild.push((builder) => {
builder.addOperation(new KnexOperation('debug'), []);
});
return this;
}
insert(modelsOrObjects) {
return writeOperation(this, () => {
const insertOperation = this._insertOperationFactory(this);
this.addOperation(insertOperation, [modelsOrObjects]);
});
}
insertAndFetch(modelsOrObjects) {
return writeOperation(this, () => {
const insertOperation = this._insertOperationFactory(this);
const insertAndFetchOperation = new InsertAndFetchOperation('insertAndFetch', {
delegate: insertOperation,
});
this.addOperation(insertAndFetchOperation, [modelsOrObjects]);
});
}
insertGraph(modelsOrObjects, opt) {
return writeOperation(this, () => {
const insertOperation = this._insertOperationFactory(this);
const insertGraphOperation = new InsertGraphOperation('insertGraph', {
delegate: insertOperation,
opt,
});
this.addOperation(insertGraphOperation, [modelsOrObjects]);
});
}
insertGraphAndFetch(modelsOrObjects, opt) {
return writeOperation(this, () => {
const insertOperation = this._insertOperationFactory(this);
const insertGraphOperation = new InsertGraphOperation('insertGraph', {
delegate: insertOperation,
opt,
});
const insertGraphAndFetchOperation = new InsertGraphAndFetchOperation('insertGraphAndFetch', {
delegate: insertGraphOperation,
});
return this.addOperation(insertGraphAndFetchOperation, [modelsOrObjects]);
});
}
update(modelOrObject) {
return writeOperation(this, () => {
const updateOperation = this._updateOperationFactory(this);
this.addOperation(updateOperation, [modelOrObject]);
});
}
updateAndFetch(modelOrObject) {
return writeOperation(this, () => {
const updateOperation = this._updateOperationFactory(this);
if (!(updateOperation.instance instanceof this.modelClass())) {
throw new Error('updateAndFetch can only be called for instance operations');
}
const updateAndFetch = new UpdateAndFetchOperation('updateAndFetch', {
delegate: updateOperation,
});
// patchOperation is an instance update operation that already adds the
// required "where id = $" clause.
updateAndFetch.skipIdWhere = true;
this.addOperation(updateAndFetch, [updateOperation.instance.$id(), modelOrObject]);
});
}
updateAndFetchById(id, modelOrObject) {
return writeOperation(this, () => {
const updateOperation = this._updateOperationFactory(this);
const updateAndFetch = new UpdateAndFetchOperation('updateAndFetch', {
delegate: updateOperation,
});
this.addOperation(updateAndFetch, [id, modelOrObject]);
});
}
upsertGraph(modelsOrObjects, upsertOptions) {
return writeOperation(this, () => {
const upsertGraphOperation = new UpsertGraphOperation('upsertGraph', {
upsertOptions,
});
this.addOperation(upsertGraphOperation, [modelsOrObjects]);
});
}
upsertGraphAndFetch(modelsOrObjects, upsertOptions) {
return writeOperation(this, () => {
const upsertGraphOperation = new UpsertGraphOperation('upsertGraph', {
upsertOptions,
});
const upsertGraphAndFetchOperation = new UpsertGraphAndFetchOperation('upsertGraphAndFetch', {
delegate: upsertGraphOperation,
});
return this.addOperation(upsertGraphAndFetchOperation, [modelsOrObjects]);
});
}
patch(modelOrObject) {
return writeOperation(this, () => {
const patchOperation = this._patchOperationFactory(this);
this.addOperation(patchOperation, [modelOrObject]);
});
}
patchAndFetch(modelOrObject) {
return writeOperation(this, () => {
const patchOperation = this._patchOperationFactory(this);
if (!(patchOperation.instance instanceof this.modelClass())) {
throw new Error('patchAndFetch can only be called for instance operations');
}
const patchAndFetch = new UpdateAndFetchOperation('patchAndFetch', {
delegate: patchOperation,
});
// patchOperation is an instance update operation that already adds the
// required "where id = $" clause.
patchAndFetch.skipIdWhere = true;
this.addOperation(patchAndFetch, [patchOperation.instance.$id(), modelOrObject]);
});
}
patchAndFetchById(id, modelOrObject) {
return writeOperation(this, () => {
const patchOperation = this._patchOperationFactory(this);
const patchAndFetch = new UpdateAndFetchOperation('patchAndFetch', {
delegate: patchOperation,
});
this.addOperation(patchAndFetch, [id, modelOrObject]);
});
}
delete(...args) {
return writeOperation(this, () => {
if (args.length) {
throw new Error(
`Don't pass arguments to delete(). You should use it like this: delete().where('foo', 'bar').andWhere(...)`,
);
}
const deleteOperation = this._deleteOperationFactory(this);
this.addOperation(deleteOperation, args);
});
}
del(...args) {
return this.delete(...args);
}
relate(...args) {
return writeOperation(this, () => {
const relateOperation = this._relateOperationFactory(this);
this.addOperation(relateOperation, args);
});
}
unrelate(...args) {
return writeOperation(this, () => {
if (args.length) {
throw new Error(
`Don't pass arguments to unrelate(). You should use it like this: unrelate().where('foo', 'bar').andWhere(...)`,
);
}
const unrelateOperation = this._unrelateOperationFactory(this);
this.addOperation(unrelateOperation, args);
});
}
increment(propertyName, howMuch) {
const columnName = this.modelClass().propertyNameToColumnName(propertyName);
return this.patch({
[columnName]: raw('?? + ?', [columnName, howMuch]),
});
}
decrement(propertyName, howMuch) {
const columnName = this.modelClass().propertyNameToColumnName(propertyName);
return this.patch({
[columnName]: raw('?? - ?', [columnName, howMuch]),
});
}
findOne(...args) {
return this.where.apply(this, args).first();
}
range(...args) {
return this.clear(RangeOperation).addOperation(new RangeOperation('range'), args);
}
first(...args) {
return this.addOperation(new FirstOperation('first'), args);
}
joinRelated(expression, options) {
ensureJoinRelatedOperation(this, 'innerJoin').addCall({
expression,
options,
});
return this;
}
innerJoinRelated(expression, options) {
ensureJoinRelatedOperation(this, 'innerJoin').addCall({
expression,
options,
});
return this;
}
outerJoinRelated(expression, options) {
ensureJoinRelatedOperation(this, 'outerJoin').addCall({
expression,
options,
});
return this;
}
fullOuterJoinRelated(expression, options) {
ensureJoinRelatedOperation(this, 'fullOuterJoin').addCall({
expression,
options,
});
return this;
}
leftJoinRelated(expression, options) {
ensureJoinRelatedOperation(this, 'leftJoin').addCall({
expression,
options,
});
return this;
}
leftOuterJoinRelated(expression, options) {
ensureJoinRelatedOperation(this, 'leftOuterJoin').addCall({
expression,
options,
});
return this;
}
rightJoinRelated(expression, options) {
ensureJoinRelatedOperation(this, 'rightJoin').addCall({
expression,
options,
});
return this;
}
rightOuterJoinRelated(expression, options) {
ensureJoinRelatedOperation(this, 'rightOuterJoin').addCall({
expression,
options,
});
return this;
}
deleteById(id) {
return this.findById(id)
.delete()
.runBefore((_, builder) => {
if (!builder.internalOptions().skipUndefined) {
assertIdNotUndefined(id, `undefined was passed to deleteById`);
}
});
}
findById(...args) {
return this.addOperation(new FindByIdOperation('findById'), args).first();
}
findByIds(...args) {
return this.addOperation(new FindByIdsOperation('findByIds'), args);
}
runBefore(...args) {
return this.addOperation(new RunBeforeOperation('runBefore'), args);
}
onBuild(...args) {
return this.addOperation(new OnBuildOperation('onBuild'), args);
}
onBuildKnex(...args) {
return this.addOperation(new OnBuildKnexOperation('onBuildKnex'), args);
}
runAfter(...args) {
return this.addOperation(new RunAfterOperation('runAfter'), args);
}
onError(...args) {
return this.addOperation(new OnErrorOperation('onError'), args);
}
from(...args) {
return this.addOperation(new FromOperation('from'), args);
}
updateFrom(...args) {
return this.addOperation(new KnexOperation('updateFrom'), args);
}
table(...args) {
return this.addOperation(new FromOperation('table'), args);
}
for(relatedQueryFor) {
if (arguments.length === 0) {
return this._relatedQueryFor;
} else {
this._relatedQueryFor = relatedQueryFor;
return this;
}
}
}
Object.defineProperties(QueryBuilder.prototype, {
isObjectionQueryBuilder: {
enumerable: false,
writable: false,
value: true,
},
});
function getTableName(modelClassOrTableName) {
if (isString(modelClassOrTableName)) {
return modelClassOrTableName;
} else {
return modelClassOrTableName.getTableName();
}
}
function ensureEagerOperation(builder, algorithm = null) {
const modelClass = builder.modelClass();
const defaultGraphOptions = modelClass.getDefaultGraphOptions();
const eagerOp = builder.findOperation(EagerOperation);
if (algorithm) {
const EagerOperationClass = getOperationClassForEagerAlgorithm(builder, algorithm);
if (eagerOp instanceof EagerOperationClass) {
return eagerOp;
} else {
const newEagerOp = new EagerOperationClass('eager', {
defaultGraphOptions,
});
if (eagerOp) {
newEagerOp.cloneFrom(eagerOp);
}
builder.clear(EagerOperation);
builder.addOperation(newEagerOp);
return newEagerOp;
}
} else {
if (eagerOp) {
return eagerOp;
} else {
const EagerOperationClass = getOperationClassForEagerAlgorithm(
builder,
getWhereInEagerAlgorithm(builder),
);
const newEagerOp = new EagerOperationClass('eager', {
defaultGraphOptions,
});
builder.addOperation(newEagerOp);
return newEagerOp;
}
}
}
function getWhereInEagerAlgorithm(builder) {
return builder.modelClass().WhereInEagerAlgorithm;
}
function getJoinEagerAlgorithm(builder) {
return builder.modelClass().JoinEagerAlgorithm;
}
function getNaiveEagerAlgorithm(builder) {
return builder.modelClass().NaiveEagerAlgorithm;
}
function getOperationClassForEagerAlgorithm(builder, algorithm) {
if (algorithm === getJoinEagerAlgorithm(builder)) {
return JoinEagerOperation;
} else if (algorithm === getNaiveEagerAlgorithm(builder)) {
return NaiveEagerOperation;
} else {
return WhereInEagerOperation;
}
}
function parseRelationExpression(modelClass, exp) {
try {
return RelationExpression.create(exp);
} catch (err) {
if (err instanceof DuplicateRelationError) {
throw modelClass.createValidationError({
type: ValidationErrorType.RelationExpression,
message: `Duplicate relation name "${err.relationName}" in relation expression "${exp}". For example, use "${err.relationName}.[foo, bar]" instead of "[${err.relationName}.foo, ${err.relationName}.bar]".`,
});
} else {
throw modelClass.createValidationError({
type: ValidationErrorType.RelationExpression,
message: `Invalid relation expression "${exp}"`,
});
}
}
}
function checkEager(builder) {
const eagerOp = builder.findOperation(EagerOperation);
if (!eagerOp) {
return;
}
const expr = eagerOp.expression;
const allowedExpr = builder.allowedGraphExpression();
if (expr.numChildren > 0 && allowedExpr && !allowedExpr.isSubExpression(expr)) {
const modelClass = builder.modelClass();
builder.reject(
modelClass.createValidationError({
type: ValidationErrorType.UnallowedRelation,
message: 'eager expression not allowed',
}),
);
}
}
function findQueryExecutorOperation(builder) {
return builder.findOperation((op) => op.hasQueryExecutor());
}
function beforeExecute(builder) {
let promise = Promise.resolve();
builder = addImplicitOperations(builder);
// Resolve all before hooks before building and executing the query
// and the rest of the hooks.
promise = chainOperationHooks(promise, builder, 'onBefore1');
promise = chainOperationHooks(promise, builder, 'onBefore2');
promise = chainHooks(promise, builder, builder.context().runBefore);
promise = chainHooks(promise, builder, builder.internalContext().runBefore);
promise = chainOperationHooks(promise, builder, 'onBefore3');
return promise;
}
function doExecute(builder) {
let promise = Promise.resolve();
builder = callOnBuildHooks(builder);
const queryExecutorOperation = findQueryExecutorOperation(builder);
const explicitRejectValue = builder._explicitRejectValue;
const explicitResolveValue = builder._explicitResolveValue;
if (explicitRejectValue !== null) {
promise = Promise.reject(explicitRejectValue);
} else if (explicitResolveValue !== null) {
promise = Promise.resolve(explicitResolveValue);
} else if (queryExecutorOperation !== null) {
promise = Promise.resolve(queryExecutorOperation.queryExecutor(builder));
} else {
promise = Promise.resolve(buildKnexQuery(builder));
promise = chainOperationHooks(promise, builder, 'onRawResult');
promise = promise.then((result) => createModels(result, builder));
}
return promise;
}
function afterExecute(builder, result) {
let promise = Promise.resolve(result);
promise = chainOperationHooks(promise, builder, 'onAfter1');
promise = chainOperationHooks(promise, builder, 'onAfter2');
promise = chainHooks(promise, builder, builder.context().runAfter);
promise = chainHooks(promise, builder, builder.internalContext().runAfter);
promise = chainOperationHooks(promise, builder, 'onAfter3');
return promise;
}
class ReturnImmediatelyException {
constructor(value) {
this.value = value;
}
}
function handleReturnImmediatelyValue(builder) {
const { returnImmediatelyValue } = builder.internalOptions();
if (returnImmediatelyValue !== undefined) {
throw new ReturnImmediatelyException(returnImmediatelyValue);
}
}
function handleExecuteError(builder, err) {
if (err instanceof ReturnImmediatelyException) {
return Promise.resolve(err.value);
}
let promise = Promise.reject(wrapError(err));
builder.forEachOperation(true, (op) => {
if (op.hasOnError()) {
promise = promise.catch((err) =>
builder.callAsyncOperationMethod(op, 'onError', [builder, err]),
);
}
});
return promise;
}
function chainOperationHooks(promise, builder, hookName) {
return promise.then((result) => {
let promise = Promise.resolve(result);
builder.forEachOperation(true, (op) => {
if (op.hasHook(hookName)) {
promise = promise.then((result) => {
const res = builder.callAsyncOperationMethod(op, hookName, [builder, result]);
handleReturnImmediatelyValue(builder);
return res;
});
}
});
return promise;
});
}
function ensureJoinRelatedOperation(builder, joinOperation) {
const opName = joinOperation + 'Relation';
let op = builder.findOperation(opName);
if (!op) {
op = new JoinRelatedOperation(opName, { joinOperation });
builder.addOperation(op);
}
return op;
}
function prebuildQuery(builder) {
builder = addImplicitOperations(builder);
builder = callOnBuildHooks(builder);
const queryExecutorOperation = findQueryExecutorOperation(builder);
if (queryExecutorOperation) {
return prebuildQuery(queryExecutorOperation.queryExecutor(builder));
} else {
return builder;
}
}
function addImplicitOperations(builder) {
if (builder.isFind()) {
// If no write operations have been called at this point this query is a
// find query and we need to call the custom find implementation.
addFindOperation(builder);
}
if (builder.hasWithGraph()) {
moveEagerOperationToEnd(builder);
}
return builder;
}
function addFindOperation(builder) {
if (!builder.has(FindOperation)) {
const operation = builder._findOperationFactory(builder);
builder.addOperationToFront(operation, []);
}
}
function moveEagerOperationToEnd(builder) {
const eagerOp = builder.findOperation(EagerOperation);
builder.clear(EagerOperation);
builder.addOperation(eagerOp);
}
function callOnBuildHooks(builder) {
callOnBuildFuncs(builder, builder.context().onBuild);
callOnBuildFuncs(builder, builder.internalContext().onBuild);
builder.executeOnBuild();
return builder;
}
function callOnBuildFuncs(builder, func) {
if (isFunction(func)) {
func.call(builder, builder);
} else if (Array.isArray(func)) {
func.forEach((func) => callOnBuildFuncs(builder, func));
}
}
function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
knexBuilder = builder.executeOnBuildKnex(knexBuilder);
const fromOperation = builder.findLastOperation(QueryBuilderBase.FromSelector);
if (!builder.isPartial()) {
// Set the table only if it hasn't been explicitly set yet.
if (!fromOperation) {
knexBuilder = setDefaultTable(builder, knexBuilder);
}
// Only add `table.*` select if there are no explicit selects
// and `from` is a table name and not a subquery.
if (!builder.hasSelects() && (!fromOperation || fromOperation.table)) {
knexBuilder = setDefaultSelect(builder, knexBuilder);
}
}
return knexBuilder;
}
function setDefaultTable(builder, knexBuilder) {
const table = builder.tableName();
const tableRef = builder.tableRef();
if (table === tableRef) {
return knexBuilder.table(table);
} else {
return knexBuilder.table(`${table} as ${tableRef}`);
}
}
function setDefaultSelect(builder, knexBuilder) {
const tableRef = builder.tableRef();
return knexBuilder.select(`${tableRef}.*`);
}
async function chainHooks(promise, builder, func) {
return promise.then((result) => {
let promise = Promise.resolve(result);
if (isFunction(func)) {
promise = promise.then((result) => {
const res = func.call(builder, result, builder);
handleReturnImmediatelyValue(builder);
return res;
});
} else if (Array.isArray(func)) {
func.forEach((func) => {
promise = chainHooks(promise, builder, func);
});
}
return promise;
});
}
function createModels(result, builder) {
if (result === null || result === undefined) {
return null;
}
if (builder.isInsert()) {
// results are applied to input models in `InsertOperation.onAfter1` instead.
return result;
}
const modelClass = builder.resultModelClass();
if (Array.isArray(result)) {
if (result.length && shouldBeConvertedToModel(result[0], modelClass)) {
for (let i = 0, l = result.length; i < l; ++i) {
result[i] = modelClass.fromDatabaseJson(result[i]);
}
}
} else if (shouldBeConvertedToModel(result, modelClass)) {
result = modelClass.fromDatabaseJson(result);
}
return result;
}
function shouldBeConvertedToModel(obj, modelClass) {
return isObject(obj) && !(obj instanceof modelClass);
}
function writeOperation(builder, cb) {
if (!builder.isFind()) {
return builder.reject(
new Error(
'Double call to a write method. ' +
'You can only call one of the write methods ' +
'(insert, update, patch, delete, relate, unrelate, increment, decrement) ' +
'and only once per query builder.',
),
);
}
try {
cb();
return builder;
} catch (err) {
return builder.reject(err);
}
}
function findOperationFactory() {
return new FindOperation('find');
}
function insertOperationFactory() {
return new InsertOperation('insert');
}
function updateOperationFactory() {
return new UpdateOperation('update');
}
function patchOperationFactory() {
return new UpdateOperation('patch', {
modelOptions: { patch: true },
});
}
function relateOperationFactory() {
return new RelateOperation('relate', {});
}
function unrelateOperationFactory() {
return new UnrelateOperation('unrelate', {});
}
function deleteOperationFactory() {
return new DeleteOperation('delete');
}
module.exports = {
QueryBuilder,
};
================================================
FILE: lib/queryBuilder/QueryBuilderBase.js
================================================
'use strict';
const { QueryBuilderOperationSupport } = require('./QueryBuilderOperationSupport');
const { isSqlite, isMsSql } = require('../utils/knexUtils');
const { KnexOperation } = require('./operations/KnexOperation');
const { MergeOperation } = require('./operations/MergeOperation');
const { SelectOperation } = require('./operations/select/SelectOperation');
const { ReturningOperation } = require('./operations/ReturningOperation');
const { WhereCompositeOperation } = require('./operations/WhereCompositeOperation');
const { WhereJsonPostgresOperation } = require('./operations/jsonApi/WhereJsonPostgresOperation');
const {
WhereInCompositeOperation,
} = require('./operations/whereInComposite/WhereInCompositeOperation');
const {
WhereInCompositeSqliteOperation,
} = require('./operations/whereInComposite/WhereInCompositeSqliteOperation');
const {
WhereInCompositeMsSqlOperation,
} = require('./operations/whereInComposite/WhereInCompositeMsSqlOperation');
const {
WhereJsonHasPostgresOperation,
} = require('./operations/jsonApi/WhereJsonHasPostgresOperation');
const {
WhereJsonNotObjectPostgresOperation,
} = require('./operations/jsonApi/WhereJsonNotObjectPostgresOperation');
class QueryBuilderBase extends QueryBuilderOperationSupport {
modify(...args) {
const func = args[0];
if (!func) {
return this;
}
if (args.length === 1) {
func.call(this, this);
} else {
args[0] = this;
func(...args);
}
return this;
}
transacting(trx) {
this._context.knex = trx || null;
return this;
}
select(...args) {
return this.addOperation(new SelectOperation('select'), args);
}
insert(...args) {
return this.addOperation(new KnexOperation('insert'), args);
}
update(...args) {
return this.addOperation(new KnexOperation('update'), args);
}
delete(...args) {
return this.addOperation(new KnexOperation('delete'), args);
}
del(...args) {
return this.delete(...args);
}
forUpdate(...args) {
return this.addOperation(new KnexOperation('forUpdate'), args);
}
forShare(...args) {
return this.addOperation(new KnexOperation('forShare'), args);
}
forNoKeyUpdate(...args) {
return this.addOperation(new KnexOperation('forNoKeyUpdate'), args);
}
forKeyShare(...args) {
return this.addOperation(new KnexOperation('forKeyShare'), args);
}
skipLocked(...args) {
return this.addOperation(new KnexOperation('skipLocked'), args);
}
noWait(...args) {
return this.addOperation(new KnexOperation('noWait'), args);
}
as(...args) {
return this.addOperation(new KnexOperation('as'), args);
}
columns(...args) {
return this.addOperation(new SelectOperation('columns'), args);
}
column(...args) {
return this.addOperation(new SelectOperation('column'), args);
}
from(...args) {
return this.addOperation(new KnexOperation('from'), args);
}
fromJS(...args) {
return this.addOperation(new KnexOperation('fromJS'), args);
}
fromRaw(...args) {
return this.addOperation(new KnexOperation('fromRaw'), args);
}
into(...args) {
return this.addOperation(new KnexOperation('into'), args);
}
withSchema(...args) {
return this.addOperation(new KnexOperation('withSchema'), args);
}
table(...args) {
return this.addOperation(new KnexOperation('table'), args);
}
distinct(...args) {
return this.addOperation(new SelectOperation('distinct'), args);
}
distinctOn(...args) {
return this.addOperation(new SelectOperation('distinctOn'), args);
}
join(...args) {
return this.addOperation(new KnexOperation('join'), args);
}
joinRaw(...args) {
return this.addOperation(new KnexOperation('joinRaw'), args);
}
innerJoin(...args) {
return this.addOperation(new KnexOperation('innerJoin'), args);
}
leftJoin(...args) {
return this.addOperation(new KnexOperation('leftJoin'), args);
}
leftOuterJoin(...args) {
return this.addOperation(new KnexOperation('leftOuterJoin'), args);
}
rightJoin(...args) {
return this.addOperation(new KnexOperation('rightJoin'), args);
}
rightOuterJoin(...args) {
return this.addOperation(new KnexOperation('rightOuterJoin'), args);
}
outerJoin(...args) {
return this.addOperation(new KnexOperation('outerJoin'), args);
}
fullOuterJoin(...args) {
return this.addOperation(new KnexOperation('fullOuterJoin'), args);
}
crossJoin(...args) {
return this.addOperation(new KnexOperation('crossJoin'), args);
}
where(...args) {
return this.addOperation(new KnexOperation('where'), args);
}
andWhere(...args) {
return this.addOperation(new KnexOperation('andWhere'), args);
}
orWhere(...args) {
return this.addOperation(new KnexOperation('orWhere'), args);
}
whereNot(...args) {
return this.addOperation(new KnexOperation('whereNot'), args);
}
andWhereNot(...args) {
return this.addOperation(new KnexOperation('andWhereNot'), args);
}
orWhereNot(...args) {
return this.addOperation(new KnexOperation('orWhereNot'), args);
}
whereRaw(...args) {
return this.addOperation(new KnexOperation('whereRaw'), args);
}
andWhereRaw(...args) {
return this.addOperation(new KnexOperation('andWhereRaw'), args);
}
orWhereRaw(...args) {
return this.addOperation(new KnexOperation('orWhereRaw'), args);
}
whereWrapped(...args) {
return this.addOperation(new KnexOperation('whereWrapped'), args);
}
havingWrapped(...args) {
return this.addOperation(new KnexOperation('havingWrapped'), args);
}
whereExists(...args) {
return this.addOperation(new KnexOperation('whereExists'), args);
}
orWhereExists(...args) {
return this.addOperation(new KnexOperation('orWhereExists'), args);
}
whereNotExists(...args) {
return this.addOperation(new KnexOperation('whereNotExists'), args);
}
orWhereNotExists(...args) {
return this.addOperation(new KnexOperation('orWhereNotExists'), args);
}
whereIn(...args) {
return this.addOperation(new KnexOperation('whereIn'), args);
}
orWhereIn(...args) {
return this.addOperation(new KnexOperation('orWhereIn'), args);
}
whereNotIn(...args) {
return this.addOperation(new KnexOperation('whereNotIn'), args);
}
orWhereNotIn(...args) {
return this.addOperation(new KnexOperation('orWhereNotIn'), args);
}
whereNull(...args) {
return this.addOperation(new KnexOperation('whereNull'), args);
}
orWhereNull(...args) {
return this.addOperation(new KnexOperation('orWhereNull'), args);
}
whereNotNull(...args) {
return this.addOperation(new KnexOperation('whereNotNull'), args);
}
orWhereNotNull(...args) {
return this.addOperation(new KnexOperation('orWhereNotNull'), args);
}
whereBetween(...args) {
return this.addOperation(new KnexOperation('whereBetween'), args);
}
andWhereBetween(...args) {
return this.addOperation(new KnexOperation('andWhereBetween'), args);
}
whereNotBetween(...args) {
return this.addOperation(new KnexOperation('whereNotBetween'), args);
}
andWhereNotBetween(...args) {
return this.addOperation(new KnexOperation('andWhereNotBetween'), args);
}
orWhereBetween(...args) {
return this.addOperation(new KnexOperation('orWhereBetween'), args);
}
orWhereNotBetween(...args) {
return this.addOperation(new KnexOperation('orWhereNotBetween'), args);
}
whereLike(...args) {
return this.addOperation(new KnexOperation('whereLike'), args);
}
andWhereLike(...args) {
return this.addOperation(new KnexOperation('andWhereLike'), args);
}
orWhereLike(...args) {
return this.addOperation(new KnexOperation('orWhereLike'), args);
}
whereILike(...args) {
return this.addOperation(new KnexOperation('whereILike'), args);
}
andWhereILike(...args) {
return this.addOperation(new KnexOperation('andWhereILike'), args);
}
orWhereILike(...args) {
return this.addOperation(new KnexOperation('orWhereILike'), args);
}
groupBy(...args) {
return this.addOperation(new KnexOperation('groupBy'), args);
}
groupByRaw(...args) {
return this.addOperation(new KnexOperation('groupByRaw'), args);
}
orderBy(...args) {
return this.addOperation(new KnexOperation('orderBy'), args);
}
orderByRaw(...args) {
return this.addOperation(new KnexOperation('orderByRaw'), args);
}
union(...args) {
return this.addOperation(new KnexOperation('union'), args);
}
unionAll(...args) {
return this.addOperation(new KnexOperation('unionAll'), args);
}
intersect(...args) {
return this.addOperation(new KnexOperation('intersect'), args);
}
except(...args) {
return this.addOperation(new KnexOperation('except'), args);
}
having(...args) {
return this.addOperation(new KnexOperation('having'), args);
}
clearHaving(...args) {
return this.addOperation(new KnexOperation('clearHaving'), args);
}
clearGroup(...args) {
return this.addOperation(new KnexOperation('clearGroup'), args);
}
orHaving(...args) {
return this.addOperation(new KnexOperation('orHaving'), args);
}
havingIn(...args) {
return this.addOperation(new KnexOperation('havingIn'), args);
}
orHavingIn(...args) {
return this.addOperation(new KnexOperation('havingIn'), args);
}
havingNotIn(...args) {
return this.addOperation(new KnexOperation('havingNotIn'), args);
}
orHavingNotIn(...args) {
return this.addOperation(new KnexOperation('orHavingNotIn'), args);
}
havingNull(...args) {
return this.addOperation(new KnexOperation('havingNull'), args);
}
orHavingNull(...args) {
return this.addOperation(new KnexOperation('orHavingNull'), args);
}
havingNotNull(...args) {
return this.addOperation(new KnexOperation('havingNotNull'), args);
}
orHavingNotNull(...args) {
return this.addOperation(new KnexOperation('orHavingNotNull'), args);
}
havingExists(...args) {
return this.addOperation(new KnexOperation('havingExists'), args);
}
orHavingExists(...args) {
return this.addOperation(new KnexOperation('orHavingExists'), args);
}
havingNotExists(...args) {
return this.addOperation(new KnexOperation('havingNotExists'), args);
}
orHavingNotExists(...args) {
return this.addOperation(new KnexOperation('orHavingNotExists'), args);
}
havingBetween(...args) {
return this.addOperation(new KnexOperation('havingBetween'), args);
}
orHavingBetween(...args) {
return this.addOperation(new KnexOperation('havingBetween'), args);
}
havingNotBetween(...args) {
return this.addOperation(new KnexOperation('havingNotBetween'), args);
}
orHavingNotBetween(...args) {
return this.addOperation(new KnexOperation('havingNotBetween'), args);
}
havingRaw(...args) {
return this.addOperation(new KnexOperation('havingRaw'), args);
}
orHavingRaw(...args) {
return this.addOperation(new KnexOperation('orHavingRaw'), args);
}
offset(...args) {
return this.addOperation(new KnexOperation('offset'), args);
}
limit(...args) {
return this.addOperation(new KnexOperation('limit'), args);
}
count(...args) {
return this.addOperation(new SelectOperation('count'), args);
}
countDistinct(...args) {
return this.addOperation(new SelectOperation('countDistinct'), args);
}
min(...args) {
return this.addOperation(new SelectOperation('min'), args);
}
max(...args) {
return this.addOperation(new SelectOperation('max'), args);
}
sum(...args) {
return this.addOperation(new SelectOperation('sum'), args);
}
sumDistinct(...args) {
return this.addOperation(new SelectOperation('sumDistinct'), args);
}
avg(...args) {
return this.addOperation(new SelectOperation('avg'), args);
}
avgDistinct(...args) {
return this.addOperation(new SelectOperation('avgDistinct'), args);
}
debug(...args) {
return this.addOperation(new KnexOperation('debug'), args);
}
returning(...args) {
return this.addOperation(new ReturningOperation('returning'), args);
}
truncate(...args) {
return this.addOperation(new KnexOperation('truncate'), args);
}
connection(...args) {
return this.addOperation(new KnexOperation('connection'), args);
}
options(...args) {
return this.addOperation(new KnexOperation('options'), args);
}
columnInfo(...args) {
return this.addOperation(new KnexOperation('columnInfo'), args);
}
off(...args) {
return this.addOperation(new KnexOperation('off'), args);
}
timeout(...args) {
return this.addOperation(new KnexOperation('timeout'), args);
}
with(...args) {
return this.addOperation(new KnexOperation('with'), args);
}
withWrapped(...args) {
return this.addOperation(new KnexOperation('withWrapped'), args);
}
withRecursive(...args) {
return this.addOperation(new KnexOperation('withRecursive'), args);
}
withMaterialized(...args) {
return this.addOperation(new KnexOperation('withMaterialized'), args);
}
withNotMaterialized(...args) {
return this.addOperation(new KnexOperation('withNotMaterialized'), args);
}
whereComposite(...args) {
return this.addOperation(new WhereCompositeOperation('whereComposite'), args);
}
whereInComposite(...args) {
let operation = null;
if (isSqlite(this.knex())) {
operation = new WhereInCompositeSqliteOperation('whereInComposite');
} else if (isMsSql(this.knex())) {
operation = new WhereInCompositeMsSqlOperation('whereInComposite');
} else {
operation = new WhereInCompositeOperation('whereInComposite');
}
return this.addOperation(operation, args);
}
whereNotInComposite(...args) {
let operation = null;
if (isSqlite(this.knex())) {
operation = new WhereInCompositeSqliteOperation('whereNotInComposite', { prefix: 'not' });
} else if (isMsSql(this.knex())) {
operation = new WhereInCompositeMsSqlOperation('whereNotInComposite', { prefix: 'not' });
} else {
operation = new WhereInCompositeOperation('whereNotInComposite', { prefix: 'not' });
}
return this.addOperation(operation, args);
}
jsonExtract(...args) {
return this.addOperation(new KnexOperation('jsonExtract'), args);
}
jsonSet(...args) {
return this.addOperation(new KnexOperation('jsonSet'), args);
}
jsonInsert(...args) {
return this.addOperation(new KnexOperation('jsonInsert'), args);
}
jsonRemove(...args) {
return this.addOperation(new KnexOperation('jsonRemove'), args);
}
whereJsonObject(...args) {
return this.addOperation(new KnexOperation('whereJsonObject'), args);
}
orWhereJsonObject(...args) {
return this.addOperation(new KnexOperation('orWhereJsonObject'), args);
}
andWhereJsonObject(...args) {
return this.addOperation(new KnexOperation('andWhereJsonObject'), args);
}
whereNotJsonObject(...args) {
return this.addOperation(new KnexOperation('whereNotJsonObject'), args);
}
orWhereNotJsonObject(...args) {
return this.addOperation(new KnexOperation('orWhereNotJsonObject'), args);
}
andWhereNotJsonObject(...args) {
return this.addOperation(new KnexOperation('andWhereNotJsonObject'), args);
}
whereJsonPath(...args) {
return this.addOperation(new KnexOperation('whereJsonPath'), args);
}
orWhereJsonPath(...args) {
return this.addOperation(new KnexOperation('orWhereJsonPath'), args);
}
andWhereJsonPath(...args) {
return this.addOperation(new KnexOperation('andWhereJsonPath'), args);
}
// whereJson(Not)SupersetOf / whereJson(Not)SubsetOf are now supported by knex >= 1.0, but for now
// objection handles them differently and only for postgres.
// Changing them to utilize knex methods directly may require a major version bump and upgrade guide.
whereJsonSupersetOf(...args) {
return this.addOperation(
new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
args,
);
}
andWhereJsonSupersetOf(...args) {
return this.whereJsonSupersetOf(...args);
}
orWhereJsonSupersetOf(...args) {
return this.addOperation(
new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
args,
);
}
whereJsonNotSupersetOf(...args) {
return this.addOperation(
new WhereJsonPostgresOperation('whereJsonNotSupersetOf', {
operator: '@>',
bool: 'and',
prefix: 'not',
}),
args,
);
}
andWhereJsonNotSupersetOf(...args) {
return this.andWhereJsonNotSupersetOf(...args);
}
orWhereJsonNotSupersetOf(...args) {
return this.addOperation(
new WhereJsonPostgresOperation('orWhereJsonNotSupersetOf', {
operator: '@>',
bool: 'or',
prefix: 'not',
}),
args,
);
}
whereJsonSubsetOf(...args) {
return this.addOperation(
new WhereJsonPostgresOperation('whereJsonSubsetOf', { operator: '<@', bool: 'and' }),
args,
);
}
andWhereJsonSubsetOf(...args) {
return this.whereJsonSubsetOf(...args);
}
orWhereJsonSubsetOf(...args) {
return this.addOperation(
new WhereJsonPostgresOperation('orWhereJsonSubsetOf', { operator: '<@', bool: 'or' }),
args,
);
}
whereJsonNotSubsetOf(...args) {
return this.addOperation(
new WhereJsonPostgresOperation('whereJsonNotSubsetOf', {
operator: '<@',
bool: 'and',
prefix: 'not',
}),
args,
);
}
andWhereJsonNotSubsetOf(...args) {
return this.whereJsonNotSubsetOf(...args);
}
orWhereJsonNotSubsetOf(...args) {
return this.addOperation(
new WhereJsonPostgresOperation('orWhereJsonNotSubsetOf', {
operator: '<@',
bool: 'or',
prefix: 'not',
}),
args,
);
}
whereJsonNotArray(...args) {
return this.addOperation(
new WhereJsonNotObjectPostgresOperation('whereJsonNotArray', {
bool: 'and',
compareValue: [],
}),
args,
);
}
orWhereJsonNotArray(...args) {
return this.addOperation(
new WhereJsonNotObjectPostgresOperation('orWhereJsonNotArray', {
bool: 'or',
compareValue: [],
}),
args,
);
}
whereJsonNotObject(...args) {
return this.addOperation(
new WhereJsonNotObjectPostgresOperation('whereJsonNotObject', {
bool: 'and',
compareValue: {},
}),
args,
);
}
orWhereJsonNotObject(...args) {
return this.addOperation(
new WhereJsonNotObjectPostgresOperation('orWhereJsonNotObject', {
bool: 'or',
compareValue: {},
}),
args,
);
}
whereJsonHasAny(...args) {
return this.addOperation(
new WhereJsonHasPostgresOperation('whereJsonHasAny', { bool: 'and', operator: '?|' }),
args,
);
}
orWhereJsonHasAny(...args) {
return this.addOperation(
new WhereJsonHasPostgresOperation('orWhereJsonHasAny', { bool: 'or', operator: '?|' }),
args,
);
}
whereJsonHasAll(...args) {
return this.addOperation(
new WhereJsonHasPostgresOperation('whereJsonHasAll', { bool: 'and', operator: '?&' }),
args,
);
}
orWhereJsonHasAll(...args) {
return this.addOperation(
new WhereJsonHasPostgresOperation('orWhereJsonHasAll', { bool: 'or', operator: '?&' }),
args,
);
}
whereJsonIsArray(fieldExpression) {
return this.whereJsonSupersetOf(fieldExpression, []);
}
orWhereJsonIsArray(fieldExpression) {
return this.orWhereJsonSupersetOf(fieldExpression, []);
}
whereJsonIsObject(fieldExpression) {
return this.whereJsonSupersetOf(fieldExpression, {});
}
orWhereJsonIsObject(fieldExpression) {
return this.orWhereJsonSupersetOf(fieldExpression, {});
}
whereColumn(...args) {
return this.addOperation(new KnexOperation('whereColumn'), args);
}
andWhereColumn(...args) {
return this.addOperation(new KnexOperation('andWhereColumn'), args);
}
orWhereColumn(...args) {
return this.addOperation(new KnexOperation('orWhereColumn'), args);
}
whereNotColumn(...args) {
return this.addOperation(new KnexOperation('whereNotColumn'), args);
}
andWhereNotColumn(...args) {
return this.addOperation(new KnexOperation('andWhereNotColumn'), args);
}
orWhereNotColumn(...args) {
return this.addOperation(new KnexOperation('orWhereNotColumn'), args);
}
onConflict(...args) {
return this.addOperation(new KnexOperation('onConflict'), args);
}
ignore(...args) {
return this.addOperation(new KnexOperation('ignore'), args);
}
merge(...args) {
return this.addOperation(new MergeOperation('merge'), args);
}
}
Object.defineProperties(QueryBuilderBase.prototype, {
isObjectionQueryBuilderBase: {
enumerable: false,
writable: false,
value: true,
},
});
module.exports = {
QueryBuilderBase,
};
================================================
FILE: lib/queryBuilder/QueryBuilderContext.js
================================================
'use strict';
const { QueryBuilderContextBase } = require('./QueryBuilderContextBase');
class QueryBuilderContext extends QueryBuilderContextBase {
constructor(builder) {
super(builder);
this.runBefore = [];
this.runAfter = [];
this.onBuild = [];
}
clone() {
const ctx = super.clone();
ctx.runBefore = this.runBefore.slice();
ctx.runAfter = this.runAfter.slice();
ctx.onBuild = this.onBuild.slice();
return ctx;
}
}
module.exports = {
QueryBuilderContext,
};
================================================
FILE: lib/queryBuilder/QueryBuilderContextBase.js
================================================
'use strict';
const { InternalOptions } = require('./InternalOptions');
class QueryBuilderContextBase {
constructor(builder) {
this.userContext = builder ? new builder.constructor.QueryBuilderUserContext(builder) : null;
this.options = builder ? new this.constructor.InternalOptions() : null;
this.knex = null;
this.aliasMap = null;
this.tableMap = null;
}
static get InternalOptions() {
return InternalOptions;
}
clone() {
const ctx = new this.constructor();
ctx.userContext = this.userContext;
ctx.options = this.options.clone();
ctx.knex = this.knex;
ctx.aliasMap = this.aliasMap;
ctx.tableMap = this.tableMap;
return ctx;
}
}
module.exports = {
QueryBuilderContextBase,
};
================================================
FILE: lib/queryBuilder/QueryBuilderOperationSupport.js
================================================
'use strict';
const { isString, isFunction, isRegExp, mergeMaps, last } = require('../utils/objectUtils');
const { QueryBuilderContextBase } = require('./QueryBuilderContextBase');
const { QueryBuilderUserContext } = require('./QueryBuilderUserContext');
const { deprecate } = require('../utils/deprecate');
const AllSelector = () => true;
const SelectSelector =
/^(select|columns|column|distinct|count|countDistinct|min|max|sum|sumDistinct|avg|avgDistinct)$/;
const WhereSelector = /^(where|orWhere|andWhere|find\w+)/;
const OnSelector = /^(on|orOn|andOn)/;
const OrderBySelector = /orderBy/;
const JoinSelector = /(join|joinRaw|joinRelated)$/i;
const FromSelector = /^(from|fromRaw|into|table)$/;
class QueryBuilderOperationSupport {
constructor(...args) {
this.constructor.init(this, ...args);
}
static init(self, modelClass) {
self._modelClass = modelClass;
self._operations = [];
self._context = new this.QueryBuilderContext(self);
self._parentQuery = null;
self._isPartialQuery = false;
self._activeOperations = [];
}
static forClass(modelClass) {
return new this(modelClass);
}
static get AllSelector() {
return AllSelector;
}
static get QueryBuilderContext() {
return QueryBuilderContextBase;
}
static get QueryBuilderUserContext() {
return QueryBuilderUserContext;
}
static get SelectSelector() {
return SelectSelector;
}
static get WhereSelector() {
return WhereSelector;
}
static get OnSelector() {
return OnSelector;
}
static get JoinSelector() {
return JoinSelector;
}
static get FromSelector() {
return FromSelector;
}
static get OrderBySelector() {
return OrderBySelector;
}
modelClass() {
return this._modelClass;
}
context(obj) {
const ctx = this._context;
if (arguments.length === 0) {
return ctx.userContext;
} else {
ctx.userContext = ctx.userContext.newMerge(this, obj);
return this;
}
}
clearContext() {
const ctx = this._context;
ctx.userContext = new this.constructor.QueryBuilderUserContext(this);
return this;
}
internalContext(ctx) {
if (arguments.length === 0) {
return this._context;
} else {
this._context = ctx;
return this;
}
}
internalOptions(opt) {
if (arguments.length === 0) {
return this._context.options;
} else {
const oldOpt = this._context.options;
this._context.options = Object.assign(oldOpt, opt);
return this;
}
}
isPartial(isPartial) {
if (arguments.length === 0) {
return this._isPartialQuery;
} else {
this._isPartialQuery = isPartial;
return this;
}
}
isInternal() {
return this.internalOptions().isInternalQuery;
}
tableNameFor(tableName, newTableName) {
const ctx = this.internalContext();
const tableMap = ctx.tableMap;
if (isString(newTableName)) {
ctx.tableMap = tableMap || new Map();
ctx.tableMap.set(tableName, newTableName);
return this;
} else {
return (tableMap && tableMap.get(tableName)) || tableName;
}
}
aliasFor(tableName, alias) {
const ctx = this.internalContext();
const aliasMap = ctx.aliasMap;
if (isString(alias)) {
ctx.aliasMap = aliasMap || new Map();
ctx.aliasMap.set(tableName, alias);
return this;
} else {
return (aliasMap && aliasMap.get(tableName)) || null;
}
}
tableRefFor(tableName) {
return this.aliasFor(tableName) || this.tableNameFor(tableName);
}
childQueryOf(query, { fork, isInternalQuery } = {}) {
if (query) {
let currentCtx = this.context();
let ctx = query.internalContext();
if (fork) {
ctx = ctx.clone();
}
if (isInternalQuery) {
ctx.options.isInternalQuery = true;
}
this._parentQuery = query;
this.internalContext(ctx);
this.context(currentCtx);
// Use the parent's knex if there was no knex in `ctx`.
if (this.unsafeKnex() === null) {
this.knex(query.unsafeKnex());
}
}
return this;
}
subqueryOf(query) {
if (query) {
if (this._isPartialQuery) {
// Merge alias and table name maps for "partial" subqueries.
const ctx = this.internalContext();
ctx.aliasMap = mergeMaps(query.internalContext().aliasMap, ctx.aliasMap);
ctx.tableMap = mergeMaps(query.internalContext().tableMap, ctx.tableMap);
}
this._parentQuery = query;
if (this.unsafeKnex() === null) {
this.knex(query.unsafeKnex());
}
}
return this;
}
parentQuery() {
return this._parentQuery;
}
knex(...args) {
if (args.length === 0) {
const knex = this.unsafeKnex();
if (!knex) {
throw new Error(
`no database connection available for a query. You need to bind the model class or the query to a knex instance.`,
);
}
return knex;
} else {
this._context.knex = args[0];
return this;
}
}
unsafeKnex() {
return this._context.knex || this._modelClass.knex() || null;
}
clear(operationSelector) {
const operationsToRemove = new Set();
this.forEachOperation(operationSelector, (op) => {
// If an ancestor operation has already been removed,
// there's no need to remove the children anymore.
if (!op.isAncestorInSet(operationsToRemove)) {
operationsToRemove.add(op);
}
});
for (const op of operationsToRemove) {
this.removeOperation(op);
}
return this;
}
toFindQuery() {
const findQuery = this.clone();
const operationsToReplace = [];
const operationsToRemove = [];
findQuery.forEachOperation(
(op) => op.hasToFindOperation(),
(op) => {
const findOp = op.toFindOperation(findQuery);
if (!findOp) {
operationsToRemove.push(op);
} else {
operationsToReplace.push({ op, findOp });
}
},
);
for (const op of operationsToRemove) {
findQuery.removeOperation(op);
}
for (const { op, findOp } of operationsToReplace) {
findQuery.replaceOperation(op, findOp);
}
return findQuery;
}
clearSelect() {
return this.clear(SelectSelector);
}
clearWhere() {
return this.clear(WhereSelector);
}
clearOrder() {
return this.clear(OrderBySelector);
}
copyFrom(queryBuilder, operationSelector) {
const operationsToAdd = new Set();
queryBuilder.forEachOperation(operationSelector, (op) => {
// If an ancestor operation has already been added,
// there is no need to add
if (!op.isAncestorInSet(operationsToAdd)) {
operationsToAdd.add(op);
}
});
for (const op of operationsToAdd) {
const opClone = op.clone();
// We may be moving nested operations to the root. Clear
// any links to the parent operations.
opClone.parentOperation = null;
opClone.adderHookName = null;
// We don't use `addOperation` here because we don't what to
// call `onAdd` or add these operations as child operations.
this._operations.push(opClone);
}
return this;
}
has(operationSelector) {
return !!this.findOperation(operationSelector);
}
forEachOperation(operationSelector, callback, match = true) {
const selector = buildFunctionForOperationSelector(operationSelector);
for (const op of this._operations) {
if (selector(op) === match && callback(op) === false) {
break;
}
const childRes = op.forEachDescendantOperation((op) => {
if (selector(op) === match && callback(op) === false) {
return false;
}
});
if (childRes === false) {
break;
}
}
return this;
}
findOperation(operationSelector) {
let op = null;
this.forEachOperation(operationSelector, (it) => {
op = it;
return false;
});
return op;
}
findLastOperation(operationSelector) {
let op = null;
this.forEachOperation(operationSelector, (it) => {
op = it;
});
return op;
}
everyOperation(operationSelector) {
let every = true;
this.forEachOperation(
operationSelector,
() => {
every = false;
return false;
},
false,
);
return every;
}
callOperationMethod(operation, hookName, args) {
try {
operation.removeChildOperationsByHookName(hookName);
this._activeOperations.push({
operation,
hookName,
});
return operation[hookName](...args);
} finally {
this._activeOperations.pop();
}
}
async callAsyncOperationMethod(operation, hookName, args) {
operation.removeChildOperationsByHookName(hookName);
this._activeOperations.push({
operation,
hookName,
});
try {
return await operation[hookName](...args);
} finally {
this._activeOperations.pop();
}
}
addOperation(operation, args) {
const ret = this.addOperationUsingMethod('push', operation, args);
return ret;
}
addOperationToFront(operation, args) {
return this.addOperationUsingMethod('unshift', operation, args);
}
addOperationUsingMethod(arrayMethod, operation, args) {
const shouldAdd = this.callOperationMethod(operation, 'onAdd', [this, args]);
if (shouldAdd) {
if (this._activeOperations.length) {
const { operation: parentOperation, hookName } = last(this._activeOperations);
parentOperation.addChildOperation(hookName, operation);
} else {
this._operations[arrayMethod](operation);
}
}
return this;
}
removeOperation(operation) {
if (operation.parentOperation) {
operation.parentOperation.removeChildOperation(operation);
} else {
const index = this._operations.indexOf(operation);
if (index !== -1) {
this._operations.splice(index, 1);
}
}
return this;
}
replaceOperation(operation, newOperation) {
if (operation.parentOperation) {
operation.parentOperation.replaceChildOperation(operation, newOperation);
} else {
const index = this._operations.indexOf(operation);
if (index !== -1) {
this._operations[index] = newOperation;
}
}
return this;
}
clone() {
return this.baseCloneInto(new this.constructor(this.unsafeKnex()));
}
baseCloneInto(builder) {
builder._modelClass = this._modelClass;
builder._operations = this._operations.map((it) => it.clone());
builder._context = this._context.clone();
builder._parentQuery = this._parentQuery;
builder._isPartialQuery = this._isPartialQuery;
// Don't copy the active operation stack. We never continue (nor can we)
// a query from the exact mid-hook-call state.
builder._activeOperations = [];
return builder;
}
toKnexQuery(knexBuilder = this.knex().queryBuilder()) {
this.executeOnBuild();
return this.executeOnBuildKnex(knexBuilder);
}
executeOnBuild() {
this.forEachOperation(true, (op) => {
if (op.hasOnBuild()) {
this.callOperationMethod(op, 'onBuild', [this]);
}
});
}
executeOnBuildKnex(knexBuilder) {
this.forEachOperation(true, (op) => {
if (op.hasOnBuildKnex()) {
const newKnexBuilder = this.callOperationMethod(op, 'onBuildKnex', [knexBuilder, this]);
// Default to the input knex builder for backwards compatibility
// with QueryBuilder.onBuildKnex hooks.
knexBuilder = newKnexBuilder || knexBuilder;
}
});
return knexBuilder;
}
toString() {
return this.toKnexQuery().toString();
}
toSql() {
return this.toString();
}
skipUndefined() {
deprecate('skipUndefined() is deprecated and will be removed in objection 4.0');
this.internalOptions().skipUndefined = true;
return this;
}
}
function buildFunctionForOperationSelector(operationSelector) {
if (operationSelector === true) {
return AllSelector;
} else if (isRegExp(operationSelector)) {
return (op) => operationSelector.test(op.name);
} else if (isString(operationSelector)) {
return (op) => op.name === operationSelector;
} else if (
isFunction(operationSelector) &&
operationSelector.isObjectionQueryBuilderOperationClass
) {
return (op) => op.is(operationSelector);
} else if (isFunction(operationSelector)) {
return operationSelector;
} else {
return () => false;
}
}
module.exports = {
QueryBuilderOperationSupport,
};
================================================
FILE: lib/queryBuilder/QueryBuilderUserContext.js
================================================
'use strict';
const SYMBOL_BUILDER = Symbol();
class QueryBuilderUserContext {
constructor(builder) {
// This should never ever be accessed outside this class. We only
// store it so that we can access builder.knex() lazily.
this[SYMBOL_BUILDER] = builder;
}
get transaction() {
return this[SYMBOL_BUILDER].knex();
}
newFromObject(builder, obj) {
const ctx = new this.constructor(builder);
Object.assign(ctx, obj);
return ctx;
}
newMerge(builder, obj) {
const ctx = new this.constructor(builder);
Object.assign(ctx, this, obj);
return ctx;
}
}
module.exports = {
QueryBuilderUserContext,
};
================================================
FILE: lib/queryBuilder/RawBuilder.js
================================================
'use strict';
const { isPlainObject } = require('../utils/objectUtils');
const { buildArg } = require('../utils/buildUtils');
class RawBuilder {
constructor(sql, args) {
this._sql = `${sql}`;
this._args = args;
this._as = null;
}
get alias() {
return this._as;
}
as(as) {
this._as = as;
return this;
}
toKnexRaw(builder) {
let args = null;
let sql = this._sql;
if (this._args.length === 1 && isPlainObject(this._args[0])) {
args = buildObject(this._args[0], builder);
if (this._as) {
args.__alias__ = this._as;
sql += ' as :__alias__:';
}
} else {
args = buildArray(this._args, builder);
if (this._as) {
args.push(this._as);
sql += ' as ??';
}
}
return builder.knex().raw(sql, args);
}
}
Object.defineProperties(RawBuilder.prototype, {
isObjectionRawBuilder: {
enumerable: false,
writable: false,
value: true,
},
});
function buildArray(arr, builder) {
return arr.map((it) => buildArg(it, builder));
}
function buildObject(obj, builder) {
return Object.keys(obj).reduce((args, key) => {
args[key] = buildArg(obj[key], builder);
return args;
}, {});
}
function normalizeRawArgs(argsIn) {
const [sql, ...restArgs] = argsIn;
if (restArgs.length === 1 && Array.isArray(restArgs[0])) {
return {
sql,
args: restArgs[0],
};
} else {
return {
sql,
args: restArgs,
};
}
}
function raw(...argsIn) {
const { sql, args } = normalizeRawArgs(argsIn);
return new RawBuilder(sql, args);
}
module.exports = {
RawBuilder,
normalizeRawArgs,
raw,
};
================================================
FILE: lib/queryBuilder/ReferenceBuilder.js
================================================
'use strict';
const { parseFieldExpression } = require('../utils/parseFieldExpression');
const { isObject } = require('../utils/objectUtils');
class ReferenceBuilder {
constructor(expr) {
this._expr = expr;
this._parsedExpr = null;
this._column = null;
this._table = null;
this._cast = null;
this._toJson = false;
this._table = null;
this._alias = null;
this._modelClass = null;
// This `if` makes it possible for `clone` to skip
// parsing the expression again.
if (expr !== null) {
this._parseExpression(expr);
}
}
get parsedExpr() {
return this._parsedExpr;
}
get column() {
return this._column;
}
set column(column) {
this._column = column;
}
get alias() {
return this._alias;
}
set alias(alias) {
this._alias = alias;
}
get tableName() {
return this._table;
}
set tableName(table) {
this._table = table;
}
get modelClass() {
return this._modelClass;
}
set modelClass(modelClass) {
this._modelClass = modelClass;
}
get isPlainColumnRef() {
return (
(!this._parsedExpr || this._parsedExpr.access.length === 0) && !this._cast && !this._toJson
);
}
get expression() {
return this._expr;
}
get cast() {
return this._cast;
}
fullColumn(builder) {
const table = this.tableName
? this.tableName
: this.modelClass
? builder.tableRefFor(this.modelClass)
: null;
if (table) {
return `${table}.${this.column}`;
} else {
return this.column;
}
}
castText() {
return this.castTo('text');
}
castInt() {
return this.castTo('integer');
}
castBigInt() {
return this.castTo('bigint');
}
castFloat() {
return this.castTo('float');
}
castDecimal() {
return this.castTo('decimal');
}
castReal() {
return this.castTo('real');
}
castBool() {
return this.castTo('boolean');
}
castJson() {
this._toJson = true;
return this;
}
castTo(sqlType) {
this._cast = sqlType;
return this;
}
from(table) {
this._table = table;
return this;
}
table(table) {
this._table = table;
return this;
}
model(modelClass) {
this._modelClass = modelClass;
return this;
}
as(alias) {
this._alias = alias;
return this;
}
clone() {
const clone = new this.constructor(null);
clone._expr = this._expr;
clone._parsedExpr = this._parsedExpr;
clone._column = this._column;
clone._table = this._table;
clone._cast = this._cast;
clone._toJson = this._toJson;
clone._alias = this._alias;
clone._modelClass = this._modelClass;
return clone;
}
toKnexRaw(builder) {
return builder.knex().raw(...this._createRawArgs(builder));
}
_parseExpression(expr) {
this._parsedExpr = parseFieldExpression(expr);
this._column = this._parsedExpr.column;
this._table = this._parsedExpr.table;
}
_createRawArgs(builder) {
let bindings = [];
let sql = this._createReferenceSql(builder, bindings);
sql = this._maybeCast(sql, bindings);
sql = this._maybeToJsonb(sql, bindings);
sql = this._maybeAlias(sql, bindings);
return [sql, bindings];
}
_createReferenceSql(builder, bindings) {
bindings.push(this.fullColumn(builder));
if (this._parsedExpr.access.length > 0) {
const extractor = this._cast ? '#>>' : '#>';
const jsonFieldRef = this._parsedExpr.access.map((field) => field.ref).join(',');
return `??${extractor}'{${jsonFieldRef}}'`;
} else {
return '??';
}
}
_maybeCast(sql) {
if (this._cast) {
return `CAST(${sql} AS ${this._cast})`;
} else {
return sql;
}
}
_maybeToJsonb(sql) {
if (this._toJson) {
return `to_jsonb(${sql})`;
} else {
return sql;
}
}
_maybeAlias(sql, bindings) {
if (this._shouldAlias()) {
bindings.push(this._alias);
return `${sql} as ??`;
} else {
return sql;
}
}
_shouldAlias() {
if (!this._alias) {
return false;
} else if (!this.isPlainColumnRef) {
return true;
} else {
// No need to alias if we are dealing with a simple column reference
// and the alias is the same as the column name.
return this._alias !== this._column;
}
}
}
Object.defineProperties(ReferenceBuilder.prototype, {
isObjectionReferenceBuilder: {
enumerable: false,
writable: false,
value: true,
},
});
const ref = (reference) => {
if (isObject(reference) && reference.isObjectionReferenceBuilder) {
return reference;
} else {
return new ReferenceBuilder(reference);
}
};
module.exports = {
ReferenceBuilder,
ref,
};
================================================
FILE: lib/queryBuilder/RelationExpression.js
================================================
'use strict';
const parser = require('./parsers/relationExpressionParser');
const { isObject, isNumber, isString, union } = require('../utils/objectUtils');
const { RelationDoesNotExistError } = require('../model/RelationDoesNotExistError');
class RelationExpressionParseError extends Error {}
class DuplicateRelationError extends RelationExpressionParseError {
constructor(relationName) {
super();
this.relationName = relationName;
}
}
class RelationExpression {
constructor(node = newNode(), recursionDepth = 0) {
this.node = node;
this.recursionDepth = recursionDepth;
}
// Create a relation expression from a string, a pojo or another
// RelationExpression instance.
static create(expr) {
if (isObject(expr)) {
if (expr.isObjectionRelationExpression) {
return expr;
} else {
return new RelationExpression(normalizeNode(expr));
}
} else if (isString(expr)) {
if (expr.trim().length === 0) {
return new RelationExpression();
} else {
try {
return new RelationExpression(parse(expr));
} catch (err) {
if (err.duplicateRelationName) {
throw new DuplicateRelationError(err.duplicateRelationName);
} else {
throw new RelationExpressionParseError(err.message);
}
}
}
} else {
return new RelationExpression();
}
}
// Create a relation expression from a model graph.
static fromModelGraph(graph) {
if (!graph) {
return new RelationExpression();
} else {
return new RelationExpression(modelGraphToNode(graph, newNode()));
}
}
get maxRecursionDepth() {
if (isNumber(this.node.$recursive)) {
return this.node.$recursive;
} else {
return this.node.$recursive ? Number.MAX_SAFE_INTEGER : 0;
}
}
get numChildren() {
return this.node.$childNames.length;
}
get isEmpty() {
return this.numChildren === 0;
}
// Merges this relation expression with another. `expr` can be a string,
// a pojo, or a RelationExpression instance.
merge(expr) {
expr = RelationExpression.create(expr);
if (this.isEmpty) {
// Nothing to merge.
return expr;
}
return new RelationExpression(mergeNodes(this.node, expr.node));
}
// Returns true if `expr` is contained by this expression. For example
// `a.b` is contained by `a.[b, c]`.
isSubExpression(expr) {
expr = RelationExpression.create(expr);
if (this.node.$allRecursive) {
return true;
}
if (expr.node.$allRecursive) {
return this.node.$allRecursive;
}
if (this.node.$relation !== expr.node.$relation) {
return false;
}
const maxRecursionDepth = expr.maxRecursionDepth;
if (maxRecursionDepth > 0) {
return this.node.$allRecursive || this.maxRecursionDepth >= maxRecursionDepth;
}
for (const childName of expr.node.$childNames) {
const ownSubExpression = this.childExpression(childName);
const subExpression = expr.childExpression(childName);
if (!ownSubExpression || !ownSubExpression.isSubExpression(subExpression)) {
return false;
}
}
return true;
}
// Returns a RelationExpression for a child node or null if there
// is no child with the given name `childName`.
childExpression(childName) {
if (
this.node.$allRecursive ||
(childName === this.node.$name && this.recursionDepth < this.maxRecursionDepth - 1)
) {
return new RelationExpression(this.node, this.recursionDepth + 1);
}
const child = this.node[childName];
if (child) {
return new RelationExpression(child);
} else {
return null;
}
}
// Loops throught all first level children.
forEachChildExpression(modelClass, cb) {
const maxRecursionDepth = this.maxRecursionDepth;
if (this.node.$allRecursive) {
for (const relationName of modelClass.getRelationNames()) {
const node = newNode(relationName, true);
const relation = modelClass.getRelationUnsafe(relationName);
const childExpr = new RelationExpression(node);
cb(childExpr, relation);
}
} else if (this.recursionDepth < maxRecursionDepth - 1) {
const relation = modelClass.getRelationUnsafe(this.node.$relation) || null;
const childExpr = new RelationExpression(this.node, this.recursionDepth + 1);
cb(childExpr, relation);
} else if (maxRecursionDepth === 0) {
for (const childName of this.node.$childNames) {
const node = this.node[childName];
const relation = modelClass.getRelationUnsafe(node.$relation);
if (!relation) {
throw new RelationDoesNotExistError(node.$relation);
}
const childExpr = new RelationExpression(node);
cb(childExpr, relation);
}
}
}
expressionsAtPath(path) {
return findExpressionsAtPath(this, RelationExpression.create(path), []);
}
clone() {
return new RelationExpression(cloneNode(this.node), this.recursionDepth);
}
toString() {
return toString(this.node);
}
toPojo() {
return cloneNode(this.node);
}
toJSON() {
return this.toPojo();
}
}
const parseCache = new Map();
function parse(str) {
const cachedNode = parseCache.get(str);
if (cachedNode) {
return cloneNode(cachedNode);
} else {
const node = parser.parse(str);
parseCache.set(str, cloneNode(node));
return node;
}
}
// All enumerable properties of a node that don't start with `$`
// are child nodes.
function getChildNames(node) {
if (!node) {
return [];
}
const childNames = [];
for (const key of Object.keys(node)) {
if (key[0] !== '$') {
childNames.push(key);
}
}
return childNames;
}
function toString(node) {
const childNames = node.$childNames;
let childExpr = childNames.map((childName) => node[childName]).map(toString);
let str = node.$relation;
if (node.$recursive) {
if (isNumber(node.$recursive)) {
str += '.^' + node.$recursive;
} else {
str += '.^';
}
} else if (node.$allRecursive) {
str += '.*';
}
if (childExpr.length > 1) {
childExpr = `[${childExpr.join(', ')}]`;
} else {
childExpr = childExpr[0];
}
if (node.$modify.length) {
str += `(${node.$modify.join(', ')})`;
}
if (node.$name !== node.$relation) {
str += ` as ${node.$name}`;
}
if (childExpr) {
if (str) {
return `${str}.${childExpr}`;
} else {
return childExpr;
}
} else {
return str;
}
}
function cloneNode(node) {
return normalizeNode(node);
}
function modelGraphToNode(models, node) {
if (!models) {
return;
}
if (Array.isArray(models)) {
for (let i = 0, l = models.length; i < l; ++i) {
modelToNode(models[i], node);
}
} else {
modelToNode(models, node);
}
return node;
}
function modelToNode(model, node) {
const modelClass = model.constructor;
const relationNames = modelClass.getRelationNames();
for (let r = 0, lr = relationNames.length; r < lr; ++r) {
const relName = relationNames[r];
if (model[relName] !== undefined) {
let childNode = node[relName];
if (!childNode) {
childNode = newNode(relName);
node[relName] = childNode;
node.$childNames.push(relName);
}
modelGraphToNode(model[relName], childNode);
}
}
}
function newNode(name = null, allRecusive = false) {
return normalizeNode(null, name, allRecusive);
}
function normalizeNode(node = null, name = null, allRecusive = false) {
const normalized = {
$name: normalizeName(node, name),
$relation: normalizeRelation(node, name),
$modify: normalizeModify(node),
$recursive: normalizeRecursive(node),
$allRecursive: normalizeAllRecursive(node, allRecusive),
$childNames: normalizeChildNames(node),
};
for (const childName of normalized.$childNames) {
const childNode = node[childName];
if (isObject(childNode) || childNode === true) {
normalized[childName] = normalizeNode(childNode, childName);
}
}
return normalized;
}
function normalizeName(node, name) {
return (node && node.$name) || name || null;
}
function normalizeRelation(node, name) {
return (node && node.$relation) || name || null;
}
function normalizeModify(node) {
if (!node || !node.$modify) {
return [];
}
return Array.isArray(node.$modify) ? node.$modify.slice() : [node.$modify];
}
function normalizeRecursive(node) {
return (node && node.$recursive) || false;
}
function normalizeAllRecursive(node, allRecusive) {
return (node && node.$allRecursive) || allRecusive || false;
}
function normalizeChildNames(node) {
return (node && node.$childNames && node.$childNames.slice()) || getChildNames(node);
}
function findExpressionsAtPath(target, path, results) {
if (path.node.$childNames.length == 0) {
// Path leaf reached, add target node to result set.
results.push(target);
} else {
for (const childName of path.node.$childNames) {
const pathChild = path.childExpression(childName);
const targetChild = target.childExpression(childName);
if (targetChild) {
findExpressionsAtPath(targetChild, pathChild, results);
}
}
}
return results;
}
function mergeNodes(node1, node2) {
const node = {
$name: node1.$name,
$relation: node1.$relation,
$modify: union(node1.$modify, node2.$modify),
$recursive: mergeRecursion(node1.$recursive, node2.$recursive),
$allRecursive: node1.$allRecursive || node2.$allRecursive,
$childNames: null,
};
if (!node.$recursive && !node.$allRecursive) {
node.$childNames = union(node1.$childNames, node2.$childNames);
for (const childName of node.$childNames) {
const child1 = node1[childName];
const child2 = node2[childName];
if (child1 && child2) {
node[childName] = mergeNodes(child1, child2);
} else {
node[childName] = child1 || child2;
}
}
} else {
node.$childNames = [];
}
return node;
}
function mergeRecursion(rec1, rec2) {
if (rec1 === true || rec2 === true) {
return true;
} else if (isNumber(rec1) && isNumber(rec2)) {
return Math.max(rec1, rec2);
} else {
return rec1 || rec2;
}
}
Object.defineProperties(RelationExpression.prototype, {
isObjectionRelationExpression: {
enumerable: false,
writable: false,
value: true,
},
});
module.exports = {
RelationExpression,
RelationExpressionParseError,
DuplicateRelationError,
};
================================================
FILE: lib/queryBuilder/StaticHookArguments.js
================================================
'use strict';
const { asArray } = require('../utils/objectUtils');
const BUILDER_SYMBOL = Symbol();
class StaticHookArguments {
constructor({ builder, result = null }) {
// The builder should never be accessed through the arguments.
// Hide it as well as possible to discourage people from
// digging it out.
Object.defineProperty(this, BUILDER_SYMBOL, {
value: builder,
});
Object.defineProperty(this, 'result', {
value: asArray(result),
});
}
static create(args) {
return new StaticHookArguments(args);
}
get asFindQuery() {
return () => {
return this[BUILDER_SYMBOL].toFindQuery().clearWithGraphFetched().runAfter(asArray);
};
}
get context() {
return this[BUILDER_SYMBOL].context();
}
get transaction() {
return this[BUILDER_SYMBOL].unsafeKnex();
}
get relation() {
const op = this[BUILDER_SYMBOL].findOperation(hasRelation);
if (op) {
return getRelation(op);
} else {
return undefined;
}
}
get modelOptions() {
const op = this[BUILDER_SYMBOL].findOperation(hasModelOptions);
if (op) {
return getModelOptions(op);
} else {
return undefined;
}
}
get items() {
const op = this[BUILDER_SYMBOL].findOperation(hasItems);
if (op) {
return asArray(getItems(op));
} else {
return [];
}
}
get inputItems() {
const op = this[BUILDER_SYMBOL].findOperation(hasInputItems);
if (op) {
return asArray(getInputItems(op));
} else {
return [];
}
}
get cancelQuery() {
const args = this;
return (cancelValue) => {
const builder = this[BUILDER_SYMBOL];
if (cancelValue === undefined) {
if (builder.isInsert()) {
cancelValue = args.inputItems;
} else if (builder.isFind()) {
cancelValue = [];
} else {
cancelValue = 0;
}
}
builder.resolve(cancelValue);
};
}
}
function getRelation(op) {
return op.relation;
}
function hasRelation(op) {
return !!getRelation(op);
}
function getModelOptions(op) {
return op.modelOptions;
}
function hasModelOptions(op) {
return !!getModelOptions(op);
}
function getItems(op) {
return op.instance || (op.owner && op.owner.isModels && op.owner.modelArray);
}
function hasItems(op) {
return !!getItems(op);
}
function getInputItems(op) {
return op.models || op.model;
}
function hasInputItems(op) {
return !!getInputItems(op);
}
module.exports = {
StaticHookArguments,
};
================================================
FILE: lib/queryBuilder/ValueBuilder.js
================================================
'use strict';
const { asArray, isObject } = require('../utils/objectUtils');
const { buildArg } = require('../utils/buildUtils');
class ValueBuilder {
constructor(value) {
this._value = value;
this._cast = null;
// Cast objects and arrays to json by default.
this._toJson = isObject(value);
this._toArray = false;
this._alias = null;
}
get cast() {
return this._cast;
}
castText() {
return this.castTo('text');
}
castInt() {
return this.castTo('integer');
}
castBigInt() {
return this.castTo('bigint');
}
castFloat() {
return this.castTo('float');
}
castDecimal() {
return this.castTo('decimal');
}
castReal() {
return this.castTo('real');
}
castBool() {
return this.castTo('boolean');
}
castJson() {
this._toArray = false;
this._toJson = true;
this._cast = 'jsonb';
return this;
}
castTo(sqlType) {
this._cast = sqlType;
return this;
}
asArray() {
this._toJson = false;
this._toArray = true;
return this;
}
as(alias) {
this._alias = alias;
return this;
}
toKnexRaw(builder) {
return builder.knex().raw(...this._createRawArgs(builder));
}
_createRawArgs(builder) {
let sql = null;
let bindings = [];
if (this._toJson) {
bindings.push(JSON.stringify(this._value));
sql = '?';
} else if (this._toArray) {
const values = asArray(this._value);
bindings.push(...values.map((it) => buildArg(it, builder)));
sql = `ARRAY[${values.map(() => '?').join(', ')}]`;
} else {
bindings.push(this._value);
sql = '?';
}
if (this._cast) {
sql = `CAST(${sql} AS ${this._cast})`;
}
if (this._alias) {
bindings.push(this._alias);
sql = `${sql} as ??`;
}
return [sql, bindings];
}
}
function val(val) {
return new ValueBuilder(val);
}
module.exports = {
ValueBuilder,
val,
};
================================================
FILE: lib/queryBuilder/graph/GraphAction.js
================================================
'use strict';
const { isPostgres } = require('../../utils/knexUtils');
const POSTGRES_MAX_INSERT_BATCH_SIZE = 100;
const MAX_CONCURRENCY = 100;
class GraphAction {
constructor(graphData) {
this.graphData = graphData;
}
static get ReturningAllSelector() {
return (op) => {
// Only select `returning('*')` operation.
return op.name === 'returning' && op.args.includes('*');
};
}
static getConcurrency(builder, nodes) {
return nodes.reduce((minConcurrency, node) => {
return Math.min(minConcurrency, node.modelClass.getConcurrency(builder.unsafeKnex()));
}, MAX_CONCURRENCY);
}
get graph() {
return this.graphData.graph;
}
get currentGraph() {
return this.graphData.currentGraph;
}
get graphOptions() {
return this.graphData.graphOptions;
}
run(builder) {
return null;
}
_getConcurrency(builder, nodes) {
return GraphAction.getConcurrency(builder, nodes);
}
_getBatchSize(builder) {
return isPostgres(builder.unsafeKnex()) ? POSTGRES_MAX_INSERT_BATCH_SIZE : 1;
}
_resolveReferences(node) {
if (node.isReference) {
this._resolveReference(node);
}
}
_resolveReference(node) {
const refNode = node.referencedNode;
for (const prop of Object.keys(refNode.obj)) {
if (!node.obj.hasOwnProperty(prop)) {
node.obj[prop] = refNode.obj[prop];
}
}
}
}
module.exports = {
GraphAction,
};
================================================
FILE: lib/queryBuilder/graph/GraphData.js
================================================
'use strict';
class GraphData {
constructor({ graph, currentGraph, graphOptions, nodeDbExistence }) {
this.graph = graph;
this.currentGraph = currentGraph;
this.graphOptions = graphOptions;
this.nodeDbExistence = nodeDbExistence;
}
}
module.exports = {
GraphData,
};
================================================
FILE: lib/queryBuilder/graph/GraphFetcher.js
================================================
'use strict';
const { asArray, groupBy } = require('../../utils/objectUtils');
const { ModelGraph } = require('../../model/graph/ModelGraph');
const { FetchStrategy } = require('./GraphOptions');
const { RelationExpression } = require('../RelationExpression');
class GraphFetcher {
/**
* Given a graph and options, fetches the current state of that graph
* from the database and returns it as a ModelGraph instance.
*/
static async fetchCurrentGraph({ builder, graph, graphOptions }) {
const { rootObjects } = graph;
const rootIds = getRootIds(rootObjects);
const modelClass = builder.modelClass();
if (rootIds.length === 0) {
return Promise.resolve(ModelGraph.create(modelClass, []));
}
const eagerExpr = RelationExpression.fromModelGraph(rootObjects);
const models = await modelClass
.query()
.childQueryOf(builder, childQueryOptions())
.modify(propagateMethodCallsFromQuery(builder))
.modify(buildFetchQuerySelects(graph, graphOptions, eagerExpr))
.findByIds(rootIds)
.withGraphFetched(eagerExpr)
.internalOptions(fetchQueryInternalOptions());
return ModelGraph.create(modelClass, models);
}
}
function getRootIds(rootObjects) {
return asArray(rootObjects)
.filter((it) => it.$hasId())
.map((root) => root.$id());
}
function propagateMethodCallsFromQuery(builder) {
return (fetchBuilder) => {
// Propagate some method calls from the root query.
for (const method of ['forUpdate', 'forShare', 'forNoKeyUpdate', 'forKeyShare']) {
if (builder.has(method)) {
fetchBuilder[method]();
}
}
};
}
function buildFetchQuerySelects(graph, graphOptions, eagerExpr) {
return (builder) => {
const nodesByRelationPath = groupNodesByRelationPath(graph, eagerExpr);
for (const [relationPath, nodes] of nodesByRelationPath.entries()) {
const selectModifier = createFetchSelectModifier(nodes, graphOptions);
if (!relationPath) {
builder.modify(selectModifier);
} else {
builder.modifyGraph(relationPath, selectModifier);
}
}
};
}
function groupNodesByRelationPath(graph, eagerExpr) {
const nodesByRelationPath = groupBy(graph.nodes, (node) => node.relationPathKey);
// Not all relation paths have nodes. Relations with nulls or empty arrays
// don't have nodes, but will still need to be fetched. Add these to the
// map as empty arrays.
forEachPath(eagerExpr.node, (relationPath) => {
if (!nodesByRelationPath.has(relationPath)) {
nodesByRelationPath.set(relationPath, []);
}
});
return nodesByRelationPath;
}
function createFetchSelectModifier(nodes, graphOptions) {
if (graphOptions.isFetchStrategy(FetchStrategy.OnlyIdentifiers)) {
return createIdentifierSelector();
} else if (graphOptions.isFetchStrategy(FetchStrategy.OnlyNeeded)) {
return createInputColumnSelector(nodes);
} else {
return () => {};
}
}
// Returns a function that only selects the id column.
function createIdentifierSelector() {
return (builder) => {
builder.select(builder.fullIdColumn());
};
}
// Returns a function that only selects the columns that exist in the input.
function createInputColumnSelector(nodes) {
return (builder) => {
const selects = new Map();
for (const node of nodes) {
const databaseJson = node.obj.$toDatabaseJson(builder);
for (const column of Object.keys(databaseJson)) {
if (!shouldSelectColumn(column, selects, node)) {
continue;
}
const selection =
createManyToManyExtraSelectionIfNeeded(builder, column, node) ||
createSelection(builder, column, node);
selects.set(column, selection);
}
}
const selectArr = Array.from(selects.values());
const idColumns = asArray(builder.fullIdColumn());
for (const idColumn of idColumns) {
if (!selectArr.includes(idColumn)) {
// Always select the identifers.
selectArr.push(idColumn);
}
}
builder.select(selectArr);
};
}
function shouldSelectColumn(column, selects, node) {
const modelClass = node.modelClass;
return (
!selects.has(column) &&
column !== modelClass.propertyNameToColumnName(modelClass.dbRefProp) &&
column !== modelClass.propertyNameToColumnName(modelClass.uidRefProp) &&
column !== modelClass.propertyNameToColumnName(modelClass.uidProp)
);
}
function createManyToManyExtraSelectionIfNeeded(builder, column, node) {
if (node.parentEdge && node.parentEdge.relation.isObjectionManyToManyRelation) {
const relation = node.parentEdge.relation;
const extra = relation.joinTableExtras.find((extra) => extra.aliasCol === column);
if (extra) {
return `${builder.tableRefFor(relation.joinModelClass)}.${extra.joinTableCol} as ${
extra.aliasCol
}`;
}
}
return null;
}
function createSelection(builder, column, node) {
return `${builder.tableRefFor(node.modelClass)}.${column}`;
}
function childQueryOptions() {
return {
fork: true,
isInternalQuery: true,
};
}
function fetchQueryInternalOptions() {
return {
keepImplicitJoinProps: true,
};
}
function forEachPath(eagerExprNode, cb, path = []) {
for (const relation of eagerExprNode.$childNames) {
path.push(relation);
cb(path.join('.'));
forEachPath(eagerExprNode[relation], cb, path);
path.pop();
}
}
module.exports = {
GraphFetcher,
};
================================================
FILE: lib/queryBuilder/graph/GraphNodeDbExistence.js
================================================
'use strict';
const { GraphData } = require('./GraphData');
const { GraphAction } = require('./GraphAction');
const promiseUtils = require('../../utils/promiseUtils');
/**
* This weird little class is responsible for checking (and maintaining information)
* whether certain nodes exist in the database.
*
* Note that this information is only calculated for nodes for which `insertMissing`
* option is true.
*/
class GraphNodeDbExistence {
static createEveryNodeExistsExistence() {
return new GraphNodeDbExistence(new Map());
}
/**
* Goes through the graphs and identifies nodes that may not exist in the db
* (nodes that have an id, but are not found in `currentGraph`) and
* creates an instance of GraphNodeDbExistence that can be used to
* synchronously check if a node exist in the database.
*/
static async create({ builder, graph, graphOptions, currentGraph }) {
if (graphOptions.isInsertOnly()) {
// With insertGraph, we never want to do anything but inserts.
return GraphNodeDbExistence.createEveryNodeExistsExistence();
}
const graphData = new GraphData({
graph,
graphOptions,
currentGraph,
// We don't yet have an instance of GraphNodeDbExistence since we are
// creating one. We can (and should) safely use an instance that
// assumes that all nodes exist in the db for the purposes of this
// method.
nodeDbExistence: GraphNodeDbExistence.createEveryNodeExistsExistence(),
});
const { mayNotExist, mayNotExistNodes } = createMayNotExistMap(graphData);
if (mayNotExist.size == 0) {
// Early exit if we found no items for which we should check their
// existence in the db.
return GraphNodeDbExistence.createEveryNodeExistsExistence();
}
const dontExist = await createDontExistMap({ builder, mayNotExist, mayNotExistNodes });
return new GraphNodeDbExistence(dontExist);
}
constructor(dontExist) {
this.dontExist = dontExist;
}
doesNodeExistInDb(node) {
const idMap = this.dontExist.get(node.modelClass);
if (!idMap) {
return true;
}
return !idMap.has(node.obj.$idKey());
}
}
function createMayNotExistMap(graphData) {
const { graph, currentGraph, graphOptions } = graphData;
const mayNotExist = new Map();
const mayNotExistNodes = [];
for (const node of graph.nodes) {
if (
// As an optimization, only consider nodes for which `insertMissing` is true.
// We only need the information for those nodes.
graphOptions.shouldInsertMissing(node) &&
// Only consider nodes that will be related. We don't consider nodes that
// would get inserted with an id. Those will still result in a unique
// constraint error.
graphOptions.shouldRelate(node, graphData) &&
// Relate nodes may not have an id if they are `#ref` nodes. Only consider
// nodes that have an id so that we can check the existence.
node.hasId &&
// We can ignore nodes if they are found anywhere in the graph. `shouldRelate`
// only checks if the node is found in the same relation.
!hasNodeById(currentGraph, node)
) {
if (!mayNotExist.has(node.modelClass)) {
mayNotExist.set(node.modelClass, new Map());
}
mayNotExist.get(node.modelClass).set(node.obj.$idKey(), node.obj.$id());
mayNotExistNodes.push(node);
}
}
return {
mayNotExist,
mayNotExistNodes,
};
}
function hasNodeById(currentGraph, nodeToFind) {
const { modelClass } = nodeToFind;
const tableToFind = modelClass.getTableName();
const idProps = modelClass.getIdPropertyArray();
return currentGraph.nodes.some((node) => {
return (
node.modelClass.getTableName() === tableToFind &&
idProps.every((idProp) => node.obj[idProp] === nodeToFind.obj[idProp])
);
});
}
async function createDontExistMap({ builder, mayNotExist, mayNotExistNodes }) {
const dontExist = cloneExistenceMap(mayNotExist);
const existenceCheckQueries = createExistenceCheckQueries({ builder, mayNotExist });
const results = await promiseUtils.map(existenceCheckQueries, (builder) => builder.execute(), {
concurrency: GraphAction.getConcurrency(builder, mayNotExistNodes),
});
// Remove all items from the mayNotExist map that we have just proven to exist
// by executing the existenceCheckQueries.
for (const modelResult of results) {
for (const item of modelResult) {
const modelClass = item.constructor;
const idMap = dontExist.get(modelClass);
// Exist, remove from the map.
idMap.delete(item.$idKey());
}
}
// Now only items that don't exist in the db are left in this map.
return dontExist;
}
function createExistenceCheckQueries({ builder, mayNotExist }) {
const builders = [];
for (const [modelClass, idMap] of mayNotExist.entries()) {
const ids = Array.from(idMap.values());
// Create one query per model class (table) to fetch the identifiers
// of the nodes that may not exist. These queries should be super fast
// since they come straight from the index.
builders.push(
modelClass
.query()
.childQueryOf(builder, childQueryOptions())
.findByIds(ids)
.select(modelClass.getIdColumnArray()),
);
}
return builders;
}
function childQueryOptions() {
return {
fork: true,
isInternalQuery: true,
};
}
function cloneExistenceMap(exixtenseMap) {
const clone = new Map(exixtenseMap);
for (const modelClass of clone.keys()) {
clone.set(modelClass, new Map(clone.get(modelClass)));
}
return clone;
}
module.exports = {
GraphNodeDbExistence,
};
================================================
FILE: lib/queryBuilder/graph/GraphOperation.js
================================================
'use strict';
class GraphOperation {
constructor(graphData) {
this.graphData = graphData;
}
get graph() {
return this.graphData.graph;
}
get currentGraph() {
return this.graphData.currentGraph;
}
get graphOptions() {
return this.graphData.graphOptions;
}
createActions() {
return [];
}
}
module.exports = {
GraphOperation,
};
================================================
FILE: lib/queryBuilder/graph/GraphOptions.js
================================================
'use strict';
const NO_RELATE = 'noRelate';
const NO_UNRELATE = 'noUnrelate';
const NO_INSERT = 'noInsert';
const NO_UPDATE = 'noUpdate';
const NO_DELETE = 'noDelete';
const UPDATE = 'update';
const RELATE = 'relate';
const UNRELATE = 'unrelate';
const INSERT_MISSING = 'insertMissing';
const FETCH_STRATEGY = 'fetchStrategy';
const ALLOW_REFS = 'allowRefs';
const FetchStrategy = {
OnlyIdentifiers: 'OnlyIdentifiers',
Everything: 'Everything',
OnlyNeeded: 'OnlyNeeded',
};
class GraphOptions {
constructor(options) {
if (options instanceof GraphOptions) {
this.options = options.options;
} else {
this.options = options;
}
}
isFetchStrategy(strategy) {
if (!FetchStrategy[strategy]) {
throw new Error(`unknown strategy "${strategy}"`);
}
if (!this.options[FETCH_STRATEGY]) {
return strategy === FetchStrategy.OnlyNeeded;
} else {
return this.options[FETCH_STRATEGY] === strategy;
}
}
isInsertOnly() {
// NO_RELATE is not in the list, since the `insert only` mode does
// relate things that can be related using inserts.
// TODO: Use a special key for this.
return [NO_DELETE, NO_UPDATE, NO_UNRELATE, INSERT_MISSING].every((opt) => {
return this.options[opt] === true;
});
}
// Like `shouldRelate` but ignores settings that explicitly disable relate operations.
shouldRelateIgnoreDisable(node, graphData) {
if (node.isReference || node.isDbReference) {
return true;
}
return (
this._hasOption(node, RELATE) &&
!getCurrentNode(node, graphData) &&
!!node.parentEdge &&
!!node.parentEdge.relation &&
node.parentEdge.relation.hasRelateProp(node.obj) &&
graphData.nodeDbExistence.doesNodeExistInDb(node)
);
}
shouldRelate(node, graphData) {
return !this._hasOption(node, NO_RELATE) && this.shouldRelateIgnoreDisable(node, graphData);
}
// Like `shouldInsert` but ignores settings that explicitly disable insert operations.
shouldInsertIgnoreDisable(node, graphData) {
return (
!getCurrentNode(node, graphData) &&
!this.shouldRelateIgnoreDisable(node, graphData) &&
(!node.hasId || this.shouldInsertMissing(node))
);
}
shouldInsert(node, graphData) {
return !this._hasOption(node, NO_INSERT) && this.shouldInsertIgnoreDisable(node, graphData);
}
shouldInsertMissing(node) {
return this._hasOption(node, INSERT_MISSING);
}
// Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
// update or patch operations.
shouldPatchOrUpdateIgnoreDisable(node, graphData) {
if (this.shouldRelate(node, graphData)) {
// We should update all nodes that are going to be related. Note that
// we don't actually update anything unless there is something to update
// so this is just a preliminary test.
return true;
}
return !!getCurrentNode(node, graphData);
}
shouldPatch(node, graphData) {
return (
this.shouldPatchOrUpdateIgnoreDisable(node, graphData) &&
!this._hasOption(node, NO_UPDATE) &&
!this._hasOption(node, UPDATE)
);
}
shouldUpdate(node, graphData) {
return (
this.shouldPatchOrUpdateIgnoreDisable(node, graphData) &&
!this._hasOption(node, NO_UPDATE) &&
this._hasOption(node, UPDATE)
);
}
// Like `shouldUnrelate` but ignores settings that explicitly disable unrelate operations.
shouldUnrelateIgnoreDisable(currentNode) {
return this._hasOption(currentNode, UNRELATE);
}
shouldUnrelate(currentNode, graphData) {
return (
!getNode(currentNode, graphData.graph) &&
!this._hasOption(currentNode, NO_UNRELATE) &&
this.shouldUnrelateIgnoreDisable(currentNode)
);
}
shouldDelete(currentNode, graphData) {
return (
!getNode(currentNode, graphData.graph) &&
!this._hasOption(currentNode, NO_DELETE) &&
!this.shouldUnrelateIgnoreDisable(currentNode)
);
}
shouldInsertOrRelate(node, graphData) {
return this.shouldInsert(node, graphData) || this.shouldRelate(node, graphData);
}
shouldDeleteOrUnrelate(currentNode, graphData) {
return this.shouldDelete(currentNode, graphData) || this.shouldUnrelate(currentNode, graphData);
}
allowRefs() {
return !!this.options[ALLOW_REFS];
}
rebasedOptions(newRoot) {
const newOpt = {};
const newRootRelationPath = newRoot.relationPathKey;
for (const name of Object.keys(this.options)) {
const value = this.options[name];
if (Array.isArray(value)) {
newOpt[name] = value
.filter((it) => it.startsWith(newRootRelationPath))
.map((it) => it.slice(newRootRelationPath.length + 1))
.filter((it) => !!it);
} else {
newOpt[name] = value;
}
}
return new GraphOptions(newOpt);
}
_hasOption(node, optionName) {
const option = this.options[optionName];
if (Array.isArray(option)) {
return option.indexOf(node.relationPathKey) !== -1;
} else if (typeof option === 'boolean') {
return option;
} else if (option === undefined) {
return false;
} else {
throw new Error(
`expected ${optionName} option value "${option}" to be an instance of boolean or array of strings`,
);
}
}
}
function getCurrentNode(node, graphData) {
if (!graphData || !node) {
return null;
}
return graphData.currentGraph.nodeForNode(node);
}
function getNode(currentNode, graph) {
if (!graph || !currentNode) {
return null;
}
return graph.nodeForNode(currentNode);
}
module.exports = {
GraphOptions,
FetchStrategy,
};
================================================
FILE: lib/queryBuilder/graph/GraphUpsert.js
================================================
'use strict';
const { ModelGraph } = require('../../model/graph/ModelGraph');
const { ModelGraphEdge } = require('../../model/graph/ModelGraphEdge');
const { createNotModelError } = require('../../model/graph/ModelGraphBuilder');
const { GraphFetcher } = require('../graph/GraphFetcher');
const { GraphInsert } = require('./insert/GraphInsert');
const { GraphPatch } = require('./patch/GraphPatch');
const { GraphDelete } = require('./delete/GraphDelete');
const { GraphRecursiveUpsert } = require('./recursiveUpsert/GraphRecursiveUpsert');
const { GraphOptions } = require('./GraphOptions');
const { ValidationErrorType } = require('../../model/ValidationError');
const { RelationExpression } = require('../RelationExpression');
const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
const { GraphData } = require('./GraphData');
const { uniqBy, asArray, isObject } = require('../../utils/objectUtils');
class GraphUpsert {
constructor({ rootModelClass, objects, upsertOptions }) {
checkCanBeConvertedToModels(rootModelClass, objects);
this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
this.isArray = Array.isArray(objects);
this.upsertOpt = upsertOptions;
}
static get modelOptions() {
return { skipValidation: true };
}
run(builder) {
const modelClass = builder.modelClass();
const graphOptions = new GraphOptions(this.upsertOpt);
const graph = ModelGraph.create(modelClass, this.objects);
assignDbRefsAsRelateProps(graph);
return createGraphData(builder, graphOptions, graph)
.then(checkForErrors(builder))
.then(pruneGraphs())
.then(executeOperations(builder))
.then(returnResult(this.objects, this.isArray));
}
}
function checkCanBeConvertedToModels(modelClass, objects) {
asArray(objects).forEach((obj) => {
if (!isObject(obj)) {
throw createNotModelError(modelClass, obj);
}
});
}
function assignDbRefsAsRelateProps(graph) {
for (const node of graph.nodes) {
if (!node.parentEdge || !node.parentEdge.relation || !node.isDbReference) {
continue;
}
node.parentEdge.relation.setRelateProp(node.obj, asArray(node.dbReference));
}
}
async function createGraphData(builder, graphOptions, graph) {
const currentGraph = await fetchCurrentGraph(builder, graphOptions, graph);
const nodeDbExistence = await GraphNodeDbExistence.create({
builder,
graph,
graphOptions,
currentGraph,
});
return new GraphData({ graph, currentGraph, graphOptions, nodeDbExistence });
}
function fetchCurrentGraph(builder, graphOptions, graph) {
if (graphOptions.isInsertOnly()) {
return Promise.resolve(ModelGraph.createEmpty());
} else {
return GraphFetcher.fetchCurrentGraph({ builder, graph, graphOptions });
}
}
// Remove branches from the graph that require no operations. For example
// we never want to do anything for descendant nodes of a node that is
// deleted or unrelated. We never delete recursively.
function pruneGraphs() {
return (graphData) => {
pruneRelatedBranches(graphData);
if (!graphData.graphOptions.isInsertOnly()) {
pruneDeletedBranches(graphData);
}
return graphData;
};
}
function pruneRelatedBranches(graphData) {
const relateNodes = graphData.graph.nodes.filter((node) => {
return (
!graphData.currentGraph.nodeForNode(node) &&
!graphData.graphOptions.shouldInsertIgnoreDisable(node, graphData)
);
});
removeBranchesFromGraph(findRoots(relateNodes), graphData.graph);
}
function pruneDeletedBranches(graphData) {
const { graph, currentGraph } = graphData;
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;
}
return parentNode.obj[relation.name] === undefined;
});
removeBranchesFromGraph(roots, currentGraph);
removeNodesFromGraph(new Set(rootsNotInRelation), currentGraph);
}
function findRoots(nodes) {
const nodeSet = new Set(nodes);
return uniqBy(
nodes.filter((node) => {
let parentNode = node.parentNode;
while (parentNode) {
if (nodeSet.has(parentNode)) {
return false;
}
parentNode = parentNode.parentNode;
}
return true;
}),
);
}
function removeBranchesFromGraph(branchRoots, graph) {
const nodesToRemove = new Set(
branchRoots.reduce(
(nodesToRemove, node) => [...nodesToRemove, ...node.descendantRelationNodes],
[],
),
);
removeNodesFromGraph(nodesToRemove, graph);
}
function removeNodesFromGraph(nodesToRemove, graph) {
const edgesToRemove = new Set();
for (const node of nodesToRemove) {
for (const edge of node.edges) {
const otherNode = edge.getOtherNode(node);
if (!nodesToRemove.has(otherNode)) {
otherNode.removeEdge(edge);
edgesToRemove.add(edge);
}
}
}
graph.nodes = graph.nodes.filter((node) => !nodesToRemove.has(node));
graph.edges = graph.edges.filter((edge) => !edgesToRemove.has(edge));
return graph;
}
function checkForErrors(builder) {
return (graphData) => {
checkForNotFoundErrors(graphData, builder);
checkForUnallowedRelationErrors(graphData, builder);
checkForUnallowedReferenceErrors(graphData, builder);
if (graphData.graphOptions.isInsertOnly()) {
checkForHasManyRelateErrors(graphData);
}
return graphData;
};
}
function checkForNotFoundErrors(graphData, builder) {
const { graphOptions, currentGraph, graph } = graphData;
for (const node of graph.nodes) {
if (
node.obj.$hasId() &&
!graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
!graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
!currentGraph.nodeForNode(node)
) {
if (!node.parentNode) {
throw node.modelClass.createNotFoundError(builder.context(), {
message: `root model (id=${node.obj.$id()}) does not exist. If you want to insert it with an id, use the insertMissing option`,
dataPath: node.dataPath,
});
} else {
throw node.modelClass.createNotFoundError(builder.context(), {
message: `model (id=${node.obj.$id()}) is not a child of model (id=${node.parentNode.obj.$id()}). If you want to relate it, use the relate option. If you want to insert it with an id, use the insertMissing option`,
dataPath: node.dataPath,
});
}
}
}
}
function checkForUnallowedRelationErrors(graphData, builder) {
const { graph } = graphData;
const allowedExpression = builder.allowedGraphExpression();
if (allowedExpression) {
const rootsObjs = graph.nodes.filter((node) => !node.parentEdge).map((node) => node.obj);
const expression = RelationExpression.fromModelGraph(rootsObjs);
if (!allowedExpression.isSubExpression(expression)) {
throw builder.modelClass().createValidationError({
type: ValidationErrorType.UnallowedRelation,
message: 'trying to upsert an unallowed relation',
});
}
}
}
function checkForUnallowedReferenceErrors(graphData, builder) {
const { graph, graphOptions } = graphData;
if (graphOptions.allowRefs()) {
return;
}
if (graph.edges.some((edge) => edge.type === ModelGraphEdge.Type.Reference)) {
throw builder.modelClass().createValidationError({
type: ValidationErrorType.InvalidGraph,
message:
'#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option',
});
}
}
function checkForHasManyRelateErrors(graphData) {
const { graph, graphOptions } = graphData;
for (const node of graph.nodes) {
if (
graphOptions.shouldRelate(node, graphData) &&
node.parentEdge.relation.isObjectionHasManyRelation
) {
throw new Error(
'You cannot relate HasManyRelation or HasOneRelation using insertGraph, because those require update operations. Consider using upsertGraph instead.',
);
}
}
}
function executeOperations(builder) {
return (graphData) => {
const operations = graphData.graphOptions.isInsertOnly()
? [GraphInsert]
: [GraphDelete, GraphInsert, GraphPatch, GraphRecursiveUpsert];
return operations.reduce((promise, Operation) => {
const operation = new Operation(graphData);
const actions = operation.createActions();
return promise.then(() => executeActions(builder, actions));
}, Promise.resolve());
};
}
function executeActions(builder, actions) {
return actions.reduce(
(promise, action) => promise.then(() => action.run(builder)),
Promise.resolve(),
);
}
function returnResult(objects, isArray) {
return () => (isArray ? objects : objects[0]);
}
module.exports = {
GraphUpsert,
};
================================================
FILE: lib/queryBuilder/graph/delete/GraphDelete.js
================================================
'use strict';
const { GraphOperation } = require('../GraphOperation');
const { GraphDeleteAction } = require('./GraphDeleteAction');
class GraphDelete extends GraphOperation {
createActions() {
return [
new GraphDeleteAction(this.graphData, {
nodes: this.currentGraph.nodes.filter((currentNode) =>
this.graphOptions.shouldDeleteOrUnrelate(currentNode, this.graphData),
),
}),
];
}
}
module.exports = {
GraphDelete,
};
================================================
FILE: lib/queryBuilder/graph/delete/GraphDeleteAction.js
================================================
'use strict';
const { GraphAction } = require('../GraphAction');
const { groupBy } = require('../../../utils/objectUtils');
const promiseUtils = require('../../../utils/promiseUtils');
class GraphDeleteAction extends GraphAction {
constructor(graphData, { nodes }) {
super(graphData);
// Nodes to delete.
this.nodes = nodes;
}
run(builder) {
const nodesTodelete = this._filterOutBelongsToOneRelationUnrelates(this.nodes);
const builders = this._createDeleteBuilders(builder, nodesTodelete);
return promiseUtils.map(builders, (builder) => builder.execute(), {
concurrency: this._getConcurrency(builder, nodesTodelete),
});
}
_filterOutBelongsToOneRelationUnrelates(nodes) {
// `BelongsToOneRelation` unrelate is handled by `GraphPatch` because
// unrelating a `BelongsToOneRelation` is just a matter of updating
// one field of the parent node.
return nodes.filter((node) => {
return !(
this.graphOptions.shouldUnrelate(node, this.graphData) &&
node.parentEdge.relation.isObjectionBelongsToOneRelation
);
});
}
_createDeleteBuilders(parentBuilder, nodesTodelete) {
const nodesByRelation = groupBy(nodesTodelete, getRelation);
const builders = [];
nodesByRelation.forEach((nodes, relation) => {
const nodesByParent = groupBy(nodes, getParent);
nodesByParent.forEach((nodes, parentNode) => {
const shouldUnrelate = this.graphOptions.shouldUnrelate(nodes[0], this.graphData);
const builder = parentNode.obj.$relatedQuery(relation.name).childQueryOf(parentBuilder);
if (!relation.isObjectionBelongsToOneRelation) {
// This is useless in case of BelongsToOneRelation.
builder.findByIds(nodes.map((node) => node.obj.$id()));
}
for (const node of nodes) {
node.userData.deleted = true;
}
builders.push(shouldUnrelate ? builder.unrelate() : builder.delete());
});
});
return builders;
}
}
function getRelation(node) {
return node.parentEdge.relation;
}
function getParent(node) {
return node.parentNode;
}
module.exports = {
GraphDeleteAction,
};
================================================
FILE: lib/queryBuilder/graph/insert/GraphInsert.js
================================================
'use strict';
const { JoinRowGraphInsertAction } = require('./JoinRowGraphInsertAction');
const { GraphInsertAction } = require('./GraphInsertAction');
const { GraphOperation } = require('../GraphOperation');
const { ModelGraphEdge } = require('../../../model/graph/ModelGraphEdge');
class GraphInsert extends GraphOperation {
constructor(...args) {
super(...args);
this.dependencies = this._createDependencyMap();
}
createActions() {
return [...this._createNormalActions(), ...this._createJoinRowActions()];
}
_createDependencyMap() {
const dependencies = new Map();
for (const edge of this.graph.edges) {
if (edge.type == ModelGraphEdge.Type.Relation) {
this._createRelationDependency(edge, dependencies);
} else {
this._createReferenceDependency(edge, dependencies);
}
}
return dependencies;
}
_createRelationDependency(edge, dependencies) {
if (edge.relation.isObjectionHasManyRelation) {
// In case of HasManyRelation the related node depends on the owner node
// because the related node has the foreign key.
this._addDependency(edge.relatedNode, edge, dependencies);
} else if (edge.relation.isObjectionBelongsToOneRelation) {
// In case of BelongsToOneRelation the owner node depends on the related
// node because the owner node has the foreign key.
this._addDependency(edge.ownerNode, edge, dependencies);
}
}
_createReferenceDependency(edge, dependencies) {
this._addDependency(edge.ownerNode, edge, dependencies);
}
_addDependency(node, edge, dependencies) {
let edges = dependencies.get(node);
if (!edges) {
edges = [];
dependencies.set(node, edges);
}
edges.push(edge);
}
_createNormalActions() {
const handledNodes = new Set();
const actions = [];
while (true) {
// At this point, don't care if the nodes have already been inserted before
// given to this class. `GraphInsertAction` will test that and only insert
// new ones. We need to pass all nodes to `GraphInsertActions` so that we
// can resolve all dependencies.
const nodesToInsert = this.graph.nodes.filter((node) => {
return !this._isHandled(node, handledNodes) && !this._hasDependencies(node, handledNodes);
});
if (nodesToInsert.length === 0) {
break;
}
actions.push(
new GraphInsertAction(this.graphData, {
nodes: nodesToInsert,
dependencies: this.dependencies,
}),
);
for (const node of nodesToInsert) {
this._markHandled(node, handledNodes);
}
}
if (handledNodes.size !== this.graph.nodes.length) {
throw new Error('the object graph contains cyclic references');
}
return actions;
}
_isHandled(node, handledNodes) {
return handledNodes.has(node);
}
_hasDependencies(node, handledNodes) {
if (!this.dependencies.has(node)) {
return false;
}
for (const edge of this.dependencies.get(node)) {
const dependencyNode = edge.getOtherNode(node);
if (!handledNodes.has(dependencyNode) && !this.currentGraph.nodeForNode(dependencyNode)) {
return true;
}
}
return false;
}
_markHandled(node, handledNodes) {
handledNodes.add(node);
// The referencing nodes are all references that don't
// represent any real entity. They are simply intermediate nodes
// that depend on this node. Once this node is handled, we can
// also mark those nodes as handled as there is nothing to actually
// insert.
for (const refNode of node.referencingNodes) {
this._markHandled(refNode, handledNodes);
}
}
_createJoinRowActions() {
return [
new JoinRowGraphInsertAction(this.graphData, {
nodes: this.graph.nodes.filter((node) => {
return (
this.currentGraph.nodeForNode(node) === null &&
node.parentEdge &&
node.parentEdge.relation.isObjectionManyToManyRelation
);
}),
}),
];
}
}
module.exports = {
GraphInsert,
};
================================================
FILE: lib/queryBuilder/graph/insert/GraphInsertAction.js
================================================
'use strict';
const { GraphAction } = require('../GraphAction');
const { groupBy, chunk, get, set } = require('../../../utils/objectUtils');
const { ModelGraphEdge } = require('../../../model/graph/ModelGraphEdge');
const promiseUtils = require('../../../utils/promiseUtils');
/**
* Inserts a batch of nodes for a GraphInsert.
*
* One of these is created for each batch of nodes that can be inserted at once.
* However, the nodes can have a different table and not all databases support
* batch inserts, so this class splits the inserts into further sub batches
* when needed.
*/
class GraphInsertAction extends GraphAction {
constructor(graphData, { nodes, dependencies }) {
super(graphData);
// Nodes to insert.
this.nodes = nodes;
this.dependencies = dependencies;
}
run(builder) {
const batches = this._createInsertBatches(builder);
const concurrency = this._getConcurrency(builder, this.nodes);
return promiseUtils.map(batches, (batch) => this._insertBatch(builder, batch), { concurrency });
}
_createInsertBatches(builder) {
const batches = [];
const batchSize = this._getBatchSize(builder);
const nodesByModelClass = groupBy(this.nodes, getModelClass);
for (const nodes of nodesByModelClass.values()) {
for (const nodeBatch of chunk(nodes, batchSize)) {
batches.push(nodeBatch);
}
}
return batches;
}
async _insertBatch(parentBuilder, nodes) {
await this._beforeInsert(nodes);
await this._insert(parentBuilder, nodes);
await this._afterInsert(nodes);
}
_beforeInsert(nodes) {
this._resolveDependencies(nodes);
this._omitManyToManyExtraProps(nodes);
this._copyRelationPropsFromCurrentIfNeeded(nodes);
return Promise.resolve();
}
_resolveDependencies(nodes) {
for (const node of nodes) {
const edges = this.dependencies.get(node);
if (edges) {
for (const edge of edges) {
// `node` needs `dependencyNode` to have been inserted (and it has been).
const dependencyNode = edge.getOtherNode(node);
this._resolveDependency(dependencyNode, edge);
}
}
}
}
_resolveDependency(dependencyNode, edge) {
if (edge.type === ModelGraphEdge.Type.Relation && !edge.relation.joinTable) {
this._resolveRelationDependency(dependencyNode, edge);
} else if (edge.refType === ModelGraphEdge.ReferenceType.Property) {
this._resolvePropertyReferenceNode(dependencyNode, edge);
}
}
_resolveRelationDependency(dependencyNode, edge) {
const dependentNode = edge.getOtherNode(dependencyNode);
let sourceProp;
let targetProp;
if (edge.isOwnerNode(dependencyNode)) {
sourceProp = edge.relation.ownerProp;
targetProp = edge.relation.relatedProp;
} else {
targetProp = edge.relation.ownerProp;
sourceProp = edge.relation.relatedProp;
}
this._resolveReferences(dependencyNode);
targetProp.forEach((i) => {
targetProp.setProp(dependentNode.obj, i, sourceProp.getProp(dependencyNode.obj, i));
});
}
_resolvePropertyReferenceNode(dependencyNode, edge) {
const dependentNode = edge.getOtherNode(dependencyNode);
let sourcePath;
let targetPath;
if (edge.isOwnerNode(dependencyNode)) {
sourcePath = edge.refOwnerDataPath;
targetPath = edge.refRelatedDataPath;
} else {
targetPath = edge.refOwnerDataPath;
sourcePath = edge.refRelatedDataPath;
}
const sourceValue = get(dependencyNode.obj, sourcePath);
const targetValue = get(dependentNode.obj, targetPath);
if (targetValue === edge.refMatch) {
set(dependentNode.obj, targetPath, sourceValue);
} else {
set(dependentNode.obj, targetPath, targetValue.replace(edge.refMatch, sourceValue));
}
}
_omitManyToManyExtraProps(nodes) {
for (const node of nodes) {
if (
node.parentEdge &&
node.parentEdge.type === ModelGraphEdge.Type.Relation &&
node.parentEdge.relation.joinTableExtras.length > 0
) {
node.parentEdge.relation.omitExtraProps([node.obj]);
}
}
}
_copyRelationPropsFromCurrentIfNeeded(nodes) {
for (const node of nodes) {
const currentNode = this.currentGraph.nodeForNode(node);
if (!currentNode) {
continue;
}
for (const edge of node.edges) {
if (edge.type !== ModelGraphEdge.Type.Relation) {
continue;
}
const prop = edge.isOwnerNode(node) ? edge.relation.ownerProp : edge.relation.relatedProp;
prop.forEach((i) => {
const value = prop.getProp(node.obj, i);
if (value !== undefined) {
return;
}
prop.setProp(node.obj, i, prop.getProp(currentNode.obj, i));
});
}
}
}
_insert(parentBuilder, nodes) {
const [{ modelClass }] = nodes;
nodes = nodes.filter((node) => {
return this.graphOptions.shouldInsert(node, this.graphData);
});
for (const node of nodes) {
delete node.obj[modelClass.uidProp];
node.obj.$validate(null, { dataPath: node.dataPathKey });
}
if (nodes.length === 0) {
return;
}
for (const node of nodes) {
node.userData.inserted = true;
}
return this._runRelationBeforeInsertMethods(parentBuilder, nodes).then(() => {
return modelClass
.query()
.insert(nodes.map((node) => node.obj))
.childQueryOf(parentBuilder)
.copyFrom(parentBuilder, GraphAction.ReturningAllSelector)
.execute();
});
}
_runRelationBeforeInsertMethods(parentBuilder, nodes) {
return Promise.all(
nodes.map((node) => {
if (node.parentEdge) {
return node.parentEdge.relation.beforeInsert(node.obj, parentBuilder.context());
} else {
return null;
}
}),
);
}
_afterInsert(nodes) {
for (const node of nodes) {
for (const refNode of node.referencingNodes) {
this._resolveDependency(refNode, refNode.parentEdge);
}
}
}
}
function getModelClass(node) {
return node.modelClass;
}
module.exports = {
GraphInsertAction,
};
================================================
FILE: lib/queryBuilder/graph/insert/JoinRowGraphInsertAction.js
================================================
'use strict';
const { GraphAction } = require('../GraphAction');
const { groupBy, chunk } = require('../../../utils/objectUtils');
const promiseUtils = require('../../../utils/promiseUtils');
class JoinRowGraphInsertAction extends GraphAction {
constructor(graphData, { nodes }) {
super(graphData);
this.nodes = nodes;
}
run(builder) {
const batches = this._createInsertBatches(builder);
const concurrency = this._getConcurrency(builder, this.nodes);
return promiseUtils.map(batches, (batch) => this._insertBatch(builder, batch), { concurrency });
}
_createInsertBatches(builder) {
const batches = [];
const batchSize = this._getBatchSize(builder);
const nodesByModel = groupBy(this.nodes, (node) => getJoinTableModel(builder, node));
for (const [joinTableModelClass, nodes] of nodesByModel.entries()) {
for (const nodeBatch of chunk(nodes, batchSize)) {
batches.push(this._createBatch(joinTableModelClass, nodeBatch));
}
}
return batches;
}
_createBatch(joinTableModelClass, nodes) {
return nodes
.filter((node) => {
return this.graphOptions.shouldRelate(node, this.graphData) || node.userData.inserted;
})
.map((node) => ({
node,
joinTableModelClass,
joinTableObj: this._createJoinTableObj(joinTableModelClass, node),
}));
}
_createJoinTableObj(joinTableModelClass, node) {
const { parentEdge, parentNode } = node;
const { relation } = parentEdge;
this._resolveReferences(node);
return joinTableModelClass.fromJson(
relation.createJoinModel(relation.ownerProp.getProps(parentNode.obj), node.obj),
);
}
_insertBatch(parentBuilder, batch) {
return this._beforeInsert(parentBuilder, batch).then(() => this._insert(parentBuilder, batch));
}
_beforeInsert(parentBuilder, batch) {
return Promise.all(
batch.map(({ node, joinTableObj }) => {
if (node.parentEdge) {
return node.parentEdge.relation.joinTableBeforeInsert(
joinTableObj,
parentBuilder.context(),
);
} else {
return null;
}
}),
);
}
_insert(parentBuilder, batch) {
if (batch.length > 0) {
return batch[0].joinTableModelClass
.query()
.childQueryOf(parentBuilder)
.insert(batch.map((it) => it.joinTableObj));
}
}
}
function getJoinTableModel(builder, node) {
return node.parentEdge.relation.getJoinModelClass(builder.unsafeKnex());
}
module.exports = {
JoinRowGraphInsertAction,
};
================================================
FILE: lib/queryBuilder/graph/patch/GraphPatch.js
================================================
'use strict';
const { GraphOperation } = require('../GraphOperation');
const { GraphPatchAction } = require('./GraphPatchAction');
class GraphPatch extends GraphOperation {
createActions() {
return [
new GraphPatchAction(this.graphData, {
nodes: this.graph.nodes.filter((node) =>
this.graphOptions.shouldPatchOrUpdateIgnoreDisable(node, this.graphData),
),
}),
];
}
}
module.exports = {
GraphPatch,
};
================================================
FILE: lib/queryBuilder/graph/patch/GraphPatchAction.js
================================================
'use strict';
const { getModel } = require('../../../model/getModel');
const { GraphAction } = require('../GraphAction');
const { isInternalProp } = require('../../../utils/internalPropUtils');
const { difference, isObject, jsonEquals } = require('../../../utils/objectUtils');
const promiseUtils = require('../../../utils/promiseUtils');
class GraphPatchAction extends GraphAction {
constructor(graphData, { nodes }) {
super(graphData);
// Nodes to patch.
this.nodes = nodes;
}
run(builder) {
return promiseUtils.map(this.nodes, (node) => this._runForNode(builder, node), {
concurrency: this._getConcurrency(builder, this.nodes),
});
}
_runForNode(builder, node) {
const shouldPatch = this.graphOptions.shouldPatch(node, this.graphData);
const shouldUpdate = this.graphOptions.shouldUpdate(node, this.graphData);
// BelongsToOneRelation inserts and relates change the parent object's
// properties. That's why we handle them here.
const changedPropsBecauseOfBelongsToOneInsert = this._handleBelongsToOneInserts(node);
// BelongsToOneRelation deletes and unrelates change the parent object's
// properties. That's why we handle them here.
const changePropsBecauseOfBelongsToOneDelete = this._handleBelongsToOneDeletes(node);
const handleUpdate = () => {
const { changedProps, unchangedProps } = this._findChanges(node);
const allProps = [...changedProps, ...unchangedProps];
const propsToUpdate = difference(
shouldPatch || shouldUpdate
? changedProps
: [...changedPropsBecauseOfBelongsToOneInsert, ...changePropsBecauseOfBelongsToOneDelete],
// Remove id properties from the props to update. With upsertGraph
// it never makes sense to change the id.
node.modelClass.getIdPropertyArray(),
);
const update = propsToUpdate.length > 0;
if (update) {
// Don't update the fields that we know not to change.
node.obj.$omitFromDatabaseJson(difference(allProps, propsToUpdate));
node.userData.updated = true;
}
return update;
};
const Model = getModel();
// See if the model defines a beforeUpdate or $beforeUpdate hook. If it does
// not, we can check for updated properties now and drop out immediately if
// there is nothing to update. Otherwise, we need to wait for the hook to be
// called before calling handleUpdate, but only if the node contains changes
// that aren't id properties (relates). See issues #2233, #2605.
const hasBeforeUpdate =
node.obj.constructor.beforeUpdate !== Model.beforeUpdate ||
node.obj.$beforeUpdate !== Model.prototype.$beforeUpdate;
if (
(hasBeforeUpdate && !this._hasNonIdPropertyChanges(node)) ||
(!hasBeforeUpdate && !handleUpdate())
) {
return null;
}
delete node.obj[node.modelClass.uidProp];
delete node.obj[node.modelClass.uidRefProp];
delete node.obj[node.modelClass.dbRefProp];
node.obj.$validate(null, {
dataPath: node.dataPathKey,
patch: shouldPatch || (!shouldPatch && !shouldUpdate),
});
const updateBuilder = this._createBuilder(node)
.childQueryOf(builder, childQueryOptions())
.copyFrom(builder, GraphAction.ReturningAllSelector);
if (hasBeforeUpdate) {
updateBuilder.internalContext().runBefore.push((result, builder) => {
// Call handleUpdate in the runBefore hook which runs after the
// $beforeUpdate hook, allowing it to modify the object before the
// updated properties are determined. See issue #2233.
if (hasBeforeUpdate && !handleUpdate()) {
builder.internalOptions().returnImmediatelyValue = null;
}
return result;
});
}
if (shouldPatch) {
updateBuilder.patch(node.obj);
} else {
updateBuilder.update(node.obj);
}
return updateBuilder.execute().then((result) => {
if (isObject(result) && result.$isObjectionModel) {
// Handle returning('*').
node.obj.$set(result);
}
return result;
});
}
_handleBelongsToOneInserts(node) {
const currentNode = this.currentGraph.nodeForNode(node);
const updatedProps = [];
for (const edge of node.edges) {
if (
edge.isOwnerNode(node) &&
edge.relation &&
edge.relation.isObjectionBelongsToOneRelation &&
edge.relation.relatedProp.hasProps(edge.relatedNode.obj)
) {
const { relation } = edge;
relation.ownerProp.forEach((i) => {
const currentValue = currentNode && relation.ownerProp.getProp(currentNode.obj, i);
const relatedValue = relation.relatedProp.getProp(edge.relatedNode.obj, i);
if (currentValue != relatedValue) {
relation.ownerProp.setProp(node.obj, i, relatedValue);
updatedProps.push(relation.ownerProp.props[i]);
}
});
}
}
return updatedProps;
}
_handleBelongsToOneDeletes(node) {
const currentNode = this.currentGraph.nodeForNode(node);
const updatedProps = [];
if (!currentNode) {
return updatedProps;
}
for (const edge of currentNode.edges) {
if (
edge.isOwnerNode(currentNode) &&
edge.relation.isObjectionBelongsToOneRelation &&
node.obj[edge.relation.name] === null &&
this.graphOptions.shouldDeleteOrUnrelate(edge.relatedNode, this.graphData)
) {
const { relation } = edge;
relation.ownerProp.forEach((i) => {
const currentValue = relation.ownerProp.getProp(currentNode.obj, i);
if (currentValue != null) {
relation.ownerProp.setProp(node.obj, i, null);
updatedProps.push(relation.ownerProp.props[i]);
}
});
}
}
return updatedProps;
}
_findChanges(node) {
const obj = node.obj;
const currentNode = this.currentGraph.nodeForNode(node);
const currentObj = (currentNode && currentNode.obj) || {};
const relationNames = node.modelClass.getRelationNames();
const unchangedProps = [];
const changedProps = [];
for (const prop of Object.keys(obj)) {
if (isInternalProp(prop) || relationNames.includes(prop)) {
continue;
}
const value = obj[prop];
const currentValue = currentObj[prop];
// If the current object doesn't have the property, we have to assume
// it changes (we cannot know if it will). If the object does have the
// property, we test non-strict equality. See issue #732.
if (currentValue === undefined || !nonStrictEquals(currentValue, value)) {
changedProps.push(prop);
} else {
unchangedProps.push(prop);
}
}
// We cannot know if the query properties cause changes to the values.
// We must assume that they do.
if (obj.$$queryProps) {
changedProps.push(...Object.keys(obj.$$queryProps));
}
return {
changedProps,
unchangedProps,
};
}
_hasNonIdPropertyChanges(node) {
const idProps = node.modelClass.getIdPropertyArray();
return this._findChanges(node).changedProps.some((prop) => !idProps.includes(prop));
}
_createBuilder(node) {
if (node.parentEdge && !node.parentEdge.relation.isObjectionHasManyRelation) {
return this._createRelatedBuilder(node);
} else {
return this._createRootBuilder(node);
}
}
_createRelatedBuilder(node) {
return node.parentNode.obj
.$relatedQuery(node.parentEdge.relation.name)
.findById(node.obj.$id());
}
_createRootBuilder(node) {
const currentNode = this.currentGraph.nodeForNode(node);
const currentObj = currentNode && currentNode.obj;
return currentObj ? currentObj.$query() : node.obj.$query();
}
}
function childQueryOptions() {
return {
fork: true,
isInternalQuery: true,
};
}
function nonStrictEquals(val1, val2) {
if (val1 == val2) {
return true;
}
return jsonEquals(val1, val2);
}
module.exports = {
GraphPatchAction,
};
================================================
FILE: lib/queryBuilder/graph/recursiveUpsert/GraphRecursiveUpsert.js
================================================
'use strict';
const { GraphOperation } = require('../GraphOperation');
const { GraphRecursiveUpsertAction } = require('./GraphRecursiveUpsertAction');
class GraphRecursiveUpsert extends GraphOperation {
createActions() {
return [
new GraphRecursiveUpsertAction(this.graphData, {
nodes: this.graph.nodes.filter((node) => {
const shouldRelate = this.graphOptions.shouldRelate(node, this.graphData);
return shouldRelate && hasRelations(node.obj);
}),
}),
];
}
}
function hasRelations(obj) {
for (const relationName of obj.constructor.getRelationNames()) {
if (obj[relationName] !== undefined) {
return true;
}
}
return false;
}
module.exports = {
GraphRecursiveUpsert,
};
================================================
FILE: lib/queryBuilder/graph/recursiveUpsert/GraphRecursiveUpsertAction.js
================================================
'use strict';
const { GraphAction } = require('../GraphAction');
const { groupBy, get, set } = require('../../../utils/objectUtils');
const { forEachPropertyReference } = require('../../../model/graph/ModelGraphBuilder');
const promiseUtils = require('../../../utils/promiseUtils');
class GraphRecursiveUpsertAction extends GraphAction {
constructor(graphData, { nodes }) {
super(graphData);
// Nodes to upsert.
this.nodes = nodes;
}
run(builder) {
const builders = this._createUpsertBuilders(builder, this.nodes);
return promiseUtils.map(builders, (builder) => builder.execute(), {
concurrency: this._getConcurrency(builder, this.nodes),
});
}
_createUpsertBuilders(parentBuilder, nodesToUpsert) {
const nodesByRelation = groupBy(nodesToUpsert, getRelation);
const builders = [];
nodesByRelation.forEach((nodes) => {
const nodesByParent = groupBy(nodes, getParent);
nodesByParent.forEach((nodes) => {
for (const node of nodes) {
this._resolveReferences(node);
node.userData.upserted = true;
}
builders.push(
nodes[0].modelClass
.query()
.childQueryOf(parentBuilder)
.copyFrom(parentBuilder, GraphAction.ReturningAllSelector)
.upsertGraph(
nodes.map((node) => node.obj),
this.graphOptions.rebasedOptions(nodes[0]),
),
);
});
});
return builders;
}
/**
* The nodes inside the subgraph we are about to recursively upsert may
* have references outside that graph that won't be available during the
* recursive upsertGraph call. This method resolves the references.
*
* TODO: This doesn't work if a recursively upserted node refers to
* a node inside another recursively upsertable graph.
*/
_resolveReferences(node) {
node.obj.$traverse((obj) => this._resolveReferencesForObject(obj));
}
_resolveReferencesForObject(obj) {
this._resolveObjectReference(obj);
this._resolvePropertyReferences(obj);
}
_resolveObjectReference(obj) {
const modelClass = obj.constructor;
const ref = obj[modelClass.uidRefProp];
if (!ref) {
return;
}
const referencedNode = this.graph.nodes.find((it) => it.uid === ref);
if (!referencedNode) {
return;
}
const relationNames = referencedNode.modelClass.getRelationNames();
for (const prop of Object.keys(referencedNode.obj)) {
if (relationNames.includes(prop)) {
continue;
}
obj[prop] = referencedNode.obj[prop];
}
delete obj[modelClass.uidRefProp];
}
_resolvePropertyReferences(obj) {
forEachPropertyReference(obj, ({ path, refMatch, ref, refPath }) => {
const referencedNode = this.graph.nodes.find((it) => it.uid === ref);
if (!referencedNode) {
return;
}
const referencedValue = get(referencedNode.obj, refPath);
const value = get(obj, path);
if (value === refMatch) {
set(obj, path, referencedValue);
} else {
set(obj, path, value.replace(refMatch, referencedValue));
}
});
}
}
function getRelation(node) {
return node.parentEdge.relation;
}
function getParent(node) {
return node.parentNode;
}
module.exports = {
GraphRecursiveUpsertAction,
};
================================================
FILE: lib/queryBuilder/join/JoinResultColumn.js
================================================
'use strict';
class JoinResultColumn {
constructor({ columnAlias, tableNode, name }) {
this.columnAlias = columnAlias;
this.tableNode = tableNode;
this.name = name;
}
static create({ tableTree, columnAlias }) {
const tableNode = tableTree.getNodeForColumnAlias(columnAlias);
return new JoinResultColumn({
columnAlias,
tableNode,
name: tableNode.getColumnForColumnAlias(columnAlias),
});
}
}
module.exports = {
JoinResultColumn,
};
================================================
FILE: lib/queryBuilder/join/JoinResultParser.js
================================================
'use strict';
const { JoinResultColumn } = require('./JoinResultColumn');
const { groupBy } = require('../../utils/objectUtils');
class JoinResultParser {
constructor({ tableTree, omitColumnAliases = [] }) {
this.tableTree = tableTree;
this.omitColumnAliases = new Set(omitColumnAliases);
this.columnsByTableNode = null;
this.parentMap = null;
this.rootModels = null;
}
static create(args) {
return new JoinResultParser(args);
}
parse(flatRows) {
if (!Array.isArray(flatRows) || flatRows.length === 0) {
return flatRows;
}
this.columnsByTableNode = this._createColumns(flatRows[0]);
this.parentMap = new Map();
this.rootModels = [];
for (const flatRow of flatRows) {
this._parseNode(this.tableTree.rootNode, flatRow);
}
return this.rootModels;
}
_parseNode(tableNode, flatRow, parentModel = null, parentKey = null) {
const id = tableNode.getIdFromFlatRow(flatRow);
if (id === null) {
return;
}
const key = getKey(parentKey, id, tableNode);
let model = this.parentMap.get(key);
if (!model) {
model = this._createModel(tableNode, flatRow);
this._addToParent(tableNode, model, parentModel);
this.parentMap.set(key, model);
}
for (const childNode of tableNode.childNodes) {
this._parseNode(childNode, flatRow, model, key);
}
}
_createModel(tableNode, flatRow) {
const row = {};
const columns = this.columnsByTableNode.get(tableNode);
if (columns) {
for (const column of columns) {
if (!this.omitColumnAliases.has(column.columnAlias)) {
row[column.name] = flatRow[column.columnAlias];
}
}
}
const model = tableNode.modelClass.fromDatabaseJson(row);
for (const childNode of tableNode.childNodes) {
if (childNode.relation.isOneToOne()) {
model[childNode.relationProperty] = null;
} else {
model[childNode.relationProperty] = [];
}
}
return model;
}
_addToParent(tableNode, model, parentModel) {
if (tableNode.parentNode) {
if (tableNode.relation.isOneToOne()) {
parentModel[tableNode.relationProperty] = model;
} else {
parentModel[tableNode.relationProperty].push(model);
}
} else {
// Root model. Add to root list.
this.rootModels.push(model);
}
}
_createColumns(row) {
const columns = Object.keys(row).map((columnAlias) =>
JoinResultColumn.create({ tableTree: this.tableTree, columnAlias }),
);
return groupBy(columns, getTableNode);
}
}
function getTableNode(column) {
return column.tableNode;
}
function getKey(parentKey, id, tableNode) {
if (parentKey !== null) {
return `${parentKey}/${tableNode.relationProperty}/${id}`;
} else {
return `/${id}`;
}
}
module.exports = {
JoinResultParser,
};
================================================
FILE: lib/queryBuilder/join/RelationJoiner.js
================================================
'use strict';
const { uniqBy } = require('../../utils/objectUtils');
const { Selection } = require('../operations/select/Selection');
const { createModifier } = require('../../utils/createModifier');
const { map: mapPromise } = require('../../utils/promiseUtils');
const { ValidationErrorType } = require('../../model/ValidationError');
const { TableTree } = require('./TableTree');
const { JoinResultParser } = require('./JoinResultParser');
const { ID_LENGTH_LIMIT } = require('./utils');
/**
* Given a relation expression, builds a query using joins to fetch it.
*/
class RelationJoiner {
constructor({ modelClass }) {
this.rootModelClass = modelClass;
// The relation expression to join.
this.expression = null;
// Explicit modifiers for the relation expression.
this.modifiers = null;
this.options = defaultOptions();
this.tableTree = null;
this.internalSelections = null;
}
setExpression(expression) {
if (!this.expression) {
this.expression = expression;
}
return this;
}
setModifiers(modifiers) {
if (!this.modifiers) {
this.modifiers = modifiers;
}
return this;
}
setOptions(options) {
this.options = Object.assign(this.options, options);
return this;
}
/**
* Fetches the column information needed for building the select clauses.
*
* This must be called before calling `build(builder, true)`. `build(builder, false)`
* can be called without this since it doesn't build selects.
*/
fetchColumnInfo(builder) {
const tableTree = this._getTableTree(builder);
const allModelClasses = new Set(tableTree.nodes.map((node) => node.modelClass));
return mapPromise(
Array.from(allModelClasses),
(modelClass) => modelClass.fetchTableMetadata({ parentBuilder: builder }),
{
concurrency: this.rootModelClass.getConcurrency(builder.unsafeKnex()),
},
);
}
build(builder, buildSelects = true) {
const rootTableNode = this._getTableTree(builder).rootNode;
const userSelectQueries = new Map([[rootTableNode, builder]]);
for (const tableNode of rootTableNode.childNodes) {
this._buildJoinsForNode({ builder, tableNode, userSelectQueries });
}
if (buildSelects) {
this._buildSelects({ builder, tableNode: rootTableNode, userSelectQueries });
}
}
parseResult(builder, flatRows) {
const parser = JoinResultParser.create({
tableTree: this._getTableTree(builder),
omitColumnAliases: this.internalSelections.map((it) => it.alias),
});
return parser.parse(flatRows);
}
_getTableTree(builder) {
if (!this.tableTree) {
// Create the table tree lazily.
this.tableTree = TableTree.create({
rootModelClass: this.rootModelClass,
rootTableAlias: builder.tableRef(),
expression: this.expression,
options: this.options,
});
}
return this.tableTree;
}
_buildJoinsForNode({ builder, tableNode, userSelectQueries }) {
const subqueryToJoin = createSubqueryToJoin({
builder,
tableNode,
modifiers: this.modifiers,
});
const userSelectQuery = subqueryToJoin.clone();
// relation.join applies the relation modifier that can
// also contain selects.
userSelectQuery.modify(tableNode.relation.modify);
// Save the query that contains the user specified selects
// for later use.
userSelectQueries.set(tableNode, userSelectQuery);
tableNode.relation.join(builder, {
joinOperation: this.options.joinOperation,
ownerTable: tableNode.parentNode.alias,
relatedTableAlias: tableNode.alias,
joinTableAlias: tableNode.getJoinTableAlias(builder),
relatedJoinSelectQuery: ensureIdAndRelationPropsAreSelected({
builder: subqueryToJoin,
tableNode,
}),
});
for (const childNode of tableNode.childNodes) {
this._buildJoinsForNode({ builder, tableNode: childNode, userSelectQueries });
}
}
_buildSelects({ builder, tableNode, userSelectQueries }) {
const { selections, internalSelections } = getSelectionsForNode({
builder,
tableNode,
userSelectQueries,
});
for (const selection of selections) {
checkAliasLength(tableNode.modelClass, selection.name);
}
// Save the selections that were added internally (not by the user)
// so that we can later remove the corresponding properties when
// parsing the result.
this.internalSelections = internalSelections;
builder.select(selectionsToStrings(selections));
}
}
function defaultOptions() {
return {
joinOperation: 'leftJoin',
minimize: false,
separator: ':',
aliases: {},
};
}
function createSubqueryToJoin({ builder, tableNode, modifiers }) {
const { relation, expression, modelClass } = tableNode;
const modifierQuery = modelClass.query().childQueryOf(builder);
for (const modifierName of expression.node.$modify) {
const modifier = createModifier({
modifier: modifierName,
modelClass,
modifiers,
});
try {
modifier(modifierQuery);
} catch (err) {
if (err instanceof modelClass.ModifierNotFoundError) {
throw modelClass.createValidationError({
type: ValidationErrorType.RelationExpression,
message: `could not find modifier "${modifierName}" for relation "${relation.name}"`,
});
} else {
throw err;
}
}
}
return modifierQuery;
}
function ensureIdAndRelationPropsAreSelected({ builder, tableNode }) {
const tableRef = builder.tableRef();
const cols = [
...builder.modelClass().getIdColumnArray(),
...tableNode.relation.relatedProp.cols,
...tableNode.childNodes.reduce(
(cols, childNode) => [...cols, ...childNode.relation.ownerProp.cols],
[],
),
];
const selectStrings = uniqBy(cols)
.filter((col) => !builder.hasSelectionAs(col, col))
.map((col) => `${tableRef}.${col}`);
return builder.select(selectStrings);
}
function getSelectionsForNode({ builder, tableNode, userSelectQueries }) {
const userSelectQuery = userSelectQueries.get(tableNode);
const userSelections = userSelectQuery.findAllSelections();
const userSelectedAllColumns = isSelectAllSelectionSet(userSelections);
let selections = [];
let internalSelections = [];
if (tableNode.parentNode) {
selections = mapUserSelectionsFromSubqueryToMainQuery({ userSelections, tableNode });
if (userSelectedAllColumns && tableNode.relation.isObjectionManyToManyRelation) {
const extraSelections = getJoinTableExtraSelectionsForNode({ builder, tableNode });
selections = selections.concat(extraSelections);
}
}
if (userSelectedAllColumns) {
const allColumnSelections = getAllColumnSelectionsForNode({ builder, tableNode });
selections = allColumnSelections.concat(selections);
} else {
const idSelections = getIdSelectionsForNode({ tableNode });
for (const idSelection of idSelections) {
if (!userSelectQuery.hasSelectionAs(idSelection.column, idSelection.column)) {
selections.push(idSelection);
internalSelections.push(idSelection);
}
}
}
for (const childNode of tableNode.childNodes) {
const childResult = getSelectionsForNode({ builder, tableNode: childNode, userSelectQueries });
selections = selections.concat(childResult.selections);
internalSelections = internalSelections.concat(childResult.internalSelections);
}
return {
selections,
internalSelections,
};
}
function mapUserSelectionsFromSubqueryToMainQuery({ userSelections, tableNode }) {
return userSelections.filter(isNotSelectAll).map((selection) => {
return new Selection(
tableNode.alias,
selection.name,
tableNode.getColumnAliasForColumn(selection.name),
);
});
}
function getJoinTableExtraSelectionsForNode({ builder, tableNode }) {
return tableNode.relation.joinTableExtras.map((extra) => {
return new Selection(
tableNode.getJoinTableAlias(builder),
extra.joinTableCol,
tableNode.getColumnAliasForColumn(extra.aliasCol),
);
});
}
function getAllColumnSelectionsForNode({ builder, tableNode }) {
const table = builder.tableNameFor(tableNode.modelClass);
const tableMeta = tableNode.modelClass.tableMetadata({ table });
if (!tableMeta) {
throw new Error(
`table metadata has not been fetched for table '${table}'. Are you trying to call toKnexQuery() for a withGraphJoined query? To make sure the table metadata is fetched see the objection.initialize function.`,
);
}
return tableMeta.columns.map((columnName) => {
return new Selection(
tableNode.alias,
columnName,
tableNode.getColumnAliasForColumn(columnName),
);
});
}
function getIdSelectionsForNode({ tableNode }) {
return tableNode.modelClass.getIdColumnArray().map((columnName) => {
return new Selection(
tableNode.alias,
columnName,
tableNode.getColumnAliasForColumn(columnName),
);
});
}
function selectionsToStrings(selections) {
return selections.map((selection) => {
const selectStr = `${selection.table}.${selection.column}`;
return `${selectStr} as ${selection.alias}`;
});
}
function isSelectAll(selection) {
return selection.column === '*';
}
function isNotSelectAll(selection) {
return selection.column !== '*';
}
function isSelectAllSelectionSet(selections) {
return selections.length === 0 || selections.some(isSelectAll);
}
function checkAliasLength(modelClass, alias) {
if (alias.length > ID_LENGTH_LIMIT) {
throw modelClass.createValidationError({
type: ValidationErrorType.RelationExpression,
message: `identifier ${alias} is over ${ID_LENGTH_LIMIT} characters long and would be truncated by the database engine.`,
});
}
}
module.exports = {
RelationJoiner,
};
================================================
FILE: lib/queryBuilder/join/TableNode.js
================================================
'use strict';
class TableNode {
constructor({ tableTree, modelClass, expression, parentNode = null, relation = null }) {
this.tableTree = tableTree;
this.modelClass = modelClass;
this.parentNode = parentNode;
this.relation = relation;
this.expression = expression;
this.childNodes = [];
this.alias = this._calculateAlias();
this.idGetter = this._createIdGetter();
}
static create(args) {
const node = new TableNode(args);
if (node.parentNode) {
node.parentNode.childNodes.push(node);
}
return node;
}
get options() {
return this.tableTree.options;
}
get relationProperty() {
return this.expression.node.$name;
}
getReferenceForColumn(column) {
return `${this.alias}.${column}`;
}
getColumnAliasForColumn(column) {
if (this.parentNode) {
return `${this.alias}${this.options.separator}${column}`;
} else {
return column;
}
}
getColumnForColumnAlias(columnAlias) {
const lastSepIndex = columnAlias.lastIndexOf(this.options.separator);
if (lastSepIndex === -1) {
return columnAlias;
} else {
return columnAlias.slice(lastSepIndex + this.options.separator.length);
}
}
getIdFromFlatRow(flatRow) {
return this.idGetter(flatRow);
}
getJoinTableAlias(builder) {
if (this.relation.isObjectionManyToManyRelation) {
return (
builder.aliasFor(this.relation.joinTableModelClass) ||
this.modelClass.joinTableAlias(this.alias)
);
} else {
return undefined;
}
}
_calculateAlias() {
if (this.parentNode) {
const relationName = this.expression.node.$name;
const alias = this.options.aliases[relationName] || relationName;
if (this.options.minimize) {
return `_t${this.tableTree.createNextUid()}`;
} else if (this.parentNode.parentNode) {
return `${this.parentNode.alias}${this.options.separator}${alias}`;
} else {
return alias;
}
} else {
return this.tableTree.rootTableAlias;
}
}
_createIdGetter() {
const idColumns = this.modelClass.getIdColumnArray();
const columnAliases = idColumns.map((column) => this.getColumnAliasForColumn(column));
if (idColumns.length === 1) {
return createIdGetter(columnAliases);
} else {
return createCompositeIdGetter(columnAliases);
}
}
}
function createIdGetter(columnAliases) {
const columnAlias = columnAliases[0];
return (flatRow) => {
const id = flatRow[columnAlias];
if (id === null) {
return null;
}
return `${id}`;
};
}
function createCompositeIdGetter(columnAliases) {
if (columnAliases.length === 2) {
return createTwoIdGetter(columnAliases);
} else {
return createMultiIdGetter(columnAliases);
}
}
function createTwoIdGetter(columnAliases) {
const columnAlias1 = columnAliases[0];
const columnAlias2 = columnAliases[1];
return (flatRow) => {
const id1 = flatRow[columnAlias1];
const id2 = flatRow[columnAlias2];
if (id1 === null || id2 === null) {
return null;
}
return `${id1},${id2}`;
};
}
function createMultiIdGetter(columnAliases) {
return (flatRow) => {
let idStr = '';
for (let i = 0, l = columnAliases.length; i < l; ++i) {
const columnAlias = columnAliases[i];
const id = flatRow[columnAlias];
if (id === null) {
return null;
}
idStr += id;
if (i !== l - 1) {
idStr += ',';
}
}
return idStr;
};
}
module.exports = {
TableNode,
};
================================================
FILE: lib/queryBuilder/join/TableTree.js
================================================
'use strict';
const { forEachChildExpression } = require('./utils');
const { TableNode } = require('./TableNode');
class TableTree {
constructor({ expression, rootModelClass, rootTableAlias, options }) {
this.options = options;
this.rootModelClass = rootModelClass;
this.rootTableAlias = rootTableAlias;
this.nodes = [];
this.nodesByAlias = new Map();
this.uidCounter = 0;
this._createNodes({ expression, modelClass: rootModelClass });
}
static create(args) {
return new TableTree(args);
}
get rootNode() {
return this.nodes[0];
}
getNodeForColumnAlias(columnAlias) {
const lastSepIndex = columnAlias.lastIndexOf(this.options.separator);
if (lastSepIndex === -1) {
return this.rootNode;
} else {
const tableAlias = columnAlias.slice(0, lastSepIndex);
return this.nodesByAlias.get(tableAlias);
}
}
createNextUid() {
return this.uidCounter++;
}
_createNodes({ expression, modelClass }) {
const rootNode = this._createRootNode({ expression, modelClass });
this._createChildNodes({ expression, modelClass, parentNode: rootNode });
for (const node of this.nodes) {
this.nodesByAlias.set(node.alias, node);
}
}
_createRootNode({ expression, modelClass }) {
const node = TableNode.create({
tableTree: this,
modelClass,
expression,
});
this.nodes.push(node);
return node;
}
_createChildNodes({ expression, modelClass, parentNode }) {
forEachChildExpression(expression, modelClass, (childExpr, relation) => {
const node = TableNode.create({
tableTree: this,
modelClass: relation.relatedModelClass,
expression: childExpr,
parentNode,
relation,
});
this.nodes.push(node);
this._createChildNodes({
expression: childExpr,
modelClass: relation.relatedModelClass,
parentNode: node,
});
});
}
}
module.exports = {
TableTree,
};
================================================
FILE: lib/queryBuilder/join/utils.js
================================================
'use strict';
const { ValidationErrorType } = require('../../model/ValidationError');
const ID_LENGTH_LIMIT = 63;
const RELATION_RECURSION_LIMIT = 64;
// Given a relation expression, goes through all first level children.
function forEachChildExpression(expr, modelClass, callback) {
if (expr.node.$allRecursive || expr.maxRecursionDepth > RELATION_RECURSION_LIMIT) {
throw modelClass.createValidationError({
type: ValidationErrorType.RelationExpression,
message: `recursion depth of eager expression ${expr.toString()} too big for JoinEagerAlgorithm`,
});
}
expr.forEachChildExpression(modelClass, callback);
}
module.exports = {
ID_LENGTH_LIMIT,
RELATION_RECURSION_LIMIT,
forEachChildExpression,
};
================================================
FILE: lib/queryBuilder/operations/DelegateOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
// Operation that simply delegates all calls to the operation passed
// to to the constructor in `opt.delegate`.
class DelegateOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.delegate = opt.delegate;
}
get modelOptions() {
return this.delegate.modelOptions;
}
is(OperationClass) {
return super.is(OperationClass) || this.delegate.is(OperationClass);
}
onAdd(builder, args) {
return this.delegate.onAdd(builder, args);
}
onBefore1(builder, result) {
return this.delegate.onBefore1(builder, result);
}
hasOnBefore1() {
return this.onBefore1 !== DelegateOperation.prototype.onBefore1 || this.delegate.hasOnBefore1();
}
onBefore2(builder, result) {
return this.delegate.onBefore2(builder, result);
}
hasOnBefore2() {
return this.onBefore2 !== DelegateOperation.prototype.onBefore2 || this.delegate.hasOnBefore2();
}
onBefore3(builder, result) {
return this.delegate.onBefore3(builder, result);
}
hasOnBefore3() {
return this.onBefore3 !== DelegateOperation.prototype.onBefore3 || this.delegate.hasOnBefore3();
}
onBuild(builder) {
return this.delegate.onBuild(builder);
}
hasOnBuild() {
return this.onBuild !== DelegateOperation.prototype.onBuild || this.delegate.hasOnBuild();
}
onBuildKnex(knexBuilder, builder) {
return this.delegate.onBuildKnex(knexBuilder, builder);
}
hasOnBuildKnex() {
return (
this.onBuildKnex !== DelegateOperation.prototype.onBuildKnex || this.delegate.hasOnBuildKnex()
);
}
onRawResult(builder, result) {
return this.delegate.onRawResult(builder, result);
}
hasOnRawResult() {
return (
this.onRawResult !== DelegateOperation.prototype.onRawResult || this.delegate.hasOnRawResult()
);
}
onAfter1(builder, result) {
return this.delegate.onAfter1(builder, result);
}
hasOnAfter1() {
return this.onAfter1 !== DelegateOperation.prototype.onAfter1 || this.delegate.hasOnAfter1();
}
onAfter2(builder, result) {
return this.delegate.onAfter2(builder, result);
}
hasOnAfter2() {
return this.onAfter2 !== DelegateOperation.prototype.onAfter2 || this.delegate.hasOnAfter2();
}
onAfter3(builder, result) {
return this.delegate.onAfter3(builder, result);
}
hasOnAfter3() {
return this.onAfter3 !== DelegateOperation.prototype.onAfter3 || this.delegate.hasOnAfter3();
}
queryExecutor(builder) {
return this.delegate.queryExecutor(builder);
}
hasQueryExecutor() {
return (
this.queryExecutor !== DelegateOperation.prototype.queryExecutor ||
this.delegate.hasQueryExecutor()
);
}
onError(builder, error) {
return this.delegate.onError(builder, error);
}
hasOnError() {
return this.onError !== DelegateOperation.prototype.onError || this.delegate.hasOnError();
}
toFindOperation(builder) {
return this.delegate.toFindOperation(builder);
}
hasToFindOperation() {
return (
this.hasToFindOperation !== DelegateOperation.prototype.hasToFindOperation ||
this.delegate.hasToFindOperation()
);
}
clone() {
const clone = super.clone();
clone.delegate = this.delegate && this.delegate.clone();
return clone;
}
}
module.exports = {
DelegateOperation,
};
================================================
FILE: lib/queryBuilder/operations/DeleteOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { StaticHookArguments } = require('../StaticHookArguments');
class DeleteOperation extends QueryBuilderOperation {
async onBefore2(builder, result) {
await callBeforeDelete(builder);
return result;
}
onBuildKnex(knexBuilder) {
return knexBuilder.delete();
}
onAfter2(builder, result) {
return callAfterDelete(builder, result);
}
toFindOperation() {
return null;
}
}
function callBeforeDelete(builder) {
return callStaticBeforeDelete(builder);
}
function callStaticBeforeDelete(builder) {
const args = StaticHookArguments.create({ builder });
return builder.modelClass().beforeDelete(args);
}
function callAfterDelete(builder, result) {
return callStaticAfterDelete(builder, result);
}
async function callStaticAfterDelete(builder, result) {
const args = StaticHookArguments.create({ builder, result });
const maybeResult = await builder.modelClass().afterDelete(args);
if (maybeResult === undefined) {
return result;
} else {
return maybeResult;
}
}
module.exports = {
DeleteOperation,
};
================================================
FILE: lib/queryBuilder/operations/FindByIdOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { assertIdNotUndefined } = require('../../utils/assert');
class FindByIdOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.id = this.opt.id;
}
onAdd(builder, args) {
if (this.id === null || this.id === undefined) {
this.id = args[0];
}
return super.onAdd(builder, args);
}
onBuild(builder) {
if (!builder.internalOptions().skipUndefined) {
assertIdNotUndefined(this.id, `undefined was passed to ${this.name}`);
}
builder.whereComposite(builder.fullIdColumn(), this.id);
}
clone() {
const clone = super.clone();
clone.id = this.id;
return clone;
}
}
module.exports = {
FindByIdOperation,
};
================================================
FILE: lib/queryBuilder/operations/FindByIdsOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class FindByIdsOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.ids = null;
}
onAdd(builder, args) {
this.ids = args[0];
return super.onAdd(builder, args);
}
onBuild(builder) {
builder.whereInComposite(builder.fullIdColumn(), this.ids);
}
clone() {
const clone = super.clone();
clone.ids = this.ids;
return clone;
}
}
module.exports = {
FindByIdsOperation,
};
================================================
FILE: lib/queryBuilder/operations/FindOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { StaticHookArguments } = require('../StaticHookArguments');
const { isPromise, after, afterReturn } = require('../../utils/promiseUtils');
const { isObject } = require('../../utils/objectUtils');
class FindOperation extends QueryBuilderOperation {
onBefore2(builder, result) {
return afterReturn(callStaticBeforeFind(builder), result);
}
onAfter3(builder, results) {
const opt = builder.findOptions();
if (opt.dontCallFindHooks) {
return results;
} else {
return callAfterFind(builder, results);
}
}
}
function callStaticBeforeFind(builder) {
const args = StaticHookArguments.create({ builder });
return builder.modelClass().beforeFind(args);
}
function callAfterFind(builder, result) {
const opt = builder.findOptions();
const maybePromise = callInstanceAfterFind(builder.context(), result, opt.callAfterFindDeeply);
return after(maybePromise, () => callStaticAfterFind(builder, result));
}
function callStaticAfterFind(builder, result) {
const args = StaticHookArguments.create({ builder, result });
const maybePromise = builder.modelClass().afterFind(args);
return after(maybePromise, (maybeResult) => {
if (maybeResult === undefined) {
return result;
} else {
return maybeResult;
}
});
}
function callInstanceAfterFind(ctx, results, deep) {
if (Array.isArray(results)) {
if (results.length === 1) {
return callAfterFindForOne(ctx, results[0], results, deep);
} else {
return callAfterFindArray(ctx, results, deep);
}
} else {
return callAfterFindForOne(ctx, results, results, deep);
}
}
function callAfterFindArray(ctx, results, deep) {
if (results.length === 0 || !isObject(results[0])) {
return results;
}
const mapped = new Array(results.length);
let containsPromise = false;
for (let i = 0, l = results.length; i < l; ++i) {
mapped[i] = callAfterFindForOne(ctx, results[i], results[i], deep);
if (isPromise(mapped[i])) {
containsPromise = true;
}
}
if (containsPromise) {
return Promise.all(mapped);
} else {
return mapped;
}
}
function callAfterFindForOne(ctx, model, result, deep) {
if (!isObject(model) || !model.$isObjectionModel) {
return result;
}
if (deep) {
const results = [];
const containsPromise = callAfterFindForRelations(ctx, model, results);
if (containsPromise) {
return Promise.all(results).then(() => {
return doCallAfterFind(ctx, model, result);
});
} else {
return doCallAfterFind(ctx, model, result);
}
} else {
return doCallAfterFind(ctx, model, result);
}
}
function callAfterFindForRelations(ctx, model, results) {
const keys = Object.keys(model);
let containsPromise = false;
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
const value = model[key];
if (isRelation(value)) {
const maybePromise = callInstanceAfterFind(ctx, value, true);
if (isPromise(maybePromise)) {
containsPromise = true;
}
results.push(maybePromise);
}
}
return containsPromise;
}
function isRelation(value) {
return (
(isObject(value) && value.$isObjectionModel) ||
(isNonEmptyObjectArray(value) && value[0].$isObjectionModel)
);
}
function isNonEmptyObjectArray(value) {
return Array.isArray(value) && value.length > 0 && isObject(value[0]);
}
function doCallAfterFind(ctx, model, result) {
const afterFind = getAfterFindHook(model);
if (afterFind !== null) {
const maybePromise = afterFind.call(model, ctx);
if (isPromise(maybePromise)) {
return maybePromise.then(() => result);
} else {
return result;
}
} else {
return result;
}
}
function getAfterFindHook(model) {
if (model.$afterFind !== model.$objectionModelClass.prototype.$afterFind) {
return model.$afterFind;
} else {
return null;
}
}
module.exports = {
FindOperation,
};
================================================
FILE: lib/queryBuilder/operations/FirstOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class FirstOperation extends QueryBuilderOperation {
onBuildKnex(knexBuilder, builder) {
const modelClass = builder.modelClass();
if (builder.isFind() && modelClass.useLimitInFirst) {
knexBuilder = knexBuilder.limit(1);
}
return knexBuilder;
}
onAfter3(_, result) {
if (Array.isArray(result)) {
return result[0];
} else {
return result;
}
}
}
module.exports = {
FirstOperation,
};
================================================
FILE: lib/queryBuilder/operations/FromOperation.js
================================================
'use strict';
const { ObjectionToKnexConvertingOperation } = require('./ObjectionToKnexConvertingOperation');
const { isPlainObject, isString } = require('../../utils/objectUtils');
const ALIAS_REGEX = /\s+as\s+/i;
// FromOperation corresponds to a `.from(args)` call. The call is delegated to
// knex, but we first try to parse the arguments so that we can determine which
// tables have been mentioned in a query's from clause. We only parse string
// references and not `raw` or `ref` etc. references at this point thouhg.
class FromOperation extends ObjectionToKnexConvertingOperation {
constructor(name, opt) {
super(name, opt);
this.table = null;
this.alias = null;
}
onAdd(builder, args) {
const ret = super.onAdd(builder, args);
const parsed = parseTableAndAlias(this.args[0], builder);
if (parsed.table) {
builder.tableName(parsed.table);
this.table = parsed.table;
}
if (parsed.alias) {
builder.aliasFor(builder.modelClass().getTableName(), parsed.alias);
this.alias = parsed.alias;
}
return ret;
}
onBuildKnex(knexBuilder, builder) {
// Simply call knex's from method with the converted arguments.
return knexBuilder.from.apply(knexBuilder, this.getKnexArgs(builder));
}
clone() {
const clone = super.clone();
clone.table = this.table;
clone.alias = this.alias;
return clone;
}
}
function parseTableAndAlias(arg, builder) {
if (isString(arg)) {
return parseTableAndAliasFromString(arg);
} else if (isPlainObject(arg)) {
return parseTableAndAliasFromObject(arg, builder);
} else {
// Could not parse table and alias from the arguments.
return {
table: null,
alias: null,
};
}
}
function parseTableAndAliasFromString(arg) {
if (ALIAS_REGEX.test(arg)) {
const parts = arg.split(ALIAS_REGEX);
return {
table: parts[0].trim(),
alias: parts[1].trim(),
};
} else {
return {
table: arg.trim(),
alias: null,
};
}
}
function parseTableAndAliasFromObject(arg, builder) {
for (const alias of Object.keys(arg)) {
const table = arg[alias].trim();
if (table === builder.modelClass().getTableName()) {
return {
alias,
table,
};
}
}
throw new Error(
`one of the tables in ${JSON.stringify(arg)} must be the query's model class's table.`,
);
}
module.exports = {
FromOperation,
};
================================================
FILE: lib/queryBuilder/operations/InsertAndFetchOperation.js
================================================
'use strict';
const { InsertOperation } = require('./InsertOperation');
const { DelegateOperation } = require('./DelegateOperation');
const { keyByProps } = require('../../model/modelUtils');
const { asArray } = require('../../utils/objectUtils');
class InsertAndFetchOperation extends DelegateOperation {
constructor(name, opt) {
super(name, opt);
if (!this.delegate.is(InsertOperation)) {
throw new Error('Invalid delegate');
}
}
get models() {
return this.delegate.models;
}
async onAfter2(builder, inserted) {
const modelClass = builder.modelClass();
const insertedModels = await super.onAfter2(builder, inserted);
const insertedModelArray = asArray(insertedModels);
const idProps = modelClass.getIdPropertyArray();
const ids = insertedModelArray.map((model) => model.$id());
const fetchedModels = await modelClass
.query()
.childQueryOf(builder)
.findByIds(ids)
.castTo(builder.resultModelClass());
const modelsById = keyByProps(fetchedModels, idProps);
// Instead of returning the freshly fetched models, update the input
// models with the fresh values.
insertedModelArray.forEach((insertedModel) => {
insertedModel.$set(modelsById.get(insertedModel.$propKey(idProps)));
});
return insertedModels;
}
}
module.exports = {
InsertAndFetchOperation,
};
================================================
FILE: lib/queryBuilder/operations/InsertGraphAndFetchOperation.js
================================================
'use strict';
const { DelegateOperation } = require('./DelegateOperation');
const { InsertGraphOperation } = require('./InsertGraphOperation');
const { RelationExpression } = require('../RelationExpression');
class InsertGraphAndFetchOperation extends DelegateOperation {
constructor(name, opt) {
super(name, opt);
if (!this.delegate.is(InsertGraphOperation)) {
throw new Error('Invalid delegate');
}
}
get models() {
return this.delegate.models;
}
get isArray() {
return this.delegate.isArray;
}
async onAfter2(builder) {
if (this.models.length === 0) {
return this.isArray ? [] : null;
}
const eager = RelationExpression.fromModelGraph(this.models);
const modelClass = this.models[0].constructor;
const ids = this.models.map((model) => model.$id());
const models = await modelClass
.query()
.childQueryOf(builder)
.findByIds(ids)
.withGraphFetched(eager);
return this.isArray ? models : models[0] || null;
}
}
module.exports = {
InsertGraphAndFetchOperation,
};
================================================
FILE: lib/queryBuilder/operations/InsertGraphOperation.js
================================================
'use strict';
const { DelegateOperation } = require('./DelegateOperation');
const { InsertOperation } = require('./InsertOperation');
const { GraphUpsert } = require('../graph/GraphUpsert');
class InsertGraphOperation extends DelegateOperation {
constructor(name, opt = null) {
super(name, opt);
if (!this.delegate.is(InsertOperation)) {
throw new Error('Invalid delegate');
}
Object.assign(this.delegate.modelOptions, GraphUpsert.modelOptions);
this.upsertOptions = opt.opt || {};
this.upsert = null;
}
get models() {
return this.delegate.models;
}
get isArray() {
return this.delegate.isArray;
}
get relation() {
return this.delegate.relation;
}
onAdd(builder, args) {
const retVal = super.onAdd(builder, args);
this.upsert = new GraphUpsert({
objects: this.models,
rootModelClass: builder.modelClass(),
upsertOptions: Object.assign({}, this.upsertOptions, {
noUpdate: true,
noDelete: true,
noUnrelate: true,
insertMissing: true,
}),
});
// We resolve this query here and will not execute it. This is because the root
// value may depend on other models in the graph and cannot be inserted first.
builder.resolve([]);
return retVal;
}
onBefore1(_, result) {
// Do nothing.
return result;
}
onBefore2(builder, result) {
// We override this with empty implementation so that the $beforeInsert()
// hooks are not called twice for the root models.
if (this.relation) {
// We still need to call the relation before insert hook if the the
// delegate operation is a RelationInsertOperation.
return this.relation.executeBeforeInsert(this.models, builder.context(), result);
} else {
return result;
}
}
onBefore3(_, result) {
// Do nothing.
return result;
}
onBuild() {
// Do nothing.
}
onBuildKnex(knexBuilder) {
// Do nothing.
return knexBuilder;
}
// We overrode all other hooks but this one and do all the work in here.
// This is a bit hacky.
async onAfter1(builder, ...restArgs) {
await this.upsert.run(builder);
return await super.onAfter1(builder, ...restArgs);
}
onAfter2() {
// We override this with empty implementation so that the $afterInsert() hooks
// are not called twice for the root models.
return this.isArray ? this.models : this.models[0] || null;
}
clone() {
const clone = super.clone();
clone.upsert = this.upsert;
return clone;
}
}
module.exports = {
InsertGraphOperation,
};
================================================
FILE: lib/queryBuilder/operations/InsertOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { StaticHookArguments } = require('../StaticHookArguments');
const { after, mapAfterAllReturn } = require('../../utils/promiseUtils');
const { isPostgres, isSqlite, isMySql, isMsSql } = require('../../utils/knexUtils');
const { isObject } = require('../../utils/objectUtils');
// Base class for all insert operations.
class InsertOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.models = null;
this.isArray = false;
this.modelOptions = Object.assign({}, this.opt.modelOptions || {});
}
onAdd(builder, args) {
const json = args[0];
const modelClass = builder.modelClass();
this.isArray = Array.isArray(json);
this.models = modelClass.ensureModelArray(json, this.modelOptions);
return true;
}
async onBefore2(builder, result) {
if (this.models.length > 1 && !isPostgres(builder.knex()) && !isMsSql(builder.knex())) {
throw new Error('batch insert only works with Postgresql and SQL Server');
} else {
await callBeforeInsert(builder, this.models);
return result;
}
}
onBuildKnex(knexBuilder, builder) {
if (!isSqlite(builder.knex()) && !isMySql(builder.knex()) && !builder.has(/returning/)) {
// If the user hasn't specified a `returning` clause, we make sure
// that at least the identifier is returned.
knexBuilder = knexBuilder.returning(builder.modelClass().getIdColumn());
}
return knexBuilder.insert(this.models.map((model) => model.$toDatabaseJson(builder)));
}
onAfter1(_, ret) {
if (!Array.isArray(ret) || !ret.length || ret === this.models) {
// Early exit if there is nothing to do.
return this.models;
}
if (isObject(ret[0])) {
// If the user specified a `returning` clause the result may be an array of objects.
// Merge all values of the objects to our models.
for (let i = 0, l = this.models.length; i < l; ++i) {
this.models[i].$setDatabaseJson(ret[i]);
}
} else {
// If the return value is not an array of objects, we assume it is an array of identifiers.
for (let i = 0, l = this.models.length; i < l; ++i) {
const model = this.models[i];
// Don't set the id if the model already has one. MySQL and Sqlite don't return the correct
// primary key value if the id is not generated in db, but given explicitly.
if (!model.$id()) {
model.$id(ret[i]);
}
}
}
return this.models;
}
onAfter2(builder, models) {
const result = this.isArray ? models : models[0] || null;
return callAfterInsert(builder, this.models, result);
}
toFindOperation() {
return null;
}
clone() {
const clone = super.clone();
clone.models = this.models;
clone.isArray = this.isArray;
return clone;
}
}
function callBeforeInsert(builder, models) {
const maybePromise = callInstanceBeforeInsert(builder, models);
return after(maybePromise, () => callStaticBeforeInsert(builder));
}
function callInstanceBeforeInsert(builder, models) {
return mapAfterAllReturn(models, (model) => model.$beforeInsert(builder.context()), models);
}
function callStaticBeforeInsert(builder) {
const args = StaticHookArguments.create({ builder });
return builder.modelClass().beforeInsert(args);
}
function callAfterInsert(builder, models, result) {
const maybePromise = callInstanceAfterInsert(builder, models);
return after(maybePromise, () => callStaticAfterInsert(builder, result));
}
function callInstanceAfterInsert(builder, models) {
return mapAfterAllReturn(models, (model) => model.$afterInsert(builder.context()), models);
}
function callStaticAfterInsert(builder, result) {
const args = StaticHookArguments.create({ builder, result });
const maybePromise = builder.modelClass().afterInsert(args);
return after(maybePromise, (maybeResult) => {
if (maybeResult === undefined) {
return result;
} else {
return maybeResult;
}
});
}
module.exports = {
InsertOperation,
};
================================================
FILE: lib/queryBuilder/operations/InstanceDeleteOperation.js
================================================
'use strict';
const { DeleteOperation } = require('./DeleteOperation');
const { InstanceFindOperation } = require('./InstanceFindOperation');
const { assertHasId } = require('../../utils/assert');
class InstanceDeleteOperation extends DeleteOperation {
constructor(name, opt) {
super(name, opt);
this.instance = opt.instance;
}
async onBefore2(builder, result) {
await this.instance.$beforeDelete(builder.context());
await super.onBefore2(builder, result);
return result;
}
onBuild(builder) {
super.onBuild(builder);
assertHasId(this.instance);
builder.findById(this.instance.$id());
}
async onAfter2(builder, result) {
// The result may be an object if `returning` was used.
if (Array.isArray(result)) {
result = result[0];
}
await this.instance.$afterDelete(builder.context());
return super.onAfter2(builder, result);
}
toFindOperation() {
return new InstanceFindOperation('find', {
instance: this.instance,
});
}
}
module.exports = {
InstanceDeleteOperation,
};
================================================
FILE: lib/queryBuilder/operations/InstanceFindOperation.js
================================================
'use strict';
const { FindOperation } = require('./FindOperation');
const { assertHasId } = require('../../utils/assert');
class InstanceFindOperation extends FindOperation {
constructor(name, opt) {
super(name, opt);
this.instance = opt.instance;
}
onBuild(builder) {
assertHasId(this.instance);
builder.findById(this.instance.$id());
}
}
module.exports = {
InstanceFindOperation,
};
================================================
FILE: lib/queryBuilder/operations/InstanceInsertOperation.js
================================================
'use strict';
const { InsertOperation } = require('./InsertOperation');
class InstanceInsertOperation extends InsertOperation {
constructor(name, opt) {
super(name, opt);
this.instance = opt.instance;
}
onAdd(builder, args) {
if (!args || args.length === 0) {
args = [this.instance];
} else {
args[0] = this.instance;
}
return super.onAdd(builder, args);
}
}
module.exports = {
InstanceInsertOperation,
};
================================================
FILE: lib/queryBuilder/operations/InstanceUpdateOperation.js
================================================
'use strict';
const { UpdateOperation } = require('./UpdateOperation');
const { InstanceFindOperation } = require('./InstanceFindOperation');
const { assertHasId } = require('../../utils/assert');
const { isObject } = require('../../utils/objectUtils');
class InstanceUpdateOperation extends UpdateOperation {
constructor(name, opt) {
super(name, opt);
this.instance = opt.instance;
this.modelOptions.old = opt.instance;
}
onAdd(builder, args) {
const retVal = super.onAdd(builder, args);
if (!this.model) {
this.model = this.instance;
}
return retVal;
}
onBuild(builder) {
super.onBuild(builder);
assertHasId(this.instance);
builder.findById(this.instance.$id());
}
async onAfter2(builder, result) {
// The result may be an object if `returning` was used.
if (Array.isArray(result)) {
result = result[0];
}
result = await super.onAfter2(builder, result);
this.instance.$set(this.model);
if (isObject(result)) {
this.instance.$set(result);
}
return result;
}
toFindOperation() {
return new InstanceFindOperation('find', {
instance: this.instance,
});
}
}
module.exports = {
InstanceUpdateOperation,
};
================================================
FILE: lib/queryBuilder/operations/JoinRelatedOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { RelationExpression } = require('../RelationExpression');
const { RelationJoiner } = require('../join/RelationJoiner');
const { isString } = require('../../utils/objectUtils');
class JoinRelatedOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.calls = [];
}
get joinOperation() {
return this.opt.joinOperation;
}
addCall(call) {
this.calls.push(call);
}
onBuild(builder) {
const modelClass = builder.modelClass();
const joinOperation = this.joinOperation;
let mergedExpr = RelationExpression.create();
for (const call of this.calls) {
const expr = RelationExpression.create(call.expression).toPojo();
const childNames = expr.$childNames;
const options = call.options || {};
if (childNames.length === 1) {
applyAlias(expr, modelClass, builder, options);
}
if (options.aliases) {
applyAliases(expr, modelClass, options);
}
mergedExpr = mergedExpr.merge(expr);
}
const joiner = new RelationJoiner({
modelClass,
});
joiner.setOptions({ joinOperation });
joiner.setExpression(mergedExpr);
joiner.setModifiers(builder.modifiers());
joiner.build(builder, false);
}
clone() {
const clone = super.clone();
clone.calls = this.calls.slice();
return clone;
}
}
function applyAlias(expr, modelClass, builder, options) {
const childNames = expr.$childNames;
const childName = childNames[0];
const childExpr = expr[childName];
const relation = modelClass.getRelation(childExpr.$relation);
let alias = childName;
if (options.alias === false) {
alias = builder.tableRefFor(relation.relatedModelClass);
} else if (isString(options.alias)) {
alias = options.alias;
}
if (childName !== alias) {
renameRelationExpressionNode(expr, childName, alias);
}
}
function applyAliases(expr, modelClass, options) {
for (const childName of expr.$childNames) {
const childExpr = expr[childName];
const relation = modelClass.getRelation(childExpr.$relation);
const alias = options.aliases[childExpr.$relation];
if (alias && alias !== childName) {
renameRelationExpressionNode(expr, childName, alias);
}
applyAliases(childExpr, relation.relatedModelClass, options);
}
}
function renameRelationExpressionNode(expr, oldName, newName) {
const childExpr = expr[oldName];
delete expr[oldName];
expr[newName] = childExpr;
childExpr.$name = newName;
expr.$childNames = expr.$childNames.map((it) => (it === oldName ? newName : it));
}
module.exports = {
JoinRelatedOperation,
};
================================================
FILE: lib/queryBuilder/operations/KnexOperation.js
================================================
'use strict';
const { ObjectionToKnexConvertingOperation } = require('./ObjectionToKnexConvertingOperation');
// An operation that simply calls the equivalent knex method.
class KnexOperation extends ObjectionToKnexConvertingOperation {
onBuildKnex(knexBuilder, builder) {
return knexBuilder[this.name].apply(knexBuilder, this.getKnexArgs(builder));
}
}
module.exports = {
KnexOperation,
};
================================================
FILE: lib/queryBuilder/operations/MergeOperation.js
================================================
'use strict';
const { isFunction, isEmpty, isObject } = require('../../utils/objectUtils');
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { convertFieldExpressionsToRaw } = require('./UpdateOperation');
class MergeOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.model = null;
this.args = null;
}
onAdd(builder, args) {
this.args = args;
if (!isEmpty(args) && isObject(args[0]) && !Array.isArray(args[0])) {
const json = args[0];
const modelClass = builder.modelClass();
this.model = modelClass.ensureModel(json, { patch: true });
}
return true;
}
onBuildKnex(knexBuilder, builder) {
if (!isFunction(knexBuilder.merge)) {
throw new Error('merge method can only be chained right after onConflict method');
}
if (this.model) {
const json = this.model.$toDatabaseJson(builder);
const convertedJson = convertFieldExpressionsToRaw(builder, this.model, json);
return knexBuilder.merge(convertedJson);
}
return knexBuilder.merge(...this.args);
}
toFindOperation() {
return null;
}
clone() {
const clone = super.clone();
clone.model = this.model;
clone.args = this.args;
return clone;
}
}
module.exports = {
MergeOperation,
};
================================================
FILE: lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { isPlainObject, isObject, isFunction, once } = require('../../utils/objectUtils');
const { isKnexQueryBuilder, isKnexJoinBuilder } = require('../../utils/knexUtils');
const { transformation } = require('../transformations');
const getJoinBuilder = once(() => require('../JoinBuilder').JoinBuilder);
// An abstract operation base class that converts all arguments from objection types
// to knex types. For example objection query builders are converted into knex query
// builders and objection RawBuilder instances are converted into knex Raw instances.
class ObjectionToKnexConvertingOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.args = null;
}
getKnexArgs(builder) {
return convertArgs(this.name, builder, this.args);
}
onAdd(builder, args) {
this.args = Array.from(args);
return shouldBeAdded(this.name, builder, this.args);
}
clone() {
const clone = super.clone();
clone.args = this.args;
return clone;
}
}
function shouldBeAdded(opName, builder, args) {
const skipUndefined = builder.internalOptions().skipUndefined;
for (let i = 0, l = args.length; i < l; ++i) {
const arg = args[i];
if (isUndefined(arg)) {
if (skipUndefined) {
return false;
} else {
throw new Error(
`undefined passed as argument #${i} for '${opName}' operation. Call skipUndefined() method to ignore the undefined values.`,
);
}
}
}
return true;
}
function convertArgs(opName, builder, args) {
const skipUndefined = builder.internalOptions().skipUndefined;
return args.map((arg, i) => {
if (hasToKnexRawMethod(arg)) {
return convertToKnexRaw(arg, builder);
} else if (isObjectionQueryBuilderBase(arg)) {
return convertQueryBuilderBase(arg, builder);
} else if (isArray(arg)) {
return convertArray(arg, builder, i, opName, skipUndefined);
} else if (isFunction(arg)) {
return convertFunction(arg, builder);
} else if (isModel(arg)) {
return convertModel(arg);
} else if (isPlainObject(arg)) {
return convertPlainObject(arg, builder, i, opName, skipUndefined);
} else {
return arg;
}
});
}
function isUndefined(item) {
return item === undefined;
}
function hasToKnexRawMethod(item) {
return isObject(item) && isFunction(item.toKnexRaw);
}
function convertToKnexRaw(item, builder) {
return item.toKnexRaw(builder);
}
function isObjectionQueryBuilderBase(item) {
return isObject(item) && item.isObjectionQueryBuilderBase === true;
}
function convertQueryBuilderBase(item, builder) {
item = transformation.onConvertQueryBuilderBase(item, builder);
return item.subqueryOf(builder).toKnexQuery();
}
function isArray(item) {
return Array.isArray(item);
}
function convertArray(arr, builder, i, opName, skipUndefined) {
return arr.map((item) => {
if (item === undefined) {
if (!skipUndefined) {
throw new Error(
`undefined passed as an item in argument #${i} for '${opName}' operation. Call skipUndefined() method to ignore the undefined values.`,
);
}
} else if (hasToKnexRawMethod(item)) {
return convertToKnexRaw(item, builder);
} else if (isObjectionQueryBuilderBase(item)) {
return convertQueryBuilderBase(item, builder);
} else {
return item;
}
});
}
function convertFunction(func, builder) {
return function convertedKnexArgumentFunction(...args) {
if (isKnexQueryBuilder(this)) {
convertQueryBuilderFunction(this, func, builder);
} else if (isKnexJoinBuilder(this)) {
convertJoinBuilderFunction(this, func, builder);
} else {
return func.apply(this, args);
}
};
}
function convertQueryBuilderFunction(knexQueryBuilder, func, builder) {
const convertedQueryBuilder = builder.constructor.forClass(builder.modelClass());
convertedQueryBuilder.isPartial(true).subqueryOf(builder);
func.call(convertedQueryBuilder, convertedQueryBuilder);
convertedQueryBuilder.toKnexQuery(knexQueryBuilder);
}
function convertJoinBuilderFunction(knexJoinBuilder, func, builder) {
const JoinBuilder = getJoinBuilder();
const joinClauseBuilder = JoinBuilder.forClass(builder.modelClass());
joinClauseBuilder.isPartial(true).subqueryOf(builder);
func.call(joinClauseBuilder, joinClauseBuilder);
joinClauseBuilder.toKnexQuery(knexJoinBuilder);
}
function isModel(item) {
return isObject(item) && item.$isObjectionModel;
}
function convertModel(model) {
return model.$toDatabaseJson();
}
function convertPlainObject(obj, builder, i, opName, skipUndefined) {
return Object.keys(obj).reduce((out, key) => {
const item = obj[key];
if (item === undefined) {
if (!skipUndefined) {
throw new Error(
`undefined passed as a property in argument #${i} for '${opName}' operation. Call skipUndefined() method to ignore the undefined values.`,
);
}
} else if (hasToKnexRawMethod(item)) {
out[key] = convertToKnexRaw(item, builder);
} else if (isObjectionQueryBuilderBase(item)) {
out[key] = convertQueryBuilderBase(item, builder);
} else {
out[key] = item;
}
return out;
}, {});
}
module.exports = {
ObjectionToKnexConvertingOperation,
};
================================================
FILE: lib/queryBuilder/operations/OnBuildKnexOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class OnBuildKnexOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.func = null;
}
onAdd(_, args) {
this.func = args[0];
return true;
}
onBuildKnex(knexBuilder, builder) {
return this.func.call(knexBuilder, knexBuilder, builder);
}
clone() {
const clone = super.clone();
clone.func = this.func;
return clone;
}
}
module.exports = {
OnBuildKnexOperation,
};
================================================
FILE: lib/queryBuilder/operations/OnBuildOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class OnBuildOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.func = null;
}
onAdd(_, args) {
this.func = args[0];
return true;
}
onBuild(builder) {
return this.func.call(builder, builder);
}
clone() {
const clone = super.clone();
clone.func = this.func;
return clone;
}
}
module.exports = {
OnBuildOperation,
};
================================================
FILE: lib/queryBuilder/operations/OnErrorOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class OnErrorOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.func = null;
}
onAdd(_, args) {
this.func = args[0];
return true;
}
onError(builder, error) {
return this.func.call(builder, error, builder);
}
clone() {
const clone = super.clone();
clone.func = this.func;
return clone;
}
}
module.exports = {
OnErrorOperation,
};
================================================
FILE: lib/queryBuilder/operations/QueryBuilderOperation.js
================================================
'use strict';
const hookNameToHasMethodName = {
onAdd: 'hasOnAdd',
onBefore1: 'hasOnBefore1',
onBefore2: 'hasOnBefore2',
onBefore3: 'hasOnBefore3',
onBuild: 'hasOnBuild',
onBuildKnex: 'hasOnBuildKnex',
onRawResult: 'hasOnRawResult',
queryExecutor: 'hasQueryExecutor',
onAfter1: 'hasOnAfter1',
onAfter2: 'hasOnAfter2',
onAfter3: 'hasOnAfter3',
onError: 'hasOnError',
};
// An abstract base class for all query builder operations. QueryBuilderOperations almost always
// correspond to a single query builder method call. For example SelectOperation could be added when
// a `select` method is called.
//
// QueryBuilderOperation is just a bunch of query execution lifecycle hooks that subclasses
// can (but don't have to) implement.
//
// Basically a query builder is nothing but an array of QueryBuilderOperations. When the query is
// executed the hooks are called in the order explained below. The hooks are called so that a
// certain hook is called for _all_ operations before the next hook is called. For example if
// a builder has 5 operations, onBefore1 hook is called for each of them (and their results are awaited)
// before onBefore2 hook is called for any of the operations.
class QueryBuilderOperation {
constructor(name = null, opt = {}) {
this.name = name;
this.opt = opt;
// From which hook was this operation added as a child
// operation.
this.adderHookName = null;
// The parent operation that added this operation.
this.parentOperation = null;
// Operations this operation added in any of its hooks.
this.childOperations = [];
}
is(OperationClass) {
return this instanceof OperationClass;
}
hasHook(hookName) {
return this[hookNameToHasMethodName[hookName]]();
}
// This is called immediately when a query builder method is called.
//
// This method must be synchronous.
// This method should never call any methods that add operations to the builder.
onAdd(builder, args) {
return true;
}
hasOnAdd() {
return true;
}
// This is called as the first thing when the query is executed but before
// the actual database operation (knex query) is executed.
//
// This method can be asynchronous.
// You may call methods that add operations to to the builder.
onBefore1(builder, result) {}
hasOnBefore1() {
return this.onBefore1 !== QueryBuilderOperation.prototype.onBefore1;
}
// This is called as the second thing when the query is executed but before
// the actual database operation (knex query) is executed.
//
// This method can be asynchronous.
// You may call methods that add operations to to the builder.
onBefore2(builder, result) {}
hasOnBefore2() {
return this.onBefore2 !== QueryBuilderOperation.prototype.onBefore2;
}
// This is called as the third thing when the query is executed but before
// the actual database operation (knex query) is executed.
//
// This method can be asynchronous.
// You may call methods that add operations to to the builder.
onBefore3(builder, result) {}
hasOnBefore3() {
return this.onBefore3 !== QueryBuilderOperation.prototype.onBefore3;
}
// This is called as the last thing when the query is executed but before
// the actual database operation (knex query) is executed. If your operation
// needs to call other query building operations (methods that add QueryBuilderOperations)
// this is the best and last place to do it.
//
// This method must be synchronous.
// You may call methods that add operations to to the builder.
onBuild(builder) {}
hasOnBuild() {
return this.onBuild !== QueryBuilderOperation.prototype.onBuild;
}
// This is called when the knex query is built. Here you should only call knex
// methods. You may call getters and other immutable methods of the `builder`
// but you should never call methods that add QueryBuilderOperations.
//
// This method must be synchronous.
// This method should never call any methods that add operations to the builder.
// This method should always return the knex query builder.
onBuildKnex(knexBuilder, builder) {
return knexBuilder;
}
hasOnBuildKnex() {
return this.onBuildKnex !== QueryBuilderOperation.prototype.onBuildKnex;
}
// The raw knex result is passed to this method right after the database query
// has finished. This method may modify it and return the modified rows. The
// rows are automatically converted to models (if possible) after this hook
// is called.
//
// This method can be asynchronous.
onRawResult(builder, rows) {
return rows;
}
hasOnRawResult() {
return this.onRawResult !== QueryBuilderOperation.prototype.onRawResult;
}
// This is called as the first thing after the query has been executed and
// rows have been converted to model instances.
//
// This method can be asynchronous.
onAfter1(builder, result) {
return result;
}
hasOnAfter1() {
return this.onAfter1 !== QueryBuilderOperation.prototype.onAfter1;
}
// This is called as the second thing after the query has been executed and
// rows have been converted to model instances.
//
// This method can be asynchronous.
onAfter2(builder, result) {
return result;
}
hasOnAfter2() {
return this.onAfter2 !== QueryBuilderOperation.prototype.onAfter2;
}
// This is called as the third thing after the query has been executed and
// rows have been converted to model instances.
//
// This method can be asynchronous.
onAfter3(builder, result) {
return result;
}
hasOnAfter3() {
return this.onAfter3 !== QueryBuilderOperation.prototype.onAfter3;
}
// This method can be implemented to return another operation that will replace
// this one. This method is called after all `onBeforeX` and `onBuildX` hooks
// but before the database query is executed.
//
// This method must return a QueryBuilder instance.
queryExecutor(builder) {}
hasQueryExecutor() {
return this.queryExecutor !== QueryBuilderOperation.prototype.queryExecutor;
}
// This is called if an error occurs in the query execution.
//
// This method must return a QueryBuilder instance.
onError(builder, error) {}
hasOnError() {
return this.onError !== QueryBuilderOperation.prototype.onError;
}
// Returns the "find" equivalent of this operation.
//
// For example an operation that finds an item and updates it
// should return an operation that simply finds the item but
// doesn't update anything. An insert operation should return
// null since there is no find equivalent for it etc.
toFindOperation(builder) {
return this;
}
hasToFindOperation() {
return this.toFindOperation !== QueryBuilderOperation.prototype.toFindOperation;
}
// Given a set of operations, returns true if any of this operation's
// ancestor operations are included in the set.
isAncestorInSet(operationSet) {
let ancestor = this.parentOperation;
while (ancestor) {
if (operationSet.has(ancestor)) {
return true;
}
ancestor = ancestor.parentOperation;
}
return false;
}
// Takes a deep clone of this operation.
clone() {
const clone = new this.constructor(this.name, this.opt);
clone.adderHookName = this.adderHookName;
clone.parentOperation = this.parentOperation;
clone.childOperations = this.childOperations.map((childOp) => {
const childOpClone = childOp.clone();
childOpClone.parentOperation = clone;
return childOpClone;
});
return clone;
}
// Add an operation as a child operation. `hookName` must be the
// name of the parent operation's hook that called this method.
addChildOperation(hookName, operation) {
operation.adderHookName = hookName;
operation.parentOperation = this;
this.childOperations.push(operation);
}
// Removes a single child operation.
removeChildOperation(operation) {
const index = this.childOperations.indexOf(operation);
if (index !== -1) {
operation.parentOperation = null;
this.childOperations.splice(index, 1);
}
}
// Replaces a single child operation.
replaceChildOperation(operation, newOperation) {
const index = this.childOperations.indexOf(operation);
if (index !== -1) {
newOperation.adderHookName = operation.adderHookName;
newOperation.parentOperation = this;
operation.parentOperation = null;
this.childOperations[index] = newOperation;
}
}
// Removes all child operations that were added from the `hookName` hook.
removeChildOperationsByHookName(hookName) {
this.childOperations = this.childOperations.filter((op) => op.adderHookName !== hookName);
}
// Iterates through all descendant operations recursively.
forEachDescendantOperation(callback) {
for (const operation of this.childOperations) {
if (callback(operation) === false) {
return false;
}
if (operation.forEachDescendantOperation(callback) === false) {
return false;
}
}
return true;
}
}
Object.defineProperties(QueryBuilderOperation, {
isObjectionQueryBuilderOperationClass: {
enumerable: false,
writable: false,
value: true,
},
});
module.exports = {
QueryBuilderOperation,
};
================================================
FILE: lib/queryBuilder/operations/RangeOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class RangeOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.resultSizeBuilder = null;
}
onAdd(builder, args) {
if (args.length === 2) {
const start = args[0];
const end = args[1];
// Need to set these here instead of `onBuildKnex` so that they
// don't end up in the resultSize query.
builder.limit(end - start + 1).offset(start);
}
return true;
}
onBefore1(builder, result) {
this.resultSizeBuilder = builder.clone();
return super.onBefore1(builder, result);
}
async onAfter3(_, results) {
const resultSize = await this.resultSizeBuilder.resultSize();
return {
results,
total: parseInt(resultSize),
};
}
clone() {
const clone = super.clone();
clone.resultSizeBuilder = this.resultSizeBuilder;
return clone;
}
}
module.exports = {
RangeOperation,
};
================================================
FILE: lib/queryBuilder/operations/RelateOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class RelateOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.relation = opt.relation;
this.owner = opt.owner;
this.input = null;
this.ids = null;
}
clone() {
const clone = super.clone();
clone.input = this.input;
clone.ids = this.ids;
return clone;
}
}
module.exports = {
RelateOperation,
};
================================================
FILE: lib/queryBuilder/operations/ReturningOperation.js
================================================
'use strict';
const { flatten } = require('../../utils/objectUtils');
const { ObjectionToKnexConvertingOperation } = require('./ObjectionToKnexConvertingOperation');
// This class's only purpose is to normalize the arguments into an array.
//
// In knex, if a single column is given to `returning` it returns an array with the that column's value
// in it. If an array is given with a one item inside, the return value is an object.
class ReturningOperation extends ObjectionToKnexConvertingOperation {
onAdd(builder, args) {
args = flatten(args);
// Don't add an empty returning list.
if (args.length === 0) {
return false;
}
return super.onAdd(builder, args);
}
onBuildKnex(knexBuilder, builder) {
// Always pass an array of columns to knex.returning.
return knexBuilder.returning(this.getKnexArgs(builder));
}
}
module.exports = {
ReturningOperation,
};
================================================
FILE: lib/queryBuilder/operations/RunAfterOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class RunAfterOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.func = null;
}
onAdd(_, args) {
this.func = args[0];
return true;
}
onAfter3(builder, result) {
return this.func.call(builder, result, builder);
}
clone() {
const clone = super.clone();
clone.func = this.func;
return clone;
}
}
module.exports = {
RunAfterOperation,
};
================================================
FILE: lib/queryBuilder/operations/RunBeforeOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class RunBeforeOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.func = null;
}
onAdd(_, args) {
this.func = args[0];
return true;
}
onBefore1(builder, result) {
return this.func.call(builder, result, builder);
}
clone() {
const clone = super.clone();
clone.func = this.func;
return clone;
}
}
module.exports = {
RunBeforeOperation,
};
================================================
FILE: lib/queryBuilder/operations/UnrelateOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
class UnrelateOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.relation = opt.relation;
this.owner = opt.owner;
this.ids = null;
}
clone() {
const clone = super.clone();
clone.ids = this.ids;
return clone;
}
}
module.exports = {
UnrelateOperation,
};
================================================
FILE: lib/queryBuilder/operations/UpdateAndFetchOperation.js
================================================
'use strict';
const { DelegateOperation } = require('./DelegateOperation');
const { FindByIdOperation } = require('./FindByIdOperation');
const { UpdateOperation } = require('./UpdateOperation');
class UpdateAndFetchOperation extends DelegateOperation {
constructor(name, opt) {
super(name, opt);
if (!this.delegate.is(UpdateOperation)) {
throw new Error('Invalid delegate');
}
this.id = null;
this.skipIdWhere = false;
}
get model() {
return this.delegate.model;
}
onAdd(builder, args) {
this.id = args[0];
return this.delegate.onAdd(builder, args.slice(1));
}
onBuild(builder) {
if (!this.skipIdWhere) {
builder.findById(this.id);
}
super.onBuild(builder);
}
async onAfter2(builder, numUpdated) {
if (numUpdated == 0) {
// If nothing was updated, we should fetch nothing.
await super.onAfter2(builder, numUpdated);
return undefined;
}
const fetched = await builder
.emptyInstance()
.childQueryOf(builder)
.modify((builder) => {
if (!this.skipIdWhere) {
builder.findById(this.id);
}
})
.castTo(builder.resultModelClass());
if (fetched) {
this.model.$set(fetched);
}
await super.onAfter2(builder, numUpdated);
return fetched ? this.model : undefined;
}
toFindOperation() {
return new FindByIdOperation('findById', {
id: this.id,
});
}
clone() {
const clone = super.clone();
clone.id = this.id;
clone.skipIdWhere = this.skipIdWhere;
return clone;
}
}
module.exports = {
UpdateAndFetchOperation,
};
================================================
FILE: lib/queryBuilder/operations/UpdateOperation.js
================================================
'use strict';
const { ref } = require('../../queryBuilder/ReferenceBuilder');
const { isEmpty } = require('../../utils/objectUtils');
const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { StaticHookArguments } = require('../StaticHookArguments');
class UpdateOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.model = null;
this.modelOptions = Object.assign({}, this.opt.modelOptions || {});
}
onAdd(builder, args) {
const json = args[0];
const modelClass = builder.modelClass();
this.model = modelClass.ensureModel(json, this.modelOptions);
return true;
}
async onBefore2(builder, result) {
await callBeforeUpdate(builder, this.model, this.modelOptions);
return result;
}
onBefore3(builder) {
const row = this.model.$toDatabaseJson(builder);
if (isEmpty(row)) {
// Resolve the query if there is nothing to update.
builder.resolve(0);
}
}
onBuildKnex(knexBuilder, builder) {
const json = this.model.$toDatabaseJson(builder);
const convertedJson = convertFieldExpressionsToRaw(builder, this.model, json);
return knexBuilder.update(convertedJson);
}
onAfter2(builder, numUpdated) {
return callAfterUpdate(builder, this.model, this.modelOptions, numUpdated);
}
toFindOperation() {
return null;
}
clone() {
const clone = super.clone();
clone.model = this.model;
return clone;
}
}
async function callBeforeUpdate(builder, model, modelOptions) {
await callInstanceBeforeUpdate(builder, model, modelOptions);
return callStaticBeforeUpdate(builder);
}
function callInstanceBeforeUpdate(builder, model, modelOptions) {
return model.$beforeUpdate(modelOptions, builder.context());
}
function callStaticBeforeUpdate(builder) {
const args = StaticHookArguments.create({ builder });
return builder.modelClass().beforeUpdate(args);
}
async function callAfterUpdate(builder, model, modelOptions, result) {
await callInstanceAfterUpdate(builder, model, modelOptions);
return callStaticAfterUpdate(builder, result);
}
function callInstanceAfterUpdate(builder, model, modelOptions) {
return model.$afterUpdate(modelOptions, builder.context());
}
async function callStaticAfterUpdate(builder, result) {
const args = StaticHookArguments.create({ builder, result });
const maybeResult = await builder.modelClass().afterUpdate(args);
if (maybeResult === undefined) {
return result;
} else {
return maybeResult;
}
}
function convertFieldExpressionsToRaw(builder, model, json) {
const knex = builder.knex();
const convertedJson = {};
for (const key of Object.keys(json)) {
let val = json[key];
if (key.indexOf(':') > -1) {
// 'col:attr' : ref('other:lol') is transformed to
// "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
let parsed = ref(key);
let jsonRefs = '{' + parsed.parsedExpr.access.map((it) => it.ref).join(',') + '}';
let valuePlaceholder = '?';
if (isKnexQueryBuilder(val) || isKnexRaw(val)) {
valuePlaceholder = 'to_jsonb(?)';
} else {
val = JSON.stringify(val);
}
convertedJson[parsed.column] = knex.raw(
`jsonb_set(??, '${jsonRefs}', ${valuePlaceholder}, true)`,
[convertedJson[parsed.column] || parsed.column, val],
);
delete model[key];
} else {
convertedJson[key] = val;
}
}
return convertedJson;
}
module.exports = {
UpdateOperation,
convertFieldExpressionsToRaw,
};
================================================
FILE: lib/queryBuilder/operations/UpsertGraphAndFetchOperation.js
================================================
'use strict';
const { DelegateOperation } = require('./DelegateOperation');
const { UpsertGraphOperation } = require('./UpsertGraphOperation');
const { RelationExpression } = require('../RelationExpression');
class UpsertGraphAndFetchOperation extends DelegateOperation {
constructor(name, opt) {
super(name, opt);
if (!this.delegate.is(UpsertGraphOperation)) {
throw new Error('Invalid delegate');
}
}
get models() {
return this.delegate.models;
}
get isArray() {
return this.delegate.isArray;
}
async onAfter3(builder) {
if (this.models.length === 0) {
return this.isArray ? [] : null;
}
const eager = RelationExpression.fromModelGraph(this.models);
const modelClass = this.models[0].constructor;
const ids = this.models.map((model) => model.$id());
const models = await modelClass
.query()
.childQueryOf(builder)
.findByIds(ids)
.withGraphFetched(eager);
return this.isArray ? models : models[0] || null;
}
}
module.exports = {
UpsertGraphAndFetchOperation,
};
================================================
FILE: lib/queryBuilder/operations/UpsertGraphOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
const { GraphUpsert } = require('../graph/GraphUpsert');
const { RelationFindOperation } = require('../../relations/RelationFindOperation');
class UpsertGraphOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(
name,
Object.assign({}, opt, {
upsertOptions: {},
}),
);
this.upsertOptions = opt.upsertOptions || {};
this.upsert = null;
}
get models() {
return this.upsert.objects;
}
get isArray() {
return this.upsert.isArray;
}
onAdd(builder, args) {
const [objects] = args;
this.upsert = new GraphUpsert({
objects,
rootModelClass: builder.modelClass(),
upsertOptions: this.upsertOptions,
});
// Never execute this builder.
builder.resolve([]);
return true;
}
onAfter1(builder) {
if (hasOtherSqlModifyingQueryBuilderCalls(builder)) {
throw new Error(
'upsertGraph query should contain no other query builder calls like `findById`, `where` or `$relatedQuery` that would affect the SQL. They have no effect.',
);
}
return this.upsert.run(builder);
}
clone() {
const clone = super.clone();
clone.upsert = this.upsert;
return clone;
}
}
function hasOtherSqlModifyingQueryBuilderCalls(builder) {
return builder.has(/where/) || builder.has(RelationFindOperation);
}
module.exports = {
UpsertGraphOperation,
};
================================================
FILE: lib/queryBuilder/operations/WhereCompositeOperation.js
================================================
'use strict';
const { ObjectionToKnexConvertingOperation } = require('./ObjectionToKnexConvertingOperation');
const { asSingle } = require('../../utils/objectUtils');
class WhereCompositeOperation extends ObjectionToKnexConvertingOperation {
onBuildKnex(knexBuilder, builder) {
const args = this.getKnexArgs(builder);
if (args.length === 2) {
// Convert whereComposite('foo', 1) into whereComposite('foo', '=', 1)
args.splice(1, 0, '=');
} else if (args.length !== 3) {
throw new Error(`invalid number of arguments ${args.length}`);
}
return knexBuilder.where(...buildWhereArgs(...args));
}
}
function buildWhereArgs(cols, op, values) {
if (isNormalWhere(cols, values)) {
return buildNormalWhereArgs(cols, op, values);
} else if (isCompositeWhere(cols, values)) {
return buildCompositeWhereArgs(cols, op, values);
} else {
throw new Error(`both cols and values must have same dimensions`);
}
}
function isNormalWhere(cols, values) {
return (
(!Array.isArray(cols) || cols.length === 1) && (!Array.isArray(values) || values.length === 1)
);
}
function buildNormalWhereArgs(cols, op, values) {
return [asSingle(cols), op, asSingle(values)];
}
function isCompositeWhere(cols, values) {
return Array.isArray(cols) && Array.isArray(values) && cols.length === values.length;
}
function buildCompositeWhereArgs(cols, op, values) {
return [
(builder) => {
for (let i = 0, l = cols.length; i < l; ++i) {
builder.where(cols[i], op, values[i]);
}
},
];
}
module.exports = {
WhereCompositeOperation,
};
================================================
FILE: lib/queryBuilder/operations/eager/EagerOperation.js
================================================
'use strict';
const { QueryBuilderOperation } = require('../QueryBuilderOperation');
const { RelationExpression } = require('../../RelationExpression');
class EagerOperation extends QueryBuilderOperation {
constructor(name, opt) {
super(name, opt);
this.expression = RelationExpression.create();
this.modifiersAtPath = [];
this.graphOptions = this.opt.defaultGraphOptions;
}
buildFinalExpression() {
const expression = this.expression.clone();
this.modifiersAtPath.forEach((modifier, i) => {
const modifierName = getModifierName(i);
expression.expressionsAtPath(modifier.path).forEach((expr) => {
expr.node.$modify.push(modifierName);
});
});
return expression;
}
buildFinalModifiers(builder) {
// `modifiers()` returns a clone so we can modify it.
const modifiers = builder.modifiers();
this.modifiersAtPath.forEach((modifier, i) => {
const modifierName = getModifierName(i);
modifiers[modifierName] = modifier.modifier;
});
return modifiers;
}
cloneFrom(eagerOp) {
this.expression = eagerOp.expression.clone();
this.modifiersAtPath = eagerOp.modifiersAtPath.slice();
this.graphOptions = Object.assign({}, eagerOp.graphOptions);
}
clone() {
const clone = super.clone();
clone.cloneFrom(this);
return clone;
}
}
function getModifierName(index) {
return `_f${index}_`;
}
module.exports = {
EagerOperation,
};
================================================
FILE: lib/queryBuilder/operations/eager/JoinEagerOperation.js
================================================
'use strict';
const { EagerOperation } = require('./EagerOperation');
const { RelationJoiner } = require('../../join/RelationJoiner');
class JoinEagerOperation extends EagerOperation {
constructor(name, opt) {
super(name, opt);
this.joiner = null;
}
onAdd(builder) {
builder.findOptions({ callAfterFindDeeply: true });
this.joiner = new RelationJoiner({
modelClass: builder.modelClass(),
});
return true;
}
onBefore3(builder) {
return this.joiner
.setExpression(this.buildFinalExpression())
.setModifiers(this.buildFinalModifiers(builder))
.setOptions(this.graphOptions)
.fetchColumnInfo(builder);
}
onBuild(builder) {
this.joiner
.setExpression(this.buildFinalExpression())
.setModifiers(this.buildFinalModifiers(builder))
.setOptions(this.graphOptions)
.build(builder);
}
onRawResult(builder, rows) {
return this.joiner.parseResult(builder, rows);
}
clone() {
const clone = super.clone();
clone.joiner = this.joiner;
return clone;
}
}
module.exports = {
JoinEagerOperation,
};
================================================
FILE: lib/queryBuilder/operations/eager/NaiveEagerOperation.js
================================================
'use strict';
const { WhereInEagerOperation } = require('./WhereInEagerOperation');
class NaiveEagerOperation extends WhereInEagerOperation {
batchSize() {
return 1;
}
}
module.exports = {
NaiveEagerOperation,
};
================================================
FILE: lib/queryBuilder/operations/eager/WhereInEagerOperation.js
================================================
'use strict';
const promiseUtils = require('../../../utils/promiseUtils');
const { EagerOperation } = require('./EagerOperation');
const { isMsSql, isOracle, isSqlite } = require('../../../utils/knexUtils');
const { isObject, asArray, flatten, chunk } = require('../../../utils/objectUtils');
const { ValidationErrorType } = require('../../../model/ValidationError');
const { createModifier } = require('../../../utils/createModifier');
const { RelationDoesNotExistError } = require('../../../model/RelationDoesNotExistError');
const { RelationOwner } = require('../../../relations/RelationOwner');
class WhereInEagerOperation extends EagerOperation {
constructor(name, opt) {
super(name, opt);
this.relationsToFetch = [];
this.omitProps = [];
}
batchSize(knex) {
if (this.graphOptions.maxBatchSize) {
return this.graphOptions.maxBatchSize;
} else if (isMsSql(knex)) {
// On MSSQL the parameter limit is actually 2100, but since I couldn't figure out
// if the limit is for all parameters in a query or for individual clauses, we set
// the limit to 2000 to leave 100 parameters for where clauses etc.
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.
return 10000;
}
}
onBuild(builder) {
const relationsToFetch = findRelationsToFetch(
builder.modelClass(),
this.buildFinalExpression(),
);
const { selectionsToAdd, selectedProps } = findRelationPropsToSelect(builder, relationsToFetch);
if (selectionsToAdd.length) {
builder.select(selectionsToAdd);
}
this.relationsToFetch = relationsToFetch;
this.omitProps = selectedProps;
}
async onAfter2(builder, result) {
const modelClass = builder.resultModelClass();
if (!result) {
return result;
}
const models = asArray(result);
// Check models to be actual objects, to filter out `count` results (#2397).
if (!models.length || !isObject(models[0])) {
return result;
}
await promiseUtils.map(
this.relationsToFetch,
(it) => this.fetchRelation(builder, models, it.relation, it.childExpression),
{ concurrency: modelClass.getConcurrency(builder.unsafeKnex()) },
);
const intOpt = builder.internalOptions();
if (!this.omitProps.length || intOpt.keepImplicitJoinProps) {
return result;
}
// Now that relations have been fetched for `models` we can omit the
// columns that were implicitly selected by this class.
for (let i = 0, l = result.length; i < l; ++i) {
const model = result[i];
for (let c = 0, lc = this.omitProps.length; c < lc; ++c) {
modelClass.omitImpl(model, this.omitProps[c]);
}
}
return result;
}
async fetchRelation(builder, models, relation, expr) {
const modelClass = builder.resultModelClass();
const batchSize = this.batchSize(builder.knex());
const modelBatches = chunk(models, batchSize);
const result = await promiseUtils.map(
modelBatches,
(batch) => this.fetchRelationBatch(builder, batch, relation, expr),
{
concurrency: modelClass.getConcurrency(builder.unsafeKnex()),
},
);
return flatten(result);
}
fetchRelationBatch(builder, models, relation, expr) {
if (this.shouldSkipFetched(models, relation, expr)) {
return this.createSkippedQuery(builder, models, relation, expr);
}
const queryBuilder = this.createRelationQuery(builder, relation, expr);
const findOperation = relation.find(queryBuilder, RelationOwner.create(models));
findOperation.alwaysReturnArray = true;
findOperation.assignResultToOwner = true;
findOperation.relationProperty = expr.node.$name;
queryBuilder.addOperation(findOperation, []);
for (const modifierName of expr.node.$modify) {
const modifier = createModifier({
modifier: modifierName,
modelClass: relation.relatedModelClass,
modifiers: this.buildFinalModifiers(builder),
});
try {
modifier(queryBuilder);
} catch (err) {
const modelClass = builder.modelClass();
if (err instanceof modelClass.ModifierNotFoundError) {
throw modelClass.createValidationError({
type: ValidationErrorType.RelationExpression,
message: `could not find modifier "${modifierName}" for relation "${relation.name}"`,
});
} else {
throw err;
}
}
}
return queryBuilder;
}
shouldSkipFetched(models, relation, expr) {
if (!this.graphOptions.skipFetched) {
return false;
}
if (models.some((it) => it[expr.node.$name] === undefined)) {
return false;
}
const relationsToFetch = findRelationsToFetch(relation.relatedModelClass, expr);
const childModels = getRelatedModels(models, expr);
// We can only skip fetching a relation if all already fetched models
// have all needed relation properties so that we can fetch the next
// level of relations.
for (const { relation } of relationsToFetch) {
const { ownerProp } = relation;
for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
const prop = ownerProp.props[c];
for (const model of childModels) {
if (model[prop] === undefined) {
return false;
}
}
}
}
return true;
}
createSkippedQuery(builder, models, relation, expr) {
const childModels = getRelatedModels(models, expr);
return relation.relatedModelClass
.query()
.childQueryOf(builder)
.findOptions({ dontCallFindHooks: true })
.withGraphFetched(expr, this.graphOptions)
.resolve(childModels);
}
createRelationQuery(builder, relation, expr) {
return relation.relatedModelClass
.query()
.childQueryOf(builder)
.withGraphFetched(expr, this.graphOptions)
.modifiers(this.buildFinalModifiers(builder));
}
clone() {
const clone = super.clone();
clone.relationsToFetch = this.relationsToFetch.slice();
clone.omitProps = this.omitProps.slice();
return clone;
}
}
function findRelationsToFetch(modelClass, eagerExpression) {
const relationsToFetch = [];
try {
eagerExpression.forEachChildExpression(modelClass, (childExpression, relation) => {
relationsToFetch.push({
childExpression,
relation,
});
});
} catch (err) {
if (err instanceof RelationDoesNotExistError) {
throw modelClass.createValidationError({
type: ValidationErrorType.RelationExpression,
message: `unknown relation "${err.relationName}" in an eager expression`,
});
}
throw err;
}
return relationsToFetch;
}
function findRelationPropsToSelect(builder, relationsToFetch) {
const selectionsToAdd = [];
const selectedProps = [];
// Collect columns that need to be selected for the eager fetch
// to work that are not currently selected.
for (const { relation } of relationsToFetch) {
const ownerProp = relation.ownerProp;
for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
const fullCol = ownerProp.ref(builder, c).fullColumn(builder);
const prop = ownerProp.props[c];
const col = ownerProp.cols[c];
if (!builder.hasSelectionAs(fullCol, col) && selectionsToAdd.indexOf(fullCol) === -1) {
selectedProps.push(prop);
selectionsToAdd.push(fullCol);
}
}
}
return {
selectionsToAdd,
selectedProps,
};
}
function getRelatedModels(models, expr) {
const allRelated = [];
for (const model of models) {
const related = model[expr.node.$name];
if (related) {
if (Array.isArray(related)) {
for (const rel of related) {
allRelated.push(rel);
}
} else {
allRelated.push(related);
}
}
}
return allRelated;
}
module.exports = {
WhereInEagerOperation,
};
================================================
FILE: lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js
================================================
'use strict';
const jsonApi = require('./postgresJsonApi');
const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
class WhereJsonHasPostgresOperation extends ObjectionToKnexConvertingOperation {
onBuildKnex(knexBuilder, builder) {
const args = this.getKnexArgs(builder);
const sql = jsonApi.whereJsonFieldRightStringArrayOnLeftQuery(
builder.knex(),
args[0],
this.opt.operator,
args[1],
);
if (this.opt.bool === 'or') {
knexBuilder = knexBuilder.orWhereRaw(sql);
} else {
knexBuilder = knexBuilder.whereRaw(sql);
}
return knexBuilder;
}
}
module.exports = {
WhereJsonHasPostgresOperation,
};
================================================
FILE: lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js
================================================
'use strict';
const jsonApi = require('./postgresJsonApi');
const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
class WhereJsonNotObjectPostgresOperation extends ObjectionToKnexConvertingOperation {
onBuildKnex(knexBuilder, builder) {
return this.whereJsonNotObject(knexBuilder, builder.knex(), this.getKnexArgs(builder)[0]);
}
whereJsonNotObject(knexBuilder, knex, fieldExpression) {
const innerQuery = (innerQuery) => {
const builder = jsonApi.whereJsonbRefOnLeftJsonbValOrRefOnRight(
innerQuery,
fieldExpression,
'@>',
this.opt.compareValue,
'not',
);
builder.orWhereRaw(jsonApi.whereJsonFieldQuery(knex, fieldExpression, 'IS', null));
};
if (this.opt.bool === 'or') {
knexBuilder = knexBuilder.orWhere(innerQuery);
} else {
knexBuilder = knexBuilder.where(innerQuery);
}
return knexBuilder;
}
}
module.exports = {
WhereJsonNotObjectPostgresOperation,
};
================================================
FILE: lib/queryBuilder/operations/jsonApi/WhereJsonPostgresOperation.js
================================================
'use strict';
const jsonApi = require('./postgresJsonApi');
const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
class WhereJsonPostgresOperation extends ObjectionToKnexConvertingOperation {
onBuildKnex(knexBuilder, builder) {
const args = this.getKnexArgs(builder);
const rawArgs = jsonApi.whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(
args[0],
this.opt.operator,
args[1],
this.opt.prefix,
);
if (this.opt.bool === 'or') {
knexBuilder = knexBuilder.orWhereRaw.apply(knexBuilder, rawArgs);
} else {
knexBuilder = knexBuilder.whereRaw.apply(knexBuilder, rawArgs);
}
return knexBuilder;
}
}
module.exports = {
WhereJsonPostgresOperation,
};
================================================
FILE: lib/queryBuilder/operations/jsonApi/postgresJsonApi.js
================================================
'use strict';
const parser = require('../../../utils/parseFieldExpression');
const { asArray, isObject, isString } = require('../../../utils/objectUtils');
/**
* @typedef {String} FieldExpression
*
* Field expressions allow one to refer to separate JSONB fields inside columns.
*
* Syntax: [:]
*
* e.g. `Person.jsonColumnName:details.names[1]` would refer to value `'Second'`
* in column `Person.jsonColumnName` which has
* `{ details: { names: ['First', 'Second', 'Last'] } }` object stored in it.
*
* First part `` is compatible with column references used in
* knex e.g. `MyFancyTable.tributeToThBestColumnNameEver`.
*
* Second part describes a path to an attribute inside the referred column.
* It is optional and it always starts with colon which follows directly with
* first path element. e.g. `Table.jsonObjectColumnName:jsonFieldName` or
* `Table.jsonArrayColumn:[321]`.
*
* Syntax supports `[]` and `.` flavors of reference
* to json keys / array indexes:
*
* e.g. both `Table.myColumn:[1][3]` and `Table.myColumn:1.3` would access correctly
* both of the following objects `[null, [null,null,null, "I was accessed"]]` and
* `{ "1": { "3" : "I was accessed" } }`
*
* Caveats when using special characters in keys:
*
* 1. `objectColumn.key` This is the most common syntax, good if you are
* not using dots or square brackets `[]` in your json object key name.
* 2. Keys containing dots `objectColumn:[keywith.dots]` Column `{ "keywith.dots" : "I was referred" }`
* 3. Keys containing square brackets `column['[]']` `{ "[]" : "This is getting ridiculous..." }`
* 4. Keys containing square brackets and quotes
* `objectColumn:['Double."Quote".[]']` and `objectColumn:["Sinlge.'Quote'.[]"]`
* Column `{ "Double.\"Quote\".[]" : "I was referred", "Single.'Quote'.[]" : "Mee too!" }`
* 99. Keys containing dots, square brackets, single quotes and double quotes in one json key is
* not currently supported
*/
function parseFieldExpression(expression, extractAsText) {
let parsed = parser.parseFieldExpression(expression);
let jsonRefs = parsed.access.map((it) => it.ref).join(',');
let extractor = extractAsText ? '#>>' : '#>';
let middleQuotedColumnName = parsed.columnName.split('.').join('"."');
return `"${middleQuotedColumnName}"${extractor}'{${jsonRefs}}'`;
}
function whereJsonbRefOnLeftJsonbValOrRefOnRight(
builder,
fieldExpression,
operator,
jsonObjectOrFieldExpression,
queryPrefix,
) {
let queryParams = whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(
fieldExpression,
operator,
jsonObjectOrFieldExpression,
queryPrefix,
);
return builder.whereRaw.apply(builder, queryParams);
}
function whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(
fieldExpression,
operator,
jsonObjectOrFieldExpression,
queryPrefix,
) {
let fieldReference = parseFieldExpression(fieldExpression);
if (isString(jsonObjectOrFieldExpression)) {
let rightHandReference = parseFieldExpression(jsonObjectOrFieldExpression);
let refRefQuery = [
'(',
fieldReference,
')::jsonb',
operator,
'(',
rightHandReference,
')::jsonb',
];
if (queryPrefix) {
refRefQuery.unshift(queryPrefix);
}
return [refRefQuery.join(' ')];
} else if (isObject(jsonObjectOrFieldExpression)) {
let refValQuery = ['(', fieldReference, ')::jsonb', operator, '?::jsonb'];
if (queryPrefix) {
refValQuery.unshift(queryPrefix);
}
return [refValQuery.join(' '), JSON.stringify(jsonObjectOrFieldExpression)];
}
throw new Error('Invalid right hand expression.');
}
function whereJsonFieldRightStringArrayOnLeftQuery(knex, fieldExpression, operator, keys) {
let fieldReference = parseFieldExpression(fieldExpression);
keys = asArray(keys);
let questionMarksArray = keys.map((key) => {
if (!isString(key)) {
throw new Error('All keys to find must be strings.');
}
return '?';
});
let rawSqlTemplateString = 'array[' + questionMarksArray.join(',') + ']';
let rightHandExpression = knex.raw(rawSqlTemplateString, keys);
return `${fieldReference} ${operator.replace('?', '\\?')} ${rightHandExpression}`;
}
function whereJsonFieldQuery(knex, fieldExpression, operator, value) {
let fieldReference = parseFieldExpression(fieldExpression, true);
let normalizedOperator = normalizeOperator(knex, operator);
// json type comparison takes json type in string format
let cast;
let escapedValue = knex.raw(' ?', [value]);
let type = typeof value;
if (type === 'number') {
cast = '::NUMERIC';
} else if (type === 'boolean') {
cast = '::BOOLEAN';
} else if (type === 'string') {
cast = '::TEXT';
} else if (value === null) {
cast = '::TEXT';
escapedValue = 'NULL';
} else {
throw new Error('Value must be string, number, boolean or null.');
}
return `(${fieldReference})${cast} ${normalizedOperator} ${escapedValue}`;
}
function normalizeOperator(knex, operator) {
let trimmedLowerCase = operator.trim().toLowerCase();
switch (trimmedLowerCase) {
case 'is':
case 'is not':
return trimmedLowerCase;
default:
return knex.client.formatter().operator(operator);
}
}
module.exports = {
parseFieldExpression: parseFieldExpression,
whereJsonbRefOnLeftJsonbValOrRefOnRight: whereJsonbRefOnLeftJsonbValOrRefOnRight,
whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams:
whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams,
whereJsonFieldRightStringArrayOnLeftQuery: whereJsonFieldRightStringArrayOnLeftQuery,
whereJsonFieldQuery: whereJsonFieldQuery,
};
================================================
FILE: lib/queryBuilder/operations/select/SelectOperation.js
================================================
'use strict';
const { flatten } = require('../../../utils/objectUtils');
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 = [];
}
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 (const selection of selections) {
const selectionInstance = Selection.create(selection);
if (selectionInstance) {
this.selections.push(selectionInstance);
}
}
return ret;
}
onBuildKnex(knexBuilder, builder) {
return knexBuilder[this.name].apply(knexBuilder, this.getKnexArgs(builder));
}
findSelection(builder, selectionToFind) {
const selectionInstanceToFind = Selection.create(selectionToFind);
if (!selectionInstanceToFind) {
return null;
}
for (const selection of this.selections) {
if (Selection.doesSelect(builder, selection, selectionInstanceToFind)) {
return selection;
}
}
return null;
}
clone() {
const clone = super.clone();
clone.selections = this.selections.slice();
return clone;
}
}
module.exports = {
SelectOperation,
};
================================================
FILE: lib/queryBuilder/operations/select/Selection.js
================================================
'use strict';
const { isString, isObject } = require('../../../utils/objectUtils');
const ALIAS_REGEX = /\s+as\s+/i;
class Selection {
constructor(table, column, alias) {
this.table = table;
this.column = column;
this.alias = alias;
}
get name() {
return this.alias || this.column;
}
static create(selection) {
if (isObject(selection)) {
if (selection.isObjectionSelection) {
return selection;
} else if (selection.isObjectionReferenceBuilder) {
return createSelectionFromReference(selection);
} else if (selection.isObjectionRawBuilder) {
return createSelectionFromRaw(selection);
} else {
return null;
}
} else if (isString(selection)) {
return createSelectionFromString(selection);
} else {
return null;
}
}
/**
* Returns true if `selectionInBuilder` causes `selectionToTest` to be selected.
*
* Examples that return true:
*
* doesSelect(Person.query(), '*', 'name')
* doesSelect(Person.query(), 'Person.*', 'name')
* doesSelect(Person.query(), 'name', 'name')
* doesSelect(Person.query(), 'name', 'Person.name')
*/
static doesSelect(builder, selectionInBuilder, selectionToTest) {
selectionInBuilder = Selection.create(selectionInBuilder);
selectionToTest = Selection.create(selectionToTest);
if (selectionInBuilder.column === '*') {
if (selectionInBuilder.table) {
if (selectionToTest.column === '*') {
return selectionToTest.table === selectionInBuilder.table;
} else {
return (
selectionToTest.table === null || selectionToTest.table === selectionInBuilder.table
);
}
} else {
return true;
}
} else {
const selectionInBuilderTable = selectionInBuilder.table || builder.tableRef();
if (selectionToTest.column === '*') {
return false;
} else {
return (
selectionToTest.column === selectionInBuilder.column &&
(selectionToTest.table === null || selectionToTest.table === selectionInBuilderTable)
);
}
}
}
}
Object.defineProperties(Selection.prototype, {
isObjectionSelection: {
enumerable: false,
writable: false,
value: true,
},
});
function createSelectionFromReference(ref) {
return new Selection(ref.tableName, ref.column, ref.alias);
}
function createSelectionFromRaw(raw) {
if (raw.alias) {
return new Selection(null, null, raw.alias);
} else {
return null;
}
}
function createSelectionFromString(selection) {
let table = null;
let column = null;
let alias = null;
if (ALIAS_REGEX.test(selection)) {
const parts = selection.split(ALIAS_REGEX);
selection = parts[0].trim();
alias = parts[1].trim();
}
const dotIdx = selection.lastIndexOf('.');
if (dotIdx !== -1) {
table = selection.substr(0, dotIdx);
column = selection.substr(dotIdx + 1);
} else {
column = selection;
}
return new Selection(table, column, alias);
}
module.exports = {
Selection,
};
================================================
FILE: lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js
================================================
'use strict';
const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
const { flatten, zipObject, isString } = require('../../../utils/objectUtils');
const { getTempColumn } = require('../../../utils/tmpColumnUtils');
class WhereInCompositeMsSqlOperation extends ObjectionToKnexConvertingOperation {
constructor(name, opt) {
super(name, opt);
this.prefix = this.opt.prefix || null;
}
onBuildKnex(knexBuilder, builder) {
const args = this.getKnexArgs(builder);
return this.build(builder.knex(), knexBuilder, args[0], args[1]);
}
build(knex, knexBuilder, columns, values) {
let isCompositeKey = Array.isArray(columns) && columns.length > 1;
if (isCompositeKey) {
return this.buildComposite(knex, knexBuilder, columns, values);
} else {
return this.buildNonComposite(knexBuilder, columns, values);
}
}
buildComposite(knex, knexBuilder, columns, values) {
const helperColumns = columns.map((_, index) => getTempColumn(index));
if (Array.isArray(values)) {
return this.buildCompositeValue(knex, knexBuilder, columns, helperColumns, values);
} else {
return this.buildCompositeSubquery(
knex,
knexBuilder,
columns,
helperColumns,
values.as(knex.raw(`V(${helperColumns.map((_) => '??')})`, helperColumns)),
);
}
}
buildCompositeValue(knex, knexBuilder, columns, helperColumns, values) {
return this.buildCompositeSubquery(
knex,
knexBuilder,
columns,
helperColumns,
knex.raw(
`(VALUES ${values
.map((value) => `(${value.map((_) => '?').join(',')})`)
.join(',')}) AS V(${helperColumns.map((_) => '??').join(',')})`,
flatten(values).concat(helperColumns),
),
);
}
buildCompositeSubquery(knex, knexBuilder, columns, helperColumns, subQuery) {
const wrapperQuery = knex.from(subQuery).where(
zipObject(
helperColumns,
columns.map((column) => knex.raw('??', column)),
),
);
if (this.prefix === 'not') {
return knexBuilder.whereNotExists(wrapperQuery);
} else {
return knexBuilder.whereExists(wrapperQuery);
}
}
buildNonComposite(knexBuilder, columns, values) {
const col = isString(columns) ? columns : columns[0];
if (Array.isArray(values)) {
values = pickNonNull(values, []);
} else {
values = [values];
}
return this.whereIn(knexBuilder, col, values);
}
whereIn(knexBuilder, col, val) {
if (this.prefix === 'not') {
return knexBuilder.whereNotIn(col, val);
} else {
return knexBuilder.whereIn(col, val);
}
}
clone() {
const clone = super.clone();
clone.prefix = this.prefix;
return clone;
}
}
function pickNonNull(values, output) {
for (let i = 0, l = values.length; i < l; ++i) {
const val = values[i];
if (Array.isArray(val)) {
pickNonNull(val, output);
} else if (val !== null && val !== undefined) {
output.push(val);
}
}
return output;
}
module.exports = {
WhereInCompositeMsSqlOperation,
};
================================================
FILE: lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js
================================================
'use strict';
const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
const { isObject, asSingle } = require('../../../utils/objectUtils');
const { isKnexQueryBuilder } = require('../../../utils/knexUtils');
class WhereInCompositeOperation extends ObjectionToKnexConvertingOperation {
constructor(name, opt) {
super(name, opt);
this.prefix = this.opt.prefix || null;
}
onBuildKnex(knexBuilder, builder) {
const whereInArgs = buildWhereInArgs(builder.knex(), ...this.getKnexArgs(builder));
if (this.prefix === 'not') {
return knexBuilder.whereNotIn(...whereInArgs);
} else {
return knexBuilder.whereIn(...whereInArgs);
}
}
clone() {
const clone = super.clone();
clone.prefix = this.prefix;
return clone;
}
}
function buildWhereInArgs(knex, columns, values) {
if (isCompositeKey(columns)) {
return buildCompositeArgs(knex, columns, values);
} else {
return buildNonCompositeArgs(columns, values);
}
}
function isCompositeKey(columns) {
return Array.isArray(columns) && columns.length > 1;
}
function buildCompositeArgs(knex, columns, values) {
if (Array.isArray(values)) {
return buildCompositeValueArgs(columns, values);
} else {
return buildCompositeSubqueryArgs(knex, columns, values);
}
}
function buildCompositeValueArgs(columns, values) {
if (!Array.isArray(values[0])) {
return [columns, [values]];
} else {
return [columns, values];
}
}
function buildCompositeSubqueryArgs(knex, columns, subquery) {
const sql = `(${columns
.map((col) => {
// On older versions of knex, raw doesn't work
// with `??`. We use `?` for those.
if (isObject(col)) {
return '?';
} else {
return '??';
}
})
.join(',')})`;
return [knex.raw(sql, columns), subquery];
}
function buildNonCompositeArgs(columns, values) {
if (Array.isArray(values)) {
values = pickNonNull(values, []);
} else if (!isKnexQueryBuilder(values)) {
values = [values];
}
return [asSingle(columns), values];
}
function pickNonNull(values, output) {
for (let i = 0, l = values.length; i < l; ++i) {
const val = values[i];
if (Array.isArray(val)) {
pickNonNull(val, output);
} else if (val !== null && val !== undefined) {
output.push(val);
}
}
return output;
}
module.exports = {
WhereInCompositeOperation,
};
================================================
FILE: lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js
================================================
'use strict';
const { ObjectionToKnexConvertingOperation } = require('../ObjectionToKnexConvertingOperation');
const { isKnexQueryBuilder } = require('../../../utils/knexUtils');
const { asSingle } = require('../../../utils/objectUtils');
class WhereInCompositeSqliteOperation extends ObjectionToKnexConvertingOperation {
constructor(name, opt) {
super(name, opt);
this.prefix = this.opt.prefix || null;
}
onBuildKnex(knexBuilder, builder) {
const { method, args } = buildWhereArgs(...this.getKnexArgs(builder));
if (method === 'where') {
if (this.prefix === 'not') {
return knexBuilder.whereNot(...args);
} else {
return knexBuilder.where(...args);
}
} else {
if (this.prefix === 'not') {
return knexBuilder.whereNotIn(...args);
} else {
return knexBuilder.whereIn(...args);
}
}
}
clone() {
const clone = super.clone();
clone.prefix = this.prefix;
return clone;
}
}
function buildWhereArgs(columns, values) {
if (isCompositeKey(columns)) {
return buildCompositeArgs(columns, values);
} else {
return buildNonCompositeArgs(columns, values);
}
}
function isCompositeKey(columns) {
return Array.isArray(columns) && columns.length > 1;
}
function buildCompositeArgs(columns, values) {
if (!Array.isArray(values)) {
// If the `values` is not an array of values but a function or a subquery
// we have no way to implement this method.
throw new Error(`sqlite doesn't support multi-column where in clauses`);
}
// Sqlite doesn't support the `where in` syntax for multiple columns but
// we can emulate it using grouped `or` clauses.
return {
method: 'where',
args: [
(builder) => {
values.forEach((val) => {
builder.orWhere((builder) => {
columns.forEach((col, idx) => {
builder.andWhere(col, val[idx]);
});
});
});
},
],
};
}
function buildNonCompositeArgs(columns, values) {
if (Array.isArray(values)) {
values = pickNonNull(values, []);
} else if (!isKnexQueryBuilder(values)) {
values = [values];
}
return {
method: 'whereIn',
args: [asSingle(columns), values],
};
}
function pickNonNull(values, output) {
for (let i = 0, l = values.length; i < l; ++i) {
const val = values[i];
if (Array.isArray(val)) {
pickNonNull(val, output);
} else if (val !== null && val !== undefined) {
output.push(val);
}
}
return output;
}
module.exports = {
WhereInCompositeSqliteOperation,
};
================================================
FILE: lib/queryBuilder/parsers/jsonFieldExpressionParser.js
================================================
'use strict';
/*
* Generated by PEG.js 0.10.0.
*
* http://pegjs.org/
*/
function peg$subclass(child, parent) {
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
}
function peg$SyntaxError(message, expected, found, location) {
this.message = message;
this.expected = expected;
this.found = found;
this.location = location;
this.name = 'SyntaxError';
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, peg$SyntaxError);
}
}
peg$subclass(peg$SyntaxError, Error);
peg$SyntaxError.buildMessage = function (expected, found) {
var DESCRIBE_EXPECTATION_FNS = {
literal: function (expectation) {
return '"' + literalEscape(expectation.text) + '"';
},
class: function (expectation) {
var escapedParts = '',
i;
for (i = 0; i < expectation.parts.length; i++) {
escapedParts +=
expectation.parts[i] instanceof Array
? classEscape(expectation.parts[i][0]) + '-' + classEscape(expectation.parts[i][1])
: classEscape(expectation.parts[i]);
}
return '[' + (expectation.inverted ? '^' : '') + escapedParts + ']';
},
any: function (expectation) {
return 'any character';
},
end: function (expectation) {
return 'end of input';
},
other: function (expectation) {
return expectation.description;
},
};
function hex(ch) {
return ch.charCodeAt(0).toString(16).toUpperCase();
}
function literalEscape(s) {
return s
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\0/g, '\\0')
.replace(/\t/g, '\\t')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/[\x00-\x0F]/g, function (ch) {
return '\\x0' + hex(ch);
})
.replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) {
return '\\x' + hex(ch);
});
}
function classEscape(s) {
return s
.replace(/\\/g, '\\\\')
.replace(/\]/g, '\\]')
.replace(/\^/g, '\\^')
.replace(/-/g, '\\-')
.replace(/\0/g, '\\0')
.replace(/\t/g, '\\t')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/[\x00-\x0F]/g, function (ch) {
return '\\x0' + hex(ch);
})
.replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) {
return '\\x' + hex(ch);
});
}
function describeExpectation(expectation) {
return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
}
function describeExpected(expected) {
var descriptions = new Array(expected.length),
i,
j;
for (i = 0; i < expected.length; i++) {
descriptions[i] = describeExpectation(expected[i]);
}
descriptions.sort();
if (descriptions.length > 0) {
for (i = 1, j = 1; i < descriptions.length; i++) {
if (descriptions[i - 1] !== descriptions[i]) {
descriptions[j] = descriptions[i];
j++;
}
}
descriptions.length = j;
}
switch (descriptions.length) {
case 1:
return descriptions[0];
case 2:
return descriptions[0] + ' or ' + descriptions[1];
default:
return (
descriptions.slice(0, -1).join(', ') + ', or ' + descriptions[descriptions.length - 1]
);
}
}
function describeFound(found) {
return found ? '"' + literalEscape(found) + '"' : 'end of input';
}
return 'Expected ' + describeExpected(expected) + ' but ' + describeFound(found) + ' found.';
};
function peg$parse(input, options) {
options = options !== void 0 ? options : {};
var peg$FAILED = {},
peg$startRuleFunctions = { start: peg$parsestart },
peg$startRuleFunction = peg$parsestart,
peg$c0 = ':',
peg$c1 = peg$literalExpectation(':', false),
peg$c2 = function (column, refs) {
var access = [];
if (refs) {
var firstAccess = refs[1];
access = refs[2];
access.unshift(firstAccess);
}
return { columnName: column, access: access };
},
peg$c3 = '[',
peg$c4 = peg$literalExpectation('[', false),
peg$c5 = '"',
peg$c6 = peg$literalExpectation('"', false),
peg$c7 = "'",
peg$c8 = peg$literalExpectation("'", false),
peg$c9 = ']',
peg$c10 = peg$literalExpectation(']', false),
peg$c11 = function (key) {
return { type: 'object', ref: Array.isArray(key) ? key[1] : key };
},
peg$c12 = function (index) {
return { type: 'array', ref: parseInt(index, 10) };
},
peg$c13 = function (key) {
return { type: 'object', ref: key };
},
peg$c14 = '.',
peg$c15 = peg$literalExpectation('.', false),
peg$c16 = /^[^\][]/,
peg$c17 = peg$classExpectation([']', '['], true, false),
peg$c18 = function (chars) {
return chars.join('');
},
peg$c19 = /^[^:]/,
peg$c20 = peg$classExpectation([':'], true, false),
peg$c21 = /^[^"]/,
peg$c22 = peg$classExpectation(['"'], true, false),
peg$c23 = /^[^']/,
peg$c24 = peg$classExpectation(["'"], true, false),
peg$c25 = /^[^.\][]/,
peg$c26 = peg$classExpectation(['.', ']', '['], true, false),
peg$c27 = /^[0-9]/,
peg$c28 = peg$classExpectation([['0', '9']], false, false),
peg$c29 = function (digits) {
return digits.join('');
},
peg$currPos = 0,
peg$savedPos = 0,
peg$posDetailsCache = [{ line: 1, column: 1 }],
peg$maxFailPos = 0,
peg$maxFailExpected = [],
peg$silentFails = 0,
peg$result;
if ('startRule' in options) {
if (!(options.startRule in peg$startRuleFunctions)) {
throw new Error('Can\'t start parsing from rule "' + options.startRule + '".');
}
peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
}
function text() {
return input.substring(peg$savedPos, peg$currPos);
}
function location() {
return peg$computeLocation(peg$savedPos, peg$currPos);
}
function expected(description, location) {
location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos);
throw peg$buildStructuredError(
[peg$otherExpectation(description)],
input.substring(peg$savedPos, peg$currPos),
location,
);
}
function error(message, location) {
location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos);
throw peg$buildSimpleError(message, location);
}
function peg$literalExpectation(text, ignoreCase) {
return { type: 'literal', text: text, ignoreCase: ignoreCase };
}
function peg$classExpectation(parts, inverted, ignoreCase) {
return { type: 'class', parts: parts, inverted: inverted, ignoreCase: ignoreCase };
}
function peg$anyExpectation() {
return { type: 'any' };
}
function peg$endExpectation() {
return { type: 'end' };
}
function peg$otherExpectation(description) {
return { type: 'other', description: description };
}
function peg$computePosDetails(pos) {
var details = peg$posDetailsCache[pos],
p;
if (details) {
return details;
} else {
p = pos - 1;
while (!peg$posDetailsCache[p]) {
p--;
}
details = peg$posDetailsCache[p];
details = {
line: details.line,
column: details.column,
};
while (p < pos) {
if (input.charCodeAt(p) === 10) {
details.line++;
details.column = 1;
} else {
details.column++;
}
p++;
}
peg$posDetailsCache[pos] = details;
return details;
}
}
function peg$computeLocation(startPos, endPos) {
var startPosDetails = peg$computePosDetails(startPos),
endPosDetails = peg$computePosDetails(endPos);
return {
start: {
offset: startPos,
line: startPosDetails.line,
column: startPosDetails.column,
},
end: {
offset: endPos,
line: endPosDetails.line,
column: endPosDetails.column,
},
};
}
function peg$fail(expected) {
if (peg$currPos < peg$maxFailPos) {
return;
}
if (peg$currPos > peg$maxFailPos) {
peg$maxFailPos = peg$currPos;
peg$maxFailExpected = [];
}
peg$maxFailExpected.push(expected);
}
function peg$buildSimpleError(message, location) {
return new peg$SyntaxError(message, null, null, location);
}
function peg$buildStructuredError(expected, found, location) {
return new peg$SyntaxError(
peg$SyntaxError.buildMessage(expected, found),
expected,
found,
location,
);
}
function peg$parsestart() {
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = peg$parsestringWithoutColon();
if (s1 !== peg$FAILED) {
s2 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 58) {
s3 = peg$c0;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c1);
}
}
if (s3 !== peg$FAILED) {
s4 = peg$parsebracketIndexRef();
if (s4 === peg$FAILED) {
s4 = peg$parsebracketStringRef();
if (s4 === peg$FAILED) {
s4 = peg$parsecolonReference();
}
}
if (s4 !== peg$FAILED) {
s5 = [];
s6 = peg$parsebracketIndexRef();
if (s6 === peg$FAILED) {
s6 = peg$parsebracketStringRef();
if (s6 === peg$FAILED) {
s6 = peg$parsedotReference();
}
}
while (s6 !== peg$FAILED) {
s5.push(s6);
s6 = peg$parsebracketIndexRef();
if (s6 === peg$FAILED) {
s6 = peg$parsebracketStringRef();
if (s6 === peg$FAILED) {
s6 = peg$parsedotReference();
}
}
}
if (s5 !== peg$FAILED) {
s3 = [s3, s4, s5];
s2 = s3;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
if (s2 === peg$FAILED) {
s2 = null;
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c2(s1, s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsebracketStringRef() {
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 91) {
s1 = peg$c3;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c4);
}
}
if (s1 !== peg$FAILED) {
s2 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 34) {
s3 = peg$c5;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c6);
}
}
if (s3 !== peg$FAILED) {
s4 = peg$parsestringWithoutDoubleQuotes();
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 34) {
s5 = peg$c5;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c6);
}
}
if (s5 !== peg$FAILED) {
s3 = [s3, s4, s5];
s2 = s3;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
if (s2 === peg$FAILED) {
s2 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 39) {
s3 = peg$c7;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c8);
}
}
if (s3 !== peg$FAILED) {
s4 = peg$parsestringWithoutSingleQuotes();
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 39) {
s5 = peg$c7;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c8);
}
}
if (s5 !== peg$FAILED) {
s3 = [s3, s4, s5];
s2 = s3;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
if (s2 === peg$FAILED) {
s2 = peg$parsestringWithoutSquareBrackets();
}
}
if (s2 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 93) {
s3 = peg$c9;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c10);
}
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c11(s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsebracketIndexRef() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 91) {
s1 = peg$c3;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c4);
}
}
if (s1 !== peg$FAILED) {
s2 = peg$parseinteger();
if (s2 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 93) {
s3 = peg$c9;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c10);
}
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c12(s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsecolonReference() {
var s0, s1;
s0 = peg$currPos;
s1 = peg$parsestringWithoutSquareBracketsOrDots();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c13(s1);
}
s0 = s1;
return s0;
}
function peg$parsedotReference() {
var s0, s1, s2;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 46) {
s1 = peg$c14;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c15);
}
}
if (s1 !== peg$FAILED) {
s2 = peg$parsestringWithoutSquareBracketsOrDots();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c13(s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsestringWithoutSquareBrackets() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = [];
if (peg$c16.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c17);
}
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
if (peg$c16.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c17);
}
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c18(s1);
}
s0 = s1;
return s0;
}
function peg$parsestringWithoutColon() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = [];
if (peg$c19.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c20);
}
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
if (peg$c19.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c20);
}
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c18(s1);
}
s0 = s1;
return s0;
}
function peg$parsestringWithoutDoubleQuotes() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = [];
if (peg$c21.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c22);
}
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
if (peg$c21.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c22);
}
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c18(s1);
}
s0 = s1;
return s0;
}
function peg$parsestringWithoutSingleQuotes() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = [];
if (peg$c23.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c24);
}
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
if (peg$c23.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c24);
}
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c18(s1);
}
s0 = s1;
return s0;
}
function peg$parsestringWithoutSquareBracketsOrDots() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = [];
if (peg$c25.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c26);
}
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
if (peg$c25.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c26);
}
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c18(s1);
}
s0 = s1;
return s0;
}
function peg$parseinteger() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = [];
if (peg$c27.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c28);
}
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
if (peg$c27.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c28);
}
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c29(s1);
}
s0 = s1;
return s0;
}
peg$result = peg$startRuleFunction();
if (peg$result !== peg$FAILED && peg$currPos === input.length) {
return peg$result;
} else {
if (peg$result !== peg$FAILED && peg$currPos < input.length) {
peg$fail(peg$endExpectation());
}
throw peg$buildStructuredError(
peg$maxFailExpected,
peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
peg$maxFailPos < input.length
? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
: peg$computeLocation(peg$maxFailPos, peg$maxFailPos),
);
}
}
module.exports = {
SyntaxError: peg$SyntaxError,
parse: peg$parse,
};
================================================
FILE: lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs
================================================
/**
* Parser for parsing field expressions.
*
* Syntax: [:]
*
* e.g. `Person.jsonColumnName:details.names[1]` would refer to value `'Second'`
* in column `Person.jsonColumnName` which has
* `{ details: { names: ['First', 'Second', 'Last'] } }` object stored in it.
*
* First part is compatible with column references used in
* knex e.g. "MyFancyTable.tributeToThBestColumnNameEver".
*
* Second part describes a path to an attribute inside the referred column.
* It is optional and it always starts with colon which follows directly with
* first path element. e.g. `Table.jsonObjectColumnName:jsonFieldName` or
* `Table.jsonArrayColumn:[321]`.
*
* Syntax supports `[]` and `.` flavors of reference
* to json keys / array indexes:
*
* e.g. both `Table.myColumn:[1][3]` and `Table.myColumn:1.3` would access correctly
* both of the following objects `[null, [null,null,null, "I was accessed"]]` and
* `{ "1": { "3" : "I was accessed" } }`
*
* Caveats when using special characters in keys:
*
* 1. `objectColumn.key` This is the most common syntax, good if you are
* not using dots or square brackets `[]` in your json object key name.
* 2. Keys containing dots `objectColumn:[keywith.dots]` Column `{ "keywith.dots" : "I was referred" }`
* 3. Keys containing square brackets `column['[]']` `{ "[]" : "This is getting ridiculous..." }`
* 4. Keys containing square brackets and quotes
* `objectColumn:['Double."Quote".[]']` and `objectColumn:["Sinlge.'Quote'.[]"]`
* Column `{ "Double.\"Quote\".[]" : "I was referred", "Sinlge.'Quote'.[]" : "Mee too!" }`
* 99. Keys containing dots, square brackets, single quotes and double quotes in one json key is
* not currently supported
*
* For compiling this to parser run `pegjs JsonFieldExpressionParser.pegjs` which generates
* the `JsonFieldExpressionParser.js`
*
* For development there is nice page for interactively hacking parser code
* http://pegjs.org/online
*/
start =
column:stringWithoutColon
refs:(':'
(bracketIndexRef / bracketStringRef / colonReference)
(bracketIndexRef / bracketStringRef / dotReference)*
)?
{
var access = [];
if (refs) {
var firstAccess = refs[1];
access = refs[2];
access.unshift(firstAccess);
}
return { columnName: column, access: access };
}
bracketStringRef =
'['
key:(
'"' stringWithoutDoubleQuotes '"' /
"'" stringWithoutSingleQuotes "'" /
stringWithoutSquareBrackets
)
']'
{ return { type: 'object', ref: Array.isArray(key) ? key[1] : key }; }
bracketIndexRef =
'[' index:integer ']'
{ return { type: 'array', ref: parseInt(index, 10) }; }
colonReference =
key:stringWithoutSquareBracketsOrDots
{ return { type: 'object', ref: key }; }
dotReference =
'.' key:stringWithoutSquareBracketsOrDots
{ return { type: 'object', ref: key }; }
stringWithoutSquareBrackets =
chars:([^\x5D\x5B])+ { return chars.join(""); }
stringWithoutColon =
chars:([^:])+ { return chars.join(""); }
stringWithoutDoubleQuotes =
chars:([^"])+ { return chars.join(""); }
stringWithoutSingleQuotes =
chars:([^'])+ { return chars.join(""); }
stringWithoutSquareBracketsOrDots =
chars:([^.\x5D\x5B])+ { return chars.join(""); }
integer = digits:[0-9]+ { return digits.join(""); }
================================================
FILE: lib/queryBuilder/parsers/relationExpressionParser.js
================================================
/*
* Generated by PEG.js 0.10.0.
*
* http://pegjs.org/
*/
'use strict';
function peg$subclass(child, parent) {
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
}
function peg$SyntaxError(message, expected, found, location) {
this.message = message;
this.expected = expected;
this.found = found;
this.location = location;
this.name = 'SyntaxError';
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, peg$SyntaxError);
}
}
peg$subclass(peg$SyntaxError, Error);
peg$SyntaxError.buildMessage = function (expected, found) {
var DESCRIBE_EXPECTATION_FNS = {
literal: function (expectation) {
return '"' + literalEscape(expectation.text) + '"';
},
class: function (expectation) {
var escapedParts = '',
i;
for (i = 0; i < expectation.parts.length; i++) {
escapedParts +=
expectation.parts[i] instanceof Array
? classEscape(expectation.parts[i][0]) + '-' + classEscape(expectation.parts[i][1])
: classEscape(expectation.parts[i]);
}
return '[' + (expectation.inverted ? '^' : '') + escapedParts + ']';
},
any: function (expectation) {
return 'any character';
},
end: function (expectation) {
return 'end of input';
},
other: function (expectation) {
return expectation.description;
},
};
function hex(ch) {
return ch.charCodeAt(0).toString(16).toUpperCase();
}
function literalEscape(s) {
return s
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\0/g, '\\0')
.replace(/\t/g, '\\t')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/[\x00-\x0F]/g, function (ch) {
return '\\x0' + hex(ch);
})
.replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) {
return '\\x' + hex(ch);
});
}
function classEscape(s) {
return s
.replace(/\\/g, '\\\\')
.replace(/\]/g, '\\]')
.replace(/\^/g, '\\^')
.replace(/-/g, '\\-')
.replace(/\0/g, '\\0')
.replace(/\t/g, '\\t')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/[\x00-\x0F]/g, function (ch) {
return '\\x0' + hex(ch);
})
.replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) {
return '\\x' + hex(ch);
});
}
function describeExpectation(expectation) {
return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
}
function describeExpected(expected) {
var descriptions = new Array(expected.length),
i,
j;
for (i = 0; i < expected.length; i++) {
descriptions[i] = describeExpectation(expected[i]);
}
descriptions.sort();
if (descriptions.length > 0) {
for (i = 1, j = 1; i < descriptions.length; i++) {
if (descriptions[i - 1] !== descriptions[i]) {
descriptions[j] = descriptions[i];
j++;
}
}
descriptions.length = j;
}
switch (descriptions.length) {
case 1:
return descriptions[0];
case 2:
return descriptions[0] + ' or ' + descriptions[1];
default:
return (
descriptions.slice(0, -1).join(', ') + ', or ' + descriptions[descriptions.length - 1]
);
}
}
function describeFound(found) {
return found ? '"' + literalEscape(found) + '"' : 'end of input';
}
return 'Expected ' + describeExpected(expected) + ' but ' + describeFound(found) + ' found.';
};
function peg$parse(input, options) {
options = options !== void 0 ? options : {};
var peg$FAILED = {},
peg$startRuleFunctions = { start: peg$parsestart },
peg$startRuleFunction = peg$parsestart,
peg$c0 = function (expr) {
const node = newNode();
if (expr.$name === '*') {
node.$allRecursive = true;
} else {
assertDuplicateRelation(node, expr);
node[expr.$name] = expr;
node.$childNames.push(expr.$name);
}
return node;
},
peg$c1 = function (list) {
const node = newNode();
list.forEach((expr) => {
assertDuplicateRelation(node, expr);
node[expr.$name] = expr;
node.$childNames.push(expr.$name);
});
return node;
},
peg$c2 = function (name, args, alias, list) {
const node = newNode();
node.$name = alias || name;
node.$relation = name;
node.$modify = args || [];
list.forEach((expr) => {
assertDuplicateRelation(node, expr);
node[expr.$name] = expr;
node.$childNames.push(expr.$name);
});
return node;
},
peg$c3 = function (name, args, alias, expr) {
const node = newNode();
node.$name = alias || name;
node.$relation = name;
node.$modify = args || [];
if (expr) {
const match = /^\^(\d*)$/.exec(expr.$name);
if (match) {
if (match[1]) {
node.$recursive = parseInt(match[1], 10);
} else {
node.$recursive = true;
}
} else if (expr.$name === '*') {
node.$allRecursive = true;
} else {
assertDuplicateRelation(node, expr);
node[expr.$name] = expr;
node.$childNames.push(expr.$name);
}
}
return node;
},
peg$c4 = 'as',
peg$c5 = peg$literalExpectation('as', false),
peg$c6 = function (alias) {
return alias;
},
peg$c7 = function (name) {
return name.join('');
},
peg$c8 = /^[^[\](),. \t\r\n]/,
peg$c9 = peg$classExpectation(
['[', ']', '(', ')', ',', '.', ' ', '\t', '\r', '\n'],
true,
false,
),
peg$c10 = '(',
peg$c11 = peg$literalExpectation('(', false),
peg$c12 = ')',
peg$c13 = peg$literalExpectation(')', false),
peg$c14 = function (args) {
return args;
},
peg$c15 = ',',
peg$c16 = peg$literalExpectation(',', false),
peg$c17 = function (arg) {
return arg;
},
peg$c18 = /^[ \t\r\n]/,
peg$c19 = peg$classExpectation([' ', '\t', '\r', '\n'], false, false),
peg$c20 = '.',
peg$c21 = peg$literalExpectation('.', false),
peg$c22 = function (list) {
return list;
},
peg$c23 = '[',
peg$c24 = peg$literalExpectation('[', false),
peg$c25 = ']',
peg$c26 = peg$literalExpectation(']', false),
peg$c27 = function (items) {
return items;
},
peg$c28 = function (expr) {
return expr;
},
peg$c29 = function (sub) {
return sub;
},
peg$currPos = 0,
peg$savedPos = 0,
peg$posDetailsCache = [{ line: 1, column: 1 }],
peg$maxFailPos = 0,
peg$maxFailExpected = [],
peg$silentFails = 0,
peg$result;
if ('startRule' in options) {
if (!(options.startRule in peg$startRuleFunctions)) {
throw new Error('Can\'t start parsing from rule "' + options.startRule + '".');
}
peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
}
function text() {
return input.substring(peg$savedPos, peg$currPos);
}
function location() {
return peg$computeLocation(peg$savedPos, peg$currPos);
}
function expected(description, location) {
location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos);
throw peg$buildStructuredError(
[peg$otherExpectation(description)],
input.substring(peg$savedPos, peg$currPos),
location,
);
}
function error(message, location) {
location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos);
throw peg$buildSimpleError(message, location);
}
function peg$literalExpectation(text, ignoreCase) {
return { type: 'literal', text: text, ignoreCase: ignoreCase };
}
function peg$classExpectation(parts, inverted, ignoreCase) {
return { type: 'class', parts: parts, inverted: inverted, ignoreCase: ignoreCase };
}
function peg$anyExpectation() {
return { type: 'any' };
}
function peg$endExpectation() {
return { type: 'end' };
}
function peg$otherExpectation(description) {
return { type: 'other', description: description };
}
function peg$computePosDetails(pos) {
var details = peg$posDetailsCache[pos],
p;
if (details) {
return details;
} else {
p = pos - 1;
while (!peg$posDetailsCache[p]) {
p--;
}
details = peg$posDetailsCache[p];
details = {
line: details.line,
column: details.column,
};
while (p < pos) {
if (input.charCodeAt(p) === 10) {
details.line++;
details.column = 1;
} else {
details.column++;
}
p++;
}
peg$posDetailsCache[pos] = details;
return details;
}
}
function peg$computeLocation(startPos, endPos) {
var startPosDetails = peg$computePosDetails(startPos),
endPosDetails = peg$computePosDetails(endPos);
return {
start: {
offset: startPos,
line: startPosDetails.line,
column: startPosDetails.column,
},
end: {
offset: endPos,
line: endPosDetails.line,
column: endPosDetails.column,
},
};
}
function peg$fail(expected) {
if (peg$currPos < peg$maxFailPos) {
return;
}
if (peg$currPos > peg$maxFailPos) {
peg$maxFailPos = peg$currPos;
peg$maxFailExpected = [];
}
peg$maxFailExpected.push(expected);
}
function peg$buildSimpleError(message, location) {
return new peg$SyntaxError(message, null, null, location);
}
function peg$buildStructuredError(expected, found, location) {
return new peg$SyntaxError(
peg$SyntaxError.buildMessage(expected, found),
expected,
found,
location,
);
}
function peg$parsestart() {
var s0, s1;
s0 = peg$currPos;
s1 = peg$parseexpression();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c0(s1);
}
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
s1 = peg$parselistExpression();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c1(s1);
}
s0 = s1;
}
return s0;
}
function peg$parseexpression() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = peg$parsename();
if (s1 !== peg$FAILED) {
s2 = peg$parseargs();
if (s2 === peg$FAILED) {
s2 = null;
}
if (s2 !== peg$FAILED) {
s3 = peg$parsealias();
if (s3 === peg$FAILED) {
s3 = null;
}
if (s3 !== peg$FAILED) {
s4 = peg$parsesubListExpression();
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c2(s1, s2, s3, s4);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
s1 = peg$parsename();
if (s1 !== peg$FAILED) {
s2 = peg$parseargs();
if (s2 === peg$FAILED) {
s2 = null;
}
if (s2 !== peg$FAILED) {
s3 = peg$parsealias();
if (s3 === peg$FAILED) {
s3 = null;
}
if (s3 !== peg$FAILED) {
s4 = peg$parsesubExpression();
if (s4 === peg$FAILED) {
s4 = null;
}
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c3(s1, s2, s3, s4);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
}
return s0;
}
function peg$parsealias() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsews();
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parsews();
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c4) {
s2 = peg$c4;
peg$currPos += 2;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c5);
}
}
if (s2 !== peg$FAILED) {
s3 = [];
s4 = peg$parsews();
if (s4 !== peg$FAILED) {
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$parsews();
}
} else {
s3 = peg$FAILED;
}
if (s3 !== peg$FAILED) {
s4 = peg$parsename();
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c6(s4);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsename() {
var s0, s1, s2;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsechar();
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parsechar();
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c7(s1);
}
s0 = s1;
return s0;
}
function peg$parsechar() {
var s0;
if (peg$c8.test(input.charAt(peg$currPos))) {
s0 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c9);
}
}
return s0;
}
function peg$parseargs() {
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsews();
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parsews();
}
if (s1 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 40) {
s2 = peg$c10;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c11);
}
}
if (s2 !== peg$FAILED) {
s3 = [];
s4 = peg$parseargListItem();
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$parseargListItem();
}
if (s3 !== peg$FAILED) {
s4 = [];
s5 = peg$parsews();
while (s5 !== peg$FAILED) {
s4.push(s5);
s5 = peg$parsews();
}
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 41) {
s5 = peg$c12;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c13);
}
}
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c14(s3);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parseargListItem() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsews();
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parsews();
}
if (s1 !== peg$FAILED) {
s2 = peg$parsename();
if (s2 !== peg$FAILED) {
s3 = [];
s4 = peg$parsews();
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$parsews();
}
if (s3 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 44) {
s4 = peg$c15;
peg$currPos++;
} else {
s4 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c16);
}
}
if (s4 === peg$FAILED) {
s4 = null;
}
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c17(s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsews() {
var s0;
if (peg$c18.test(input.charAt(peg$currPos))) {
s0 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c19);
}
}
return s0;
}
function peg$parsesubListExpression() {
var s0, s1, s2, s3;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsews();
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parsews();
}
if (s1 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 46) {
s2 = peg$c20;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c21);
}
}
if (s2 !== peg$FAILED) {
s3 = peg$parselistExpression();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c22(s3);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parselistExpression() {
var s0, s1, s2, s3, s4, s5, s6, s7;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsews();
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parsews();
}
if (s1 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 91) {
s2 = peg$c23;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c24);
}
}
if (s2 !== peg$FAILED) {
s3 = [];
s4 = peg$parselistExpressionItem();
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$parselistExpressionItem();
}
if (s3 !== peg$FAILED) {
s4 = [];
s5 = peg$parsews();
while (s5 !== peg$FAILED) {
s4.push(s5);
s5 = peg$parsews();
}
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 93) {
s5 = peg$c25;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c26);
}
}
if (s5 !== peg$FAILED) {
s6 = [];
s7 = peg$parsews();
while (s7 !== peg$FAILED) {
s6.push(s7);
s7 = peg$parsews();
}
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c27(s3);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parselistExpressionItem() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsews();
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parsews();
}
if (s1 !== peg$FAILED) {
s2 = peg$parseexpression();
if (s2 !== peg$FAILED) {
s3 = [];
s4 = peg$parsews();
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$parsews();
}
if (s3 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 44) {
s4 = peg$c15;
peg$currPos++;
} else {
s4 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c16);
}
}
if (s4 === peg$FAILED) {
s4 = null;
}
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c28(s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsesubExpression() {
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = [];
s2 = peg$parsews();
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parsews();
}
if (s1 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 46) {
s2 = peg$c20;
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
peg$fail(peg$c21);
}
}
if (s2 !== peg$FAILED) {
s3 = [];
s4 = peg$parsews();
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$parsews();
}
if (s3 !== peg$FAILED) {
s4 = peg$parseexpression();
if (s4 !== peg$FAILED) {
s5 = [];
s6 = peg$parsews();
while (s6 !== peg$FAILED) {
s5.push(s6);
s6 = peg$parsews();
}
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c29(s4);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
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`,
);
// TODO: enable for v2.0.
// const err = new Error();
// err.duplicateRelationName = expr.$name;
// throw err;
}
}
function newNode() {
return {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: [],
};
}
peg$result = peg$startRuleFunction();
if (peg$result !== peg$FAILED && peg$currPos === input.length) {
return peg$result;
} else {
if (peg$result !== peg$FAILED && peg$currPos < input.length) {
peg$fail(peg$endExpectation());
}
throw peg$buildStructuredError(
peg$maxFailExpected,
peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
peg$maxFailPos < input.length
? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
: peg$computeLocation(peg$maxFailPos, peg$maxFailPos),
);
}
}
module.exports = {
SyntaxError: peg$SyntaxError,
parse: peg$parse,
};
================================================
FILE: lib/queryBuilder/parsers/relationExpressionParser.pegjs
================================================
{
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`);
// TODO: enable for v2.0.
// const err = new Error();
// err.duplicateRelationName = expr.$name;
// throw err;
}
}
function newNode() {
return {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: []
};
}
}
start =
expr:expression {
const node = newNode()
if (expr.$name === '*') {
node.$allRecursive = true;
} else {
assertDuplicateRelation(node, expr);
node[expr.$name] = expr;
node.$childNames.push(expr.$name)
}
return node;
}
/
list:listExpression {
const node = newNode()
list.forEach(expr => {
assertDuplicateRelation(node, expr);
node[expr.$name] = expr;
node.$childNames.push(expr.$name)
});
return node;
}
expression =
name:name args:args? alias:alias? list:subListExpression {
const node = newNode()
node.$name = alias || name;
node.$relation = name;
node.$modify = args || [];
list.forEach(expr => {
assertDuplicateRelation(node, expr);
node[expr.$name] = expr;
node.$childNames.push(expr.$name)
});
return node;
}
/
name:name args:args? alias:alias? expr:subExpression? {
const node = newNode()
node.$name = alias || name;
node.$relation = name;
node.$modify = args || [];
if (expr) {
const match = /^\^(\d*)$/.exec(expr.$name);
if (match) {
if (match[1]) {
node.$recursive = parseInt(match[1], 10);
} else {
node.$recursive = true;
}
} else if (expr.$name === '*') {
node.$allRecursive = true;
} else {
assertDuplicateRelation(node, expr);
node[expr.$name] = expr;
node.$childNames.push(expr.$name)
}
}
return node;
}
alias =
ws+ "as" ws+ alias:name {
return alias;
}
name =
name:char+ {
return name.join('');
}
char =
[^\[\]\(\),\. \t\r\n]
args =
ws* "(" args:argListItem* ws* ")" {
return args;
}
argListItem =
ws* arg:name ws* ","? {
return arg;
}
ws =
[ \t\r\n]
subListExpression =
ws* "." list:listExpression {
return list;
}
listExpression =
ws* "[" items:listExpressionItem* ws* "]" ws* {
return items;
}
listExpressionItem =
ws* expr:expression ws* ","? {
return expr;
}
subExpression =
ws* "." ws* sub:expression ws* {
return sub;
}
================================================
FILE: lib/queryBuilder/transformations/CompositeQueryTransformation.js
================================================
'use strict';
const { QueryTransformation } = require('./QueryTransformation');
class CompositeQueryTransformation extends QueryTransformation {
constructor(transformations) {
super();
this.transformations = transformations;
}
onConvertQueryBuilderBase(item, builder) {
for (const transformation of this.transformations) {
item = transformation.onConvertQueryBuilderBase(item, builder);
}
return item;
}
}
module.exports = {
CompositeQueryTransformation,
};
================================================
FILE: lib/queryBuilder/transformations/QueryTransformation.js
================================================
'use strict';
class QueryTransformation {
onConvertQueryBuilderBase(item, builder) {
return item;
}
}
module.exports = {
QueryTransformation,
};
================================================
FILE: lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js
================================================
'use strict';
const { QueryTransformation } = require('./QueryTransformation');
const { isMySql } = require('../../utils/knexUtils');
/**
* Mysql doesn't allow queries like this:
*
* update foo set bar = 1 where id in (select id from foo)
*
* because the subquery is for the same table `foo` as the parent update query.
* The same goes for delete queries too.
*
* This transformation wraps those subqueries like this:
*
* update foo set bar = 1 where id in (select * from (select id from foo))
*/
class WrapMysqlModifySubqueryTransformation extends QueryTransformation {
onConvertQueryBuilderBase(query, parentQuery) {
const knex = parentQuery.unsafeKnex();
// Cannot detect anything if, for whatever reason, a knex instance
// or a transaction is not registered at this point.
if (!knex) {
return query;
}
// This transformation only applies to MySQL.
if (!isMySql(knex)) {
return query;
}
// This transformation only applies to update and delete queries.
if (!parentQuery.isUpdate() && !parentQuery.isDelete()) {
return query;
}
// If the subquery is for another table and the query doesn't join the
// parent query's table, we're good to go.
if (
parentQuery.tableName() !== query.tableName() &&
!hasJoinsToTable(query, parentQuery.tableName())
) {
return query;
}
return query.modelClass().query().from(query.as('mysql_subquery_fix'));
}
}
function hasJoinsToTable(query, tableName) {
let found = false;
query.forEachOperation(query.constructor.JoinSelector, (op) => {
if (op.args[0] === tableName) {
found = true;
return false;
}
});
return found;
}
module.exports = {
WrapMysqlModifySubqueryTransformation,
};
================================================
FILE: lib/queryBuilder/transformations/index.js
================================================
'use strict';
const { CompositeQueryTransformation } = require('./CompositeQueryTransformation');
const {
WrapMysqlModifySubqueryTransformation,
} = require('./WrapMysqlModifySubqueryTransformation');
const transformation = new CompositeQueryTransformation([
new WrapMysqlModifySubqueryTransformation(),
]);
module.exports = {
transformation,
};
================================================
FILE: lib/relations/Relation.js
================================================
'use strict';
const { RelationProperty } = require('./RelationProperty');
const { RelationFindOperation } = require('./RelationFindOperation');
const { RelationUpdateOperation } = require('./RelationUpdateOperation');
const { RelationDeleteOperation } = require('./RelationDeleteOperation');
const { resolveModel } = require('../utils/resolveModel');
const { get, isFunction } = require('../utils/objectUtils');
const { mapAfterAllReturn } = require('../utils/promiseUtils');
const { createModifier } = require('../utils/createModifier');
class Relation {
constructor(relationName, OwnerClass) {
this.name = relationName;
this.ownerModelClass = OwnerClass;
this.relatedModelClass = null;
this.ownerProp = null;
this.relatedProp = null;
this.joinTableModelClass = null;
this.joinTableOwnerProp = null;
this.joinTableRelatedProp = null;
this.joinTableBeforeInsert = null;
this.joinTableExtras = [];
this.modify = null;
this.beforeInsert = null;
}
setMapping(mapping) {
let ctx = {
name: this.name,
mapping,
ownerModelClass: this.ownerModelClass,
relatedModelClass: null,
relatedProp: null,
ownerProp: null,
modify: null,
beforeInsert: null,
forbiddenMappingProperties: this.forbiddenMappingProperties,
createError: (msg) => this.createError(msg),
};
ctx = checkForbiddenProperties(ctx);
ctx = checkOwnerModelClass(ctx);
ctx = checkRelatedModelClass(ctx);
ctx = resolveRelatedModelClass(ctx);
ctx = checkRelation(ctx);
ctx = createJoinProperties(ctx);
ctx = parseModify(ctx);
ctx = parseBeforeInsert(ctx);
this.relatedModelClass = ctx.relatedModelClass;
this.ownerProp = ctx.ownerProp;
this.relatedProp = ctx.relatedProp;
this.modify = ctx.modify;
this.beforeInsert = ctx.beforeInsert;
}
get forbiddenMappingProperties() {
return ['join.through'];
}
get joinTable() {
return this.joinTableModelClass ? this.joinTableModelClass.getTableName() : null;
}
get joinModelClass() {
return this.getJoinModelClass(this.ownerModelClass.knex());
}
getJoinModelClass(knex) {
return this.joinTableModelClass && knex !== this.joinTableModelClass.knex()
? this.joinTableModelClass.bindKnex(knex)
: this.joinTableModelClass;
}
isOneToOne() {
return false;
}
clone() {
const relation = new this.constructor(this.name, this.ownerModelClass);
relation.relatedModelClass = this.relatedModelClass;
relation.ownerProp = this.ownerProp;
relation.relatedProp = this.relatedProp;
relation.modify = this.modify;
relation.beforeInsert = this.beforeInsert;
relation.joinTableModelClass = this.joinTableModelClass;
relation.joinTableOwnerProp = this.joinTableOwnerProp;
relation.joinTableRelatedProp = this.joinTableRelatedProp;
relation.joinTableBeforeInsert = this.joinTableBeforeInsert;
relation.joinTableExtras = this.joinTableExtras;
return relation;
}
bindKnex(knex) {
const bound = this.clone();
bound.relatedModelClass = this.relatedModelClass.bindKnex(knex);
bound.ownerModelClass = this.ownerModelClass.bindKnex(knex);
if (this.joinTableModelClass) {
bound.joinTableModelClass = this.joinTableModelClass.bindKnex(knex);
}
return bound;
}
findQuery(builder, owner) {
const relatedRefs = this.relatedProp.refs(builder);
owner.buildFindQuery(builder, this, relatedRefs);
return this.applyModify(builder);
}
applyModify(builder) {
try {
return builder.modify(this.modify);
} catch (err) {
if (err instanceof this.relatedModelClass.ModifierNotFoundError) {
throw this.createError(err.message);
} else {
throw err;
}
}
}
join(
builder,
{
joinOperation = 'join',
relatedTableAlias = builder.tableRefFor(this.relatedModelClass),
relatedJoinSelectQuery = this.relatedModelClass.query().childQueryOf(builder),
relatedTable = builder.tableNameFor(this.relatedModelClass),
ownerTable = builder.tableRefFor(this.ownerModelClass),
} = {},
) {
let relatedJoinSelect = this.applyModify(relatedJoinSelectQuery).as(relatedTableAlias);
if (relatedJoinSelect.isSelectAll()) {
// No need to join a subquery if the query is `select * from "RelatedTable"`.
relatedJoinSelect = aliasedTableName(relatedTable, relatedTableAlias);
}
return builder[joinOperation](relatedJoinSelect, (join) => {
const relatedProp = this.relatedProp;
const ownerProp = this.ownerProp;
relatedProp.forEach((i) => {
const relatedRef = relatedProp.ref(builder, i).table(relatedTableAlias);
const ownerRef = ownerProp.ref(builder, i).table(ownerTable);
join.on(relatedRef, ownerRef);
});
});
}
find(_, owner) {
return new RelationFindOperation('find', {
relation: this,
owner,
});
}
insert(_, owner) {
/* istanbul ignore next */
throw this.createError('not implemented');
}
update(_, owner) {
return new RelationUpdateOperation('update', {
relation: this,
owner,
});
}
patch(_, owner) {
return new RelationUpdateOperation('patch', {
relation: this,
owner,
modelOptions: { patch: true },
});
}
delete(_, owner) {
return new RelationDeleteOperation('delete', {
relation: this,
owner,
});
}
relate() {
/* istanbul ignore next */
throw this.createError('not implemented');
}
unrelate() {
/* istanbul ignore next */
throw this.createError('not implemented');
}
hasRelateProp(model) {
return model.$hasProps(this.relatedProp.props);
}
setRelateProp(model, values) {
this.relatedProp.forEach((i) => {
this.relatedProp.setProp(model, i, values[i]);
});
}
executeBeforeInsert(models, queryContext, result) {
return mapAfterAllReturn(models, (model) => this.beforeInsert(model, queryContext), result);
}
createError(message) {
if (this.ownerModelClass && this.ownerModelClass.name && this.name) {
return new Error(`${this.ownerModelClass.name}.relationMappings.${this.name}: ${message}`);
} else {
return new Error(`${this.constructor.name}: ${message}`);
}
}
}
Object.defineProperties(Relation, {
isObjectionRelationClass: {
enumerable: false,
writable: false,
value: true,
},
});
Object.defineProperties(Relation.prototype, {
isObjectionRelation: {
enumerable: false,
writable: false,
value: true,
},
});
function checkForbiddenProperties(ctx) {
ctx.forbiddenMappingProperties.forEach((prop) => {
if (get(ctx.mapping, prop.split('.')) !== undefined) {
throw ctx.createError(`Property ${prop} is not supported for this relation type.`);
}
});
return ctx;
}
function checkOwnerModelClass(ctx) {
if (!isFunction(ctx.ownerModelClass) || !ctx.ownerModelClass.isObjectionModelClass) {
throw ctx.createError(`Relation's owner is not a subclass of Model`);
}
return ctx;
}
function checkRelatedModelClass(ctx) {
if (!ctx.mapping.modelClass) {
throw ctx.createError('modelClass is not defined');
}
return ctx;
}
function resolveRelatedModelClass(ctx) {
let relatedModelClass;
try {
relatedModelClass = resolveModel(
ctx.mapping.modelClass,
ctx.ownerModelClass.modelPaths,
'modelClass',
);
} catch (err) {
throw ctx.createError(err.message);
}
return Object.assign(ctx, { relatedModelClass });
}
function checkRelation(ctx) {
if (!ctx.mapping.relation) {
throw ctx.createError('relation is not defined');
}
if (!isFunction(ctx.mapping.relation) || !ctx.mapping.relation.isObjectionRelationClass) {
throw ctx.createError('relation is not a subclass of Relation');
}
return ctx;
}
function createJoinProperties(ctx) {
const mapping = ctx.mapping;
if (!mapping.join || !mapping.join.from || !mapping.join.to) {
throw ctx.createError(
'join must be an object that maps the columns of the related models together. For example: {from: "SomeTable.id", to: "SomeOtherTable.someModelId"}',
);
}
const fromProp = createRelationProperty(ctx, mapping.join.from, 'join.from');
const toProp = createRelationProperty(ctx, mapping.join.to, 'join.to');
let ownerProp;
let relatedProp;
if (fromProp.modelClass.getTableName() === ctx.ownerModelClass.getTableName()) {
ownerProp = fromProp;
relatedProp = toProp;
} else if (toProp.modelClass.getTableName() === ctx.ownerModelClass.getTableName()) {
ownerProp = toProp;
relatedProp = fromProp;
} else {
throw ctx.createError('join: either `from` or `to` must point to the owner model table.');
}
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.`,
);
}
if (relatedProp.modelClass.getTableName() !== ctx.relatedModelClass.getTableName()) {
throw ctx.createError('join: either `from` or `to` must point to the related model table.');
}
return Object.assign(ctx, { ownerProp, relatedProp });
}
function createRelationProperty(ctx, refString, propName) {
try {
return new RelationProperty(refString, (table) => {
return [ctx.ownerModelClass, ctx.relatedModelClass].find((it) => it.getTableName() === table);
});
} 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`,
);
} else if (err instanceof RelationProperty.InvalidReferenceError) {
throw ctx.createError(
`${propName} must have format TableName.columnName. For example "SomeTable.id" or in case of composite key ["SomeTable.a", "SomeTable.b"].`,
);
} else {
throw err;
}
}
}
function parseModify(ctx) {
const mapping = ctx.mapping;
const modifier = mapping.modify || mapping.filter;
const modify =
modifier &&
createModifier({
modifier,
modelClass: ctx.relatedModelClass,
});
return Object.assign(ctx, { modify });
}
function parseBeforeInsert(ctx) {
let beforeInsert;
if (isFunction(ctx.mapping.beforeInsert)) {
beforeInsert = ctx.mapping.beforeInsert;
} else {
beforeInsert = (model) => model;
}
return Object.assign(ctx, { beforeInsert });
}
function aliasedTableName(tableName, alias) {
if (tableName === alias) {
return tableName;
} else {
return `${tableName} as ${alias}`;
}
}
module.exports = {
Relation,
};
================================================
FILE: lib/relations/RelationDeleteOperation.js
================================================
'use strict';
const { DeleteOperation } = require('../queryBuilder/operations/DeleteOperation');
const { RelationFindOperation } = require('./RelationFindOperation');
class RelationDeleteOperation extends DeleteOperation {
constructor(name, opt) {
super(name, opt);
this.relation = opt.relation;
this.owner = opt.owner;
}
onBuild(builder) {
super.onBuild(builder);
this.relation.findQuery(builder, this.owner);
}
toFindOperation() {
return new RelationFindOperation('find', {
relation: this.relation,
owner: this.owner,
});
}
}
module.exports = {
RelationDeleteOperation,
};
================================================
FILE: lib/relations/RelationFindOperation.js
================================================
'use strict';
const { FindOperation } = require('../queryBuilder/operations/FindOperation');
class RelationFindOperation extends FindOperation {
constructor(name, opt) {
super(name, opt);
this.relation = opt.relation;
this.owner = opt.owner;
this.alwaysReturnArray = false;
this.assignResultToOwner = true;
this.relationProperty = opt.relationProperty || this.relation.name;
this.omitProps = [];
this.alias = null;
}
onBuild(builder) {
this.maybeApplyAlias(builder);
this.relation.findQuery(builder, this.owner);
if (this.assignResultToOwner && this.owner.isModels) {
this.selectMissingJoinColumns(builder);
}
}
onAfter2(_, related) {
const isOneToOne = this.relation.isOneToOne();
if (this.assignResultToOwner && this.owner.isModels) {
const owners = this.owner.modelArray;
const relatedByOwnerId = new Map();
for (let i = 0, l = related.length; i < l; ++i) {
const rel = related[i];
const key = this.relation.relatedProp.propKey(rel);
let arr = relatedByOwnerId.get(key);
if (!arr) {
arr = [];
relatedByOwnerId.set(key, arr);
}
arr.push(rel);
}
for (let i = 0, l = owners.length; i < l; ++i) {
const own = owners[i];
const key = this.relation.ownerProp.propKey(own);
const related = relatedByOwnerId.get(key);
if (isOneToOne) {
own[this.relationProperty] = (related && related[0]) || null;
} else {
own[this.relationProperty] = related || [];
}
}
}
return related;
}
onAfter3(builder, related) {
const isOneToOne = this.relation.isOneToOne();
const intOpt = builder.internalOptions();
if (!intOpt.keepImplicitJoinProps) {
this.omitImplicitJoinProps(related);
}
if (!this.alwaysReturnArray && isOneToOne && related.length <= 1) {
related = related[0] || undefined;
}
return super.onAfter3(builder, related);
}
selectMissingJoinColumns(builder) {
const relatedProp = this.relation.relatedProp;
const addedSelects = [];
for (let c = 0, lc = relatedProp.size; c < lc; ++c) {
const fullCol = relatedProp.ref(builder, c).fullColumn(builder);
const prop = relatedProp.props[c];
const col = relatedProp.cols[c];
if (!builder.hasSelectionAs(fullCol, col) && addedSelects.indexOf(fullCol) === -1) {
this.omitProps.push(prop);
addedSelects.push(fullCol);
}
}
if (addedSelects.length) {
builder.select(addedSelects);
}
}
maybeApplyAlias(builder) {
if (!builder.alias() && this.alias) {
builder.alias(this.alias);
}
}
omitImplicitJoinProps(related) {
const relatedModelClass = this.relation.relatedModelClass;
if (!this.omitProps.length || !related) {
return related;
}
if (!Array.isArray(related)) {
return this.omitImplicitJoinPropsFromOne(relatedModelClass, related);
}
if (!related.length) {
return related;
}
for (let i = 0, l = related.length; i < l; ++i) {
this.omitImplicitJoinPropsFromOne(relatedModelClass, related[i]);
}
return related;
}
omitImplicitJoinPropsFromOne(relatedModelClass, model) {
for (let c = 0, lc = this.omitProps.length; c < lc; ++c) {
relatedModelClass.omitImpl(model, this.omitProps[c]);
}
return model;
}
clone() {
const clone = super.clone();
clone.alwaysReturnArray = this.alwaysReturnArray;
clone.assignResultToOwner = this.assignResultToOwner;
clone.relationProperty = this.relationProperty;
clone.omitProps = this.omitProps.slice();
clone.alias = this.alias;
return clone;
}
}
module.exports = {
RelationFindOperation,
};
================================================
FILE: lib/relations/RelationInsertOperation.js
================================================
'use strict';
const { InsertOperation } = require('../queryBuilder/operations/InsertOperation');
class RelationInsertOperation extends InsertOperation {
constructor(name, opt) {
super(name, opt);
this.relation = opt.relation;
this.owner = opt.owner;
this.assignResultToOwner = true;
}
async onBefore2(builder, result) {
const queryContext = builder.context();
result = await this.relation.executeBeforeInsert(this.models, queryContext, result);
return super.onBefore2(builder, result);
}
clone() {
const clone = super.clone();
clone.relation = this.relation;
clone.owner = this.owner;
clone.assignResultToOwner = this.assignResultToOwner;
return clone;
}
}
module.exports = {
RelationInsertOperation,
};
================================================
FILE: lib/relations/RelationOwner.js
================================================
'use strict';
const { isObject, asArray, asSingle, uniqBy } = require('../utils/objectUtils');
const { normalizeIds } = require('../utils/normalizeIds');
const Type = {
Models: 'Models',
Reference: 'Reference',
QueryBuilder: 'QueryBuilder',
Identifiers: 'Identifiers',
};
class RelationOwner {
constructor(owner) {
this.owner = owner;
this.type = detectType(owner);
}
static create(owner) {
return new RelationOwner(owner);
}
static createParentReference(builder, relation) {
return this.create(relation.ownerProp.refs(findFirstNonPartialAncestorQuery(builder)));
}
get isModels() {
return this.type === Type.Models;
}
get isReference() {
return this.type === Type.Reference;
}
get isQueryBuilder() {
return this.type === Type.QueryBuilder;
}
get isIdentifiers() {
return this.type === Type.Identifiers;
}
get modelArray() {
return asArray(this.owner);
}
get model() {
return asSingle(this.owner);
}
get reference() {
return this.owner;
}
get queryBuilder() {
return this.owner;
}
get identifiers() {
return this.owner;
}
buildFindQuery(builder, relation, relatedRefs) {
if (this.isReference) {
relatedRefs.forEach((relatedRef, i) => {
builder.where(relatedRef, this.reference[i]);
});
} else if (this.isModels || this.isIdentifiers || this.isQueryBuilder) {
const values = this.getProps(relation);
if (values) {
builder.whereInComposite(relatedRefs, values);
} else {
builder.where(false).resolve([]);
}
} else {
builder.where(false).resolve([]);
}
return builder;
}
getProps(relation, ownerProp = relation.ownerProp) {
if (this.isModels) {
return this._getPropsFromModels(ownerProp);
} else if (this.isIdentifiers) {
return this._getPropsFromIdentifiers(relation, ownerProp);
} else if (this.isQueryBuilder) {
return this._getPropsFromQuery(relation, ownerProp);
}
}
getSplitProps(builder, relation, ownerProp = relation.ownerProp) {
const values = this.getProps(relation, relation.ownerProp);
if (isQueryBuilder(values)) {
if (ownerProp.size === 1) {
return [[values]];
} else {
// For composite keys, we need to create a query builder for each
// key. Each query builder only select that key.
return [
Array.from({ length: ownerProp.size }).map((_, i) => {
return values.clone().clearSelect().select(ownerProp.ref(builder, i));
}),
];
}
} else {
return values;
}
}
getNormalizedIdentifiers(ownerProp) {
return normalizeIds(this.identifiers, ownerProp, {
arrayOutput: true,
});
}
_getPropsFromModels(ownerProp) {
const props = this.modelArray.map((owner) => ownerProp.getProps(owner));
if (!containsNonNull(props)) {
return null;
}
return uniqBy(props, join);
}
_getPropsFromIdentifiers(relation, ownerProp) {
const ids = this.getNormalizedIdentifiers(ownerProp);
if (isIdProp(ownerProp)) {
return ids;
} else {
const query = relation.ownerModelClass.query();
query.findByIds(ids);
query.select(ownerProp.refs(query));
return query;
}
}
_getPropsFromQuery(relation, ownerProp) {
const query = this.queryBuilder.clone();
if (isOwnerModelClassQuery(query, relation)) {
query.clearSelect();
query.select(ownerProp.refs(query));
}
return query;
}
}
function detectType(owner) {
if (isModel(owner) || isModelArray(owner)) {
return Type.Models;
} else if (isReferenceArray(owner)) {
return Type.Reference;
} else if (isQueryBuilder(owner)) {
return Type.QueryBuilder;
} else {
return Type.Identifiers;
}
}
function isModel(item) {
return isObject(item) && !!item.$isObjectionModel;
}
function isModelArray(item) {
return Array.isArray(item) && isModel(item[0]);
}
function isReference(item) {
return isObject(item) && !!item.isObjectionReferenceBuilder;
}
function isReferenceArray(item) {
return Array.isArray(item) && isReference(item[0]);
}
function isQueryBuilder(item) {
return isObject(item) && !!item.isObjectionQueryBuilder;
}
function findFirstNonPartialAncestorQuery(builder) {
builder = builder.parentQuery();
if (!builder)
throw Error(
'query method `for` ommitted outside a subquery, can not figure out relation target',
);
while (builder.isPartial()) {
if (!builder.parentQuery()) {
break;
}
builder = builder.parentQuery();
}
return builder;
}
function containsNonNull(arr) {
for (let i = 0, l = arr.length; i < l; ++i) {
const val = arr[i];
if (Array.isArray(val)) {
if (containsNonNull(val)) {
return true;
}
} else if (val !== null && val !== undefined) {
return true;
}
}
return false;
}
function join(id) {
return id.map((x) => (Buffer.isBuffer(x) ? x.toString('hex') : x)).join(',');
}
function isIdProp(relationProp) {
const idProp = relationProp.modelClass.getIdRelationProperty();
return idProp.props.every((prop, i) => prop === relationProp.props[i]);
}
function isOwnerModelClassQuery(query, relation) {
return query.modelClass().getTableName() === relation.ownerModelClass.getTableName();
}
module.exports = {
RelationOwner,
};
================================================
FILE: lib/relations/RelationProperty.js
================================================
'use strict';
const { asArray, isObject, uniqBy, get, set } = require('../utils/objectUtils');
const { ref: createRef } = require('../queryBuilder/ReferenceBuilder');
const { propToStr, PROP_KEY_PREFIX } = require('../model/modelValues');
class ModelNotFoundError extends Error {
constructor(tableName) {
super();
this.name = this.constructor.name;
this.tableName = tableName;
}
}
class InvalidReferenceError extends Error {
constructor() {
super();
this.name = this.constructor.name;
}
}
// A pair of these define how two tables are related to each other.
// Both the owner and the related table have one of these.
//
// A relation property can be a single column, an array of columns
// (composite key) a json column reference, an array of json column
// references or any combination of the above.
class RelationProperty {
// references must be a reference string like `Table.column:maybe.some.json[1].path`.
// or an array of such references (composite key).
//
// modelClassResolver must be a function that takes a table name
// and returns a model class.
constructor(references, modelClassResolver) {
const refs = createRefs(asArray(references));
const paths = createPaths(refs, modelClassResolver);
const modelClass = resolveModelClass(paths);
this._refs = refs.map((ref) => ref.model(modelClass));
this._modelClass = modelClass;
this._props = paths.map((it) => it.path[0]);
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, i) => createPatcher(it, paths[i].path));
}
static get ModelNotFoundError() {
return ModelNotFoundError;
}
static get InvalidReferenceError() {
return InvalidReferenceError;
}
// The number of columns.
get size() {
return this._refs.length;
}
// The model class that owns the property.
get modelClass() {
return this._modelClass;
}
// An array of property names. Contains multiple values in case of composite key.
// This may be different from `cols` if the model class has some kind of conversion
// between database and "external" formats, for example a snake_case to camelCase
// conversion.
get props() {
return this._props;
}
// An array of column names. Contains multiple values in case of composite key.
// This may be different from `props` if the model class has some kind of conversion
// between database and "external" formats, for example a snake_case to camelCase
// conversion.
get cols() {
return this._cols;
}
forEach(callback) {
for (let i = 0, l = this.size; i < l; ++i) {
callback(i);
}
}
// Creates a concatenated string from the property values of the given object.
propKey(obj) {
const size = this.size;
let key = PROP_KEY_PREFIX;
for (let i = 0; i < size; ++i) {
key += propToStr(this.getProp(obj, i));
if (i !== size - 1) {
key += ',';
}
}
return key;
}
// Returns the property values of the given object as an array.
getProps(obj) {
const size = this.size;
const props = new Array(size);
for (let i = 0; i < size; ++i) {
props[i] = this.getProp(obj, i);
}
return props;
}
// Returns true if the given object has a non-null value in all properties.
hasProps(obj) {
const size = this.size;
for (let i = 0; i < size; ++i) {
const prop = this.getProp(obj, i);
if (prop === null || prop === undefined) {
return false;
}
}
return true;
}
// Returns the index:th property value of the given object.
getProp(obj, index) {
return this._propGetters[index](obj);
}
// Sets the index:th property value of the given object.
setProp(obj, index, value) {
return this._propSetters[index](obj, value);
}
// Returns an instance of ReferenceBuilder that points to the index:th
// value of a row.
ref(builder, index) {
const table = builder.tableRefFor(this.modelClass);
return this._refs[index].clone().table(table);
}
// Returns an array of reference builders. `ref(builder, i)` for each i.
refs(builder) {
const refs = new Array(this.size);
for (let i = 0, l = refs.length; i < l; ++i) {
refs[i] = this.ref(builder, i);
}
return refs;
}
// Appends an update operation for the index:th column into `patch` object.
patch(patch, index, value) {
return this._patchers[index](patch, value);
}
// String representation of this property's index:th column for logging.
propDescription(index) {
return this._refs[index].expression;
}
}
function createRefs(refs) {
try {
return refs.map((it) => {
if (!isObject(it) || !it.isObjectionReferenceBuilder) {
return createRef(it);
} else {
return it;
}
});
} catch (err) {
throw new InvalidReferenceError();
}
}
function createPaths(refs, modelClassResolver) {
return refs.map((ref) => {
if (!ref.tableName) {
throw new InvalidReferenceError();
}
const modelClass = modelClassResolver(ref.tableName);
if (!modelClass) {
throw new ModelNotFoundError(ref.tableName);
}
const prop = modelClass.columnNameToPropertyName(ref.column);
const jsonPath = ref.parsedExpr.access.map((it) => it.ref);
return {
path: [prop].concat(jsonPath),
modelClass,
};
});
}
function resolveModelClass(paths) {
const modelClasses = paths.map((it) => it.modelClass);
const uniqueModelClasses = uniqBy(modelClasses);
if (uniqueModelClasses.length !== 1) {
throw new InvalidReferenceError();
}
return modelClasses[0];
}
function createGetter(path) {
if (path.length === 1) {
const prop = path[0];
return (obj) => obj[prop];
} else {
return (obj) => get(obj, path);
}
}
function createSetter(path) {
if (path.length === 1) {
const prop = path[0];
return (obj, value) => (obj[prop] = value);
} else {
return (obj, value) => set(obj, path, value);
}
}
function createPatcher(ref, path) {
if (ref.isPlainColumnRef) {
return (patch, value) => (patch[path[0]] = value);
} else {
// Objection `patch`, `update` etc. methods understand field expressions.
return (patch, value) => (patch[ref.expression] = value);
}
}
module.exports = {
RelationProperty,
};
================================================
FILE: lib/relations/RelationUpdateOperation.js
================================================
'use strict';
const { UpdateOperation } = require('../queryBuilder/operations/UpdateOperation');
const { RelationFindOperation } = require('./RelationFindOperation');
class RelationUpdateOperation extends UpdateOperation {
constructor(name, opt) {
super(name, opt);
this.relation = opt.relation;
this.owner = opt.owner;
}
onBuild(builder) {
super.onBuild(builder);
this.relation.findQuery(builder, this.owner);
}
toFindOperation() {
return new RelationFindOperation('find', {
relation: this.relation,
owner: this.owner,
});
}
}
module.exports = {
RelationUpdateOperation,
};
================================================
FILE: lib/relations/belongsToOne/BelongsToOneDeleteOperation.js
================================================
'use strict';
const { RelationDeleteOperation } = require('../RelationDeleteOperation');
class BelongsToOneDeleteOperation extends RelationDeleteOperation {
onAfter1(_, result) {
if (this.owner.isModels) {
const ownerProp = this.relation.ownerProp;
for (const owner of this.owner.modelArray) {
ownerProp.forEach((i) => {
ownerProp.setProp(owner, i, null);
});
}
}
return result;
}
}
module.exports = {
BelongsToOneDeleteOperation,
};
================================================
FILE: lib/relations/belongsToOne/BelongsToOneInsertOperation.js
================================================
'use strict';
const { RelationInsertOperation } = require('../RelationInsertOperation');
const { BelongsToOneRelateOperation } = require('./BelongsToOneRelateOperation');
class BelongsToOneInsertOperation extends RelationInsertOperation {
onAdd(builder, args) {
const retVal = super.onAdd(builder, args);
if (this.models.length > 1) {
throw this.relation.createError('can only insert one model to a BelongsToOneRelation');
}
return retVal;
}
async onAfter1(builder, ret) {
const inserted = await super.onAfter1(builder, ret);
if (!builder.isExecutable()) {
return inserted;
}
const relateOp = new BelongsToOneRelateOperation('relate', {
relation: this.relation,
owner: this.owner,
});
if (this.assignResultToOwner && this.owner.isModels) {
for (const owner of this.owner.modelArray) {
owner.$setRelated(this.relation, inserted);
}
}
relateOp.onAdd(builder, [inserted]);
await relateOp.queryExecutor(builder);
return inserted;
}
}
module.exports = {
BelongsToOneInsertOperation,
};
================================================
FILE: lib/relations/belongsToOne/BelongsToOneRelateOperation.js
================================================
'use strict';
const { normalizeIds } = require('../../utils/normalizeIds');
const { RelateOperation } = require('../../queryBuilder/operations/RelateOperation');
class BelongsToOneRelateOperation extends RelateOperation {
onAdd(_, args) {
this.input = args[0];
this.ids = normalizeIds(args[0], this.relation.relatedProp, { arrayOutput: true });
assertOneId(this.ids);
return true;
}
queryExecutor(builder) {
const patch = {};
const ownerProp = this.relation.ownerProp;
ownerProp.forEach((i) => {
const relatedValue = this.ids[0][i];
if (this.owner.isModels) {
for (const owner of this.owner.modelArray) {
ownerProp.setProp(owner, i, relatedValue);
}
}
ownerProp.patch(patch, i, relatedValue);
});
const ownerIdProp = this.relation.ownerModelClass.getIdRelationProperty();
const ownerIds = this.owner.getProps(this.relation, ownerIdProp);
return this.relation.ownerModelClass
.query()
.childQueryOf(builder)
.patch(patch)
.whereInComposite(ownerIdProp.refs(builder), ownerIds);
}
}
function assertOneId(ids) {
if (ids.length > 1) {
throw new Error('can only relate one model to a BelongsToOneRelation');
}
}
module.exports = {
BelongsToOneRelateOperation,
};
================================================
FILE: lib/relations/belongsToOne/BelongsToOneRelation.js
================================================
'use strict';
const { Relation } = require('../Relation');
const { BelongsToOneInsertOperation } = require('./BelongsToOneInsertOperation');
const { BelongsToOneDeleteOperation } = require('./BelongsToOneDeleteOperation');
const { BelongsToOneRelateOperation } = require('./BelongsToOneRelateOperation');
const { BelongsToOneUnrelateOperation } = require('./BelongsToOneUnrelateOperation');
class BelongsToOneRelation extends Relation {
isOneToOne() {
return true;
}
insert(_, owner) {
return new BelongsToOneInsertOperation('insert', {
relation: this,
owner,
});
}
delete(_, owner) {
return new BelongsToOneDeleteOperation('delete', {
relation: this,
owner,
});
}
relate(_, owner) {
return new BelongsToOneRelateOperation('relate', {
relation: this,
owner,
});
}
unrelate(_, owner) {
return new BelongsToOneUnrelateOperation('unrelate', {
relation: this,
owner,
});
}
}
Object.defineProperties(BelongsToOneRelation.prototype, {
isObjectionBelongsToOneRelation: {
enumerable: false,
writable: false,
value: true,
},
});
module.exports = {
BelongsToOneRelation,
};
================================================
FILE: lib/relations/belongsToOne/BelongsToOneUnrelateOperation.js
================================================
'use strict';
const { UnrelateOperation } = require('../../queryBuilder/operations/UnrelateOperation');
class BelongsToOneUnrelateOperation extends UnrelateOperation {
onAdd() {
const ids = new Array(this.relation.ownerProp.size);
this.relation.ownerProp.forEach((i) => {
ids[i] = null;
});
this.ids = [ids];
return true;
}
queryExecutor(builder) {
const patch = {};
const ownerProp = this.relation.ownerProp;
ownerProp.forEach((i) => {
const relatedValue = this.ids[0][i];
if (this.owner.isModels) {
for (const owner of this.owner.modelArray) {
ownerProp.setProp(owner, i, relatedValue);
}
}
ownerProp.patch(patch, i, relatedValue);
});
const ownerIdProp = this.relation.ownerModelClass.getIdRelationProperty();
const ownerRefs = ownerIdProp.refs(builder);
const ownerIds = this.owner.getProps(this.relation, ownerIdProp);
const query = this.relation.ownerModelClass
.query()
.childQueryOf(builder)
.patch(patch)
.whereInComposite(ownerRefs, ownerIds);
// We are creating a query to the related items. So any `where` statements
// must filter the *related* items, not the root query above, which is actually
// for the owners.
if (builder.has(builder.constructor.WhereSelector)) {
query.whereExists(
this.relation.ownerModelClass
.relatedQuery(this.relation.name)
.copyFrom(builder, builder.constructor.JoinSelector)
.copyFrom(builder, builder.constructor.WhereSelector),
);
}
return query;
}
}
module.exports = {
BelongsToOneUnrelateOperation,
};
================================================
FILE: lib/relations/hasMany/HasManyInsertOperation.js
================================================
'use strict';
const { RelationInsertOperation } = require('../RelationInsertOperation');
class HasManyInsertOperation extends RelationInsertOperation {
onAdd(builder, args) {
const retVal = super.onAdd(builder, args);
assertOwnerIsSingleItem(this.owner, this.relation);
const ownerValues = this.owner.getSplitProps(builder, this.relation);
const relatedProp = this.relation.relatedProp;
for (const model of this.models) {
for (let j = 0, lp = relatedProp.size; j < lp; ++j) {
relatedProp.setProp(model, j, ownerValues[0][j]);
}
}
return retVal;
}
async onAfter1(builder, ret) {
const inserted = await super.onAfter1(builder, ret);
if (!this.assignResultToOwner) {
return inserted;
}
if (this.owner.isModels) {
for (const owner of this.owner.modelArray) {
owner.$appendRelated(this.relation, inserted);
}
}
return inserted;
}
}
function assertOwnerIsSingleItem(owner, relation) {
const { isModels, isIdentifiers, isQueryBuilder } = owner;
const { ownerProp } = relation;
const singleModel = isModels && owner.modelArray.length === 1;
const singleId = isIdentifiers && owner.getNormalizedIdentifiers(ownerProp).length === 1;
if (!singleModel && !singleId && !isQueryBuilder) {
throw new Error(
[
'Can only insert items for one parent at a time in case of HasManyRelation.',
'Otherwise multiple insert queries would need to be created.',
'If you need to insert items for multiple parents, simply loop through them.',
`That's the most performant way.`,
].join(' '),
);
}
}
module.exports = {
HasManyInsertOperation,
};
================================================
FILE: lib/relations/hasMany/HasManyRelateOperation.js
================================================
'use strict';
const { normalizeIds } = require('../../utils/normalizeIds');
const { RelateOperation } = require('../../queryBuilder/operations/RelateOperation');
class HasManyRelateOperation extends RelateOperation {
onAdd(_, args) {
this.input = args[0];
this.ids = normalizeIds(args[0], this.relation.relatedModelClass.getIdRelationProperty(), {
arrayOutput: true,
});
assertOwnerIsSingleItem(this.owner, this.relation);
return true;
}
queryExecutor(builder) {
const patch = {};
const relatedProp = this.relation.relatedProp;
const ownerValues = this.owner.getSplitProps(builder, this.relation);
relatedProp.forEach((i) => {
relatedProp.patch(patch, i, ownerValues[0][i]);
});
return this.relation.relatedModelClass
.query()
.childQueryOf(builder)
.patch(patch)
.copyFrom(builder, builder.constructor.JoinSelector)
.copyFrom(builder, builder.constructor.WhereSelector)
.findByIds(this.ids)
.modify(this.relation.modify);
}
}
function assertOwnerIsSingleItem(owner, relation) {
const { isModels, isIdentifiers, isQueryBuilder } = owner;
const { ownerProp } = relation;
const singleModel = isModels && owner.modelArray.length === 1;
const singleId = isIdentifiers && owner.getNormalizedIdentifiers(ownerProp).length === 1;
if (!singleModel && !singleId && !isQueryBuilder) {
throw new Error(
[
'Can only relate items for one parent at a time in case of HasManyRelation.',
'Otherwise multiple update queries would need to be created.',
'If you need to relate items for multiple parents, simply loop through them.',
`That's the most performant way.`,
].join(' '),
);
}
}
module.exports = {
HasManyRelateOperation,
};
================================================
FILE: lib/relations/hasMany/HasManyRelation.js
================================================
'use strict';
const { Relation } = require('../Relation');
const { HasManyInsertOperation } = require('./HasManyInsertOperation');
const { HasManyRelateOperation } = require('./HasManyRelateOperation');
const { HasManyUnrelateOperation } = require('./HasManyUnrelateOperation');
class HasManyRelation extends Relation {
insert(_, owner) {
return new HasManyInsertOperation('insert', {
relation: this,
owner: owner,
});
}
relate(_, owner) {
return new HasManyRelateOperation('relate', {
relation: this,
owner: owner,
});
}
unrelate(_, owner) {
return new HasManyUnrelateOperation('unrelate', {
relation: this,
owner: owner,
});
}
hasRelateProp(model) {
return model.$hasId();
}
setRelateProp(model, values) {
model.$id(values);
}
}
Object.defineProperties(HasManyRelation.prototype, {
isObjectionHasManyRelation: {
enumerable: false,
writable: false,
value: true,
},
});
module.exports = {
HasManyRelation,
};
================================================
FILE: lib/relations/hasMany/HasManyUnrelateOperation.js
================================================
'use strict';
const { UnrelateOperation } = require('../../queryBuilder/operations/UnrelateOperation');
class HasManyUnrelateOperation extends UnrelateOperation {
queryExecutor(builder) {
const patch = {};
const relatedProp = this.relation.relatedProp;
const ownerValues = this.owner.getProps(this.relation);
const relatedRefs = relatedProp.refs(builder);
relatedProp.forEach((i) => {
relatedProp.patch(patch, i, null);
});
return this.relation.relatedModelClass
.query()
.childQueryOf(builder)
.patch(patch)
.copyFrom(builder, builder.constructor.JoinSelector)
.copyFrom(builder, builder.constructor.WhereSelector)
.whereInComposite(relatedRefs, ownerValues)
.modify(this.relation.modify);
}
}
module.exports = {
HasManyUnrelateOperation,
};
================================================
FILE: lib/relations/hasOne/HasOneRelation.js
================================================
'use strict';
const { HasManyRelation } = require('../hasMany/HasManyRelation');
class HasOneRelation extends HasManyRelation {
isOneToOne() {
return true;
}
}
module.exports = {
HasOneRelation,
};
================================================
FILE: lib/relations/hasOneThrough/HasOneThroughRelation.js
================================================
'use strict';
const { ManyToManyRelation } = require('../manyToMany/ManyToManyRelation');
class HasOneThroughRelation extends ManyToManyRelation {
isOneToOne() {
return true;
}
}
module.exports = {
HasOneThroughRelation,
};
================================================
FILE: lib/relations/manyToMany/ManyToManyModifyMixin.js
================================================
'use strict';
const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
// This mixin contains the shared code for all modify operations (update, delete, relate, unrelate)
// for ManyToManyRelation operations.
//
// The most important thing this mixin does is that it moves the filters from the main query
// into a subquery and then adds a single where clause that uses the subquery. This is done so
// that we are able to `innerJoin` the join table to the query. Most SQL engines don't allow
// joins in updates or deletes. Join table is joined so that queries can reference the join
// table columns.
const ManyToManyModifyMixin = (Operation) => {
return class extends Operation {
constructor(...args) {
super(...args);
this.modifyFilterSubquery = null;
}
get modifyMainQuery() {
return true;
}
// At this point `builder` should only have the user's own wheres and joins. There can
// be other operations (like orderBy) too, but those are meaningless with modify operations.
onBuild(builder) {
this.modifyFilterSubquery = this.createModifyFilterSubquery(builder);
if (this.modifyMainQuery) {
// We can now remove the where and join statements from the main query.
this.removeFiltersFromMainQuery(builder);
// Add a single where clause that uses the created subquery.
this.applyModifyFilterForRelatedTable(builder);
}
return super.onBuild(builder);
}
createModifyFilterSubquery(builder) {
const relatedModelClass = this.relation.relatedModelClass;
const builderClass = builder.constructor;
// Create an empty subquery.
const modifyFilterSubquery = relatedModelClass.query().childQueryOf(builder);
// Add the necessary joins and wheres so that only rows related to
// `this.owner` are selected.
this.relation.findQuery(modifyFilterSubquery, this.owner);
// Copy all where and join statements from the main query to the subquery.
modifyFilterSubquery
.copyFrom(builder, builderClass.WhereSelector)
.copyFrom(builder, builderClass.JoinSelector);
return modifyFilterSubquery.clearSelect();
}
removeFiltersFromMainQuery(builder) {
const builderClass = builder.constructor;
builder.clear(builderClass.WhereSelector);
builder.clear(builderClass.JoinSelector);
}
applyModifyFilterForRelatedTable(builder) {
const idRefs = this.relation.relatedModelClass.getIdRelationProperty().refs(builder);
const subquery = this.modifyFilterSubquery.clone().select(idRefs);
return builder.whereInComposite(idRefs, subquery);
}
applyModifyFilterForJoinTable(builder) {
const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
const joinTableRelatedRefs = this.relation.joinTableRelatedProp.refs(builder);
const relatedRefs = this.relation.relatedProp.refs(builder);
const ownerValues = this.owner.getProps(this.relation);
const subquery = this.modifyFilterSubquery.clone().select(relatedRefs);
return builder
.whereInComposite(joinTableRelatedRefs, subquery)
.whereInComposite(joinTableOwnerRefs, ownerValues);
}
toFindOperation() {
return new ManyToManyFindOperation('find', {
relation: this.relation,
owner: this.owner,
});
}
clone() {
const clone = super.clone();
clone.modifyFilterSubquery = this.modifyFilterSubquery;
return clone;
}
};
};
module.exports = {
ManyToManyModifyMixin,
};
================================================
FILE: lib/relations/manyToMany/ManyToManyRelation.js
================================================
'use strict';
const { getModel } = require('../../model/getModel');
const { Relation } = require('../Relation');
const { RelationProperty } = require('../RelationProperty');
const { isSqlite } = require('../../utils/knexUtils');
const { inheritModel } = require('../../model/inheritModel');
const { resolveModel } = require('../../utils/resolveModel');
const { mapAfterAllReturn } = require('../../utils/promiseUtils');
const { isFunction, isObject, isString } = require('../../utils/objectUtils');
const { createModifier } = require('../../utils/createModifier');
const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
const { ManyToManyInsertOperation } = require('./insert/ManyToManyInsertOperation');
const { ManyToManyRelateOperation } = require('./relate/ManyToManyRelateOperation');
const { ManyToManyUnrelateOperation } = require('./unrelate/ManyToManyUnrelateOperation');
const {
ManyToManyUnrelateSqliteOperation,
} = require('./unrelate/ManyToManyUnrelateSqliteOperation');
const { ManyToManyUpdateOperation } = require('./update/ManyToManyUpdateOperation');
const { ManyToManyUpdateSqliteOperation } = require('./update/ManyToManyUpdateSqliteOperation');
const { ManyToManyDeleteOperation } = require('./delete/ManyToManyDeleteOperation');
const { ManyToManyDeleteSqliteOperation } = require('./delete/ManyToManyDeleteSqliteOperation');
class ManyToManyRelation extends Relation {
setMapping(mapping) {
const retVal = super.setMapping(mapping);
let ctx = {
mapping,
ownerModelClass: this.ownerModelClass,
relatedModelClass: this.relatedModelClass,
ownerProp: this.ownerProp,
relatedProp: this.relatedProp,
joinTableModelClass: null,
joinTableOwnerProp: null,
joinTableRelatedProp: null,
joinTableBeforeInsert: null,
joinTableExtras: [],
createError: (msg) => this.createError(msg),
};
ctx = checkThroughObject(ctx);
ctx = resolveJoinModelClassIfDefined(ctx);
ctx = createJoinProperties(ctx);
ctx = parseExtras(ctx);
ctx = parseModify(ctx);
ctx = parseBeforeInsert(ctx);
ctx = finalizeJoinModelClass(ctx);
this.joinTableExtras = ctx.joinTableExtras;
this.joinTableModify = ctx.joinTableModify;
this.joinTableModelClass = ctx.joinTableModelClass;
this.joinTableOwnerProp = ctx.joinTableOwnerProp;
this.joinTableRelatedProp = ctx.joinTableRelatedProp;
this.joinTableBeforeInsert = ctx.joinTableBeforeInsert;
return retVal;
}
get forbiddenMappingProperties() {
return [];
}
findQuery(builder, owner) {
const joinTableOwnerRefs = this.joinTableOwnerProp.refs(builder);
const joinTable = builder.tableNameFor(this.joinTable);
const joinTableAlias = builder.tableRefFor(this.joinTable);
let joinTableSelect = this.joinTableModelClass
.query()
.childQueryOf(builder)
.modify(this.joinTableModify)
.as(joinTableAlias);
if (joinTableSelect.isSelectAll()) {
joinTableSelect = aliasedTableName(joinTable, joinTableAlias);
}
builder.join(joinTableSelect, (join) => {
this.relatedProp.forEach((i) => {
const relatedRef = this.relatedProp.ref(builder, i);
const joinTableRelatedRef = this.joinTableRelatedProp.ref(builder, i);
join.on(relatedRef, joinTableRelatedRef);
});
});
owner.buildFindQuery(builder, this, joinTableOwnerRefs);
return this.applyModify(builder);
}
join(
builder,
{
joinOperation = defaultJoinOperation(this, builder),
relatedTableAlias = defaultRelatedTableAlias(this, builder),
relatedJoinSelectQuery = defaultRelatedJoinSelectQuery(this, builder),
relatedTable = defaultRelatedTable(this, builder),
ownerTable = defaultOwnerTable(this, builder),
joinTableAlias = defaultJoinTableAlias(this, relatedTableAlias, builder),
} = {},
) {
let relatedJoinSelect = this.applyModify(relatedJoinSelectQuery).as(relatedTableAlias);
if (relatedJoinSelect.isSelectAll()) {
// No need to join a subquery if the query is `select * from "RelatedTable"`.
relatedJoinSelect = aliasedTableName(relatedTable, relatedTableAlias);
}
let joinTableSelect = this.joinTableModelClass
.query()
.childQueryOf(builder)
.modify(this.joinTableModify)
.as(joinTableAlias);
if (joinTableSelect.isSelectAll()) {
joinTableSelect = aliasedTableName(this.joinTable, joinTableAlias);
}
return builder[joinOperation](joinTableSelect, (join) => {
const ownerProp = this.ownerProp;
const joinTableOwnerProp = this.joinTableOwnerProp;
ownerProp.forEach((i) => {
const joinTableOwnerRef = joinTableOwnerProp.ref(builder, i).table(joinTableAlias);
const ownerRef = ownerProp.ref(builder, i).table(ownerTable);
join.on(joinTableOwnerRef, ownerRef);
});
})[joinOperation](relatedJoinSelect, (join) => {
const relatedProp = this.relatedProp;
const joinTableRelatedProp = this.joinTableRelatedProp;
relatedProp.forEach((i) => {
const joinTableRelatedRef = joinTableRelatedProp.ref(builder, i).table(joinTableAlias);
const relatedRef = relatedProp.ref(builder, i).table(relatedTableAlias);
join.on(joinTableRelatedRef, relatedRef);
});
});
}
find(_, owner) {
return new ManyToManyFindOperation('find', {
relation: this,
owner,
});
}
insert(_, owner) {
return new ManyToManyInsertOperation('insert', {
relation: this,
owner,
});
}
update(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUpdateSqliteOperation('update', {
relation: this,
owner,
});
} else {
return new ManyToManyUpdateOperation('update', {
relation: this,
owner,
});
}
}
patch(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUpdateSqliteOperation('patch', {
modelOptions: { patch: true },
relation: this,
owner,
});
} else {
return new ManyToManyUpdateOperation('patch', {
modelOptions: { patch: true },
relation: this,
owner,
});
}
}
delete(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyDeleteSqliteOperation('delete', {
relation: this,
owner,
});
} else {
return new ManyToManyDeleteOperation('delete', {
relation: this,
owner,
});
}
}
relate(builder, owner) {
return new ManyToManyRelateOperation('relate', {
relation: this,
owner,
});
}
unrelate(builder, owner) {
if (isSqlite(builder.knex())) {
return new ManyToManyUnrelateSqliteOperation('unrelate', {
relation: this,
owner,
});
} else {
return new ManyToManyUnrelateOperation('unrelate', {
relation: this,
owner,
});
}
}
createJoinModels(ownerId, related) {
return related.map((related) => this.createJoinModel(ownerId, related));
}
createJoinModel(ownerId, rel) {
let joinModel = {};
for (let j = 0, lp = this.joinTableOwnerProp.size; j < lp; ++j) {
this.joinTableOwnerProp.setProp(joinModel, j, ownerId[j]);
}
for (let j = 0, lp = this.joinTableRelatedProp.size; j < lp; ++j) {
const value = this.relatedProp.getProp(rel, j);
if (value !== undefined) {
this.joinTableRelatedProp.setProp(joinModel, j, value);
}
}
for (const extra of this.joinTableExtras) {
let extraValue = rel[extra.aliasProp];
if (extraValue === undefined && rel.$$queryProps) {
extraValue = rel.$$queryProps[extra.aliasProp];
}
if (extraValue !== undefined) {
joinModel[extra.joinTableProp] = extraValue;
}
}
return joinModel;
}
omitExtraProps(models) {
if (this.joinTableExtras && this.joinTableExtras.length) {
const props = this.joinTableExtras.map((extra) => extra.aliasProp);
for (const model of models) {
// Omit extra properties instead of deleting them from the models so that they can
// be used in the `$before` and `$after` hooks.
model.$omitFromDatabaseJson(props);
}
}
}
executeJoinTableBeforeInsert(models, queryContext, result) {
return mapAfterAllReturn(
models,
(model) => this.joinTableBeforeInsert(model, queryContext),
result,
);
}
}
Object.defineProperties(ManyToManyRelation.prototype, {
isObjectionManyToManyRelation: {
enumerable: false,
writable: false,
value: true,
},
});
function defaultJoinOperation() {
return 'join';
}
function defaultRelatedTableAlias(relation, builder) {
return builder.tableRefFor(relation.relatedModelClass);
}
function defaultRelatedJoinSelectQuery(relation, builder) {
return relation.relatedModelClass.query().childQueryOf(builder);
}
function defaultRelatedTable(relation, builder) {
return builder.tableNameFor(relation.relatedModelClass);
}
function defaultOwnerTable(relation, builder) {
return builder.tableRefFor(relation.ownerModelClass);
}
function defaultJoinTableAlias(relation, relatedTableAlias, builder) {
const alias = builder.tableRefFor(relation.joinTable);
if (alias === relation.joinTable) {
return relation.ownerModelClass.joinTableAlias(relatedTableAlias);
} else {
return alias;
}
}
function aliasedTableName(tableName, alias) {
if (tableName === alias) {
return tableName;
} else {
return `${tableName} as ${alias}`;
}
}
function checkThroughObject(ctx) {
const mapping = ctx.mapping;
if (!isObject(mapping.join.through)) {
throw ctx.createError('join must have a `through` object that describes the join table.');
}
if (!mapping.join.through.from || !mapping.join.through.to) {
throw ctx.createError(
'join.through must be an object that describes the join table. For example: {from: "JoinTable.someId", to: "JoinTable.someOtherId"}',
);
}
return ctx;
}
function resolveJoinModelClassIfDefined(ctx) {
let joinTableModelClass = null;
if (ctx.mapping.join.through.modelClass) {
try {
joinTableModelClass = resolveModel(
ctx.mapping.join.through.modelClass,
ctx.ownerModelClass.modelPaths,
'join.through.modelClass',
);
} catch (err) {
throw ctx.createError(err.message);
}
}
return Object.assign(ctx, { joinTableModelClass });
}
function createJoinProperties(ctx) {
let ret;
let fromProp;
let toProp;
let relatedProp;
let ownerProp;
ret = createRelationProperty(ctx, ctx.mapping.join.through.from, 'join.through.from');
fromProp = ret.prop;
ctx = ret.ctx;
ret = createRelationProperty(ctx, ctx.mapping.join.through.to, 'join.through.to');
toProp = ret.prop;
ctx = ret.ctx;
if (fromProp.modelClass.getTableName() !== toProp.modelClass.getTableName()) {
throw ctx.createError('join.through `from` and `to` must point to the same join table.');
}
if (ctx.relatedProp.modelClass.getTableName() === fromProp.modelClass.getTableName()) {
relatedProp = fromProp;
ownerProp = toProp;
} else {
relatedProp = toProp;
ownerProp = fromProp;
}
return Object.assign(ctx, {
joinTableOwnerProp: ownerProp,
joinTableRelatedProp: relatedProp,
});
}
function createRelationProperty(ctx, refString, messagePrefix) {
let prop = null;
let joinTableModelClass = ctx.joinTableModelClass;
const resolveModelClass = (table) => {
if (joinTableModelClass === null) {
joinTableModelClass = inheritModel(getModel());
joinTableModelClass.tableName = table;
joinTableModelClass.idColumn = null;
joinTableModelClass.concurrency = ctx.ownerModelClass.concurrency;
}
if (joinTableModelClass.getTableName() === table) {
return joinTableModelClass;
} else {
return null;
}
};
try {
prop = new RelationProperty(refString, resolveModelClass);
} catch (err) {
if (err instanceof RelationProperty.ModelNotFoundError) {
throw ctx.createError('join.through `from` and `to` must point to the same join table.');
} else {
throw ctx.createError(
`${messagePrefix} must have format JoinTable.columnName. For example "JoinTable.someId" or in case of composite key ["JoinTable.a", "JoinTable.b"].`,
);
}
}
return {
ctx: Object.assign(ctx, { joinTableModelClass }),
prop,
};
}
function parseExtras(ctx) {
let extraDef = ctx.mapping.join.through.extra;
if (!extraDef) {
return ctx;
}
if (isString(extraDef)) {
extraDef = {
[extraDef]: extraDef,
};
} else if (Array.isArray(extraDef)) {
extraDef = extraDef.reduce((extraDef, col) => {
extraDef[col] = col;
return extraDef;
}, {});
}
const joinTableExtras = Object.keys(extraDef).map((key) => {
const val = extraDef[key];
return {
joinTableCol: val,
joinTableProp: ctx.joinTableModelClass.columnNameToPropertyName(val),
aliasCol: key,
aliasProp: ctx.joinTableModelClass.columnNameToPropertyName(key),
};
});
return Object.assign(ctx, { joinTableExtras });
}
function parseModify(ctx) {
const mapping = ctx.mapping.join.through;
const modifier = mapping.modify || mapping.filter;
const joinTableModify =
modifier &&
createModifier({
modifier,
modelClass: ctx.relatedModelClass,
});
return Object.assign(ctx, { joinTableModify });
}
function parseBeforeInsert(ctx) {
let joinTableBeforeInsert;
if (isFunction(ctx.mapping.join.through.beforeInsert)) {
joinTableBeforeInsert = ctx.mapping.join.through.beforeInsert;
} else {
joinTableBeforeInsert = (model) => model;
}
return Object.assign(ctx, { joinTableBeforeInsert });
}
function finalizeJoinModelClass(ctx) {
if (ctx.joinTableModelClass.getIdColumn() === null) {
// We cannot know if the join table has a primary key. Therefore we set some
// known column as the idColumn so that inserts will work.
ctx.joinTableModelClass.idColumn = ctx.joinTableRelatedProp.cols;
}
return ctx;
}
module.exports = {
ManyToManyRelation,
};
================================================
FILE: lib/relations/manyToMany/ManyToManySqliteModifyMixin.js
================================================
'use strict';
const { ManyToManyModifyMixin } = require('./ManyToManyModifyMixin');
const SQLITE_BUILTIN_ROW_ID = '_rowid_';
// We need to override this mixin for sqlite because sqlite doesn't support
// multi-column where in statements with subqueries. We need to use the
// internal _rowid_ column instead.
const ManyToManySqliteModifyMixin = (Operation) => {
return class extends ManyToManyModifyMixin(Operation) {
applyModifyFilterForRelatedTable(builder) {
const tableRef = builder.tableRefFor(this.relation.relatedModelClass);
const rowIdRef = `${tableRef}.${SQLITE_BUILTIN_ROW_ID}`;
const subquery = this.modifyFilterSubquery.clone().select(rowIdRef);
return builder.whereInComposite(rowIdRef, subquery);
}
applyModifyFilterForJoinTable(builder) {
const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
const rowIdRef = `${tableRef}.${SQLITE_BUILTIN_ROW_ID}`;
const ownerValues = this.owner.getProps(this.relation);
const subquery = this.modifyFilterSubquery.clone().select(rowIdRef);
return builder
.whereInComposite(rowIdRef, subquery)
.whereInComposite(joinTableOwnerRefs, ownerValues);
}
};
};
module.exports = {
ManyToManySqliteModifyMixin,
};
================================================
FILE: lib/relations/manyToMany/delete/ManyToManyDeleteOperation.js
================================================
'use strict';
const { ManyToManyDeleteOperationBase } = require('./ManyToManyDeleteOperationBase');
const { ManyToManyModifyMixin } = require('../ManyToManyModifyMixin');
class ManyToManyDeleteOperation extends ManyToManyModifyMixin(ManyToManyDeleteOperationBase) {}
module.exports = {
ManyToManyDeleteOperation,
};
================================================
FILE: lib/relations/manyToMany/delete/ManyToManyDeleteOperationBase.js
================================================
'use strict';
const { DeleteOperation } = require('../../../queryBuilder/operations/DeleteOperation');
class ManyToManyDeleteOperationBase extends DeleteOperation {
constructor(name, opt) {
super(name, opt);
this.relation = opt.relation;
this.owner = opt.owner;
}
/* istanbul ignore next */
applyModifyFilterForRelatedTable(builder) {
throw new Error('not implemented');
}
/* istanbul ignore next */
applyModifyFilterForJoinTable(builder) {
throw new Error('not implemented');
}
}
module.exports = {
ManyToManyDeleteOperationBase,
};
================================================
FILE: lib/relations/manyToMany/delete/ManyToManyDeleteSqliteOperation.js
================================================
'use strict';
const { ManyToManyDeleteOperationBase } = require('./ManyToManyDeleteOperationBase');
const { ManyToManySqliteModifyMixin } = require('../ManyToManySqliteModifyMixin');
class ManyToManyDeleteSqliteOperation extends ManyToManySqliteModifyMixin(
ManyToManyDeleteOperationBase,
) {}
module.exports = {
ManyToManyDeleteSqliteOperation,
};
================================================
FILE: lib/relations/manyToMany/find/ManyToManyFindOperation.js
================================================
'use strict';
const { RelationFindOperation } = require('../../RelationFindOperation');
const { getTempColumn } = require('../../../utils/tmpColumnUtils');
class ManyToManyFindOperation extends RelationFindOperation {
constructor(name, opt) {
super(name, opt);
this.ownerJoinColumnAlias = new Array(this.relation.joinTableOwnerProp.size);
for (let i = 0, l = this.ownerJoinColumnAlias.length; i < l; ++i) {
this.ownerJoinColumnAlias[i] = getTempColumn(i);
}
}
onBuild(builder) {
const relatedModelClass = this.relation.relatedModelClass;
this.maybeApplyAlias(builder);
this.relation.findQuery(builder, this.owner);
if (!builder.hasSelects()) {
const table = builder.tableRefFor(relatedModelClass);
// If the user hasn't specified a select clause, select the related model's columns.
// If we don't do this we also get the join table's columns.
builder.select(`${table}.*`);
// Also select all extra columns.
for (const extra of this.relation.joinTableExtras) {
const joinTable = builder.tableRefFor(this.relation.joinTable);
builder.select(`${joinTable}.${extra.joinTableCol} as ${extra.aliasCol}`);
}
}
if (this.assignResultToOwner && this.owner.isModels) {
this.selectMissingJoinColumns(builder);
}
}
onAfter2(_, related) {
const isOneToOne = this.relation.isOneToOne();
if (this.assignResultToOwner && this.owner.isModels) {
const owners = this.owner.modelArray;
const ownerProp = this.relation.ownerProp;
const relatedByOwnerId = new Map();
for (let i = 0, l = related.length; i < l; ++i) {
const rel = related[i];
const key = rel.$propKey(this.ownerJoinColumnAlias);
let arr = relatedByOwnerId.get(key);
if (!arr) {
arr = [];
relatedByOwnerId.set(key, arr);
}
arr.push(rel);
}
for (let i = 0, l = owners.length; i < l; ++i) {
const own = owners[i];
const key = ownerProp.propKey(own);
const related = relatedByOwnerId.get(key);
if (isOneToOne) {
own[this.relationProperty] = (related && related[0]) || null;
} else {
own[this.relationProperty] = related || [];
}
}
}
return related;
}
clone() {
const clone = super.clone();
clone.ownerJoinColumnAlias = this.ownerJoinColumnAlias.slice();
return clone;
}
selectMissingJoinColumns(builder) {
const { relatedModelClass, joinTableOwnerProp } = this.relation;
// We must select the owner join columns so that we know for which owner model the related
// models belong to after the requests.
joinTableOwnerProp.forEach((i) => {
const joinTableOwnerRef = joinTableOwnerProp.ref(builder, i);
const propName = relatedModelClass.columnNameToPropertyName(this.ownerJoinColumnAlias[i]);
builder.select(joinTableOwnerRef.as(this.ownerJoinColumnAlias[i]));
// Mark them to be omitted later.
this.omitProps.push(propName);
});
super.selectMissingJoinColumns(builder);
}
}
module.exports = {
ManyToManyFindOperation,
};
================================================
FILE: lib/relations/manyToMany/insert/ManyToManyInsertOperation.js
================================================
'use strict';
const { RelationInsertOperation } = require('../../RelationInsertOperation');
const { ManyToManyRelateOperation } = require('../relate/ManyToManyRelateOperation');
class ManyToManyInsertOperation extends RelationInsertOperation {
onAdd(builder, args) {
const retVal = super.onAdd(builder, args);
// Omit extra properties so that we don't try to insert
// them to the related table. We don't actually remove them
// from the objects, but simply mark them to be removed
// from the inserted row.
this.relation.omitExtraProps(this.models);
return retVal;
}
async onAfter1(builder, ret) {
const inserted = await super.onAfter1(builder, ret);
const relateOp = new ManyToManyRelateOperation('relate', {
dontCopyReturning: true,
dontCopyOnConflict: true,
relation: this.relation,
owner: this.owner,
});
const modelsToRelate = inserted.filter((it) => {
return this.relation.relatedProp.hasProps(it);
});
if (this.assignResultToOwner && this.owner.isModels) {
for (const owner of this.owner.modelArray) {
owner.$appendRelated(this.relation, inserted);
}
}
if (modelsToRelate.length === 0) {
return inserted;
}
relateOp.onAdd(builder, [modelsToRelate]);
await relateOp.queryExecutor(builder);
return inserted;
}
}
module.exports = {
ManyToManyInsertOperation,
};
================================================
FILE: lib/relations/manyToMany/relate/ManyToManyRelateOperation.js
================================================
'use strict';
const { isPostgres } = require('../../../utils/knexUtils');
const { normalizeIds } = require('../../../utils/normalizeIds');
const { RelateOperation } = require('../../../queryBuilder/operations/RelateOperation');
class ManyToManyRelateOperation extends RelateOperation {
onAdd(builder, args) {
this.input = args[0];
this.ids = normalizeIds(args[0], this.relation.relatedProp);
assertOwnerIsSingleItem(builder, this.owner, this.relation);
return true;
}
queryExecutor(builder) {
const joinModelClass = this.relation.getJoinModelClass(builder.knex());
const ownerValues = this.owner.getSplitProps(builder, this.relation);
const joinModels = [];
for (const ownerValue of ownerValues) {
for (const relatedValue of this.ids) {
joinModels.push(
joinModelClass.fromJson(this.relation.createJoinModel(ownerValue, relatedValue)),
);
}
}
return joinModelClass
.query()
.childQueryOf(builder)
.modify((query) => {
if (!this.opt.dontCopyReturning) {
query.copyFrom(builder, /returning/);
}
if (!this.opt.dontCopyOnConflict) {
query.copyFrom(builder, /onConflict|ignore|merge/);
}
})
.runBefore((_, builder) => {
return this.relation.executeJoinTableBeforeInsert(joinModels, builder.context(), null);
})
.insert(joinModels)
.runAfter((models) => {
return Array.isArray(models) ? models.length : 1;
});
}
}
function assertOwnerIsSingleItem(builder, owner, relation) {
const { isModels, isIdentifiers, isQueryBuilder } = owner;
const { ownerProp } = relation;
const singleModel = isModels && owner.modelArray.length === 1;
const singleId = isIdentifiers && owner.getNormalizedIdentifiers(ownerProp).length === 1;
if (isPostgres(builder.unsafeKnex())) {
if (!isModels && !isIdentifiers && !isQueryBuilder) {
throw new Error(
'Parent must be a list of identifiers or a list of models when relating a ManyToManyRelation',
);
}
} else {
if (!singleModel && !singleId && !isQueryBuilder) {
throw new Error(
[
'Can only relate items for one parent at a time in case of ManyToManyRelation.',
'Otherwise multiple insert queries would need to be created.',
'If you need to relate items for multiple parents, simply loop through them.',
`That's the most performant way.`,
].join(' '),
);
}
}
}
module.exports = {
ManyToManyRelateOperation,
};
================================================
FILE: lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js
================================================
'use strict';
const { ManyToManyUnrelateOperationBase } = require('./ManyToManyUnrelateOperationBase');
const { ManyToManyModifyMixin } = require('../ManyToManyModifyMixin');
class ManyToManyUnrelateOperation extends ManyToManyModifyMixin(ManyToManyUnrelateOperationBase) {
get modifyMainQuery() {
return false;
}
}
module.exports = {
ManyToManyUnrelateOperation,
};
================================================
FILE: lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js
================================================
'use strict';
const { UnrelateOperation } = require('../../../queryBuilder/operations/UnrelateOperation');
class ManyToManyUnrelateOperationBase extends UnrelateOperation {
queryExecutor(builder) {
const unrelateQuery = this.relation
.getJoinModelClass(builder.knex())
.query()
.childQueryOf(builder)
.delete();
return this.applyModifyFilterForJoinTable(unrelateQuery).modify(this.relation.joinTableModify);
}
/* istanbul ignore next */
applyModifyFilterForRelatedTable(builder) {
throw new Error('not implemented');
}
/* istanbul ignore next */
applyModifyFilterForJoinTable(builder) {
throw new Error('not implemented');
}
}
module.exports = {
ManyToManyUnrelateOperationBase,
};
================================================
FILE: lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js
================================================
'use strict';
const { ManyToManyUnrelateOperationBase } = require('./ManyToManyUnrelateOperationBase');
const { ManyToManySqliteModifyMixin } = require('../ManyToManySqliteModifyMixin');
class ManyToManyUnrelateSqliteOperation extends ManyToManySqliteModifyMixin(
ManyToManyUnrelateOperationBase,
) {
get modifyMainQuery() {
return false;
}
}
module.exports = {
ManyToManyUnrelateSqliteOperation,
};
================================================
FILE: lib/relations/manyToMany/update/ManyToManyUpdateOperation.js
================================================
'use strict';
const { ManyToManyUpdateOperationBase } = require('./ManyToManyUpdateOperationBase');
const { ManyToManyModifyMixin } = require('../ManyToManyModifyMixin');
class ManyToManyUpdateOperation extends ManyToManyModifyMixin(ManyToManyUpdateOperationBase) {}
module.exports = {
ManyToManyUpdateOperation,
};
================================================
FILE: lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js
================================================
'use strict';
const { UpdateOperation } = require('../../../queryBuilder/operations/UpdateOperation');
class ManyToManyUpdateOperationBase extends UpdateOperation {
constructor(name, opt) {
super(name, opt);
this.relation = opt.relation;
this.owner = opt.owner;
this.hasExtraProps = false;
this.joinTablePatch = {};
this.joinTablePatchFilterQuery = null;
}
onAdd(builder, args) {
const obj = args[0];
// Copy all extra properties to the `joinTablePatch` object.
for (const extra of this.relation.joinTableExtras) {
if (extra.aliasProp in obj) {
this.hasExtraProps = true;
this.joinTablePatch[extra.joinTableProp] = obj[extra.aliasProp];
}
}
const res = super.onAdd(builder, args);
if (this.hasExtraProps) {
// Make sure we don't try to insert the extra properties
// to the target table.
this.relation.omitExtraProps([this.model]);
}
return res;
}
async onAfter1(builder, result) {
if (this.hasExtraProps) {
const joinTableUpdateQuery = this.relation
.getJoinModelClass(builder.knex())
.query()
.childQueryOf(builder)
.patch(this.joinTablePatch);
await this.applyModifyFilterForJoinTable(joinTableUpdateQuery).modify(
this.relation.joinTableModify,
);
return result;
} else {
return result;
}
}
/* istanbul ignore next */
applyModifyFilterForRelatedTable(builder) {
throw new Error('not implemented');
}
/* istanbul ignore next */
applyModifyFilterForJoinTable(builder) {
throw new Error('not implemented');
}
clone() {
const clone = super.clone();
clone.hasExtraProps = this.hasExtraProps;
clone.joinTablePatch = this.joinTablePatch;
clone.joinTablePatchFilterQuery = this.joinTablePatchFilterQuery;
return clone;
}
}
module.exports = {
ManyToManyUpdateOperationBase,
};
================================================
FILE: lib/relations/manyToMany/update/ManyToManyUpdateSqliteOperation.js
================================================
'use strict';
const { ManyToManyUpdateOperationBase } = require('./ManyToManyUpdateOperationBase');
const { ManyToManySqliteModifyMixin } = require('../ManyToManySqliteModifyMixin');
class ManyToManyUpdateSqliteOperation extends ManyToManySqliteModifyMixin(
ManyToManyUpdateOperationBase,
) {}
module.exports = {
ManyToManyUpdateSqliteOperation,
};
================================================
FILE: lib/transaction.js
================================================
'use strict';
const promiseUtils = require('./utils/promiseUtils');
const { isFunction } = require('./utils/objectUtils');
function transaction() {
// There must be at least one model class and the callback.
if (arguments.length < 2) {
return Promise.reject(
new Error(
'objection.transaction: provide at least one Model class to bind to the transaction or a knex instance',
),
);
}
let args = Array.from(arguments);
if (!isModelClass(args[0]) && isFunction(args[0].transaction)) {
let knex = args[0];
args = args.slice(1);
return knex.transaction.apply(knex, args);
} else {
// The last argument should be the callback and all other Model subclasses.
let callback = args[args.length - 1];
let modelClasses = args.slice(0, args.length - 1);
let i;
for (i = 0; i < modelClasses.length; ++i) {
if (!isModelClass(modelClasses[i])) {
return Promise.reject(
new Error('objection.transaction: all but the last argument should be Model subclasses'),
);
}
}
let knex = modelClasses[0].knex();
for (i = 0; i < modelClasses.length; ++i) {
if (modelClasses[i].knex() !== knex) {
return Promise.reject(
new Error(
'objection.transaction: all Model subclasses must be bound to the same database',
),
);
}
}
return knex.transaction((trx) => {
let args = new Array(modelClasses.length + 1);
for (let i = 0; i < modelClasses.length; ++i) {
args[i] = modelClasses[i].bindTransaction(trx);
}
args[args.length - 1] = trx;
return promiseUtils.try(() => {
return callback.apply(trx, args);
});
});
}
}
transaction.start = function (modelClassOrKnex) {
let knex = modelClassOrKnex;
if (isModelClass(modelClassOrKnex)) {
knex = modelClassOrKnex.knex();
}
if (!knex || !isFunction(knex.transaction)) {
return Promise.reject(
new Error(
'objection.transaction.start: first argument must be a model class or a knex instance',
),
);
}
return new Promise((resolve, reject) => {
knex
.transaction((trx) => {
resolve(trx);
})
.catch((err) => {
reject(err);
});
});
};
function isModelClass(maybeModel) {
return isFunction(maybeModel) && maybeModel.isObjectionModelClass;
}
module.exports = {
transaction,
};
================================================
FILE: lib/utils/assert.js
================================================
'use strict';
function assertHasId(model) {
if (!model.$hasId()) {
const modelClass = model.constructor;
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?`,
);
}
}
function assertIdNotUndefined(id, message) {
if (Array.isArray(id)) {
id.forEach((id) => assertIdNotUndefined(id, message));
} else if (id === undefined) {
throw Error(message);
}
}
module.exports = {
assertHasId,
assertIdNotUndefined,
};
================================================
FILE: lib/utils/buildUtils.js
================================================
'use strict';
const { isObject, isFunction } = require('./objectUtils');
function buildArg(arg, builder) {
if (!isObject(arg)) {
return arg;
}
if (isFunction(arg.toKnexRaw)) {
return arg.toKnexRaw(builder);
} else if (arg.isObjectionQueryBuilderBase === true) {
return arg.subqueryOf(builder).toKnexQuery();
} else {
return arg;
}
}
module.exports = {
buildArg,
};
================================================
FILE: lib/utils/classUtils.js
================================================
'use strict';
function inherit(Constructor, BaseConstructor) {
Constructor.prototype = Object.create(BaseConstructor.prototype);
Constructor.prototype.constructor = BaseConstructor;
Object.setPrototypeOf(Constructor, BaseConstructor);
return Constructor;
}
module.exports = {
inherit,
};
================================================
FILE: lib/utils/clone.js
================================================
'use strict';
/**
* @license
* Lodash (Custom Build)
* Build: `lodash include="cloneDeep,clone" exports="node" --development`
* Copyright JS Foundation and other contributors
* Released under MIT license
* Based on Underscore.js 1.8.3
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
(function () {
/** Used as a safe reference for `undefined` in pre-ES5 environments. */
var undefined;
/** Used as the semantic version number. */
var VERSION = '4.17.5';
/** Used as the size to enable large array optimizations. */
var LARGE_ARRAY_SIZE = 200;
/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
/** Used to compose bitmasks for cloning. */
var CLONE_DEEP_FLAG = 1,
CLONE_FLAT_FLAG = 2,
CLONE_SYMBOLS_FLAG = 4;
/** Used as references for various `Number` constants. */
var MAX_SAFE_INTEGER = 9007199254740991;
/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
arrayTag = '[object Array]',
asyncTag = '[object AsyncFunction]',
boolTag = '[object Boolean]',
dateTag = '[object Date]',
errorTag = '[object Error]',
funcTag = '[object Function]',
genTag = '[object GeneratorFunction]',
mapTag = '[object Map]',
numberTag = '[object Number]',
nullTag = '[object Null]',
objectTag = '[object Object]',
promiseTag = '[object Promise]',
proxyTag = '[object Proxy]',
regexpTag = '[object RegExp]',
setTag = '[object Set]',
stringTag = '[object String]',
symbolTag = '[object Symbol]',
undefinedTag = '[object Undefined]',
weakMapTag = '[object WeakMap]';
var arrayBufferTag = '[object ArrayBuffer]',
dataViewTag = '[object DataView]',
float32Tag = '[object Float32Array]',
float64Tag = '[object Float64Array]',
int8Tag = '[object Int8Array]',
int16Tag = '[object Int16Array]',
int32Tag = '[object Int32Array]',
uint8Tag = '[object Uint8Array]',
uint8ClampedTag = '[object Uint8ClampedArray]',
uint16Tag = '[object Uint16Array]',
uint32Tag = '[object Uint32Array]';
/**
* Used to match `RegExp`
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
*/
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
/** Used to match `RegExp` flags from their coerced string values. */
var reFlags = /\w*$/;
/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\[object .+?Constructor\]$/;
/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;
/** Used to identify `toStringTag` values of typed arrays. */
var typedArrayTags = {};
typedArrayTags[float32Tag] =
typedArrayTags[float64Tag] =
typedArrayTags[int8Tag] =
typedArrayTags[int16Tag] =
typedArrayTags[int32Tag] =
typedArrayTags[uint8Tag] =
typedArrayTags[uint8ClampedTag] =
typedArrayTags[uint16Tag] =
typedArrayTags[uint32Tag] =
true;
typedArrayTags[argsTag] =
typedArrayTags[arrayTag] =
typedArrayTags[arrayBufferTag] =
typedArrayTags[boolTag] =
typedArrayTags[dataViewTag] =
typedArrayTags[dateTag] =
typedArrayTags[errorTag] =
typedArrayTags[funcTag] =
typedArrayTags[mapTag] =
typedArrayTags[numberTag] =
typedArrayTags[objectTag] =
typedArrayTags[regexpTag] =
typedArrayTags[setTag] =
typedArrayTags[stringTag] =
typedArrayTags[weakMapTag] =
false;
/** Used to identify `toStringTag` values supported by `_.clone`. */
var cloneableTags = {};
cloneableTags[argsTag] =
cloneableTags[arrayTag] =
cloneableTags[arrayBufferTag] =
cloneableTags[dataViewTag] =
cloneableTags[boolTag] =
cloneableTags[dateTag] =
cloneableTags[float32Tag] =
cloneableTags[float64Tag] =
cloneableTags[int8Tag] =
cloneableTags[int16Tag] =
cloneableTags[int32Tag] =
cloneableTags[mapTag] =
cloneableTags[numberTag] =
cloneableTags[objectTag] =
cloneableTags[regexpTag] =
cloneableTags[setTag] =
cloneableTags[stringTag] =
cloneableTags[symbolTag] =
cloneableTags[uint8Tag] =
cloneableTags[uint8ClampedTag] =
cloneableTags[uint16Tag] =
cloneableTags[uint32Tag] =
true;
cloneableTags[errorTag] = cloneableTags[funcTag] = cloneableTags[weakMapTag] = false;
/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
/** Used as a reference to the global object. */
var root = freeGlobal || Function('return this')();
/** Detect free variable `exports`. */
var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;
/** Detect free variable `module`. */
var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports = freeModule && freeModule.exports === freeExports;
/** Detect free variable `process` from Node.js. */
var freeProcess = moduleExports && freeGlobal.process;
/** Used to access faster Node.js helpers. */
var nodeUtil = (function () {
try {
return freeProcess && freeProcess.binding && freeProcess.binding('util');
} catch (e) {}
})();
/* Node.js helper references. */
var nodeIsMap = nodeUtil && nodeUtil.isMap,
nodeIsSet = nodeUtil && nodeUtil.isSet,
nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;
/*--------------------------------------------------------------------------*/
/**
* A specialized version of `_.forEach` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns `array`.
*/
function arrayEach(array, iteratee) {
var index = -1,
length = array == null ? 0 : array.length;
while (++index < length) {
if (iteratee(array[index], index, array) === false) {
break;
}
}
return array;
}
/**
* A specialized version of `_.filter` for arrays without support for
* iteratee shorthands.
*
* @private
* @param {Array} [array] The array to iterate over.
* @param {Function} predicate The function invoked per iteration.
* @returns {Array} Returns the new filtered array.
*/
function arrayFilter(array, predicate) {
var index = -1,
length = array == null ? 0 : array.length,
resIndex = 0,
result = [];
while (++index < length) {
var value = array[index];
if (predicate(value, index, array)) {
result[resIndex++] = value;
}
}
return result;
}
/**
* Appends the elements of `values` to `array`.
*
* @private
* @param {Array} array The array to modify.
* @param {Array} values The values to append.
* @returns {Array} Returns `array`.
*/
function arrayPush(array, values) {
var index = -1,
length = values.length,
offset = array.length;
while (++index < length) {
array[offset + index] = values[index];
}
return array;
}
/**
* The base implementation of `_.times` without support for iteratee shorthands
* or max array length checks.
*
* @private
* @param {number} n The number of times to invoke `iteratee`.
* @param {Function} iteratee The function invoked per iteration.
* @returns {Array} Returns the array of results.
*/
function baseTimes(n, iteratee) {
var index = -1,
result = Array(n);
while (++index < n) {
result[index] = iteratee(index);
}
return result;
}
/**
* The base implementation of `_.unary` without support for storing metadata.
*
* @private
* @param {Function} func The function to cap arguments for.
* @returns {Function} Returns the new capped function.
*/
function baseUnary(func) {
return function (value) {
return func(value);
};
}
/**
* Gets the value at `key` of `object`.
*
* @private
* @param {Object} [object] The object to query.
* @param {string} key The key of the property to get.
* @returns {*} Returns the property value.
*/
function getValue(object, key) {
return object == null ? undefined : object[key];
}
/**
* Creates a unary function that invokes `func` with its argument transformed.
*
* @private
* @param {Function} func The function to wrap.
* @param {Function} transform The argument transform.
* @returns {Function} Returns the new function.
*/
function overArg(func, transform) {
return function (arg) {
return func(transform(arg));
};
}
/*--------------------------------------------------------------------------*/
/** Used for built-in method references. */
var arrayProto = Array.prototype,
funcProto = Function.prototype,
objectProto = Object.prototype;
/** Used to detect overreaching core-js shims. */
var coreJsData = root['__core-js_shared__'];
/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/** Used to detect methods masquerading as native. */
var maskSrcKey = (function () {
var uid = /[^.]+$/.exec((coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO) || '');
return uid ? 'Symbol(src)_1.' + uid : '';
})();
/**
* Used to resolve the
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var nativeObjectToString = objectProto.toString;
/** Used to detect if a method is native. */
var reIsNative = RegExp(
'^' +
funcToString
.call(hasOwnProperty)
.replace(reRegExpChar, '\\$&')
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') +
'$',
);
/** Built-in value references. */
var Buffer = moduleExports ? root.Buffer : undefined,
Symbol = root.Symbol,
Uint8Array = root.Uint8Array,
allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined,
getPrototype = overArg(Object.getPrototypeOf, Object),
objectCreate = Object.create,
propertyIsEnumerable = objectProto.propertyIsEnumerable,
splice = arrayProto.splice,
symToStringTag = Symbol ? Symbol.toStringTag : undefined;
var defineProperty = (function () {
try {
var func = getNative(Object, 'defineProperty');
func({}, '', {});
return func;
} catch (e) {}
})();
/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeGetSymbols = Object.getOwnPropertySymbols,
nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined,
nativeKeys = overArg(Object.keys, Object);
/* Built-in method references that are verified to be native. */
var DataView = getNative(root, 'DataView'),
Map = getNative(root, 'Map'),
Promise = getNative(root, 'Promise'),
Set = getNative(root, 'Set'),
WeakMap = getNative(root, 'WeakMap'),
nativeCreate = getNative(Object, 'create');
/** Used to lookup unminified function names. */
var realNames = {};
/** Used to detect maps, sets, and weakmaps. */
var dataViewCtorString = toSource(DataView),
mapCtorString = toSource(Map),
promiseCtorString = toSource(Promise),
setCtorString = toSource(Set),
weakMapCtorString = toSource(WeakMap);
/** Used to convert symbols to primitives and strings. */
var symbolProto = Symbol ? Symbol.prototype : undefined,
symbolValueOf = symbolProto ? symbolProto.valueOf : undefined;
/*------------------------------------------------------------------------*/
/**
* Creates a `lodash` object which wraps `value` to enable implicit method
* chain sequences. Methods that operate on and return arrays, collections,
* and functions can be chained together. Methods that retrieve a single value
* or may return a primitive value will automatically end the chain sequence
* and return the unwrapped value. Otherwise, the value must be unwrapped
* with `_#value`.
*
* Explicit chain sequences, which must be unwrapped with `_#value`, may be
* enabled using `_.chain`.
*
* The execution of chained methods is lazy, that is, it's deferred until
* `_#value` is implicitly or explicitly called.
*
* Lazy evaluation allows several methods to support shortcut fusion.
* Shortcut fusion is an optimization to merge iteratee calls; this avoids
* the creation of intermediate arrays and can greatly reduce the number of
* iteratee executions. Sections of a chain sequence qualify for shortcut
* fusion if the section is applied to an array and iteratees accept only
* one argument. The heuristic for whether a section qualifies for shortcut
* fusion is subject to change.
*
* Chaining is supported in custom builds as long as the `_#value` method is
* directly or indirectly included in the build.
*
* In addition to lodash methods, wrappers have `Array` and `String` methods.
*
* The wrapper `Array` methods are:
* `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift`
*
* The wrapper `String` methods are:
* `replace` and `split`
*
* The wrapper methods that support shortcut fusion are:
* `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`,
* `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`,
* `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray`
*
* The chainable wrapper methods are:
* `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`,
* `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`,
* `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`,
* `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`,
* `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`,
* `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`,
* `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`,
* `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`,
* `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`,
* `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`,
* `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`,
* `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`,
* `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`,
* `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`,
* `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`,
* `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`,
* `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`,
* `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`,
* `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`,
* `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`,
* `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`,
* `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`,
* `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`,
* `zipObject`, `zipObjectDeep`, and `zipWith`
*
* The wrapper methods that are **not** chainable by default are:
* `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`,
* `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`,
* `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`,
* `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`,
* `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`,
* `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`,
* `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`,
* `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`,
* `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`,
* `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`,
* `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`,
* `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`,
* `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`,
* `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`,
* `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`,
* `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`,
* `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`,
* `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`,
* `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`,
* `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`,
* `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`,
* `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`,
* `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`,
* `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`,
* `upperFirst`, `value`, and `words`
*
* @name _
* @constructor
* @category Seq
* @param {*} value The value to wrap in a `lodash` instance.
* @returns {Object} Returns the new `lodash` wrapper instance.
* @example
*
* function square(n) {
* return n * n;
* }
*
* var wrapped = _([1, 2, 3]);
*
* // Returns an unwrapped value.
* wrapped.reduce(_.add);
* // => 6
*
* // Returns a wrapped value.
* var squares = wrapped.map(square);
*
* _.isArray(squares);
* // => false
*
* _.isArray(squares.value());
* // => true
*/
function lodash() {
// No operation performed.
}
/**
* The base implementation of `_.create` without support for assigning
* properties to the created object.
*
* @private
* @param {Object} proto The object to inherit from.
* @returns {Object} Returns the new object.
*/
var baseCreate = (function () {
function object() {}
return function (proto) {
if (!isObject(proto)) {
return {};
}
if (objectCreate) {
return objectCreate(proto);
}
object.prototype = proto;
var result = new object();
object.prototype = undefined;
return result;
};
})();
/*------------------------------------------------------------------------*/
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Hash(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the hash.
*
* @private
* @name clear
* @memberOf Hash
*/
function hashClear() {
this.__data__ = nativeCreate ? nativeCreate(null) : {};
this.size = 0;
}
/**
* Removes `key` and its value from the hash.
*
* @private
* @name delete
* @memberOf Hash
* @param {Object} hash The hash to modify.
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function hashDelete(key) {
var result = this.has(key) && delete this.__data__[key];
this.size -= result ? 1 : 0;
return result;
}
/**
* Gets the hash value for `key`.
*
* @private
* @name get
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function hashGet(key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return hasOwnProperty.call(data, key) ? data[key] : undefined;
}
/**
* Checks if a hash value for `key` exists.
*
* @private
* @name has
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function hashHas(key) {
var data = this.__data__;
return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key);
}
/**
* Sets the hash `key` to `value`.
*
* @private
* @name set
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
function hashSet(key, value) {
var data = this.__data__;
this.size += this.has(key) ? 0 : 1;
data[key] = nativeCreate && value === undefined ? HASH_UNDEFINED : value;
return this;
}
// Add methods to `Hash`.
Hash.prototype.clear = hashClear;
Hash.prototype['delete'] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;
/*------------------------------------------------------------------------*/
/**
* Creates an list cache object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function ListCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the list cache.
*
* @private
* @name clear
* @memberOf ListCache
*/
function listCacheClear() {
this.__data__ = [];
this.size = 0;
}
/**
* Removes `key` and its value from the list cache.
*
* @private
* @name delete
* @memberOf ListCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function listCacheDelete(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
--this.size;
return true;
}
/**
* Gets the list cache value for `key`.
*
* @private
* @name get
* @memberOf ListCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function listCacheGet(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
return index < 0 ? undefined : data[index][1];
}
/**
* Checks if a list cache value for `key` exists.
*
* @private
* @name has
* @memberOf ListCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function listCacheHas(key) {
return assocIndexOf(this.__data__, key) > -1;
}
/**
* Sets the list cache `key` to `value`.
*
* @private
* @name set
* @memberOf ListCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the list cache instance.
*/
function listCacheSet(key, value) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
++this.size;
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
// Add methods to `ListCache`.
ListCache.prototype.clear = listCacheClear;
ListCache.prototype['delete'] = listCacheDelete;
ListCache.prototype.get = listCacheGet;
ListCache.prototype.has = listCacheHas;
ListCache.prototype.set = listCacheSet;
/*------------------------------------------------------------------------*/
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function MapCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the map.
*
* @private
* @name clear
* @memberOf MapCache
*/
function mapCacheClear() {
this.size = 0;
this.__data__ = {
hash: new Hash(),
map: new (Map || ListCache)(),
string: new Hash(),
};
}
/**
* Removes `key` and its value from the map.
*
* @private
* @name delete
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function mapCacheDelete(key) {
var result = getMapData(this, key)['delete'](key);
this.size -= result ? 1 : 0;
return result;
}
/**
* Gets the map value for `key`.
*
* @private
* @name get
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function mapCacheGet(key) {
return getMapData(this, key).get(key);
}
/**
* Checks if a map value for `key` exists.
*
* @private
* @name has
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function mapCacheHas(key) {
return getMapData(this, key).has(key);
}
/**
* Sets the map `key` to `value`.
*
* @private
* @name set
* @memberOf MapCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the map cache instance.
*/
function mapCacheSet(key, value) {
var data = getMapData(this, key),
size = data.size;
data.set(key, value);
this.size += data.size == size ? 0 : 1;
return this;
}
// Add methods to `MapCache`.
MapCache.prototype.clear = mapCacheClear;
MapCache.prototype['delete'] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;
/*------------------------------------------------------------------------*/
/**
* Creates a stack cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Stack(entries) {
var data = (this.__data__ = new ListCache(entries));
this.size = data.size;
}
/**
* Removes all key-value entries from the stack.
*
* @private
* @name clear
* @memberOf Stack
*/
function stackClear() {
this.__data__ = new ListCache();
this.size = 0;
}
/**
* Removes `key` and its value from the stack.
*
* @private
* @name delete
* @memberOf Stack
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function stackDelete(key) {
var data = this.__data__,
result = data['delete'](key);
this.size = data.size;
return result;
}
/**
* Gets the stack value for `key`.
*
* @private
* @name get
* @memberOf Stack
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function stackGet(key) {
return this.__data__.get(key);
}
/**
* Checks if a stack value for `key` exists.
*
* @private
* @name has
* @memberOf Stack
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function stackHas(key) {
return this.__data__.has(key);
}
/**
* Sets the stack `key` to `value`.
*
* @private
* @name set
* @memberOf Stack
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the stack cache instance.
*/
function stackSet(key, value) {
var data = this.__data__;
if (data instanceof ListCache) {
var pairs = data.__data__;
if (!Map || pairs.length < LARGE_ARRAY_SIZE - 1) {
pairs.push([key, value]);
this.size = ++data.size;
return this;
}
data = this.__data__ = new MapCache(pairs);
}
data.set(key, value);
this.size = data.size;
return this;
}
// Add methods to `Stack`.
Stack.prototype.clear = stackClear;
Stack.prototype['delete'] = stackDelete;
Stack.prototype.get = stackGet;
Stack.prototype.has = stackHas;
Stack.prototype.set = stackSet;
/*------------------------------------------------------------------------*/
/**
* Creates an array of the enumerable property names of the array-like `value`.
*
* @private
* @param {*} value The value to query.
* @param {boolean} inherited Specify returning inherited property names.
* @returns {Array} Returns the array of property names.
*/
function arrayLikeKeys(value, inherited) {
var isArr = isArray(value),
isArg = !isArr && isArguments(value),
isBuff = !isArr && !isArg && isBuffer(value),
isType = !isArr && !isArg && !isBuff && isTypedArray(value),
skipIndexes = isArr || isArg || isBuff || isType,
result = skipIndexes ? baseTimes(value.length, String) : [],
length = result.length;
for (var key in value) {
if (
(inherited || hasOwnProperty.call(value, key)) &&
!(
skipIndexes &&
// Safari 9 has enumerable `arguments.length` in strict mode.
(key == 'length' ||
// Node.js 0.10 has enumerable non-index properties on buffers.
(isBuff && (key == 'offset' || key == 'parent')) ||
// PhantomJS 2 has enumerable non-index properties on typed arrays.
(isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||
// Skip index properties.
isIndex(key, length))
)
) {
result.push(key);
}
}
return result;
}
/**
* Assigns `value` to `key` of `object` if the existing value is not equivalent
* using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* for equality comparisons.
*
* @private
* @param {Object} object The object to modify.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function assignValue(object, key, value) {
var objValue = object[key];
if (
!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||
(value === undefined && !(key in object))
) {
baseAssignValue(object, key, value);
}
}
/**
* Gets the index at which the `key` is found in `array` of key-value pairs.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} key The key to search for.
* @returns {number} Returns the index of the matched value, else `-1`.
*/
function assocIndexOf(array, key) {
var length = array.length;
while (length--) {
if (eq(array[length][0], key)) {
return length;
}
}
return -1;
}
/**
* The base implementation of `_.assign` without support for multiple sources
* or `customizer` functions.
*
* @private
* @param {Object} object The destination object.
* @param {Object} source The source object.
* @returns {Object} Returns `object`.
*/
function baseAssign(object, source) {
return object && copyObject(source, keys(source), object);
}
/**
* The base implementation of `_.assignIn` without support for multiple sources
* or `customizer` functions.
*
* @private
* @param {Object} object The destination object.
* @param {Object} source The source object.
* @returns {Object} Returns `object`.
*/
function baseAssignIn(object, source) {
return object && copyObject(source, keysIn(source), object);
}
/**
* The base implementation of `assignValue` and `assignMergeValue` without
* value checks.
*
* @private
* @param {Object} object The object to modify.
* @param {string} key The key of the property to assign.
* @param {*} value The value to assign.
*/
function baseAssignValue(object, key, value) {
if (key == '__proto__' && defineProperty) {
defineProperty(object, key, {
configurable: true,
enumerable: true,
value: value,
writable: true,
});
} else {
object[key] = value;
}
}
/**
* The base implementation of `_.clone` and `_.cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone.
* @param {boolean} bitmask The bitmask flags.
* 1 - Deep clone
* 2 - Flatten inherited properties
* 4 - Clone symbols
* @param {Function} [customizer] The function to customize cloning.
* @param {string} [key] The key of `value`.
* @param {Object} [object] The parent object of `value`.
* @param {Object} [stack] Tracks traversed objects and their clone counterparts.
* @returns {*} Returns the cloned value.
*/
function baseClone(value, bitmask, customizer, key, object, stack) {
var result,
isDeep = bitmask & CLONE_DEEP_FLAG,
isFlat = bitmask & CLONE_FLAT_FLAG,
isFull = bitmask & CLONE_SYMBOLS_FLAG;
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value);
}
if (result !== undefined) {
return result;
}
if (!isObject(value)) {
return value;
}
var isArr = isArray(value);
if (isArr) {
result = initCloneArray(value);
if (!isDeep) {
return copyArray(value, result);
}
} else {
var tag = getTag(value),
isFunc = tag == funcTag || tag == genTag;
if (isBuffer(value)) {
return cloneBuffer(value, isDeep);
}
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
result = isFlat || isFunc ? {} : initCloneObject(value);
if (!isDeep) {
return isFlat
? copySymbolsIn(value, baseAssignIn(result, value))
: copySymbols(value, baseAssign(result, value));
}
} else {
if (!cloneableTags[tag]) {
return object ? value : {};
}
result = initCloneByTag(value, tag, isDeep);
}
}
// Check for circular references and return its corresponding clone.
stack || (stack = new Stack());
var stacked = stack.get(value);
if (stacked) {
return stacked;
}
stack.set(value, result);
if (isSet(value)) {
value.forEach(function (subValue) {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));
});
return result;
}
if (isMap(value)) {
value.forEach(function (subValue, key) {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));
});
return result;
}
var keysFunc = isFull ? (isFlat ? getAllKeysIn : getAllKeys) : isFlat ? keysIn : keys;
var props = isArr ? undefined : keysFunc(value);
arrayEach(props || value, function (subValue, key) {
if (props) {
key = subValue;
subValue = value[key];
}
// Recursively populate clone (susceptible to call stack limits).
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
});
return result;
}
/**
* The base implementation of `getAllKeys` and `getAllKeysIn` which uses
* `keysFunc` and `symbolsFunc` to get the enumerable property names and
* symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {Function} keysFunc The function to get the keys of `object`.
* @param {Function} symbolsFunc The function to get the symbols of `object`.
* @returns {Array} Returns the array of property names and symbols.
*/
function baseGetAllKeys(object, keysFunc, symbolsFunc) {
var result = keysFunc(object);
return isArray(object) ? result : arrayPush(result, symbolsFunc(object));
}
/**
* The base implementation of `getTag` without fallbacks for buggy environments.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
function baseGetTag(value) {
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return symToStringTag && symToStringTag in Object(value)
? getRawTag(value)
: objectToString(value);
}
/**
* The base implementation of `_.isArguments`.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
*/
function baseIsArguments(value) {
return isObjectLike(value) && baseGetTag(value) == argsTag;
}
/**
* The base implementation of `_.isMap` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a map, else `false`.
*/
function baseIsMap(value) {
return isObjectLike(value) && getTag(value) == mapTag;
}
/**
* The base implementation of `_.isNative` without bad shim checks.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a native function,
* else `false`.
*/
function baseIsNative(value) {
if (!isObject(value) || isMasked(value)) {
return false;
}
var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
return pattern.test(toSource(value));
}
/**
* The base implementation of `_.isSet` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a set, else `false`.
*/
function baseIsSet(value) {
return isObjectLike(value) && getTag(value) == setTag;
}
/**
* The base implementation of `_.isTypedArray` without Node.js optimizations.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
*/
function baseIsTypedArray(value) {
return isObjectLike(value) && isLength(value.length) && !!typedArrayTags[baseGetTag(value)];
}
/**
* The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function baseKeys(object) {
if (!isPrototype(object)) {
return nativeKeys(object);
}
var result = [];
for (var key in Object(object)) {
if (hasOwnProperty.call(object, key) && key != 'constructor') {
result.push(key);
}
}
return result;
}
/**
* The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function baseKeysIn(object) {
if (!isObject(object)) {
return nativeKeysIn(object);
}
var isProto = isPrototype(object),
result = [];
for (var key in object) {
if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
result.push(key);
}
}
return result;
}
/**
* Creates a clone of `buffer`.
*
* @private
* @param {Buffer} buffer The buffer to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Buffer} Returns the cloned buffer.
*/
function cloneBuffer(buffer, isDeep) {
if (isDeep) {
return buffer.slice();
}
var length = buffer.length,
result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);
buffer.copy(result);
return result;
}
/**
* Creates a clone of `arrayBuffer`.
*
* @private
* @param {ArrayBuffer} arrayBuffer The array buffer to clone.
* @returns {ArrayBuffer} Returns the cloned array buffer.
*/
function cloneArrayBuffer(arrayBuffer) {
var result = new arrayBuffer.constructor(arrayBuffer.byteLength);
new Uint8Array(result).set(new Uint8Array(arrayBuffer));
return result;
}
/**
* Creates a clone of `dataView`.
*
* @private
* @param {Object} dataView The data view to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the cloned data view.
*/
function cloneDataView(dataView, isDeep) {
var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;
return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);
}
/**
* Creates a clone of `regexp`.
*
* @private
* @param {Object} regexp The regexp to clone.
* @returns {Object} Returns the cloned regexp.
*/
function cloneRegExp(regexp) {
var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
result.lastIndex = regexp.lastIndex;
return result;
}
/**
* Creates a clone of the `symbol` object.
*
* @private
* @param {Object} symbol The symbol object to clone.
* @returns {Object} Returns the cloned symbol object.
*/
function cloneSymbol(symbol) {
return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
}
/**
* Creates a clone of `typedArray`.
*
* @private
* @param {Object} typedArray The typed array to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the cloned typed array.
*/
function cloneTypedArray(typedArray, isDeep) {
var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
}
/**
* Copies the values of `source` to `array`.
*
* @private
* @param {Array} source The array to copy values from.
* @param {Array} [array=[]] The array to copy values to.
* @returns {Array} Returns `array`.
*/
function copyArray(source, array) {
var index = -1,
length = source.length;
array || (array = Array(length));
while (++index < length) {
array[index] = source[index];
}
return array;
}
/**
* Copies properties of `source` to `object`.
*
* @private
* @param {Object} source The object to copy properties from.
* @param {Array} props The property identifiers to copy.
* @param {Object} [object={}] The object to copy properties to.
* @param {Function} [customizer] The function to customize copied values.
* @returns {Object} Returns `object`.
*/
function copyObject(source, props, object, customizer) {
var isNew = !object;
object || (object = {});
var index = -1,
length = props.length;
while (++index < length) {
var key = props[index];
var newValue = customizer
? customizer(object[key], source[key], key, object, source)
: undefined;
if (newValue === undefined) {
newValue = source[key];
}
if (isNew) {
baseAssignValue(object, key, newValue);
} else {
assignValue(object, key, newValue);
}
}
return object;
}
/**
* Copies own symbols of `source` to `object`.
*
* @private
* @param {Object} source The object to copy symbols from.
* @param {Object} [object={}] The object to copy symbols to.
* @returns {Object} Returns `object`.
*/
function copySymbols(source, object) {
return copyObject(source, getSymbols(source), object);
}
/**
* Copies own and inherited symbols of `source` to `object`.
*
* @private
* @param {Object} source The object to copy symbols from.
* @param {Object} [object={}] The object to copy symbols to.
* @returns {Object} Returns `object`.
*/
function copySymbolsIn(source, object) {
return copyObject(source, getSymbolsIn(source), object);
}
/**
* Creates an array of own enumerable property names and symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names and symbols.
*/
function getAllKeys(object) {
return baseGetAllKeys(object, keys, getSymbols);
}
/**
* Creates an array of own and inherited enumerable property names and
* symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names and symbols.
*/
function getAllKeysIn(object) {
return baseGetAllKeys(object, keysIn, getSymbolsIn);
}
/**
* Gets the data for `map`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key) ? data[typeof key == 'string' ? 'string' : 'hash'] : data.map;
}
/**
* Gets the native function at `key` of `object`.
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the method to get.
* @returns {*} Returns the function if it's native, else `undefined`.
*/
function getNative(object, key) {
var value = getValue(object, key);
return baseIsNative(value) ? value : undefined;
}
/**
* A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the raw `toStringTag`.
*/
function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag),
tag = value[symToStringTag];
try {
value[symToStringTag] = undefined;
var unmasked = true;
} catch (e) {}
var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}
/**
* Creates an array of the own enumerable symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of symbols.
*/
var getSymbols = !nativeGetSymbols
? stubArray
: function (object) {
if (object == null) {
return [];
}
object = Object(object);
return arrayFilter(nativeGetSymbols(object), function (symbol) {
return propertyIsEnumerable.call(object, symbol);
});
};
/**
* Creates an array of the own and inherited enumerable symbols of `object`.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of symbols.
*/
var getSymbolsIn = !nativeGetSymbols
? stubArray
: function (object) {
var result = [];
while (object) {
arrayPush(result, getSymbols(object));
object = getPrototype(object);
}
return result;
};
/**
* Gets the `toStringTag` of `value`.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
var getTag = baseGetTag;
// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
if (
(DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
(Map && getTag(new Map()) != mapTag) ||
(Promise && getTag(Promise.resolve()) != promiseTag) ||
(Set && getTag(new Set()) != setTag) ||
(WeakMap && getTag(new WeakMap()) != weakMapTag)
) {
getTag = function (value) {
var result = baseGetTag(value),
Ctor = result == objectTag ? value.constructor : undefined,
ctorString = Ctor ? toSource(Ctor) : '';
if (ctorString) {
switch (ctorString) {
case dataViewCtorString:
return dataViewTag;
case mapCtorString:
return mapTag;
case promiseCtorString:
return promiseTag;
case setCtorString:
return setTag;
case weakMapCtorString:
return weakMapTag;
}
}
return result;
};
}
/**
* Initializes an array clone.
*
* @private
* @param {Array} array The array to clone.
* @returns {Array} Returns the initialized clone.
*/
function initCloneArray(array) {
var length = array.length,
result = new array.constructor(length);
// Add properties assigned by `RegExp#exec`.
if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index;
result.input = array.input;
}
return result;
}
/**
* Initializes an object clone.
*
* @private
* @param {Object} object The object to clone.
* @returns {Object} Returns the initialized clone.
*/
function initCloneObject(object) {
return typeof object.constructor == 'function' && !isPrototype(object)
? baseCreate(getPrototype(object))
: {};
}
/**
* Initializes an object clone based on its `toStringTag`.
*
* **Note:** This function only supports cloning values with tags of
* `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`.
*
* @private
* @param {Object} object The object to clone.
* @param {string} tag The `toStringTag` of the object to clone.
* @param {boolean} [isDeep] Specify a deep clone.
* @returns {Object} Returns the initialized clone.
*/
function initCloneByTag(object, tag, isDeep) {
var Ctor = object.constructor;
switch (tag) {
case arrayBufferTag:
return cloneArrayBuffer(object);
case boolTag:
case dateTag:
return new Ctor(+object);
case dataViewTag:
return cloneDataView(object, isDeep);
case float32Tag:
case float64Tag:
case int8Tag:
case int16Tag:
case int32Tag:
case uint8Tag:
case uint8ClampedTag:
case uint16Tag:
case uint32Tag:
return cloneTypedArray(object, isDeep);
case mapTag:
return new Ctor();
case numberTag:
case stringTag:
return new Ctor(object);
case regexpTag:
return cloneRegExp(object);
case setTag:
return new Ctor();
case symbolTag:
return cloneSymbol(object);
}
}
/**
* Checks if `value` is a valid array-like index.
*
* @private
* @param {*} value The value to check.
* @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
*/
function isIndex(value, length) {
var type = typeof value;
length = length == null ? MAX_SAFE_INTEGER : length;
return (
!!length &&
(type == 'number' || (type != 'symbol' && reIsUint.test(value))) &&
value > -1 &&
value % 1 == 0 &&
value < length
);
}
/**
* Checks if `value` is suitable for use as unique object key.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is suitable, else `false`.
*/
function isKeyable(value) {
var type = typeof value;
return type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean'
? value !== '__proto__'
: value === null;
}
/**
* Checks if `func` has its source masked.
*
* @private
* @param {Function} func The function to check.
* @returns {boolean} Returns `true` if `func` is masked, else `false`.
*/
function isMasked(func) {
return !!maskSrcKey && maskSrcKey in func;
}
/**
* Checks if `value` is likely a prototype object.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
*/
function isPrototype(value) {
var Ctor = value && value.constructor,
proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;
return value === proto;
}
/**
* This function is like
* [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
* except that it includes inherited enumerable properties.
*
* @private
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
*/
function nativeKeysIn(object) {
var result = [];
if (object != null) {
for (var key in Object(object)) {
result.push(key);
}
}
return result;
}
/**
* Converts `value` to a string using `Object.prototype.toString`.
*
* @private
* @param {*} value The value to convert.
* @returns {string} Returns the converted string.
*/
function objectToString(value) {
return nativeObjectToString.call(value);
}
/**
* Converts `func` to its source code.
*
* @private
* @param {Function} func The function to convert.
* @returns {string} Returns the source code.
*/
function toSource(func) {
if (func != null) {
try {
return funcToString.call(func);
} catch (e) {}
try {
return func + '';
} catch (e) {}
}
return '';
}
/*------------------------------------------------------------------------*/
/**
* Creates a shallow clone of `value`.
*
* **Note:** This method is loosely based on the
* [structured clone algorithm](https://mdn.io/Structured_clone_algorithm)
* and supports cloning arrays, array buffers, booleans, date objects, maps,
* numbers, `Object` objects, regexes, sets, strings, symbols, and typed
* arrays. The own enumerable properties of `arguments` objects are cloned
* as plain objects. An empty object is returned for uncloneable values such
* as error objects, functions, DOM nodes, and WeakMaps.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to clone.
* @returns {*} Returns the cloned value.
* @see _.cloneDeep
* @example
*
* var objects = [{ 'a': 1 }, { 'b': 2 }];
*
* var shallow = _.clone(objects);
* console.log(shallow[0] === objects[0]);
* // => true
*/
function clone(value) {
return baseClone(value, CLONE_SYMBOLS_FLAG);
}
/**
* This method is like `_.clone` except that it recursively clones `value`.
*
* @static
* @memberOf _
* @since 1.0.0
* @category Lang
* @param {*} value The value to recursively clone.
* @returns {*} Returns the deep cloned value.
* @see _.clone
* @example
*
* var objects = [{ 'a': 1 }, { 'b': 2 }];
*
* var deep = _.cloneDeep(objects);
* console.log(deep[0] === objects[0]);
* // => false
*/
function cloneDeep(value) {
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}
/**
* Performs a
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* comparison between two values to determine if they are equivalent.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
* @example
*
* var object = { 'a': 1 };
* var other = { 'a': 1 };
*
* _.eq(object, object);
* // => true
*
* _.eq(object, other);
* // => false
*
* _.eq('a', 'a');
* // => true
*
* _.eq('a', Object('a'));
* // => false
*
* _.eq(NaN, NaN);
* // => true
*/
function eq(value, other) {
return value === other || (value !== value && other !== other);
}
/**
* Checks if `value` is likely an `arguments` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
* else `false`.
* @example
*
* _.isArguments(function() { return arguments; }());
* // => true
*
* _.isArguments([1, 2, 3]);
* // => false
*/
var isArguments = baseIsArguments(
(function () {
return arguments;
})(),
)
? baseIsArguments
: function (value) {
return (
isObjectLike(value) &&
hasOwnProperty.call(value, 'callee') &&
!propertyIsEnumerable.call(value, 'callee')
);
};
/**
* Checks if `value` is classified as an `Array` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an array, else `false`.
* @example
*
* _.isArray([1, 2, 3]);
* // => true
*
* _.isArray(document.body.children);
* // => false
*
* _.isArray('abc');
* // => false
*
* _.isArray(_.noop);
* // => false
*/
var isArray = Array.isArray;
/**
* Checks if `value` is array-like. A value is considered array-like if it's
* not a function and has a `value.length` that's an integer greater than or
* equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is array-like, else `false`.
* @example
*
* _.isArrayLike([1, 2, 3]);
* // => true
*
* _.isArrayLike(document.body.children);
* // => true
*
* _.isArrayLike('abc');
* // => true
*
* _.isArrayLike(_.noop);
* // => false
*/
function isArrayLike(value) {
return value != null && isLength(value.length) && !isFunction(value);
}
/**
* Checks if `value` is a buffer.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
* @example
*
* _.isBuffer(new Buffer(2));
* // => true
*
* _.isBuffer(new Uint8Array(2));
* // => false
*/
var isBuffer = nativeIsBuffer || stubFalse;
/**
* Checks if `value` is classified as a `Function` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a function, else `false`.
* @example
*
* _.isFunction(_);
* // => true
*
* _.isFunction(/abc/);
* // => false
*/
function isFunction(value) {
if (!isObject(value)) {
return false;
}
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 9 which returns 'object' for typed arrays and other constructors.
var tag = baseGetTag(value);
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}
/**
* Checks if `value` is a valid array-like length.
*
* **Note:** This method is loosely based on
* [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
* @example
*
* _.isLength(3);
* // => true
*
* _.isLength(Number.MIN_VALUE);
* // => false
*
* _.isLength(Infinity);
* // => false
*
* _.isLength('3');
* // => false
*/
function isLength(value) {
return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* _.isObjectLike({});
* // => true
*
* _.isObjectLike([1, 2, 3]);
* // => true
*
* _.isObjectLike(_.noop);
* // => false
*
* _.isObjectLike(null);
* // => false
*/
function isObjectLike(value) {
return value != null && typeof value == 'object';
}
/**
* Checks if `value` is classified as a `Map` object.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a map, else `false`.
* @example
*
* _.isMap(new Map);
* // => true
*
* _.isMap(new WeakMap);
* // => false
*/
var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap;
/**
* Checks if `value` is classified as a `Set` object.
*
* @static
* @memberOf _
* @since 4.3.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a set, else `false`.
* @example
*
* _.isSet(new Set);
* // => true
*
* _.isSet(new WeakSet);
* // => false
*/
var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet;
/**
* Checks if `value` is classified as a typed array.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
* @example
*
* _.isTypedArray(new Uint8Array);
* // => true
*
* _.isTypedArray([]);
* // => false
*/
var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;
/*------------------------------------------------------------------------*/
/**
* Creates an array of the own enumerable property names of `object`.
*
* **Note:** Non-object values are coerced to objects. See the
* [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
* for more details.
*
* @static
* @since 0.1.0
* @memberOf _
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.keys(new Foo);
* // => ['a', 'b'] (iteration order is not guaranteed)
*
* _.keys('hi');
* // => ['0', '1']
*/
function keys(object) {
return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}
/**
* Creates an array of the own and inherited enumerable property names of `object`.
*
* **Note:** Non-object values are coerced to objects.
*
* @static
* @memberOf _
* @since 3.0.0
* @category Object
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property names.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.keysIn(new Foo);
* // => ['a', 'b', 'c'] (iteration order is not guaranteed)
*/
function keysIn(object) {
return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);
}
/*------------------------------------------------------------------------*/
/**
* This method returns a new empty array.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {Array} Returns the new empty array.
* @example
*
* var arrays = _.times(2, _.stubArray);
*
* console.log(arrays);
* // => [[], []]
*
* console.log(arrays[0] === arrays[1]);
* // => false
*/
function stubArray() {
return [];
}
/**
* This method returns `false`.
*
* @static
* @memberOf _
* @since 4.13.0
* @category Util
* @returns {boolean} Returns `false`.
* @example
*
* _.times(2, _.stubFalse);
* // => [false, false]
*/
function stubFalse() {
return false;
}
/*------------------------------------------------------------------------*/
// Add methods that return wrapped values in chain sequences.
lodash.keys = keys;
lodash.keysIn = keysIn;
/*------------------------------------------------------------------------*/
// Add methods that return unwrapped values in chain sequences.
lodash.clone = clone;
lodash.cloneDeep = cloneDeep;
lodash.eq = eq;
lodash.isArguments = isArguments;
lodash.isArray = isArray;
lodash.isArrayLike = isArrayLike;
lodash.isBuffer = isBuffer;
lodash.isFunction = isFunction;
lodash.isLength = isLength;
lodash.isMap = isMap;
lodash.isObject = isObject;
lodash.isObjectLike = isObjectLike;
lodash.isSet = isSet;
lodash.isTypedArray = isTypedArray;
lodash.stubArray = stubArray;
lodash.stubFalse = stubFalse;
/*------------------------------------------------------------------------*/
/**
* The semantic version number.
*
* @static
* @memberOf _
* @type {string}
*/
lodash.VERSION = VERSION;
/*--------------------------------------------------------------------------*/
if (freeModule) {
// Export for Node.js.
(freeModule.exports = lodash)._ = lodash;
// Export for CommonJS support.
freeExports._ = lodash;
}
}).call(this);
================================================
FILE: lib/utils/createModifier.js
================================================
'use strict';
const { asArray, isString, isFunction, isPlainObject } = require('./objectUtils');
function createModifier({ modelClass, modifier, modifiers }) {
const modelModifiers = modelClass ? modelClass.getModifiers() : {};
const modifierFunctions = asArray(modifier).map((modifier) => {
let modify = null;
if (isString(modifier)) {
modify = (modifiers && modifiers[modifier]) || modelModifiers[modifier];
// Modifiers can be pointers to other modifiers. Call this function recursively.
if (modify && !isFunction(modify)) {
return createModifier({ modelClass, modifier: modify, modifiers });
}
} else if (isFunction(modifier)) {
modify = modifier;
} else if (isPlainObject(modifier)) {
modify = (builder) => builder.where(modifier);
} else if (Array.isArray(modifier)) {
return createModifier({ modelClass, modifier, modifiers });
}
if (!modify) {
modify = (builder) => modelClass.modifierNotFound(builder, modifier);
}
return modify;
});
return (builder, ...args) => {
for (const modifier of modifierFunctions) {
modifier.call(builder, builder, ...args);
}
};
}
module.exports = {
createModifier,
};
================================================
FILE: lib/utils/deprecate.js
================================================
'use strict';
const LOGGED_DEPRECATIONS = new Set();
function deprecate(message) {
// Only log deprecation messages once.
if (!LOGGED_DEPRECATIONS.has(message)) {
LOGGED_DEPRECATIONS.add(message);
console.warn(message);
}
}
module.exports = {
deprecate,
};
================================================
FILE: lib/utils/identifierMapping.js
================================================
'use strict';
const { isObject } = require('./objectUtils');
// Super fast memoize for single argument functions.
function memoize(func) {
const cache = new Map();
return (input) => {
let output = cache.get(input);
if (output === undefined) {
output = func(input);
cache.set(input, output);
}
return output;
};
}
// camelCase to snake_case converter that also works with non-ascii characters
// This is needed especially so that aliases containing the `:` character,
// objection uses internally, work.
function snakeCase(
str,
{
upperCase = false,
underscoreBeforeDigits = false,
underscoreBetweenUppercaseLetters = false,
} = {},
) {
if (str.length === 0) {
return str;
}
const upper = str.toUpperCase();
const lower = str.toLowerCase();
let out = lower[0];
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
const prevChar = str[i - 1];
const upperChar = upper[i];
const prevUpperChar = upper[i - 1];
const lowerChar = lower[i];
const prevLowerChar = lower[i - 1];
// If underScoreBeforeDigits is true then, well, insert an underscore
// before digits :). Only the first digit gets an underscore if
// there are multiple.
if (underscoreBeforeDigits && isDigit(char) && !isDigit(prevChar)) {
out += '_' + char;
continue;
}
// Test if `char` is an upper-case character and that the character
// actually has different upper and lower case versions.
if (char === upperChar && upperChar !== lowerChar) {
const prevCharacterIsUppercase =
prevChar === prevUpperChar && prevUpperChar !== prevLowerChar;
// If underscoreBetweenUppercaseLetters is true, we always place an underscore
// before consecutive uppercase letters (e.g. "fooBAR" becomes "foo_b_a_r").
// Otherwise, we don't (e.g. "fooBAR" becomes "foo_bar").
if (underscoreBetweenUppercaseLetters || !prevCharacterIsUppercase) {
out += '_' + lowerChar;
} else {
out += lowerChar;
}
} else {
out += char;
}
}
if (upperCase) {
return out.toUpperCase();
} else {
return out;
}
}
// snake_case to camelCase converter that simply reverses
// the actions done by `snakeCase` function.
function camelCase(str, { upperCase = false } = {}) {
if (str.length === 0) {
return str;
}
if (upperCase && isAllUpperCaseSnakeCase(str)) {
// Only convert to lower case if the string is all upper
// case snake_case. This allowes camelCase strings to go
// through without changing.
str = str.toLowerCase();
}
let out = str[0];
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
const prevChar = str[i - 1];
if (char !== '_') {
if (prevChar === '_') {
out += char.toUpperCase();
} else {
out += char;
}
}
}
return out;
}
function isAllUpperCaseSnakeCase(str) {
for (let i = 1, l = str.length; i < l; ++i) {
const char = str[i];
if (char !== '_' && char !== char.toUpperCase()) {
return false;
}
}
return true;
}
function isDigit(char) {
return char >= '0' && char <= '9';
}
// Returns a function that splits the inputs string into pieces using `separator`,
// only calls `mapper` for the last part and concatenates the string back together.
// If no separators are found, `mapper` is called for the entire string.
function mapLastPart(mapper, separator) {
return (str) => {
if (!str) return str;
const idx = str.lastIndexOf(separator);
const mapped = mapper(str.slice(idx + separator.length));
return str.slice(0, idx + separator.length) + mapped;
};
}
// Returns a function that takes an object as an input and maps the object's keys
// using `mapper`. If the input is not an object, the input is returned unchanged.
function keyMapper(mapper) {
return (obj) => {
if (!isObject(obj) || Array.isArray(obj)) {
return obj;
}
const keys = Object.keys(obj);
const out = {};
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
out[mapper(key)] = obj[key];
}
return out;
};
}
function snakeCaseMappers(opt = {}) {
return {
parse: keyMapper(memoize((str) => camelCase(str, opt))),
format: keyMapper(memoize((str) => snakeCase(str, opt))),
};
}
function knexIdentifierMappers({ parse, format, idSeparator = ':' } = {}) {
const formatId = memoize(mapLastPart(format, idSeparator));
const parseId = memoize(mapLastPart(parse, idSeparator));
const parseKeys = keyMapper(parseId);
return {
wrapIdentifier(identifier, origWrap) {
return origWrap(formatId(identifier));
},
postProcessResponse(result) {
if (Array.isArray(result)) {
const output = new Array(result.length);
for (let i = 0, l = result.length; i < l; ++i) {
output[i] = parseKeys(result[i]);
}
return output;
} else {
return parseKeys(result);
}
},
};
}
function knexSnakeCaseMappers(opt = {}) {
return knexIdentifierMappers({
parse: (str) => camelCase(str, opt),
format: (str) => snakeCase(str, opt),
});
}
function knexIdentifierMapping(colToProp) {
const propToCol = Object.keys(colToProp).reduce((propToCol, column) => {
propToCol[colToProp[column]] = column;
return propToCol;
}, {});
return knexIdentifierMappers({
parse: (column) => colToProp[column] || column,
format: (prop) => propToCol[prop] || prop,
});
}
module.exports = {
snakeCase,
camelCase,
snakeCaseMappers,
knexSnakeCaseMappers,
knexIdentifierMappers,
knexIdentifierMapping,
camelCaseKeys: keyMapper(memoize(camelCase)),
snakeCaseKeys: keyMapper(memoize(snakeCase)),
};
================================================
FILE: lib/utils/internalPropUtils.js
================================================
'use strict';
const INTERNAL_PROP_PREFIX = '$';
function isInternalProp(propName) {
return propName[0] === INTERNAL_PROP_PREFIX;
}
module.exports = {
isInternalProp,
};
================================================
FILE: lib/utils/knexUtils.js
================================================
'use strict';
const { isObject, isFunction } = require('../utils/objectUtils');
function getDialect(knex) {
const type = typeof knex;
return (
(knex !== null &&
(type === 'object' || type === 'function') &&
knex.client &&
knex.client.dialect) ||
null
);
}
function isPostgres(knex) {
return getDialect(knex) === 'postgresql';
}
function isOracle(knex) {
const dialect = getDialect(knex);
return dialect === 'oracle' || dialect === 'oracledb';
}
function isMySql(knex) {
const dialect = getDialect(knex);
return dialect === 'mysql' || dialect === 'mysql2';
}
function isSqlite(knex) {
return getDialect(knex) === 'sqlite3';
}
function isMsSql(knex) {
return getDialect(knex) === 'mssql';
}
function isKnexQueryBuilder(value) {
return (
hasConstructor(value) &&
isFunction(value.select) &&
isFunction(value.column) &&
value.select === value.column &&
'client' in value
);
}
function isKnexJoinBuilder(value) {
return hasConstructor(value) && value.grouping === 'join' && 'joinType' in value;
}
function isKnexRaw(value) {
return hasConstructor(value) && value.isRawInstance && 'client' in value;
}
function isKnexTransaction(knex) {
return !!getDialect(knex) && isFunction(knex.commit) && isFunction(knex.rollback);
}
function hasConstructor(value) {
return isObject(value) && isFunction(value.constructor);
}
module.exports = {
getDialect,
isPostgres,
isMySql,
isSqlite,
isMsSql,
isOracle,
isKnexQueryBuilder,
isKnexJoinBuilder,
isKnexRaw,
isKnexTransaction,
};
================================================
FILE: lib/utils/mixin.js
================================================
'use strict';
const { flatten } = require('./objectUtils');
function mixin() {
const args = flatten(arguments);
const mixins = args.slice(1);
return mixins.reduce((Class, mixinFunc) => {
return mixinFunc(Class);
}, args[0]);
}
function compose() {
const mixins = flatten(arguments);
return function (Class) {
return mixin(Class, mixins);
};
}
module.exports = {
compose,
mixin,
};
================================================
FILE: lib/utils/normalizeIds.js
================================================
'use strict';
const { isObject } = require('../utils/objectUtils');
// ids is of type RelationProperty.
function normalizeIds(ids, prop, opt) {
opt = opt || {};
let isComposite = prop.size > 1;
let ret;
if (isComposite) {
// For composite ids these are okay:
//
// 1. [1, 'foo', 4]
// 2. {a: 1, b: 'foo', c: 4}
// 3. [[1, 'foo', 4], [4, 'bar', 1]]
// 4. [{a: 1, b: 'foo', c: 4}, {a: 4, b: 'bar', c: 1}]
//
if (Array.isArray(ids)) {
if (Array.isArray(ids[0])) {
ret = new Array(ids.length);
// 3.
for (let i = 0, l = ids.length; i < l; ++i) {
ret[i] = convertIdArrayToObject(ids[i], prop);
}
} else if (isObject(ids[0])) {
ret = new Array(ids.length);
// 4.
for (let i = 0, l = ids.length; i < l; ++i) {
ret[i] = ensureObject(ids[i], prop);
}
} else {
// 1.
ret = [convertIdArrayToObject(ids, prop)];
}
} else if (isObject(ids)) {
// 2.
ret = [ids];
} else {
throw new Error(`invalid composite key ${JSON.stringify(ids)}`);
}
} else {
// For non-composite ids, these are okay:
//
// 1. 1
// 2. {id: 1}
// 3. [1, 'foo', 4]
// 4. [{id: 1}, {id: 'foo'}, {id: 4}]
//
if (Array.isArray(ids)) {
if (isObject(ids[0])) {
ret = new Array(ids.length);
// 4.
for (let i = 0, l = ids.length; i < l; ++i) {
ret[i] = ensureObject(ids[i]);
}
} else {
ret = new Array(ids.length);
// 3.
for (let i = 0, l = ids.length; i < l; ++i) {
ret[i] = {};
prop.setProp(ret[i], 0, ids[i]);
}
}
} else if (isObject(ids)) {
// 2.
ret = [ids];
} else {
// 1.
const obj = {};
prop.setProp(obj, 0, ids);
ret = [obj];
}
}
checkProperties(ret, prop);
if (opt.arrayOutput) {
return normalizedToArray(ret, prop);
} else {
return ret;
}
}
function convertIdArrayToObject(ids, prop) {
if (!Array.isArray(ids)) {
throw new Error(`invalid composite key ${JSON.stringify(ids)}`);
}
if (ids.length != prop.size) {
throw new Error(`composite identifier ${JSON.stringify(ids)} should have ${prop.size} values`);
}
const obj = {};
for (let i = 0; i < ids.length; ++i) {
prop.setProp(obj, i, ids[i]);
}
return obj;
}
function ensureObject(ids) {
if (isObject(ids)) {
return ids;
} else {
throw new Error(`invalid composite key ${JSON.stringify(ids)}`);
}
}
function checkProperties(ret, prop) {
for (let i = 0, l = ret.length; i < l; ++i) {
const obj = ret[i];
for (let j = 0, lp = prop.size; j < lp; ++j) {
const val = prop.getProp(obj, j);
if (typeof val === 'undefined') {
throw new Error(
`expected id ${JSON.stringify(obj)} to have property ${prop.propDescription(j)}`,
);
}
}
}
}
function normalizedToArray(ret, prop) {
const arr = new Array(ret.length);
for (let i = 0, l = ret.length; i < l; ++i) {
arr[i] = prop.getProps(ret[i]);
}
return arr;
}
module.exports = {
normalizeIds,
};
================================================
FILE: lib/utils/objectUtils.js
================================================
'use strict';
const { clone, cloneDeep } = require('./clone');
const SMALL_ARRAY_SIZE = 10;
function isEmpty(item) {
if (Array.isArray(item) || Buffer.isBuffer(item)) {
return item.length === 0;
} else if (isObject(item)) {
return Object.keys(item).length === 0;
} else {
return true;
}
}
function isObject(value) {
return value !== null && typeof value === 'object';
}
// Quick and dirty check if an object is a plain object and not
// for example an instance of some class.
function isPlainObject(value) {
return (
isObject(value) &&
(!value.constructor || value.constructor === Object) &&
(!value.toString || value.toString === Object.prototype.toString)
);
}
function isFunction(value) {
return typeof value === 'function';
}
function isRegExp(value) {
return value instanceof RegExp;
}
function isString(value) {
return typeof value === 'string';
}
function isNumber(value) {
return typeof value === 'number';
}
function asArray(value) {
return Array.isArray(value) ? value : [value];
}
function asSingle(value) {
return Array.isArray(value) ? value[0] : value;
}
function uniqBy(items, keyGetter = null) {
const map = new Map();
for (let i = 0, l = items.length; i < l; ++i) {
const item = items[i];
const key = keyGetter !== null ? keyGetter(item) : item;
map.set(key, item);
}
return Array.from(map.values());
}
function groupBy(items, keyGetter = null) {
const groups = new Map();
for (const item of items) {
const key = keyGetter !== null ? keyGetter(item) : item;
let group = groups.get(key);
if (!group) {
group = [];
groups.set(key, group);
}
group.push(item);
}
return groups;
}
function omit(obj, keysToOmit) {
keysToOmit = asArray(keysToOmit);
const keys = Object.keys(obj);
const out = {};
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
if (!keysToOmit.includes(key)) {
out[key] = obj[key];
}
}
return out;
}
function difference(arr1, arr2) {
const arr2Set = new Set(arr2);
const diff = [];
for (let i = 0; i < arr1.length; ++i) {
const value = arr1[i];
if (!arr2Set.has(value)) {
diff.push(value);
}
}
return diff;
}
function union(arr1, arr2) {
if (arr1.length < SMALL_ARRAY_SIZE && arr2.length < SMALL_ARRAY_SIZE) {
return unionSmall(arr1, arr2);
} else {
return unionGeneric(arr1, arr2);
}
}
function unionSmall(arr1, arr2) {
const all = arr1.slice();
for (let i = 0, l = arr2.length; i < l; ++i) {
const item = arr2[i];
if (all.indexOf(item) === -1) {
all.push(item);
}
}
return all;
}
function unionGeneric(arr1, arr2) {
const all = new Set();
for (let i = 0; i < arr1.length; ++i) {
all.add(arr1[i]);
}
for (let i = 0; i < arr2.length; ++i) {
all.add(arr2[i]);
}
return Array.from(all);
}
function last(arr) {
return arr[arr.length - 1];
}
function upperFirst(str) {
return str[0].toUpperCase() + str.substring(1);
}
function values(obj) {
if (isObject(obj)) {
const keys = Object.keys(obj);
const values = new Array(keys.length);
for (let i = 0, l = keys.length; i < l; ++i) {
values[i] = obj[keys[i]];
}
return values;
} else {
return [];
}
}
function once(func) {
let called = false;
let value = undefined;
return function () {
if (called === false) {
called = true;
value = func.apply(this, arguments);
}
return value;
};
}
function flatten(arrays) {
const out = [];
for (let i = 0, l = arrays.length; i < l; ++i) {
const value = arrays[i];
if (Array.isArray(value)) {
for (let j = 0; j < value.length; ++j) {
out.push(value[j]);
}
} else {
out.push(value);
}
}
return out;
}
function get(obj, path) {
for (let i = 0, l = path.length; i < l; ++i) {
const key = path[i];
if (!isObject(obj)) {
return undefined;
}
obj = obj[key];
}
return obj;
}
function set(obj, path, value) {
const inputObj = obj;
for (let i = 0, l = path.length - 1; i < l; ++i) {
const key = path[i];
if (!isSafeKey(key)) {
return inputObj;
}
let child = obj[key];
if (!isObject(child)) {
const nextKey = path[i + 1];
if (isNaN(nextKey)) {
child = {};
} else {
child = [];
}
obj[key] = child;
}
obj = child;
}
if (path.length > 0 && isObject(obj)) {
const key = path[path.length - 1];
if (isSafeKey(key)) {
obj[key] = value;
}
}
return inputObj;
}
function zipObject(keys, values) {
const out = {};
for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
if (isSafeKey(key)) {
out[key] = values[i];
}
}
return out;
}
function chunk(arr, chunkSize) {
const out = [];
for (let i = 0, l = arr.length; i < l; ++i) {
const item = arr[i];
if (out.length === 0 || out[out.length - 1].length === chunkSize) {
out.push([]);
}
out[out.length - 1].push(item);
}
return out;
}
function jsonEquals(val1, val2) {
return jsonEqualsBase(val1, val2, compareStrict);
}
function jsonEqualsBase(val1, val2, compare) {
if (val1 === val2) {
return true;
}
return jsonEqualsSlowPath(val1, val2, compare);
}
function jsonEqualsSlowPath(val1, val2, compare) {
const type1 = typeof val1;
const type2 = typeof val2;
const isNonNullObject1 = type1 === 'object' && !compare(val1, null);
const isNonNullObject2 = type2 === 'object' && !compare(val2, null);
if (isNonNullObject1 && isNonNullObject2) {
const isArray1 = Array.isArray(val1);
const isArray2 = Array.isArray(val2);
if (isArray1 && isArray2) {
return jsonEqualsArray(val1, val2, compare);
} else if (!isArray1 && !isArray2) {
return jsonEqualsObject(val1, val2, compare);
} else {
return false;
}
} else if (isNonNullObject1 !== isNonNullObject2) {
return false;
} else {
return compare(val1, val2);
}
}
function jsonEqualsArray(arr1, arr2, compare) {
if (arr1.length !== arr2.length) {
return false;
}
for (let i = 0, l = arr1.length; i < l; ++i) {
if (!jsonEqualsBase(arr1[i], arr2[i], compare)) {
return false;
}
}
return true;
}
function jsonEqualsObject(obj1, obj2, compare) {
if (obj1.constructor === Date && obj2.constructor === Date) {
return equalsDate(obj1, obj2);
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
for (let i = 0, l = keys1.length; i < l; ++i) {
const key = keys1[i];
if (!jsonEqualsBase(obj1[key], obj2[key], compare)) {
return false;
}
}
return true;
}
function equalsDate(date1, date2) {
return date1.getTime() === date2.getTime();
}
function compareStrict(val1, val2) {
return val1 === val2;
}
function isSafeKey(key) {
return isNumber(key) || (isString(key) && key !== '__proto__');
}
function mergeMaps(map1, map2) {
const map = new Map(map1);
if (map2) {
for (const key of map2.keys()) {
map.set(key, map2.get(key));
}
}
return map;
}
module.exports = {
isEmpty,
isString,
isRegExp,
isObject,
isNumber,
isFunction,
jsonEquals,
isPlainObject,
difference,
upperFirst,
zipObject,
mergeMaps,
cloneDeep,
asSingle,
asArray,
flatten,
groupBy,
uniqBy,
values,
union,
chunk,
clone,
omit,
once,
last,
get,
set,
};
================================================
FILE: lib/utils/parseFieldExpression.js
================================================
'use strict';
const jsonFieldExpressionParser = require('../queryBuilder/parsers/jsonFieldExpressionParser');
const cache = new Map();
function parseFieldExpression(expr) {
let parsedExpr = cache.get(expr);
if (parsedExpr !== undefined) {
return parsedExpr;
} else {
parsedExpr = jsonFieldExpressionParser.parse(expr);
parsedExpr = preprocessParsedExpression(parsedExpr);
// We don't take a copy of the parsedExpr each time we
// use if from cache. Instead to make sure it's never
// mutated we deep-freeze it.
parsedExpr = freezeParsedExpr(parsedExpr);
cache.set(expr, parsedExpr);
return parsedExpr;
}
}
function preprocessParsedExpression(parsedExpr) {
const columnParts = parsedExpr.columnName.split('.').map((part) => part.trim());
parsedExpr.column = columnParts[columnParts.length - 1];
if (columnParts.length >= 2) {
parsedExpr.table = columnParts.slice(0, columnParts.length - 1).join('.');
} else {
parsedExpr.table = null;
}
return parsedExpr;
}
function freezeParsedExpr(parsedExpr) {
for (const access of parsedExpr.access) {
Object.freeze(access);
}
Object.freeze(parsedExpr.access);
Object.freeze(parsedExpr);
return parsedExpr;
}
module.exports = {
parseFieldExpression,
};
================================================
FILE: lib/utils/promiseUtils/after.js
================================================
'use strict';
const { isPromise } = require('./isPromise');
// Call `func` after `obj` has been resolved. Call `func` synchronously if
// `obj` is not a promise for performance reasons.
function after(obj, func) {
if (isPromise(obj)) {
return obj.then(func);
} else {
return func(obj);
}
}
module.exports = {
after,
};
================================================
FILE: lib/utils/promiseUtils/afterReturn.js
================================================
'use strict';
const { isPromise } = require('./isPromise');
// Return `returnValue` after `obj` has been resolved. Return `returnValue`
// synchronously if `obj` is not a promise for performance reasons.
function afterReturn(obj, returnValue) {
if (isPromise(obj)) {
return obj.then(() => returnValue);
} else {
return returnValue;
}
}
module.exports = {
afterReturn,
};
================================================
FILE: lib/utils/promiseUtils/index.js
================================================
'use strict';
const { isPromise } = require('./isPromise');
const { after } = require('./after');
const { afterReturn } = require('./afterReturn');
const { mapAfterAllReturn } = require('./mapAfterAllReturn');
const { promiseMap } = require('./map');
const { promiseTry } = require('./try');
module.exports = {
isPromise,
after,
afterReturn,
mapAfterAllReturn,
map: promiseMap,
try: promiseTry,
};
================================================
FILE: lib/utils/promiseUtils/isPromise.js
================================================
'use strict';
const { isObject, isFunction } = require('../objectUtils');
function isPromise(obj) {
return isObject(obj) && isFunction(obj.then);
}
module.exports = {
isPromise,
};
================================================
FILE: lib/utils/promiseUtils/map.js
================================================
'use strict';
const { isPromise } = require('./isPromise');
// Works like Bluebird.map.
function promiseMap(items, mapper, opt) {
switch (items.length) {
case 0:
return mapZero();
case 1:
return mapOne(items, mapper);
default:
return mapMany(items, mapper, opt);
}
}
function mapZero() {
return Promise.resolve([]);
}
function mapOne(items, mapper) {
try {
const maybePromise = mapper(items[0], 0);
if (isPromise(maybePromise)) {
return maybePromise.then(wrapArray);
} else {
return Promise.resolve(wrapArray(maybePromise));
}
} catch (err) {
return Promise.reject(err);
}
}
function wrapArray(item) {
return [item];
}
function mapMany(items, mapper, opt = {}) {
return new Promise((resolve, reject) => {
const concurrency = opt.concurrency || Number.MAX_SAFE_INTEGER;
const ctx = {
reject,
resolve,
rejected: false,
index: 0,
numFinished: 0,
results: new Array(items.length),
items,
mapper,
};
while (ctx.index < concurrency && ctx.index < items.length && !ctx.rejected) {
executeNext(ctx);
}
});
}
function executeNext(ctx) {
try {
if (ctx.rejected) {
return;
}
const index = ctx.index++;
const item = ctx.items[index];
const maybePromise = ctx.mapper(item, index);
if (isPromise(maybePromise)) {
maybePromise
.then((result) => afterExecute(ctx, result, index))
.catch((err) => onError(ctx, err));
} else {
process.nextTick(() => afterExecute(ctx, maybePromise, index));
}
return null;
} catch (err) {
onError(ctx, err);
}
}
function afterExecute(ctx, result, index) {
if (ctx.rejected) {
return null;
}
ctx.results[index] = result;
ctx.numFinished++;
if (ctx.numFinished === ctx.items.length) {
ctx.resolve(ctx.results);
}
if (ctx.index < ctx.items.length) {
executeNext(ctx);
}
return null;
}
function onError(ctx, err) {
ctx.rejected = true;
ctx.reject(err);
}
module.exports = {
promiseMap,
};
================================================
FILE: lib/utils/promiseUtils/mapAfterAllReturn.js
================================================
'use strict';
const { isPromise } = require('./isPromise');
// Map `arr` with `mapper` and after that return `returnValue`. If none of
// the mapped values is a promise, return synchronously for performance
// reasons.
function mapAfterAllReturn(arr, mapper, returnValue) {
const results = new Array(arr.length);
let containsPromise = false;
for (let i = 0, l = arr.length; i < l; ++i) {
results[i] = mapper(arr[i]);
if (isPromise(results[i])) {
containsPromise = true;
}
}
if (containsPromise) {
return Promise.all(results).then(() => returnValue);
} else {
return returnValue;
}
}
module.exports = {
mapAfterAllReturn,
};
================================================
FILE: lib/utils/promiseUtils/try.js
================================================
'use strict';
const { isPromise } = require('./isPromise');
// Works like Bluebird.try.
function promiseTry(callback) {
try {
const maybePromise = callback();
if (isPromise(maybePromise)) {
return maybePromise;
} else {
return Promise.resolve(maybePromise);
}
} catch (err) {
return Promise.reject(err);
}
}
module.exports = {
promiseTry,
};
================================================
FILE: lib/utils/resolveModel.js
================================================
'use strict';
const path = require('path');
const { isString, isFunction } = require('../utils/objectUtils');
class ResolveError extends Error {}
function resolveModel(modelRef, modelPaths, errorPrefix) {
try {
if (isString(modelRef)) {
if (isAbsolutePath(modelRef)) {
return requireModel(modelRef);
} else if (modelPaths) {
return requireUsingModelPaths(modelRef, modelPaths);
}
} else {
if (isFunction(modelRef) && !isModelClass(modelRef)) {
modelRef = modelRef();
}
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.`,
);
}
return modelRef;
}
} catch (err) {
if (err instanceof ResolveError) {
throw new Error(`${errorPrefix}: ${err.message}`);
} else {
throw err;
}
}
}
function requireUsingModelPaths(modelRef, modelPaths) {
let firstError = null;
for (const modelPath of modelPaths) {
try {
return requireModel(path.join(modelPath, modelRef));
} catch (err) {
if (firstError === null) {
firstError = err;
}
}
}
if (firstError) {
throw firstError;
} else {
throw new ResolveError(`could not resolve ${modelRef} using modelPaths`);
}
}
function requireModel(modelPath) {
/**
* Wrap path string in template literal to prevent
* warnings about Objection.JS being an expression
* in webpack builds.
* @link https://github.com/webpack/webpack/issues/196
*/
let mod = require(`${path.resolve(modelPath)}`);
let modelClass = null;
if (isModelClass(mod)) {
modelClass = mod;
} 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 (isModelClass(exp)) {
if (modelClass !== null) {
throw new ResolveError(
`path ${modelPath} exports multiple models. Don't know which one to choose.`,
);
}
modelClass = exp;
}
});
}
if (!isModelClass(modelClass)) {
throw new ResolveError(`${modelPath} is an invalid file path to a model class`);
}
return modelClass;
}
function isAbsolutePath(pth) {
return path.normalize(pth + '/') === path.normalize(path.resolve(pth) + '/');
}
function isModelClass(maybeModel) {
return isFunction(maybeModel) && maybeModel.isObjectionModelClass;
}
module.exports = {
resolveModel,
};
================================================
FILE: lib/utils/tmpColumnUtils.js
================================================
'use strict';
const OWNER_JOIN_COLUMN_ALIAS_PREFIX = 'objectiontmpjoin';
function getTempColumn(index) {
return `${OWNER_JOIN_COLUMN_ALIAS_PREFIX}${index}`;
}
function isTempColumn(col) {
return col.startsWith(OWNER_JOIN_COLUMN_ALIAS_PREFIX);
}
module.exports = {
getTempColumn,
isTempColumn,
};
================================================
FILE: package.json
================================================
{
"name": "objection",
"version": "3.1.5",
"description": "An SQL-friendly ORM for Node.js",
"main": "lib/objection.js",
"license": "MIT",
"scripts": {
"test": "npm run eslint && mocha --slow 10 --timeout 15000 --reporter spec --recursive tests --exclude \"tests/unit/relations/files/**\" && npm run test:typings",
"test:fast": "mocha --slow 10 --timeout 15000 --reporter spec --recursive tests --bail --exclude \"tests/unit/relations/files/**\"",
"test:typings": "tsc",
"prettier": "prettier --write \"{examples,lib,tests,testUtils,typings,doc}/**/*.{js,ts}\"",
"eslint": "eslint --format codeframe \"examples/**/*.js\" \"lib/**/*.js\" \"tests/**/*.js\"",
"docs:dev": "vuepress dev doc",
"docs:build": "vuepress build doc"
},
"publishConfig": {
"tag": "latest"
},
"author": {
"name": "Sami Koskimäki",
"email": "sami@jakso.me",
"url": "https://github.com/koskimas"
},
"contributors": [
"Sami Koskimäki (https://github.com/koskimas)",
"Mikael Lepistö (https://github.com/elhigu)",
"Matthew McEachen (https://github.com/mceachen)",
"Jürg Lehni (https://github.com/lehni)",
"Igor Savin (https://github.com/kibertoad)"
],
"repository": {
"type": "git",
"url": "git://github.com/vincit/objection.js.git"
},
"engines": {
"node": ">=14.0.0"
},
"keywords": [
"orm",
"knex",
"sql",
"query",
"query builder",
"postgresql",
"mysql",
"sqlite3"
],
"files": [
"README.md",
"LICENSE",
"lib/*",
"typings/*"
],
"types": "./typings/objection/index.d.ts",
"dependencies": {
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"db-errors": "^0.2.3"
},
"peerDependencies": {
"knex": ">=1.0.1"
},
"devDependencies": {
"@types/node": "^22.15.3",
"chai": "^4.3.10",
"chai-subset": "^1.6.0",
"eslint": "^8.54.0",
"eslint-formatter-codeframe": "^7.32.1",
"eslint-plugin-prettier": "^5.3.1",
"expect.js": "^0.3.1",
"knex": "^3.1.0",
"mocha": "^11.2.2",
"mysql": "^2.18.1",
"pg": "^8.15.6",
"prettier": "3.5.3",
"sqlite3": "^5.1.7",
"typescript": ">=5.8.3",
"vuepress": "1.9.10"
}
}
================================================
FILE: publish-docs.sh
================================================
#!/bin/bash
NODE_OPTIONS=--openssl-legacy-provider npm run docs:build
rm -rf ../objection-doc/*
mv doc/.vuepress/dist/* ../objection-doc/
cd ../objection-doc
git add -A
git commit -m "update docs"
git push origin gh-pages:gh-pages
================================================
FILE: reproduction-template.js
================================================
/**
* This is a simple template for bug reproductions. It contains three models `Person`, `Animal` and `Movie`.
* They create a simple IMDB-style database. Try to add minimal modifications to this file to reproduce
* your bug.
*
* install:
* npm install objection knex sqlite3 chai
*
* run:
* node reproduction-template
*/
let Model;
try {
Model = require('./').Model;
} catch (err) {
Model = require('objection').Model;
}
const Knex = require('knex');
const chai = require('chai');
async function main() {
await createSchema();
///////////////////////////////////////////////////////////////
// Your reproduction
///////////////////////////////////////////////////////////////
await Person.query().insertGraph({
firstName: 'Jennifer',
lastName: 'Lawrence',
pets: [
{
name: 'Doggo',
species: 'dog'
}
]
});
const jennifer = await Person.query()
.findOne({ firstName: 'Jennifer' })
.withGraphFetched('pets');
chai.expect(jennifer.pets[0].name).to.equal('Doggo');
}
///////////////////////////////////////////////////////////////
// Database
///////////////////////////////////////////////////////////////
const knex = Knex({
client: 'sqlite3',
useNullAsDefault: true,
debug: false,
connection: {
filename: ':memory:'
}
});
Model.knex(knex);
///////////////////////////////////////////////////////////////
// Models
///////////////////////////////////////////////////////////////
class Person extends Model {
static get tableName() {
return 'Person';
}
static get jsonSchema() {
return {
type: 'object',
required: ['firstName', 'lastName'],
properties: {
id: { type: 'integer' },
parentId: { type: ['integer', 'null'] },
firstName: { type: 'string', minLength: 1, maxLength: 255 },
lastName: { type: 'string', minLength: 1, maxLength: 255 },
age: { type: 'number' },
address: {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
zipCode: { type: 'string' }
}
}
}
};
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'Person.id',
to: 'Animal.ownerId'
}
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'Person.id',
through: {
from: 'Person_Movie.personId',
to: 'Person_Movie.movieId'
},
to: 'Movie.id'
}
},
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'Person.id',
to: 'Person.parentId'
}
},
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'Person.parentId',
to: 'Person.id'
}
}
};
}
}
class Animal extends Model {
static get tableName() {
return 'Animal';
}
static get jsonSchema() {
return {
type: 'object',
required: ['name'],
properties: {
id: { type: 'integer' },
ownerId: { type: ['integer', 'null'] },
name: { type: 'string', minLength: 1, maxLength: 255 },
species: { type: 'string', minLength: 1, maxLength: 255 }
}
};
}
static get relationMappings() {
return {
owner: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'Animal.ownerId',
to: 'Person.id'
}
}
};
}
}
class Movie extends Model {
static get tableName() {
return 'Movie';
}
static get jsonSchema() {
return {
type: 'object',
required: ['name'],
properties: {
id: { type: 'integer' },
name: { type: 'string', minLength: 1, maxLength: 255 }
}
};
}
static get relationMappings() {
return {
actors: {
relation: Model.ManyToManyRelation,
modelClass: Person,
join: {
from: 'Movie.id',
through: {
from: 'Person_Movie.movieId',
to: 'Person_Movie.personId'
},
to: 'Person.id'
}
}
};
}
}
///////////////////////////////////////////////////////////////
// Schema
///////////////////////////////////////////////////////////////
async function createSchema() {
await knex.schema
.dropTableIfExists('Person_Movie')
.dropTableIfExists('Animal')
.dropTableIfExists('Movie')
.dropTableIfExists('Person');
await knex.schema
.createTable('Person', table => {
table.increments('id').primary();
table
.integer('parentId')
.unsigned()
.references('id')
.inTable('Person');
table.string('firstName');
table.string('lastName');
table.integer('age');
table.json('address');
})
.createTable('Movie', table => {
table.increments('id').primary();
table.string('name');
})
.createTable('Animal', table => {
table.increments('id').primary();
table
.integer('ownerId')
.unsigned()
.references('id')
.inTable('Person');
table.string('name');
table.string('species');
})
.createTable('Person_Movie', table => {
table.increments('id').primary();
table
.integer('personId')
.unsigned()
.references('id')
.inTable('Person')
.onDelete('CASCADE');
table
.integer('movieId')
.unsigned()
.references('id')
.inTable('Movie')
.onDelete('CASCADE');
});
}
main()
.then(() => {
console.log('success');
return knex.destroy();
})
.catch(err => {
console.error(err);
return knex.destroy();
});
================================================
FILE: setup-test-db.js
================================================
const knex = require('knex');
// DATABASES environment variable can contain a comma separated list
// of databases to setup. Defaults to all databases.
const DATABASES = (process.env.DATABASES && process.env.DATABASES.split(',')) || [
'postgres',
'mysql',
];
async function setup() {
if (DATABASES.includes('postgres')) {
const postgres = await createKnex({
client: 'postgres',
connection: {
user: 'postgres',
host: 'localhost',
database: 'postgres',
},
});
await postgres.raw('DROP DATABASE IF EXISTS objection_test');
await postgres.raw('DROP USER IF EXISTS objection');
await postgres.raw('CREATE USER objection SUPERUSER');
await postgres.raw('CREATE DATABASE objection_test');
await postgres.destroy();
}
if (DATABASES.includes('mysql')) {
const mysql = await createKnex({
client: 'mysql',
connection: {
user: 'root',
host: 'localhost',
},
});
await mysql.raw('DROP DATABASE IF EXISTS objection_test');
await mysql.raw('DROP USER IF EXISTS objection');
await mysql.raw('CREATE USER objection');
await mysql.raw('GRANT ALL PRIVILEGES ON *.* TO objection');
await mysql.raw('CREATE DATABASE objection_test');
await mysql.destroy();
}
}
async function createKnex(config) {
const startTime = new Date();
while (true) {
try {
const knexInstance = knex(config);
await knexInstance.raw('SELECT 1');
return knexInstance;
} catch (err) {
const now = new Date();
if (now.getTime() - startTime.getTime() > 60000) {
process.exit(1);
} else {
console.log(`failed to connect to ${config.client}. Trying again soon`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
}
setup();
================================================
FILE: testUtils/TestSession.js
================================================
const _ = require('lodash');
const path = require('path');
const Promise = require('bluebird');
const knexUtils = require('../lib/utils/knexUtils');
const { Model, transaction, snakeCaseMappers, ref } = require('../');
const chai = require('chai');
chai.use(require('chai-subset'));
class TestSession {
static init() {
if (this.staticInitCalled) {
return;
}
registerUnhandledRejectionHandler();
this.staticInitCalled = true;
}
constructor(opt) {
TestSession.init();
this.opt = opt;
this.knex = this.createKnex(opt);
this.unboundModels = this.createModels();
this.models = _.mapValues(this.unboundModels, (model) => model.bindKnex(this.knex));
}
createKnex() {
return require('knex')(this.opt.knexConfig);
}
createModels() {
class Model1 extends Model {
static get tableName() {
return 'Model1';
}
// Function instead of getter on purpose.
static idColumn() {
return 'id';
}
static get modifiers() {
return {
orderById: (builder) => builder.orderBy('Model1.id'),
'select:id': (builder) => builder.select(this.ref('id')),
'select:model1Prop1': (builder) => builder.select('model1Prop1'),
'select:model1Prop1Aliased': (builder) =>
builder.select('model1Prop1 as aliasedInFilter'),
'orderBy:model1Prop1': (builder) => builder.orderBy('model1Prop1'),
idGreaterThan: (builder) => builder.where('id', '>', builder.context().filterArgs[0]),
};
}
static get relationMappings() {
return {
model1Relation1: {
relation: Model.BelongsToOneRelation,
modelClass: Model1,
join: {
from: 'Model1.model1Id',
to: 'Model1.id',
},
},
model1Relation1Inverse: {
relation: Model.HasOneRelation,
modelClass: Model1,
join: {
from: 'Model1.id',
to: 'Model1.model1Id',
},
},
model1Relation2: {
relation: Model.HasManyRelation,
modelClass: Model2,
join: {
from: 'Model1.id',
to: 'model2.model1_id',
},
},
model1Relation3: {
relation: Model.ManyToManyRelation,
modelClass: Model2,
join: {
from: 'Model1.id',
through: {
from: 'Model1Model2.model1Id',
to: 'Model1Model2.model2Id',
extra: ['extra1', 'extra2'],
},
to: 'model2.id_col',
},
},
};
}
}
class Model2 extends Model {
// Function instead of getter on purpose.
static tableName() {
return 'model2';
}
static get idColumn() {
return 'id_col';
}
static get columnNameMappers() {
return snakeCaseMappers();
}
static get modifiers() {
return {
orderById: (builder) => builder.orderBy('model2.id_col'),
};
}
static get relationMappings() {
return {
model2Relation1: {
relation: Model.ManyToManyRelation,
modelClass: Model1,
join: {
from: 'model2.id_col',
through: {
from: 'Model1Model2.model2Id',
to: 'Model1Model2.model1Id',
extra: { aliasedExtra: 'extra3' },
},
to: 'Model1.id',
},
},
model2Relation2: {
relation: Model.HasOneThroughRelation,
modelClass: Model1,
join: {
from: 'model2.id_col',
through: {
from: 'Model1Model2One.model2Id',
to: 'Model1Model2One.model1Id',
},
to: 'Model1.id',
},
},
model2Relation3: {
relation: Model.ManyToManyRelation,
modelClass: Model3,
join: {
from: 'model2.id_col',
through: {
from: 'Model2Model3ManyToMany.model2Id',
to: 'Model2Model3ManyToMany.model3Id',
},
to: 'model3.id',
},
},
};
}
}
class Model3 extends Model {
// Function instead of getter on purpose.
static tableName() {
return 'model3';
}
static get idColumn() {
return 'id';
}
static get jsonAttributes() {
return ['model3JsonProp'];
}
static get modifiers() {
return {
orderById: (builder) => builder.orderBy('model3.id'),
};
}
}
[
['$beforeInsert', 1],
['$afterInsert', 0],
['$beforeDelete', 1],
['$afterDelete', 1],
['$beforeUpdate', 1, (self, args) => (self.$beforeUpdateOptions = _.cloneDeep(args[0]))],
['$afterUpdate', 1, (self, args) => (self.$afterUpdateOptions = _.cloneDeep(args[0]))],
['$afterFind', 1],
].forEach((hook) => {
Model1.prototype[hook[0]] = createHook(hook[0], hook[1], hook[2]);
Model2.prototype[hook[0]] = createHook(hook[0], hook[1], hook[2]);
Model3.prototype[hook[0]] = createHook(hook[0], hook[1], hook[2]);
});
return {
Model1: Model1,
Model2: Model2,
Model3: Model3,
};
}
createDb() {
const knex = this.knex;
const opt = this.opt;
return Promise.resolve()
.then(() => knex.schema.dropTableIfExists('Model1Model2'))
.then(() => knex.schema.dropTableIfExists('Model1Model2One'))
.then(() => knex.schema.dropTableIfExists('Model2Model3ManyToMany'))
.then(() => knex.schema.dropTableIfExists('model2'))
.then(() => knex.schema.dropTableIfExists('Model1'))
.then(() => knex.schema.dropTableIfExists('model3'))
.then(() => {
return knex.schema
.createTable('Model1', (table) => {
table.increments('id').primary();
table
.integer('model1Id')
.index()
.unsigned()
.references('Model1.id')
.onDelete('SET NULL');
table.string('model1Prop1');
table.integer('model1Prop2');
})
.createTable('model2', (table) => {
table.increments('id_col').primary();
table
.integer('model1_id')
.index()
.unsigned()
.references('Model1.id')
.onDelete('SET NULL');
table.string('model2_prop1');
table.integer('model2_prop2');
})
.createTable('model3', (table) => {
table.increments('id').primary();
table.string('model3Prop1');
table.text('model3JsonProp');
})
.createTable('Model1Model2', (table) => {
table.increments('id').primary();
table.string('extra1');
table.string('extra2');
table.string('extra3');
table
.integer('model1Id')
.unsigned()
.notNullable()
.references('id')
.inTable('Model1')
.onDelete('CASCADE')
.index();
table
.integer('model2Id')
.unsigned()
.notNullable()
.references('id_col')
.inTable('model2')
.onDelete('CASCADE')
.index();
})
.createTable('Model1Model2One', (table) => {
table
.integer('model1Id')
.unsigned()
.notNullable()
.references('id')
.inTable('Model1')
.onDelete('CASCADE')
.index();
table
.integer('model2Id')
.unsigned()
.notNullable()
.references('id_col')
.inTable('model2')
.onDelete('CASCADE')
.index();
})
.createTable('Model2Model3ManyToMany', (table) => {
table
.integer('model2Id')
.unsigned()
.notNullable()
.references('id_col')
.inTable('model2')
.onDelete('CASCADE')
.index();
table
.integer('model3Id')
.unsigned()
.notNullable()
.references('id')
.inTable('model3')
.onDelete('CASCADE')
.index();
});
})
.catch((cause) => {
const err = new Error(
'Could not connect to ' +
opt.knexConfig.client +
'. Make sure the server is running and the database ' +
opt.knexConfig.connection.database +
' is created. You can see the test database configurations from file ' +
path.join(__dirname, 'index.js'),
);
const oldStack = err.stack;
Object.defineProperties(err, {
stack: {
get() {
return oldStack + `\n\nCaused by:\n${cause.stack}`;
},
},
});
throw err;
});
}
populate(data) {
return transaction(this.knex, (trx) => {
return trx('Model1Model2')
.delete()
.then(() => trx('Model1Model2One').delete())
.then(() => trx('Model2Model3ManyToMany').delete())
.then(() => trx('model2').delete())
.then(() => trx('Model1').delete())
.then(() => trx('model3').delete())
.then(() => this.models.Model1.query(trx).insertGraph(data))
.then(() => {
return Promise.resolve(['Model1', 'model2', 'model3', 'Model1Model2']).map((table) => {
const idCol = (
_.find(this.models, (it) => it.getTableName() === table) || {
getIdColumn: () => 'id',
}
).getIdColumn();
return trx(table)
.max(idCol)
.then((res) => {
const maxId = parseInt(res[0][_.keys(res[0])[0]], 10) || 0;
// Reset sequence.
if (knexUtils.isSqlite(trx)) {
return trx.raw(
'UPDATE sqlite_sequence SET seq = ' + maxId + ' WHERE name = "' + table + '"',
);
} else if (knexUtils.isPostgres(trx)) {
return trx.raw(
'ALTER SEQUENCE "' + table + '_' + idCol + '_seq" RESTART WITH ' + (maxId + 1),
);
} else if (knexUtils.isMySql(trx)) {
return trx.raw('ALTER TABLE ' + table + ' AUTO_INCREMENT = ' + (maxId + 1));
} else {
throw new Error('sequence truncate not implemented for the given database');
}
});
});
})
.then(() => data);
});
}
destroy() {
return this.knex.destroy();
}
addUnhandledRejectionHandler(handler) {
const handlers = TestSession.unhandledRejectionHandlers;
handlers.push(handler);
}
removeUnhandledRejectionHandler(handler) {
const handlers = TestSession.unhandledRejectionHandlers;
handlers.splice(handlers.indexOf(handler), 1);
}
isPostgres() {
return knexUtils.isPostgres(this.knex);
}
isMySql() {
return knexUtils.isMySql(this.knex);
}
isSqlite() {
return knexUtils.isSqlite(this.knex);
}
}
TestSession.staticInitCalled = false;
TestSession.unhandledRejectionHandlers = [];
TestSession.hookCounter = 0;
// Creates a hook that waits for `delay` milliseconds and then
// increments a `${name}Called` property. The hook is asynchronous
// every other time it is called so that the synchronous path is
// also tested.
function createHook(name, delay, extraAction) {
const hook = (model, args) => {
// Increment the property so that it can be checked in the tests.
inc(model, `${name}Called`);
// Optionally run the extraAction function.
(extraAction || _.noop)(model, args);
};
return function () {
const args = arguments;
if (TestSession.hookCounter++ % 2 === 0) {
return hook(this, args);
} else {
return Promise.delay(delay).then(() => hook(this, args));
}
};
}
function inc(obj, key) {
obj[key] = (obj[key] || 0) + 1;
}
function registerUnhandledRejectionHandler() {
Promise.onPossiblyUnhandledRejection((error) => {
if (_.isEmpty(TestSession.unhandledRejectionHandlers)) {
console.error(error.stack);
}
TestSession.unhandledRejectionHandlers.forEach((handler) => {
handler(error);
});
});
}
module.exports = TestSession;
================================================
FILE: testUtils/mockKnex.js
================================================
const _ = require('lodash');
const knexMethods = require('knex/lib/query/method-constants').concat('queryBuilder', 'raw');
/**
* @param {function} knex
* Knex instance to mock.
*
* @param {function(object, function, Array)} mockExecutor
* The mock executor.
*
* @returns {function}
* Mocked knex.
*/
module.exports = function mockKnex(knex, mockExecutor) {
const mock = (table) => {
return mock.queryBuilder().table(table);
};
// Mock query builder methods.
knexMethods.forEach((methodName) => {
mock[methodName] = (...args) => {
return wrapBuilder(knex[methodName](...args));
};
});
const keys = _.uniqBy([...Object.keys(knex), 'client']);
// Mock all other methods and properties.
keys.forEach((key) => {
const value = knex[key];
if (knexMethods.indexOf(key) !== -1) {
return;
}
if (_.isFunction(value)) {
mock[key] = (...args) => {
return knex[key](...args);
};
} else {
Object.defineProperty(mock, key, {
enumerable: true,
get() {
return knex[key];
},
set(value) {
knex[key] = value;
},
});
}
});
function wrapBuilder(builder) {
const oldImpl = builder.then;
builder.then = function (...args) {
return mockExecutor.call(this, mock, oldImpl, args);
};
return builder;
}
return mock;
};
================================================
FILE: testUtils/testUtils.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
/**
* Expect that `result` contains all attributes of `partial` and their values equal.
*
* Example:
*
* ```js
* // doesn't throw.
* expectPartialEqual({a: 1, b: 2}, {a: 1});
* // doesn't throw.
* expectPartialEqual([{a: 1, b: 2}, {a: 2, b: 4}], [{a: 1}, {b: 4}]);
* // Throws
* expectPartialEqual({a: 1}, {b: 1});
* // Throws
* expectPartialEqual({a: 1}, {a: 2});
* ```
*/
function expectPartialEqual(result, partial) {
if (Array.isArray(result) && Array.isArray(partial)) {
expect(result).to.have.length(partial.length);
result.forEach((value, idx) => {
expectPartialEqual(result[idx], partial[idx]);
});
} else if (
_.isObject(result) &&
!Array.isArray(partial) &&
_.isObject(partial) &&
!Array.isArray(result)
) {
var partialKeys = _.keys(partial);
expect(_.pick(result, partialKeys)).to.eql(partial);
} else {
throw new Error('result and partial must both be arrays or objects');
}
}
function createRejectionReflection(err) {
return {
isRejected: () => true,
isFulfilled: () => false,
reason: () => err,
};
}
module.exports = {
expectPartialEqual,
createRejectionReflection,
};
================================================
FILE: tests/integration/compositeKeys.js
================================================
const _ = require('lodash');
const { Model } = require('../../');
const expect = require('expect.js');
const Promise = require('bluebird');
const mockKnexFactory = require('../../testUtils/mockKnex');
module.exports = (session) => {
describe('Composite keys', () => {
let mockKnex;
let queries;
let A;
let B;
before(() => {
mockKnex = mockKnexFactory(session.knex, function (mock, oldImpl, args) {
queries.push(this.toSQL());
return oldImpl.apply(this, args);
});
});
before(() => {
return session.knex.schema
.dropTableIfExists('A_B')
.dropTableIfExists('A')
.dropTableIfExists('B')
.createTable('A', (table) => {
table.integer('id1');
table.string('id2', 32);
table.string('aval');
table.integer('bid3');
table.string('bid4');
table.primary(['id1', 'id2']);
})
.createTable('B', (table) => {
table.integer('id3');
table.string('id4', 32);
table.string('bval');
table.primary(['id3', 'id4']);
})
.createTable('A_B', (table) => {
table.integer('aid1');
table.string('aid2');
table.integer('bid3');
table.string('bid4');
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('A_B')
.dropTableIfExists('A')
.dropTableIfExists('B');
});
before(() => {
class ModelA extends Model {
static get tableName() {
return 'A';
}
static get idColumn() {
return ['id1', 'id2'];
}
static get relationMappings() {
return {
b: {
relation: Model.BelongsToOneRelation,
modelClass: ModelB,
join: {
from: ['A.bid3', 'A.bid4'],
to: ['B.id3', 'B.id4'],
},
},
ba: {
relation: Model.ManyToManyRelation,
modelClass: ModelB,
join: {
from: ['A.id1', 'A.id2'],
through: {
from: ['A_B.aid1', 'A_B.aid2'],
to: ['A_B.bid3', 'A_B.bid4'],
},
to: ['B.id3', 'B.id4'],
},
},
};
}
}
class ModelB extends Model {
static get tableName() {
return 'B';
}
static get idColumn() {
return ['id3', 'id4'];
}
static get relationMappings() {
return {
a: {
relation: Model.HasManyRelation,
modelClass: ModelA,
join: {
from: ['B.id3', 'B.id4'],
to: ['A.bid3', 'A.bid4'],
},
},
ab: {
relation: Model.ManyToManyRelation,
modelClass: ModelA,
join: {
from: ['B.id3', 'B.id4'],
through: {
from: ['A_B.bid3', 'A_B.bid4'],
to: ['A_B.aid1', 'A_B.aid2'],
},
to: ['A.id1', 'A.id2'],
},
},
};
}
}
A = ModelA.bindKnex(mockKnex);
B = ModelB.bindKnex(mockKnex);
});
beforeEach(() => {
queries = [];
});
describe('insert', () => {
afterEach(() => {
return session.knex('A').delete();
});
it('should insert a model', () => {
return A.query()
.insert({ id1: 1, id2: '1', aval: 'a' })
.then((ret) => {
expect(ret).to.eql({ id1: 1, id2: '1', aval: 'a' });
return A.query().insertAndFetch({ id1: 1, id2: '2', aval: 'b' });
})
.then((ret) => {
expect(ret.$toJson()).to.eql({ id1: 1, id2: '2', aval: 'b', bid3: null, bid4: null });
return session.knex('A').orderBy('id2');
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'a', bid3: null, bid4: null },
{ id1: 1, id2: '2', aval: 'b', bid3: null, bid4: null },
]);
});
});
it('insert should fail (unique violation)', (done) => {
A.query()
.insert({ id1: 1, id2: '1', aval: 'a' })
.then(() => {
return A.query().insert({ id1: 1, id2: '1', aval: 'b' });
})
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
});
describe('find', () => {
beforeEach(() => {
return A.query().insertGraph([
{ id1: 1, id2: '1', aval: 'a' },
{ id1: 1, id2: '2', aval: 'b' },
{ id1: 2, id2: '2', aval: 'c' },
{ id1: 2, id2: '3', aval: 'd' },
{ id1: 3, id2: '3', aval: 'e' },
]);
});
afterEach(() => {
return session.knex('A').delete();
});
it('findById should fetch one model by composite id', () => {
return A.query()
.findById([2, '2'])
.then((model) => {
expect(model.toJSON()).to.eql({ id1: 2, id2: '2', aval: 'c', bid3: null, bid4: null });
});
});
it('findByIds should fetch two models by composite ids', () => {
return A.query()
.findByIds([
[1, '1'],
[2, '2'],
])
.then((models) => {
expect(models).to.eql([
{ id1: 1, id2: '1', aval: 'a', bid3: null, bid4: null },
{ id1: 2, id2: '2', aval: 'c', bid3: null, bid4: null },
]);
});
});
it('whereComposite should fetch one model by composite id', () => {
return A.query()
.whereComposite(['id1', 'id2'], [2, '2'])
.first()
.then((model) => {
expect(model.toJSON()).to.eql({ id1: 2, id2: '2', aval: 'c', bid3: null, bid4: null });
});
});
it('whereInComposite should fetch multiple models by composite id', () => {
return A.query()
.whereInComposite(
['id1', 'id2'],
[
[1, '2'],
[2, '3'],
[3, '3'],
],
)
.orderBy(['id1', 'id2'])
.then((models) => {
expect(models).to.eql([
{ id1: 1, id2: '2', aval: 'b', bid3: null, bid4: null },
{ id1: 2, id2: '3', aval: 'd', bid3: null, bid4: null },
{ id1: 3, id2: '3', aval: 'e', bid3: null, bid4: null },
]);
});
});
it('whereNotInComposite should fetch multiple models by composite id', () => {
return A.query()
.whereNotInComposite(
['id1', 'id2'],
[
[1, '2'],
[2, '3'],
[3, '3'],
],
)
.orderBy(['id1', 'id2'])
.then((models) => {
expect(models).to.eql([
{ id1: 1, id2: '1', aval: 'a', bid3: null, bid4: null },
{ id1: 2, id2: '2', aval: 'c', bid3: null, bid4: null },
]);
});
});
});
describe('update', () => {
beforeEach(() => {
return A.query().insertGraph([
{ id1: 1, id2: '1', aval: 'a' },
{ id1: 1, id2: '2', aval: 'b' },
{ id1: 2, id2: '2', aval: 'c' },
{ id1: 2, id2: '3', aval: 'd' },
{ id1: 3, id2: '3', aval: 'e' },
]);
});
afterEach(() => {
return session.knex('A').delete();
});
it('updateAndFetchById should accept a composite id', () => {
return A.query()
.updateAndFetchById([1, '2'], { aval: 'updated' })
.orderBy(['id1', 'id2'])
.then((model) => {
expect(model).to.eql({ id1: 1, id2: '2', aval: 'updated', bid3: null, bid4: null });
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'a', bid3: null, bid4: null },
{ id1: 1, id2: '2', aval: 'updated', bid3: null, bid4: null },
{ id1: 2, id2: '2', aval: 'c', bid3: null, bid4: null },
{ id1: 2, id2: '3', aval: 'd', bid3: null, bid4: null },
{ id1: 3, id2: '3', aval: 'e', bid3: null, bid4: null },
]);
});
});
});
describe('upsertGraph', () => {
beforeEach(() => {
return A.query().insertGraph({
id1: 1,
id2: '1',
aval: 'val1',
b: {
id3: 1,
id4: '1',
bval: 'val2',
a: [
{
id1: 1,
id2: '2',
aval: 'val3',
},
{
id1: 2,
id2: '2',
aval: 'val4',
},
],
},
ba: [
{
id3: 2,
id4: '1',
bval: 'val5',
},
{
id3: 2,
id4: '2',
bval: 'val6',
},
],
});
});
beforeEach(() => {
queries = [];
});
afterEach(() => {
return Promise.all([
session.knex('A').delete(),
session.knex('B').delete(),
session.knex('A_B').delete(),
]);
});
it('should work when inserting a missing root', async () => {
await A.query().upsertGraph(
{
id1: 1000,
id2: "doesn't exist in the db",
},
{ insertMissing: true },
);
expect(queries.length).to.equal(2);
expect(await A.query().findById([1000, "doesn't exist in the db"])).not.equal(undefined);
});
it('should work when updating the root', async () => {
const result = await A.query().upsertGraph({
id1: 1,
id2: '1',
aval: 'updated',
});
expect(result.aval).to.equal('updated');
expect(queries.length).to.equal(2);
expect(queries[0].bindings).to.eql([1, '1']);
if (session.isPostgres()) {
expect(queries[0].sql).to.equal(
'select "A"."id1", "A"."id2", "A"."aval" from "A" where ("A"."id1", "A"."id2") in ((?, ?))',
);
}
const fromDb = await A.query().findById([1, '1']);
expect(fromDb.aval).to.equal('updated');
});
it('should work when `insertMissing` option is true', () => {
return A.query()
.upsertGraph(
{
// update
id1: 1,
id2: '1',
aval: 'x',
b: {
// update
id3: 1,
id4: '1',
bval: 'z',
// [2, '2'] is deleted
a: [
{
// This is the root. Note that a is simply b in reverse.
// We need to mention the root here so that is doesn't
// get deleted.
id1: 1,
id2: '1',
},
{
// update
id1: 1,
id2: '2',
aval: 'w',
},
{
// insert
id1: 400,
id2: '600',
aval: 'new a',
},
],
},
// [2, '2'] is deleted
ba: [
{
// update
id3: 2,
id4: '1',
bval: 'y',
},
{
// insert
id3: 200,
id4: '300',
bval: 'new b',
},
],
},
{ insertMissing: true },
)
.then(() => {
return A.query()
.findById([1, '1'])
.withGraphFetched('[b.a, ba]')
.modifyGraph('b.a', (qb) => qb.orderBy(['id1', 'id2']))
.modifyGraph('ba', (qb) => qb.orderBy(['id3', 'id4']));
})
.then((model) => {
expect(model).to.eql({
id1: 1,
id2: '1',
aval: 'x',
bid3: 1,
bid4: '1',
b: {
id3: 1,
id4: '1',
bval: 'z',
a: [
{
id1: 1,
id2: '1',
aval: 'x',
bid3: 1,
bid4: '1',
},
{
id1: 1,
id2: '2',
bid3: 1,
bid4: '1',
aval: 'w',
},
{
id1: 400,
id2: '600',
bid3: 1,
bid4: '1',
aval: 'new a',
},
],
},
ba: [
{
id3: 2,
id4: '1',
bval: 'y',
},
{
id3: 200,
id4: '300',
bval: 'new b',
},
],
});
return Promise.all([
session.knex('A').orderBy(['id1', 'id2']),
session.knex('B').orderBy(['id3', 'id4']),
]);
})
.then(([a, b]) => {
expect(a).to.eql([
{ id1: 1, id2: '1', aval: 'x', bid3: 1, bid4: '1' },
{ id1: 1, id2: '2', aval: 'w', bid3: 1, bid4: '1' },
{ id1: 400, id2: '600', aval: 'new a', bid3: 1, bid4: '1' },
]);
expect(b).to.eql([
{ id3: 1, id4: '1', bval: 'z' },
{ id3: 2, id4: '1', bval: 'y' },
{ id3: 200, id4: '300', bval: 'new b' },
]);
});
});
it('should insert if partial id is given', () => {
const upsert = A.fromJson({
id1: 1,
id2: '1',
aval: 'aUpdated',
ba: [
{
id3: 2,
id4: '1',
bval: 'bUpdated',
},
{
id4: '3',
bval: 'bNew',
},
],
});
// Add the other key just before it is actually inserted
// so that we don't insert a row with null id.
upsert.ba[1].$beforeInsert = function () {
this.id3 = 2;
};
return A.query()
.upsertGraph(upsert)
.then((model) => {
return A.query()
.findById([1, '1'])
.withGraphFetched('ba')
.modifyGraph('ba', (qb) => qb.orderBy(['id3', 'id4']));
})
.then((model) => {
expect(model).to.eql({
id1: 1,
id2: '1',
aval: 'aUpdated',
bid3: 1,
bid4: '1',
ba: [
{
id3: 2,
id4: '1',
bval: 'bUpdated',
},
{
id3: 2,
id4: '3',
bval: 'bNew',
},
],
});
});
});
});
describe('delete', () => {
beforeEach(() => {
return A.query().insertGraph([
{ id1: 1, id2: '1', aval: 'a' },
{ id1: 1, id2: '2', aval: 'b' },
{ id1: 2, id2: '2', aval: 'c' },
{ id1: 2, id2: '3', aval: 'd' },
{ id1: 3, id2: '3', aval: 'e' },
]);
});
afterEach(() => {
return session.knex('A').delete();
});
it('deleteById should accept a composite id', () => {
return A.query()
.deleteById([1, '2'])
.orderBy(['id1', 'id2'])
.then((count) => {
expect(count).to.eql(1);
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'a', bid3: null, bid4: null },
{ id1: 2, id2: '2', aval: 'c', bid3: null, bid4: null },
{ id1: 2, id2: '3', aval: 'd', bid3: null, bid4: null },
{ id1: 3, id2: '3', aval: 'e', bid3: null, bid4: null },
]);
});
});
});
describe('relations', () => {
beforeEach(() => {
return B.query().insertGraph(
[
{
id3: 1,
id4: '1',
bval: 'b1',
a: [
{ id1: 1, id2: '1', aval: 'a1', '#id': 'a1' },
{ id1: 1, id2: '2', aval: 'a2' },
{ id1: 2, id2: '1', aval: 'a3' },
],
ab: [
{ id1: 11, id2: '11', aval: 'a7', '#id': 'a7' },
{ id1: 11, id2: '12', aval: 'a8' },
{ id1: 12, id2: '11', aval: 'a9' },
],
},
{
id3: 1,
id4: '2',
bval: 'b2',
a: [
{ id1: 2, id2: '2', aval: 'a4' },
{ id1: 2, id2: '3', aval: 'a5' },
{ id1: 3, id2: '2', aval: 'a6' },
],
ab: [
{ '#ref': 'a1' },
{ '#ref': 'a7' },
{ id1: 21, id2: '21', aval: 'a10' },
{ id1: 21, id2: '22', aval: 'a11' },
{ id1: 22, id2: '21', aval: 'a12' },
],
},
],
{ allowRefs: true },
);
});
afterEach(() => {
return Promise.all([
session.knex('A').delete(),
session.knex('B').delete(),
session.knex('A_B').delete(),
]);
});
describe('eager fetch', () => {
['withGraphFetched', 'withGraphJoined'].map((method) => {
it('basic ' + method, () => {
return B.query()
[method]('[a(oa).b(ob), ab(oa)]')
.modifiers({
oa: (builder) => {
builder.orderBy(['id1', 'id2']);
},
ob: (builder) => {
builder.orderBy(['id3', 'id4']);
},
})
.then((models) => {
models = _.sortBy(models, ['id3', 'id4']);
models.forEach((it) => {
it.a = _.sortBy(it.a, ['id1', 'id2']);
});
models.forEach((it) => {
it.ab = _.sortBy(it.ab, ['id1', 'id2']);
});
expect(models).to.eql([
{
id3: 1,
id4: '1',
bval: 'b1',
a: [
{
id1: 1,
id2: '1',
aval: 'a1',
bid3: 1,
bid4: '1',
b: { id3: 1, id4: '1', bval: 'b1' },
},
{
id1: 1,
id2: '2',
aval: 'a2',
bid3: 1,
bid4: '1',
b: { id3: 1, id4: '1', bval: 'b1' },
},
{
id1: 2,
id2: '1',
aval: 'a3',
bid3: 1,
bid4: '1',
b: { id3: 1, id4: '1', bval: 'b1' },
},
],
ab: [
{ id1: 11, id2: '11', aval: 'a7', bid3: null, bid4: null },
{ id1: 11, id2: '12', aval: 'a8', bid3: null, bid4: null },
{ id1: 12, id2: '11', aval: 'a9', bid3: null, bid4: null },
],
},
{
id3: 1,
id4: '2',
bval: 'b2',
a: [
{
id1: 2,
id2: '2',
aval: 'a4',
bid3: 1,
bid4: '2',
b: { id3: 1, id4: '2', bval: 'b2' },
},
{
id1: 2,
id2: '3',
aval: 'a5',
bid3: 1,
bid4: '2',
b: { id3: 1, id4: '2', bval: 'b2' },
},
{
id1: 3,
id2: '2',
aval: 'a6',
bid3: 1,
bid4: '2',
b: { id3: 1, id4: '2', bval: 'b2' },
},
],
ab: [
{ id1: 1, id2: '1', aval: 'a1', bid3: 1, bid4: '1' },
{ id1: 11, id2: '11', aval: 'a7', bid3: null, bid4: null },
{ id1: 21, id2: '21', aval: 'a10', bid3: null, bid4: null },
{ id1: 21, id2: '22', aval: 'a11', bid3: null, bid4: null },
{ id1: 22, id2: '21', aval: 'a12', bid3: null, bid4: null },
],
},
]);
});
});
it('belongs to one $relatedQuery and ' + method, () => {
return B.query()
.findById([1, '1'])
.then((b) => {
return b.$relatedQuery('a')[method]('b');
})
.then((b) => {
b = _.sortBy(b, ['id1', 'id2']);
expect(b).to.eql([
{
id1: 1,
id2: '1',
aval: 'a1',
bid3: 1,
bid4: '1',
b: { id3: 1, id4: '1', bval: 'b1' },
},
{
id1: 1,
id2: '2',
aval: 'a2',
bid3: 1,
bid4: '1',
b: { id3: 1, id4: '1', bval: 'b1' },
},
{
id1: 2,
id2: '1',
aval: 'a3',
bid3: 1,
bid4: '1',
b: { id3: 1, id4: '1', bval: 'b1' },
},
]);
});
});
it('many to many $relatedQuery and ' + method, () => {
return B.query()
.findById([1, '1'])
.then((b) => {
return b.$relatedQuery('ab')[method]('ba');
})
.then((b) => {
b = _.sortBy(b, ['id1', 'id2']);
b[0].ba = _.sortBy(b[0].ba, ['id3', 'id4']);
expect(b).to.eql([
{
id1: 11,
id2: '11',
aval: 'a7',
bid3: null,
bid4: null,
ba: [
{ bval: 'b1', id3: 1, id4: '1' },
{ bval: 'b2', id3: 1, id4: '2' },
],
},
{
id1: 11,
id2: '12',
aval: 'a8',
bid3: null,
bid4: null,
ba: [{ bval: 'b1', id3: 1, id4: '1' }],
},
{
id1: 12,
id2: '11',
aval: 'a9',
bid3: null,
bid4: null,
ba: [{ bval: 'b1', id3: 1, id4: '1' }],
},
]);
});
});
});
});
describe('belongs to one relation', () => {
it('find', () => {
return A.query()
.findById([1, '1'])
.then((a1) => {
return Promise.all([a1, a1.$relatedQuery('b')]);
})
.then(([_, b1]) => {
expect(b1).to.eql({ id3: 1, id4: '1', bval: 'b1' });
});
});
it('insert', () => {
return A.query()
.findById([1, '1'])
.then((a1) => {
return Promise.all([
a1,
a1.$relatedQuery('b').insert({ id3: 1000, id4: '2000', bval: 'new' }),
]);
})
.then(([a1, bNew]) => {
expect(bNew).to.eql({ id3: 1000, id4: '2000', bval: 'new' });
expect(a1).to.eql({
id1: 1,
id2: '1',
aval: 'a1',
bid3: 1000,
bid4: '2000',
});
return Promise.all([
session.knex('A').where({ id1: 1, id2: '1' }).first(),
session.knex('B').where({ id3: 1000, id4: '2000' }).first(),
]);
})
.then(([a1, bNew]) => {
expect(a1).to.eql({ id1: 1, id2: '1', aval: 'a1', bid3: 1000, bid4: '2000' });
expect(bNew).to.eql({ id3: 1000, id4: '2000', bval: 'new' });
});
});
it('update', () => {
return A.query()
.findById([1, '1'])
.then((a1) => {
return Promise.all([a1, a1.$relatedQuery('b').update({ bval: 'updated' })]);
})
.then(([a1, numUpdated]) => {
expect(numUpdated).to.equal(1);
return session.knex('B').where('bval', 'updated');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0]).to.eql({ id3: 1, id4: '1', bval: 'updated' });
});
});
it('updateAndFetchById', () => {
return A.query()
.findById([1, '1'])
.then((a1) => {
return Promise.all([
a1,
a1.$relatedQuery('b').updateAndFetchById([1, '1'], { bval: 'updated' }),
]);
})
.then(([a1, b1]) => {
expect(b1).to.eql({ id3: 1, id4: '1', bval: 'updated' });
return session.knex('B').where('bval', 'updated');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0]).to.eql({ id3: 1, id4: '1', bval: 'updated' });
});
});
it('delete', () => {
return A.query()
.findById([2, '2'])
.then((a1) => {
return Promise.all([a1, a1.$relatedQuery('b').delete()]);
})
.then(([a1, numDeleted]) => {
expect(numDeleted).to.equal(1);
return session.knex('B');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0].bval).to.equal('b1');
});
});
it('relate', () => {
return A.query()
.findById([2, '2'])
.then((a1) => {
expect(a1.bid3).to.equal(1);
expect(a1.bid4).to.equal('2');
return Promise.all([a1, a1.$relatedQuery('b').relate([1, '1'])]);
})
.then(([a1]) => {
expect(a1.bid3).to.equal(1);
expect(a1.bid4).to.equal('1');
return A.query().findById([2, '2']);
})
.then((a1) => {
expect(a1.bid3).to.equal(1);
expect(a1.bid4).to.equal('1');
});
});
it('unrelate', () => {
return A.query()
.findById([2, '2'])
.then((a1) => {
expect(a1.bid3).to.equal(1);
expect(a1.bid4).to.equal('2');
return Promise.all([a1, a1.$relatedQuery('b').unrelate()]);
})
.then(([a1]) => {
expect(a1.bid3).to.equal(null);
expect(a1.bid4).to.equal(null);
return A.query().findById([2, '2']);
})
.then((a1) => {
expect(a1.bid3).to.equal(null);
expect(a1.bid4).to.equal(null);
});
});
});
describe('has many relation', () => {
it('find', () => {
return B.query()
.findById([1, '1'])
.then((b1) => {
return Promise.all([b1, b1.$relatedQuery('a').orderBy(['id1', 'id2'])]);
})
.then(([_, a]) => {
expect(a).to.eql([
{ id1: 1, id2: '1', aval: 'a1', bid3: 1, bid4: '1' },
{ id1: 1, id2: '2', aval: 'a2', bid3: 1, bid4: '1' },
{ id1: 2, id2: '1', aval: 'a3', bid3: 1, bid4: '1' },
]);
});
});
it('insert', () => {
return B.query()
.findById([1, '1'])
.then((b1) => {
return Promise.all([
b1,
b1.$relatedQuery('a').insert({ id1: 1000, id2: '2000', aval: 'new' }),
]);
})
.then(([b1, aNew]) => {
expect(aNew).to.eql({
id1: 1000,
id2: '2000',
aval: 'new',
bid3: 1,
bid4: '1',
});
return session.knex('A').where({ id1: 1000, id2: '2000' }).first();
})
.then((aNew) => {
expect(aNew).to.eql({ id1: 1000, id2: '2000', aval: 'new', bid3: 1, bid4: '1' });
});
});
it('update', () => {
return B.query()
.findById([1, '1'])
.then((b1) => {
return b1.$relatedQuery('a').update({ aval: 'up' }).where('id2', '>', '1');
})
.then((count) => {
expect(count).to.equal(1);
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'a1', bid3: 1, bid4: '1' },
{ id1: 1, id2: '2', aval: 'up', bid3: 1, bid4: '1' },
{ id1: 2, id2: '1', aval: 'a3', bid3: 1, bid4: '1' },
{ id1: 2, id2: '2', aval: 'a4', bid3: 1, bid4: '2' },
{ id1: 2, id2: '3', aval: 'a5', bid3: 1, bid4: '2' },
{ id1: 3, id2: '2', aval: 'a6', bid3: 1, bid4: '2' },
{ id1: 11, id2: '11', aval: 'a7', bid3: null, bid4: null },
{ id1: 11, id2: '12', aval: 'a8', bid3: null, bid4: null },
{ id1: 12, id2: '11', aval: 'a9', bid3: null, bid4: null },
{ id1: 21, id2: '21', aval: 'a10', bid3: null, bid4: null },
{ id1: 21, id2: '22', aval: 'a11', bid3: null, bid4: null },
{ id1: 22, id2: '21', aval: 'a12', bid3: null, bid4: null },
]);
});
});
it('delete', () => {
return B.query()
.findById([1, '2'])
.then((b2) => {
return b2.$relatedQuery('a').delete();
})
.then((count) => {
expect(count).to.equal(3);
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'a1', bid3: 1, bid4: '1' },
{ id1: 1, id2: '2', aval: 'a2', bid3: 1, bid4: '1' },
{ id1: 2, id2: '1', aval: 'a3', bid3: 1, bid4: '1' },
{ id1: 11, id2: '11', aval: 'a7', bid3: null, bid4: null },
{ id1: 11, id2: '12', aval: 'a8', bid3: null, bid4: null },
{ id1: 12, id2: '11', aval: 'a9', bid3: null, bid4: null },
{ id1: 21, id2: '21', aval: 'a10', bid3: null, bid4: null },
{ id1: 21, id2: '22', aval: 'a11', bid3: null, bid4: null },
{ id1: 22, id2: '21', aval: 'a12', bid3: null, bid4: null },
]);
});
});
it('relate', () => {
return B.query()
.findById([1, '2'])
.then((b2) => {
return b2.$relatedQuery('a').relate([1, '1']);
})
.then(() => {
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'a1', bid3: 1, bid4: '2' },
{ id1: 1, id2: '2', aval: 'a2', bid3: 1, bid4: '1' },
{ id1: 2, id2: '1', aval: 'a3', bid3: 1, bid4: '1' },
{ id1: 2, id2: '2', aval: 'a4', bid3: 1, bid4: '2' },
{ id1: 2, id2: '3', aval: 'a5', bid3: 1, bid4: '2' },
{ id1: 3, id2: '2', aval: 'a6', bid3: 1, bid4: '2' },
{ id1: 11, id2: '11', aval: 'a7', bid3: null, bid4: null },
{ id1: 11, id2: '12', aval: 'a8', bid3: null, bid4: null },
{ id1: 12, id2: '11', aval: 'a9', bid3: null, bid4: null },
{ id1: 21, id2: '21', aval: 'a10', bid3: null, bid4: null },
{ id1: 21, id2: '22', aval: 'a11', bid3: null, bid4: null },
{ id1: 22, id2: '21', aval: 'a12', bid3: null, bid4: null },
]);
});
});
it('relate (object value)', () => {
return B.query()
.findById([1, '2'])
.then((b2) => {
return b2.$relatedQuery('a').relate({ id1: 1, id2: '1' });
})
.then(() => {
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'a1', bid3: 1, bid4: '2' },
{ id1: 1, id2: '2', aval: 'a2', bid3: 1, bid4: '1' },
{ id1: 2, id2: '1', aval: 'a3', bid3: 1, bid4: '1' },
{ id1: 2, id2: '2', aval: 'a4', bid3: 1, bid4: '2' },
{ id1: 2, id2: '3', aval: 'a5', bid3: 1, bid4: '2' },
{ id1: 3, id2: '2', aval: 'a6', bid3: 1, bid4: '2' },
{ id1: 11, id2: '11', aval: 'a7', bid3: null, bid4: null },
{ id1: 11, id2: '12', aval: 'a8', bid3: null, bid4: null },
{ id1: 12, id2: '11', aval: 'a9', bid3: null, bid4: null },
{ id1: 21, id2: '21', aval: 'a10', bid3: null, bid4: null },
{ id1: 21, id2: '22', aval: 'a11', bid3: null, bid4: null },
{ id1: 22, id2: '21', aval: 'a12', bid3: null, bid4: null },
]);
});
});
it('unrelate', () => {
return B.query()
.findById([1, '2'])
.then((b2) => {
return b2.$relatedQuery('a').unrelate().where('aval', 'a5');
})
.then(() => {
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'a1', bid3: 1, bid4: '1' },
{ id1: 1, id2: '2', aval: 'a2', bid3: 1, bid4: '1' },
{ id1: 2, id2: '1', aval: 'a3', bid3: 1, bid4: '1' },
{ id1: 2, id2: '2', aval: 'a4', bid3: 1, bid4: '2' },
{ id1: 2, id2: '3', aval: 'a5', bid3: null, bid4: null },
{ id1: 3, id2: '2', aval: 'a6', bid3: 1, bid4: '2' },
{ id1: 11, id2: '11', aval: 'a7', bid3: null, bid4: null },
{ id1: 11, id2: '12', aval: 'a8', bid3: null, bid4: null },
{ id1: 12, id2: '11', aval: 'a9', bid3: null, bid4: null },
{ id1: 21, id2: '21', aval: 'a10', bid3: null, bid4: null },
{ id1: 21, id2: '22', aval: 'a11', bid3: null, bid4: null },
{ id1: 22, id2: '21', aval: 'a12', bid3: null, bid4: null },
]);
});
});
});
describe('many to many relation', () => {
it('find', () => {
return B.query()
.findById([1, '2'])
.then((b2) => {
return b2.$relatedQuery('ab').orderBy(['id1', 'id2']);
})
.then((ret) => {
expect(ret).to.eql([
{ id1: 1, id2: '1', aval: 'a1', bid3: 1, bid4: '1' },
{ id1: 11, id2: '11', aval: 'a7', bid3: null, bid4: null },
{ id1: 21, id2: '21', aval: 'a10', bid3: null, bid4: null },
{ id1: 21, id2: '22', aval: 'a11', bid3: null, bid4: null },
{ id1: 22, id2: '21', aval: 'a12', bid3: null, bid4: null },
]);
});
});
it('insert', () => {
let aOld;
let abOld;
return B.query()
.findById([1, '2'])
.then((b2) => {
return Promise.all([
b2,
session.knex('A').orderBy(['id1', 'id2']),
session.knex('A_B').orderBy(['bid3', 'bid4', 'aid1', 'aid2']),
]);
})
.then(([b2, a, ab]) => {
aOld = a;
abOld = ab;
return b2.$relatedQuery('ab').insert({ id1: 1000, id2: 2000, aval: 'new' });
})
.then(() => {
return Promise.all([
session.knex('A').orderBy(['id1', 'id2']),
session.knex('A_B').orderBy(['bid3', 'bid4', 'aid1', 'aid2']),
]);
})
.then(([a, ab]) => {
expect(a).to.eql(
aOld.concat([{ id1: 1000, id2: '2000', aval: 'new', bid3: null, bid4: null }]),
);
expect(ab).to.eql(abOld.concat([{ aid1: 1000, aid2: '2000', bid3: 1, bid4: '2' }]));
});
});
it('update', () => {
return B.query()
.findById([1, '2'])
.then((b2) => {
return b2.$relatedQuery('ab').update({ aval: 'XX' });
})
.then(() => {
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '1', aval: 'XX', bid3: 1, bid4: '1' },
{ id1: 1, id2: '2', aval: 'a2', bid3: 1, bid4: '1' },
{ id1: 2, id2: '1', aval: 'a3', bid3: 1, bid4: '1' },
{ id1: 2, id2: '2', aval: 'a4', bid3: 1, bid4: '2' },
{ id1: 2, id2: '3', aval: 'a5', bid3: 1, bid4: '2' },
{ id1: 3, id2: '2', aval: 'a6', bid3: 1, bid4: '2' },
{ id1: 11, id2: '11', aval: 'XX', bid3: null, bid4: null },
{ id1: 11, id2: '12', aval: 'a8', bid3: null, bid4: null },
{ id1: 12, id2: '11', aval: 'a9', bid3: null, bid4: null },
{ id1: 21, id2: '21', aval: 'XX', bid3: null, bid4: null },
{ id1: 21, id2: '22', aval: 'XX', bid3: null, bid4: null },
{ id1: 22, id2: '21', aval: 'XX', bid3: null, bid4: null },
]);
});
});
it('delete', () => {
return B.query()
.findById([1, '2'])
.then((b2) => {
return b2.$relatedQuery('ab').delete();
})
.then(() => {
return session.knex('A').orderBy(['id1', 'id2']);
})
.then((rows) => {
expect(rows).to.eql([
{ id1: 1, id2: '2', aval: 'a2', bid3: 1, bid4: '1' },
{ id1: 2, id2: '1', aval: 'a3', bid3: 1, bid4: '1' },
{ id1: 2, id2: '2', aval: 'a4', bid3: 1, bid4: '2' },
{ id1: 2, id2: '3', aval: 'a5', bid3: 1, bid4: '2' },
{ id1: 3, id2: '2', aval: 'a6', bid3: 1, bid4: '2' },
{ id1: 11, id2: '12', aval: 'a8', bid3: null, bid4: null },
{ id1: 12, id2: '11', aval: 'a9', bid3: null, bid4: null },
]);
});
});
it('relate', () => {
let aOld;
let abOld;
return B.query()
.findById([1, '2'])
.then((b2) => {
return Promise.all([
b2,
session.knex('A').orderBy(['id1', 'id2']),
session.knex('A_B').orderBy(['bid3', 'bid4', 'aid1', 'aid2']),
]);
})
.then(([b2, a, ab]) => {
aOld = a;
abOld = ab;
return b2.$relatedQuery('ab').relate([1, '2']);
})
.then(() => {
return Promise.all([
session.knex('A').orderBy(['id1', 'id2']),
session.knex('A_B').orderBy(['bid3', 'bid4', 'aid1', 'aid2']),
]);
})
.then(([a, ab]) => {
expect(a).to.eql(aOld);
expect(ab).to.eql(
_.sortBy(abOld.concat([{ aid1: 1, aid2: '2', bid3: 1, bid4: '2' }]), [
'bid3',
'bid4',
'aid1',
'aid2',
]),
);
});
});
it('unrelate', () => {
let aOld;
let abOld;
return B.query()
.findById([1, '2'])
.then((b2) => {
return Promise.all([
b2,
session.knex('A').orderBy(['id1', 'id2']),
session.knex('A_B').orderBy(['bid3', 'bid4', 'aid1', 'aid2']),
]);
})
.then(([b2, a, ab]) => {
aOld = a;
abOld = ab;
return b2.$relatedQuery('ab').unrelate();
})
.then(() => {
return Promise.all([
session.knex('A').orderBy(['id1', 'id2']),
session.knex('A_B').orderBy(['bid3', 'bid4', 'aid1', 'aid2']),
]);
})
.then(([a, ab]) => {
expect(a).to.eql(aOld);
expect(ab).to.eql(_.reject(abOld, { bid3: 1, bid4: '2' }));
});
});
});
});
});
};
================================================
FILE: tests/integration/crossDb/index.js
================================================
const knexUtils = require('../../../lib/utils/knexUtils');
module.exports = (session) => {
describe('cross db', () => {
if (knexUtils.isMySql(session.knex)) {
require('./mysql')(session);
}
});
};
================================================
FILE: tests/integration/crossDb/mysql.js
================================================
const _ = require('lodash');
const Knex = require('knex');
const { Model } = require('../../../');
const expect = require('expect.js');
const Promise = require('bluebird');
module.exports = (session) => {
describe('mysql', () => {
let db2Knex;
let T1;
let T2;
before(
Promise.coroutine(function* () {
yield session.knex.raw('CREATE DATABASE IF NOT EXISTS objection_test_2');
const db2Config = _.cloneDeep(session.opt.knexConfig);
db2Config.connection.database = 'objection_test_2';
db2Knex = Knex(db2Config);
yield db2Knex.schema.dropTableIfExists('t2');
yield db2Knex.schema.dropTableIfExists('t1');
yield db2Knex.schema.createTable('t1', (table) => {
table.integer('id').primary();
table.integer('foo');
});
yield db2Knex.schema.createTable('t2', (table) => {
table.integer('id').primary();
table.integer('t1_id').references('t1.id');
table.integer('bar');
});
}),
);
after(
Promise.coroutine(function* () {
yield db2Knex.schema.dropTableIfExists('t2');
yield db2Knex.schema.dropTableIfExists('t1');
yield db2Knex.destroy();
yield session.knex.raw('DROP DATABASE IF EXISTS objection_test_2');
}),
);
beforeEach(() => {
class T1Model extends Model {
static get tableName() {
return 'objection_test_2.t1';
}
static get relationMappings() {
return {
manyT2: {
relation: Model.HasManyRelation,
modelClass: T2Model,
join: {
from: 'objection_test_2.t1.id',
to: 'objection_test_2.t2.t1_id',
},
},
};
}
}
class T2Model extends Model {
static get tableName() {
return 'objection_test_2.t2';
}
static get relationMappings() {
return {
oneT1: {
relation: Model.BelongsToOneRelation,
modelClass: T1Model,
join: {
from: 'objection_test_2.t1.id',
to: 'objection_test_2.t2.t1_id',
},
},
};
}
}
T1 = T1Model.bindKnex(session.knex);
T2 = T2Model.bindKnex(session.knex);
});
beforeEach(
Promise.coroutine(function* () {
yield db2Knex('t2').delete();
yield db2Knex('t1').delete();
}),
);
it('should be able to insert to another database', () => {
return T1.query()
.insert({ id: 1, foo: 1 })
.then(() => {
return db2Knex('t1');
})
.then((rows) => {
expect(rows).to.eql([{ id: 1, foo: 1 }]);
});
});
it('should be able to insert a graph to another database', () => {
return T1.query()
.insertGraph({
id: 1,
foo: 1,
manyT2: [
{
id: 1,
bar: 2,
},
],
})
.then(() => {
return Promise.all([db2Knex('t1'), db2Knex('t2')]);
})
.then((res) => {
expect(res).to.eql([[{ id: 1, foo: 1 }], [{ id: 1, bar: 2, t1_id: 1 }]]);
});
});
it('select should work with a normal query', () => {
return T1.query()
.insert({ id: 1, foo: 1 })
.then(() => {
return T1.query().select('objection_test_2.t1.*');
})
.then((models) => {
expect(models).to.eql([{ id: 1, foo: 1 }]);
})
.then(() => {
return T1.query().select('objection_test_2.t1.id');
})
.then((models) => {
expect(models).to.eql([{ id: 1 }]);
});
});
it('select should work with an eager query', () => {
return T1.query()
.insertGraph({
id: 1,
foo: 1,
manyT2: [
{
id: 1,
bar: 2,
},
],
})
.then(() => {
return T1.query().withGraphFetched('manyT2').select('objection_test_2.t1.*');
})
.then((models) => {
expect(models).to.eql([
{
id: 1,
foo: 1,
manyT2: [
{
id: 1,
t1_id: 1,
bar: 2,
},
],
},
]);
})
.then(() => {
return T1.query()
.withGraphFetched('manyT2')
.select('objection_test_2.t1.foo')
.modifyGraph('manyT2', (builder) => {
builder.select('bar');
});
})
.then((models) => {
expect(models).to.eql([
{
foo: 1,
manyT2: [
{
bar: 2,
},
],
},
]);
});
});
});
};
================================================
FILE: tests/integration/delete.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const expectPartEql = require('./../../testUtils/testUtils').expectPartialEqual;
const isPostgres = require('../../lib/utils/knexUtils').isPostgres;
module.exports = (session) => {
const Model1 = session.models.Model1;
const Model2 = session.models.Model2;
describe('Model delete queries', () => {
describe('.query().delete()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 2,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 1,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
{
id: 3,
model1Prop1: 'hello 3',
},
]);
});
it('should delete a model (1)', () => {
return Model1.query()
.delete()
.where('id', '=', 2)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should delete a model (2)', () => {
return Model2.query()
.del()
.where('model2_prop2', 1)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(1);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1', model2_prop2: 2 });
});
});
it('should delete a model using deleteById', () => {
return Model1.query()
.deleteById(2)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 3, model1Prop1: 'hello 3' });
});
});
if (session.isPostgres()) {
it('should delete a model using deleteById and return the deleted column when `returning` is used', () => {
return Model1.query()
.deleteById(2)
.returning('*')
.then((deletedRow) => {
expect(deletedRow).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
});
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 3, model1Prop1: 'hello 3' });
});
});
}
it('should delete multiple', () => {
return Model1.query()
.delete()
.where('model1Prop1', '<', 'hello 3')
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(1);
expectPartEql(rows[0], { id: 3, model1Prop1: 'hello 3' });
});
});
if (isPostgres(session.knex)) {
it('should delete and return multiple', () => {
let deleted1;
return Model1.query()
.delete()
.where('model1Prop1', '<', 'hello 3')
.returning('*')
.then((deletedObjects) => {
expect(deletedObjects).to.have.length(2);
deleted1 = _.find(deletedObjects, { id: 1 });
expect(deleted1).to.be.a(Model1);
expectPartEql(deleted1, { id: 1, model1Prop1: 'hello 1' });
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(1);
expectPartEql(rows[0], { id: 3, model1Prop1: 'hello 3' });
});
});
}
});
it('an error with a clear message should be thrown if undefined is passed to deleteById', (done) => {
Model1.query()
.deleteById(undefined)
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal('undefined was passed to deleteById');
done();
})
.catch(done);
});
describe('.$query().delete()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should delete a model', () => {
let model = Model1.fromJson({ id: 1 });
return model
.$query()
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(model.$beforeDeleteCalled).to.equal(1);
expect(model.$afterDeleteCalled).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(1);
expectPartEql(rows[0], { id: 2, model1Prop1: 'hello 2' });
});
});
if (isPostgres(session.knex)) {
it('should work with returning', () => {
let model = Model1.fromJson({ id: 1 });
return model
.$query()
.delete()
.returning('model1Prop1', 'model1Prop2')
.then((deleted) => {
const expected = { model1Prop1: 'hello 1', model1Prop2: null };
expect(deleted).to.be.a(Model1);
expect(deleted).to.eql(expected);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(1);
expectPartEql(rows[0], { id: 2, model1Prop1: 'hello 2' });
});
});
it('should work with returning *', () => {
let model = Model1.fromJson({ id: 2 });
return model
.$query()
.delete()
.returning('*')
.then((deleted) => {
const expected = {
id: 2,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
};
expect(deleted).to.be.a(Model1);
expect(deleted).to.eql(expected);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(1);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
});
});
}
it('should should call $beforeDelete and $afterDelete hooks', () => {
let model = Model1.fromJson({ id: 1 });
model.$beforeDelete = function () {
let self = this;
return Model1.query()
.findById(this.id)
.then((model) => {
self.before = model;
});
};
model.$afterDelete = function () {
let self = this;
return Model1.query()
.findById(this.id)
.then((model) => {
self.after = model || null;
});
};
return model
.$query()
.delete()
.then(() => {
expect(model.before.id).to.equal(model.id);
expect(model.after).to.equal(null);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(1);
expectPartEql(rows[0], { id: 2, model1Prop1: 'hello 2' });
});
});
it('should throw if the id is undefiend', (done) => {
let model = Model1.fromJson({ model1Prop2: 1 });
model
.$query()
.delete()
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`one of the identifier columns [id] is null or undefined. Have you specified the correct identifier column for the model 'Model1' using the 'idColumn' property?`,
);
done();
})
.catch(done);
});
it('should throw if the id is null', (done) => {
let model = Model1.fromJson({ id: null });
model
.$query()
.delete()
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`one of the identifier columns [id] is null or undefined. Have you specified the correct identifier column for the model 'Model1' using the 'idColumn' property?`,
);
done();
})
.catch(done);
});
});
describe('.$relatedQuery().delete()', () => {
describe('belongs to one relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
},
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 3 });
});
});
it('should delete a related object (1)', () => {
return parent1
.$relatedQuery('model1Relation1')
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[2], { id: 4, model1Prop1: 'hello 4' });
});
});
if (isPostgres(session.knex)) {
it('should delete and return a related object (1)', () => {
return parent1
.$relatedQuery('model1Relation1')
.delete()
.first()
.returning('*')
.then((deletedObject) => {
expect(deletedObject).to.be.a(Model1);
expectPartEql(deletedObject, { id: 2, model1Prop1: 'hello 2' });
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[2], { id: 4, model1Prop1: 'hello 4' });
});
});
}
it('should delete a related object (2)', () => {
return parent2
.$relatedQuery('model1Relation1')
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
});
describe('has many relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
{
idCol: 5,
model2Prop1: 'text 5',
model2Prop2: 2,
},
{
idCol: 6,
model2Prop1: 'text 6',
model2Prop2: 1,
},
],
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 2 });
});
});
it('should delete all related objects', () => {
return parent1
.$relatedQuery('model1Relation2')
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(3);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[1], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[2], { id_col: 6, model2_prop1: 'text 6' });
});
});
if (isPostgres(session.knex)) {
it('should delete and return all related objects', () => {
let child1;
return parent1
.$relatedQuery('model1Relation2')
.delete()
.returning('*')
.then((deletedObjects) => {
expect(deletedObjects).to.have.length(3);
child1 = _.find(deletedObjects, { idCol: 1 });
expect(child1).to.be.a(Model2);
expectPartEql(child1, { idCol: 1, model2Prop1: 'text 1' });
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[1], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[2], { id_col: 6, model2_prop1: 'text 6' });
});
});
}
it('should delete a related object', () => {
return parent1
.$relatedQuery('model1Relation2')
.delete()
.where('id_col', 2)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(5);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], { id_col: 3, model2_prop1: 'text 3' });
expectPartEql(rows[2], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[3], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[4], { id_col: 6, model2_prop1: 'text 6' });
});
});
it('should delete multiple related objects', () => {
return parent1
.$relatedQuery('model1Relation2')
.delete()
.where('model2_prop2', '<', 6)
.where('model2_prop1', 'like', 'text %')
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[2], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[3], { id_col: 6, model2_prop1: 'text 6' });
});
});
});
describe('many to many relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 5,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
},
{
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 2,
},
{
id: 8,
model1Prop1: 'blaa 6',
model1Prop2: 1,
},
],
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent1 = _.find(parents, { idCol: 1 });
parent2 = _.find(parents, { idCol: 2 });
});
});
it('should delete all related objects', () => {
return parent1
.$relatedQuery('model2Relation1')
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(3);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(5);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[3], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[4], { id: 8, model1Prop1: 'blaa 6' });
});
});
it('should delete a related object', () => {
return parent1
.$relatedQuery('model2Relation1')
.delete()
.where('Model1.id', 5)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[6], { id: 8, model1Prop1: 'blaa 6' });
});
});
it('should delete a related object using deleteById', () => {
return parent1
.$relatedQuery('model2Relation1')
.deleteById(5)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[6], { id: 8, model1Prop1: 'blaa 6' });
});
});
if (session.isPostgres()) {
it('should delete a related object using deleteById and return the deleted row when `returning` is used', () => {
return parent1
.$relatedQuery('model2Relation1')
.returning('*')
.deleteById(5)
.then((deletedRow) => {
expect(deletedRow).to.eql({
id: 5,
model1Id: null,
model1Prop1: 'blaa 3',
model1Prop2: 4,
});
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[6], { id: 8, model1Prop1: 'blaa 6' });
});
});
}
it('should delete multiple objects (1)', () => {
return parent2
.$relatedQuery('model2Relation1')
.delete()
.where('model1Prop1', 'like', 'blaa 4')
.orWhere('model1Prop1', 'like', 'blaa 6')
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
});
});
if (isPostgres(session.knex)) {
it('should delete and return multiple objects (1)', () => {
let child1;
return parent2
.$relatedQuery('model2Relation1')
.delete()
.where('model1Prop1', 'like', 'blaa 4')
.orWhere('model1Prop1', 'like', 'blaa 6')
.returning('*')
.then((deletedObjects) => {
expect(deletedObjects).to.have.length(2);
child1 = _.find(deletedObjects, { id: 6 });
expect(child1).to.be.a(Model1);
expectPartEql(child1, { id: 6, model1Prop1: 'blaa 4' });
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
});
});
}
it('should delete multiple objects (2)', () => {
return parent1
.$relatedQuery('model2Relation1')
.delete()
.where('model1Prop2', '<', 6)
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[4], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[5], { id: 8, model1Prop1: 'blaa 6' });
});
});
});
describe('has one through relation', () => {
let parent;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation2: {
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 1,
},
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation2: {
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 2,
},
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent = _.find(parents, { idCol: 2 });
});
});
it('should delete the related object', () => {
return parent
.$relatedQuery('model2Relation2')
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
});
});
if (isPostgres(session.knex)) {
it('should delete and return the related object', () => {
return parent
.$relatedQuery('model2Relation2')
.delete()
.first()
.returning('*')
.then((deletedObject) => {
expect(deletedObject).to.be.a(Model1);
expectPartEql(deletedObject, { id: 4, model1Prop1: 'blaa 2' });
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
});
});
}
});
});
describe('relatedQuery().delete()', () => {
describe('belongs to one relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
},
},
]);
});
it('should delete a related object (1)', () => {
return Model1.relatedQuery('model1Relation1')
.for(1)
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[2], { id: 4, model1Prop1: 'hello 4' });
});
});
if (isPostgres(session.knex)) {
it('should delete and return a related object', () => {
return Model1.relatedQuery('model1Relation1')
.for(1)
.delete()
.first()
.returning('*')
.then((deletedObject) => {
expect(deletedObject).to.be.a(Model1);
expectPartEql(deletedObject, { id: 2, model1Prop1: 'hello 2' });
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[2], { id: 4, model1Prop1: 'hello 4' });
});
});
}
it('should delete a related object using a subquery', () => {
return Model1.relatedQuery('model1Relation1')
.for(Model1.query().findById(3))
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
});
describe('has many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
{
idCol: 5,
model2Prop1: 'text 5',
model2Prop2: 2,
},
{
idCol: 6,
model2Prop1: 'text 6',
model2Prop2: 1,
},
],
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation2: [
{
idCol: 7,
model2Prop1: 'text 7',
model2Prop2: 0,
},
],
},
]);
});
it('should delete all related objects', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(3);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[1], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[2], { id_col: 6, model2_prop1: 'text 6' });
expectPartEql(rows[3], { id_col: 7, model2_prop1: 'text 7' });
});
});
if (isPostgres(session.knex)) {
it('should delete and return all related objects', () => {
let child1;
return Model1.relatedQuery('model1Relation2')
.for(1)
.delete()
.returning('*')
.then((deletedObjects) => {
expect(deletedObjects).to.have.length(3);
child1 = _.find(deletedObjects, { idCol: 1 });
expect(child1).to.be.a(Model2);
expectPartEql(child1, { idCol: 1, model2Prop1: 'text 1' });
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[1], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[2], { id_col: 6, model2_prop1: 'text 6' });
expectPartEql(rows[3], { id_col: 7, model2_prop1: 'text 7' });
});
});
}
it('should delete a related object', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.delete()
.where('id_col', 2)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], { id_col: 3, model2_prop1: 'text 3' });
expectPartEql(rows[2], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[3], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[4], { id_col: 6, model2_prop1: 'text 6' });
expectPartEql(rows[5], { id_col: 7, model2_prop1: 'text 7' });
});
});
it('should delete multiple related objects using using a subquery', () => {
return Model1.relatedQuery('model1Relation2')
.for(Model1.query().findByIds([1, 2]))
.delete()
.where('model2_prop2', '<', 6)
.where('model2_prop1', 'like', 'text %')
.then((numDeleted) => {
expect(numDeleted).to.equal(5);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], { id_col: 7, model2_prop1: 'text 7' });
});
});
});
describe('many to many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 5,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
},
{
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 2,
},
{
id: 8,
model1Prop1: 'blaa 6',
model1Prop2: 1,
},
],
},
],
},
]);
});
it('should delete all related objects', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(3);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(5);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[3], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[4], { id: 8, model1Prop1: 'blaa 6' });
});
});
it('should delete a related object', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.delete()
.where('Model1.id', 5)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[6], { id: 8, model1Prop1: 'blaa 6' });
});
});
it('should delete a related object using deleteById', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.deleteById(5)
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[6], { id: 8, model1Prop1: 'blaa 6' });
});
});
if (session.isPostgres()) {
it('should delete a related object using deleteById and return the deleted row when `returning` is used', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.returning('*')
.deleteById(5)
.then((deletedRow) => {
expect(deletedRow).to.eql({
id: 5,
model1Id: null,
model1Prop1: 'blaa 3',
model1Prop2: 4,
});
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[6], { id: 8, model1Prop1: 'blaa 6' });
});
});
}
it('should delete multiple objects (1)', () => {
return Model2.relatedQuery('model2Relation1')
.for(2)
.delete()
.where('model1Prop1', 'like', 'blaa 4')
.orWhere('model1Prop1', 'like', 'blaa 6')
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
});
});
if (isPostgres(session.knex)) {
it('should delete and return multiple objects (1)', () => {
let child1;
return Model2.relatedQuery('model2Relation1')
.for(2)
.delete()
.where('model1Prop1', 'like', 'blaa 4')
.orWhere('model1Prop1', 'like', 'blaa 6')
.returning('*')
.then((deletedObjects) => {
expect(deletedObjects).to.have.length(2);
child1 = _.find(deletedObjects, { id: 6 });
expect(child1).to.be.a(Model1);
expectPartEql(child1, { id: 6, model1Prop1: 'blaa 4' });
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 7, model1Prop1: 'blaa 5' });
});
});
}
it('should delete multiple objects (2)', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.delete()
.where('model1Prop2', '<', 6)
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[4], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[5], { id: 8, model1Prop1: 'blaa 6' });
});
});
it('should delete multiple objects for multiple parents', () => {
return Model2.relatedQuery('model2Relation1')
.for([1, 2])
.delete()
.where('model1Prop2', '<', 6)
.then((numDeleted) => {
expect(numDeleted).to.equal(5);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
});
});
it('should delete multiple objects for multiple parents using a subquery', () => {
return Model2.relatedQuery('model2Relation1')
.for(Model2.query().findByIds([1, 2]))
.delete()
.where('model1Prop2', '<', 6)
.then((numDeleted) => {
expect(numDeleted).to.equal(5);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
});
});
});
});
});
};
================================================
FILE: tests/integration/find.js
================================================
const _ = require('lodash');
const utils = require('../../lib/utils/knexUtils');
const expect = require('expect.js');
const Promise = require('bluebird');
const { KnexTimeoutError } = require('knex');
const { raw, ref, val, fn, Model, QueryBuilderOperation } = require('../..');
module.exports = (session) => {
let Model1 = session.models.Model1;
let Model2 = session.models.Model2;
describe('Model find queries', () => {
describe('.query()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'hejsan 1',
model2Prop2: 30,
},
{
idCol: 2,
model2Prop1: 'hejsan 2',
model2Prop2: 20,
},
{
idCol: 3,
model2Prop1: 'hejsan 3',
model2Prop2: 10,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should return all rows when no knex methods are chained', () => {
return Model1.query()
.then((models) => {
expect(models[0]).to.be.a(Model1);
expect(models[1]).to.be.a(Model1);
expect(_.map(models, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2']);
expect(_.map(models, 'id').sort()).to.eql([1, 2]);
return Model2.query();
})
.then((models) => {
expect(models[0]).to.be.a(Model2);
expect(models[1]).to.be.a(Model2);
expect(models[2]).to.be.a(Model2);
expect(_.map(models, 'model2Prop1').sort()).to.eql([
'hejsan 1',
'hejsan 2',
'hejsan 3',
]);
expect(_.map(models, 'model2Prop2').sort()).to.eql([10, 20, 30]);
expect(_.map(models, 'idCol').sort()).to.eql([1, 2, 3]);
});
});
it('should return the given range and total count when range() is called', () => {
return Model2.query()
.range(1, 2)
.orderBy('model2_prop2', 'desc')
.then((result) => {
expect(result.results[0]).to.be.a(Model2);
expect(result.results[1]).to.be.a(Model2);
expect(result.total === 3).to.equal(true);
expect(_.map(result.results, 'model2Prop2')).to.eql([20, 10]);
});
});
it('should return the given range and total count when range() is called without arguments', () => {
return Model2.query()
.offset(1)
.limit(2)
.range()
.orderBy('model2_prop2', 'desc')
.then((result) => {
expect(result.results[0]).to.be.a(Model2);
expect(result.results[1]).to.be.a(Model2);
expect(result.total === 3).to.equal(true);
expect(_.map(result.results, 'model2Prop2')).to.eql([20, 10]);
});
});
it('should return the given page and total count when page() is called', () => {
return Model2.query()
.page(1, 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([10]);
});
});
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()
.select('model2.id_col', 'model2_prop2')
.then((models) => {
expect(models[0]).to.be.a(Model2);
// Test that only the selected columns (and stuff set by the $afterFind hook) were returned.
expect(_.uniq(_.flattenDeep(_.map(models, _.keys))).sort()).to.eql([
'$afterFindCalled',
'idCol',
'model2Prop2',
]);
expect(_.map(models, 'idCol').sort()).to.eql([1, 2, 3]);
expect(_.map(models, 'model2Prop2').sort()).to.eql([10, 20, 30]);
expect(_.map(models, '$afterFindCalled').sort()).to.eql([1, 1, 1]);
});
});
it('.where()', () => {
return Model2.query()
.where('model2_prop2', '>', 15)
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20, 30]);
});
});
it('.findOne()', () => {
return Model2.query()
.findOne('model2_prop2', '>', 20)
.then((model) => {
expect(model.model2Prop2).to.eql(30);
});
});
it('.findById()', () => {
return Model2.query()
.findById(2)
.then((model) => {
expect(model.model2Prop2).to.eql(20);
});
});
it('.findByIds()', () => {
return session.knex
.transaction((trx) => {
return Model2.query(trx)
.findByIds([1, 2])
.patch({ model2Prop1: 'what' })
.then(() => {
return Model2.query(trx).findByIds([1, 2]).orderBy('id_col');
})
.then((models) => {
expect(models.map((it) => it.model2Prop1)).to.eql(['what', 'what']);
})
.then(() => {
throw new Error();
});
})
.catch(() => {
return Model2.query().findByIds([1, 2]).orderBy('id_col');
})
.then((models) => {
expect(models.map((it) => it.model2Prop1)).to.eql(['hejsan 1', 'hejsan 2']);
});
});
it('.join() with a subquery', () => {
return Model1.query()
.findByIds(1)
.join(
Model2.query()
// Test objection raw instance in subquery where while we're at it.
.where(raw('1 = 1'))
.as('alias'),
(joinBuilder) => {
joinBuilder.on('Model1.id', 'alias.model1_id');
},
)
.then((models) => {
// Three items because Model1 (id = 1) has three related Model2 instances.
expect(models.length).to.equal(3);
});
});
it('.join() with objection.raw', () => {
return Model1.query()
.findByIds(1)
.select('Model1.id as model1Id', 'model2.id_col as model2Id')
.join('model2', (builder) => {
builder.andOn(raw('?? = ??', ['Model1.id', 'model2.model1_id']));
})
.orderBy('model2.id_col')
.then((result) => {
expect(result).to.eql([
{ model1Id: 1, model2Id: 1, $afterFindCalled: 1 },
{ model1Id: 1, model2Id: 2, $afterFindCalled: 1 },
{ model1Id: 1, model2Id: 3, $afterFindCalled: 1 },
]);
});
});
it('.where() with an a raw instance', () => {
return Model2.query()
.where(raw('model2_prop2 = 20'))
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('grouped .where() with an a raw instance', () => {
return Model2.query()
.where((builder) => {
builder.where(raw('model2_prop2 = 20'));
})
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with an a knex.raw instance', () => {
return Model2.query()
.where(session.knex.raw('model2_prop2 = 20'))
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with an object', () => {
return Model2.query()
.where({ model2_prop2: 20 })
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() and object with toKnexRaw method', () => {
return Model2.query()
.where('model2_prop2', '>', {
toKnexRaw(builder) {
return builder.knex().raw('?', 15);
},
})
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20, 30]);
});
});
it('.where() with object and object with toKnexRaw method', () => {
return Model2.query()
.where({
model2_prop2: {
toKnexRaw(builder) {
return builder.knex().raw('?', 20);
},
},
})
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with an object and query builder', () => {
return Model2.query()
.where({
model2_prop2: Model2.query().max('model2_prop2'),
})
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([30]);
});
});
it('.where() with a subquery and aliases (#769)', () => {
return Model2.query()
.alias('m1')
.where(
'id_col',
Model2.query()
.select('m2.id_col')
.alias('m2')
.where('m2.model2_prop2', ref('m1.model2_prop2')),
)
.orderBy('m1.model2_prop2')
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([10, 20, 30]);
});
});
it('.where() with a subquery and aliases (using tableRefFor in subquery)', () => {
return Model2.query()
.alias('m1')
.where((query) =>
query.where(
'id_col',
Model2.query()
.select('m2.id_col')
.alias('m2')
.where(
'm2.model2_prop2',
ref(`${query.tableRefFor(query.modelClass())}.model2_prop2`),
),
),
)
.orderBy('m1.model2_prop2')
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([10, 20, 30]);
});
});
it('.where() with an object and knex query builder', () => {
return Model2.query()
.where({
model2_prop2: session.knex('model2').max('model2_prop2'),
})
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([30]);
});
});
it('.where() with an object and knex.raw', () => {
return Model2.query()
.where({
model2_prop2: session.knex.raw('10 + 10'),
})
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with an object and objection.raw', () => {
return Model2.query()
.where({
model2_prop2: raw('10 + 10'),
})
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with objection.raw and a subquery builder', () => {
return Model2.query()
.where('id_col', raw('?', Model2.query().select('id_col').where('model2_prop2', 20)))
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('raw should accept non-string values', () => {
return Model2.query()
.where('model2_prop2', raw(20))
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with objection.raw and a subquery builder in an object', () => {
return Model2.query()
.where(
'id_col',
raw(':subQuery', {
subQuery: Model2.query().select('id_col').where('model2_prop2', 20),
}),
)
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with objection.raw and a nested mess of things', () => {
return Model2.query()
.where(
'id_col',
raw(':nestedMess', {
nestedMess: raw('?', Model2.query().select('id_col').where('model2_prop2', 20)),
}),
)
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with objection.raw and a knex subquery builder', () => {
return Model2.query()
.where(
'id_col',
raw('?', Model2.query().select('id_col').where('model2_prop2', 20).toKnexQuery()),
)
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with objection.raw and a subquery builder (array bindings)', () => {
return Model2.query()
.where('id_col', raw('?', [Model2.query().select('id_col').where('model2_prop2', 20)]))
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with objection.raw and a knex subquery builder (array bindings)', () => {
return Model2.query()
.where(
'id_col',
raw('?', [Model2.query().select('id_col').where('model2_prop2', 20).toKnexQuery()]),
)
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20]);
});
});
it('.where() with a model instance', () => {
const where = Model1.fromJson({ model1Prop1: 'hello 1' });
return Model1.query()
.where(where)
.then((models) => {
expect(_.map(models, 'model1Prop1').sort()).to.eql(['hello 1']);
});
});
it('.orderBy()', () => {
return Model2.query()
.where('model2_prop2', '>', 15)
.orderBy('model2_prop2')
.then((models) => {
expect(_.map(models, 'model2Prop2')).to.eql([20, 30]);
});
});
it('.join()', () => {
return Model2.query()
.select('model2.*', 'Model1.model1Prop1')
.where('model2_prop2', '>', 15)
.join('Model1', 'model2.model1_id', 'Model1.id')
.then((models) => {
expect(_.map(models, 'model2Prop1').sort()).to.eql(['hejsan 1', 'hejsan 2']);
expect(_.map(models, 'model1Prop1')).to.eql(['hello 1', 'hello 1']);
});
});
it('.distinct()', () => {
return Model1.query()
.distinct('Model1.id', 'Model1.model1Prop1')
.leftJoinRelated('model1Relation1', { alias: 'balls' })
.where('Model1.model1Prop1', 'hello 1')
.orderBy('Model1.model1Prop1')
.page(0, 1)
.then((res) => {
expect(res.results[0].model1Prop1).to.equal('hello 1');
});
});
if (session.isPostgres()) {
it('.distinctOn()', async () => {
await session.populate([]);
await Model1.query().insertGraph(
[
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
'#id': 'rel1',
id: 3,
model1Prop1: 'rel 1',
},
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation1: {
'#ref': 'rel1',
},
},
],
{ allowRefs: true },
);
return Model1.query()
.distinctOn('model1Relation1.id')
.select('model1Relation1.*')
.joinRelated('model1Relation1')
.then((res) => {
expect(res).to.have.length(1);
expect(res[0].id).to.equal(3);
});
});
it('.distinctOn() many-to-many', async () => {
await session.populate([]);
await Model2.query().insertGraph([
{
idCol: 1,
model2Relation1: [{ id: 1 }, { id: 2 }],
},
{
idCol: 2,
model2Relation1: [{ id: 5 }, { id: 6 }],
},
{
idCol: 3,
model2Relation1: [{ id: 3 }, { id: 4 }],
},
]);
const result = await Model2.query()
.distinctOn('id_col')
.joinRelated('model2Relation1')
.where('model2Relation1.id', '<', 5)
.orderBy('id_col');
expect(result).to.eql([
{
idCol: 1,
model1Id: null,
model2Prop1: null,
model2Prop2: null,
$afterFindCalled: 1,
},
{
idCol: 3,
model1Id: null,
model2Prop1: null,
model2Prop2: null,
$afterFindCalled: 1,
},
]);
});
}
it('.count()', () => {
return Model2.query()
.count()
.first()
.then((res) => {
expect(res[Object.keys(res)[0]]).to.eql(3);
});
});
it('.countDistinct()', () => {
return Model2.query()
.countDistinct('id_col')
.first()
.then((res) => {
expect(res[Object.keys(res)[0]]).to.eql(3);
});
});
it('from (objection subquery)', () => {
return Model1.query()
.select('sub.*')
.from(Model1.query().where('id', 2).as('sub'))
.then((res) => {
expect(res.length).to.equal(1);
expect(res[0].id).to.equal(2);
});
});
it('from (knex subquery)', () => {
return Model1.query()
.select('sub.*')
.from(session.knex('Model1').where('id', 2).as('sub'))
.then((res) => {
expect(res.length).to.equal(1);
expect(res[0].id).to.equal(2);
});
});
it('from (knex raw subquery)', () => {
return Model1.query()
.select('sub.*')
.from(session.knex.raw('(select * from ?? where ?? = 2) as sub', ['Model1', 'id']))
.then((res) => {
expect(res.length).to.equal(1);
expect(res[0].id).to.equal(2);
});
});
it('from (objection raw subquery)', () => {
return Model1.query()
.select('sub.*')
.from(raw('(select * from ?? where ?? = 2) as sub', ['Model1', 'id']))
.then((res) => {
expect(res.length).to.equal(1);
expect(res[0].id).to.equal(2);
});
});
it('from (function subquery)', () => {
return Model1.query()
.select('sub.*')
.from((builder) => builder.from('Model1').where('id', 2).as('sub'))
.then((res) => {
expect(res.length).to.equal(1);
expect(res[0].id).to.equal(2);
});
});
it('.throwIfNotFound() with empty result', (done) => {
Model1.query()
.where('model1Prop1', 'There is no value like me')
.throwIfNotFound()
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(Model1.NotFoundError);
expect(err.type).to.equal('NotFound');
expect(err.modelClass).to.equal(Model1);
done();
})
.catch(done);
});
it('custom .throwIfNotFound() with message', (done) => {
Model1.query()
.where('model1Prop1', 'There is no value like me')
.throwIfNotFound({ message: 'customMessage' })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(Model1.NotFoundError);
expect(err.data.message).to.equal('customMessage');
expect(err.modelClass).to.equal(Model1);
done();
})
.catch(done);
});
it('.throwIfNotFound() with non-empty result', () => {
return Model2.query()
.throwIfNotFound()
.where('model2_prop2', '>', 15)
.then((models) => {
expect(_.map(models, 'model2Prop2').sort()).to.eql([20, 30]);
});
});
it('.throwIfNotFound() with single result', (done) => {
Model1.query()
.where('model1Prop1', 'There is no value like me')
.first()
.throwIfNotFound()
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(Model1.NotFoundError);
done();
})
.catch(done);
});
it('.throwIfNotFound() with result equal to 0', (done) => {
Model1.query()
.where('model1Prop1', 'There is no value like me')
.delete()
.throwIfNotFound()
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(Model1.NotFoundError);
done();
})
.catch(done);
});
it('an error with a clear message should be thrown if undefined is passed to findById', (done) => {
Model1.query()
.findById(undefined)
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal('undefined was passed to findById');
done();
})
.catch(done);
});
it('an error with a clear message should be thrown if undefined is passed to findById (composite key)', (done) => {
Model1.query()
.findById([undefined, 1])
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal('undefined was passed to findById');
done();
})
.catch(done);
});
it('.throwIfNotFound() should throw error returned by `createNotFoundError`', (done) => {
class CustomError extends Error {
constructor(ctx) {
super('CustomError');
this.ctx = ctx;
}
}
class TestModel extends Model1 {
static createNotFoundError(ctx) {
return new CustomError(ctx);
}
}
TestModel.query()
.where('model1Prop1', 'There is no value like me')
.context({ foo: 'bar' })
.throwIfNotFound()
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(CustomError);
expect(err.ctx).to.eql({ foo: 'bar' });
done();
})
.catch(done);
});
it('complex nested subquery', () => {
return Model2.query()
.from((builder) => {
builder
.from('model2')
.select('*', (builder) => {
let raw;
if (utils.isMySql(session.knex)) {
raw = Model2.raw('concat(model2_prop1, model2_prop2)');
} else {
raw = Model2.raw('model2_prop1 || model2_prop2');
}
builder.select(raw).as('concatProp');
})
.as('t');
})
.where('t.concatProp', 'hejsan 310')
.then((models) => {
expect(models).to.have.length(1);
expect(models[0]).to.eql({
idCol: 3,
model1Id: 1,
model2Prop1: 'hejsan 3',
model2Prop2: 10,
concatProp: 'hejsan 310',
$afterFindCalled: 1,
});
});
});
it('knex-style function subqueries', () => {
return Model1.query()
.from((builder) => builder.select('model2.*').from('model2').as('foo'))
.orderBy((builder) =>
builder
.from('model2')
.where('model2.id_col', ref('foo.id_col'))
.select('model2_prop2'),
)
.whereExists((builder) =>
builder.from('Model1').where('foo.model1_id', ref('Model1.id')),
)
.castTo(Model2)
.then((models) => {
expect(models.map((it) => it.model2Prop2)).to.eql([10, 20, 30]);
});
});
it('raw in select', () => {
return Model2.query()
.select('model2.*', raw('?? + ? as ??', 'model2_prop2', 10, 'model2_prop2'))
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([1, 2, 3]);
expect(_.map(models, 'model2Prop2')).to.eql([40, 30, 20]);
});
});
if (session.isMySql()) {
it('fn in select', () => {
return Model2.query()
.select('model2.*', fn('concat', ref('model2_prop2'), '10').as('model2_prop2'))
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([1, 2, 3]);
expect(_.map(models, 'model2Prop2')).to.eql(['3010', '2010', '1010']);
});
});
it('fn.concat in select', () => {
return Model2.query()
.select('model2.*', fn.concat(ref('model2_prop2'), '10').as('model2_prop2'))
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([1, 2, 3]);
expect(_.map(models, 'model2Prop2')).to.eql(['3010', '2010', '1010']);
});
});
}
if (session.isPostgres()) {
it('fn in select', () => {
return Model2.query()
.select(
'model2.*',
fn('concat', ref('model2_prop2'), val('10').castText()).as('model2_prop2'),
)
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([1, 2, 3]);
expect(_.map(models, 'model2Prop2')).to.eql(['3010', '2010', '1010']);
});
});
it('fn.concat in select', () => {
return Model2.query()
.select(
'model2.*',
fn.concat(ref('model2_prop2'), val('10').castText()).as('model2_prop2'),
)
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([1, 2, 3]);
expect(_.map(models, 'model2Prop2')).to.eql(['3010', '2010', '1010']);
});
});
it('fn.coalesce in select', () => {
return Model2.query()
.select('model2.*', fn.coalesce(null, ref('model2_prop2')).as('foo'))
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([1, 2, 3]);
expect(_.map(models, 'foo')).to.eql([30, 20, 10]);
});
});
it('fn.now in select', () => {
return Model2.query()
.select('model2.*', fn.now().as('lultz'))
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([1, 2, 3]);
expect(models[0].lultz).to.be.a(Date);
});
});
it('fn.now(precision) in select', () => {
return Model2.query()
.select('model2.*', fn.now(0).as('lultz'))
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([1, 2, 3]);
expect(models[0].lultz).to.be.a(Date);
expect(models[0].lultz.getMilliseconds()).to.equal(0);
});
});
}
it('raw in where', () => {
return Model2.query()
.where('model2_prop2', raw(':value', { value: 20 }))
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([2]);
expect(_.map(models, 'model2Prop2')).to.eql([20]);
});
});
it('raw in where object', () => {
return Model2.query()
.where({
model2_prop2: raw('?', [20]),
})
.orderBy('id_col')
.then((models) => {
expect(_.map(models, 'idCol')).to.eql([2]);
expect(_.map(models, 'model2Prop2')).to.eql([20]);
});
});
it('subquery builder in select', () => {
return Model1.query()
.select(
'Model1.*',
Model2.query()
.sum('model2_prop2')
.where('Model1.id', ref('model2.model1_id'))
.as('sum'),
)
.orderBy('id')
.then((models) => {
expect(_.map(models, 'id')).to.eql([1, 2]);
expect(_.map(models, 'sum')).to.eql([60, null]);
});
});
it('subquery builder in select (array)', () => {
return Model1.query()
.select([
'Model1.*',
Model2.query()
.sum('model2_prop2')
.where('Model1.id', ref('model2.model1_id'))
.as('sum'),
])
.orderBy('id')
.then((models) => {
expect(_.map(models, 'id')).to.eql([1, 2]);
expect(_.map(models, 'sum')).to.eql([60, null]);
});
});
if (session.isPostgres()) {
it('select subquery as an array', () => {
return Model1.query()
.select(
'Model1.*',
raw(
'ARRAY(?) as "model1Ids"',
Model1.relatedQuery('model1Relation2').select('id_col').orderBy('id_col'),
),
)
.orderBy('id')
.then((res) => {
expect(res[0].model1Ids).to.eql([1, 2, 3]);
expect(res[1].model1Ids).to.eql([]);
});
});
it('select subquery as an array with aliased table', () => {
return Model1.query()
.alias('m')
.select(
'm.*',
raw(
'ARRAY(?) as "model1Ids"',
// Test doubly nested `raw` for shits and giggles.
raw(
'?',
Model1.relatedQuery('model1Relation2').select('id_col').orderBy('id_col'),
),
),
)
.orderBy('id')
.then((res) => {
expect(res[0].model1Ids).to.eql([1, 2, 3]);
expect(res[1].model1Ids).to.eql([]);
});
});
it('select subquery as an array (unbound model)', () => {
class TestModel extends Model1 {}
TestModel.knex(null);
return TestModel.query(session.knex)
.select(
'Model1.*',
raw(
'ARRAY(?) as "model1Ids"',
TestModel.relatedQuery('model1Relation2').select('id_col').orderBy('id_col'),
),
)
.orderBy('id')
.then((res) => {
expect(res[0].model1Ids).to.eql([1, 2, 3]);
expect(res[1].model1Ids).to.eql([]);
});
});
}
it('select subquery to same table with alias', () => {
return Model1.query()
.upsertGraph(
{
id: 1,
model1Relation1: {
id: 2,
},
},
{ relate: true },
)
.then(() => {
return Model1.query()
.findById(1)
.alias('m1')
.select('m1.*', Model1.relatedQuery('model1Relation1').select('id').as('foo'));
})
.then((res) => {
expect(res.foo).to.equal(2);
});
});
it('deeply nested where subquery to same table with alias', () => {
return Model1.query()
.upsertGraph(
{
id: 1,
model1Relation1: {
id: 2,
},
},
{ relate: true },
)
.then(() => {
return Model1.query()
.alias('m1')
.where((builder) => {
// The two nested grouping where's are relevant here.
builder.where((builder) => {
builder.where(val(2), Model1.relatedQuery('model1Relation1').select('id'));
});
});
})
.then((res) => {
expect(res).to.have.length(1);
expect(res[0].id).to.equal(1);
});
});
it('deeply nested subqueries to same table with alias', () => {
return Model1.query()
.insertGraph({
id: 1001,
model1Relation1: {
id: 1002,
model1Relation1: {
id: 1003,
model1Relation1: {
id: 1004,
},
},
},
})
.then(() => {
return Model1.query()
.alias('m1')
.whereExists(
Model1.relatedQuery('model1Relation1')
.alias('m2')
.whereExists(
Model1.relatedQuery('model1Relation1')
.alias('m3')
.whereExists(Model1.relatedQuery('model1Relation1').alias('m4')),
),
);
})
.then((res) => {
expect(res).to.have.length(1);
expect(res[0].id).to.equal(1001);
});
});
it('.modify()', () => {
let builder = Model2.query();
return builder
.modify(
(modifyBuilder, arg1, arg2, arg3) => {
expect(modifyBuilder).to.equal(builder);
expect(arg1).to.equal('foo');
expect(arg2).to.equal(undefined);
expect(arg3).to.equal(10);
builder.where('model2_prop1', '>=', 'hejsan 2');
},
'foo',
undefined,
10,
)
.then((models) => {
expect(_.map(models, 'model2Prop1').sort()).to.eql(['hejsan 2', 'hejsan 3']);
});
});
it('from', () => {
return Model1.query()
.upsertGraph({
id: 2,
model1Relation2: [
{
model2Prop1: 'lol',
},
],
})
.then(() => {
return (
Model1.query()
.select('m2.*')
.from({
m1: 'Model1',
m2: 'model2',
})
// This tests that correct alias is used
// for Model1 under the hood.
.findByIds(1)
.where('m1.id', ref('m2.model1_id'))
.orderBy('m2.id_col')
.castTo(Model2)
);
})
.then((models) => {
expect(models).to.eql([
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: 30,
$afterFindCalled: 1,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: 20,
$afterFindCalled: 1,
},
{
idCol: 3,
model1Id: 1,
model2Prop1: 'hejsan 3',
model2Prop2: 10,
$afterFindCalled: 1,
},
]);
});
});
it('whereExists in nested where with relatedQuery', () => {
return Model1.query()
.where((builder) => {
builder.whereExists(Model1.relatedQuery('model1Relation2'));
})
.withGraphFetched('model1Relation2')
.then((results) => {
expect(results.length).to.equal(1);
expect(results[0].model1Prop1).to.equal('hello 1');
});
});
if (!session.isMySql()) {
it('with', () => {
return Model1.query()
.with('subquery1', Model1.query().unionAll(Model1.query()))
.with('subquery2', Model1.query())
.count('* as count')
.from('subquery1')
.first()
.then((result) => {
expect(result.count).to.eql(4);
});
});
}
if (session.isPostgres()) {
it('timeout should throw a KnexTimeoutError', (done) => {
const knexQuery = Model1.query().timeout(50).toKnexQuery();
// Now the tricky part. We add `pg_sleep` as another source table so that the query
// takes a long time.
knexQuery.from({
sleep: session.knex.raw('pg_sleep(0.1)'),
Model1: 'Model1',
});
knexQuery
.then(() => done(new Error('should not get here')))
.catch((err) => {
expect(err).to.be.a(KnexTimeoutError);
done();
})
.catch(done);
});
it('smoke test for various methods', () => {
// This test doesn't actually test that the methods work. Knex has tests
// for these. This is a smoke test in case of typos and such.
return Model2.query()
.with('wm1', (builder) =>
builder
.insert({ a: 1 })
.update({ a: 2 })
.delete()
.del()
.table('model2')
.clear(QueryBuilderOperation)
.select('*')
.from('model2'),
)
.clearSelect()
.clearWhere()
.columns('model2.model2_prop2')
.where(raw('? = ?', ref('model2.id_col'), ref('model2.model2_prop2')))
.where(raw('? in (?)', ref('model2.id_col'), Model1.query().select('id')))
.whereNot('model2.id_col', 1)
.orWhereNot('model2.id_col', 2)
.whereRaw('model2.id_col is null')
.andWhereRaw('model2.id_col is null')
.orWhereRaw('model2.id_col is null')
.whereExists(Model2.query())
.orWhereExists(Model2.query())
.whereNotExists(Model2.query())
.orWhereNotExists(Model2.query())
.orWhereIn('model2.id_col', [1, 2, 3])
.whereNotIn('model2.id_col', [1, 2, 3])
.orWhereNotIn('model2.id_col', [1, 2, 3])
.whereNull('model2.id_col')
.orWhereNull('model2.id_col')
.orWhereNotNull('model2.id_col')
.andWhereBetween('model2.id_col', [0, 1])
.whereNotBetween('model2.id_col', [0, 1])
.andWhereNotBetween('model2.id_col', [0, 1])
.orWhereBetween('model2.id_col', [0, 1])
.orWhereNotBetween('model2.id_col', [0, 1])
.whereColumn('model2.id_col', 'model2.id_col')
.andWhereColumn('model2.id_col', 'model2.id_col')
.orWhereColumn('model2.id_col', 'model2.id_col')
.whereNotColumn('model2.id_col', 'model2.id_col')
.andWhereNotColumn('model2.id_col', 'model2.id_col')
.orWhereNotColumn('model2.id_col', 'model2.id_col')
.orderByRaw('model2.id_col')
.into('model2')
.table('model2')
.joinRaw('inner join model2 as m1 on m1.model2_prop2 = 1')
.leftOuterJoin('model2 as m2', (join) =>
join
.onBetween('m2.model2_prop2', [1, 2])
.onNotBetween('m2.model2_prop2', [1, 2])
.orOnBetween('m2.model2_prop2', [1, 2])
.orOnNotBetween('m2.model2_prop2', [1, 2])
.onIn('m2.model2_prop2', [1, 2])
.onNotIn('m2.model2_prop2', [1, 2])
.orOnIn('m2.model2_prop2', [1, 2])
.andOnIn('m2.model2_prop2', [1, 2])
.orOnNotIn('m2.model2_prop2', [1, 2])
.onNull('m2.model2_prop2')
.orOnNull('m2.model2_prop2')
.onNotNull('m2.model2_prop2')
.orOnNotNull('m2.model2_prop2')
.onExists(Model2.query())
.orOnExists(Model2.query())
.onNotExists(Model2.query())
.orOnNotExists(Model2.query())
.andOnExists(Model2.query())
.andOnNotExists(Model2.query())
.andOnBetween('m2.model2_prop2', [1, 2])
.andOnNotBetween('m2.model2_prop2', [1, 2])
.andOn('m2.model2_prop2', 1)
.orOnNotIn('m2.model2_prop2', [1, 2])
.andOnNotIn('m2.model2_prop2', [1, 2])
.andOnNull('m2.model2_prop2')
.andOnNotNull('m2.model2_prop2')
.onVal('m2.model2_prop2', 1)
.andOnVal('m2.model2_prop2', 2)
.orOnVal('m2.model2_prop2', 3),
)
.rightJoin('model2 as m3', 'm3.model2_prop2', 'm1.model2_prop2')
.rightOuterJoin('model2 as m4', 'm4.model2_prop2', 'm1.model2_prop2')
.fullOuterJoin('model2 as m6', 'm6.model2_prop2', 'm1.model2_prop2')
.crossJoin('model2 as m7')
.whereWrapped('model2.id_col < 10');
});
}
});
});
describe('relatedQuery()', () => {
before(() => {
return session.populate([
{
id: 1,
model1Relation1: {
id: 3,
},
model1Relation2: [
{
idCol: 1,
},
{
idCol: 2,
},
],
model1Relation3: [
{
idCol: 4,
},
],
},
{
id: 2,
model1Relation1: {
id: 4,
},
model1Relation2: [
{
idCol: 3,
},
],
model1Relation3: [
{
idCol: 5,
},
{
idCol: 6,
},
],
},
]);
});
it('should work in select', () => {
return Model1.query()
.select([
'id',
Model1.relatedQuery('model1Relation1').count().as('rel1Count'),
Model1.relatedQuery('model1Relation2').count().as('rel2Count'),
Model1.relatedQuery('model1Relation3').count().as('rel3Count'),
])
.orderBy('id')
.then((res) => {
expect(res).to.eql([
{ id: 1, rel1Count: 1, rel2Count: '2', rel3Count: '1', $afterFindCalled: 1 },
{ id: 2, rel1Count: 1, rel2Count: '1', rel3Count: '2', $afterFindCalled: 1 },
{ id: 3, rel1Count: 0, rel2Count: '0', rel3Count: '0', $afterFindCalled: 1 },
{ id: 4, rel1Count: 0, rel2Count: '0', rel3Count: '0', $afterFindCalled: 1 },
]);
});
});
it('should work with alias', () => {
return Model1.query()
.alias('m')
.select([
'id',
Model1.relatedQuery('model1Relation1').count().as('rel1Count'),
Model1.relatedQuery('model1Relation2').count().as('rel2Count'),
Model1.relatedQuery('model1Relation3').count().as('rel3Count'),
])
.orderBy('id')
.then((res) => {
expect(res).to.eql([
{ id: 1, rel1Count: 1, rel2Count: '2', rel3Count: '1', $afterFindCalled: 1 },
{ id: 2, rel1Count: 1, rel2Count: '1', rel3Count: '2', $afterFindCalled: 1 },
{ id: 3, rel1Count: 0, rel2Count: '0', rel3Count: '0', $afterFindCalled: 1 },
{ id: 4, rel1Count: 0, rel2Count: '0', rel3Count: '0', $afterFindCalled: 1 },
]);
});
});
it('self referential relations should work', () => {
return Model1.query()
.select(['id', Model1.relatedQuery('model1Relation1').select('id').as('relId')])
.orderBy('id')
.then((res) => {
expect(res).to.eql([
{ id: 1, relId: 3, $afterFindCalled: 1 },
{ id: 2, relId: 4, $afterFindCalled: 1 },
{ id: 3, relId: null, $afterFindCalled: 1 },
{ id: 4, relId: null, $afterFindCalled: 1 },
]);
});
});
it('should work with subquery alias', () => {
return Model1.query()
.select([
'id',
Model1.relatedQuery('model1Relation1').alias('a2').select('a2.id').as('relId'),
])
.alias('a1')
.orderBy('id')
.then((res) => {
expect(res).to.eql([
{ id: 1, relId: 3, $afterFindCalled: 1 },
{ id: 2, relId: 4, $afterFindCalled: 1 },
{ id: 3, relId: null, $afterFindCalled: 1 },
{ id: 4, relId: null, $afterFindCalled: 1 },
]);
});
});
it('should work in where', () => {
return Model1.query()
.where(val(3), Model1.relatedQuery('model1Relation1').select('id'))
.first()
.then((res) => {
expect(res.id).to.equal(1);
});
});
describe('for()', () => {
describe('belongs to one relation', () => {
it('find using single id', async () => {
const result = await Model1.relatedQuery('model1Relation1').for(1).orderBy('id');
expect(result.length).to.equal(1);
expect(result[0].id).to.equal(3);
});
it('find using single model instance', async () => {
const model = Model1.fromJson({ id: 1, model1Id: 3 });
const result = await Model1.relatedQuery('model1Relation1').for(model).orderBy('id');
expect(result.length).to.equal(1);
expect(result[0].id).to.equal(3);
});
it('find using multiple model instances', async () => {
const model1 = Model1.fromJson({ id: 1, model1Id: 3 });
const model2 = Model1.fromJson({ id: 2, model1Id: 4 });
const result = await Model1.relatedQuery('model1Relation1')
.for([model1, model2])
.orderBy('id');
expect(result.length).to.equal(2);
expect(result[0].id).to.equal(3);
expect(result[1].id).to.equal(4);
});
it('find using multiple ids', async () => {
const result = await Model1.relatedQuery('model1Relation1').for([1, 2]).orderBy('id');
expect(result.length).to.equal(2);
expect(result[0].id).to.equal(3);
expect(result[1].id).to.equal(4);
});
it('find using multiple ids and a filter', async () => {
const result = await Model1.relatedQuery('model1Relation1')
.for([1, 2])
.whereNotIn('id', [1, 2, 3])
.orderBy('id');
expect(result.length).to.equal(1);
expect(result[0].id).to.equal(4);
});
it('find using query builder with one result', async () => {
const result = await Model1.relatedQuery('model1Relation1')
.for(Model1.query().findById(1))
.orderBy('id');
expect(result.length).to.equal(1);
expect(result[0].id).to.equal(3);
});
it('find using query builder with multiple results', async () => {
const result = await Model1.relatedQuery('model1Relation1')
.for(Model1.query().findByIds([1, 2]))
.orderBy('id');
expect(result.length).to.equal(2);
expect(result[0].id).to.equal(3);
expect(result[1].id).to.equal(4);
});
});
describe('has many relation', () => {
it('find using single id', async () => {
const result = await Model1.relatedQuery('model1Relation2').for(1).orderBy('id_col');
expect(result.length).to.equal(2);
expect(result[0].idCol).to.equal(1);
expect(result[1].idCol).to.equal(2);
});
it('find using multiple ids', async () => {
const result = await Model1.relatedQuery('model1Relation2')
.for([1, 2])
.orderBy('id_col');
expect(result.length).to.equal(3);
expect(result[0].idCol).to.equal(1);
expect(result[1].idCol).to.equal(2);
expect(result[2].idCol).to.equal(3);
});
it('find using query builder with one result', async () => {
const result = await Model1.relatedQuery('model1Relation2')
.for(Model1.query().findById(1))
.orderBy('id_col');
expect(result.length).to.equal(2);
expect(result[0].idCol).to.equal(1);
expect(result[1].idCol).to.equal(2);
});
it('find using query builder with multiple results', async () => {
const result = await Model1.relatedQuery('model1Relation2')
.for(Model1.query().findByIds([1, 2]))
.orderBy('id_col');
expect(result.length).to.equal(3);
expect(result[0].idCol).to.equal(1);
expect(result[1].idCol).to.equal(2);
expect(result[2].idCol).to.equal(3);
});
});
describe('many to many relation', () => {
it('find using single id', async () => {
const result = await Model1.relatedQuery('model1Relation3').for(1).orderBy('id_col');
expect(result.length).to.equal(1);
expect(result[0].idCol).to.equal(4);
});
it('find using multiple ids', async () => {
const result = await Model1.relatedQuery('model1Relation3')
.for([1, 2])
.orderBy('id_col');
expect(result.length).to.equal(3);
expect(result[0].idCol).to.equal(4);
expect(result[1].idCol).to.equal(5);
expect(result[2].idCol).to.equal(6);
});
it('find using query builder with one result', async () => {
const result = await Model1.relatedQuery('model1Relation3')
.for(Model1.query().findById(1))
.orderBy('id_col');
expect(result.length).to.equal(1);
expect(result[0].idCol).to.equal(4);
});
it('find using query builder with multiple results', async () => {
const result = await Model1.relatedQuery('model1Relation3')
.for(Model1.query().where('id', 1).orWhere('id', 2))
.orderBy('id_col');
expect(result.length).to.equal(3);
expect(result[0].idCol).to.equal(4);
expect(result[1].idCol).to.equal(5);
expect(result[2].idCol).to.equal(6);
});
});
});
});
describe('joinRelated()', () => {
before(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
model1Relation1: {
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'hejsan 4',
},
],
},
},
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'hejsan 1',
model2Relation1: [
{
id: 5,
model1Prop1: 'hello 5',
},
],
},
{
idCol: 2,
model2Prop1: 'hejsan 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'hello 6',
},
{
id: 7,
model1Prop1: 'hello 7',
model1Relation1: {
id: 8,
model1Prop1: 'hello 8',
},
model1Relation2: [
{
idCol: 3,
model2Prop1: 'hejsan 3',
},
],
},
],
},
],
},
]);
});
['joinRelated', 'innerJoinRelated'].forEach((joinMethod) => {
it(`should join a belongs to one relation using ${joinMethod}`, () => {
return Model1.query()
.select('Model1.*', 'model1Relation1.model1Prop1 as rel_model1Prop1')
[joinMethod]('model1Relation1')
.orderBy('Model1.id')
.then((models) => {
expect(_.map(models, 'id')).to.eql([1, 2, 3, 7]);
expect(_.map(models, 'rel_model1Prop1')).to.eql([
'hello 2',
'hello 3',
'hello 4',
'hello 8',
]);
});
});
});
['leftJoinRelated', 'leftOuterJoinRelated'].forEach((joinMethod) => {
it(`should join a belongs to one relation using ${joinMethod}`, () => {
return Model1.query()
.select('Model1.id', 'model1Relation1.model1Prop1 as rel_model1Prop1')
[joinMethod]('model1Relation1')
.orderBy('Model1.id')
.then((models) => {
expect(models).to.eql([
{ id: 1, rel_model1Prop1: 'hello 2', $afterFindCalled: 1 },
{ id: 2, rel_model1Prop1: 'hello 3', $afterFindCalled: 1 },
{ id: 3, rel_model1Prop1: 'hello 4', $afterFindCalled: 1 },
{ id: 4, rel_model1Prop1: null, $afterFindCalled: 1 },
{ id: 5, rel_model1Prop1: null, $afterFindCalled: 1 },
{ id: 6, rel_model1Prop1: null, $afterFindCalled: 1 },
{ id: 7, rel_model1Prop1: 'hello 8', $afterFindCalled: 1 },
{ id: 8, rel_model1Prop1: null, $afterFindCalled: 1 },
]);
});
});
});
it('should be able to use `joinRelated` in a sub query (1)', () => {
return Model1.query()
.from(
Model1.query()
.joinRelated('model1Relation2')
.select('Model1.id', 'model1Relation2.id_col as m2r2Id')
.as('inner'),
)
.select('*')
.orderBy(['id', 'm2r2Id'])
.then((models) => {
expect(models).to.eql([
{ id: 1, m2r2Id: 1, $afterFindCalled: 1 },
{ id: 1, m2r2Id: 2, $afterFindCalled: 1 },
{ id: 4, m2r2Id: 4, $afterFindCalled: 1 },
{ id: 7, m2r2Id: 3, $afterFindCalled: 1 },
]);
});
});
it('should be able to use `joinRelated` in a sub query (2)', () => {
return Model1.query()
.from(
raw(
'?',
Model1.query()
.joinRelated('model1Relation2')
.select('Model1.id', 'model1Relation2.id_col as m2r2Id')
.as('inner'),
),
)
.select('*')
.orderBy(['id', 'm2r2Id'])
.then((models) => {
expect(models).to.eql([
{ id: 1, m2r2Id: 1, $afterFindCalled: 1 },
{ id: 1, m2r2Id: 2, $afterFindCalled: 1 },
{ id: 4, m2r2Id: 4, $afterFindCalled: 1 },
{ id: 7, m2r2Id: 3, $afterFindCalled: 1 },
]);
});
});
it('should join a has many relation (1)', () => {
return Model1.query()
.select('Model1.*', 'model1Relation2.id_col')
.joinRelated('model1Relation2')
.then((models) => {
models = _.sortBy(models, ['id', 'id_col']);
expect(_.map(models, 'id')).to.eql([1, 1, 4, 7]);
expect(_.map(models, 'id_col')).to.eql([1, 2, 4, 3]);
});
});
it('should join a has many relation (2)', () => {
return Model1.query()
.select('Model1.*', 'model1Relation2.id_col')
.joinRelated('model1Relation2')
.where('model1Relation2.id_col', '<', 4)
.then((models) => {
models = _.sortBy(models, ['id', 'id_col']);
expect(_.map(models, 'id')).to.eql([1, 1, 7]);
expect(_.map(models, 'id_col')).to.eql([1, 2, 3]);
});
});
it('should join a many to many relation (1)', () => {
return Model2.query()
.select('model2.*', 'model2Relation1.id')
.joinRelated('model2Relation1')
.then((models) => {
models = _.sortBy(models, ['idCol', 'id']);
expect(_.map(models, 'idCol')).to.eql([1, 2, 2]);
expect(_.map(models, 'id')).to.eql([5, 6, 7]);
});
});
it('should join a many to many relation (2)', () => {
return Model2.query()
.select('model2.*', 'model2Relation1.id')
.joinRelated('model2Relation1')
.whereBetween('model2Relation1.id', [5, 6])
.then((models) => {
models = _.sortBy(models, ['idCol', 'id']);
expect(_.map(models, 'idCol')).to.eql([1, 2]);
expect(_.map(models, 'id')).to.eql([5, 6]);
});
});
it('should be able to alias the join table using aliasFor in many to many relation', () => {
return Model2.query()
.select('model2.*', 'model2Relation1.id')
.joinRelated('model2Relation1')
.aliasFor('Model1Model2', 'm1m2')
.where('m1m2.model1Id', '>', 5)
.then((models) => {
models = _.sortBy(models, ['idCol', 'id']);
expect(_.map(models, 'idCol')).to.eql([2, 2]);
expect(_.map(models, 'id')).to.eql([6, 7]);
});
});
it('should be able to specify innerJoin', () => {
return Model1.query()
.innerJoinRelated('model1Relation1')
.then((models) => {
expect(models.length).to.equal(4);
});
});
it('should be able to specify leftJoin', () => {
return Model1.query()
.leftJoinRelated('model1Relation1')
.then((models) => {
expect(models.length).to.equal(8);
});
});
it('should join an eager expression `a.a`', () => {
return Model1.query()
.select('Model1.id', 'Model1.model1Prop1')
.leftJoinRelated('model1Relation1.model1Relation1')
.where('model1Relation1:model1Relation1.model1Prop1', 'hello 4')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 2,
model1Prop1: 'hello 2',
});
});
});
it('should join an eager expression `a.b`', () => {
return Model1.query()
.select('Model1.id', 'Model1.model1Prop1')
.leftJoinRelated('model1Relation1.model1Relation2')
.where('model1Relation1:model1Relation2.model2_prop1', 'hejsan 4')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 3,
model1Prop1: 'hello 3',
});
});
});
it('aliases should work with eager expression `a.b`', () => {
return Model1.query()
.select('Model1.id', 'Model1.model1Prop1')
.leftJoinRelated('model1Relation1 as a . model1Relation2 as b')
.where('a:b.model2_prop1', 'hejsan 4')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 3,
model1Prop1: 'hello 3',
});
});
});
it('should join an eager expression `a.a.b`', () => {
return Model1.query()
.select('Model1.id', 'Model1.model1Prop1')
.leftJoinRelated('model1Relation1.model1Relation1.model1Relation2')
.where('model1Relation1:model1Relation1:model1Relation2.model2_prop1', 'hejsan 4')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 2,
model1Prop1: 'hello 2',
});
});
});
it('should join an eager expression `[a, b]`', () => {
return Model1.query()
.select('Model1.id', 'Model1.model1Prop1', 'model1Relation2.model2_prop1 as model2Prop1')
.leftJoinRelated('[model1Relation1, model1Relation2]')
.where('model1Relation2.model2_prop1', 'hejsan 1')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model1Prop1: 'hello 1',
model2Prop1: 'hejsan 1',
});
});
});
it('should join an eager expression `[a, b.c]`', () => {
return Model1.query()
.select(
'Model1.id',
'model1Relation2:model2Relation1.model1Prop1 as foo',
'model1Relation2.model2_prop1 as model2Prop1',
)
.leftJoinRelated('[model1Relation1, model1Relation2.model2Relation1]')
.where('model1Relation2:model2Relation1.model1Prop1', 'hello 6')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model2Prop1: 'hejsan 2',
foo: 'hello 6',
});
});
});
it('should be able to merge joinRelated calls', () => {
return (
Model1.query()
.select(
'Model1.id',
'model1Relation1.id as m1r1Id',
'model1Relation2.id_col as m1r2Id',
'model1Relation2:model2Relation1.id as m1r2M2r1Id',
)
.joinRelated('model1Relation1')
// Join the same relation again for shits and giggles.
.joinRelated('model1Relation1')
.joinRelated('model1Relation2')
.joinRelated('model1Relation2.model2Relation1')
.orderBy(['Model1.id', 'model1Relation2.id_col', 'model1Relation2:model2Relation1.id'])
.then((models) => {
expect(models).to.eql([
{
id: 1,
m1r1Id: 2,
m1r2Id: 1,
m1r2M2r1Id: 5,
$afterFindCalled: 1,
},
{
id: 1,
m1r1Id: 2,
m1r2Id: 2,
m1r2M2r1Id: 6,
$afterFindCalled: 1,
},
{
id: 1,
m1r1Id: 2,
m1r2Id: 2,
m1r2M2r1Id: 7,
$afterFindCalled: 1,
},
]);
})
);
});
it('should be able to merge joinRelated calls with different aliases (1)', () => {
return Model1.query()
.select('Model1.id', 'm1r1.id as m1r1Id', 'm1r1_2.id as m1r1Id2')
.joinRelated('model1Relation1', {
alias: 'm1r1',
})
.joinRelated('model1Relation1', {
alias: 'm1r1_2',
})
.orderBy('Model1.id')
.then((models) => {
expect(models).to.eql([
{ id: 1, m1r1Id: 2, m1r1Id2: 2, $afterFindCalled: 1 },
{ id: 2, m1r1Id: 3, m1r1Id2: 3, $afterFindCalled: 1 },
{ id: 3, m1r1Id: 4, m1r1Id2: 4, $afterFindCalled: 1 },
{ id: 7, m1r1Id: 8, m1r1Id2: 8, $afterFindCalled: 1 },
]);
});
});
it('should be able to merge joinRelated calls with different aliases (2)', () => {
return Model1.query()
.select('Model1.id', 'm1r1.id as m1r1Id', 'm1r1_2.id as m1r1Id2')
.joinRelated('model1Relation1 as m1r1')
.joinRelated('model1Relation1 as m1r1_2')
.orderBy('Model1.id')
.then((models) => {
expect(models).to.eql([
{ id: 1, m1r1Id: 2, m1r1Id2: 2, $afterFindCalled: 1 },
{ id: 2, m1r1Id: 3, m1r1Id2: 3, $afterFindCalled: 1 },
{ id: 3, m1r1Id: 4, m1r1Id2: 4, $afterFindCalled: 1 },
{ id: 7, m1r1Id: 8, m1r1Id2: 8, $afterFindCalled: 1 },
]);
});
});
it('should be able to merge different joinRelated calls', () => {
return Model1.query()
.select('Model1.id', 'm1r1.id as m1r1Id', 'model1Relation2.id_col as m1r2Id')
.joinRelated('model1Relation1', {
alias: 'm1r1',
})
.leftJoinRelated('model1Relation2')
.orderBy(['Model1.id', 'model1Relation2.id_col'])
.then((models) => {
expect(models).to.eql([
{ id: 1, m1r1Id: 2, m1r2Id: 1, $afterFindCalled: 1 },
{ id: 1, m1r1Id: 2, m1r2Id: 2, $afterFindCalled: 1 },
{ id: 2, m1r1Id: 3, m1r2Id: null, $afterFindCalled: 1 },
{ id: 3, m1r1Id: 4, m1r2Id: null, $afterFindCalled: 1 },
{ id: 7, m1r1Id: 8, m1r2Id: 3, $afterFindCalled: 1 },
]);
});
});
it('should be able to merge joinRelated calls with different aliases (3)', () => {
return Model1.query()
.select('Model1.id', 'm1r1.id as m1r1Id', 'm1r1_2.id as m1r1Id2')
.joinRelated('model1Relation1', {
aliases: {
model1Relation1: 'm1r1',
},
})
.joinRelated('model1Relation1', {
aliases: {
model1Relation1: 'm1r1_2',
},
})
.orderBy('Model1.id')
.then((models) => {
expect(models).to.eql([
{ id: 1, m1r1Id: 2, m1r1Id2: 2, $afterFindCalled: 1 },
{ id: 2, m1r1Id: 3, m1r1Id2: 3, $afterFindCalled: 1 },
{ id: 3, m1r1Id: 4, m1r1Id2: 4, $afterFindCalled: 1 },
{ id: 7, m1r1Id: 8, m1r1Id2: 8, $afterFindCalled: 1 },
]);
});
});
it('should be able to merge leftJoinRelated calls', () => {
return (
Model1.query()
.select(
'Model1.id',
'model1Relation2:model2Relation1.model1Prop1 as foo',
'model1Relation2.model2_prop1 as model2Prop1',
)
.leftJoinRelated('model1Relation1')
// Join the same relation again for shits and giggles.
.leftJoinRelated('model1Relation1')
.leftJoinRelated('model1Relation2')
.leftJoinRelated('model1Relation2.model2Relation1')
.where('model1Relation2:model2Relation1.model1Prop1', 'hello 6')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model2Prop1: 'hejsan 2',
foo: 'hello 6',
});
})
);
});
it('should be able to specify aliases', () => {
return Model1.query()
.select([
'Model1.id',
'm1r1:m1r1.id as x',
'm1r2:m2r1.model1Prop1 as foo',
'm1r2.model2_prop1 as model2Prop1',
])
.leftJoinRelated('[model1Relation1.model1Relation1, model1Relation2.model2Relation1]', {
aliases: {
model1Relation1: 'm1r1',
model1Relation2: 'm1r2',
model2Relation1: 'm2r1',
},
})
.where('m1r2:m2r1.model1Prop1', 'hello 6')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model2Prop1: 'hejsan 2',
foo: 'hello 6',
x: 3,
});
});
});
it('should be able to specify aliases in the relation expression (string)', () => {
return Model1.query()
.select([
'Model1.id',
'm1r1:m1r1.id as x',
'm1r2:m2r1.model1Prop1 as foo',
'm1r2.model2_prop1 as model2Prop1',
])
.leftJoinRelated(
`[
model1Relation1 as m1r1.[
model1Relation1 as m1r1
],
model1Relation2 as m1r2.[
model2Relation1 as m2r1
]
]`,
)
.where('m1r2:m2r1.model1Prop1', 'hello 6')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model2Prop1: 'hejsan 2',
foo: 'hello 6',
x: 3,
});
});
});
it('should be able to specify aliases in the relation expression (object)', () => {
return Model1.query()
.select([
'Model1.id',
'm1r1:m1r1.id as x',
'm1r2:m2r1.model1Prop1 as foo',
'm1r2.model2_prop1 as model2Prop1',
])
.leftJoinRelated({
m1r1: {
$relation: 'model1Relation1',
m1r1: {
$relation: 'model1Relation1',
},
},
m1r2: {
$relation: 'model1Relation2',
m2r1: {
$relation: 'model2Relation1',
},
},
})
.where('m1r2:m2r1.model1Prop1', 'hello 6')
.first()
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model2Prop1: 'hejsan 2',
foo: 'hello 6',
x: 3,
});
});
});
it('should disable alias with option alias = false', () => {
return Model1.query()
.select('model2.*', 'Model1.id')
.joinRelated('model1Relation2', { alias: false })
.where('model2.id_col', '<', 4)
.then((models) => {
models = _.sortBy(models, ['id', 'id_col']);
expect(_.map(models, 'id')).to.eql([1, 1, 7]);
expect(_.map(models, 'id_col')).to.eql([1, 2, 3]);
});
});
it('should use relation name as alias with option alias = true', () => {
return Model1.query()
.select('Model1.*', 'model1Relation2.id_col')
.joinRelated('model1Relation2', { alias: true })
.where('model1Relation2.id_col', '<', 4)
.then((models) => {
models = _.sortBy(models, ['id', 'id_col']);
expect(_.map(models, 'id')).to.eql([1, 1, 7]);
expect(_.map(models, 'id_col')).to.eql([1, 2, 3]);
});
});
it('should use custom alias with option alias = string', () => {
return Model1.query()
.select('Model1.*', 'fooBarBaz.id_col')
.joinRelated('model1Relation2', { alias: 'fooBarBaz' })
.where('fooBarBaz.id_col', '<', 4)
.then((models) => {
models = _.sortBy(models, ['id', 'id_col']);
expect(_.map(models, 'id')).to.eql([1, 1, 7]);
expect(_.map(models, 'id_col')).to.eql([1, 2, 3]);
});
});
it('should join eager expression a.b.c, select c.* and cast result to c', () => {
return Model1.query()
.joinRelated('model1Relation2.model2Relation1.model1Relation1')
.select([
'model1Relation2:model2Relation1:model1Relation1.id as id_col',
'model1Relation2:model2Relation1.id as model1_id',
])
.castTo(Model2)
.then((models) => {
expect(models[0]).to.be.a(Model2);
expect(models).to.eql([
{
idCol: 8,
model1Id: 7,
$afterFindCalled: 1,
},
]);
});
});
it('should join eager expression a.b.c, select columns with aliases and cast result to Model', () => {
return Model1.query()
.joinRelated('model1Relation2.model2Relation1.model1Relation1')
.select([
'model1Relation2:model2Relation1:model1Relation1.id as someId',
'model1Relation2:model2Relation1.id as someOtherId',
])
.castTo(Model)
.then((models) => {
expect(models[0]).to.be.a(Model);
expect(models[0]).to.not.be.a(Model1);
expect(models).to.eql([
{
someId: 8,
someOtherId: 7,
},
]);
});
});
it('should be able to call castTo with no arguments', () => {
return Model1.query()
.joinRelated('model1Relation2.model2Relation1.model1Relation1')
.select([
'model1Relation2:model2Relation1:model1Relation1.id as someId',
'model1Relation2:model2Relation1.id as someOtherId',
])
.castTo()
.then((models) => {
expect(models[0]).to.be.a(Model1);
expect(models).to.eql([
{
$afterFindCalled: 1,
someId: 8,
someOtherId: 7,
},
]);
});
});
it('should count related models', () => {
return Model1.query()
.leftJoinRelated('model1Relation2')
.select('Model1.id', 'Model1.model1Prop1')
.count('Model1.id as relCount')
.groupBy('Model1.id', 'Model1.model1Prop1')
.findByIds([1, 2])
.orderBy('id')
.then((result) => {
return result.map((it) => {
it.relCount = parseInt(it.relCount);
return it;
});
})
.then((res) => {
expect(res).to.eql([
{ id: 1, model1Prop1: 'hello 1', relCount: 2, $afterFindCalled: 1 },
{ id: 2, model1Prop1: 'hello 2', relCount: 1, $afterFindCalled: 1 },
]);
});
});
it('should work with modifiers', () => {
return Model2.query()
.joinRelated('model2Relation1(idGreaterThan)')
.select('model2Relation1.id', 'model2.*')
.context({
filterArgs: [5],
})
.then((models) => {
expect(models.map((it) => it.id)).to.not.contain(5);
});
});
if (session.isPostgres()) {
it('should work with raw selects in modifiers', () => {
class TestModel2 extends Model2 {
static get modifiers() {
return {
rawSelect: (qb) =>
qb.select('*').select(raw(`model2_prop1 || ' ' || model2_prop1 as "rawSelect"`)),
};
}
}
class TestModel1 extends Model1 {
static get relationMappings() {
return {
model1Relation2: {
relation: Model.HasManyRelation,
modelClass: TestModel2,
join: {
from: 'Model1.id',
to: 'model2.model1_id',
},
},
};
}
}
return TestModel1.query()
.joinRelated('model1Relation2(rawSelect)')
.select('rawSelect')
.findById(1)
.where('model1Relation2.id_col', 2)
.then((model) => {
expect(model.rawSelect).to.equal('hejsan 2 hejsan 2');
});
});
}
});
describe('.$query()', () => {
before(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'hejsan 1',
model2Prop2: 30,
},
{
idCol: 2,
model2Prop1: 'hejsan 2',
model2Prop2: 20,
},
{
idCol: 3,
model2Prop1: 'hejsan 3',
model2Prop2: 10,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should find the model itself', () => {
return Model1.query()
.then((models) => {
expect(_.map(models, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2']);
models[0].model1Prop1 = 'blaa';
return models[0].$query();
})
.then((model) => {
expect(model).to.be.a(Model1);
expect(model.model1Prop1).to.equal('hello 1');
});
});
it('should throw if the id is undefined', (done) => {
Model1.query()
.then((models) => {
expect(_.map(models, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2']);
delete models[0].id;
return models[0].$query();
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`one of the identifier columns [id] is null or undefined. Have you specified the correct identifier column for the model 'Model1' using the 'idColumn' property?`,
);
done();
})
.catch(done);
});
it('should throw if the id is null', (done) => {
Model1.query()
.then((models) => {
expect(_.map(models, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2']);
models[0].id = null;
return models[0].$query();
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`one of the identifier columns [id] is null or undefined. Have you specified the correct identifier column for the model 'Model1' using the 'idColumn' property?`,
);
done();
})
.catch(done);
});
});
describe('.$relatedQuery()', () => {
describe('belongs to one relation', () => {
let parent1;
let parent2;
before(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
},
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 3 });
});
});
it('should return all related rows when no knex methods are chained', () => {
return parent1.$relatedQuery('model1Relation1').then((related) => {
expect(related).to.be.a(Model1);
expect(parent1.model1Relation1).to.eql(undefined);
expect(related).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
});
});
});
it('should return undefined if the result is empty', async () => {
const parent = await Model1.query().findById(2);
const result1 = await parent.$relatedQuery('model1Relation1');
expect(result1).to.be.equal(undefined);
const result2 = await Model1.query().from(
Model1.relatedQuery('model1Relation1').for(parent).as('model1'),
);
expect(result2).to.eql([]);
const result3 = await Model1.query().from(
Model1.relatedQuery('model1Relation1').for(parent.id).as('model1'),
);
expect(result3).to.eql([]);
});
describe('knex methods', () => {
it('.select()', () => {
return parent1
.$relatedQuery('model1Relation1')
.select('id')
.then((related) => {
expect(related).to.be.a(Model1);
expect(_.keys(related).sort()).to.eql(['$afterFindCalled', 'id']);
});
});
it('.first()', () => {
return parent1
.$relatedQuery('model1Relation1')
.first()
.then((value) => {
expect(value).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
});
});
});
it('.join()', () => {
return parent1
.$relatedQuery('model1Relation1')
.select('Model1.*', 'Parent.model1Prop1 as parentProp1')
.join('Model1 as Parent', 'Parent.model1Id', 'Model1.id')
.first()
.then((related) => {
expect(related).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
parentProp1: 'hello 1',
$afterFindCalled: 1,
});
});
});
});
});
describe('has many relation', () => {
let parent1;
let parent2;
before(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
{
idCol: 5,
model2Prop1: 'text 5',
model2Prop2: 2,
},
{
idCol: 6,
model2Prop1: 'text 6',
model2Prop2: 1,
},
],
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 2 });
});
});
it('should return all related rows when no knex methods are chained', () => {
return Promise.all([
parent1
.$relatedQuery('model1Relation2')
.orderBy('id_col')
.then((related) => {
expect(related.length).to.equal(3);
expect(parent1.model1Relation2).to.equal(undefined);
expect(related[0]).to.be.a(Model2);
expect(related[1]).to.be.a(Model2);
expect(related[2]).to.be.a(Model2);
expect(_.map(related, 'model2Prop1').sort()).to.eql(['text 1', 'text 2', 'text 3']);
expect(related[0]).to.eql({
idCol: 1,
model1Id: parent1.id,
model2Prop1: 'text 1',
model2Prop2: 6,
$afterFindCalled: 1,
});
}),
parent2
.$relatedQuery('model1Relation2')
.orderBy('id_col')
.then((related) => {
expect(related.length).to.equal(3);
expect(parent2.model1Relation2).to.equal(undefined);
expect(related[0]).to.be.a(Model2);
expect(related[1]).to.be.a(Model2);
expect(related[2]).to.be.a(Model2);
expect(_.map(related, 'model2Prop1').sort()).to.eql(['text 4', 'text 5', 'text 6']);
expect(related[0]).to.eql({
idCol: 4,
model1Id: parent2.id,
model2Prop1: 'text 4',
model2Prop2: 3,
$afterFindCalled: 1,
});
}),
]);
});
describe('knex methods', () => {
it('.select()', () => {
return parent1
.$relatedQuery('model1Relation2')
.select('id_col')
.then((related) => {
expect(related.length).to.equal(3);
expect(related[0]).to.be.a(Model2);
expect(related[1]).to.be.a(Model2);
expect(related[2]).to.be.a(Model2);
expect(_.map(related, 'idCol').sort()).to.eql([1, 2, 3]);
expect(_.uniq(_.flattenDeep(_.map(related, _.keys))).sort()).to.eql([
'$afterFindCalled',
'idCol',
]);
});
});
it('.where()', () => {
return parent2
.$relatedQuery('model1Relation2')
.where('model2_prop2', '=', '2')
.then((related) => {
expect(_.map(related, 'model2Prop2')).to.eql([2]);
});
});
it('.max()', async () => {
const [{ max }] = await parent2
.$relatedQuery('model1Relation2')
.max('model2_prop2 as max');
expect(max).to.equal(3);
});
it('.orWhere()', () => {
return parent2
.$relatedQuery('model1Relation2')
.where(function () {
this.where('model2_prop2', '=', '1').orWhere('model2_prop2', '=', '3');
})
.orderBy('model2_prop2')
.then((related) => {
expect(_.map(related, 'model2Prop2')).to.eql([1, 3]);
});
});
it('.first()', () => {
return parent2
.$relatedQuery('model1Relation2')
.orderBy('id_col')
.first()
.then(({ idCol: value }) => {
expect(value).to.eql(4);
});
});
it('.join()', () => {
return parent2
.$relatedQuery('model1Relation2')
.select('model2.*', 'Parent.model1Prop1 as parentProp1')
.join('Model1 as Parent', 'model2.model1_id', 'Parent.id')
.orderBy('model2.id_col', 'desc')
.then((related) => {
expect(related).to.have.length(3);
expect(related[0]).to.be.a(Model2);
expect(related[0]).to.eql({
idCol: 6,
model1Id: parent2.id,
model2Prop1: 'text 6',
model2Prop2: 1,
parentProp1: parent2.model1Prop1,
$afterFindCalled: 1,
});
});
});
});
});
describe('many to many relation', () => {
let parent1;
let parent2;
before(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 5,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
aliasedExtra: 'extra 4',
},
{
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 2,
},
{
id: 8,
model1Prop1: 'blaa 6',
model1Prop2: 1,
aliasedExtra: 'extra 6',
},
],
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent1 = _.find(parents, { idCol: 1 });
parent2 = _.find(parents, { idCol: 2 });
});
});
it('should return all related rows when no knex methods are chained', () => {
return Promise.all([
parent1
.$relatedQuery('model2Relation1')
.orderBy('id')
.then((related) => {
expect(related.length).to.equal(3);
expect(parent1.model2Relation1).to.equal(undefined);
expect(related[0]).to.be.a(Model1);
expect(related[1]).to.be.a(Model1);
expect(related[2]).to.be.a(Model1);
expect(_.map(related, 'model1Prop1').sort()).to.eql(['blaa 1', 'blaa 2', 'blaa 3']);
expect(_.map(related, 'aliasedExtra').sort()).to.eql([null, null, null]);
expect(related[0]).to.eql({
id: 3,
model1Id: null,
model1Prop1: 'blaa 1',
model1Prop2: 6,
aliasedExtra: null,
$afterFindCalled: 1,
});
}),
parent2
.$relatedQuery('model2Relation1')
.orderBy('id')
.then((related) => {
expect(related.length).to.equal(3);
expect(parent2.model2Relation1).to.equal(undefined);
expect(related[0]).to.be.a(Model1);
expect(related[1]).to.be.a(Model1);
expect(related[2]).to.be.a(Model1);
expect(_.map(related, 'model1Prop1').sort()).to.eql(['blaa 4', 'blaa 5', 'blaa 6']);
expect(_.map(related, 'aliasedExtra').sort()).to.eql(['extra 4', 'extra 6', null]);
expect(related[0]).to.eql({
id: 6,
model1Id: null,
model1Prop1: 'blaa 4',
model1Prop2: 3,
aliasedExtra: 'extra 4',
$afterFindCalled: 1,
});
}),
]);
});
it('should work in both directions', () => {
return Model1.query()
.where({ id: 6 })
.first()
.then((model) => {
return model.$relatedQuery('model1Relation3');
})
.then((models) => {
expect(models).to.have.length(1);
expect(models[0]).to.be.a(Model2);
expect(models[0].idCol).to.equal(2);
});
});
it('should be able to filter using extra columns', () => {
return parent2
.$relatedQuery('model2Relation1')
.where('extra3', 'extra 6')
.then((related) => {
expect(related).to.eql([
{
id: 8,
model1Id: null,
model1Prop1: 'blaa 6',
model1Prop2: 1,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
]);
});
});
it('should be able to alias the join table using aliasFor', () => {
return parent2
.$relatedQuery('model2Relation1')
.aliasFor('Model1Model2', 'm1m2')
.where('m1m2.extra3', 'extra 6')
.then((related) => {
expect(related).to.eql([
{
id: 8,
model1Id: null,
model1Prop1: 'blaa 6',
model1Prop2: 1,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
]);
});
});
describe('knex methods', () => {
it('.select()', () => {
return parent1
.$relatedQuery('model2Relation1')
.select('Model1.id')
.then((related) => {
expect(related.length).to.equal(3);
expect(related[0]).to.be.a(Model1);
expect(related[1]).to.be.a(Model1);
expect(related[2]).to.be.a(Model1);
expect(_.map(related, 'id').sort()).to.eql([3, 4, 5]);
expect(_.uniq(_.flattenDeep(_.map(related, _.keys))).sort()).to.eql([
'$afterFindCalled',
'id',
]);
});
});
it('.where()', () => {
return parent2
.$relatedQuery('model2Relation1')
.where('model1Prop2', '=', '2')
.then((related) => {
expect(_.map(related, 'model1Prop2')).to.eql([2]);
});
});
it('.max()', async () => {
const [{ min }] = await parent2
.$relatedQuery('model2Relation1')
.min('model1Prop1 as min');
expect(min).to.equal('blaa 4');
});
it('.orWhere()', () => {
return parent2
.$relatedQuery('model2Relation1')
.where(function () {
this.where('model1Prop2', '1').orWhere('model1Prop2', '3');
})
.orderBy('model1Prop2')
.then((related) => {
expect(_.map(related, 'model1Prop2')).to.eql([1, 3]);
});
});
it('.first()', () => {
return parent1
.$relatedQuery('model2Relation1')
.orderBy('Model1.id')
.first()
.then(({ id: value }) => {
expect(value).to.eql(3);
});
});
});
});
describe('has one through relation', () => {
let parent;
before(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation2: {
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation2: {
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
},
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent = _.find(parents, { idCol: 1 });
});
});
it('should fetch a related model', () => {
return parent.$relatedQuery('model2Relation2').then((related) => {
expect(related).to.eql({
id: 3,
model1Id: null,
model1Prop1: 'blaa 1',
model1Prop2: 6,
$afterFindCalled: 1,
});
});
});
});
});
});
};
================================================
FILE: tests/integration/graph/GraphInsert.js
================================================
const mockKnexFactory = require('../../../testUtils/mockKnex');
const { expect } = require('chai');
const { Model, raw } = require('../../../');
const { ModelGraph } = require('../../../lib/model/graph/ModelGraph');
const { GraphInsert } = require('../../../lib/queryBuilder/graph/insert/GraphInsert');
const { GraphOptions } = require('../../../lib/queryBuilder/graph/GraphOptions');
const { GraphNodeDbExistence } = require('../../../lib/queryBuilder/graph/GraphNodeDbExistence');
const { GraphFetcher } = require('../../../lib/queryBuilder/graph/GraphFetcher');
const { asArray } = require('../../../lib/utils/objectUtils');
module.exports = (session) => {
const ID_NOT_IN_DB = 1000000;
describe('GraphInsert tests', () => {
let Pet = null;
let Person = null;
let mockKnex = null;
let numExecutedQueries = 0;
before(createSchema);
beforeEach(createModels);
beforeEach(() => {
return session
.knex('relatives')
.delete()
.then(() => session.knex('pets').delete())
.then(() => session.knex('persons').delete());
});
after(dropSchema);
it('should insert one object', () => {
return test({
modelClass: Person,
graphIn: {
name: 'Brad',
},
queryOut: (query) => query,
graphOut: [
{
name: 'Brad',
},
],
postgresNumQueries: 1,
});
});
it('should insert nested relations', () => {
return test({
modelClass: Person,
graphIn: [
{
'#id': 'matti',
name: 'Matti',
pets: [
{
name: 'Mooses',
favoritePerson: {
'#ref': 'matti',
},
},
{
name: 'Sara',
favoritePerson: {
name: 'Liisa',
relatives: [
{
'#ref': 'sami',
},
{
'#ref': 'marika',
},
],
},
},
],
relatives: [
{
'#id': 'sami',
name: 'Sami',
isChild: true,
},
{
'#id': 'marika',
name: 'Marika',
isChild: true,
},
{
name: 'Samuel',
relatives: [
{
'#ref': 'anja',
isChild: true,
},
],
},
],
},
{
'#id': 'anja',
name: 'Anja',
relatives: [
{
name: 'Marjukka',
isChild: true,
},
],
},
],
queryOut: (query) =>
query
.withGraphFetched(
'[pets.favoritePerson.relatives(orderByName), relatives(orderByName).relatives(orderByName)]',
)
.whereIn('name', ['Matti', 'Anja'])
.orderBy('name'),
graphOut: [
{
name: 'Matti',
pets: [
{
name: 'Mooses',
favoritePerson: {
name: 'Matti',
},
},
{
name: 'Sara',
favoritePerson: {
name: 'Liisa',
relatives: [
{
name: 'Marika',
},
{
name: 'Sami',
},
],
},
},
],
relatives: [
{
name: 'Marika',
isChild: true,
},
{
name: 'Sami',
isChild: true,
},
{
name: 'Samuel',
isChild: false,
relatives: [
{
name: 'Anja',
isChild: true,
},
],
},
],
},
{
name: 'Anja',
relatives: [
{
name: 'Marjukka',
isChild: true,
},
],
},
],
postgresNumQueries: 3,
});
});
describe('references inside (nested) properties', () => {
it('should resolve references inside properties', () => {
return test({
modelClass: Person,
graphIn: {
'#id': 'liisa',
name: 'Liisa',
data: {
someNumber: 42,
},
pets: [
{
name: 'Sara #ref{liisa.data.someNumber}',
},
],
relatives: [
{
name: 'Sami',
data: {
motherName: '#ref{liisa.name}',
someNumber: '#ref{liisa.data.someNumber}',
},
},
],
},
queryOut: (query) => query.where('name', 'Liisa').withGraphFetched('[relatives, pets]'),
graphOut: [
{
name: 'Liisa',
pets: [
{
name: 'Sara 42',
},
],
relatives: [
{
name: 'Sami',
data: {
motherName: 'Liisa',
someNumber: 42,
},
},
],
},
],
postgresNumQueries: 4,
});
});
});
describe('with existing graph', () => {
let matti;
let sami;
beforeEach(() => {
return Person.query()
.insert({ name: 'Matti' })
.then((model) => {
matti = model;
return Promise.all([
matti
.$relatedQuery('relatives')
.insert({ name: 'Sami', isChild: true })
.then((model) => {
sami = model;
}),
]);
});
});
it("should only insert rows that don't exists in db", () => {
return test({
modelClass: Person,
graphIn: {
id: matti.id,
relatives: [
{
id: sami.id,
},
{
name: 'Marika',
},
],
pets: [
{
name: 'Mooses',
favoritePerson: {
'#dbRef': matti.id,
},
},
],
},
queryOut: (query) =>
query.withGraphFetched('[relatives, pets.favoritePerson]').findById(matti.id),
graphOut: {
id: matti.id,
relatives: [
{
id: sami.id,
isChild: true,
isParent: false,
},
{
name: 'Marika',
},
],
pets: [
{
name: 'Mooses',
favoritePerson: {
id: matti.id,
name: 'Matti',
},
},
],
},
postgresNumQueries: 3,
});
});
});
describe('belongs to one relation', () => {
it('should insert one object in a belongs to one relation', () => {
return test({
modelClass: Pet,
graphIn: {
name: 'Doggo',
favoritePerson: {
name: 'Brad',
},
},
queryOut: (query) => query.withGraphFetched('favoritePerson'),
graphOut: [
{
name: 'Doggo',
favoritePerson: {
name: 'Brad',
},
},
],
postgresNumQueries: 2,
});
});
it('should insert one object in a belongs to one relation with existing identifiers', () => {
return test({
modelClass: Pet,
graphIn: {
id: 1,
name: 'Doggo',
favoritePerson: {
id: 1,
name: 'Brad',
},
},
queryOut: (query) => query.withGraphFetched('favoritePerson'),
graphOut: [
{
id: 1,
name: 'Doggo',
favoritePerson: {
id: 1,
name: 'Brad',
},
},
],
postgresNumQueries: 2,
});
});
it('should insert belongs to one relation using #ref', () => {
return test({
modelClass: Person,
graphIn: {
'#id': 'brad',
name: 'Brad',
pets: [
{
name: 'Doggo',
favoritePerson: {
'#ref': 'brad',
},
},
],
},
queryOut: (query) => query.withGraphFetched('pets.favoritePerson'),
graphOut: [
{
name: 'Brad',
pets: [
{
name: 'Doggo',
favoritePerson: {
name: 'Brad',
},
},
],
},
],
postgresNumQueries: 2,
check(graph) {
expect(graph[0].id).to.equal(graph[0].pets[0].favoritePerson.id);
},
});
});
describe('should relate belongs to one relation using `relate` option', () => {
beforeEach(() => {
return Person.query().insert({
id: ID_NOT_IN_DB,
name: 'Brad',
});
});
it('should relate belongs to one relation using `relate: ["relation.path"]` option', () => {
return test({
modelClass: Pet,
graphIn: {
name: 'Doggo',
favoritePerson: {
id: ID_NOT_IN_DB,
},
},
graphOptions: {
relate: ['favoritePerson'],
},
queryOut: (query) => query.withGraphFetched('favoritePerson'),
graphOut: [
{
name: 'Doggo',
favoritePerson: {
name: 'Brad',
},
},
],
postgresNumQueries: 1,
});
});
it('should relate belongs to one relation using `relate: true` option', () => {
return test({
modelClass: Pet,
graphIn: {
name: 'Doggo',
favoritePerson: {
id: ID_NOT_IN_DB,
},
},
graphOptions: {
relate: true,
},
queryOut: (query) => query.withGraphFetched('favoritePerson'),
graphOut: [
{
name: 'Doggo',
favoritePerson: {
name: 'Brad',
},
},
],
postgresNumQueries: 1,
});
});
});
describe('should relate belongs to one relation using #dbRef', () => {
beforeEach(() => {
return Person.query().insert({
id: ID_NOT_IN_DB,
name: 'Brad',
});
});
it('should insert belongs to one relation with #dbRef', () => {
return test({
modelClass: Pet,
graphIn: {
name: 'Doggo',
favoritePerson: {
'#dbRef': ID_NOT_IN_DB,
},
},
queryOut: (query) => query.withGraphFetched('favoritePerson'),
graphOut: [
{
name: 'Doggo',
favoritePerson: {
name: 'Brad',
},
},
],
postgresNumQueries: 1,
});
});
});
});
describe('many to many relation', () => {
it('should insert objects in a many to many relation', () => {
return test({
modelClass: Person,
graphIn: {
name: 'Brad',
relatives: [
{
name: 'Nick',
},
{
name: 'Sandra',
},
],
},
queryOut: (query) => query.withGraphFetched('relatives').where('name', 'Brad'),
graphOut: [
{
name: 'Brad',
relatives: [
{
name: 'Nick',
isFriend: false,
},
{
name: 'Sandra',
isChild: false,
},
],
},
],
postgresNumQueries: 2,
});
});
it('should insert objects in a many to many relation with existing ids', () => {
return test({
modelClass: Person,
graphIn: {
id: 1,
name: 'Brad',
relatives: [
{
id: 2,
name: 'Nick',
},
{
id: 3,
name: 'Sandra',
},
],
},
queryOut: (query) => query.withGraphFetched('relatives').where('name', 'Brad'),
graphOut: [
{
id: 1,
name: 'Brad',
relatives: [
{
id: 2,
name: 'Nick',
isFriend: false,
},
{
id: 3,
name: 'Sandra',
isChild: false,
},
],
},
],
postgresNumQueries: 2,
});
});
it('should insert objects in a many to many relation with extra properties', () => {
return test({
modelClass: Person,
graphIn: {
name: 'Brad',
relatives: [
{
name: 'Nick',
isFriend: raw('1 = 1'),
},
{
name: 'Sandra',
isChild: true,
},
],
},
queryOut: (query) => query.withGraphFetched('relatives').where('name', 'Brad'),
graphOut: [
{
name: 'Brad',
relatives: [
{
name: 'Nick',
isFriend: true,
isChild: false,
},
{
name: 'Sandra',
isChild: true,
},
],
},
],
postgresNumQueries: 2,
});
});
it('should execute beforeInsert hooks for many to many relations', () => {
return test({
modelClass: Person,
graphIn: {
name: 'Brad',
data: { foo: 'bar' },
relatives: [
{
name: 'Nick',
},
{
name: 'Sandra',
isChild: true,
},
],
},
queryOut: (query) => query.withGraphFetched('relatives').where('name', 'Brad'),
graphOut: [
{
name: 'Brad',
data: { foo: 'bar' },
relatives: [
{
name: 'Nick',
isFriend: false,
isChild: false,
isParent: true,
data: {},
},
{
name: 'Sandra',
isChild: true,
// These are set by the beforeInsert hooks.
isParent: false,
data: {},
},
],
},
],
postgresNumQueries: 2,
});
});
describe('should insert an object using #ref', () => {
it('should insert an object using #ref', () => {
return test({
modelClass: Person,
graphIn: {
'#id': 'brad',
name: 'Brad',
relatives: [
{
name: 'Nick',
isFriend: raw('1 = 1'),
},
{
'#ref': 'brad',
isChild: true,
},
],
},
queryOut: (query) =>
query.withGraphFetched('relatives(orderByName)').where('name', 'Brad'),
graphOut: [
{
name: 'Brad',
relatives: [
{
name: 'Nick',
isFriend: true,
},
{
name: 'Brad',
isChild: true,
},
],
},
],
postgresNumQueries: 2,
check(graphOut) {
expect(graphOut[0].id).to.equal(graphOut[0].relatives[0].id);
},
});
});
});
describe('should relate an object using #dbRef', () => {
beforeEach(() => {
return Person.query().insert({
id: ID_NOT_IN_DB,
name: 'Vlad',
});
});
it('should relate an object using #dbRef', () => {
return test({
modelClass: Person,
graphIn: {
name: 'Brad',
relatives: [
{
'#dbRef': ID_NOT_IN_DB,
isFriend: raw('1 = 1'),
},
{
name: 'Sandra',
isChild: true,
},
],
},
queryOut: (query) =>
query.withGraphFetched('relatives(orderById)').where('name', 'Brad'),
graphOut: [
{
name: 'Brad',
relatives: [
{
id: ID_NOT_IN_DB,
name: 'Vlad',
isFriend: true,
},
{
name: 'Sandra',
isChild: true,
},
],
},
],
postgresNumQueries: 2,
});
});
it('should relate an object using `relate: ["relation.path"]` option', () => {
return test({
modelClass: Person,
graphIn: {
name: 'Brad',
relatives: [
{
id: ID_NOT_IN_DB,
isFriend: raw('1 = 1'),
},
{
name: 'Sandra',
isChild: true,
},
],
},
graphOptions: {
relate: ['relatives'],
},
queryOut: (query) =>
query.withGraphFetched('relatives(orderById)').where('name', 'Brad'),
graphOut: [
{
name: 'Brad',
relatives: [
{
id: ID_NOT_IN_DB,
name: 'Vlad',
isFriend: true,
},
{
name: 'Sandra',
isChild: true,
},
],
},
],
postgresNumQueries: 2,
});
});
it('should relate an object using `relate: true` option', () => {
return test({
modelClass: Person,
graphIn: {
name: 'Brad',
relatives: [
{
id: ID_NOT_IN_DB,
isFriend: raw('1 = 1'),
},
{
name: 'Sandra',
isChild: true,
},
],
},
graphOptions: {
relate: true,
},
queryOut: (query) =>
query.withGraphFetched('relatives(orderById)').where('name', 'Brad'),
graphOut: [
{
name: 'Brad',
relatives: [
{
id: ID_NOT_IN_DB,
name: 'Vlad',
isFriend: true,
},
{
name: 'Sandra',
isChild: true,
},
],
},
],
postgresNumQueries: 2,
});
});
});
});
describe('has many relation', () => {
it('should insert objects in a has many relation', () => {
return test({
modelClass: Person,
graphIn: {
name: 'Matti',
pets: [
{
name: 'Sara',
},
{
name: 'Miina',
},
],
},
queryOut: (query) => query.withGraphFetched('pets').where('name', 'Matti'),
graphOut: [
{
name: 'Matti',
pets: [
{
name: 'Sara',
},
{
name: 'Miina',
},
],
},
],
postgresNumQueries: 2,
});
});
it('should insert objects in a has many relation with existing ids', () => {
return test({
modelClass: Person,
graphIn: {
id: 1,
name: 'Matti',
pets: [
{
id: 1,
name: 'Sara',
},
{
id: 2,
name: 'Miina',
},
],
},
queryOut: (query) => query.withGraphFetched('pets').where('name', 'Matti'),
graphOut: [
{
id: 1,
name: 'Matti',
pets: [
{
id: 1,
name: 'Sara',
},
{
id: 2,
name: 'Miina',
},
],
},
],
postgresNumQueries: 2,
});
});
});
function test({
modelClass,
graphIn,
graphOptions: rawGraphOptions = {},
queryOut,
graphOut,
postgresNumQueries = null,
check,
}) {
const builder = modelClass.query();
const models = modelClass.ensureModelArray(graphIn, { skipValidation: true });
rawGraphOptions = Object.assign({}, rawGraphOptions, { insertMissing: true });
const graphOptions = new GraphOptions(rawGraphOptions);
const graph = assignDbRefsAsRelateProps(ModelGraph.create(modelClass, models));
const nodeDbExistence = GraphNodeDbExistence.createEveryNodeExistsExistence();
return GraphFetcher.fetchCurrentGraph({ builder, graph, graphOptions })
.then((currentGraph) => {
numExecutedQueries = 0;
const graphInsert = new GraphInsert({
graph,
currentGraph,
graphOptions,
nodeDbExistence,
});
const actions = graphInsert.createActions();
let promise = Promise.resolve();
actions.forEach((action) => {
promise = promise.then(() => action.run(builder));
});
return promise;
})
.then(() => {
if (session.isPostgres() && postgresNumQueries) {
expect(numExecutedQueries).to.equal(postgresNumQueries);
}
})
.then(() => queryOut(modelClass.query()))
.then((result) => {
expect(result).to.containSubset(graphOut);
if (check) {
check(result);
}
});
}
function assignDbRefsAsRelateProps(graph) {
for (const node of graph.nodes) {
if (!node.parentEdge || !node.parentEdge.relation || !node.isDbReference) {
continue;
}
node.parentEdge.relation.setRelateProp(node.obj, asArray(node.dbReference));
}
return graph;
}
function createSchema() {
return session.knex.schema
.dropTableIfExists('relatives')
.dropTableIfExists('pets')
.dropTableIfExists('persons')
.createTable('persons', (table) => {
table.increments('id');
table.string('name');
table.text('data');
})
.createTable('pets', (table) => {
table.increments('id');
table.string('name');
table.integer('ownerId').unsigned().references('persons.id');
table.integer('favoritePersonId').unsigned().references('persons.id');
})
.createTable('relatives', (table) => {
table.increments('id');
table.boolean('isFriend').defaultTo(false);
table.boolean('isParent').defaultTo(true);
table.boolean('isChild').defaultTo(false);
table.integer('personId1').unsigned().references('persons.id');
table.integer('personId2').unsigned().references('persons.id');
// Disallow duplicates.
table.unique(['personId1', 'personId2']);
});
}
function dropSchema() {
return session.knex.schema.dropTable('relatives').dropTable('pets').dropTable('persons');
}
function createModels() {
mockKnex = mockKnexFactory(session.knex, function (_, oldImpl, args) {
++numExecutedQueries;
return oldImpl.apply(this, args);
});
Person = class PersonModel extends Model {
static get tableName() {
return 'persons';
}
static get modifiers() {
return {
orderById: (builder) => builder.orderBy('id'),
orderByName: (builder) => builder.orderBy('name'),
};
}
static get jsonSchema() {
return {
type: 'object',
additionalProperties: false,
properties: {
id: { type: 'integer' },
name: { type: 'string' },
data: { type: 'object' },
isFriend: { type: 'boolean' },
isChild: { type: 'boolean' },
isParent: { type: 'boolean' },
},
};
}
$parseDatabaseJson(json) {
const jsonSchemaProps = this.constructor.jsonSchema.properties;
for (const prop of Object.keys(jsonSchemaProps)) {
const propSchema = jsonSchemaProps[prop];
if (propSchema.type === 'boolean' && prop in json) {
json[prop] = !!json[prop];
}
}
return super.$parseDatabaseJson(...arguments);
}
static get relationMappings() {
return {
relatives: {
modelClass: Person,
relation: Model.ManyToManyRelation,
beforeInsert(obj) {
obj.data = obj.data || {};
},
join: {
from: 'persons.id',
through: {
extra: ['isFriend', 'isChild', 'isParent'],
from: 'relatives.personId1',
to: 'relatives.personId2',
beforeInsert(obj) {
obj.isParent = !obj.isChild;
},
},
to: 'persons.id',
},
},
pets: {
modelClass: Pet,
relation: Model.HasManyRelation,
join: {
from: 'persons.id',
to: 'pets.ownerId',
},
},
};
}
};
Pet = class PetModel extends Model {
static get tableName() {
return 'pets';
}
static get jsonSchema() {
return {
type: 'object',
additionalProperties: false,
properties: {
id: { type: 'integer' },
name: { type: 'string' },
ownerId: { type: ['integer', 'null'] },
favoritePersonId: { type: ['integer', 'null'] },
},
};
}
static get relationMappings() {
return {
favoritePerson: {
modelClass: Person,
relation: Model.BelongsToOneRelation,
join: {
from: 'pets.favoritePersonId',
to: 'persons.id',
},
},
};
}
};
Person.knex(mockKnex);
Pet.knex(mockKnex);
}
});
};
================================================
FILE: tests/integration/index.js
================================================
const os = require('os');
const path = require('path');
const TestSession = require('./../../testUtils/TestSession');
const Bluebird = require('bluebird');
// Helps debugging.
Bluebird.longStackTraces();
// DATABASES environment variable can contain a comma separated list
// of databases to test.
const DATABASES = (process.env.DATABASES && process.env.DATABASES.split(',')) || [];
describe('integration tests', () => {
const testDatabaseConfigs = [
{
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: path.join(os.tmpdir(), 'objection_test.db'),
},
pool: {
afterCreate: (conn, cb) => {
conn.run('PRAGMA foreign_keys = ON', cb);
},
},
},
{
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test',
},
pool: {
min: 2,
max: 10,
afterCreate: (conn, cb) => {
conn.query(`SET SESSION sql_mode='NO_AUTO_VALUE_ON_ZERO'`, (err) => {
cb(err, conn);
});
},
},
},
{
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'objection',
database: 'objection_test',
},
},
].filter((it) => {
return DATABASES.length === 0 || DATABASES.includes(it.client);
});
const sessions = testDatabaseConfigs.map((knexConfig) => {
const session = new TestSession({
knexConfig,
});
describe(knexConfig.client, () => {
before(() => {
return session.createDb();
});
require('./misc')(session);
require('./find')(session);
require('./insert')(session);
require('./insertGraph')(session);
require('./upsertGraph')(session);
require('./update')(session);
require('./patch')(session);
require('./delete')(session);
require('./relate')(session);
require('./unrelate')(session);
require('./withGraph')(session);
require('./transactions')(session);
require('./queryContext')(session);
require('./compositeKeys')(session);
require('./crossDb')(session);
require('./viewsAndAliases')(session);
require('./schema')(session);
require('./knexSnakeCase')(session);
require('./snakeCase')(session);
require('./knexIdentifierMapping')(session);
require('./graph/GraphInsert')(session);
require('./relationModify')(session);
require('./nonPrimaryKeyRelations')(session);
require('./staticHooks')(session);
require('./modifiers')(session);
require('./toKnexQuery')(session);
if (session.isPostgres()) {
require('./jsonQueries')(session);
require('./jsonRelations')(session);
}
});
return session;
});
after(() => {
return Promise.all(
sessions.map((session) => {
return session.destroy();
}),
);
});
});
================================================
FILE: tests/integration/insert.js
================================================
const _ = require('lodash');
const chai = require('chai');
const expect = require('expect.js');
const Promise = require('bluebird');
const { inheritModel } = require('../../lib/model/inheritModel');
const { ValidationError, UniqueViolationError } = require('../../');
module.exports = (session) => {
let Model1 = session.models.Model1;
let Model2 = session.models.Model2;
describe('Model insert queries', () => {
describe('.query().insert()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'test 1',
},
{
idCol: 2,
model2Prop1: 'test 2',
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should insert new model', () => {
let model = Model1.fromJson({ model1Prop1: 'hello 3' });
return Model1.query()
.insert(model)
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.eql(3);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
it('should throw a UniqueViolationError when existing id is given', async () => {
try {
await Model1.query().insert({ id: 1 });
throw new Error('should not get here');
} catch (err) {
expect(err).to.be.a(UniqueViolationError);
}
});
it('should insert new model (additionalProperties = false)', () => {
let Mod = inheritModel(Model1);
Mod.jsonSchema = {
type: 'object',
additionalProperties: false,
properties: {
model1Prop1: { type: 'string' },
model2Prop2: { type: 'number' },
},
};
return Mod.query()
.insert({ model1Prop1: 'hello 3' })
.then((inserted) => {
expect(inserted).to.be.a(Mod);
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.eql(3);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
it('should work with relations', () => {
let model = {
model1Prop1: 'hello 3',
model1Relation1: { model1Prop1: 'hello 4' },
model1Relation2: [{ model2Prop1: 'moro 1' }],
};
return Model1.query()
.insert(model)
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.eql(3);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
it('should work with relations and additionalProperties = false', () => {
let Mod = inheritModel(Model1);
let model = {
model1Prop1: 'hello 3',
model1Relation1: { model1Prop1: 'hello 4' },
model1Relation2: [{ model2Prop1: 'moro 1' }],
};
Mod.jsonSchema = {
type: 'object',
additionalProperties: false,
properties: {
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
};
return Mod.query()
.insert(model)
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.eql(3);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex(Mod.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
it('should ignore non-objects in relation properties', () => {
let model = {
model1Prop1: 'hello 3',
model1Relation1: 1,
model1Relation2: [1, 2, null, 4, undefined],
};
return Model1.query()
.insert(model)
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.eql(3);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
it('should insert new model with identifier', () => {
let model = Model1.fromJson({ id: 1000, model1Prop1: 'hello 3' });
return Model1.query()
.insert(model)
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.id).to.equal(1000);
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.filter(rows, { id: 1000, model1Prop1: 'hello 3' })).to.have.length(1);
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
if (session.isPostgres()) {
it('should accept an array', () => {
let models = [
Model1.fromJson({ model1Prop1: 'hello 3' }),
Model1.fromJson({ model1Prop1: 'hello 4' }),
];
return Model1.query()
.insert(models)
.then((inserted) => {
expect(inserted[0]).to.be.a(Model1);
expect(inserted[1]).to.be.a(Model1);
expect(inserted[0].$beforeInsertCalled).to.equal(1);
expect(inserted[0].$afterInsertCalled).to.equal(1);
expect(inserted[1].$beforeInsertCalled).to.equal(1);
expect(inserted[1].$afterInsertCalled).to.equal(1);
expect(_.map(inserted, 'id').sort()).to.eql([3, 4]);
expect(_.map(inserted, 'model1Prop1').sort()).to.eql(['hello 3', 'hello 4']);
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql([
'hello 1',
'hello 2',
'hello 3',
'hello 4',
]);
expect(_.map(rows, 'id').sort()).to.eql([1, 2, 3, 4]);
});
});
}
it('should accept json', () => {
return Model1.query()
.insert({ model1Prop1: 'hello 3' })
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.id).to.eql(3);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
it('should accept subqueries and raw expressions', () => {
return Model1.query()
.insert({
model1Prop1: Model2.query().max('model2_prop1'),
model1Prop2: Model1.raw('5 + 8'),
})
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.id).to.eql(3);
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'test 2']);
expect(_.find(rows, { id: 3, model1Prop1: 'test 2' }).model1Prop2).to.equal(13);
});
});
if (session.isPostgres()) {
it('should accept a json array', () => {
return Model1.query()
.insert([{ model1Prop1: 'hello 3' }, { model1Prop1: 'hello 4' }])
.then((inserted) => {
expect(inserted[0]).to.be.a(Model1);
expect(inserted[1]).to.be.a(Model1);
expect(_.map(inserted, 'id').sort()).to.eql([3, 4]);
expect(_.map(inserted, 'model1Prop1').sort()).to.eql(['hello 3', 'hello 4']);
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql([
'hello 1',
'hello 2',
'hello 3',
'hello 4',
]);
expect(_.map(rows, 'id').sort()).to.eql([1, 2, 3, 4]);
});
});
it('returning("*") should return all columns', () => {
return Model1.query()
.insert({ model1Prop1: 'hello 3' })
.returning('*')
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.$toJson()).to.eql({
id: 3,
model1Id: null,
model1Prop1: 'hello 3',
model1Prop2: null,
});
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
expect(_.map(rows, 'id').sort()).to.eql([1, 2, 3]);
});
});
it('returning("someColumn") should only return that `someColumn`', () => {
return Model1.query()
.insert({ model1Prop1: Model1.raw("'hello' || ' 3'") })
.returning('model1Prop1')
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.$toJson()).to.eql({ model1Prop1: 'hello 3' });
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
expect(_.map(rows, 'id').sort()).to.eql([1, 2, 3]);
});
});
}
it('should validate', (done) => {
let ModelWithSchema = subClassWithSchema(Model1, {
type: 'object',
properties: {
id: { type: ['number', 'null'] },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
});
ModelWithSchema.query()
.insert({ model1Prop1: 666 })
.then((x) => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal('model1Prop1: must be string');
expect(err).to.be.a(ValidationError);
expect(err).to.be.a(ModelWithSchema.ValidationError);
return session.knex(Model1.getTableName()).then((rows) => {
expect(_.map(rows, 'id').sort()).to.eql([1, 2]);
done();
});
})
.catch(done);
});
it('should use `Model.createValidationError` to create the error', (done) => {
class MyError extends Error {
constructor({ data }) {
super('MyError');
this.errors = data;
}
}
let ModelWithSchema = subClassWithSchema(Model1, {
type: 'object',
properties: {
id: { type: ['number', 'null'] },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
});
ModelWithSchema.createValidationError = (props) => {
return new MyError(props);
};
ModelWithSchema.query()
.insert({ model1Prop1: 666 })
.then((x) => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(MyError);
expect(err.errors).to.eql({
model1Prop1: [
{
message: 'must be string',
keyword: 'type',
params: {
type: 'string',
},
},
],
});
return session.knex(Model1.getTableName()).then((rows) => {
expect(_.map(rows, 'id').sort()).to.eql([1, 2]);
done();
});
})
.catch(done);
});
it('should allow properties with same names as relations', () => {
const Mod = inheritModel(Model1);
Mod.prototype.$parseJson = function (json, opt) {
if (typeof json.model1Relation1 === 'number') {
json.model1Prop1 = json.model1Relation1;
delete json.model1Relation1;
}
return Model1.prototype.$parseJson.call(this, json, opt);
};
return Mod.query()
.insert({ model1Prop1: 123, model1Relation1: 666 })
.then((inserted) => {
expect(inserted.model1Prop1).to.equal(666);
});
});
});
describe('.query().insertAndFetch()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'test 1',
},
{
idCol: 2,
model2Prop1: 'test 2',
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should insert and fetch new model', () => {
let model = Model1.fromJson({ model1Prop1: 'hello 3' });
return Model1.query()
.insertAndFetch(model)
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted).to.equal(model);
expect(inserted).to.eql({
id: 3,
model1Prop1: 'hello 3',
model1Prop2: null,
model1Id: null,
$beforeInsertCalled: 1,
$afterInsertCalled: 1,
});
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
if (session.isPostgres()) {
it('should insert and fetch an array of new models', () => {
let model1 = Model1.fromJson({ model1Prop1: 'hello 3' });
let model2 = Model1.fromJson({ model1Prop1: 'hello 4', model1Prop2: 10 });
return Model1.query()
.insertAndFetch([model1, model2])
.then((inserted) => {
expect(inserted).to.have.length(2);
expect(inserted[0]).to.be.a(Model1);
expect(inserted[0]).to.equal(model1);
expect(inserted[0]).to.eql({
id: 3,
model1Prop1: 'hello 3',
model1Prop2: null,
model1Id: null,
$beforeInsertCalled: 1,
$afterInsertCalled: 1,
});
expect(inserted[1]).to.be.a(Model1);
expect(inserted[1]).to.equal(model2);
expect(inserted[1]).to.eql({
id: 4,
model1Prop1: 'hello 4',
model1Prop2: 10,
model1Id: null,
$beforeInsertCalled: 1,
$afterInsertCalled: 1,
});
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql([
'hello 1',
'hello 2',
'hello 3',
'hello 4',
]);
});
});
}
});
describe('.$query().insert()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should insert new model', () => {
return Model1.fromJson({ model1Prop1: 'hello 3' })
.$query()
.insert()
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.eql(3);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
it('model edits in $beforeInsert should get into database query', () => {
let model = Model1.fromJson({});
model.$beforeInsert = function () {
let self = this;
return Promise.delay(1).then(() => {
self.model1Prop1 = 'hello 3';
});
};
return model
.$query()
.insert()
.then((inserted) => {
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('hello 3');
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
});
describe('.$relatedQuery().insert()', () => {
describe('belongs to one relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 3',
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 2 });
});
});
it('should insert a related object', () => {
let inserted = null;
// First check that there is nothing in the relation.
return parent1
.$relatedQuery('model1Relation1')
.then((model) => {
expect(parent1.model1Id).to.equal(null);
expect(model).to.eql(undefined);
return parent1
.$relatedQuery('model1Relation1')
.insert(Model1.fromJson({ model1Prop1: 'test' }));
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(3);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
expect(parent1.model1Relation1).to.equal(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.find(rows, { id: parent1.id }).model1Id).to.equal(3);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('test');
});
});
it('should accept json', () => {
let inserted = null;
return parent1
.$relatedQuery('model1Relation1')
.insert({ model1Prop1: 'inserted' })
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(3);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('inserted');
expect(parent1.model1Relation1).to.equal(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.find(rows, { id: parent1.id }).model1Id).to.equal(3);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('inserted');
});
});
it("insert replaces old related object, but doesn't remove it", () => {
let inserted = null;
return parent1
.$relatedQuery('model1Relation1')
.insert({ model1Prop1: 'inserted' })
.then(() => {
return parent1.$relatedQuery('model1Relation1').insert({ model1Prop1: 'inserted 2' });
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted).to.be.a(Model1);
expect(inserted.id).to.equal(4);
expect(inserted.model1Prop1).to.equal('inserted 2');
expect(parent1.model1Relation1).to.equal(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(_.find(rows, { id: parent1.id }).model1Id).to.equal(4);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('inserted 2');
});
});
});
describe('has one relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 3',
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 2 });
});
});
it('should insert a related object', () => {
let inserted = null;
// First check that there is nothing in the relation.
return parent1
.$relatedQuery('model1Relation1Inverse')
.then((model) => {
expect(model).to.eql(undefined);
return parent1
.$relatedQuery('model1Relation1Inverse')
.insert(Model1.fromJson({ model1Prop1: 'test' }));
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(3);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
expect(parent1.model1Relation1Inverse).to.equal(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.find(rows, { id: inserted.id }).model1Id).to.equal(parent1.id);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('test');
});
});
it('should accept json', () => {
let inserted = null;
// First check that there is nothing in the relation.
return parent1
.$relatedQuery('model1Relation1Inverse')
.then((model) => {
expect(model).to.eql(undefined);
return parent1
.$relatedQuery('model1Relation1Inverse')
.insert({ model1Prop1: 'test' });
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(3);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
expect(parent1.model1Relation1Inverse).to.equal(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.find(rows, { id: inserted.id }).model1Id).to.equal(parent1.id);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('test');
});
});
});
describe('has many relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 4',
model2Prop2: 3,
},
],
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 2 });
});
});
it('should insert a related object', () => {
let inserted = null;
return parent1
.$relatedQuery('model1Relation2')
.then((models) => {
expect(models).to.have.length(1);
return parent1
.$relatedQuery('model1Relation2')
.insert(Model2.fromJson({ model2Prop1: 'test' }));
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.idCol).to.equal(3);
expect(inserted).to.be.a(Model2);
expect(inserted.model2Prop1).to.equal('test');
expect(inserted.model1Id).to.equal(parent1.id);
expect(parent1.model1Relation2).to.eql(undefined);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.find(rows, { id_col: inserted.idCol }).model1_id).to.equal(parent1.id);
expect(_.find(rows, { id_col: inserted.idCol }).model2_prop1).to.equal('test');
});
});
it('should accept json', () => {
let inserted = null;
return parent1
.$relatedQuery('model1Relation2')
.then((models) => {
expect(models).to.have.length(1);
return parent1.$relatedQuery('model1Relation2').insert({ model2Prop1: 'test' });
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.idCol).to.equal(3);
expect(inserted).to.be.a(Model2);
expect(inserted.model2Prop1).to.equal('test');
expect(inserted.model1Id).to.equal(parent1.id);
expect(parent1.model1Relation2).to.eql(undefined);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.find(rows, { id_col: inserted.idCol }).model1_id).to.equal(parent1.id);
expect(_.find(rows, { id_col: inserted.idCol }).model2_prop1).to.equal('test');
});
});
if (session.isPostgres()) {
it('should accept an array', () => {
let inserted = null;
return parent1
.$relatedQuery('model1Relation2')
.then((models) => {
expect(models).to.have.length(1);
return parent1
.$relatedQuery('model1Relation2')
.insert([
Model2.fromJson({ model2Prop1: 'test 1' }),
Model2.fromJson({ model2Prop1: 'test 2' }),
]);
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted[0].idCol).to.equal(3);
expect(inserted[1].idCol).to.equal(4);
expect(inserted[0]).to.be.a(Model2);
expect(inserted[1]).to.be.a(Model2);
expect(inserted[0].model2Prop1).to.equal('test 1');
expect(inserted[1].model2Prop1).to.equal('test 2');
expect(inserted[0].model1Id).to.equal(parent1.id);
expect(inserted[1].model1Id).to.equal(parent1.id);
expect(parent1.model1Relation2).to.eql(undefined);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(_.find(rows, { id_col: inserted[0].idCol }).model1_id).to.equal(parent1.id);
expect(_.find(rows, { id_col: inserted[0].idCol }).model2_prop1).to.equal('test 1');
expect(_.find(rows, { id_col: inserted[1].idCol }).model1_id).to.equal(parent1.id);
expect(_.find(rows, { id_col: inserted[1].idCol }).model2_prop1).to.equal('test 2');
});
});
it('should accept a json array', () => {
let inserted = null;
return parent1
.$relatedQuery('model1Relation2')
.then((models) => {
expect(models).to.have.length(1);
return parent1
.$relatedQuery('model1Relation2')
.insert([{ model2Prop1: 'test 1' }, { model2Prop1: 'test 2' }]);
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted[0].$beforeInsertCalled).to.equal(1);
expect(inserted[0].$afterInsertCalled).to.equal(1);
expect(inserted[1].$beforeInsertCalled).to.equal(1);
expect(inserted[1].$afterInsertCalled).to.equal(1);
expect(inserted[0].idCol).to.equal(3);
expect(inserted[1].idCol).to.equal(4);
expect(inserted[0]).to.be.a(Model2);
expect(inserted[1]).to.be.a(Model2);
expect(inserted[0].model2Prop1).to.equal('test 1');
expect(inserted[1].model2Prop1).to.equal('test 2');
expect(inserted[0].model1Id).to.equal(parent1.id);
expect(inserted[1].model1Id).to.equal(parent1.id);
expect(parent1.model1Relation2).to.eql(undefined);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(_.find(rows, { id_col: inserted[0].idCol }).model1_id).to.equal(parent1.id);
expect(_.find(rows, { id_col: inserted[0].idCol }).model2_prop1).to.equal('test 1');
expect(_.find(rows, { id_col: inserted[1].idCol }).model1_id).to.equal(parent1.id);
expect(_.find(rows, { id_col: inserted[1].idCol }).model2_prop1).to.equal('test 2');
});
});
}
});
describe('many to many relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 3,
},
],
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent1 = _.find(parents, { idCol: 1 });
parent2 = _.find(parents, { idCol: 2 });
});
});
it('should insert a related object', () => {
let inserted = null;
return parent1
.$relatedQuery('model2Relation1')
.then((models) => {
expect(models).to.have.length(1);
return parent1
.$relatedQuery('model2Relation1')
.insert(Model1.fromJson({ model1Prop1: 'test' }));
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(5);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
expect(parent1.model2Relation1).to.eql(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('test');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(
_.filter(rows, { model1Id: inserted.id, model2Id: parent1.idCol }),
).to.have.length(1);
});
});
if (session.isPostgres()) {
it('should insert a related object with onConflict().ignore()', async () => {
const inserted = await Model2.relatedQuery('model2Relation1')
.for(parent1.idCol)
.insert({ id: 4 })
.onConflict('id')
.ignore();
expect(inserted.id).to.equal(4);
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
const joinTableRows = await session.knex('Model1Model2');
chai.expect(joinTableRows).to.containSubset([{ model1Id: 4, model2Id: parent1.idCol }]);
});
}
it('should accept json', () => {
let inserted = null;
return parent1
.$relatedQuery('model2Relation1')
.then((models) => {
expect(models).to.have.length(1);
return parent1.$relatedQuery('model2Relation1').insert({ model1Prop1: 'test' });
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(5);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
expect(parent1.model2Relation1).to.eql(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('test');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(
_.filter(rows, { model1Id: inserted.id, model2Id: parent1.idCol }),
).to.have.length(1);
});
});
if (session.isPostgres()) {
it('should accept an array', () => {
let inserted = null;
return parent1
.$relatedQuery('model2Relation1')
.then((models) => {
expect(models).to.have.length(1);
return parent1
.$relatedQuery('model2Relation1')
.insert([
Model1.fromJson({ model1Prop1: 'test 1' }),
Model1.fromJson({ model1Prop1: 'test 2' }),
]);
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted[0].id).to.equal(5);
expect(inserted[1].id).to.equal(6);
expect(inserted[0]).to.be.a(Model1);
expect(inserted[1]).to.be.a(Model1);
expect(inserted[0].model1Prop1).to.equal('test 1');
expect(inserted[1].model1Prop1).to.equal('test 2');
expect(parent1.model2Relation1).to.eql(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(6);
expect(_.find(rows, { id: inserted[0].id }).model1Prop1).to.equal('test 1');
expect(_.find(rows, { id: inserted[1].id }).model1Prop1).to.equal('test 2');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(
_.filter(rows, { model1Id: inserted[0].id, model2Id: parent1.idCol }),
).to.have.length(1);
expect(
_.filter(rows, { model1Id: inserted[1].id, model2Id: parent1.idCol }),
).to.have.length(1);
});
});
it('should accept a json array', () => {
let inserted = null;
return parent1
.$relatedQuery('model2Relation1')
.then((models) => {
expect(models).to.have.length(1);
return parent1
.$relatedQuery('model2Relation1')
.insert([{ model1Prop1: 'test 1' }, { model1Prop1: 'test 2' }]);
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted[0].$beforeInsertCalled).to.equal(1);
expect(inserted[0].$afterInsertCalled).to.equal(1);
expect(inserted[1].$beforeInsertCalled).to.equal(1);
expect(inserted[1].$afterInsertCalled).to.equal(1);
expect(inserted[0].id).to.equal(5);
expect(inserted[1].id).to.equal(6);
expect(inserted[0]).to.be.a(Model1);
expect(inserted[1]).to.be.a(Model1);
expect(inserted[0].model1Prop1).to.equal('test 1');
expect(inserted[1].model1Prop1).to.equal('test 2');
expect(parent1.model2Relation1).to.eql(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(6);
expect(_.find(rows, { id: inserted[0].id }).model1Prop1).to.equal('test 1');
expect(_.find(rows, { id: inserted[1].id }).model1Prop1).to.equal('test 2');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(
_.filter(rows, { model1Id: inserted[0].id, model2Id: parent1.idCol }),
).to.have.length(1);
expect(
_.filter(rows, { model1Id: inserted[1].id, model2Id: parent1.idCol }),
).to.have.length(1);
});
});
}
it('should insert extra properties to the join table', () => {
let inserted = null;
return parent1
.$relatedQuery('model2Relation1')
.then((models) => {
expect(models).to.have.length(1);
return parent1
.$relatedQuery('model2Relation1')
.insert(Model1.fromJson({ model1Prop1: 'test', aliasedExtra: 'foo' }));
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.id).to.equal(5);
expect(inserted.model1Prop1).to.equal('test');
expect(inserted.aliasedExtra).to.equal('foo');
expect(parent1.model2Relation1).to.eql(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('test');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(
_.filter(rows, {
model1Id: inserted.id,
model2Id: parent1.idCol,
extra3: inserted.aliasedExtra,
}),
).to.have.length(1);
});
});
});
describe('has one through relation', () => {
let parent;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 3,
},
],
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent = _.find(parents, { idCol: 2 });
});
});
it('should insert a related object', () => {
let inserted = null;
return parent
.$relatedQuery('model2Relation2')
.then((models) => {
expect(models).to.equal(undefined);
return parent.$relatedQuery('model2Relation2').insert({ model1Prop1: 'test' });
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(5);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.find(rows, { id: inserted.id }).model1Prop1).to.equal('test');
return session.knex('Model1Model2One');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(
_.filter(rows, { model1Id: inserted.id, model2Id: parent.idCol }),
).to.have.length(1);
});
});
});
});
describe('.relatedQuery().insert()', () => {
describe('belongs to one relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should insert a related object', () => {
let inserted = null;
// First check that there is nothing in the relation.
return Model1.relatedQuery('model1Relation1')
.for(1)
.first()
.then((model) => {
expect(model).to.eql(undefined);
return Model1.relatedQuery('model1Relation1')
.for(1)
.first()
.insert({ model1Prop1: 'inserted' });
})
.then(($inserted) => {
inserted = $inserted;
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(3);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('inserted');
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
chai.expect(rows).containSubset([
{
id: 1,
model1Prop1: 'hello 1',
model1Id: 3,
},
{
id: 2,
model1Prop1: 'hello 2',
model1Id: null,
},
{
id: 3,
model1Prop1: 'inserted',
model1Id: null,
},
]);
});
});
it('should insert a related object and relate it to multiple owners', () => {
return Model1.relatedQuery('model1Relation1')
.for([1, 2])
.insert({ model1Prop1: 'inserted' })
.then((inserted) => {
expect(inserted.id).to.not.be(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(3);
chai.expect(rows).containSubset([
{
id: 1,
model1Prop1: 'hello 1',
model1Id: 3,
},
{
id: 2,
model1Prop1: 'hello 2',
model1Id: 3,
},
{
id: 3,
model1Prop1: 'inserted',
model1Id: null,
},
]);
});
});
it('should insert a related object and relate it to multiple owners using a subquery', () => {
return Model1.relatedQuery('model1Relation1')
.for(Model1.query().findByIds([1, 2]))
.insert({ model1Prop1: 'inserted' })
.then((inserted) => {
expect(inserted.id).to.not.be(undefined);
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(3);
chai.expect(rows).containSubset([
{
id: 1,
model1Prop1: 'hello 1',
model1Id: 3,
},
{
id: 2,
model1Prop1: 'hello 2',
model1Id: 3,
},
{
id: 3,
model1Prop1: 'inserted',
model1Id: null,
},
]);
});
});
});
describe('has many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 3,
},
],
},
]);
});
it('should insert a related object', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.insert({ model2Prop1: 'inserted' })
.then((inserted) => {
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.idCol).to.equal(3);
expect(inserted).to.be.a(Model2);
expect(inserted.model2Prop1).to.equal('inserted');
expect(inserted.model1Id).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(3);
chai.expect(rows).containSubset([
{ id_col: 1, model2_prop1: 'text 1', model2_prop2: 6, model1_id: 1 },
{ id_col: 2, model2_prop1: 'text 2', model2_prop2: 3, model1_id: 2 },
{ id_col: 3, model2_prop1: 'inserted', model2_prop2: null, model1_id: 1 },
]);
});
});
it('should insert a related object using a subquery', () => {
return Model1.relatedQuery('model1Relation2')
.for(Model1.query().findById(1))
.insert({ model2Prop1: 'inserted' })
.then(() => {
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(3);
chai.expect(rows).containSubset([
{ id_col: 1, model2_prop1: 'text 1', model2_prop2: 6, model1_id: 1 },
{ id_col: 2, model2_prop1: 'text 2', model2_prop2: 3, model1_id: 2 },
{ id_col: 3, model2_prop1: 'inserted', model2_prop2: null, model1_id: 1 },
]);
});
});
it('should fail if multiple parents are given', (done) => {
Model1.relatedQuery('model1Relation2')
.for([1, 2])
.insert({ model2Prop1: 'inserted' })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
"Can only insert items for one parent at a time in case of HasManyRelation. Otherwise multiple insert queries would need to be created. If you need to insert items for multiple parents, simply loop through them. That's the most performant way.",
);
done();
})
.catch(done);
});
if (session.isPostgres()) {
it('should accept an array', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.insert([{ model2Prop1: 'inserted 1' }, { model2Prop1: 'inserted 2' }])
.then((inserted) => {
expect(inserted[0].$beforeInsertCalled).to.equal(1);
expect(inserted[0].$afterInsertCalled).to.equal(1);
expect(inserted[1].$beforeInsertCalled).to.equal(1);
expect(inserted[1].$afterInsertCalled).to.equal(1);
expect(inserted[0].idCol).to.equal(3);
expect(inserted[1].idCol).to.equal(4);
expect(inserted[0]).to.be.a(Model2);
expect(inserted[1]).to.be.a(Model2);
expect(inserted[0].model2Prop1).to.equal('inserted 1');
expect(inserted[1].model2Prop1).to.equal('inserted 2');
expect(inserted[0].model1Id).to.equal(1);
expect(inserted[1].model1Id).to.equal(1);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id_col: 1, model2_prop1: 'text 1', model2_prop2: 6, model1_id: 1 },
{ id_col: 2, model2_prop1: 'text 2', model2_prop2: 3, model1_id: 2 },
{ id_col: 3, model2_prop1: 'inserted 1', model2_prop2: null, model1_id: 1 },
{ id_col: 4, model2_prop1: 'inserted 2', model2_prop2: null, model1_id: 1 },
]);
});
});
}
});
describe('many to many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 3,
},
],
},
],
},
]);
});
it('should insert a related object for one parent using an id', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.insert({ model1Prop1: 'test' })
.then((inserted) => {
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(5);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.find(rows, { id: 5 }).model1Prop1).to.equal('test');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.filter(rows, { model1Id: 5, model2Id: 1 })).to.have.length(1);
});
});
it('should insert a related object for one parent using a subquery', () => {
return Model2.relatedQuery('model2Relation1')
.for(Model2.query().findById(1))
.insert({ model1Prop1: 'test' })
.then((inserted) => {
expect(inserted.id).to.equal(5);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.find(rows, { id: 5 }).model1Prop1).to.equal('test');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.filter(rows, { model1Id: 5, model2Id: 1 })).to.have.length(1);
});
});
if (session.isPostgres()) {
it('should insert a related object for two parents using ids', () => {
return Model2.relatedQuery('model2Relation1')
.for([1, 2])
.insert({ model1Prop1: 'test' })
.then((inserted) => {
expect(inserted.$beforeInsertCalled).to.equal(1);
expect(inserted.$afterInsertCalled).to.equal(1);
expect(inserted.id).to.equal(5);
expect(inserted).to.be.a(Model1);
expect(inserted.model1Prop1).to.equal('test');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.find(rows, { id: 5 }).model1Prop1).to.equal('test');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(_.filter(rows, { model1Id: 5, model2Id: 1 })).to.have.length(1);
expect(_.filter(rows, { model1Id: 5, model2Id: 2 })).to.have.length(1);
});
});
it('should accept an array', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.insert([{ model1Prop1: 'test 1' }, { model1Prop1: 'test 2' }])
.then((inserted) => {
expect(inserted[0].id).to.equal(5);
expect(inserted[1].id).to.equal(6);
expect(inserted[0]).to.be.a(Model1);
expect(inserted[1]).to.be.a(Model1);
expect(inserted[0].model1Prop1).to.equal('test 1');
expect(inserted[1].model1Prop1).to.equal('test 2');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(6);
expect(_.find(rows, { id: 5 }).model1Prop1).to.equal('test 1');
expect(_.find(rows, { id: 6 }).model1Prop1).to.equal('test 2');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(_.filter(rows, { model1Id: 5, model2Id: 1 })).to.have.length(1);
expect(_.filter(rows, { model1Id: 6, model2Id: 1 })).to.have.length(1);
});
});
}
it('should insert extra properties to the join table', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.insert(Model1.fromJson({ model1Prop1: 'test', aliasedExtra: 'foo' }))
.then((inserted) => {
expect(inserted.id).to.equal(5);
expect(inserted.model1Prop1).to.equal('test');
expect(inserted.aliasedExtra).to.equal('foo');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.find(rows, { id: 5 }).model1Prop1).to.equal('test');
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(
_.filter(rows, {
model1Id: 5,
model2Id: 1,
extra3: 'foo',
}),
).to.have.length(1);
});
});
});
});
describe('.query().insert().onConflict()', () => {
beforeEach(() => {
return session.populate([]);
});
describe('.ignore()', () => {
it('should silently ignore insert if id already exists', async () => {
await Model1.query().insert({ id: 1 });
const result = await Model1.query().insert({ id: 1 }).onConflict('id').ignore();
expect(result instanceof Model1).to.equal(true);
const rows = await Model1.query();
expect(rows).to.have.length(1);
expect(rows[0].id).to.equal(1);
expect(result.id).to.equal(rows[0].id);
});
});
describe('.merge()', () => {
it('should update the row if id already exists', async () => {
await Model2.query().insert({ idCol: 1 });
const result = await Model2.query()
.insert({ idCol: 1, model2Prop1: 'updated' })
.onConflict('id_col')
.merge();
expect(result instanceof Model2).to.equal(true);
const rows = await Model2.query();
expect(rows).to.have.length(1);
expect(rows[0].idCol).to.equal(1);
expect(rows[0].model2Prop1).to.equal('updated');
expect(result.id).to.equal(rows[0].id);
expect(result.model2Prop1).to.equal(rows[0].model2Prop1);
});
it('should update some columns of the row if id already exists', async () => {
await Model2.query().insert({ idCol: 1 });
const result = await Model2.query()
.insert({ idCol: 1, model2Prop1: 'updated', model2Prop2: 123456 })
.onConflict('id_col')
.merge(['model2_prop1']);
expect(result instanceof Model2).to.equal(true);
const rows = await Model2.query();
expect(rows).to.have.length(1);
expect(rows[0].idCol).to.equal(1);
expect(rows[0].model2Prop1).to.equal('updated');
expect(rows[0].model2Prop2).to.equal(null);
expect(result.id).to.equal(rows[0].id);
expect(result.model2Prop1).to.equal(rows[0].model2Prop1);
});
if (session.isPostgres()) {
it('should update the row with custom values if id already exists', async () => {
await Model2.query().insert({ idCol: 1 });
const result = await Model2.query()
.insert({ idCol: 1, model2Prop1: 'updated' })
.onConflict('id_col')
.merge({ model2Prop1: 'override updated' })
.returning('id_col', 'model2_prop1');
expect(result instanceof Model2).to.equal(true);
const rows = await Model2.query();
expect(rows).to.have.length(1);
expect(rows[0].idCol).to.equal(1);
expect(rows[0].model2Prop1).to.equal('override updated');
expect(result.id).to.equal(rows[0].id);
expect(result.model2Prop1).to.equal(rows[0].model2Prop1);
});
}
});
});
function subClassWithSchema(Model, schema) {
let SubModel = inheritModel(Model);
SubModel.jsonSchema = schema;
return SubModel;
}
});
};
================================================
FILE: tests/integration/insertGraph.js
================================================
const _ = require('lodash');
const chai = require('chai');
const utils = require('../../lib/utils/knexUtils');
const expect = require('expect.js');
const Promise = require('bluebird');
const { transaction, ValidationError, Model } = require('../../');
module.exports = (session) => {
let Model1 = session.models.Model1;
let Model2 = session.models.Model2;
describe('Model insertGraph queries', () => {
const eagerExpr = `[
model1Relation1.model1Relation3,
model1Relation1Inverse,
model1Relation2.model2Relation2
]`;
let population;
let insertion;
beforeEach(() => {
population = {
id: 1,
model1Prop1: '20',
model1Relation3: [
{
idCol: 1,
},
],
};
insertion = {
model1Prop1: 'root',
model1Relation1: {
model1Prop1: 'parent',
model1Prop2: '#ref{grandChild.idCol}',
model1Relation3: [
{
'#ref': 'child1',
},
{
'#id': 'grandChild',
model2Prop1: 'cibling2',
// These should go to the join table.
extra1: 'extraVal1',
extra2: 'extraVal2',
},
{
'#dbRef': 1,
extra1: 'foo',
},
],
},
model1Relation1Inverse: {
model1Prop1: 'rootParent',
},
model1Relation2: [
{
'#id': 'child1',
model2Prop1: 'child1',
},
{
model2Prop1: 'child2',
model2Relation2: {
model1Prop1: 'child3',
},
},
],
};
});
describe('.query().insertGraph()', () => {
beforeEach(() => {
return session.populate(population);
});
it('should do nothing if an empty array is provided', () => {
return Model1.query().insertGraph([]);
});
it('should throw if #ref is used without the `allowRefs` option', (done) => {
Model1.query()
.insertGraph({
'#id': 'id1',
model1Relation2: [
{
'#ref': 'id1',
},
],
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
'#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option',
);
done();
});
});
it('should throw if #ref{} is used without the `allowRefs` option', (done) => {
Model1.query()
.insertGraph({
'#id': 'id1',
model1Relation2: [
{
model1Prop1: '#ref{id1.id}',
},
],
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
'#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option',
);
done();
});
});
it('should insert a model with relations', () => {
return Model1.query()
.insertGraph(insertion, { allowRefs: true })
.then((inserted) => {
return check(inserted, true).then(() => inserted);
})
.then((inserted) => {
expect(inserted).to.not.have.property('model1Prop2');
return Model1.query().withGraphFetched(eagerExpr).where('id', inserted.id).first();
})
.then((model) => {
return check(model);
});
});
describe('jsonSchema: additionalProperties = false', () => {
let origSchema;
before(() => {
origSchema = Model1.jsonSchema;
Model1.jsonSchema = {
type: 'object',
additionalProperties: false,
properties: {
id: { type: 'number' },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
model1Id: { type: 'number' },
},
};
// Clear the memoized schema.
delete Model1.$$jsonSchema;
expect(Model1.getJsonSchema()).to.equal(Model1.jsonSchema);
});
after(() => {
Model1.jsonSchema = origSchema;
// Clear the memoized schema.
delete Model1.$$jsonSchema;
expect(Model1.getJsonSchema()).to.equal(origSchema);
});
it('should insert a model with relations', () => {
return Model1.query()
.insertGraph(insertion, { allowRefs: true })
.then((inserted) => {
return check(inserted, true).then(() => inserted);
})
.then((inserted) => {
expect(inserted).to.not.have.property('model1Prop2');
return Model1.query().withGraphFetched(eagerExpr).where('id', inserted.id).first();
})
.then((model) => {
return check(model);
});
});
});
it('should accept raw sql and subqueries', () => {
return Model1.query()
.insertGraph([
{
model1Prop1: '10',
},
{
model1Prop1: '50',
},
])
.then(() => {
return Model1.query().insertGraph({
model1Prop1: Model1.raw('40 + 2'),
model1Relation2: [
{
'#id': 'child1',
idCol: 100,
model2Prop1: Model1.query().min('model1Prop1'),
},
{
idCol: 101,
model2Prop1: Model1.knex().from('Model1').max('model1Prop1'),
},
],
});
})
.then((inserted) => {
inserted.model1Relation2 = _.sortBy(inserted.model1Relation2, 'idCol');
expect(inserted.toJSON()).to.eql({
id: 4,
model1Relation2: [
{ model1Id: 4, idCol: 100 },
{ model1Id: 4, idCol: 101 },
],
});
return Model1.query().withGraphFetched('model1Relation2').where('id', inserted.id);
})
.then((inserted) => {
inserted[0].model1Relation2 = _.sortBy(inserted[0].model1Relation2, 'idCol');
expect(inserted[0]).to.eql({
id: 4,
model1Id: null,
model1Prop1: '42',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation2: [
{
idCol: 100,
model1Id: 4,
model2Prop1: '10',
model2Prop2: null,
$afterFindCalled: 1,
},
{
idCol: 101,
model1Id: 4,
model2Prop1: '50',
model2Prop2: null,
$afterFindCalled: 1,
},
],
});
});
});
const testValidation = (modifyGraph, expectedProperty) => {
return (done) => {
const graph = _.cloneDeep(insertion);
modifyGraph(graph);
transaction(Model1, Model2, (Model1, Model2) => {
// We can modify Model1 and Model2 here since it is a subclass of the actual
// models shared between tests.
Model1.jsonSchema = {
type: 'object',
properties: {
id: { type: 'integer' },
model1Id: { type: 'integer' },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'integer' },
},
};
Model2.jsonSchema = {
type: 'object',
properties: {
idCol: { type: 'integer' },
model1Id: { type: 'integer' },
model2Prop1: { type: 'string' },
model2Prop2: { type: 'integer' },
},
};
delete Model1.$$jsonSchema;
delete Model2.$$jsonSchema;
expect(Model1.getJsonSchema()).to.equal(Model1.jsonSchema);
expect(Model2.getJsonSchema()).to.equal(Model2.jsonSchema);
return Model1.query().insertGraph(graph, { allowRefs: true });
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(ValidationError);
expect(err.data).to.have.property(expectedProperty);
return Promise.all([session.knex('Model1'), session.knex('model2')]);
})
.then(([rows1, rows2]) => {
expect(rows1).to.have.length(1);
expect(rows2).to.have.length(1);
done();
})
.catch(done);
};
};
it(
'should validate models upon insertion and return correct validation paths',
testValidation((graph) => {
graph.model1Relation1.model1Prop1 = 666;
}, 'model1Relation1.model1Prop1'),
);
it(
'should return correct validation paths with has-many relations',
testValidation((graph) => {
graph.model1Relation2[0].model2Prop1 = 666;
}, 'model1Relation2[0].model2Prop1'),
);
it(
'should return correct validation paths with many-to-many relations',
testValidation((graph) => {
graph.model1Relation1.model1Relation3[1].model2Prop1 = 666;
}, 'model1Relation1.model1Relation3[1].model2Prop1'),
);
it('should validate models upon insertion: references in integer columns should be accepted', () => {
return transaction(Model1, Model2, (Model1, Model2) => {
// We can modify Model1 and Model2 here since it is a subclass of the actual
// models shared between tests.
Model1.jsonSchema = {
type: 'object',
properties: {
id: { type: 'integer' },
model1Id: { type: 'integer' },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'integer' },
},
};
Model2.jsonSchema = {
type: 'object',
properties: {
idCol: { type: 'integer' },
model1Id: { type: 'integer' },
model2Prop1: { type: 'string' },
model2Prop2: { type: 'integer' },
},
};
// Clear the memoized schema.
delete Model1.$$jsonSchema;
delete Model2.$$jsonSchema;
expect(Model1.getJsonSchema()).to.equal(Model1.jsonSchema);
expect(Model2.getJsonSchema()).to.equal(Model2.jsonSchema);
return Model1.query()
.insertGraph(insertion, { allowRefs: true })
.then((inserted) => {
return check(inserted, true).then(() => inserted);
})
.then((inserted) => {
expect(inserted).to.not.have.property('model1Prop2');
return Model1.query().withGraphFetched(eagerExpr).where('id', inserted.id).first();
})
.then((model) => {
return check(model);
});
});
});
it('relate: true option should cause models with id to be related instead of inserted', () => {
return Model2.query()
.insertGraph(
{
model2Prop1: 'foo',
model2Relation1: [
{
id: population.id,
},
],
},
{
relate: true,
},
)
.then((model) => {
return Model2.query().findById(model.idCol).withGraphFetched('model2Relation1');
})
.then((model) => {
delete model.idCol;
expect(model).to.eql({
model1Id: null,
model2Prop1: 'foo',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: population.id,
model1Id: null,
model1Prop1: population.model1Prop1,
model1Prop2: null,
aliasedExtra: null,
$afterFindCalled: 1,
},
],
});
});
});
it('trying to relate a HasManyRelation should throw', (done) => {
Model1.query()
.insertGraph(
{
model1Prop1: 'foo',
model1Relation2: [
{
idCol: population.model1Relation3[0].idCol,
},
],
},
{
relate: true,
},
)
.then(() => done(new Error('should not get here')))
.catch((err) => {
expect(err.message).to.equal(
'You cannot relate HasManyRelation or HasOneRelation using insertGraph, because those require update operations. Consider using upsertGraph instead.',
);
done();
})
.catch(done);
});
it(`relate: ['relation.path'] option should cause models with id to be related instead of inserted`, () => {
return Model1.query()
.insert({ model1Prop1: 'howdy', id: 500 })
.then(() => {
return Model1.query().insertGraph(
{
model1Prop1: 'hello',
model1Relation1: {
// This should get related.
id: 500,
},
model1Relation2: [
{
model2Prop1: 'world',
model2Relation1: [
{
// This should get related.
id: population.id,
},
],
},
],
model1Relation3: [
{
// This should get inserted.
idCol: 1000,
},
],
},
{
relate: ['model1Relation1', 'model1Relation2.model2Relation1'],
},
);
})
.then((model) => {
return Model1.query()
.findById(model.id)
.withGraphFetched(
'[model1Relation1, model1Relation2.model2Relation1, model1Relation3]',
);
})
.then((model) => {
delete model.id;
delete model.model1Relation2[0].idCol;
delete model.model1Relation2[0].model1Id;
expect(model).to.eql({
model1Id: 500,
model1Prop1: 'hello',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 500,
model1Prop1: 'howdy',
model1Id: null,
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
model2Prop1: 'world',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: population.id,
model1Id: null,
model1Prop1: population.model1Prop1,
model1Prop2: null,
aliasedExtra: null,
$afterFindCalled: 1,
},
],
},
],
model1Relation3: [
{
idCol: 1000,
model1Id: null,
model2Prop1: null,
model2Prop2: null,
extra1: null,
extra2: null,
$afterFindCalled: 1,
},
],
});
});
});
if (utils.isPostgres(session.knex)) {
it('query building methods should be applied to the root models', () => {
return Model1.query()
.insertGraph(insertion, { allowRefs: true })
.returning('*')
.then((inserted) => {
return check(inserted, true).then(() => inserted);
})
.then((inserted) => {
expect(inserted).to.have.property('model1Prop2');
return Model1.query().withGraphFetched(eagerExpr).where('id', inserted.id).first();
})
.then((model) => {
return check(model);
});
});
}
});
describe('.query().insertGraphAndFetch()', () => {
beforeEach(() => {
return session.populate(population);
});
it('should do nothing if an empty array is provided', () => {
return Model1.query().insertGraphAndFetch([]);
});
it('should insert a model with relations and fetch the inserted graph', () => {
return Model1.query()
.insertGraphAndFetch(insertion, { allowRefs: true })
.then((inserted) => {
return check(inserted).then(() => inserted);
})
.then((inserted) => {
return Model1.query()
.withGraphFetched(eagerExpr)
.findById(inserted.id)
.then((fetched) => {
chai.expect(inserted.$toJson()).to.containSubset(fetched.$toJson());
chai.expect(fetched.$toJson()).to.containSubset(inserted.$toJson());
});
});
});
it('relate: true option should cause models with id to be related instead of inserted', () => {
return Model2.query()
.insertGraphAndFetch(
{
model2Prop1: 'foo',
model2Relation1: [
{
id: population.id,
},
],
},
{
relate: true,
},
)
.then((model) => {
return Model2.query().findById(model.idCol).withGraphFetched('model2Relation1');
})
.then((model) => {
delete model.idCol;
expect(model).to.eql({
model1Id: null,
model2Prop1: 'foo',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: population.id,
model1Id: null,
model1Prop1: population.model1Prop1,
model1Prop2: null,
aliasedExtra: null,
$afterFindCalled: 1,
},
],
});
});
});
});
describe('.query().insertGraph().allowGraph()', () => {
beforeEach(() => {
return session.populate(population);
});
it('should allow insert when the allowed relation expression is a superset', () => {
return Model1.query()
.insertGraph(insertion, { allowRefs: true })
.allowGraph(eagerExpr)
.then((inserted) => {
return check(inserted, true).then(() => inserted);
});
});
it('should not allow insert when the allowed relation expression is not a superset', (done) => {
Model1.query()
.insertGraph(insertion)
.allowGraph('[model1Relation1.model1Relation3, model1Relation2]')
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err instanceof ValidationError).to.equal(true);
expect(err.type).to.equal('UnallowedRelation');
expect(err.message).to.eql('trying to upsert an unallowed relation');
done();
})
.catch(done);
});
});
describe('.$query().insertGraph()', () => {
beforeEach(() => {
return session.populate(population);
});
it('should insert a model with relations', () => {
return Model1.fromJson(insertion)
.$query()
.insertGraph(undefined, { allowRefs: true })
.then((inserted) => {
return check(inserted, true).then(() => inserted);
})
.then((inserted) => {
return Model1.query().withGraphFetched(eagerExpr).where('id', inserted.id).first();
})
.then((model) => {
return check(model);
});
});
});
describe('.$relatedQuery().insertGraph()', () => {
describe('has many relation', () => {
let parent;
beforeEach(() => {
return session.populate(population);
});
beforeEach(() => {
return Model1.query()
.where('id', 1)
.first()
.then((par) => {
parent = par;
});
});
beforeEach(() => {
insertion = {
model2Prop1: 'howdy',
model2Relation1: [insertion],
};
});
it('should insert a model with relations', () => {
return parent
.$relatedQuery('model1Relation2')
.insertGraph(insertion, { allowRefs: true })
.then((inserted) => {
return check(inserted.model2Relation1[0], true);
})
.then(() => {
return parent.$relatedQuery('model1Relation2').first();
})
.then((insertion) => {
expect(insertion.model2Prop1).to.equal('howdy');
return insertion.$relatedQuery('model2Relation1').withGraphFetched(eagerExpr).first();
})
.then((model) => {
return check(model);
});
});
});
describe('many to many relation', () => {
let parent;
beforeEach(() => {
return session.populate(population);
});
beforeEach(() => {
return Model1.query()
.where('id', 1)
.first()
.then((par) => {
parent = par;
});
});
beforeEach(() => {
insertion = {
model2Prop1: 'howdy',
model2Relation1: [insertion],
};
});
it('should insert a model with relations', () => {
return parent
.$relatedQuery('model1Relation3')
.insertGraph(insertion, { allowRefs: true })
.then((inserted) => {
return check(inserted.model2Relation1[0], true);
})
.then(() => {
return parent.$relatedQuery('model1Relation3');
})
.then((models) => {
let insertion = _.find(models, { model2Prop1: 'howdy' });
return insertion.$relatedQuery('model2Relation1').withGraphFetched(eagerExpr);
})
.then((models) => {
let model = _.find(models, { model1Prop1: 'root' });
return check(model);
});
});
});
});
function check(model, shouldCheckHooks) {
model = model.$clone();
let knex = model.constructor.knex();
expect(model).to.have.property('model1Relation1');
expect(model.model1Relation1).to.have.property('model1Relation3');
expect(model).to.have.property('model1Relation2');
model.model1Relation1.model1Relation3 = _.sortBy(
model.model1Relation1.model1Relation3,
'model2Prop1',
);
model.model1Relation2 = _.sortBy(model.model1Relation2, 'model2Prop1');
expect(model.model1Prop1).to.equal('root');
shouldCheckHooks && checkHooks(model);
expect(model.model1Relation1.model1Prop1).to.equal('parent');
shouldCheckHooks && checkHooks(model.model1Relation1);
expect(model.model1Relation1Inverse.model1Prop1).to.equal('rootParent');
shouldCheckHooks && checkHooks(model.model1Relation1Inverse);
expect(model.model1Relation1.model1Relation3[0].model2Prop1).to.equal('child1');
shouldCheckHooks && checkHooks(model.model1Relation1.model1Relation3[0]);
expect(model.model1Relation1.model1Relation3[1].model2Prop1).to.equal('cibling2');
expect(model.model1Relation1.model1Relation3[1].extra1).to.equal('extraVal1');
expect(model.model1Relation1.model1Relation3[1].extra2).to.equal('extraVal2');
expect(model.model1Relation1.model1Relation3[2].idCol).to.equal(1);
expect(model.model1Relation1.model1Relation3[2].extra1).to.equal('foo');
shouldCheckHooks && checkHooks(model.model1Relation1.model1Relation3[1]);
expect(model.model1Relation2[0].model2Prop1).to.equal('child1');
shouldCheckHooks && checkHooks(model.model1Relation2[0]);
expect(model.model1Relation2[1].model2Prop1).to.equal('child2');
shouldCheckHooks && checkHooks(model.model1Relation2[1]);
expect(model.model1Relation2[1].model2Relation2.model1Prop1).to.equal('child3');
shouldCheckHooks && checkHooks(model.model1Relation2[1].model2Relation2);
return knex(Model2.getTableName()).then((rows) => {
// Check that the reference model was only inserted once.
expect(_.filter(rows, { model2_prop1: 'child1' })).to.have.length(1);
});
}
function checkHooks(model) {
expect(model.$beforeInsertCalled).to.equal(1);
expect(model.$afterInsertCalled).to.equal(1);
}
});
};
================================================
FILE: tests/integration/jsonQueries.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const Promise = require('bluebird');
const { Model, ref, val, raw } = require('../../');
function expectIdsEqual(resultArray, expectedIds) {
expectArraysEqual(_(resultArray).map('id').sort().value(), expectedIds);
}
function expectArraysEqual(arr1, arr2) {
expect({ arr: arr1 }).to.eql({ arr: arr2 });
}
module.exports = (session) => {
describe('JSON queries', () => {
class ModelJson extends Model {
static get tableName() {
return 'ModelJson';
}
static get jsonSchema() {
return {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
jsonObject: { type: 'object' },
jsonArray: { type: 'array' },
},
};
}
}
let BoundModel = ModelJson.bindKnex(session.knex);
before(() => {
return session.knex.schema
.dropTableIfExists('ModelJson')
.createTable('ModelJson', (table) => {
table.integer('id').primary();
table.string('name');
table.jsonb('jsonObject');
table.jsonb('jsonArray');
});
});
describe('QueryBuilder using ref() in normal query builder methods', () => {
describe('Querying rows', () => {
before(() => {
return BoundModel.query()
.delete()
.then(() => {
return BoundModel.query().insert([
{ id: 1, name: 'test1', jsonObject: {}, jsonArray: [1] },
{ id: 2, name: 'test2', jsonObject: { attr: 2 }, jsonArray: [2] },
{ id: 3, name: 'test3', jsonObject: { attr: 3 }, jsonArray: [3] },
{ id: 4, name: 'test4', jsonObject: { attr: 4 }, jsonArray: [4] },
]);
});
});
it('should be able to extract json attr in select(ref)', () => {
return BoundModel.query()
.select(ref('jsonArray:[0]').as('foo'))
.orderBy('foo', 'desc')
.then((result) => {
expect(result).to.have.length(4);
expect(_.first(result)).eql({ foo: 4 });
});
});
it('should be able to extract json attr in select(array)', () => {
return BoundModel.query()
.select([ref('jsonObject:attr').castBigInt().as('bar'), ref('jsonArray:[0]').as('foo')])
.orderBy('foo')
.then((result) => {
expect(result).to.have.length(4);
expect(_.first(result)).eql({ foo: 1, bar: null });
});
});
it('should be able to use ref inside select of select subquery', () => {
return BoundModel.query()
.select([
(builder) => {
builder
.select([ref('name').as('barName')])
.from('ModelJson')
.orderBy('name', 'desc')
.limit(1)
.as('foo');
},
ref('jsonArray:[0]').as('firstArrayItem'),
])
.orderBy('firstArrayItem', 'desc')
.then((result) => {
expect(result).to.have.length(4);
// foo is always name of the last row of the table (quite a nonsense query)
expect(_.first(result)).eql({ foo: 'test4', firstArrayItem: 4 });
});
});
it('should be able to use ref with where', () => {
return BoundModel.query()
.where(ref('jsonArray:[0]').castBigInt(), ref('jsonObject:attr').castBigInt())
.then((result) => {
expect(result).to.have.length(3);
});
});
it('should be able to use ref with where subquery', () => {
return BoundModel.query()
.where((builder) => {
builder.where(ref('jsonArray:[0]').castBigInt(), ref('jsonObject:attr').castBigInt());
})
.then((result) => {
expect(result).to.have.length(3);
});
});
it('should be able to use ref with join', () => {
// select * from foo join bar on ref() = ref()
return BoundModel.query()
.join('ModelJson as t2', ref('ModelJson.jsonArray:[0]'), '=', ref('t2.jsonObject:attr'))
.select('t2.*')
.then((result) => {
expect(result).to.have.length(3);
});
});
it('should be able to use ref with double nested join builder', () => {
return BoundModel.query()
.join('ModelJson as t2', (builder) => {
builder
.on(ref('ModelJson.jsonArray:[0]'), '=', ref('t2.jsonObject:attr'))
.on((nestedBuilder) => {
nestedBuilder
.on(ref('ModelJson.id').castInt(), '=', ref('t2.jsonArray:[0]').castInt())
.orOn(ref('ModelJson.id').castInt(), '=', ref('t2.jsonObject:attr').castInt());
});
})
.select('t2.*')
.then((result) => {
expect(result).to.have.length(3);
});
});
it('should be able to use ref with orderBy', () => {
return BoundModel.query()
.orderBy(ref('jsonObject:attr'), 'desc')
.then((result) => {
expect(result).to.have.length(4);
// null is first
expect(_.first(result).name).to.equal('test1');
});
});
it('should be able to use ref with groupBy and having (last argument of having is ref)', () => {
return BoundModel.query()
.select(['id', ref('jsonObject:attr').as('foo')])
.groupBy([ref('jsonObject:attr'), 'id'])
.having('id', '>=', ref('jsonObject:attr').castInt())
.orderBy('foo')
.then((result) => {
expect(result).to.have.length(3);
expect(_.first(result)).to.eql({ id: 2, foo: 2 });
});
});
it('should be able to use ref with groupBy and having (also first arg is ref)', () => {
return BoundModel.query()
.select('id', ref('jsonObject:attr').as('foo'))
.groupBy([ref('jsonObject:attr'), 'id'])
.having(ref('id').castInt(), '>=', ref('jsonObject:attr').castInt())
.orderBy('foo')
.then((result) => {
expect(result).to.have.length(3);
expect(_.first(result)).to.eql({ id: 2, foo: 2 });
});
});
it('should be able to use ref with groupBy and nested having', () => {
return BoundModel.query()
.select(['id', ref('jsonObject:attr').as('foo')])
.groupBy([ref('jsonObject:attr'), 'id'])
.having('id', '>=', ref('jsonObject:attr').castInt())
.having((builder) => {
builder.having('id', '=', ref('id')).having((nestedBuilder) => {
nestedBuilder.having('id', '=', ref('id'));
});
})
.orderBy('foo')
.then((result) => {
expect(result).to.have.length(3);
expect(_.first(result)).to.eql({ id: 2, foo: 2 });
});
});
});
describe('.insert()', () => {
it('should insert nicely', () => {
// this query actually isnt valid, but I couldn't figure any query where one would actually use ref as value
// so just testing that refs are converted to raw correctly
BoundModel.query().insert({
id: 5,
name: ref('jsonArray:[0]').castText(),
jsonObject: ref('name'),
jsonArray: [1],
});
// I have no idea how to check built result.. toSql() didn't seem to help in this case
});
});
describe('.update() and .patch()', () => {
beforeEach(() => {
return BoundModel.query()
.truncate()
.then(() => {
return BoundModel.query().insert([
{ id: 1, name: 'test1', jsonObject: {}, jsonArray: [1] },
{ id: 2, name: 'test2', jsonObject: { attr: 2 }, jsonArray: [2] },
{ id: 3, name: 'test3', jsonObject: { attr: 3 }, jsonArray: [3] },
{ id: 4, name: 'test4', jsonObject: { attr: 4 }, jsonArray: [4] },
]);
});
});
it('should be able to use knex.raw to jsonb column in update', () => {
return BoundModel.query()
.update({
jsonArray: BoundModel.knex().raw('to_jsonb(??)', ['name']),
})
.then((result) => {
expect(result).to.be(4);
});
});
it('should be able to update internal field of json column and allow ref() syntax', () => {
// should do something like:
// update 'ModelJson' set
// 'jsonArray' = jsonb_set('[]', '{0}', to_jsonb('name'), true),
// 'jsonObject' = jsonb_set('jsonObject', '{attr}', to_jsonb('name'), true),
// 'name' = 'jsonArray'#>>'{0}' where 'id' = 1 returning *;
return BoundModel.query()
.update({
name: ref('jsonArray:[0]').castText(),
'jsonObject:attr': ref('name'),
// each attribute which is updated with ref must be updated separately
// e.g. SET 'jsonArray' = '[ ref(...), ref(...) ]' just isn't valid SQL
// (though it could be kind of parsed to multiple jsonb_set calls which would be insanely cool)
jsonArray: ref('name').castJson(),
})
.where('id', 1)
.returning('*')
.then((result) => {
expect(result).to.eql([
{
id: 1,
name: '1',
jsonObject: { attr: 'test1' },
jsonArray: 'test1',
},
]);
});
});
it('should be able to patch internal field of json column and allow ref() syntax', () => {
// same stuff that with patch but different api method
return BoundModel.query()
.patch({
name: ref('jsonArray:[0]').castText(),
'jsonObject:attr': ref('name'),
jsonArray: ref('name').castJson(),
})
.where('id', 1)
.returning('*')
.then((result) => {
expect(result).to.eql([
{
id: 1,
name: '1',
jsonObject: { attr: 'test1' },
jsonArray: 'test1',
},
]);
});
});
it('should be able to patch internal field of json column using an array literal', () => {
return BoundModel.query()
.patch({
'jsonObject:attr': [1, 2, 5, 7],
})
.findById(1)
.then(() => BoundModel.query().findById(1).select('jsonObject'))
.then((result) => {
expect(result).to.eql({
jsonObject: { attr: [1, 2, 5, 7] },
});
});
});
it('should be able to patch internal field of json column using an object literal', () => {
return BoundModel.query()
.patch({
'IGNOREME.jsonObject:attr': { foo: 'bar' },
})
.where('id', 1)
.then(() => BoundModel.query().findById(1).select('jsonObject'))
.then((result) => {
expect(result).to.eql({
jsonObject: { attr: { foo: 'bar' } },
});
});
});
it('should be able to patch internal field of json column using a string', () => {
return BoundModel.query()
.patch({
'jsonObject:attr': 'baz',
})
.where('id', 2)
.then(() => BoundModel.query().findById(2).select('jsonObject'))
.then((result) => {
expect(result).to.eql({
jsonObject: { attr: 'baz' },
});
});
});
it('should be able to patch internal field of json column using a val() instance', () => {
return BoundModel.query()
.patch({
'jsonObject:attr': val('baz').castJson(),
})
.where('id', 2)
.then(() => BoundModel.query().findById(2).select('jsonObject'))
.then((result) => {
expect(result).to.eql({
jsonObject: { attr: 'baz' },
});
});
});
it('should be able to patch multiple fields inside the same json object', () => {
return BoundModel.query()
.patch({
'jsonObject:attr1': 'foo',
'jsonObject:attr2': 'bar',
})
.where('id', 2)
.then(() => BoundModel.query().findById(2).select('jsonObject'))
.then((result) => {
expect(result).to.eql({
jsonObject: {
attr: 2,
attr1: 'foo',
attr2: 'bar',
},
});
});
});
it('should be able to patch fields using $query().patch()', () => {
return BoundModel.query()
.findById(1)
.then((model) => {
return model.$query().patch({
name: 'updated name',
'jsonObject:attr': 'bar',
});
})
.then(() => {
return BoundModel.query().findById(1).select('name', 'jsonObject');
})
.then((result) => {
expect(result).to.eql({
name: 'updated name',
jsonObject: {
attr: 'bar',
},
});
});
});
it('should not have the json reference property in the result object', async () => {
const item = await BoundModel.query().findById(1);
await item.$query().patch({ 'jsonObject:attr': 'bar' }).returning('*');
expect(item).to.eql({
id: 1,
name: 'test1',
jsonObject: { attr: 'bar' },
jsonArray: [1],
});
});
});
});
describe('QueryBuilder JSON queries', () => {
let complexJsonObj;
before(() => {
complexJsonObj = {
id: 1,
name: 'complex line',
jsonObject: {
stringField: 'string in jsonObject.stringField',
numberField: 1.5,
nullField: null,
booleanField: false,
arrayField: [
{ noMoreLevels: true },
1,
true,
null,
'string in jsonObject.arrayField[4]',
],
objectField: {
object: 'string in jsonObject.objectField.object',
},
},
jsonArray: [
{
stringField: 'string in jsonArray[0].stringField',
numberField: 5.5,
nullField: null,
booleanField: true,
arrayField: [{ noMoreLevels: true }],
objectField: {
object: `I'm string in jsonArray[0].objectField.object`,
},
},
null,
1,
'string in jsonArray[3]',
false,
[{ noMoreLevels: true }, null, 1, 'string in jsonArray[5][3]', true],
],
};
complexJsonObj.jsonObject.jsonArray = _.cloneDeep(complexJsonObj.jsonArray);
return BoundModel.query()
.delete()
.then(() => {
return Promise.all([
BoundModel.query().insert(complexJsonObj),
BoundModel.query().insert({
id: 2,
name: 'empty object and array',
jsonObject: {},
jsonArray: [],
}),
BoundModel.query().insert({ id: 3, name: 'null object and array' }),
BoundModel.query().insert({
id: 4,
name: 'empty object and [1,2]',
jsonObject: {},
jsonArray: [1, 2],
}),
BoundModel.query().insert({
id: 5,
name: 'empty object and array [ null ]',
jsonObject: {},
jsonArray: [null],
}),
BoundModel.query().insert({
id: 6,
name: '{a: 1} and empty array',
jsonObject: { a: 1 },
jsonArray: [],
}),
BoundModel.query().insert({
id: 7,
name: '{a: {1:1, 2:2}, b:{2:2, 1:1}} for equality comparisons',
jsonObject: { a: { 1: 1, 2: 2 }, b: { 2: 2, 1: 1 } },
jsonArray: [],
}),
]);
});
});
it('should have test data', () => {
return BoundModel.query().then((all) => {
expect(_.find(all, { name: 'complex line' }).jsonObject.stringField).to.be(
complexJsonObj.jsonObject.stringField,
);
});
});
describe('private function parseFieldExpression(expression, extractAsText)', () => {
it('should quote ModelJson.jsonArray column reference properly', () => {
expect(
BoundModel.query().whereJsonIsArray('ModelJson.jsonArray').toKnexQuery().toString(),
).to.contain('"ModelJson"."jsonArray"');
});
it('should quote ModelJson.jsonArray:[10] column reference properly', () => {
expect(
BoundModel.query()
.whereJsonIsArray('ModelJson.jsonArray:[50]')
.toKnexQuery()
.toString(),
).to.contain('"ModelJson"."jsonArray"');
});
});
describe('.where(ref(fieldExpr), val())', () => {
it('should find results for jsonArray == []', () => {
return BoundModel.query()
.where('jsonArray', val([]).castJson())
.then((results) => {
expectIdsEqual(results, [2, 6, 7]);
});
});
it('should find results for jsonArray != []', () => {
return BoundModel.query()
.where('jsonArray', '!=', val([]))
.then((results) => {
expectIdsEqual(results, [1, 4, 5]);
});
});
it('should find results for jsonObject == {}', () => {
return BoundModel.query()
.where('jsonObject', val({}))
.then((results) => {
expectIdsEqual(results, [2, 4, 5]);
});
});
it('should not find results for jsonArray == {}', () => {
return BoundModel.query()
.where('jsonArray', val({}))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results for jsonObject == []', () => {
return BoundModel.query()
.where('jsonObject', val([]))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find result for jsonObject == {a: 1}', () => {
return BoundModel.query()
.where('jsonObject', val({ a: 1 }))
.then((results) => {
expectIdsEqual(results, [6]);
});
});
it('should find result for jsonObject.a == jsonObject[b]', () => {
return BoundModel.query()
.where(ref('jsonObject:a'), ref('jsonObject:[b]'))
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find result where keys are in different order jsonObject.a == {2:2, 1:1}', () => {
return BoundModel.query()
.where(ref('jsonObject:a'), val({ 2: 2, 1: 1 }))
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should not find result with wrong type as value jsonObject == {a: "1"}', () => {
return BoundModel.query()
.where('jsonObject', val({ a: '1' }))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find results jsonArray[0].arrayField[0] == { noMoreLevels: true }', () => {
return BoundModel.query()
.where(ref('jsonArray:[0].arrayField[0]'), val({ noMoreLevels: true }))
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should not find results jsonArray[0].arrayField[0] == { noMoreLevels: false }', () => {
return BoundModel.query()
.where(ref('jsonArray:[0].arrayField[0]'), val({ noMoreLevels: false }))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find result with jsonArray == [ null ]', () => {
return BoundModel.query()
.where('jsonArray', val([null]))
.then((results) => {
expectIdsEqual(results, [5]);
});
});
it('should find results with jsonArray == complexJsonObj.jsonArray', () => {
return BoundModel.query()
.where('jsonArray', val(complexJsonObj.jsonArray))
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should find results with jsonObject,jsonArray == complexJsonObj.jsonArray', () => {
return BoundModel.query()
.where(ref('jsonObject:jsonArray'), val(complexJsonObj.jsonArray))
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should not find results jsonArray == [2,1]', () => {
return BoundModel.query()
.where('jsonArray', val([2, 1]))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find results jsonArray == [1,2]', () => {
return BoundModel.query()
.where('jsonArray', val([1, 2]))
.then((results) => {
expectIdsEqual(results, [4]);
});
});
it('should find results jsonArray == [2,1] OR jsonArray == [1,2]', () => {
return BoundModel.query()
.where('jsonArray', val([1, 2]))
.orWhere('jsonArray', val([2, 1]))
.then((results) => {
expectIdsEqual(results, [4]);
});
});
it('should find results jsonArray == [1,2] OR jsonArray != [1,2]', () => {
return BoundModel.query()
.where('jsonArray', val([1, 2]))
.orWhere('jsonArray', '!=', val([2, 1]))
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should not find results jsonObject.a != jsonObject.b', () => {
return BoundModel.query()
.whereNot(ref('jsonObject:a'), ref('jsonObject:b'))
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should find all rows with jsonObject.a = jsonObject.b OR jsonObject.a != jsonObject.b', () => {
return BoundModel.query()
.where(ref('jsonObject:a'), ref('jsonObject:b'))
.orWhere(ref('jsonObject:a'), '!=', ref('jsonObject:b'))
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results jsonObject != jsonArray', () => {
return BoundModel.query()
.whereNot(ref('jsonObject'), ref('jsonArray'))
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
});
describe('.whereJsonSupersetOf(fieldExpr, )', () => {
it('should find all empty arrays with jsonArray @> []', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonArray', [])
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should find all empty arrays with jsonArray @> []', () => {
return BoundModel.query()
.where('jsonArray', '@>', val([]))
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should find results jsonArray @> [1,2] (set is its own superset)', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonArray', [1, 2])
.then((results) => {
expectIdsEqual(results, [4]);
});
});
it('should find results jsonArray @> [1,2] (set is its own superset)', () => {
return BoundModel.query()
.where('jsonArray', '@>', val([1, 2]))
.then((results) => {
expectIdsEqual(results, [4]);
});
});
it('should not find results jsonArray @> {}', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonArray', {})
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonArray @> {}', () => {
return BoundModel.query()
.where('jsonArray', '@>', val({}))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonObject @> []', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonObject', [])
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonObject @> []', () => {
return BoundModel.query()
.where('jsonObject', '@>', val([]))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find subset where both sides are references', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonObject:a', 'jsonObject:b')
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find subset where both sides are references', () => {
return BoundModel.query()
.where(ref('jsonObject:a').castJson(), '@>', ref('jsonObject:b').castJson())
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results jsonObject @> {}', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonObject', {})
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should find results jsonObject @> {}', () => {
return BoundModel.query()
.where('jsonObject', '@>', val({}))
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should find results jsonObject.objectField @> complexJsonObj.jsonObject.objectField', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonObject:objectField', complexJsonObj.jsonObject.objectField)
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should find results jsonObject.objectField @> complexJsonObj.jsonObject.objectField', () => {
return BoundModel.query()
.where(ref('jsonObject:objectField'), '@>', val(complexJsonObj.jsonObject.objectField))
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should not find results jsonObject.objectField @> complexJsonObj.jsonObject.objectField that has additional key', () => {
let obj = _.cloneDeep(complexJsonObj.jsonObject.objectField);
obj.otherKey = 'Im here too!';
return BoundModel.query()
.whereJsonSupersetOf('jsonObject:objectField', obj)
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonObject.objectField @> complexJsonObj.jsonObject.objectField that has additional key', () => {
let obj = _.cloneDeep(complexJsonObj.jsonObject.objectField);
obj.otherKey = 'Im here too!';
return BoundModel.query()
.where(ref('jsonObject:objectField'), '@>', val(obj))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonObject.objectField @> { object: "other string" }', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonObject:objectField', {
object: 'something else',
})
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonObject.objectField @> { object: "other string" }', () => {
return BoundModel.query()
.where(
ref('jsonObject:objectField'),
'@>',
val({
object: 'something else',
}),
)
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find results jsonObject @> [] OR jsonObject @> {}', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonObject', [])
.orWhereJsonSupersetOf('jsonObject', {})
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should find results jsonObject @> [] OR jsonObject @> {}', () => {
return BoundModel.query()
.where('jsonObject', '@>', val([]))
.orWhere('jsonObject', '@>', val({}))
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should not find results NOT(jsonObject.objectField @> complexJsonObj.jsonObject.objectField)', () => {
return BoundModel.query()
.whereJsonNotSupersetOf('jsonObject:objectField', complexJsonObj.jsonObject.objectField)
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should not find results NOT(jsonObject.objectField @> complexJsonObj.jsonObject.objectField)', () => {
return BoundModel.query()
.whereNot(
ref('jsonObject:objectField'),
'@>',
val(complexJsonObj.jsonObject.objectField),
)
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should not find results NOT(jsonObject.a @> jsonObject.b)', () => {
return BoundModel.query()
.whereJsonNotSupersetOf('jsonObject:a', 'jsonObject:b')
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should not find results NOT(jsonObject.a @> jsonObject.b)', () => {
return BoundModel.query()
.whereNot(ref('jsonObject:a').castJson(), '@>', ref('jsonObject:b').castJson())
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should find results jsonObject.a @> jsonObject.b OR NOT(jsonObject.a @> jsonObject.b)', () => {
return BoundModel.query()
.whereJsonSupersetOf('jsonObject:a', 'jsonObject:b')
.orWhereJsonNotSupersetOf('jsonObject:a', 'jsonObject:b')
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results jsonObject.a @> jsonObject.b OR NOT(jsonObject.a @> jsonObject.b)', () => {
return BoundModel.query()
.where(ref('jsonObject:a').castJson(), '@>', ref('jsonObject:b').castJson())
.orWhereNot(ref('jsonObject:a').castJson(), '@>', ref('jsonObject:b').castJson())
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should not find results NOT(jsonObject.x @> jsonObject.y)', () => {
return BoundModel.query()
.whereJsonNotSupersetOf('jsonObject:x', 'jsonObject:y')
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should not find results NOT(jsonObject.x @> jsonObject.y)', () => {
return BoundModel.query()
.whereNot(ref('jsonObject:x').castJson(), '@>', ref('jsonObject:y').castJson())
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should find results jsonArray = {} or NOT(jsonObject @> jsonArray)', () => {
return BoundModel.query()
.where('jsonArray', '=', val({}))
.orWhereNot('jsonObject', '@>', ref('jsonArray'))
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should get skipped if value is undefined and skipUndefined() is called', () => {
return BoundModel.query()
.skipUndefined()
.orWhereJsonSupersetOf('jsonObject', undefined)
.then((results) => {
expect(results.length).to.equal(7);
});
});
});
describe('.whereJsonSubsetOf(fieldExpr, )', () => {
it('should find all empty arrays with jsonArray <@ []', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonArray', [])
.then((results) => {
expectIdsEqual(results, [2, 6, 7]);
});
});
it('should find results jsonArray <@ [1,2] (set is its own subset)', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonArray', [1, 2])
.then((results) => {
expectIdsEqual(results, [2, 4, 6, 7]);
});
});
it('should not find results jsonArray <@ {}', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonArray', {})
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonArray <@ {}', () => {
return BoundModel.query()
.where('jsonArray', '<@', val({}))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonObject <@ []', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonObject', [])
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonObject <@ []', () => {
return BoundModel.query()
.where('jsonObject', '<@', val([]))
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find subset where both sides are references', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonObject:a', 'jsonObject:b')
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find subset where both sides are references', () => {
return BoundModel.query()
.where(ref('jsonObject:a').castJson(), '<@', ref('jsonObject:b').castJson())
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results jsonObject <@ {}', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonObject', {})
.then((results) => {
expectIdsEqual(results, [2, 4, 5]);
});
});
it('should find results jsonObject <@ {}', () => {
return BoundModel.query()
.where('jsonObject', '<@', val({}))
.then((results) => {
expectIdsEqual(results, [2, 4, 5]);
});
});
it('should find results jsonObject.objectField <@ complexJsonObj.jsonObject.objectField', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonObject:objectField', complexJsonObj.jsonObject.objectField)
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should find results jsonObject.objectField <@ complexJsonObj.jsonObject.objectField', () => {
return BoundModel.query()
.where(ref('jsonObject:objectField'), '<@', val(complexJsonObj.jsonObject.objectField))
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should find results jsonObject.objectField <@ complexJsonObj.jsonObject.objectField that has additional key', () => {
let obj = _.cloneDeep(complexJsonObj.jsonObject.objectField);
obj.otherKey = 'Im here too!';
return BoundModel.query()
.whereJsonSubsetOf('jsonObject:objectField', obj)
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should find results jsonObject.objectField <@ complexJsonObj.jsonObject.objectField that has additional key', () => {
let obj = _.cloneDeep(complexJsonObj.jsonObject.objectField);
obj.otherKey = 'Im here too!';
return BoundModel.query()
.where(ref('jsonObject:objectField'), '<@', val(obj))
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should not find results jsonObject.objectField <@ { object: "other string" }', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonObject:objectField', {
object: 'something else',
})
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should not find results jsonObject.objectField <@ { object: "other string" }', () => {
return BoundModel.query()
.where(
ref('jsonObject:objectField'),
'<@',
val({
object: 'something else',
}),
)
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find results jsonObject <@ {} OR jsonArray <@ []', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonObject', {})
.orWhereJsonSubsetOf('jsonArray', [])
.then((results) => {
expectIdsEqual(results, [2, 4, 5, 6, 7]);
});
});
it('should find results jsonObject <@ {} OR jsonArray <@ []', () => {
return BoundModel.query()
.where('jsonObject', '<@', val({}))
.orWhere('jsonArray', '<@', val([]))
.then((results) => {
expectIdsEqual(results, [2, 4, 5, 6, 7]);
});
});
it('should find results NOT(jsonObject.a <@ jsonObject.b)', () => {
return BoundModel.query()
.whereJsonNotSubsetOf('jsonObject:a', 'jsonObject:b')
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should find results NOT(jsonObject.a <@ jsonObject.b)', () => {
return BoundModel.query()
.whereNot(ref('jsonObject:a').castJson(), '<@', ref('jsonObject:b').castJson())
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should find results jsonObject.a <@ jsonObject.b OR NOT(jsonObject.a <@ jsonObject.b)', () => {
return BoundModel.query()
.whereJsonSubsetOf('jsonObject:a', 'jsonObject:b')
.orWhereJsonNotSubsetOf('jsonObject:a', 'jsonObject:b')
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results jsonObject.a <@ jsonObject.b OR NOT(jsonObject.a <@ jsonObject.b)', () => {
return BoundModel.query()
.where(ref('jsonObject:a').castJson(), '<@', ref('jsonObject:b').castJson())
.orWhereNot(ref('jsonObject:a').castJson(), '<@', ref('jsonObject:b').castJson())
.then((results) => {
expectIdsEqual(results, [7]);
});
});
});
describe('.whereJsonIsArray(fieldExpr)', () => {
it('should find all arrays that has an array in index 5', () => {
return BoundModel.query()
.whereJsonIsArray('jsonArray:[5]')
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should find no arrays from object type of field arrays', () => {
return BoundModel.query()
.whereJsonIsArray('jsonObject:objectField')
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find objects with orWhereJsonIsArray', () => {
return BoundModel.query()
.whereJsonIsArray('jsonObject')
.orWhereJsonIsArray('jsonArray')
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should add parenthesis for find arrays with whereJsonNotArray()', () => {
return BoundModel.query()
.whereJsonNotArray('jsonObject')
.whereJsonIsObject('jsonObject')
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should add parenthesis for find arrays with orWhereJsonNotArray()', () => {
return BoundModel.query()
.whereJsonIsObject('jsonArray')
.orWhereJsonNotArray('jsonObject')
.whereJsonIsObject('jsonObject')
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should find all rows with orWhereJsonNotArray(jsonObject)', () => {
return BoundModel.query()
.whereJsonIsObject('jsonArray')
.orWhereJsonNotArray('jsonObject')
.then((results) => {
expectIdsEqual(results, [1, 2, 3, 4, 5, 6, 7]);
});
});
});
describe('.whereJsonIsObject(fieldExpr)', () => {
it('should find first object', () => {
return BoundModel.query()
.whereJsonIsObject('jsonObject:objectField')
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should find nothing for array field', () => {
return BoundModel.query()
.whereJsonIsObject('jsonObject:arrayField')
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find nothing for non existing field', () => {
return BoundModel.query()
.whereJsonIsObject('jsonObject:arrayField.imNot')
.then((results) => {
expect(results).to.have.length(0);
});
});
it('should find objects with orWhereJsonIsObject', () => {
return BoundModel.query()
.whereJsonIsObject('jsonArray')
.orWhereJsonIsObject('jsonObject:objectField')
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should add parenthesis for find objects with whereJsonNotObject(jsonArray)', () => {
return BoundModel.query()
.whereJsonNotObject('jsonArray')
.whereJsonIsArray('jsonArray')
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should add parenthesis for find objects with orWhereJsonNotObject(jsonArray)', () => {
return BoundModel.query()
.whereJsonIsArray('jsonObject')
.orWhereJsonNotObject('jsonArray')
.whereJsonIsArray('jsonArray')
.then((results) => {
expectIdsEqual(results, [1, 2, 4, 5, 6, 7]);
});
});
it('should find all rows with orWhereJsonNotObject(jsonArray)', () => {
return BoundModel.query()
.whereJsonIsArray('jsonObject')
.orWhereJsonNotObject('jsonArray')
.then((results) => {
expectIdsEqual(results, [1, 2, 3, 4, 5, 6, 7]);
});
});
});
describe('.whereJsonHasAny(fieldExpr, keys) and .whereJsonHasAll(fieldExpr, keys)', () => {
it('should throw error if null in input array', (done) => {
BoundModel.query()
.whereJsonHasAny('jsonObject', [null])
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
done();
})
.catch(done);
});
it('should throw error if number in input array', (done) => {
BoundModel.query()
.whereJsonHasAny('jsonObject', 1)
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
done();
})
.catch(done);
});
it('should throw error if boolean in input array', (done) => {
BoundModel.query()
.whereJsonHasAny('jsonObject', false)
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
done();
})
.catch(done);
});
it('should find results for a', () => {
return BoundModel.query()
.whereJsonHasAny('jsonObject', 'a')
.then((results) => {
expectIdsEqual(results, [6, 7]);
});
});
it('should find results for a', () => {
// TODO knex doesn't support ?| operator.
return BoundModel.query()
.where(raw('?? \\?| ?', ['jsonObject', val('a').asArray()]))
.then((results) => {
expectIdsEqual(results, [6, 7]);
});
});
it('should find results for a or b', () => {
return BoundModel.query()
.whereJsonHasAny('jsonObject', 'b')
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results for b or notMe', () => {
return BoundModel.query()
.whereJsonHasAny('jsonObject', ['b', 'notMe'])
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results for hasAny(notMe) orHasAny(b)', () => {
return BoundModel.query()
.whereJsonHasAny('jsonObject', 'notMe')
.orWhereJsonHasAny('jsonObject', 'b')
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results for a and b', () => {
return BoundModel.query()
.whereJsonHasAll('jsonObject', ['a', 'b'])
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results for a and b', () => {
// TODO knex doesn't support ?& operator.
return BoundModel.query()
.where(raw('?? \\?& ?', ['jsonObject', val(['a', 'b']).asArray()]))
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results for hasAll(notMe) orHasAll([a, b])', () => {
return BoundModel.query()
.whereJsonHasAll('jsonObject', 'notMe')
.orWhereJsonHasAll('jsonObject', ['a', 'b'])
.then((results) => {
expectIdsEqual(results, [7]);
});
});
it('should find results for string in array "string in jsonArray[3]"', () => {
return BoundModel.query()
.whereJsonHasAny('jsonArray', 'string in jsonArray[3]')
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should work with range', () => {
return BoundModel.query()
.range(0, 1)
.whereJsonHasAny('jsonObject:b', '2')
.then((result) => {
expect(result.results).to.have.length(1);
expect(result.total).to.equal(1);
expect(result.results[0].id).to.equal(7);
});
});
});
describe('.where(fieldExpr, operator, value)', () => {
it('should be able to find numbers with >', () => {
return BoundModel.query()
.where(ref('jsonObject:numberField'), '>', 1.4)
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should be able to find numbers with >', () => {
return BoundModel.query()
.where(ref('jsonObject:numberField').castFloat(), '>', val(1.4).castFloat())
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should not find where 1.5 < 1.5', () => {
return BoundModel.query()
.where(ref('jsonObject:numberField'), '<', 1.5)
.then((results) => {
expectIdsEqual(results, []);
});
});
it('should be able to find strings with =', () => {
return BoundModel.query()
.where(
ref('jsonObject:stringField').castText(),
'=',
'string in jsonObject.stringField',
)
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should be able to find strings with =', () => {
return BoundModel.query()
.where(
ref('jsonObject:stringField').castText(),
'=',
val('string in jsonObject.stringField').castText(),
)
.then((results) => {
expectIdsEqual(results, [1]);
});
});
it('should be able to find every but first row where booleanField equals true or is NULL', () => {
return BoundModel.query()
.where(ref('jsonObject:booleanField'), '=', true)
.orWhere(ref('jsonObject:booleanField'), 'IS', null)
.then((results) => {
expectIdsEqual(results, [2, 3, 4, 5, 6, 7]);
});
});
it('should be able to find every but first row where booleanField equals true or is NULL', () => {
return BoundModel.query()
.where(ref('jsonObject:booleanField').castBool(), '=', val(true).castBool())
.orWhere(ref('jsonObject:booleanField'), 'IS', null)
.then((results) => {
expectIdsEqual(results, [2, 3, 4, 5, 6, 7]);
});
});
});
});
});
};
================================================
FILE: tests/integration/jsonRelations.js
================================================
const { Model, ref } = require('../../');
const find = require('lodash/find');
const expect = require('expect.js');
const sortBy = require('lodash/sortBy');
module.exports = (session) => {
describe('JSON relations', () => {
class BaseModel extends Model {
static get modifiers() {
return ['name', 'id', 'json'].reduce((obj, prop) => {
obj[prop] = (qb) => qb.select(prop);
return obj;
}, {});
}
}
class Person extends BaseModel {
static get tableName() {
return 'Person';
}
static get relationMappings() {
return {
favoritePet: {
relation: Model.BelongsToOneRelation,
modelClass: Animal,
join: {
from: ref('Person.json:stuff.favoritePetId').castInt(),
to: 'Animal.id',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'Person.id',
through: {
from: ref('PersonMovie.json:personId').castInt(),
to: ref('PersonMovie.json:movieId').castInt(),
},
to: 'Movie.id',
},
},
};
}
}
class Animal extends BaseModel {
static get tableName() {
return 'Animal';
}
static get relationMappings() {
return {
peopleWhoseFavoriteIAm: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'Animal.id',
to: ref('Person.json:stuff.favoritePetId').castInt(),
},
},
favoritePerson: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: ref('Animal.json:favoritePersonName').castText(),
to: 'Person.name',
},
},
};
}
}
class Movie extends BaseModel {
static get tableName() {
return 'Movie';
}
}
before(() => {
Person.knex(session.knex);
Animal.knex(session.knex);
Movie.knex(session.knex);
});
before(() => {
return session.knex.schema
.dropTableIfExists('PersonMovie')
.dropTableIfExists('Movie')
.dropTableIfExists('Animal')
.dropTableIfExists('Person')
.createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
table.jsonb('json');
})
.createTable('Animal', (table) => {
table.increments('id').primary();
table.string('name');
table.jsonb('json');
})
.createTable('Movie', (table) => {
table.increments('id').primary();
table.string('name');
table.jsonb('json');
})
.createTable('PersonMovie', (table) => {
table.jsonb('json');
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('PersonMovie')
.dropTableIfExists('Movie')
.dropTableIfExists('Animal')
.dropTableIfExists('Person');
});
beforeEach(() => {
return Animal.query()
.delete()
.then(() => Movie.query().delete())
.then(() => session.knex('PersonMovie').delete())
.then(() => Person.query().delete());
});
beforeEach(() => {
return Person.query().insertGraph([
{
name: 'Arnold',
favoritePet: {
name: 'Fluffy',
json: {
favoritePersonName: 'Brad',
},
},
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
},
{
name: 'Brad',
favoritePet: {
name: 'Cato',
},
movies: [
{
name: 'Inglorious bastards',
},
],
},
]);
});
describe('eager', () => {
it('eager', () => {
return Person.query()
.findOne({ 'Person.name': 'Arnold' })
.select('Person.name')
.withGraphFetched(
`[
movies(name),
favoritePet(name).[
peopleWhoseFavoriteIAm(name),
favoritePerson(name),
]
]`,
)
.then(sortRelations)
.then((person) => {
expect(person).to.eql({
name: 'Arnold',
favoritePet: {
name: 'Fluffy',
peopleWhoseFavoriteIAm: [
{
name: 'Arnold',
},
],
favoritePerson: {
name: 'Brad',
},
},
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
});
});
it('joinEager', () => {
return Person.query()
.findOne({ 'Person.name': 'Arnold' })
.select('Person.name')
.withGraphJoined(
`[
movies(name),
favoritePet(name).[
peopleWhoseFavoriteIAm(name),
favoritePerson(name),
]
]`,
)
.then(sortRelations)
.then((person) => {
expect(person).to.eql({
name: 'Arnold',
favoritePet: {
name: 'Fluffy',
peopleWhoseFavoriteIAm: [
{
name: 'Arnold',
},
],
favoritePerson: {
name: 'Brad',
},
},
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
});
});
});
describe('$relatedQuery', () => {
describe('belongs to one relation', () => {
it('insert', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => it.$relatedQuery('favoritePet').insert({ name: 'Doggo' }))
.then(() => Person.query().findOne({ name: 'Arnold' }).withGraphFetched('favoritePet'))
.then((person) => {
expect(person.json.stuff.favoritePetId).to.equal(person.favoritePet.id);
});
});
it('find', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => it.$relatedQuery('favoritePet').select('name'))
.then((pet) => {
expect(pet).to.eql({
name: 'Fluffy',
});
});
});
it('patch', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => it.$relatedQuery('favoritePet').patch({ json: { updated: true } }))
.then(() => Animal.query().select('json', 'name').orderBy('name'))
.then((pets) => {
expect(pets).to.eql([
{
json: null,
name: 'Cato',
},
{
json: {
updated: true,
},
name: 'Fluffy',
},
]);
});
});
it('relate', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => it.$relatedQuery('favoritePet').relate(123))
.then(() => Person.query().withGraphFetched('favoritePet').orderBy('name'))
.then((people) => {
const brad = find(people, { name: 'Brad' });
const ardnold = find(people, { name: 'Arnold' });
expect(ardnold.json.stuff.favoritePetId).to.equal(123);
expect(brad.json.stuff.favoritePetId).to.equal(brad.favoritePet.id);
});
});
it('unrelate', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => it.$relatedQuery('favoritePet').unrelate())
.then(() => Person.query().withGraphFetched('favoritePet').orderBy('name'))
.then((people) => {
const brad = find(people, { name: 'Brad' });
const ardnold = find(people, { name: 'Arnold' });
expect(ardnold.json.stuff.favoritePetId).to.equal(null);
expect(brad.json.stuff.favoritePetId).to.equal(brad.favoritePet.id);
});
});
});
describe('has many relation', () => {
it('insert', () => {
return Animal.query()
.findOne({ name: 'Fluffy' })
.then((it) => it.$relatedQuery('peopleWhoseFavoriteIAm').insert({ name: 'Jorge' }))
.then(() =>
Animal.query()
.findOne({ name: 'Fluffy' })
.withGraphFetched('peopleWhoseFavoriteIAm(name)')
.select('name'),
)
.then(sortRelations)
.then((pet) => {
expect(pet).to.eql({
name: 'Fluffy',
peopleWhoseFavoriteIAm: [
{
name: 'Arnold',
},
{
name: 'Jorge',
},
],
});
});
});
it('find', () => {
return Animal.query()
.findOne({ name: 'Fluffy' })
.then((it) => it.$relatedQuery('peopleWhoseFavoriteIAm').select('name'))
.then((pet) => {
expect(pet).to.eql([
{
name: 'Arnold',
},
]);
});
});
it('patch', () => {
return Animal.query()
.findOne({ name: 'Fluffy' })
.then((it) =>
it.$relatedQuery('peopleWhoseFavoriteIAm').patch({ name: 'Arnold the second' }),
)
.then(() => Person.query().select('name').orderBy('name'))
.then((pet) => {
expect(pet).to.eql([
{
name: 'Arnold the second',
},
{
name: 'Brad',
},
]);
});
});
it('relate', () => {
return Animal.query()
.findOne({ name: 'Fluffy' })
.then((it) => Promise.all([it, Person.query().findOne({ name: 'Brad' })]))
.then(([fluffy, brad]) => fluffy.$relatedQuery('peopleWhoseFavoriteIAm').relate(brad))
.then(() =>
Animal.query()
.findOne({ name: 'Fluffy' })
.withGraphFetched('peopleWhoseFavoriteIAm(name)'),
)
.then(sortRelations)
.then((pet) => {
expect(pet.peopleWhoseFavoriteIAm).to.eql([
{
name: 'Arnold',
},
{
name: 'Brad',
},
]);
});
});
it('unrelate', () => {
return Animal.query()
.findOne({ name: 'Fluffy' })
.then((it) => Promise.all([it, Person.query().findOne({ name: 'Brad' })]))
.then(([fluffy, brad]) =>
fluffy
.$relatedQuery('peopleWhoseFavoriteIAm')
.relate(brad)
.then(() => fluffy),
)
.then((it) =>
it.$relatedQuery('peopleWhoseFavoriteIAm').unrelate().where('name', 'Arnold'),
)
.then(() =>
Animal.query()
.findOne({ name: 'Fluffy' })
.withGraphFetched('peopleWhoseFavoriteIAm(name)'),
)
.then(sortRelations)
.then((pet) => {
expect(pet.peopleWhoseFavoriteIAm).to.eql([
{
name: 'Brad',
},
]);
return Person.query().findOne({ name: 'Arnold' }).select('json');
})
.then((arnold) => {
expect(arnold.json.stuff.favoritePetId).to.equal(null);
});
});
});
describe('many to many relation', () => {
it('insert', () => {
return Person.query()
.findOne({ name: 'Brad' })
.then((it) => it.$relatedQuery('movies').insert({ name: 'Seven years in Tibet' }))
.then(() =>
Person.query()
.findOne({ name: 'Brad' })
.withGraphFetched('movies(name)')
.select('name'),
)
.then(sortRelations)
.then((pet) => {
expect(pet).to.eql({
name: 'Brad',
movies: [
{
name: 'Inglorious bastards',
},
{
name: 'Seven years in Tibet',
},
],
});
});
});
it('find', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => it.$relatedQuery('movies').select('name').orderBy('name'))
.then((movies) => {
expect(movies).to.eql([
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
]);
});
});
it('patch', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => it.$relatedQuery('movies').patch({ name: 'Some terminator' }))
.then(() => Movie.query().select('name').orderBy('name'))
.then((movies) => {
expect(movies).to.eql([
{
name: 'Inglorious bastards',
},
{
name: 'Some terminator',
},
{
name: 'Some terminator',
},
]);
});
});
it('relate', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => Promise.all([it, Movie.query().findOne({ name: 'Inglorious bastards' })]))
.then(([arnold, bastards]) => arnold.$relatedQuery('movies').relate(bastards.id))
.then(() =>
Person.query()
.select('name')
.findOne({ name: 'Arnold' })
.withGraphFetched('movies(name)'),
)
.then(sortRelations)
.then((person) => {
expect(person.movies).to.eql([
{
name: 'Inglorious bastards',
},
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
]);
});
});
it('unrelate', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((it) => Promise.all([it, Movie.query().findOne({ name: 'Inglorious bastards' })]))
.then(([arnold, bastards]) =>
arnold
.$relatedQuery('movies')
.relate(bastards.id)
.then(() => arnold),
)
.then((arnold) => arnold.$relatedQuery('movies').unrelate().where('name', 'Terminator'))
.then(() =>
Person.query()
.select('name')
.findOne({ name: 'Arnold' })
.withGraphFetched('movies(name)'),
)
.then(sortRelations)
.then((person) => {
expect(person.movies).to.eql([
{
name: 'Inglorious bastards',
},
{
name: 'Terminator 2',
},
]);
});
});
});
});
function sortRelations(obj) {
if (obj instanceof Person) {
obj.movies = sortBy(obj.movies, 'name');
}
if (obj instanceof Animal) {
obj.peopleWhoseFavoriteIAm = sortBy(obj.peopleWhoseFavoriteIAm, 'name');
}
return obj;
}
});
};
================================================
FILE: tests/integration/knexIdentifierMapping.js
================================================
const Knex = require('knex');
const Promise = require('bluebird');
const { expect } = require('chai');
const { Model, knexIdentifierMapping } = require('../../');
module.exports = (session) => {
describe('knexIdentifierMapping', () => {
let knex;
class Person extends Model {
static get tableName() {
return 'person';
}
static get relationMappings() {
return {
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'person.parentId',
to: 'person.id',
},
},
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'person.id',
to: 'animal.ownerId',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'person.id',
through: {
from: 'personMovie.personId',
to: 'personMovie.movieId',
},
to: 'movie.id',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'animal';
}
}
class Movie extends Model {
static get tableName() {
return 'movie';
}
}
before(() => {
// Create schema with the knex instance that doesn't
// have identifier mapping configured.
return session.knex.schema
.dropTableIfExists('person_movie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person')
.createTable('person', (table) => {
table.increments('id').primary();
table.string('first_name');
table.integer('parent_id');
})
.createTable('animal', (table) => {
table.increments('id').primary();
table.string('animal_name');
table.integer('owner_id');
})
.createTable('movie', (table) => {
table.increments('id').primary();
table.string('movie_name');
})
.createTable('person_movie', (table) => {
table.integer('person_id');
table.integer('movie_id');
});
});
before(() => {
const config = Object.assign(
{},
session.opt.knexConfig,
knexIdentifierMapping({
person_movie: 'personMovie',
first_name: 'fName',
parent_id: 'parentId',
owner_id: 'ownerId',
movie_name: 'movieName',
person_id: 'personId',
movie_id: 'movieId',
snake_case_test_table: 'snakeCaseTestTable',
animal_name: 'animalName',
}),
);
knex = Knex(config);
});
describe('schema', () => {
const table = 'snakeCaseTestTable';
before(() => {
return knex.schema.dropTableIfExists(table);
});
afterEach(() => {
return knex.schema.dropTableIfExists(table);
});
after(() => {
return knex.schema.dropTableIfExists(table);
});
it('createTable', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
table.string('fName');
})
.then(() => {
return knex(table).insert({ id: 1, fName: 'fooBar' });
})
.then(() => {
return knex(table);
})
.then((rows) => {
expect(rows).to.eql([{ id: 1, fName: 'fooBar' }]);
// Query with a knex without case mapping.
return session.knex('snake_case_test_table');
})
.then((rows) => {
expect(rows).to.eql([{ id: 1, first_name: 'fooBar' }]);
});
});
it('dropTable', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
})
.dropTableIfExists(table);
});
it('hasTable (true)', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
})
.hasTable(table)
.then((hasTable) => {
expect(!!hasTable).to.equal(true);
});
});
it('hasTable (false)', () => {
return knex.schema.hasTable(table).then((hasTable) => {
expect(hasTable).to.equal(false);
});
});
});
describe('queries', () => {
beforeEach(() => {
return Person.query(knex).insertGraph({
fName: 'Seppo',
parent: {
fName: 'Teppo',
parent: {
fName: 'Matti',
},
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit the movie',
},
{
movieName: 'Salkkarit 2, the low quality continues',
},
],
});
});
afterEach(() => {
return ['animal', 'personMovie', 'movie', 'person'].reduce((promise, table) => {
return promise.then(() => knex(table).delete());
}, Promise.resolve());
});
it('$relatedQuery', () => {
return Person.query(knex)
.findOne({ fName: 'Seppo' })
.then((model) => {
return model.$relatedQuery('pets', knex).orderBy('animalName');
})
.then((pets) => {
expect(pets).to.containSubset([
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
]);
});
});
['withGraphFetched', 'withGraphJoined'].forEach((method) => {
it(`eager (${method})`, () => {
return Person.query(knex)
.select('person.fName as rootFirstName')
.modifyGraph('parent', (qb) => qb.select('fName as parentFirstName'))
.modifyGraph('parent.parent', (qb) => qb.select('fName as grandParentFirstName'))
[method]('[parent.parent, pets, movies]')
.orderBy('person.fName')
.then((people) => {
expect(people.length).to.equal(3);
expect(people).to.containSubset([
{
rootFirstName: 'Seppo',
parent: {
parentFirstName: 'Teppo',
parent: {
grandParentFirstName: 'Matti',
},
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit 2, the low quality continues',
},
{
movieName: 'Salkkarit the movie',
},
],
},
{
rootFirstName: 'Teppo',
parent: {
parentFirstName: 'Matti',
},
},
{
rootFirstName: 'Matti',
},
]);
});
});
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('person_movie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person');
});
after(() => {
return knex.destroy();
});
});
};
================================================
FILE: tests/integration/knexSnakeCase.js
================================================
const Knex = require('knex');
const Promise = require('bluebird');
const { Model, knexSnakeCaseMappers } = require('../../');
const { expect } = require('chai');
module.exports = (session) => {
describe('knexSnakeCaseMappers', () => {
let knex;
class Person extends Model {
static get tableName() {
return 'person';
}
static get jsonAttributes() {
return ['address'];
}
static get relationMappings() {
return {
parentPerson: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'person.parentId',
to: 'person.id',
},
},
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'person.id',
to: 'animal.ownerId',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'person.id',
through: {
from: 'personMovie.personId',
to: 'personMovie.movieId',
},
to: 'movie.id',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'animal';
}
}
class Movie extends Model {
static get tableName() {
return 'movie';
}
}
before(() => {
// Create schema with the knex instance that doesn't
// have identifier mapping configured.
return session.knex.schema
.dropTableIfExists('person_movie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person')
.createTable('person', (table) => {
table.increments('id').primary();
table.string('first_name');
table.integer('parent_id');
if (session.isPostgres()) {
table.jsonb('person_address');
}
})
.createTable('animal', (table) => {
table.increments('id').primary();
table.string('animal_name');
table.integer('owner_id');
})
.createTable('movie', (table) => {
table.increments('id').primary();
table.string('movie_name');
})
.createTable('person_movie', (table) => {
table.integer('person_id');
table.integer('movie_id');
});
});
before(() => {
const config = Object.assign({}, session.opt.knexConfig, knexSnakeCaseMappers());
knex = Knex(config);
});
describe('schema', () => {
const table = 'snakeCaseTestTable';
before(() => {
return knex.schema.dropTableIfExists(table);
});
afterEach(() => {
return knex.schema.dropTableIfExists(table);
});
after(() => {
return knex.schema.dropTableIfExists(table);
});
it('createTable', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
table.string('firstName');
})
.then(() => {
return knex(table).insert({ id: 1, firstName: 'fooBar' });
})
.then(() => {
return knex(table);
})
.then((rows) => {
expect(rows).to.eql([{ id: 1, firstName: 'fooBar' }]);
// Query with a knex without case mapping.
return session.knex('snake_case_test_table');
})
.then((rows) => {
expect(rows).to.eql([{ id: 1, first_name: 'fooBar' }]);
});
});
if (session.isPostgres()) {
it('alter', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
table.string('firstName');
})
.then(() => {
return knex.schema.table(table, (table) => {
table.text('firstName').alter();
});
});
});
}
it('dropTable', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
})
.dropTableIfExists(table);
});
it('hasTable (true)', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
})
.hasTable(table)
.then((hasTable) => {
expect(!!hasTable).to.equal(true);
});
});
it('hasTable (false)', () => {
return knex.schema.hasTable(table).then((hasTable) => {
expect(hasTable).to.equal(false);
});
});
// test for the error: 2192
// TypeError: Cannot read properties of undefined (reading 'lastIndexOf')
it('createTable with empty constraintName', () => {
return knex.schema
.dropTableIfExists('emptyConstraintName')
.createTable('emptyConstraintName', (table) => {
table.integer('id').primary();
});
});
});
describe('queries', () => {
beforeEach(() => {
function maybeWithAddress(obj, address) {
if (session.isPostgres()) {
obj.personAddress = address;
}
return obj;
}
return Person.query(knex).insertGraph({
firstName: 'Seppo',
parentPerson: {
firstName: 'Teppo',
parentPerson: maybeWithAddress(
{
firstName: 'Matti',
},
{
personCity: 'Jalasjärvi',
cityCoordinates: {
latitudeCoordinate: 61,
longitudeCoordinate: 23,
},
},
),
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit the movie',
},
{
movieName: 'Salkkarit 2, the low quality continues',
},
],
});
});
afterEach(() => {
return ['animal', 'personMovie', 'movie', 'person'].reduce((promise, table) => {
return promise.then(() => knex(table).delete());
}, Promise.resolve());
});
if (session.isPostgres()) {
it('returning', () => {
return Person.query(knex)
.insert({ firstName: 'Arto' })
.returning('*')
.then((res) => {
expect(res).to.containSubset({ firstName: 'Arto', parentId: null });
});
});
}
it('joinRelated', () => {
return Person.query(knex)
.joinRelated('parentPerson.parentPerson')
.select('parentPerson:parentPerson.firstName as nestedRef')
.then((result) => {
expect(result).to.containSubset([{ nestedRef: 'Matti' }]);
});
});
if (session.isPostgres()) {
it('update with json references', () => {
return Person.query(knex)
.where('firstName', 'Matti')
.patch({
'personAddress:cityCoordinates.latitudeCoordinate': 30,
})
.returning('*')
.then((result) => {
expect(result).to.containSubset([
{
firstName: 'Matti',
parentId: null,
personAddress: {
personCity: 'Jalasjärvi',
cityCoordinates: {
latitudeCoordinate: 30,
longitudeCoordinate: 23,
},
},
},
]);
});
});
}
it('$relatedQuery', () => {
return Person.query(knex)
.findOne({ firstName: 'Seppo' })
.then((model) => {
return model.$relatedQuery('pets', knex).orderBy('animalName');
})
.then((pets) => {
expect(pets).to.containSubset([
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
]);
});
});
['withGraphFetched', 'withGraphJoined'].forEach((method) => {
it(`eager (${method})`, () => {
return Person.query(knex)
.select('person.firstName as rootFirstName')
.modifyGraph('parentPerson', (qb) => qb.select('firstName as parentFirstName'))
.modifyGraph('parentPerson.parentPerson', (qb) =>
qb.select('firstName as grandParentFirstName'),
)
[method]('[parentPerson.parentPerson, pets, movies]')
.orderBy('person.firstName')
.then((people) => {
expect(people.length).to.equal(3);
expect(people).to.containSubset([
{
rootFirstName: 'Seppo',
parentPerson: {
parentFirstName: 'Teppo',
parentPerson: {
grandParentFirstName: 'Matti',
},
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit 2, the low quality continues',
},
{
movieName: 'Salkkarit the movie',
},
],
},
{
rootFirstName: 'Teppo',
parentPerson: {
parentFirstName: 'Matti',
},
},
{
rootFirstName: 'Matti',
},
]);
});
});
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('person_movie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person');
});
after(() => {
return knex.destroy();
});
});
describe('knexSnakeCaseMappers uppercase = true', () => {
let knex;
class Person extends Model {
static get tableName() {
return 'person';
}
static get relationMappings() {
return {
parentPerson: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'person.parentId',
to: 'person.id',
},
},
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'person.id',
to: 'animal.ownerId',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'person.id',
through: {
from: 'personMovie.personId',
to: 'personMovie.movieId',
},
to: 'movie.id',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'animal';
}
}
class Movie extends Model {
static get tableName() {
return 'movie';
}
}
before(() => {
// Create schema with the knex instance that doesn't
// have identifier mapping configured.
return session.knex.schema
.dropTableIfExists('PERSON_MOVIE')
.dropTableIfExists('ANIMAL')
.dropTableIfExists('MOVIE')
.dropTableIfExists('PERSON')
.createTable('PERSON', (table) => {
table.increments('ID').primary();
table.string('FIRST_NAME');
table.integer('PARENT_ID');
})
.createTable('ANIMAL', (table) => {
table.increments('ID').primary();
table.string('ANIMAL_NAME');
table.integer('OWNER_ID');
})
.createTable('MOVIE', (table) => {
table.increments('ID').primary();
table.string('MOVIE_NAME');
})
.createTable('PERSON_MOVIE', (table) => {
table.integer('PERSON_ID');
table.integer('MOVIE_ID');
});
});
before(() => {
const config = Object.assign(
{},
session.opt.knexConfig,
knexSnakeCaseMappers({ upperCase: true }),
);
knex = Knex(config);
});
describe('schema', () => {
const table = 'snakeCaseTestTable';
before(() => {
return knex.schema.dropTableIfExists(table);
});
afterEach(() => {
return knex.schema.dropTableIfExists(table);
});
after(() => {
return knex.schema.dropTableIfExists(table);
});
it('createTable', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
table.string('firstName');
})
.then(() => {
return knex(table).insert({ id: 1, firstName: 'fooBar' });
})
.then(() => {
return knex(table);
})
.then((rows) => {
expect(rows).to.eql([{ id: 1, firstName: 'fooBar' }]);
// Query with a knex without case mapping.
return session.knex('SNAKE_CASE_TEST_TABLE');
})
.then((rows) => {
expect(rows).to.eql([{ ID: 1, FIRST_NAME: 'fooBar' }]);
});
});
if (session.isPostgres()) {
it('alter', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
table.string('firstName');
})
.then(() => {
return knex.schema.table(table, (table) => {
table.text('firstName').alter();
});
});
});
}
it('dropTable', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
})
.dropTableIfExists(table);
});
it('hasTable (true)', () => {
return knex.schema
.createTable(table, (table) => {
table.increments('id');
})
.hasTable(table)
.then((hasTable) => {
expect(!!hasTable).to.equal(true);
});
});
it('hasTable (false)', () => {
return knex.schema.hasTable(table).then((hasTable) => {
expect(hasTable).to.equal(false);
});
});
});
describe('queries', () => {
beforeEach(() => {
return Person.query(knex).insertGraph({
firstName: 'Seppo',
parentPerson: {
firstName: 'Teppo',
parentPerson: {
firstName: 'Matti',
},
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit the movie',
},
{
movieName: 'Salkkarit 2, the low quality continues',
},
],
});
});
afterEach(() => {
return ['animal', 'personMovie', 'movie', 'person'].reduce((promise, table) => {
return promise.then(() => knex(table).delete());
}, Promise.resolve());
});
it('$relatedQuery', () => {
return Person.query(knex)
.findOne({ firstName: 'Seppo' })
.then((model) => {
return model.$relatedQuery('pets', knex).orderBy('animalName');
})
.then((pets) => {
expect(pets).to.containSubset([
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
]);
});
});
['withGraphFetched', 'withGraphJoined'].forEach((method) => {
it(`eager (${method})`, () => {
return Person.query(knex)
.select('person.firstName as rootFirstName')
.modifyGraph('parentPerson', (qb) => qb.select('firstName as parentFirstName'))
.modifyGraph('parentPerson.parentPerson', (qb) =>
qb.select('firstName as grandParentFirstName'),
)
[method]('[parentPerson.parentPerson, pets, movies]')
.orderBy('person.firstName')
.then((people) => {
expect(people.length).to.equal(3);
expect(people).to.containSubset([
{
rootFirstName: 'Seppo',
parentPerson: {
parentFirstName: 'Teppo',
parentPerson: {
grandParentFirstName: 'Matti',
},
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit 2, the low quality continues',
},
{
movieName: 'Salkkarit the movie',
},
],
},
{
rootFirstName: 'Teppo',
parentPerson: {
parentFirstName: 'Matti',
},
},
{
rootFirstName: 'Matti',
},
]);
});
});
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('person_movie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person');
});
after(() => {
return knex.destroy();
});
});
};
================================================
FILE: tests/integration/misc/#1074.js
================================================
const { Model } = require('../../../');
const { expect } = require('chai');
module.exports = (session) => {
describe(`selects in relationMapping filters don't work #1074`, () => {
let knex = session.knex;
let Person;
before(() => {
return knex.schema
.dropTableIfExists('cousins')
.dropTableIfExists('persons')
.createTable('persons', (table) => {
table.increments('id').primary();
table.integer('parentId');
table.string('name');
})
.createTable('cousins', (table) => {
table.integer('id1');
table.integer('id2');
});
});
after(() => {
return knex.schema.dropTableIfExists('cousins').dropTableIfExists('persons');
});
before(() => {
Person = class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
modify: (builder) => builder.select('name'),
join: {
from: 'persons.parentId',
to: 'persons.id',
},
},
cousins: {
relation: Model.ManyToManyRelation,
modelClass: Person,
modify: (builder) => builder.select('name'),
join: {
from: 'persons.id',
through: {
from: 'cousins.id1',
to: 'cousins.id2',
},
to: 'persons.id',
},
},
};
}
};
Person.knex(knex);
});
beforeEach(() => Person.query().delete());
beforeEach(() => {
return Person.query().insertGraph({
name: 'Matti',
parent: {
name: 'Teppo',
},
cousins: [
{
name: 'Seppo',
},
{
name: 'Taakko',
},
],
});
});
it('test', () => {
return Person.query()
.withGraphFetched({ parent: true, cousins: true })
.where('name', 'Matti')
.then((result) => {
expect(Object.keys(result[0].parent)).to.eql(['name']);
expect(Object.keys(result[0].cousins[0])).to.eql(['name']);
});
});
});
};
================================================
FILE: tests/integration/misc/#1202.js
================================================
const { Model } = require('../../../');
const { expect } = require('chai');
module.exports = (session) => {
describe(`relations should be loaded lazily #1202`, () => {
let knex = session.knex;
let Person;
let loadedRelations = [];
before(() => {
return knex.schema
.dropTableIfExists('cousins')
.dropTableIfExists('persons')
.createTable('persons', (table) => {
table.increments('id').primary();
table.integer('parentId');
table.string('name');
})
.createTable('cousins', (table) => {
table.integer('id1');
table.integer('id2');
});
});
after(() => {
return knex.schema.dropTableIfExists('cousins').dropTableIfExists('persons');
});
beforeEach(() => {
loadedRelations = [];
Person = class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
parent: {
relation: Model.BelongsToOneRelation,
modelClass() {
loadedRelations.push('parent');
return Person;
},
join: {
from: 'persons.parentId',
to: 'persons.id',
},
},
cousins: {
relation: Model.ManyToManyRelation,
modelClass() {
loadedRelations.push('cousins');
return Person;
},
join: {
from: 'persons.id',
through: {
from: 'cousins.id1',
to: 'cousins.id2',
},
to: 'persons.id',
},
},
};
}
};
Person.knex(knex);
});
beforeEach(() => Person.query().delete());
beforeEach(() => {
// This is what you get when you cannot use insertGraph.
return session
.knex('persons')
.insert({ name: 'Meinhart ' })
.returning('id')
.then(([{ id }]) => {
return session.knex('persons').insert({ name: 'Arnold', parentId: id }).returning('id');
})
.then(([{ id: arnoldId }]) => {
return Promise.all([
session.knex('persons').insert({ name: 'Hans' }).returning('id'),
session.knex('persons').insert({ name: 'Urs' }).returning('id'),
]).then(([[{ id: hansId }], [{ id: ursId }]]) => {
return Promise.all([
session.knex('cousins').insert({ id1: arnoldId, id2: hansId }),
session.knex('cousins').insert({ id1: arnoldId, id2: ursId }),
]);
});
});
});
it('inserting a model should not load relations', () => {
return Person.query()
.insert({ name: 'Arnold' })
.then(() => {
expect(loadedRelations).to.have.length(0);
});
});
it('updating a model should not load relations', () => {
return Person.query()
.patch({ name: 'Arnold' })
.findById(1)
.then(() => {
expect(loadedRelations).to.have.length(0);
});
});
it('finding a model should not load relations', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((result) => {
expect(result).to.containSubset({ name: 'Arnold' });
expect(loadedRelations).to.have.length(0);
});
});
it('toJSON a model should not load relations', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((result) => {
result.toJSON();
result.$toDatabaseJson();
expect(loadedRelations).to.have.length(0);
});
});
it('eager should only load relations in the expression', () => {
return Person.query()
.withGraphFetched('parent')
.then(() => {
expect(loadedRelations).to.eql(['parent']);
return Person.query().withGraphFetched('cousins');
})
.then(() => {
expect(loadedRelations).to.eql(['parent', 'cousins']);
});
});
it('joinEager should only load relations in the expression', () => {
return Person.query()
.withGraphJoined('parent')
.then(() => {
expect(loadedRelations).to.eql(['parent']);
return Person.query().withGraphJoined('cousins');
})
.then(() => {
expect(loadedRelations).to.eql(['parent', 'cousins']);
});
});
it('$relatedQuery should only load the queried relation', () => {
return Person.query()
.findOne({ name: 'Arnold' })
.then((arnold) => {
return arnold.$relatedQuery('cousins');
})
.then(() => {
expect(loadedRelations).to.eql(['cousins']);
});
});
it('insertGraph should only load relations in the graph', () => {
return Person.query()
.insertGraph({
name: 'Sami',
parent: {
name: 'Liisa',
},
})
.then(() => {
expect(loadedRelations).to.eql(['parent']);
});
});
it('fromJson should only load relations that are present in the object', () => {
Person.fromJson({
name: 'Ardnold',
cousins: [
{
name: 'Hans',
},
],
});
expect(loadedRelations).to.eql(['cousins']);
});
});
};
================================================
FILE: tests/integration/misc/#1215.js
================================================
const { Model } = require('../../../');
const { expect } = require('chai');
module.exports = (session) => {
describe(`joinRelated in filter in model relationMappings #1215`, () => {
let knex = session.knex;
let Person;
before(() => {
return knex.schema
.dropTableIfExists('cousins')
.dropTableIfExists('persons')
.createTable('persons', (table) => {
table.increments('id').primary();
table.integer('parentId');
table.string('name');
})
.createTable('cousins', (table) => {
table.integer('id1');
table.integer('id2');
});
});
after(() => {
return knex.schema.dropTableIfExists('cousins').dropTableIfExists('persons');
});
beforeEach(() => {
Person = class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
parent: {
modelClass: Person,
relation: Model.BelongsToOneRelation,
join: {
from: 'persons.parentId',
to: 'persons.id',
},
},
children: {
modelClass: Person,
relation: Model.HasManyRelation,
modify(builder) {
builder.leftJoinRelated('parent').select('persons.*', 'parent.name as parentName');
},
join: {
from: 'persons.id',
to: 'persons.parentId',
},
},
cousins: {
modelClass: Person,
relation: Model.ManyToManyRelation,
modify(builder) {
builder.leftJoinRelated('parent').select('persons.*', 'parent.name as parentName');
},
join: {
from: 'persons.id',
through: {
from: 'cousins.id1',
to: 'cousins.id2',
},
to: 'persons.id',
},
},
};
}
};
Person.knex(knex);
});
beforeEach(() => Person.query().delete());
beforeEach(() => {
return Person.query().insertGraph({
name: 'Matti',
parent: {
name: 'Samuel',
},
children: [
{
name: 'Sami',
},
{
name: 'Marika',
},
],
cousins: [
{
name: 'Torsti',
},
{
name: 'Taina',
parent: {
name: 'Urpo',
},
},
],
});
});
it('should be able to use joinRelated in relationMapping modifier', () => {
return Person.query()
.first()
.where('name', 'Matti')
.withGraphFetched({
children: true,
cousins: true,
})
.then((result) => {
expect(result).to.containSubset({
name: 'Matti',
children: [
{ name: 'Sami', parentName: 'Matti' },
{ name: 'Marika', parentName: 'Matti' },
],
cousins: [
{ name: 'Torsti', parentName: null },
{ name: 'Taina', parentName: 'Urpo' },
],
});
});
});
});
};
================================================
FILE: tests/integration/misc/#1223.js
================================================
const { Model } = require('../../../');
const { expect } = require('chai');
module.exports = (session) => {
describe(`Recursive eagering with an alias doesn't work with 1-m or m-m relations #1223`, () => {
let knex = session.knex;
let Person;
before(() => {
return knex.schema
.dropTableIfExists('cousins')
.dropTableIfExists('persons')
.createTable('persons', (table) => {
table.increments('id').primary();
table.integer('parentId');
table.string('name');
})
.createTable('cousins', (table) => {
table.integer('id1');
table.integer('id2');
});
});
after(() => {
return knex.schema.dropTableIfExists('cousins').dropTableIfExists('persons');
});
beforeEach(() => {
Person = class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
children: {
modelClass: Person,
relation: Model.HasManyRelation,
join: {
from: 'persons.id',
to: 'persons.parentId',
},
},
cousins: {
modelClass: Person,
relation: Model.ManyToManyRelation,
join: {
from: 'persons.id',
through: {
from: 'cousins.id1',
to: 'cousins.id2',
},
to: 'persons.id',
},
},
};
}
};
Person.knex(knex);
});
beforeEach(() => Person.query().delete());
beforeEach(() => {
return Person.query().insertGraph({
name: 'Root',
children: [
{
name: 'Child 1',
children: [
{
name: 'Child 2',
},
{
name: 'Child 3',
},
],
},
{
name: 'Child 4',
children: [
{
name: 'Child 5',
},
{
name: 'Child 6',
},
],
},
],
cousins: [
{
name: 'Cousin 1',
cousins: [
{
name: 'Cousin 2',
},
{
name: 'Cousin 3',
},
],
},
{
name: 'Cousin 4',
cousins: [
{
name: 'Cousin 5',
},
{
name: 'Cousin 6',
},
],
},
],
});
});
it('should allow recursive eagering with aliases for 1-m and m-m relations', () => {
return Person.query()
.findOne({ 'persons.name': 'Root' })
.withGraphFetched({
alias1: {
$relation: 'children',
$recursive: 10,
},
alias2: {
$relation: 'cousins',
$recursive: 10,
},
})
.then((result) => {
expect(result).to.containSubset({
parentId: null,
name: 'Root',
alias1: [
{
name: 'Child 1',
alias1: [
{
name: 'Child 2',
alias1: [],
},
{
name: 'Child 3',
alias1: [],
},
],
},
{
name: 'Child 4',
alias1: [
{
name: 'Child 5',
alias1: [],
},
{
name: 'Child 6',
alias1: [],
},
],
},
],
alias2: [
{
name: 'Cousin 1',
alias2: [
{
name: 'Cousin 2',
alias2: [],
},
{
name: 'Cousin 3',
alias2: [],
},
],
},
{
name: 'Cousin 4',
alias2: [
{
name: 'Cousin 5',
alias2: [],
},
{
name: 'Cousin 6',
alias2: [],
},
],
},
],
});
});
});
});
};
================================================
FILE: tests/integration/misc/#1227.js
================================================
const { Model } = require('../../../');
const { expect } = require('chai');
module.exports = (session) => {
describe(`primary key to primary key relations with upsertGraph #1227`, () => {
let knex = session.knex;
let Person, Animal;
before(() => {
return knex.schema
.dropTableIfExists('cousins')
.dropTableIfExists('persons')
.createTable('persons', (table) => {
table.increments('id').primary();
table.string('name');
})
.createTable('pets', (table) => {
table.integer('id').unsigned().primary().references('persons.id');
table.string('name');
});
});
after(() => {
return knex.schema.dropTableIfExists('pets').dropTableIfExists('persons');
});
beforeEach(() => {
Animal = class Animal extends Model {
static get tableName() {
return 'pets';
}
};
Animal.knex(knex);
});
beforeEach(() => {
Person = class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
pet: {
modelClass: Animal,
relation: Model.HasOneRelation,
join: {
from: 'persons.id',
to: 'pets.id',
},
},
};
}
};
Person.knex(knex);
});
beforeEach(() => Animal.query().delete());
beforeEach(() => Person.query().delete());
it('should be able to insert a primary key to primary key hasOne relation', () => {
return Person.query()
.upsertGraph({
name: 'person',
pet: {
name: 'pet',
},
})
.then((person) => {
return Person.query().findById(person.id).withGraphFetched('pet');
})
.then((person) => {
expect(person).to.containSubset({
name: 'person',
pet: {
name: 'pet',
},
});
});
});
});
};
================================================
FILE: tests/integration/misc/#1265.js
================================================
const { Model } = require('../../../');
const { expect } = require('chai');
module.exports = (session) => {
describe(`upsertGraph with composite key relation doesn't copy foreign keys #1265`, () => {
let knex = session.knex;
let Person, Animal;
before(() => {
return knex.schema
.createTable('Person', (table) => {
table.increments('id').primary();
table.integer('userId').unsigned().notNullable();
table.integer('projectId').unsigned().notNullable();
table.string('firstName');
table.string('lastName');
})
.createTable('Animal', (table) => {
table.increments('id').primary();
table.integer('userId').unsigned().notNullable();
table.integer('projectId').unsigned().notNullable();
table.string('name');
table.string('species');
});
});
after(() => {
return knex.schema.dropTableIfExists('Animal').dropTableIfExists('Person');
});
beforeEach(() => {
Animal = class Animal extends Model {
static get tableName() {
return 'Animal';
}
static get relationMappings() {
return {
owner: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: ['Animal.userId', 'Animal.projectId'],
to: ['Person.userId', 'Person.projectId'],
},
},
};
}
};
Animal.knex(knex);
});
beforeEach(() => {
Person = class Person extends Model {
static get tableName() {
return 'Person';
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: ['Person.userId', 'Person.projectId'],
to: ['Animal.userId', 'Animal.projectId'],
},
},
};
}
};
Person.knex(knex);
});
beforeEach(() => Animal.query().delete());
beforeEach(() => Person.query().delete());
beforeEach(() => {
return Person.query().insertGraph({
firstName: 'Jennifer',
lastName: 'Lawrence',
userId: 1,
projectId: 1,
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Grumpy',
species: 'cat',
},
],
});
});
it('test', () => {
return Person.query()
.upsertGraph(
{
id: 1,
pets: [
{
name: 'Peppa',
species: 'pig',
},
],
},
{ noDelete: true },
)
.then(() => {
return Person.query().findOne({ firstName: 'Jennifer' }).withGraphFetched('pets');
})
.then((jennifer) => {
expect(jennifer.pets).to.have.length(3);
expect(jennifer.pets).to.containSubset([
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Grumpy',
species: 'cat',
},
{
name: 'Peppa',
species: 'pig',
},
]);
});
});
});
};
================================================
FILE: tests/integration/misc/#1455.js
================================================
const { Model, transaction } = require('../../../');
const { expect } = require('chai');
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;
}
static get concurrency() {
return 1;
}
}
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()
.withGraphFetched('sets(orderById).setAttributes(orderByName)')
.modifiers({
orderById(query) {
query.orderBy('id');
},
orderByName(query) {
query.orderBy('name');
},
});
})
.then((setsAfterUpsertGraph) => {
expect(setsAfterUpsertGraph).to.containSubset({
id: 1,
name: 'First Role',
sets: [
{
id: 1,
name: 'First Set',
roleId: 1,
setAttributes: [
{ name: 'First SetAttribute', setId: 1 },
{ name: 'Second SetAttribute', setId: 1 },
],
},
{
id: 2,
name: 'Second Set',
roleId: 1,
setAttributes: [
{ name: 'First SetAttribute', setId: 2 },
{ name: 'Second SetAttribute', setId: 2 },
],
},
],
});
});
});
});
};
================================================
FILE: tests/integration/misc/#1467.js
================================================
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({}).withGraphFetched('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({}).withGraphFetched('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({}).withGraphFetched('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({}).withGraphFetched('cogs');
})
.then((d2) => {
chai.expect(d2.cogs.length).to.equal(0);
});
});
});
});
};
================================================
FILE: tests/integration/misc/#1489.js
================================================
const { expect } = require('chai');
const { Model } = require('../../../');
module.exports = (session) => {
describe('relation $beforeInsert not called when insertGraph is used #1627', () => {
let knex = session.knex;
let User;
let Role;
before(() => {
const { knex } = session;
return Promise.resolve()
.then(() => knex.schema.dropTableIfExists('users'))
.then(() => knex.schema.dropTableIfExists('roles'))
.then(() => {
return knex.schema
.createTable('users', (table) => {
table.increments('id');
table.string('username');
})
.createTable('roles', (table) => {
table.increments('id');
table.string('role');
table.integer('userId').unsigned().references('users.id').notNullable().index();
});
});
});
after(async () => {
await knex.schema.dropTableIfExists('roles');
await knex.schema.dropTableIfExists('users');
});
beforeEach(() => {
Role = class Role extends Model {
static get tableName() {
return 'roles';
}
};
User = class User extends Model {
static get tableName() {
return 'users';
}
static get relationMappings() {
return {
roles: {
relation: Model.HasManyRelation,
modelClass: Role,
join: {
from: 'users.id',
to: 'roles.userId',
},
beforeInsert(model) {
model.$relationBeforeInsertCalled = (model.$relationBeforeInsertCalled || 0) + 1;
},
},
};
}
};
User.knex(knex);
Role.knex(knex);
});
it('test', async () => {
const user = await User.query().insert({
username: 'user1',
});
const role = await user.$relatedQuery('roles').insertGraph({
role: 'admin',
});
expect(role.$relationBeforeInsertCalled).to.equal(1);
});
});
};
================================================
FILE: tests/integration/misc/#1627.js
================================================
const crypto = require('crypto');
const { expect } = require('chai');
const { Model, snakeCaseMappers } = require('../../../');
module.exports = (session) => {
if (!session.isMySql()) {
return;
}
describe('withGraphFetched and binary relation column #1627', () => {
let knex = session.knex;
let User;
let Role;
before(() => {
const { knex } = session;
return Promise.resolve()
.then(() => knex.schema.dropTableIfExists('users'))
.then(() => knex.schema.dropTableIfExists('roles'))
.then(() => {
return knex.schema
.createTable('users', (table) => {
table.binary('id', 16).primary();
})
.createTable('roles', (table) => {
table.binary('id', 16).primary();
table.binary('user_id', 16).references('users.id').notNullable().index();
});
});
});
after(async () => {
await knex.schema.dropTableIfExists('roles');
await knex.schema.dropTableIfExists('users');
});
beforeEach(() => {
Role = class Role extends Model {
static get tableName() {
return 'roles';
}
static get columnNameMappers() {
return snakeCaseMappers();
}
};
User = class User extends Model {
static get tableName() {
return 'users';
}
static get columnNameMappers() {
return snakeCaseMappers();
}
static get relationMappings() {
return {
roles: {
relation: Model.HasManyRelation,
modelClass: Role,
join: {
from: 'users.id',
to: 'roles.user_id',
},
},
};
}
};
User.knex(knex);
Role.knex(knex);
});
it('test', async () => {
const inserted = await User.query().insertGraph({
id: crypto.randomBytes(16),
roles: [{ id: crypto.randomBytes(16) }],
});
const result = await User.query().findById(inserted.id).withGraphFetched('roles');
expect(result).to.eql(inserted);
});
it('should fetch multiple relations correctly', async () => {
const ids = [
Buffer.from('00000000000000000000000000007AAC', 'hex'),
Buffer.from('00000000000000000000000000007AAD', 'hex'),
Buffer.from('00000000000000000000000000007AAE', 'hex'),
];
const graph = ids.map((id) => ({
id,
roles: [{ id: crypto.randomBytes(16) }],
}));
const inserted = await User.query().insertGraph(graph);
const result = await User.query().findByIds(ids).withGraphFetched('roles');
expect(result).to.eql(inserted);
});
});
};
================================================
FILE: tests/integration/misc/#1718.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const { Model } = require('../../../');
const { AjvValidator } = require('../../../lib/model/AjvValidator');
const { ValidationError } = require('../../../lib/model/ValidationError');
module.exports = (session) => {
describe('Pass through data in exceptions when Ajv verbose option is enabled #1718', () => {
class MyModel extends Model {
static get tableName() {
return 'MyModel';
}
static createValidator() {
return new AjvValidator({
options: {
allErrors: true,
validateSchema: false,
ownProperties: true,
v5: true,
verbose: true,
},
});
}
static get jsonSchema() {
return {
type: 'object',
required: ['id'],
properties: {
id: { type: 'integer' },
},
};
}
}
beforeEach(() => {
return session.knex.schema
.dropTableIfExists('MyModel')
.createTable('MyModel', (table) => {
table.integer('id').primary();
})
.then(() => {
return Promise.all([session.knex('MyModel').insert({ id: 1 })]);
});
});
afterEach(() => {
return session.knex.schema.dropTableIfExists('MyModel');
});
it('test', () => {
return MyModel.query(session.knex)
.insert({ id: 2 })
.catch((err) => {
expect(err).to.be.an(ValidationError);
expect(err.data).to.be.an('object');
expect(err.data.id).to.be.an('array');
expect(err.data).to.eql({
id: [
{
message: 'must be integer',
keyword: 'type',
params: { type: 'integer' },
data: '2',
},
],
});
});
});
});
};
================================================
FILE: tests/integration/misc/#1757.js
================================================
const { expect } = require('chai');
const { Model } = require('../../../');
module.exports = (session) => {
describe('skipFetched not working for nested relation #1757', () => {
let knex = session.knex;
class Person extends Model {
static get tableName() {
return 'Person';
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'Person.id',
to: 'Animal.ownerId',
},
},
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'Person.id',
to: 'Person.parentId',
},
},
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'Person.parentId',
to: 'Person.id',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'Animal';
}
static get relationMappings() {
return {
owner: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'Animal.ownerId',
to: 'Person.id',
},
},
};
}
}
before(async () => {
const { knex } = session;
await knex.schema.dropTableIfExists('Animal');
await knex.schema.dropTableIfExists('Person');
await knex.schema
.createTable('Person', (table) => {
table.increments('id').primary();
table.integer('parentId').unsigned().references('id').inTable('Person');
table.string('firstName');
table.string('lastName');
table.integer('age');
table.json('address');
})
.createTable('Animal', (table) => {
table.increments('id').primary();
table.integer('ownerId').unsigned().references('id').inTable('Person');
table.string('name');
table.string('species');
});
});
after(async () => {
await knex.schema.dropTableIfExists('Animal');
await knex.schema.dropTableIfExists('Person');
});
it('test', async () => {
await Person.query(knex).insertGraph({
firstName: 'JL',
lastName: 'Mom',
children: [
{
firstName: 'Jennifer',
lastName: 'Lawrence',
pets: [
{
name: 'Doggo',
species: 'dog',
},
],
},
],
});
let doggo = await Animal.query(knex).findOne({ name: 'Doggo' });
await doggo.$fetchGraph('owner', { transaction: knex });
await doggo.$fetchGraph('owner.parent', { transaction: knex });
expect(doggo.owner.parent.firstName).to.equal('JL');
// Reload doggo
doggo = await Animal.query(knex).findOne({ name: 'Doggo' });
await doggo.$fetchGraph('owner', { skipFetched: true, transaction: knex });
await doggo.$fetchGraph('owner.parent', { skipFetched: true, transaction: knex });
expect(doggo.owner.parent.firstName).to.equal('JL');
});
});
};
================================================
FILE: tests/integration/misc/#2105.js
================================================
const { Model } = require('../../../');
const { expect } = require('chai');
module.exports = (session) => {
describe(`patch() breaks if useDefineForClassField is enabled in tsconfig #2105`, () => {
let knex = session.knex;
let Person;
before(() => {
return knex.schema.dropTableIfExists('persons').createTable('persons', (table) => {
table.increments('id').primary();
table.string('firstName');
table.string('lastName');
});
});
after(() => {
return knex.schema.dropTableIfExists('persons');
});
before(() => {
Person = class Person extends Model {
constructor() {
super(...arguments);
Object.defineProperty(this, 'firstName', {
enumerable: true,
configurable: true,
writable: true,
value: void 0,
});
Object.defineProperty(this, 'lastName', {
enumerable: true,
configurable: true,
writable: true,
value: void 0,
});
}
static get tableName() {
return 'persons';
}
};
Person.knex(knex);
});
beforeEach(() => {
return Person.query().insert({ firstName: 'John', lastName: 'Doe' });
});
afterEach(() => Person.query().delete());
it('should not override lastName with undefined', () => {
return Person.query()
.where('lastName', 'Doe')
.first()
.then((person) => {
return person
.$query()
.patch({ firstName: 'Jane' })
.then(() => person);
})
.then((person) => {
expect(person.firstName).to.eql('Jane');
expect(person.lastName).to.eql('Doe');
});
});
it('should still set values to undefined if explicitly told to do so', () => {
Person.query()
.where('lastName', 'Doe')
.first()
.then((person) => {
person.$set({ lastName: undefined });
})
.then((person) => {
expect(person.firstName).to.eql('Jane');
expect(person.lastName).to.eql(undefined);
});
});
});
};
================================================
FILE: tests/integration/misc/#292.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const { Model } = require('../../../');
module.exports = (session) => {
describe('Eagerly loaded empty relations seem to short-circuit conversion to internal structure #292', () => {
class A extends Model {
static get tableName() {
return 'a';
}
static get relationMappings() {
return {
Bs: {
relation: Model.HasManyRelation,
modelClass: B,
join: {
from: 'a.id',
to: 'b.aId',
},
},
};
}
}
class B extends Model {
static get tableName() {
return 'b';
}
static get relationMappings() {
return {
Cs: {
relation: Model.ManyToManyRelation,
modelClass: C,
join: {
from: 'b.id',
through: {
from: 'b_c.bId',
to: 'b_c.cId',
},
to: 'c.id',
},
},
Ds: {
relation: Model.ManyToManyRelation,
modelClass: D,
join: {
from: 'b.id',
through: {
from: 'b_d.bId',
to: 'b_d.dId',
},
to: 'd.id',
},
},
};
}
}
class C extends Model {
static get tableName() {
return 'c';
}
}
class D extends Model {
static get tableName() {
return 'd';
}
}
beforeEach(() => {
return session.knex.schema
.dropTableIfExists('b_c')
.dropTableIfExists('b_d')
.dropTableIfExists('b')
.dropTableIfExists('a')
.dropTableIfExists('c')
.dropTableIfExists('d')
.createTable('a', (table) => {
table.integer('id').primary();
})
.createTable('b', (table) => {
table.integer('id').primary();
table.integer('aId').references('a.id');
})
.createTable('c', (table) => {
table.integer('id').primary();
})
.createTable('d', (table) => {
table.integer('id').primary();
})
.createTable('b_c', (table) => {
table.integer('bId').references('b.id').onDelete('CASCADE');
table.integer('cId').references('c.id').onDelete('CASCADE');
})
.createTable('b_d', (table) => {
table.integer('bId').references('b.id').onDelete('CASCADE');
table.integer('dId').references('d.id').onDelete('CASCADE');
})
.then(() => {
return Promise.all([
session.knex('a').insert({ id: 1 }),
session.knex('d').insert({ id: 1 }),
session.knex('d').insert({ id: 2 }),
])
.then(() => {
return session.knex('b').insert({ id: 1, aId: 1 });
})
.then(() => {
return Promise.all([
session.knex('b_d').insert({ bId: 1, dId: 1 }),
session.knex('b_d').insert({ bId: 1, dId: 2 }),
]);
});
});
});
afterEach(() => {
return session.knex.schema
.dropTableIfExists('b_c')
.dropTableIfExists('b_d')
.dropTableIfExists('b')
.dropTableIfExists('a')
.dropTableIfExists('c')
.dropTableIfExists('d');
});
it('the test', () => {
return A.query(session.knex)
.withGraphJoined('Bs.[Cs, Ds]')
.then((results) => {
results[0].Bs[0].Ds = _.sortBy(results[0].Bs[0].Ds, 'id');
expect(results).to.eql([
{
id: 1,
Bs: [
{
id: 1,
aId: 1,
Cs: [],
Ds: [
{
id: 1,
},
{
id: 2,
},
],
},
],
},
]);
});
});
});
};
================================================
FILE: tests/integration/misc/#325.js
================================================
const expect = require('expect.js');
const { Model } = require('../../../');
module.exports = (session) => {
describe('Default values not set with .insertGraph() in 0.7.2 #325', () => {
let TestModel;
before(() => {
return session.knex.schema
.dropTableIfExists('default_values_note_set_test')
.createTable('default_values_note_set_test', (table) => {
table.increments('id').primary();
table.string('value1');
table.string('value2');
});
});
after(() => {
return session.knex.schema.dropTableIfExists('default_values_note_set_test');
});
before(() => {
TestModel = class TestModel extends Model {
static get tableName() {
return 'default_values_note_set_test';
}
static get jsonSchema() {
return {
type: 'object',
properties: {
id: { type: 'integer' },
value1: { type: 'string', default: 'foo' },
value2: { type: 'string', default: 'bar' },
},
};
}
};
TestModel.knex(session.knex);
});
beforeEach(() => {
return TestModel.query().delete();
});
it('insert should set the defaults', () => {
return TestModel.query()
.insert({ value1: 'hello' })
.then((model) => {
expect(model.value1).to.equal('hello');
expect(model.value2).to.equal('bar');
return session.knex(TestModel.getTableName());
})
.then((rows) => {
expect(rows[0].value1).to.equal('hello');
expect(rows[0].value2).to.equal('bar');
});
});
it('insertGraph should set the defaults', () => {
return TestModel.query()
.insertGraph({ value1: 'hello' })
.then((model) => {
expect(model.value1).to.equal('hello');
expect(model.value2).to.equal('bar');
return session.knex(TestModel.getTableName());
})
.then((rows) => {
expect(rows[0].value1).to.equal('hello');
expect(rows[0].value2).to.equal('bar');
});
});
});
};
================================================
FILE: tests/integration/misc/#403.js
================================================
const expect = require('expect.js');
const utils = require('../../../lib/utils/knexUtils');
const { Model, ref } = require('../../../');
module.exports = (session) => {
if (utils.isPostgres(session.knex)) {
describe('Eagered grandparents disappear when selecting the pkey as an alias on eagered parents #403', () => {
let knex = session.knex;
let Page;
before(() => {
return knex.schema
.dropTableIfExists('page_relationship')
.dropTableIfExists('page')
.createTable('page', (table) => {
table.integer('page_id').primary();
table.jsonb('object_data');
})
.createTable('page_relationship', (table) => {
table.integer('parent_id').references('page.page_id').index().onDelete('CASCADE');
table.integer('child_id').references('page.page_id').index().onDelete('CASCADE');
});
});
after(() => {
return knex.schema.dropTableIfExists('page_relationship').dropTableIfExists('page');
});
before(() => {
Page = class Page extends Model {
static get tableName() {
return 'page';
}
static get idColumn() {
return 'page_id';
}
static get jsonAttributes() {
return ['object_data'];
}
static get relationMappings() {
return {
parents: {
relation: Model.ManyToManyRelation,
modelClass: Page,
join: {
from: 'page.page_id',
through: {
from: 'page_relationship.child_id',
to: 'page_relationship.parent_id',
},
to: 'page.page_id',
},
},
children: {
relation: Model.ManyToManyRelation,
modelClass: Page,
join: {
from: 'page.page_id',
through: {
from: 'page_relationship.parent_id',
to: 'page_relationship.child_id',
},
to: 'page.page_id',
},
},
};
}
};
Page.knex(knex);
});
before(() => {
return Page.query().insertGraph({
page_id: 1,
object_data: { name: '1' },
parents: [
{
page_id: 2,
object_data: { name: '1_1' },
parents: [
{
page_id: 4,
object_data: { name: '1_1_1' },
},
{
page_id: 5,
object_data: { name: '1_1_2' },
},
],
},
{
page_id: 3,
object_data: { name: '1_2' },
parents: [
{
page_id: 6,
object_data: { name: '1_2_1' },
},
{
page_id: 7,
object_data: { name: '1_2_2' },
},
],
},
],
});
});
it('test 1', () => {
return Page.query()
.findById(1)
.withGraphFetched('parents.parents')
.modifyGraph('parents', (builder) => {
builder.select('page_id as id', ref('object_data:name').as('name')).orderBy('page_id');
})
.modifyGraph('parents.parents', (builder) => {
builder.select('page_id as id', ref('object_data:name').as('name')).orderBy('page_id');
})
.select('page_id as id', ref('object_data:name').as('name'))
.then((pages) => {
expect(pages).to.eql({
id: 1,
name: '1',
parents: [
{
id: 2,
name: '1_1',
parents: [
{
id: 4,
name: '1_1_1',
},
{
id: 5,
name: '1_1_2',
},
],
},
{
id: 3,
name: '1_2',
parents: [
{
id: 6,
name: '1_2_1',
},
{
id: 7,
name: '1_2_2',
},
],
},
],
});
});
});
it('test 2', () => {
return Page.query()
.joinRelated('parents.parents')
.select(
'parents:parents.page_id as id',
ref('object_data:name').from('parents:parents').as('name'),
)
.orderBy('parents:parents.page_id')
.then((models) => {
expect(models).to.eql([
{ id: 4, name: '1_1_1' },
{ id: 5, name: '1_1_2' },
{ id: 6, name: '1_2_1' },
{ id: 7, name: '1_2_2' },
]);
});
});
});
}
};
================================================
FILE: tests/integration/misc/#517.js
================================================
const expect = require('expect.js');
const { Model } = require('../../../');
module.exports = (session) => {
describe('upsertGraph with compound key in relation #517', () => {
let knex = session.knex;
let Users;
let Preferences;
before(() => {
return knex.schema
.dropTableIfExists('Users')
.dropTableIfExists('Preferences')
.createTable('Users', (table) => {
table.integer('id').primary();
})
.createTable('Preferences', (table) => {
table.integer('userId');
table.string('category', 16);
table.string('setting');
table.primary(['userId', 'category']);
});
});
after(() => {
return knex.schema.dropTableIfExists('Preferences').dropTableIfExists('Users');
});
before(() => {
Users = class Users extends Model {
static get tableName() {
return 'Users';
}
static get relationMappings() {
return {
preferences: {
relation: Model.HasManyRelation,
modelClass: Preferences,
join: {
from: 'Preferences.userId',
to: 'Users.id',
},
},
};
}
};
Preferences = class Preferences extends Model {
static get tableName() {
return 'Preferences';
}
static get idColumn() {
return ['userId', 'category'];
}
};
Users.knex(knex);
Preferences.knex(knex);
});
before(() => {
return Users.query().insert({
id: 1,
});
});
it('test', () => {
const preferences = [
{
category: 'sms',
setting: 'off',
},
{
category: 'sound',
setting: 'off',
},
];
return Users.query()
.upsertGraph({ id: 1, preferences }, { insertMissing: true })
.then(() => {
return Users.query()
.withGraphFetched('preferences')
.modifyGraph('preferences', (qb) => qb.orderBy('category'));
})
.then((users) => {
expect(users).to.eql([
{
id: 1,
preferences: [
{
category: 'sms',
setting: 'off',
userId: 1,
},
{
category: 'sound',
setting: 'off',
userId: 1,
},
],
},
]);
});
});
});
};
================================================
FILE: tests/integration/misc/#712.js
================================================
const { expect } = require('chai');
const { Model } = require('../../../');
module.exports = (session) => {
describe(`modifiers that have no where or select statements don't work with joinRelated #712`, () => {
let knex = session.knex;
let Person;
before(() => {
return knex.schema.dropTableIfExists('Person').createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('parentId');
});
});
after(() => {
return knex.schema.dropTableIfExists('Person');
});
before(() => {
Person = class Person extends Model {
static get tableName() {
return 'Person';
}
static get modifiers() {
return {
notFirstChild: (builder) => {
builder.from((subQuery) => {
subQuery
.select('Person.*')
.from('Person')
.where('name', '!=', 'child 1')
.as('Person');
});
},
};
}
static get relationMappings() {
return {
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'Person.id',
to: 'Person.parentId',
},
},
};
}
};
Person.knex(knex);
});
before(() => {
return Person.query().insertGraph({
id: 1,
name: 'parent',
children: [
{
id: 2,
name: 'child 1',
},
{
id: 3,
name: 'child 2',
},
],
});
});
it('test', () => {
return Person.query()
.where('Person.id', 1)
.select('Person.*', 'children.name as childName')
.joinRelated('children(notFirstChild)')
.orderBy('childName')
.then((people) => {
expect(people).to.have.length(1);
expect(people[0].childName).to.equal('child 2');
});
});
});
};
================================================
FILE: tests/integration/misc/#733.js
================================================
const { expect } = require('chai');
const { Model } = require('../../../');
module.exports = (session) => {
describe(`select in modifier + joinEager #733`, () => {
let knex = session.knex;
let Person;
before(() => {
return knex.schema.dropTableIfExists('Person').createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('parentId');
});
});
after(() => {
return knex.schema.dropTableIfExists('Person');
});
before(() => {
Person = class Person extends Model {
static get tableName() {
return 'Person';
}
static get modifiers() {
return {
aliasedProps: (builder) => {
builder.select(['id as aliasedId', 'name as aliasedName']);
},
aliasedPropsAndSelectAll: (builder) => {
builder.select(['Person.*', 'id as aliasedId', 'name as aliasedName']);
},
};
}
static get relationMappings() {
return {
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'Person.id',
to: 'Person.parentId',
},
},
};
}
};
Person.knex(knex);
});
before(() => {
return Person.query().insertGraph({
id: 1,
name: 'parent',
children: [
{
id: 2,
name: 'child 1',
},
{
id: 3,
name: 'child 2',
},
],
});
});
it('aliased properties', () => {
return Person.query()
.where('Person.id', 1)
.withGraphJoined('children(aliasedProps)')
.then((result) => {
expect(result).to.containSubset([
{
id: 1,
name: 'parent',
parentId: null,
children: [
{ aliasedId: 2, aliasedName: 'child 1' },
{ aliasedId: 3, aliasedName: 'child 2' },
],
},
]);
});
});
it('aliased properties + select all', () => {
return Person.query()
.where('Person.id', 1)
.withGraphJoined('children(aliasedPropsAndSelectAll)')
.then((result) => {
expect(result).to.containSubset([
{
id: 1,
name: 'parent',
parentId: null,
children: [
{ id: 2, name: 'child 1', parentId: 1, aliasedId: 2, aliasedName: 'child 1' },
{ id: 3, name: 'child 2', parentId: 1, aliasedId: 3, aliasedName: 'child 2' },
],
},
]);
});
});
});
};
================================================
FILE: tests/integration/misc/#760.js
================================================
const { expect } = require('chai');
const { Model } = require('../../../');
module.exports = (session) => {
describe(`orderBy extra property in relation modify #760`, () => {
let knex = session.knex;
let Person;
before(() => {
return knex.schema
.dropTableIfExists('Person')
.dropTableIfExists('PersonPerson')
.createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
})
.createTable('PersonPerson', (table) => {
table.increments('id').primary();
table.integer('awesomeness');
table.integer('person1Id');
table.integer('person2Id');
});
});
after(() => {
return knex.schema.dropTableIfExists('PersonPerson').dropTableIfExists('Person');
});
before(() => {
Person = class Person extends Model {
static get tableName() {
return 'Person';
}
static get relationMappings() {
return {
relatives: {
relation: Model.ManyToManyRelation,
modelClass: Person,
modify: (builder) => builder.orderBy('awesomeness'),
join: {
from: 'Person.id',
through: {
from: 'PersonPerson.person1Id',
to: 'PersonPerson.person2Id',
extra: ['awesomeness'],
},
to: 'Person.id',
},
},
goodRelatives: {
relation: Model.ManyToManyRelation,
modelClass: Person,
modify: (builder) => builder.orderBy('awesomeness').where('awesomeness', '>', 1),
join: {
from: 'Person.id',
through: {
from: 'PersonPerson.person1Id',
to: 'PersonPerson.person2Id',
extra: ['awesomeness'],
},
to: 'Person.id',
},
},
};
}
};
Person.knex(knex);
});
beforeEach(() => {
return Person.query()
.delete()
.then(() => {
return Person.query().insertGraph({
id: 1,
name: 'parent',
relatives: [
{
id: 2,
awesomeness: 1,
name: 'relative 1',
},
{
id: 3,
awesomeness: 2,
name: 'relative 2',
},
],
});
});
});
it('eager', () => {
return Person.query()
.where('Person.id', 1)
.withGraphFetched('[relatives, goodRelatives]')
.then((result) => {
expect(result).to.containSubset([
{
id: 1,
name: 'parent',
relatives: [
{ id: 2, name: 'relative 1', awesomeness: 1 },
{ id: 3, name: 'relative 2', awesomeness: 2 },
],
goodRelatives: [{ id: 3, name: 'relative 2', awesomeness: 2 }],
},
]);
});
});
it('upsertGraph', () => {
return Person.query()
.upsertGraph({
id: 1,
relatives: [
{
id: 2,
name: 'relative 11',
awesomeness: 11,
},
{
id: 3,
name: 'relative 22',
awesomeness: 22,
},
],
})
.then(() => {
return Person.query().findById(1).withGraphFetched('relatives');
})
.then((result) => {
expect(result).to.containSubset({
id: 1,
name: 'parent',
relatives: [
{ id: 2, name: 'relative 11', awesomeness: 11 },
{ id: 3, name: 'relative 22', awesomeness: 22 },
],
});
});
});
it('upsertGraph (only extra properties)', () => {
return Person.query()
.upsertGraph({
id: 1,
relatives: [
{
id: 2,
awesomeness: 11,
},
{
id: 3,
awesomeness: 22,
},
],
})
.then(() => {
return Person.query().findById(1).withGraphFetched('relatives');
})
.then((result) => {
expect(result).to.containSubset({
id: 1,
name: 'parent',
relatives: [
{ id: 2, name: 'relative 1', awesomeness: 11 },
{ id: 3, name: 'relative 2', awesomeness: 22 },
],
});
});
});
});
};
================================================
FILE: tests/integration/misc/#844.js
================================================
const { expect } = require('chai');
const { Model } = require('../../../');
module.exports = (session) => {
describe(`Ambiguous column mapping for RelationJoinBuilder (no table ref) #844`, () => {
let knex = session.knex;
let Person;
let Movie;
before(() => {
return knex.schema
.dropTableIfExists('Person_Movie')
.dropTableIfExists('Movie')
.dropTableIfExists('Person')
.createTable('Person', (table) => {
table.increments('id').primary();
table.integer('parentId').unsigned().references('id').inTable('Person');
table.string('name');
table.integer('age');
})
.createTable('Movie', (table) => {
table.increments('id').primary();
table.string('name');
})
.createTable('Person_Movie', (table) => {
table.increments('id').primary();
table
.integer('personId')
.unsigned()
.references('id')
.inTable('Person')
.onDelete('CASCADE');
table.integer('movieId').unsigned().references('id').inTable('Movie').onDelete('CASCADE');
});
});
after(() => {
return knex.schema
.dropTableIfExists('Person_Movie')
.dropTableIfExists('Movie')
.dropTableIfExists('Person');
});
before(() => {
Person = class extends Model {
static get tableName() {
return 'Person';
}
static get relationMappings() {
return {
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'Person.id',
through: {
from: 'Person_Movie.personId',
to: 'Person_Movie.movieId',
},
to: 'Movie.id',
},
},
};
}
};
Movie = class extends Model {
static get tableName() {
return 'Movie';
}
static get modifiers() {
return {
onlyOldActors: (builder) =>
builder.select('Movie.name').joinRelated('actors').where('actors.age', '>', 40),
};
}
static get relationMappings() {
return {
actors: {
relation: Model.ManyToManyRelation,
modelClass: Person,
join: {
from: 'Movie.id',
through: {
from: 'Person_Movie.movieId',
to: 'Person_Movie.personId',
},
to: 'Person.id',
},
},
};
}
};
});
before(() => {
return Movie.query(session.knex).insertGraph([
{
id: 1,
name: 'movie 1',
actors: [
{
id: 1,
name: 'person 1',
age: 30,
},
],
},
{
id: 2,
name: 'movie 2',
actors: [
{
id: 2,
name: 'person 2',
age: 50,
},
],
},
]);
});
it('test', () => {
return Person.query(session.knex)
.select('Person.name', 'movies.name as movieName')
.joinRelated('movies(onlyOldActors)')
.then((results) => {
expect(results.length).to.equal(1);
expect(results[0].movieName).to.equal('movie 2');
});
});
});
};
================================================
FILE: tests/integration/misc/#909.js
================================================
const uuid = require('uuid');
const { expect } = require('chai');
const { Model, val, raw } = require('../../../');
module.exports = (session) => {
if (!session.isPostgres()) {
return;
}
describe(`Insert an array of UUIDS #909`, () => {
let knex = session.knex;
let Person;
before(() => {
return knex.schema.dropTableIfExists('Person').createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
table.specificType('uuids', 'uuid[]');
});
});
after(() => {
return knex.schema.dropTableIfExists('Person');
});
before(() => {
Person = class Person extends Model {
static get tableName() {
return 'Person';
}
};
Person.knex(knex);
});
beforeEach(() => Person.query().delete());
it('should be able to cast to uuid[]', () => {
const uuids = [uuid.v4(), uuid.v4()];
return Person.query()
.insert({
name: 'Margot',
uuids: val(uuids).asArray().castTo('uuid[]'),
})
.then(() => {
return Person.query();
})
.then((people) => {
expect(people).to.containSubset([
{
name: 'Margot',
uuids,
},
]);
});
});
it('should be able to cast individual array items to uuid', () => {
const uuids = [uuid.v4(), uuid.v4()];
return Person.query()
.insert({
name: 'Margot',
uuids: val(uuids.map((it) => val(it).castTo('uuid'))).asArray(),
})
.then(() => {
return Person.query();
})
.then((people) => {
expect(people).to.containSubset([
{
name: 'Margot',
uuids,
},
]);
});
});
it('should be able to give an array of raw instances that are cast to uuid', () => {
const uuids = [uuid.v4(), uuid.v4()];
return Person.query()
.insert({
name: 'Margot',
uuids: val(uuids.map((it) => raw('?::uuid', it))).asArray(),
})
.then(() => {
return Person.query();
})
.then((people) => {
expect(people).to.containSubset([
{
name: 'Margot',
uuids,
},
]);
});
});
});
};
================================================
FILE: tests/integration/misc/aggregateMethodsWithRelations.js
================================================
const expect = require('expect.js');
module.exports = (session) => {
describe('aggregate methods with relations', () => {
beforeEach(() => {
return session.populate([
{
model1Prop1: 'a',
model1Relation2: [
{ model2_prop1: 'one' },
{ model2_prop1: 'two' },
{ model2_prop1: 'three' },
],
},
{
model1Prop1: 'b',
model1Relation2: [{ model2_prop1: 'four' }, { model2_prop1: 'five' }],
},
]);
});
it('count of HasManyRelation', () => {
return session.models.Model1.query()
.select('Model1.*')
.count('model1Relation2.id_col as relCount')
.joinRelated('model1Relation2')
.groupBy('Model1.id')
.orderBy('Model1.model1Prop1')
.then((models) => {
expect(models[0].relCount).to.eql(3);
expect(models[1].relCount).to.eql(2);
});
});
});
};
================================================
FILE: tests/integration/misc/concurrency.js
================================================
const expect = require('expect.js');
const mockKnexFactory = require('../../../testUtils/mockKnex');
const { Model, snakeCaseMappers } = require('../../../');
module.exports = (session) => {
// TODO: Skipped for now for because a change in knex broke mockKnexFactory.
describe.skip('Model.concurrency', () => {
let knex;
let models = {};
let runningQueries = [];
beforeEach(() => {
models.Model1 = class Model1 extends Model {
static get tableName() {
return 'Model1';
}
static get concurrency() {
return 1;
}
static get relationMappings() {
return {
model1Relation1: {
relation: Model.BelongsToOneRelation,
modelClass: Model1,
join: {
from: 'Model1.model1Id',
to: 'Model1.id',
},
},
model1Relation1Inverse: {
relation: Model.HasOneRelation,
modelClass: Model1,
join: {
from: 'Model1.id',
to: 'Model1.model1Id',
},
},
model1Relation2: {
relation: Model.HasManyRelation,
modelClass: models.Model2,
join: {
from: 'Model1.id',
to: 'model2.model1_id',
},
},
model1Relation3: {
relation: Model.ManyToManyRelation,
modelClass: models.Model2,
join: {
from: 'Model1.id',
through: {
from: 'Model1Model2.model1Id',
to: 'Model1Model2.model2Id',
extra: ['extra1', 'extra2'],
},
to: 'model2.id_col',
},
},
};
}
};
models.Model2 = class Model2 extends Model {
// Function instead of getter on purpose.
static tableName() {
return 'model2';
}
static get idColumn() {
return 'id_col';
}
static get columnNameMappers() {
return snakeCaseMappers();
}
static get concurrency() {
return 1;
}
};
knex = mockKnexFactory(session.knex, function (mock, oldImpl, args) {
const runningQuery = {
sql: this.toString(),
};
runningQueries.push(runningQuery);
expect(runningQueries).to.have.length(1);
return oldImpl.apply(this, args).then((res) => {
runningQueries = runningQueries.filter((it) => it !== runningQuery);
return res;
});
});
Object.keys(models)
.map((it) => models[it])
.forEach((model) => model.knex(knex));
});
it('insertGraph', () => {
const { Model1 } = models;
return Model1.query().insertGraph({
model1Prop1: '1',
model1Relation1: {
model1Prop1: '2',
},
model1Relation2: [
{
model2Prop1: '3',
},
{
model2Prop1: '4',
},
],
model1Relation3: [
{
model2Prop1: '5',
},
{
model2Prop1: '6',
},
],
});
});
});
};
================================================
FILE: tests/integration/misc/defaultModelFieldValues.js
================================================
const { expect } = require('chai');
const { Model } = require('../../../');
module.exports = (session) => {
const { knex } = session;
// Typescript adds undefined default values for all declared class fields
// when the there's `target: 'esnext'` in tsconfig.json. This test set
// makes sure objection plays nice with those undefineds.
describe('default undefined model field values', () => {
class Person extends Model {
firstName;
pets;
static jsonSchema = {
type: 'object',
properties: {
firstName: {
type: 'string',
},
},
};
static tableName = 'person';
static relationMappings = () => ({
pets: {
modelClass: Pet,
relation: Model.HasManyRelation,
join: {
from: 'person.id',
to: 'pet.ownerId',
},
},
});
}
class Pet extends Model {
name;
ownerId;
owner;
toys;
static jsonSchema = {
type: 'object',
properties: {
name: {
type: 'string',
},
ownerId: {
type: 'integer',
},
},
};
static tableName = 'pet';
static relationMappings = () => ({
owner: {
modelClass: Person,
relation: Model.BelongsToOneRelation,
join: {
from: 'pet.ownerId',
to: 'person.id',
},
},
toys: {
modelClass: Toy,
relation: Model.ManyToManyRelation,
join: {
from: 'pet.id',
through: {
from: 'petToy.petId',
to: 'petToy.toyId',
},
to: 'toy.id',
},
},
});
}
class Toy extends Model {
toyName;
price;
static jsonSchema = {
type: 'object',
properties: {
toyName: {
type: 'string',
},
price: {
type: 'number',
},
},
};
static tableName = 'toy';
}
before(() => {
return knex.schema
.dropTableIfExists('petToy')
.dropTableIfExists('pet')
.dropTableIfExists('toy')
.dropTableIfExists('person')
.createTable('person', (table) => {
table.increments('id').primary();
table.string('firstName');
})
.createTable('pet', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('ownerId').unsigned().references('person.id').onDelete('cascade');
})
.createTable('toy', (table) => {
table.increments('id').primary();
table.string('toyName');
table.float('price');
})
.createTable('petToy', (table) => {
table.integer('petId').unsigned().references('pet.id').notNullable().onDelete('cascade');
table.integer('toyId').unsigned().references('toy.id').notNullable().onDelete('cascade');
});
});
after(() => {
return knex.schema
.dropTableIfExists('petToy')
.dropTableIfExists('pet')
.dropTableIfExists('toy')
.dropTableIfExists('person');
});
it('insertGraph', async () => {
const result = await Person.query(knex)
.allowGraph('pets.toys')
.insertGraph({
firstName: 'Arnold',
pets: [{ name: 'Catto' }, { name: 'Doggo', toys: [{ toyName: 'Bone' }] }],
});
expect(result).to.containSubset({
firstName: 'Arnold',
pets: [
{ name: 'Catto', owner: undefined, toys: undefined },
{
name: 'Doggo',
owner: undefined,
toys: [{ toyName: 'Bone' }],
},
],
});
const resultFromDb = await Person.query(knex)
.findById(result.id)
.withGraphFetched({
pets: {
toys: true,
},
});
expect(resultFromDb).to.containSubset({
firstName: 'Arnold',
pets: [
{ name: 'Catto', owner: undefined, toys: [] },
{
name: 'Doggo',
toys: [{ toyName: 'Bone' }],
},
],
});
});
it('withGraphFetched', async () => {
const { id } = await Person.query(knex).insertGraph({
firstName: 'Arnold',
pets: [{ name: 'Catto' }, { name: 'Doggo', toys: [{ toyName: 'Bone' }] }],
});
const result = await Person.query(knex)
.findById(id)
.withGraphFetched({
pets: {
toys: true,
},
});
expect(result).to.containSubset({
firstName: 'Arnold',
pets: [
{ name: 'Catto', owner: undefined, toys: [] },
{
name: 'Doggo',
owner: undefined,
toys: [{ toyName: 'Bone' }],
},
],
});
});
it('withGraphJoined', async () => {
const { id } = await Person.query(knex).insertGraph({
firstName: 'Arnold',
pets: [{ name: 'Catto' }, { name: 'Doggo', toys: [{ toyName: 'Bone' }] }],
});
const result = await Person.query(knex)
.findById(id)
.withGraphJoined({
pets: {
toys: true,
},
});
expect(result).to.containSubset({
firstName: 'Arnold',
pets: [
{ name: 'Catto', owner: undefined, toys: [] },
{
name: 'Doggo',
owner: undefined,
toys: [{ toyName: 'Bone' }],
},
],
});
});
it('patch', async () => {
let toy = await Toy.query(knex).insert({ toyName: 'Bone' });
await Toy.query(knex).patch({ price: 100 }).findById(toy.id);
await toy.$query(knex).patch({ toyName: 'Wheel' });
toy = await Toy.query(knex).findById(toy.id);
expect(toy.price).to.equal(100);
expect(toy.toyName).to.equal('Wheel');
toy = await Toy.query(knex).insert({ toyName: 'Wheel' });
await toy.$query(knex).update();
});
it('relatedQuery: find', async () => {
const {
id: personId,
pets: [{ id: cattoId }, { id: doggoId }],
} = await Person.query(knex).insertGraph({
firstName: 'Arnold',
pets: [{ name: 'Catto' }, { name: 'Doggo', toys: [{ toyName: 'Bone' }] }],
});
// HasManyRelation
const catto = await Person.relatedQuery('pets', knex).for(personId).findById(cattoId);
expect(catto).to.containSubset({ name: 'Catto' });
const doggo = await Person.relatedQuery('pets', knex).for(personId).findById(doggoId);
expect(doggo).to.containSubset({ name: 'Doggo' });
// BelongsToOneRelation
const person = await doggo.$relatedQuery('owner', knex);
expect(person).to.containSubset({ firstName: 'Arnold' });
// ManyToManyRelation
const toys = await Pet.relatedQuery('toys', knex).for(doggo);
expect(toys).to.have.length(1);
expect(toys).to.containSubset([{ toyName: 'Bone' }]);
});
it('relatedQuery: insert', async () => {
// BelongsToOneRelation
const doggo = await Pet.query(knex).insert({ name: 'Doggo' });
const { id: arnoldId } = await Pet.relatedQuery('owner', knex)
.for(doggo)
.insert({ firstName: 'Arnold' });
let arnold = await Person.query(knex).withGraphFetched('pets').findById(arnoldId);
expect(arnold).to.containSubset({
firstName: 'Arnold',
pets: [{ name: 'Doggo' }],
});
// HasManyRelation
const catto = await Person.relatedQuery('pets', knex)
.for(arnold.id)
.insert({ name: 'Catto' });
arnold = await Person.query(knex).withGraphFetched('pets').findById(arnoldId);
expect(arnold).to.containSubset({
firstName: 'Arnold',
pets: [{ name: 'Doggo' }, { name: 'Catto' }],
});
// ManyToManyRelation
const toy = await Pet.relatedQuery('toys', knex).for(catto).insert({ toyName: 'Bone' });
expect(toy).to.containSubset({ toyName: 'Bone' });
const result = await Person.query(knex)
.findById(arnoldId)
.withGraphJoined({
pets: {
toys: true,
},
});
expect(result).to.containSubset({
firstName: 'Arnold',
pets: [
{ name: 'Catto', owner: undefined, toys: [{ toyName: 'Bone' }] },
{
name: 'Doggo',
owner: undefined,
toys: [],
},
],
});
});
});
};
================================================
FILE: tests/integration/misc/generatedId.js
================================================
const expect = require('expect.js');
const { Model } = require('../../../');
module.exports = (session) => {
describe('generated id', () => {
let TestModel;
before(() => {
return session.knex.schema
.dropTableIfExists('generated_id_test')
.createTable('generated_id_test', (table) => {
table.string('idCol', 32).primary();
table.string('value');
});
});
after(() => {
return session.knex.schema.dropTableIfExists('generated_id_test');
});
before(() => {
TestModel = class TestModel extends Model {
static get tableName() {
return 'generated_id_test';
}
static get idColumn() {
return 'idCol';
}
$beforeInsert() {
this.idCol = 'someRandomId';
}
};
TestModel.knex(session.knex);
});
it('should return the generated id when inserted', () => {
return TestModel.query()
.insert({ value: 'hello' })
.then((ret) => {
expect(ret.idCol).to.equal('someRandomId');
return session.knex(TestModel.getTableName());
})
.then((rows) => {
expect(rows[0]).to.eql({ value: 'hello', idCol: 'someRandomId' });
});
});
});
};
================================================
FILE: tests/integration/misc/hasOneTree.js
================================================
const { Model } = require('../../../');
module.exports = (session) => {
describe('has one relation tree', () => {
let TestModel;
before(() => {
return session.knex.schema
.dropTableIfExists('has_one_relation_tree_test')
.createTable('has_one_relation_tree_test', (table) => {
table.increments('id');
table.string('value');
table.integer('previousId');
});
});
after(() => {
return session.knex.schema.dropTableIfExists('has_one_relation_tree_test');
});
before(() => {
TestModel = class TestModel extends Model {
static get tableName() {
return 'has_one_relation_tree_test';
}
static get relationMappings() {
return {
previous: {
relation: Model.BelongsToOneRelation,
modelClass: this,
join: {
from: `${this.tableName}.previousId`,
to: `${this.tableName}.id`,
},
},
next: {
relation: Model.HasOneRelation,
modelClass: this,
join: {
from: `${this.tableName}.id`,
to: `${this.tableName}.previousId`,
},
},
};
}
};
TestModel.knex(session.knex);
});
it('insertGraph should work', () => {
return TestModel.query()
.insertGraph({
value: 'root',
previous: {
value: 'previous 1',
previous: {
value: 'previous 2',
},
},
})
.then(() => {
return TestModel.query().findOne({ value: 'root' }).withGraphFetched('previous.^');
})
.then((result) => {
// console.dir(result, { depth: null });
});
});
});
};
================================================
FILE: tests/integration/misc/index.js
================================================
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
module.exports = (session) => {
describe('misc', () => {
fs.readdirSync(__dirname)
.filter((file) => _.endsWith(file, '.js'))
.filter((file) => file !== 'index.js')
.forEach((file) => require(path.join(__dirname, file))(session));
});
};
================================================
FILE: tests/integration/misc/modelWithLengthProperty.js
================================================
const expect = require('expect.js');
const { Model } = require('../../../');
module.exports = (session) => {
describe('model with `length` property', () => {
let TestModel;
before(() => {
return session.knex.schema
.dropTableIfExists('model_with_length_test')
.createTable('model_with_length_test', (table) => {
table.increments('id');
table.integer('length');
});
});
after(() => {
return session.knex.schema.dropTableIfExists('model_with_length_test');
});
before(() => {
TestModel = class TestModel extends Model {
static get tableName() {
return 'model_with_length_test';
}
};
TestModel.knex(session.knex);
});
it('should insert', () => {
return TestModel.query()
.insert({ length: 10 })
.then((model) => {
expect(model).to.eql({ id: 1, length: 10 });
return session.knex(TestModel.getTableName());
})
.then((rows) => {
expect(rows.length).to.equal(1);
expect(rows[0]).to.eql({ id: 1, length: 10 });
});
});
});
};
================================================
FILE: tests/integration/misc/multipleResultsWithOneToOneRelation.js
================================================
const expect = require('expect.js');
module.exports = (session) => {
describe('multiple results with a one-to-one relation', () => {
beforeEach(() => {
// This tests insertGraph.
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 1',
model1Relation1: {
id: 4,
model1Prop1: 'hello 2',
},
},
]);
});
it('belongs to one relation', () => {
return session.models.Model1.query()
.whereIn('id', [1, 3])
.withGraphFetched('model1Relation1')
.then((models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
},
{
id: 3,
model1Id: 4,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 4,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
},
]);
});
});
it('has one relation', () => {
return session.models.Model1.query()
.whereIn('id', [2, 4])
.withGraphFetched('model1Relation1Inverse')
.then((models) => {
expect(models).to.eql([
{
id: 2,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1Inverse: {
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
},
},
{
id: 4,
model1Id: null,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1Inverse: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
},
},
]);
});
});
});
};
================================================
FILE: tests/integration/misc/mysqlBinaryColumns.js
================================================
const expect = require('expect.js');
const { Model } = require('../../../');
module.exports = (session) => {
if (session.isMySql()) {
describe('mysql binary columns', () => {
let TestModel;
before(() => {
return session.knex.schema
.dropTableIfExists('mysql_binary_test')
.createTable('mysql_binary_test', (table) => {
table.increments('id').primary();
table.binary('binary', 4);
});
});
after(() => {
return session.knex.schema.dropTableIfExists('mysql_binary_test');
});
before(() => {
TestModel = class TestModel extends Model {
static get tableName() {
return 'mysql_binary_test';
}
};
TestModel.knex(session.knex);
});
function buffer() {
return Buffer.from([192, 168, 163, 17]);
}
function bufferEquals(a, b) {
if (!Buffer.isBuffer(a)) return false;
if (!Buffer.isBuffer(b)) return false;
if (typeof a.equals === 'function') return a.equals(b);
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
it('#insert should insert a buffer', () => {
return TestModel.query()
.insert({ binary: buffer() })
.then((ret) => {
expect(bufferEquals(buffer(), ret.binary)).to.equal(true);
return session.knex(TestModel.getTableName());
})
.then((rows) => {
expect(bufferEquals(buffer(), rows[0].binary)).to.equal(true);
});
});
});
}
};
================================================
FILE: tests/integration/misc/nonMutatingRelatedQuery.js
================================================
const { expect } = require('chai');
module.exports = (session) => {
describe('non-mutating related query', () => {
class ModelOne extends session.unboundModels.Model1 {
static get relatedFindQueryMutates() {
return false;
}
static get relatedInsertQueryMutates() {
return false;
}
}
class ModelTwo extends session.unboundModels.Model2 {
static get relatedFindQueryMutates() {
return false;
}
static get relatedInsertQueryMutates() {
return false;
}
}
before(() => {
ModelOne = ModelOne.bindKnex(session.knex);
ModelTwo = ModelTwo.bindKnex(session.knex);
});
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'root',
model1Relation1: {
id: 2,
model1Prop1: 'belongs to one',
},
model1Relation2: [
{
idCol: 3,
model2Prop1: 'has many',
},
],
model1Relation3: [
{
idCol: 4,
model2Prop1: 'many to many',
},
],
},
]);
});
describe('find', () => {
it('belongs to one', () => {
return ModelOne.query()
.findOne({ model1Prop1: 'root' })
.then((model) => {
return model.$relatedQuery('model1Relation1').then(() => model);
})
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model1Id: 2,
model1Prop1: 'root',
model1Prop2: null,
});
});
});
it('has many', () => {
return ModelOne.query()
.findOne({ model1Prop1: 'root' })
.then((model) => {
return model.$relatedQuery('model1Relation2').then(() => model);
})
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model1Id: 2,
model1Prop1: 'root',
model1Prop2: null,
});
});
});
it('many to many', () => {
return ModelOne.query()
.findOne({ model1Prop1: 'root' })
.then((model) => {
return model.$relatedQuery('model1Relation3').then(() => model);
})
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model1Id: 2,
model1Prop1: 'root',
model1Prop2: null,
});
});
});
});
describe('insert', () => {
it('belongs to one', () => {
return ModelOne.query()
.findOne({ model1Prop1: 'root' })
.then((model) => {
return model
.$relatedQuery('model1Relation1')
.insert({ id: 10, model1Prop1: 'new' })
.then(() => model);
})
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model1Id: 10,
model1Prop1: 'root',
model1Prop2: null,
});
});
});
it('has many', () => {
return ModelOne.query()
.findOne({ model1Prop1: 'root' })
.then((model) => {
return model
.$relatedQuery('model1Relation2')
.insert({ model2Prop1: 'new' })
.then(() => model);
})
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model1Id: 2,
model1Prop1: 'root',
model1Prop2: null,
});
});
});
it('many to many', () => {
return ModelOne.query()
.findOne({ model1Prop1: 'root' })
.then((model) => {
return model
.$relatedQuery('model1Relation3')
.insert({ model2Prop1: 'new' })
.then(() => model);
})
.then((model) => {
expect(model.toJSON()).to.eql({
id: 1,
model1Id: 2,
model1Prop1: 'root',
model1Prop2: null,
});
});
});
});
});
};
================================================
FILE: tests/integration/misc/refAttack.js
================================================
const { Model } = require('../../../');
const { expect } = require('chai');
module.exports = (session) => {
const { knex } = session;
describe('#ref attack', () => {
class Role extends Model {
static get tableName() {
return 'roles';
}
}
class User extends Model {
static get tableName() {
return 'users';
}
static get relationMappings() {
return {
roles: {
relation: Model.HasManyRelation,
modelClass: Role,
join: {
from: 'users.id',
to: 'roles.userId',
},
},
};
}
}
before(() => {
return knex.schema
.dropTableIfExists('users')
.dropTableIfExists('roles')
.createTable('users', (table) => {
table.increments('id').primary();
table.string('firstName');
table.string('lastName');
table.string('passwordHash');
})
.createTable('roles', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('userId');
});
});
after(() => {
return knex.schema.dropTableIfExists('users').dropTableIfExists('roles');
});
beforeEach(async () => {
await User.query(knex).delete();
await User.query(knex).insertGraph([
{
id: 1,
firstName: 'dork 1',
passwordHash: 'secret',
},
{
id: 2,
firstName: 'dork 2',
},
]);
});
it('#ref{} should not be able to dig out secrets from db', async () => {
const attackGraph = [
{
id: 1,
firstName: 'updated dork',
'#id': 'id',
},
{
id: 2,
firstName: '#ref{id.passwordHash}',
lastName: 'something to trigger an update',
roles: [
{
name: '#ref{id.passwordHash}',
},
],
},
// This gets inserted.
{
id: 3,
firstName: '#ref{id.passwordHash}',
roles: [
{
name: '#ref{id.passwordHash}',
},
],
},
];
await User.query(knex).returning('*').upsertGraph(attackGraph, {
allowRefs: true,
insertMissing: true,
});
const user2 = await User.query(knex).findById(2).withGraphFetched('roles');
const user3 = await User.query(knex).findById(3).withGraphFetched('roles');
expect(user2.firstName).to.equal('dork 2');
expect(user2.roles[0].name).to.equal(null);
expect(user3.firstName).to.equal(null);
expect(user3.roles[0].name).to.equal(null);
});
});
};
================================================
FILE: tests/integration/misc/relatedQueryErrors.js
================================================
const { expect } = require('chai');
const { Model } = require('../../../');
module.exports = (session) => {
describe('model relatedQueries fail when they lack a proper target', () => {
let knex = session.knex;
class Post extends Model {
static get tableName() {
return 'posts';
}
}
class User extends Model {
static get tableName() {
return 'users';
}
static get relationMappings() {
return {
posts: {
modelClass: Post,
relation: Model.HasManyRelation,
join: {
from: 'users.id',
to: 'posts.user_id',
},
},
};
}
}
before(async () => {
const knex = session.knex;
await knex.schema.dropTableIfExists('posts');
await knex.schema.dropTableIfExists('users');
await knex.schema
.createTable('users', (table) => {
table.increments('id').primary();
table.string('name');
})
.createTable('posts', (table) => {
table.increments('id').primary();
table.integer('user_id').unsigned().references('id').inTable('users');
table.string('content');
});
});
after(async () => {
await knex.schema.dropTableIfExists('posts');
await knex.schema.dropTableIfExists('users');
});
it('relatedQuery insert does not silently fail when ommiting `for` a target', async () => {
try {
await User.relatedQuery('posts', knex).insert({ content: 'my post content' });
} catch (e) {
expect(e.message).to.equal(
'query method `for` ommitted outside a subquery, can not figure out relation target',
);
}
});
it('relatedQuery where fails when `for` is ommited and is not a subquery', async () => {
try {
await User.relatedQuery('posts', knex).where({ content: 'my post content' });
} catch (e) {
expect(e.message).to.equal(
'query method `for` ommitted outside a subquery, can not figure out relation target',
);
}
});
});
};
================================================
FILE: tests/integration/misc/relationHooks.js
================================================
const { expect } = require('chai');
const { Model, snakeCaseMappers } = require('../../../');
module.exports = (session) => {
describe('relation hooks', () => {
describe('beforeInsert', () => {
class Model1 extends Model {
static get tableName() {
return 'Model1';
}
static get relationMappings() {
return {
model1Relation1: {
relation: Model.BelongsToOneRelation,
modelClass: Model1,
join: {
from: 'Model1.model1Id',
to: 'Model1.id',
},
beforeInsert(model, ctx) {
model.model1Prop2 = ctx.belongsToOneValue;
},
},
model1Relation1Inverse: {
relation: Model.HasOneRelation,
modelClass: Model1,
join: {
from: 'Model1.id',
to: 'Model1.model1Id',
},
},
model1Relation2: {
relation: Model.HasManyRelation,
modelClass: Model2,
join: {
from: 'Model1.id',
to: 'model2.model1_id',
},
beforeInsert(model, ctx) {
model.model2Prop2 = ctx.hasManyValue;
},
},
model1Relation3: {
relation: Model.ManyToManyRelation,
modelClass: Model2,
join: {
from: 'Model1.id',
through: {
from: 'Model1Model2.model1Id',
to: 'Model1Model2.model2Id',
extra: ['extra1', 'extra2'],
beforeInsert(model, ctx) {
model.extra2 = ctx.manyToManyJoinValue;
},
},
to: 'model2.id_col',
},
beforeInsert(model, ctx) {
model.model2Prop2 = ctx.manyToManyValue;
},
},
};
}
}
class Model2 extends Model {
static get tableName() {
return 'model2';
}
static get idColumn() {
return 'id_col';
}
static get columnNameMappers() {
return snakeCaseMappers();
}
static get relationMappings() {
return {
model2Relation1: {
relation: Model.ManyToManyRelation,
modelClass: Model1,
join: {
from: 'model2.id_col',
through: {
from: 'Model1Model2.model2Id',
to: 'Model1Model2.model1Id',
extra: { aliasedExtra: 'extra3' },
},
to: 'Model1.id',
},
},
model2Relation2: {
relation: Model.HasOneThroughRelation,
modelClass: Model1,
join: {
from: 'model2.id_col',
through: {
from: 'Model1Model2One.model2Id',
to: 'Model1Model2One.model1Id',
},
to: 'Model1.id',
},
},
};
}
}
before(() => {
Model1.knex(session.knex);
Model2.knex(session.knex);
});
beforeEach(() => {
return session.populate([
{
model1Prop1: 'root',
},
]);
});
describe('$relatedQuery', () => {
let root;
beforeEach(() => {
return Model1.query()
.findOne({ model1Prop1: 'root' })
.then((model) => {
root = model;
});
});
it('belongs to one relation', () => {
return root
.$relatedQuery('model1Relation1')
.insert({ model1Prop1: 'new' })
.context({ belongsToOneValue: 42 })
.then((model) => {
return session.knex(Model1.getTableName()).where({ model1Prop1: 'new' }).first();
})
.then((row) => {
expect(row.model1Prop2).to.equal(42);
});
});
it('has many relation', () => {
return root
.$relatedQuery('model1Relation2')
.insert({ model2Prop1: 'new' })
.context({ hasManyValue: 100 })
.then((model) => {
return session.knex(Model2.getTableName()).where({ model2_prop1: 'new' }).first();
})
.then((row) => {
expect(row.model2_prop2).to.equal(100);
});
});
it('many to many relation (insert)', () => {
return root
.$relatedQuery('model1Relation3')
.insert({ model2Prop1: 'new' })
.context({
manyToManyValue: 7,
manyToManyJoinValue: 'Hello',
})
.then((model) => {
return session.knex(Model2.getTableName()).where({ model2_prop1: 'new' }).first();
})
.then((row) => {
expect(row.model2_prop2).to.equal(7);
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows.length).to.equal(1);
expect(rows[0].extra2).to.equal('Hello');
});
});
it('many to many relation (relate)', () => {
return Model2.query()
.insert({ model2Prop1: 'rel' })
.then((model) => {
return root.$relatedQuery('model1Relation3').relate(model.idCol).context({
manyToManyJoinValue: 'Extra',
});
})
.then(() => {
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows.length).to.equal(1);
expect(rows[0].extra2).to.equal('Extra');
});
});
it('insertGraph', () => {
return Model1.query()
.context({
belongsToOneValue: 1,
hasManyValue: 2,
manyToManyValue: 3,
manyToManyJoinValue: 4,
})
.insertGraph({
model1Prop1: 'parent',
model1Relation1: {
model1Prop1: 'child1',
},
model1Relation2: [
{
model2Prop1: 'child2',
},
{
model2Prop1: 'child3',
},
],
model1Relation3: [
{
model2Prop1: 'child4',
},
{
model2Prop1: 'child5',
},
],
})
.then(() => {
return Model1.query()
.findOne({ model1Prop1: 'parent' })
.withGraphFetched('[model1Relation1, model1Relation2, model1Relation3]')
.then((model) => {
expect(model).to.containSubset({
model1Prop1: 'parent',
model1Relation1: {
model1Prop1: 'child1',
model1Prop2: 1,
},
model1Relation2: [
{
model2Prop1: 'child2',
model2Prop2: 2,
},
{
model2Prop1: 'child3',
model2Prop2: 2,
},
],
model1Relation3: [
{
model2Prop1: 'child4',
model2Prop2: 3,
extra2: '4',
},
{
model2Prop1: 'child5',
model2Prop2: 3,
extra2: '4',
},
],
});
});
});
});
it('upsertGraph', () => {
return Model1.query()
.insertGraph({
model1Prop1: 'parent',
model1Relation1: null,
model1Relation2: [
{
model2Prop1: 'child2',
},
],
model1Relation3: [
{
model2Prop1: 'child5',
},
],
})
.then((model) => {
return Model1.query()
.context({
belongsToOneValue: 1,
hasManyValue: 2,
manyToManyValue: 3,
manyToManyJoinValue: 4,
})
.upsertGraph({
id: model.id,
model1Prop1: 'parent',
model1Relation1: {
model1Prop1: 'child1',
},
model1Relation2: [
{
idCol: model.model1Relation2[0].idCol,
model2Prop1: 'child2',
},
{
model2Prop1: 'child3',
},
],
model1Relation3: [
{
model2Prop1: 'child4',
},
{
idCol: model.model1Relation3[0].idCol,
model2Prop1: 'child5',
},
],
});
})
.then(() => {
return Model1.query()
.findOne({ model1Prop1: 'parent' })
.withGraphFetched('[model1Relation1, model1Relation2, model1Relation3]')
.then((model) => {
expect(model).to.containSubset({
model1Prop1: 'parent',
model1Relation1: {
model1Prop1: 'child1',
model1Prop2: 1,
},
model1Relation2: [
{
model2Prop1: 'child2',
model2Prop2: null,
},
{
model2Prop1: 'child3',
model2Prop2: 2,
},
],
model1Relation3: [
{
model2Prop1: 'child4',
model2Prop2: 3,
extra2: '4',
},
{
model2Prop1: 'child5',
model2Prop2: null,
extra2: null,
},
],
});
});
});
});
});
});
});
};
================================================
FILE: tests/integration/misc/tableMetadata.js
================================================
const expect = require('expect.js');
const { Model } = require('../../../');
const mockKnexFactory = require('../../../testUtils/mockKnex');
module.exports = (session) => {
describe('tableMetadata', () => {
let knex;
let queries = [];
let Table1;
let UnboundTable1;
let OverriddenTable1;
before(() => {
return session.knex.schema.dropTableIfExists('table1').createTable('table1', (table) => {
table.increments('id').primary();
table.integer('relId');
table.string('value');
});
});
before(() => {
knex = mockKnexFactory(session.knex, function (mock, oldImpl, args) {
queries.push(this.toString());
return oldImpl.apply(this, args);
});
});
after(() => {
return Promise.all([session.knex.schema.dropTableIfExists('table1')]);
});
beforeEach(() => {
Table1 = class Table1 extends Model {
static get tableName() {
return 'table1';
}
};
OverriddenTable1 = class OverriddenTable1 extends Model {
static get tableName() {
return 'table1';
}
static get jsonSchema() {
return {
type: 'object',
properties: {
id: { type: 'integer' },
relId: { type: 'integer' },
value: { type: 'string' },
},
};
}
static tableMetadata() {
return {
columns: Object.keys(this.jsonSchema.properties),
};
}
static get relationMappings() {
return {
rel: {
relation: Model.BelongsToOneRelation,
modelClass: OverriddenTable1,
join: {
from: 'table1.relId',
to: 'table1.id',
},
},
};
}
};
UnboundTable1 = Table1;
Table1 = Table1.bindKnex(knex);
queries = [];
});
it('should fetch metadata', () => {
return Promise.all([
Table1.fetchTableMetadata(),
Table1.fetchTableMetadata(),
Table1.fetchTableMetadata(),
Table1.fetchTableMetadata(),
])
.then((metadatas) => {
// Only one query should have been generated.
expect(queries).to.have.length(1);
metadatas.forEach((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
expect(metadata === metadatas[0]).to.equal(true);
});
expect(Table1.tableMetadata()).to.eql({
columns: ['id', 'relId', 'value'],
});
return Table1.fetchTableMetadata();
})
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
// Cache should be used the second time.
expect(queries).to.have.length(1);
return Table1.fetchTableMetadata({ force: true });
})
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
// Cache should be ignored if `force = true`.
expect(queries).to.have.length(2);
});
});
it('should accept knex instance as an argument', () => {
return UnboundTable1.fetchTableMetadata({ knex })
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
// Only one query should have been generated.
expect(queries).to.have.length(1);
return Table1.fetchTableMetadata({ knex });
})
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
// Cache should be used the second time.
expect(queries).to.have.length(1);
return Table1.fetchTableMetadata({ knex, force: true });
})
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
// Cache should be ignored if `force = true`.
expect(queries).to.have.length(2);
});
});
it('should accept knex instance as an argument', () => {
return UnboundTable1.fetchTableMetadata({ knex })
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
// Only one query should have been generated.
expect(queries).to.have.length(1);
return Table1.fetchTableMetadata({ knex });
})
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
// Cache should be used the second time.
expect(queries).to.have.length(1);
return Table1.fetchTableMetadata({ knex, force: true });
})
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
// Cache should be ignored if `force = true`.
expect(queries).to.have.length(2);
});
});
it('fetchTableMetadata should use tableMetadata function if overridden', () => {
return OverriddenTable1.fetchTableMetadata()
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
expect(queries).to.have.length(0);
return OverriddenTable1.fetchTableMetadata();
})
.then((metadata) => {
expect(metadata.columns).to.eql(['id', 'relId', 'value']);
expect(queries).to.have.length(0);
});
});
it('joinEager should work with overridden tableMetadata', () => {
const metadata = OverriddenTable1.tableMetadata();
expect(metadata).to.eql({
columns: ['id', 'relId', 'value'],
});
return OverriddenTable1.query(knex)
.insertGraph({
value: '1',
rel: {
value: '2',
rel: {
value: '3',
},
},
})
.then(() => {
return OverriddenTable1.query(knex).withGraphJoined('rel.rel').where('table1.value', '1');
})
.then((res) =>
res.map((it) => ({
value: it.value,
rel: { value: it.rel.value, rel: { value: it.rel.rel.value } },
})),
)
.then((res) => {
if (session.isPostgres()) {
expect(queries[queries.length - 1]).to.eql(
`select "table1"."id" as "id", "table1"."relId" as "relId", "table1"."value" as "value", "rel"."id" as "rel:id", "rel"."relId" as "rel:relId", "rel"."value" as "rel:value", "rel:rel"."id" as "rel:rel:id", "rel:rel"."relId" as "rel:rel:relId", "rel:rel"."value" as "rel:rel:value" from "table1" left join "table1" as "rel" on "rel"."id" = "table1"."relId" left join "table1" as "rel:rel" on "rel:rel"."id" = "rel"."relId" where "table1"."value" = '1'`,
);
}
expect(res).to.eql([
{
value: '1',
rel: {
value: '2',
rel: {
value: '3',
},
},
},
]);
});
});
});
};
================================================
FILE: tests/integration/misc/unhandledRejectionErrors.js
================================================
const Promise = require('bluebird');
const expect = require('expect.js');
module.exports = (session) => {
// Tests that various queries that start multiple queries behind the scenes
// don't cause unhandled rejection errors if one of the queries fail.
describe('unhandler rejection errors', () => {
const Model1 = session.models.Model1;
let unhandledErrors = [];
const unhandledRejectionHandler = (err) => {
unhandledErrors.push(err);
};
before(() => {
session.addUnhandledRejectionHandler(unhandledRejectionHandler);
});
after(() => {
session.removeUnhandledRejectionHandler(unhandledRejectionHandler);
});
beforeEach(() => {
unhandledErrors = [];
});
beforeEach(() => {
return session.populate([
{
model1Prop1: '1',
model1Relation1: {
model1Prop1: '3',
},
model1Relation2: [
{
model2Prop1: '1',
},
],
},
{
model1Prop1: '2',
model1Relation1: {
model1Prop1: '4',
},
model1Relation2: [
{
model2Prop1: '2',
},
],
},
]);
});
it('range', (done) => {
Model1.query()
.table('doesnt_exist')
.range(1, 2)
.then(() => done(new Error('should not get here')))
.catch((err) => {
expect(unhandledErrors).to.be.empty();
done();
})
.catch(done);
});
});
};
================================================
FILE: tests/integration/misc/usingUnboundModelsByPassingKnex.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const Promise = require('bluebird');
const { createRejectionReflection } = require('../../../testUtils/testUtils');
module.exports = (session) => {
describe('using unbound models by passing a knex to query', () => {
let Model1 = session.unboundModels.Model1;
let Model2 = session.unboundModels.Model2;
beforeEach(() => {
// This tests insertGraph.
return session.populate([]).then(() => {
return Model1.query(session.knex).insertGraph([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
model1Relation1: {
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'hejsan 4',
},
],
},
},
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'hejsan 1',
},
{
idCol: 2,
model2Prop1: 'hejsan 2',
model2Relation1: [
{
id: 5,
model1Prop1: 'hello 5',
aliasedExtra: 'extra 5',
},
{
id: 6,
model1Prop1: 'hello 6',
aliasedExtra: 'extra 6',
model1Relation1: {
id: 7,
model1Prop1: 'hello 7',
},
model1Relation2: [
{
idCol: 3,
model2Prop1: 'hejsan 3',
},
],
},
],
},
],
},
]);
});
});
it('basic wheres', () => {
const query = Model1.query().orWhereNot('id', '>', 10).whereIn('id', [1, 8, 11]);
return query.knex(session.knex).then((models) => {
expect(models[0].model1Prop1).to.equal('hello 1');
});
});
it('findById', () => {
const query = Model1.query().findById(1);
return query.knex(session.knex).then((model) => {
expect(model.model1Prop1).to.equal('hello 1');
});
});
it('findById composite', () => {
class TestModel extends Model1 {
static get idColumn() {
return ['id', 'model1Prop1'];
}
}
const query = TestModel.query().findById([1, 'hello 1']);
return query.knex(session.knex).then((model) => {
expect(model.model1Prop1).to.equal('hello 1');
});
});
it('eager', () => {
return Promise.all([
// Give connection after building the query.
Model1.query()
.findById(1)
.withGraphJoined(
'[model1Relation1, model1Relation2.model2Relation1.[model1Relation1, model1Relation2]]',
)
.knex(session.knex),
Model1.query(session.knex)
.findById(1)
.withGraphFetched(
'[model1Relation1, model1Relation2.model2Relation1.[model1Relation1, model1Relation2]]',
),
Model1.query(session.knex)
.findById(1)
.withGraphJoined(
'[model1Relation1, model1Relation2.model2Relation1.[model1Relation1, model1Relation2]]',
),
]).then((results) => {
results.forEach((models) => {
expect(sortRelations(models)).to.eql({
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
aliasedExtra: 'extra 5',
model1Relation1: null,
model1Relation2: [],
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
model1Relation1: {
id: 7,
model1Id: null,
model1Prop1: 'hello 7',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
],
},
],
});
});
});
});
describe('subqueries', () => {
it('basic', () => {
const query = Model1.query().whereIn('id', Model1.query().select('id').where('id', 5));
return query.knex(session.knex).then((models) => {
expect(models[0].model1Prop1).to.equal('hello 5');
});
});
it('joinRelated in subquery', () => {
const query = Model1.query().whereIn(
'id',
Model1.query()
.select('Model1.id')
.joinRelated('model1Relation1')
.where('model1Relation1.id', 4),
);
return query.knex(session.knex).then((models) => {
expect(models[0].id).to.equal(3);
});
});
it('static relatedQuery', () => {
const query = Model1.query()
.findById(1)
.select('Model1.*', Model1.relatedQuery('model1Relation2').count().as('count'));
return query.knex(session.knex).then((model) => {
expect(model.count).to.eql(2);
});
});
});
describe('$relatedQuery', () => {
it('fetch', () => {
return Promise.all([
Model1.query(session.knex)
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation1', session.knex);
}),
Model1.query(session.knex)
.findById(2)
.then((model) => {
return model.$relatedQuery('model1Relation1Inverse', session.knex);
}),
Model1.query(session.knex)
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation2', session.knex);
}),
Model2.query(session.knex)
.findById(2)
.then((model) => {
return model.$relatedQuery('model2Relation1', session.knex);
}),
]).then((results) => {
expect(results[0]).to.eql({
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
});
expect(results[1]).to.eql({
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
});
expect(_.sortBy(results[2], 'idCol')).to.eql([
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
},
]);
expect(_.sortBy(results[3], 'id')).to.eql([
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
aliasedExtra: 'extra 5',
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
]);
});
});
});
describe('$query', () => {
it('fetch', () => {
return Promise.all([
Model1.query(session.knex)
.findById(1)
.then((model) => {
return model.$query(session.knex);
}),
]).then((model) => {
expect(model).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
},
]);
});
});
it('insert', () => {
return Model1.fromJson({ model1Prop1: 'foo', id: 100 })
.$query(session.knex)
.insert()
.then((model) => {
expect(model).to.eql({
id: 100,
model1Prop1: 'foo',
$afterInsertCalled: 1,
$beforeInsertCalled: 1,
});
});
});
it('insertAndFetch', () => {
return Model1.fromJson({ model1Prop1: 'foo', id: 101 })
.$query(session.knex)
.insertAndFetch()
.then((model) => {
expect(model).to.eql({
id: 101,
model1Id: null,
model1Prop1: 'foo',
model1Prop2: null,
$afterInsertCalled: 1,
$beforeInsertCalled: 1,
});
});
});
});
it('joinRelated (BelongsToOneRelation)', () => {
return Model1.query(session.knex)
.select('Model1.id as id', 'model1Relation1.id as relId')
.innerJoinRelated('model1Relation1')
.then((models) => {
expect(_.sortBy(models, 'id')).to.eql([
{ id: 1, relId: 2, $afterFindCalled: 1 },
{ id: 2, relId: 3, $afterFindCalled: 1 },
{ id: 3, relId: 4, $afterFindCalled: 1 },
{ id: 6, relId: 7, $afterFindCalled: 1 },
]);
});
});
it('joinRelated (ManyToManyRelation)', () => {
return Model1.query(session.knex)
.select('Model1.id as id', 'model1Relation3.id_col as relId')
.innerJoinRelated('model1Relation3')
.then((models) => {
expect(_.sortBy(models, 'id')).to.eql([
{ id: 5, relId: 2, $afterFindCalled: 1 },
{ id: 6, relId: 2, $afterFindCalled: 1 },
]);
});
});
it('should fail with a descriptive error message if knex is not provided', () => {
return Promise.all([
Promise.try(() => {
return Model1.query()
.findById(1)
.withGraphFetched(
'[model1Relation1, model1Relation2.model2Relation1.[model1Relation1, model1Relation2]]',
);
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model1.query()
.findById(1)
.withGraphJoined(
'[model1Relation1, model1Relation2.model2Relation1.[model1Relation1, model1Relation2]]',
);
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model1.query();
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model1.query().where('id', 1);
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model1.query().joinRelated('model1Relation1');
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model1.query(session.knex)
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation1');
});
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model1.query(session.knex)
.findById(2)
.then((model) => {
return model.$relatedQuery('model1Relation1Inverse');
});
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model1.query(session.knex)
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation2');
});
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model2.query(session.knex)
.findById(2)
.then((model) => {
return model.$relatedQuery('model2Relation1');
});
}).catch((err) => createRejectionReflection(err)),
Promise.try(() => {
return Model1.query(session.knex)
.findById(1)
.then((model) => {
return model.$query();
});
}).catch((err) => createRejectionReflection(err)),
]).then((results) => {
results.forEach((result) => {
expect(result.isRejected()).to.equal(true);
expect(result.reason().message).to.match(
/no database connection available for a query. You need to bind the model class or the query to a knex instance./,
);
});
});
});
function sortRelations(models) {
Model1.traverse(models, (model) => {
if (model.model1Relation2) {
model.model1Relation2 = _.sortBy(model.model1Relation2, 'idCol');
}
if (model.model2Relation1) {
model.model2Relation1 = _.sortBy(model.model2Relation1, 'id');
}
});
return models;
}
});
};
================================================
FILE: tests/integration/misc/zeroValueInRelationColumn.js
================================================
const expect = require('expect.js');
const { Model } = require('../../../');
module.exports = (session) => {
describe('zero value in relation column', () => {
let Table1;
let Table2;
before(() => {
return session.knex.schema
.dropTableIfExists('table1')
.dropTableIfExists('table2')
.createTable('table1', (table) => {
table.increments('id').primary();
table.integer('value').notNullable();
})
.createTable('table2', (table) => {
table.increments('id').primary();
table.integer('value').notNullable();
});
});
after(() => {
return Promise.all([
session.knex.schema.dropTableIfExists('table1'),
session.knex.schema.dropTableIfExists('table2'),
]);
});
before(() => {
Table1 = class Table1 extends Model {
static get tableName() {
return 'table1';
}
static get relationMappings() {
return {
relation: {
relation: Model.HasManyRelation,
modelClass: Table2,
join: {
from: 'table1.value',
to: 'table2.value',
},
},
};
}
};
Table2 = class Table2 extends Model {
static get tableName() {
return 'table2';
}
};
Table1.knex(session.knex);
Table2.knex(session.knex);
});
before(() => {
return Promise.all([
Table1.query().insert({ id: 1, value: 0 }),
Table1.query().insert({ id: 2, value: 1 }),
Table2.query().insert({ id: 1, value: 0 }),
Table2.query().insert({ id: 2, value: 1 }),
]);
});
it('should work with zero value', () => {
return Table1.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('relation');
})
.then((models) => {
expect(models).to.eql([{ id: 1, value: 0 }]);
});
});
});
};
================================================
FILE: tests/integration/modifiers.js
================================================
const { Model } = require('../../');
const expect = require('chai').expect;
module.exports = (session) => {
describe('modifiers', () => {
class Person extends Model {
static get tableName() {
return 'person';
}
static get relationMappings() {
return {
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'person.parentId',
to: 'person.id',
},
},
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'person.id',
to: 'animal.ownerId',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'person.id',
through: {
from: 'personMovie.personId',
to: 'personMovie.movieId',
},
to: 'movie.id',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'animal';
}
static get modifiers() {
return {
filterByName(query, name) {
query.where('name', name);
},
};
}
}
class Movie extends Model {
static get tableName() {
return 'movie';
}
static get modifiers() {
return {
atLeastStars(query, starLimit = 4) {
query.where('stars', '>=', starLimit);
},
};
}
}
before(() => {
return session.knex.schema
.dropTableIfExists('personMovie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person')
.createTable('person', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('parentId');
})
.createTable('animal', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('ownerId');
})
.createTable('movie', (table) => {
table.increments('id').primary();
table.integer('stars').defaultTo(0).notNullable();
table.string('name');
})
.createTable('personMovie', (table) => {
table.integer('personId');
table.integer('movieId');
});
});
before(() => {
Person.knex(session.knex);
Animal.knex(session.knex);
Movie.knex(session.knex);
});
beforeEach(() => {
return Person.query()
.delete()
.then(() => Animal.query().delete())
.then(() => Movie.query().delete())
.then(() => {
return Person.query().insertGraph([
{
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
stars: 4,
},
{
name: 'Terminator 2',
stars: 3,
},
{
name: 'Terminator 3',
stars: 2,
},
],
},
{
name: 'Meinhard',
pets: [
{
name: 'Ruffus',
},
],
},
]);
});
});
it('eager', async () => {
const arnold = await Person.query()
.findOne('name', 'Arnold')
.withGraphFetched('[movies(goodMovies), pets(onlyDictators)]')
.modifiers({
goodMovies(query) {
query.modify('atLeastStars', 3);
},
onlyDictators(query) {
query.modify('filterByName', 'Stalin');
},
});
for (const movie of arnold.movies) {
expect(movie.stars).to.be.greaterThan(2);
}
expect(arnold.pets.length).to.equal(1);
expect(arnold.pets[0].name).to.equal('Stalin');
});
it('joinEager', async () => {
const arnold = await Person.query()
.findOne('person.name', 'Arnold')
.withGraphJoined('[movies(goodMovies), pets(onlyDictators)]')
.modifiers({
goodMovies(query) {
query.modify('atLeastStars', 3);
},
onlyDictators(query) {
query.modify('filterByName', 'Stalin');
},
});
for (const movie of arnold.movies) {
expect(movie.stars).to.be.greaterThan(2);
}
expect(arnold.pets.length).to.equal(1);
expect(arnold.pets[0].name).to.equal('Stalin');
});
it('joinRelated', async () => {
const result = await Person.query()
.where('person.name', 'Arnold')
.select('person.name', 'movies.name as movieName', 'pets.name as petName')
.joinRelated('[movies(goodMovies), pets(onlyDictators)]')
.modifiers({
goodMovies(query) {
query.modify('atLeastStars', 3);
},
onlyDictators(query) {
query.modify('filterByName', 'Stalin');
},
})
.orderBy(['person.name', 'movies.name', 'pets.name']);
expect(result).to.eql([
{ name: 'Arnold', movieName: 'Terminator', petName: 'Stalin' },
{ name: 'Arnold', movieName: 'Terminator 2', petName: 'Stalin' },
]);
});
after(() => {
return session.knex.schema
.dropTableIfExists('personMovie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person');
});
});
};
================================================
FILE: tests/integration/nonPrimaryKeyRelations.js
================================================
const { Model, raw } = require('../../');
const { expect } = require('chai');
const { orderBy } = require('lodash');
module.exports = (session) => {
describe("relations that don't use the primary keys", () => {
class Person extends Model {
static get tableName() {
return 'person';
}
static get relationMappings() {
return {
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'person.parentName',
to: 'person.name',
},
},
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'person.name',
to: 'animal.ownerName',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'person.name',
through: {
modelClass: PersonMovie,
from: 'personMovie.personName',
to: 'personMovie.movieName',
},
to: 'movie.name',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'animal';
}
}
class Movie extends Model {
static get tableName() {
return 'movie';
}
}
class PersonMovie extends Model {
static get tableName() {
return 'personMovie';
}
static get idColumn() {
return ['personName', 'movieName'];
}
static uniqueTag() {
return 'personMovie_nonPrimaryKeys';
}
}
before(() => {
return session.knex.schema
.dropTableIfExists('personMovie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person')
.createTable('person', (table) => {
table.increments('id').primary();
table.string('name');
table.string('nickname');
table.string('parentName');
})
.createTable('animal', (table) => {
table.increments('id').primary();
table.string('name');
table.string('nickname');
table.string('ownerName');
})
.createTable('movie', (table) => {
table.increments('id').primary();
table.string('name');
table.string('altName');
})
.createTable('personMovie', (table) => {
table.string('personName');
table.string('movieName');
});
});
before(() => {
Person.knex(session.knex);
PersonMovie.knex(session.knex);
Animal.knex(session.knex);
Movie.knex(session.knex);
});
beforeEach(() => {
return Person.query()
.delete()
.then(() => PersonMovie.query().delete())
.then(() => Animal.query().delete())
.then(() => Movie.query().delete())
.then(() => {
return Person.query().insertGraph([
{
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
},
{
name: 'Meinhard',
pets: [
{
name: 'Ruffus',
},
],
},
]);
});
});
describe('$relatedQuery', () => {
describe('belongs to one relation', () => {
it('find', () => {
return findArnold()
.then((arnold) => arnold.$relatedQuery('parent'))
.then((gustav) => expect(gustav.name).to.eql('Gustav'));
});
it('update', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('parent').update({ nickname: 'Gus' });
})
.then((numUpdated) => expect(numUpdated).to.equal(1))
.then(findGustav)
.then((gustav) => expect(gustav.nickname).to.equal('Gus'));
});
it('delete', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('parent').delete();
})
.then((numDeleted) => expect(numDeleted).to.equal(1))
.then(findGustav)
.then((gustav) => expect(gustav).to.equal(undefined));
});
it('insert', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('parent').insert({ name: 'Gustav-neue' });
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('parent'))
.then((gustavNeue) => expect(gustavNeue.name).to.equal('Gustav-neue'))
.then(findGustav)
.then((gustav) => expect(gustav.name).to.equal('Gustav'));
});
it('relate', () => {
return Promise.all([findArnold(), findMeinhard()])
.then(([arnold, meinhard]) => {
return arnold.$relatedQuery('parent').relate(meinhard.name);
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('parent'))
.then((meinhard) => expect(meinhard.name).to.equal('Meinhard'))
.then(findGustav)
.then((gustav) => expect(gustav.name).to.equal('Gustav'));
});
it('unrelate', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('parent').unrelate();
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('parent'))
.then((parent) => expect(parent).to.eql(undefined))
.then(findGustav)
.then((gustav) => expect(gustav.name).to.equal('Gustav'));
});
});
describe('has many relation', () => {
it('find', () => {
return findArnold()
.then((arnold) => arnold.$relatedQuery('pets'))
.then((pets) => expect(pets.map((it) => it.name).sort()).to.eql(['Freud', 'Stalin']));
});
it('update', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('pets').update({ nickname: concat('name', "'zilla'") });
})
.then((numUpdated) => expect(numUpdated).to.equal(2))
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('pets'))
.then((pets) => {
expect(orderBy(pets, 'nickname').map((pet) => pet.nickname)).to.eql([
'Freudzilla',
'Stalinzilla',
]);
});
});
it('delete', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('pets').delete();
})
.then((numDeleted) => expect(numDeleted).to.equal(2))
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('pets'))
.then((pets) => expect(pets).to.have.length(0));
});
it('insert', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('pets').insert({ name: 'Cat' });
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('pets').orderBy('name').select('name'))
.then((pets) => expect(pets.map((it) => it.name)).to.eql(['Cat', 'Freud', 'Stalin']));
});
it('relate', () => {
return Promise.all([findArnold(), insertTerminator3()])
.then(([arnold, terminator3]) => {
return arnold.$relatedQuery('movies').relate(terminator3.name);
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('movies').orderBy('name').select('name'))
.then((movies) =>
expect(movies.map((it) => it.name)).to.eql([
'Terminator',
'Terminator 2',
'Terminator 3',
]),
);
});
it('unrelate', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('movies').unrelate();
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('movies'))
.then((movies) => expect(movies).to.eql([]));
});
});
describe('many to many relation', () => {
it('find', () => {
return findArnold()
.then((arnold) => arnold.$relatedQuery('movies').orderBy('name'))
.then((movies) =>
expect(movies.map((it) => it.name)).to.eql(['Terminator', 'Terminator 2']),
);
});
it('update', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('movies')
.where('name', 'Terminator')
.patch({ altName: concat('name', "': This Time its Personal'") });
})
.then((numUpdated) => expect(numUpdated).to.equal(1))
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('movies'))
.then((movies) => {
expect(movies.length).to.equal(2);
expect(movies.filter((it) => it.altName).map((it) => it.altName)).to.eql([
'Terminator: This Time its Personal',
]);
});
});
it('delete', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('movies').delete().where('name', 'Terminator 2');
})
.then((numDeleted) => expect(numDeleted).to.equal(1))
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('movies'))
.then((movies) => {
expect(movies.map((it) => it.name)).to.eql(['Terminator']);
});
});
it('relate', () => {
return Promise.all([findArnold(), findMeinhard()])
.then(([arnold, meinhard]) => {
return arnold.$relatedQuery('parent').relate(meinhard.name);
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('parent'))
.then((meinhard) => expect(meinhard.name).to.equal('Meinhard'))
.then(findGustav)
.then((gustav) => expect(gustav.name).to.equal('Gustav'));
});
it('unrelate', () => {
return findArnold()
.then((arnold) => {
return arnold.$relatedQuery('parent').unrelate();
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('parent'))
.then((parent) => expect(parent).to.eql(undefined))
.then(findGustav)
.then((gustav) => expect(gustav.name).to.equal('Gustav'));
});
});
});
it('eager', () => {
return Person.query()
.withGraphFetched({
parent: true,
pets: true,
movies: true,
})
.whereExists(Person.relatedQuery('pets'))
.orderBy('name')
.then((result) => {
expect(result.length).to.equal(2);
expect(result).to.containSubset([
{
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
},
{
name: 'Meinhard',
pets: [
{
name: 'Ruffus',
},
],
},
]);
});
});
it('joinEager', () => {
return Person.query()
.withGraphJoined({
parent: true,
pets: true,
movies: true,
})
.whereExists(Person.relatedQuery('pets'))
.orderBy('person.name')
.then((result) => {
expect(result.length).to.equal(2);
expect(result).to.containSubset([
{
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
},
{
name: 'Meinhard',
pets: [
{
name: 'Ruffus',
},
],
},
]);
});
});
describe('upsertGraph', () => {
describe('belongs to one relation', () => {
it('insert', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.parent = { name: 'Kustaa' };
return Person.query().upsertGraph(arnold, { fetchStrategy: 'OnlyNeeded' });
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Kustaa',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
})
.then(findGustav)
.then((gustav) => {
expect(gustav).to.equal(undefined);
});
});
it('delete', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.parent = null;
return Person.query().upsertGraph(arnold, { fetchStrategy: 'OnlyNeeded' });
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: null,
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
})
.then(findGustav)
.then((gustav) => {
expect(gustav).to.equal(undefined);
});
});
it('relate', () => {
// TODO: Could be optimized! Useless update happens.
return Promise.all([findArnoldEagerly(), insertTeppo()])
.then(([arnold, teppo]) => {
arnold.parent = teppo;
return Person.query().upsertGraph(arnold, {
fetchStrategy: 'OnlyNeeded',
relate: ['parent'],
unrelate: ['parent'],
});
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Teppo',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
})
.then(findGustav)
.then((gustav) => {
expect(gustav.name).to.equal('Gustav');
});
});
it('unrelate', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.parent = null;
return Person.query().upsertGraph(arnold, {
fetchStrategy: 'OnlyNeeded',
relate: ['parent'],
unrelate: ['parent'],
});
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: null,
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
})
.then(findGustav)
.then((gustav) => {
expect(gustav.name).to.equal('Gustav');
});
});
});
describe('has many relation', () => {
it('insert', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.pets.push({
name: 'Tahvo',
});
return Person.query().upsertGraph(arnold, { fetchStrategy: 'OnlyNeeded' });
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
{
name: 'Tahvo',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
});
});
it('delete', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.pets = arnold.pets.filter((it) => it.name !== 'Stalin');
return Person.query().upsertGraph(arnold, { fetchStrategy: 'OnlyNeeded' });
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
})
.then(findStalin)
.then((stalin) => {
expect(stalin).to.equal(undefined);
});
});
it('relate', () => {
return Promise.all([findArnoldEagerly(), insertTahvo()])
.then(([arnold, tahvo]) => {
arnold.pets.push(tahvo);
return Person.query().upsertGraph(arnold, {
fetchStrategy: 'OnlyNeeded',
relate: ['pets'],
unrelate: ['pets'],
});
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
{
name: 'Tahvo',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
})
.then(() => Animal.query().where('name', 'Tahvo'))
.then((tahvos) => {
expect(tahvos.length).to.equal(1);
});
});
it('unrelate', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.pets = arnold.pets.filter((it) => it.name !== 'Stalin');
return Person.query().upsertGraph(arnold, {
fetchStrategy: 'OnlyNeeded',
relate: ['pets'],
unrelate: ['pets'],
});
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
});
})
.then(findStalin)
.then((stalin) => {
expect(stalin.name).to.equal('Stalin');
expect(stalin.ownerName).to.equal(null);
});
});
});
describe('many to many relation', () => {
it('insert', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.movies.push({ name: 'Terminator 3' });
return Person.query().upsertGraph(arnold, { fetchStrategy: 'OnlyNeeded' });
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
{
name: 'Terminator 3',
},
],
});
});
});
it('delete', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.movies = arnold.movies.filter((it) => it.name !== 'Terminator');
return Person.query().upsertGraph(arnold, { fetchStrategy: 'OnlyNeeded' });
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator 2',
},
],
});
})
.then(findTerminator)
.then((terminator) => {
expect(terminator).to.equal(undefined);
});
});
it('relate', () => {
return Promise.all([findArnoldEagerly(), insertTerminator3()])
.then(([arnold, terminator3]) => {
arnold.movies.push(terminator3);
return Person.query().upsertGraph(arnold, {
fetchStrategy: 'OnlyNeeded',
relate: ['movies'],
unrelate: ['movies'],
});
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
{
name: 'Terminator 3',
},
],
});
})
.then(() => Movie.query().where('name', 'Terminator 3'))
.then((terminator3s) => {
expect(terminator3s.length).to.equal(1);
});
});
it('unrelate', () => {
return findArnoldEagerly()
.then((arnold) => {
arnold.movies = arnold.movies.filter((it) => it.name !== 'Terminator');
return Person.query().upsertGraph(arnold, {
fetchStrategy: 'OnlyNeeded',
relate: ['movies'],
unrelate: ['movies'],
});
})
.then(findArnoldEagerly)
.then((arnold) => {
expect(arnold).to.containSubset({
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator 2',
},
],
});
})
.then(findTerminator)
.then((terminator) => {
expect(terminator.name).to.equal('Terminator');
});
});
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('personMovie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person');
});
function findArnold() {
return Person.query().findOne('name', 'Arnold');
}
function findArnoldEagerly() {
return Person.query().findOne('name', 'Arnold').withGraphFetched({
parent: true,
pets: true,
movies: true,
});
}
function findGustav() {
return Person.query().findOne('name', 'Gustav');
}
function findMeinhard() {
return Person.query().findOne('name', 'Meinhard');
}
function findStalin() {
return Animal.query().findOne('name', 'Stalin');
}
function findTerminator() {
return Movie.query().findOne('name', 'Terminator');
}
function insertTeppo() {
return Person.query().insert({ name: 'Teppo' });
}
function insertTahvo() {
return Animal.query().insert({ name: 'Tahvo' });
}
function insertTerminator3() {
return Movie.query().insert({ name: 'Terminator 3' });
}
});
function concat(str1, str2) {
if (session.isMySql()) {
return raw(`CONCAT(${str1}, ${str2})`);
} else {
return raw(`${str1} || ${str2}`);
}
}
};
================================================
FILE: tests/integration/patch.js
================================================
const _ = require('lodash');
const chai = require('chai');
const expect = require('expect.js');
const Promise = require('bluebird');
const { inheritModel } = require('../../lib/model/inheritModel');
const { expectPartialEqual: expectPartEql } = require('./../../testUtils/testUtils');
const { Model, QueryBuilder, ValidationError, raw } = require('../../');
const { isPostgres, isSqlite } = require('../../lib/utils/knexUtils');
const mockKnexFactory = require('../../testUtils/mockKnex');
module.exports = (session) => {
const Model1 = session.models.Model1;
const Model2 = session.models.Model2;
describe('Model patch queries', () => {
describe('.query().patch()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 2,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 1,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
{
id: 3,
model1Prop1: 'hello 3',
},
]);
});
it('should patch a model (1)', () => {
let model = Model1.fromJson({ model1Prop1: 'updated text' });
return Model1.query()
.patch(model)
.where('id', '=', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should patch a model (2)', () => {
let model = Model2.fromJson({ model2Prop1: 'updated text' });
return Model2.query()
.patch(model)
.where('id_col', '=', 1)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'updated text', model2_prop2: 2 });
expectPartEql(rows[1], { id_col: 2, model2_prop1: 'text 2', model2_prop2: 1 });
});
});
it('should accept json', () => {
return Model1.query()
.patch({ model1Prop1: 'updated text' })
.where('id', '=', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should ignore non-objects in relation properties', () => {
return Model1.query()
.patch({
model1Prop1: 'updated text',
model1Relation1: 1,
model1Relation2: [1, 2, null, undefined, 5],
})
.where('id', '=', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should accept subqueries and raw expressions (1)', () => {
return Model1.query()
.patch({
model1Prop1: Model2.raw('(select max(??) from ??)', ['model2_prop1', 'model2']),
model1Prop2: Model2.query().sum('model2_prop2'),
})
.where('id', '=', 1)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'text 2', model1Prop2: 3 });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should accept subqueries and raw expressions (2)', () => {
return Model1.query()
.patch({
model1Prop1: 'Morten',
model1Prop2: Model2.knexQuery().sum('model2_prop2'),
})
.where('id', '=', 1)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'Morten', model1Prop2: 3 });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should patch multiple', () => {
return Model1.query()
.patch({ model1Prop1: 'updated text' })
.where('model1Prop1', '<', 'hello 3')
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
if (session.isPostgres()) {
it('should patch multiple and return updated rows if returning is useed', () => {
return Model1.query()
.patch({ model1Prop1: 'updated text' })
.where('model1Prop1', '<', 'hello 3')
.returning('*')
.then((updatedRows) => {
expect(updatedRows).to.have.length(2);
chai.expect(updatedRows).to.containSubset([
{
id: 1,
model1Id: null,
model1Prop1: 'updated text',
model1Prop2: null,
},
{
id: 2,
model1Id: null,
model1Prop1: 'updated text',
model1Prop2: null,
},
]);
});
});
}
it('increment should create patch', () => {
return Model2.query()
.increment('model2Prop2', 10)
.where('id_col', '=', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id_col: 1, model2_prop2: 2 });
expectPartEql(rows[1], { id_col: 2, model2_prop2: 11 });
});
});
it('decrement should create patch', () => {
return Model2.query()
.decrement('model2Prop2', 10)
.where('id_col', '=', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id_col: 1, model2_prop2: 2 });
expectPartEql(rows[1], { id_col: 2, model2_prop2: -9 });
});
});
it('should validate (1)', (done) => {
let ModelWithSchema = subClassWithSchema(Model1, {
type: 'object',
required: ['model1Prop2'],
properties: {
id: { type: ['number', 'null'] },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
});
ModelWithSchema.query()
.patch({ model1Prop1: 100 })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(ValidationError);
expect(err.type).to.equal('ModelValidation');
expect(err.data).to.eql({
model1Prop1: [
{
message: 'must be string',
keyword: 'type',
params: {
type: 'string',
},
},
],
});
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
done();
})
.catch(done);
});
it('should skip requirement validation', (done) => {
let ModelWithSchema = subClassWithSchema(Model1, {
type: 'object',
required: ['model1Prop2'],
properties: {
id: { type: ['number', 'null'] },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
});
ModelWithSchema.query()
.patch({ model1Prop1: 'text' })
.then(() => {
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['text', 'text', 'text']);
done();
})
.catch(done);
});
it('should be able to use objection.raw in hooks', () => {
class Test extends Model {
static get tableName() {
return 'Model1';
}
$beforeUpdate(opt, ctx) {
this.model1Prop2 = raw(`100 + 200`);
}
}
return Test.query(session.knex)
.findById(2)
.patch({
model1Prop1: 'updated',
})
.then(() => {
return Test.query(session.knex).findById(2);
})
.then((model) => {
expect(model).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'updated',
model1Prop2: 300,
});
});
});
it('should not attempt to eager-load relations on patch `count` results when using overwritten `execute()` to load relations (#2397)', () => {
let runBeforeCalled = 0;
class MyModel1 extends Model1 {}
MyModel1.QueryBuilder = class MyQueryBuilder1 extends QueryBuilder {
execute() {
this.withGraphFetched('model1Relation2');
return super.execute();
}
};
return MyModel1.query()
.context({
runBefore() {
runBeforeCalled++;
},
})
.where({ id: 1 })
.patch({ model1Prop1: 'updated text' })
.then((count) => {
expect(count).to.eql(1);
expect(runBeforeCalled).to.eql(1);
});
});
});
describe('.query().patchAndFetchById()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 2,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 1,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
{
id: 3,
model1Prop1: 'hello 3',
},
]);
});
it('should patch and fetch a model', () => {
let model = Model1.fromJson({ model1Prop1: 'updated text' });
return Model1.query()
.patchAndFetchById(2, model)
.then((fetchedModel) => {
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
expect(fetchedModel).to.equal(model);
expect(fetchedModel).eql({
id: 2,
model1Prop1: 'updated text',
model1Prop2: null,
model1Id: null,
$beforeUpdateCalled: 1,
$beforeUpdateOptions: { patch: true },
$afterUpdateCalled: 1,
$afterUpdateOptions: { patch: true },
});
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should work with `eager` method', () => {
let model = Model1.fromJson({ model1Prop1: 'updated text' });
return Model1.query()
.patchAndFetchById(1, model)
.withGraphFetched('model1Relation2')
.modifyGraph('model1Relation2', (qb) => qb.orderBy('id_col'))
.then((fetchedModel) => {
expect(fetchedModel).eql({
id: 1,
model1Prop1: 'updated text',
model1Prop2: null,
model1Id: null,
$beforeUpdateCalled: 1,
$beforeUpdateOptions: { patch: true },
$afterUpdateCalled: 1,
$afterUpdateOptions: { patch: true },
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'text 1',
model2Prop2: 2,
$afterFindCalled: 1,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'text 2',
model2Prop2: 1,
$afterFindCalled: 1,
},
],
});
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should fetch nothing if nothing is updated', () => {
return Model1.query()
.patchAndFetchById(2, { model1Prop1: 'updated text' })
.where('id', -1)
.then((fetchedModel) => {
expect(fetchedModel).to.equal(undefined);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
});
describe('.$query().patch()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should patch a model (1)', () => {
let model = Model1.fromJson({ id: 1 });
return model
.$query()
.patch({ model1Prop1: 'updated text', undefinedShouldBeIgnored: undefined })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.model1Prop1).to.equal('updated text');
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
if (isPostgres(session.knex)) {
it('should work with returning', () => {
let model = Model1.fromJson({ id: 1 });
return model
.$query()
.patch({ model1Prop1: 'updated text' })
.returning('model1Prop1', 'model1Prop2')
.then((patched) => {
const expected = { model1Prop1: 'updated text', model1Prop2: null };
expect(patched).to.be.a(Model1);
expect(patched).to.eql(expected);
expect(model).to.eql(Object.assign({}, expected, { id: 1 }));
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
it('should work with returning *', () => {
const model = Model1.fromJson({ id: 1 });
return model
.$query()
.patch({ model1Prop1: 'updated text' })
.returning('*')
.then((patched) => {
const expected = {
id: 1,
model1Id: null,
model1Prop1: 'updated text',
model1Prop2: null,
};
expect(patched).to.be.a(Model1);
expect(patched).to.eql(expected);
expect(model).to.eql(expected);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
}
it('should patch a model (2)', () => {
return Model1.fromJson({ id: 1, model1Prop1: 'updated text' })
.$query()
.patch()
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
it('should pass the old values to $beforeUpdate and $afterUpdate hooks in options.old', () => {
let patch = Model1.fromJson({ model1Prop1: 'updated text' });
return Model1.fromJson({ id: 1 })
.$query()
.patch(patch)
.then(() => {
expect(patch.$beforeInsertCalled).to.equal(undefined);
expect(patch.$afterInsertCalled).to.equal(undefined);
expect(patch.$beforeDeleteCalled).to.equal(undefined);
expect(patch.$afterDeleteCalled).to.equal(undefined);
expect(patch.$beforeUpdateCalled).to.equal(1);
expect(patch.$beforeUpdateOptions).to.eql({ patch: true, old: { id: 1 } });
expect(patch.$afterUpdateCalled).to.equal(1);
expect(patch.$afterUpdateOptions).to.eql({ patch: true, old: { id: 1 } });
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
it('model edits in $beforeUpdate should get into database query', () => {
let model = Model1.fromJson({ id: 1 });
model.$beforeUpdate = function () {
let self = this;
return Promise.delay(1).then(() => {
self.model1Prop1 = 'updated text';
});
};
return model
.$query()
.patch()
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
it('should throw if the id is undefined', (done) => {
let model = Model1.fromJson({ model1Prop2: 1 });
model
.$query()
.patch({ model1Prop1: 'updated text', undefinedShouldBeIgnored: undefined })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`one of the identifier columns [id] is null or undefined. Have you specified the correct identifier column for the model 'Model1' using the 'idColumn' property?`,
);
done();
})
.catch(done);
});
it('should throw if the id is null', (done) => {
let model = Model1.fromJson({ id: null });
model
.$query()
.patch({ model1Prop1: 'updated text', undefinedShouldBeIgnored: undefined })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`one of the identifier columns [id] is null or undefined. Have you specified the correct identifier column for the model 'Model1' using the 'idColumn' property?`,
);
done();
})
.catch(done);
});
});
describe('.$query().patchAndFetch()', () => {
let ModelOne;
let queries = [];
before(() => {
const knex = mockKnexFactory(session.knex, function (mock, oldImpl, args) {
queries.push(this.toString());
return oldImpl.apply(this, args);
});
ModelOne = session.unboundModels.Model1.bindKnex(knex);
});
beforeEach(() => {
queries = [];
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should patch and fetch a model', () => {
let model = ModelOne.fromJson({ id: 1 });
return model
.$query()
.patchAndFetch({ model1Prop2: 10, undefinedShouldBeIgnored: undefined })
.then((updated) => {
expect(updated.id).to.equal(1);
expect(updated.model1Id).to.equal(null);
expect(updated.model1Prop1).to.equal('hello 1');
expect(updated.model1Prop2).to.equal(10);
expectPartEql(model, {
id: 1,
model1Prop1: 'hello 1',
model1Prop2: 10,
model1Id: null,
});
if (session.isPostgres()) {
expect(queries).to.eql([
'update "Model1" set "model1Prop2" = 10 where "Model1"."id" = 1',
'select "Model1".* from "Model1" where "Model1"."id" = 1',
]);
}
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1', model1Prop2: 10 });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2', model1Prop2: null });
});
});
});
describe('.$relatedQuery().patch()', () => {
describe('belongs to one relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
},
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 3 });
});
});
it('should patch a related object (1)', () => {
const model = Model1.fromJson({ model1Prop1: 'updated text' });
return parent1
.$relatedQuery('model1Relation1')
.patch(model)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'hello 4' });
});
});
it('should patch a related object (2)', () => {
return parent2
.$relatedQuery('model1Relation1')
.patch({ model1Prop1: 'updated text', model1Prop2: 1000 })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'updated text', model1Prop2: 1000 });
});
});
});
describe('has many relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
{
idCol: 5,
model2Prop1: 'text 5',
model2Prop2: 2,
},
{
idCol: 6,
model2Prop1: 'text 6',
model2Prop2: 1,
},
],
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 2 });
});
});
it('should patch a related object', () => {
const model = Model2.fromJson({ model2Prop1: 'updated text' });
return parent1
.$relatedQuery('model1Relation2')
.patch(model)
.where('id_col', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], {
id_col: 2,
model2_prop1: 'updated text',
model2_prop2: 5,
});
expectPartEql(rows[2], { id_col: 3, model2_prop1: 'text 3' });
expectPartEql(rows[3], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[4], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[5], { id_col: 6, model2_prop1: 'text 6' });
});
});
it('should patch multiple related objects', () => {
return parent1
.$relatedQuery('model1Relation2')
.patch({ model2Prop1: 'updated text' })
.where('model2_prop2', '<', 6)
.where('model2_prop1', 'like', 'text %')
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], {
id_col: 2,
model2_prop1: 'updated text',
model2_prop2: 5,
});
expectPartEql(rows[2], {
id_col: 3,
model2_prop1: 'updated text',
model2_prop2: 4,
});
expectPartEql(rows[3], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[4], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[5], { id_col: 6, model2_prop1: 'text 6' });
});
});
});
describe('many to many relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
model1Relation1: {
id: 9,
model1Prop1: 'hoot',
},
},
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 5,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
],
},
],
model1Relation3: [
{
idCol: 3,
model2Prop1: 'foo 1',
extra1: 'extra 11',
extra2: 'extra 21',
},
{
idCol: 4,
model2Prop1: 'foo 2',
extra1: 'extra 12',
extra2: 'extra 22',
},
{
idCol: 5,
model2Prop1: 'foo 3',
extra1: 'extra 13',
extra2: 'extra 23',
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
},
{
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 2,
},
{
id: 8,
model1Prop1: 'blaa 6',
model1Prop2: 1,
},
],
},
],
model1Relation3: [
{
idCol: 6,
model2Prop1: 'foo 4',
extra1: 'extra 14',
extra2: 'extra 24',
},
{
idCol: 7,
model2Prop1: 'foo 5',
extra1: 'extra 15',
extra2: 'extra 25',
},
{
idCol: 8,
model2Prop1: 'foo 6',
extra1: 'extra 16',
extra2: 'extra 26',
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent1 = _.find(parents, { idCol: 1 });
parent2 = _.find(parents, { idCol: 2 });
});
});
it('should patch a related object', () => {
const model = Model1.fromJson({ model1Prop1: 'updated text' });
return parent1
.$relatedQuery('model2Relation1')
.patch(model)
.where('Model1.id', 5)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'updated text' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'blaa 6' });
expectPartEql(rows[8], { id: 9, model1Prop1: 'hoot' });
});
});
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).withGraphFetched('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',
$afterFindCalled: 1,
},
{
idCol: 4,
model1Id: null,
model2Prop1: 'iam updated',
model2Prop2: null,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
$afterFindCalled: 1,
},
{
idCol: 3,
model1Id: null,
model2Prop1: 'foo 1',
model2Prop2: null,
extra1: 'extra 11',
extra2: 'extra 21',
$afterFindCalled: 1,
},
],
$afterFindCalled: 1,
});
});
});
it('should patch a related object with extras', () => {
return Model1.query()
.findById(1)
.then((parent) => {
return parent
.$relatedQuery('model1Relation3')
.where('id_col', '>', 3)
.patch({
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')`),
})
.where('id_col', '<', 5)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return Promise.all([
session.knex('model2').orderBy('id_col'),
session
.knex('Model1Model2')
.select('model1Id', 'model2Id', 'extra1', 'extra2')
.orderBy(['model1Id', 'model2Id']),
]);
})
.then(([model2, model1Model2]) => {
expect(model2.length).to.equal(8);
expect(model1Model2.length).to.equal(12);
expectPartEql(model2[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(model2[1], { id_col: 2, model2_prop1: 'text 2' });
expectPartEql(model2[2], { id_col: 3, model2_prop1: 'foo 1' });
expectPartEql(model2[3], { id_col: 4, model2_prop1: 'iam updated' });
expectPartEql(model2[4], { id_col: 5, model2_prop1: 'foo 3' });
expectPartEql(model2[5], { id_col: 6, model2_prop1: 'foo 4' });
expectPartEql(model2[6], { id_col: 7, model2_prop1: 'foo 5' });
expectPartEql(model2[7], { id_col: 8, model2_prop1: 'foo 6' });
expectPartEql(model1Model2[0], {
model1Id: 1,
extra1: 'extra 11',
extra2: 'extra 21',
});
expectPartEql(model1Model2[1], {
model1Id: 1,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
});
expectPartEql(model1Model2[2], {
model1Id: 1,
extra1: 'extra 13',
extra2: 'extra 23',
});
expectPartEql(model1Model2[3], {
model1Id: 2,
extra1: 'extra 14',
extra2: 'extra 24',
});
expectPartEql(model1Model2[4], {
model1Id: 2,
extra1: 'extra 15',
extra2: 'extra 25',
});
expectPartEql(model1Model2[5], {
model1Id: 2,
extra1: 'extra 16',
extra2: 'extra 26',
});
expectPartEql(model1Model2[6], { extra1: null, extra2: null });
expectPartEql(model1Model2[7], { extra1: null, extra2: null });
expectPartEql(model1Model2[8], { extra1: null, extra2: null });
expectPartEql(model1Model2[9], { extra1: null, extra2: null });
expectPartEql(model1Model2[10], { extra1: null, extra2: null });
expectPartEql(model1Model2[11], { extra1: null, extra2: null });
});
});
});
it('should patch all related objects with extras', () => {
return Model1.query()
.findById(1)
.then((parent) => {
return parent
.$relatedQuery('model1Relation3')
.patch({
model2Prop1: 'iam updated',
extra1: 'updated extra 1',
extra2: 'updated extra 2',
})
.then((numUpdated) => {
expect(numUpdated).to.equal(3);
return Promise.all([
session.knex('model2').orderBy('id_col'),
session
.knex('Model1Model2')
.select('model1Id', 'model2Id', 'extra1', 'extra2')
.orderBy(['model1Id', 'model2Id']),
]);
})
.then(([model2, model1Model2]) => {
expect(model2.length).to.equal(8);
expect(model1Model2.length).to.equal(12);
expectPartEql(model2[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(model2[1], { id_col: 2, model2_prop1: 'text 2' });
expectPartEql(model2[2], { id_col: 3, model2_prop1: 'iam updated' });
expectPartEql(model2[3], { id_col: 4, model2_prop1: 'iam updated' });
expectPartEql(model2[4], { id_col: 5, model2_prop1: 'iam updated' });
expectPartEql(model2[5], { id_col: 6, model2_prop1: 'foo 4' });
expectPartEql(model2[6], { id_col: 7, model2_prop1: 'foo 5' });
expectPartEql(model2[7], { id_col: 8, model2_prop1: 'foo 6' });
expectPartEql(model1Model2[0], {
model1Id: 1,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
});
expectPartEql(model1Model2[1], {
model1Id: 1,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
});
expectPartEql(model1Model2[2], {
model1Id: 1,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
});
expectPartEql(model1Model2[3], {
model1Id: 2,
extra1: 'extra 14',
extra2: 'extra 24',
});
expectPartEql(model1Model2[4], {
model1Id: 2,
extra1: 'extra 15',
extra2: 'extra 25',
});
expectPartEql(model1Model2[5], {
model1Id: 2,
extra1: 'extra 16',
extra2: 'extra 26',
});
expectPartEql(model1Model2[6], { extra1: null, extra2: null });
expectPartEql(model1Model2[7], { extra1: null, extra2: null });
expectPartEql(model1Model2[8], { extra1: null, extra2: null });
expectPartEql(model1Model2[9], { extra1: null, extra2: null });
expectPartEql(model1Model2[10], { extra1: null, extra2: null });
expectPartEql(model1Model2[11], { extra1: null, extra2: null });
});
});
});
it('should patch all related objects', () => {
return parent2
.$relatedQuery('model2Relation1')
.patch({ model1Prop1: 'updated text', model1Prop2: 123 })
.then((numUpdated) => {
expect(numUpdated).to.equal(3);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[6], { id: 7, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[7], { id: 8, model1Prop1: 'updated text', model1Prop2: 123 });
});
});
it('should patch multiple objects (1)', () => {
return parent2
.$relatedQuery('model2Relation1')
.patch({ model1Prop1: 'updated text', model1Prop2: 123 })
.where('model1Prop1', 'like', 'blaa 4')
.orWhere('model1Prop1', 'like', 'blaa 6')
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'updated text', model1Prop2: 123 });
});
});
it('should patch multiple objects (2)', () => {
return parent1
.$relatedQuery('model2Relation1')
.patch({ model1Prop1: 'updated text', model1Prop2: 123 })
.where('model1Prop2', '<', 6)
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[4], { id: 5, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[5], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'blaa 6' });
});
});
it('should be able to use `joinRelated`', () => {
return parent1
.$relatedQuery('model2Relation1')
.innerJoinRelated('model1Relation1')
.patch({ model1Prop1: 'updated text', model1Prop2: 123 })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'blaa 6' });
});
});
});
describe('has one through relation', () => {
let parent;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation2: {
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation2: {
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 2,
},
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent = _.find(parents, { idCol: 1 });
});
});
it('should patch the related object', () => {
return parent
.$relatedQuery('model2Relation2')
.patch({ model1Prop1: 'updated text' })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'updated text' });
expectPartEql(rows[3], { id: 7, model1Prop1: 'blaa 5' });
});
});
});
});
describe('.relatedQuery().patch()', () => {
describe('belongs to one relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
},
},
]);
});
it('should patch a related object by id (1)', () => {
const model = Model1.fromJson({ model1Prop1: 'updated text' });
return Model1.relatedQuery('model1Relation1')
.for(1)
.patch(model)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'hello 4' });
});
});
it('should patch a related object by id (2)', () => {
return Model1.relatedQuery('model1Relation1')
.for(3)
.patch({ model1Prop1: 'updated text', model1Prop2: 1000 })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'updated text', model1Prop2: 1000 });
});
});
it('should patch a related object by two ids', () => {
return Model1.relatedQuery('model1Relation1')
.for([1, 3])
.patch({ model1Prop1: 'updated text', model1Prop2: 1000 })
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text', model1Prop2: 1000 });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'updated text', model1Prop2: 1000 });
});
});
it('should patch a related object by subquery', () => {
return Model1.relatedQuery('model1Relation1')
.for(Model1.query().where('id', 1))
.patch({ model1Prop1: 'updated text', model1Prop2: 1000 })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text', model1Prop2: 1000 });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'hello 4' });
});
});
});
describe('has many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
{
idCol: 5,
model2Prop1: 'text 5',
model2Prop2: 2,
},
{
idCol: 6,
model2Prop1: 'text 6',
model2Prop2: 1,
},
],
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation2: [
{
idCol: 7,
model2Prop1: 'text 7',
model2Prop2: 0,
},
],
},
]);
});
it('should patch a related object', () => {
const model = Model2.fromJson({ model2Prop1: 'updated text' });
return Model1.relatedQuery('model1Relation2')
.for(1)
.patch(model)
.where('id_col', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], {
id_col: 2,
model2_prop1: 'updated text',
model2_prop2: 5,
});
expectPartEql(rows[2], { id_col: 3, model2_prop1: 'text 3' });
expectPartEql(rows[3], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[4], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[5], { id_col: 6, model2_prop1: 'text 6' });
expectPartEql(rows[6], { id_col: 7, model2_prop1: 'text 7' });
});
});
it('should patch multiple related objects', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.patch({ model2Prop1: 'updated text' })
.where('model2_prop2', '<', 6)
.where('model2_prop1', 'like', 'text %')
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], {
id_col: 2,
model2_prop1: 'updated text',
model2_prop2: 5,
});
expectPartEql(rows[2], {
id_col: 3,
model2_prop1: 'updated text',
model2_prop2: 4,
});
expectPartEql(rows[3], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[4], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[5], { id_col: 6, model2_prop1: 'text 6' });
expectPartEql(rows[6], { id_col: 7, model2_prop1: 'text 7' });
});
});
it('should patch multiple related objects for multiple parents', () => {
return Model1.relatedQuery('model1Relation2')
.for([1, 2])
.patch({ model2Prop1: 'updated text' })
.where('model2_prop1', '!=', 'text 4')
.then((numUpdated) => {
expect(numUpdated).to.equal(5);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'updated text' });
expectPartEql(rows[1], { id_col: 2, model2_prop1: 'updated text' });
expectPartEql(rows[2], { id_col: 3, model2_prop1: 'updated text' });
expectPartEql(rows[3], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[4], { id_col: 5, model2_prop1: 'updated text' });
expectPartEql(rows[5], { id_col: 6, model2_prop1: 'updated text' });
expectPartEql(rows[6], { id_col: 7, model2_prop1: 'text 7' });
});
});
it('should patch multiple related objects for multiple parents using a subquery', () => {
return Model1.relatedQuery('model1Relation2')
.for(Model1.query().findByIds([1, 2]))
.patch({ model2Prop1: 'updated text' })
.where('model2_prop1', '!=', 'text 4')
.then((numUpdated) => {
expect(numUpdated).to.equal(5);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(7);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'updated text' });
expectPartEql(rows[1], { id_col: 2, model2_prop1: 'updated text' });
expectPartEql(rows[2], { id_col: 3, model2_prop1: 'updated text' });
expectPartEql(rows[3], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[4], { id_col: 5, model2_prop1: 'updated text' });
expectPartEql(rows[5], { id_col: 6, model2_prop1: 'updated text' });
expectPartEql(rows[6], { id_col: 7, model2_prop1: 'text 7' });
});
});
});
describe('many to many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
model1Relation1: {
id: 9,
model1Prop1: 'hoot',
},
},
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 5,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
],
},
],
model1Relation3: [
{
idCol: 3,
model2Prop1: 'foo 1',
extra1: 'extra 11',
extra2: 'extra 21',
},
{
idCol: 4,
model2Prop1: 'foo 2',
extra1: 'extra 12',
extra2: 'extra 22',
},
{
idCol: 5,
model2Prop1: 'foo 3',
extra1: 'extra 13',
extra2: 'extra 23',
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
},
{
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 2,
},
{
id: 8,
model1Prop1: 'blaa 6',
model1Prop2: 1,
},
],
},
],
model1Relation3: [
{
idCol: 6,
model2Prop1: 'foo 4',
extra1: 'extra 14',
extra2: 'extra 24',
},
{
idCol: 7,
model2Prop1: 'foo 5',
extra1: 'extra 15',
extra2: 'extra 25',
},
{
idCol: 8,
model2Prop1: 'foo 6',
extra1: 'extra 16',
extra2: 'extra 26',
},
],
},
]);
});
it('should patch a related object', () => {
const model = Model1.fromJson({ model1Prop1: 'updated text' });
return Model2.relatedQuery('model2Relation1')
.for(1)
.patch(model)
.where('Model1.id', 5)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.$beforeInsertCalled).to.equal(undefined);
expect(model.$afterInsertCalled).to.equal(undefined);
expect(model.$beforeDeleteCalled).to.equal(undefined);
expect(model.$afterDeleteCalled).to.equal(undefined);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ patch: true });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ patch: true });
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'updated text' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'blaa 6' });
expectPartEql(rows[8], { id: 9, model1Prop1: 'hoot' });
});
});
it('should patch a related object with extras', () => {
return Model1.relatedQuery('model1Relation3')
.for(1)
.where('id_col', '>', 3)
.patch({
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')`),
})
.where('id_col', '<', 5)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return Promise.all([
session.knex('model2').orderBy('id_col'),
session
.knex('Model1Model2')
.select('model1Id', 'model2Id', 'extra1', 'extra2')
.orderBy(['model1Id', 'model2Id']),
]);
})
.then(([model2, model1Model2]) => {
expect(model2.length).to.equal(8);
expect(model1Model2.length).to.equal(12);
expectPartEql(model2[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(model2[1], { id_col: 2, model2_prop1: 'text 2' });
expectPartEql(model2[2], { id_col: 3, model2_prop1: 'foo 1' });
expectPartEql(model2[3], { id_col: 4, model2_prop1: 'iam updated' });
expectPartEql(model2[4], { id_col: 5, model2_prop1: 'foo 3' });
expectPartEql(model2[5], { id_col: 6, model2_prop1: 'foo 4' });
expectPartEql(model2[6], { id_col: 7, model2_prop1: 'foo 5' });
expectPartEql(model2[7], { id_col: 8, model2_prop1: 'foo 6' });
expectPartEql(model1Model2[0], {
model1Id: 1,
extra1: 'extra 11',
extra2: 'extra 21',
});
expectPartEql(model1Model2[1], {
model1Id: 1,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
});
expectPartEql(model1Model2[2], {
model1Id: 1,
extra1: 'extra 13',
extra2: 'extra 23',
});
expectPartEql(model1Model2[3], {
model1Id: 2,
extra1: 'extra 14',
extra2: 'extra 24',
});
expectPartEql(model1Model2[4], {
model1Id: 2,
extra1: 'extra 15',
extra2: 'extra 25',
});
expectPartEql(model1Model2[5], {
model1Id: 2,
extra1: 'extra 16',
extra2: 'extra 26',
});
expectPartEql(model1Model2[6], { extra1: null, extra2: null });
expectPartEql(model1Model2[7], { extra1: null, extra2: null });
expectPartEql(model1Model2[8], { extra1: null, extra2: null });
expectPartEql(model1Model2[9], { extra1: null, extra2: null });
expectPartEql(model1Model2[10], { extra1: null, extra2: null });
expectPartEql(model1Model2[11], { extra1: null, extra2: null });
});
});
it('should patch all related objects with extras', () => {
return Model1.relatedQuery('model1Relation3')
.for(1)
.patch({
model2Prop1: 'iam updated',
extra1: 'updated extra 1',
extra2: 'updated extra 2',
})
.then((numUpdated) => {
expect(numUpdated).to.equal(3);
return Promise.all([
session.knex('model2').orderBy('id_col'),
session
.knex('Model1Model2')
.select('model1Id', 'model2Id', 'extra1', 'extra2')
.orderBy(['model1Id', 'model2Id']),
]);
})
.then(([model2, model1Model2]) => {
expect(model2.length).to.equal(8);
expect(model1Model2.length).to.equal(12);
expectPartEql(model2[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(model2[1], { id_col: 2, model2_prop1: 'text 2' });
expectPartEql(model2[2], { id_col: 3, model2_prop1: 'iam updated' });
expectPartEql(model2[3], { id_col: 4, model2_prop1: 'iam updated' });
expectPartEql(model2[4], { id_col: 5, model2_prop1: 'iam updated' });
expectPartEql(model2[5], { id_col: 6, model2_prop1: 'foo 4' });
expectPartEql(model2[6], { id_col: 7, model2_prop1: 'foo 5' });
expectPartEql(model2[7], { id_col: 8, model2_prop1: 'foo 6' });
expectPartEql(model1Model2[0], {
model1Id: 1,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
});
expectPartEql(model1Model2[1], {
model1Id: 1,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
});
expectPartEql(model1Model2[2], {
model1Id: 1,
extra1: 'updated extra 1',
extra2: 'updated extra 2',
});
expectPartEql(model1Model2[3], {
model1Id: 2,
extra1: 'extra 14',
extra2: 'extra 24',
});
expectPartEql(model1Model2[4], {
model1Id: 2,
extra1: 'extra 15',
extra2: 'extra 25',
});
expectPartEql(model1Model2[5], {
model1Id: 2,
extra1: 'extra 16',
extra2: 'extra 26',
});
expectPartEql(model1Model2[6], { extra1: null, extra2: null });
expectPartEql(model1Model2[7], { extra1: null, extra2: null });
expectPartEql(model1Model2[8], { extra1: null, extra2: null });
expectPartEql(model1Model2[9], { extra1: null, extra2: null });
expectPartEql(model1Model2[10], { extra1: null, extra2: null });
expectPartEql(model1Model2[11], { extra1: null, extra2: null });
});
});
it('should patch all related objects', () => {
return Model2.relatedQuery('model2Relation1')
.for(2)
.patch({ model1Prop1: 'updated text', model1Prop2: 123 })
.then((numUpdated) => {
expect(numUpdated).to.equal(3);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[6], { id: 7, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[7], { id: 8, model1Prop1: 'updated text', model1Prop2: 123 });
});
});
it('should patch multiple related objects for multiple parents', () => {
return Model2.relatedQuery('model2Relation1')
.for([1, 2])
.patch({ model1Prop1: 'updated text' })
.where('model1Prop1', '!=', 'blaa 2')
.then((numUpdated) => {
expect(numUpdated).to.equal(5);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'updated text' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'updated text' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'updated text' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'updated text' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'updated text' });
});
});
it('should patch multiple related objects for multiple parents using a subquery', () => {
return Model2.relatedQuery('model2Relation1')
.for(Model2.query().findByIds([1, 2]))
.patch({ model1Prop1: 'updated text' })
.where('model1Prop1', '!=', 'blaa 2')
.then((numUpdated) => {
expect(numUpdated).to.equal(5);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'updated text' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'updated text' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'updated text' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'updated text' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'updated text' });
});
});
it('should patch multiple objects (1)', () => {
return Model2.relatedQuery('model2Relation1')
.for(2)
.patch({ model1Prop1: 'updated text', model1Prop2: 123 })
.where('model1Prop1', 'like', 'blaa 4')
.orWhere('model1Prop1', 'like', 'blaa 6')
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'updated text', model1Prop2: 123 });
});
});
it('should patch multiple objects (2)', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.patch({ model1Prop1: 'updated text', model1Prop2: 123 })
.where('model1Prop2', '<', 6)
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[4], { id: 5, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[5], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'blaa 6' });
});
});
it('should be able to use `joinRelated`', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.innerJoinRelated('model1Relation1')
.patch({ model1Prop1: 'updated text', model1Prop2: 123 })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(9);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'blaa 6' });
});
});
});
});
describe('hooks', () => {
let ModelOne;
let ModelTwo;
let beforeUpdateCalled = '';
let afterUpdateCalled = '';
let beforeUpdateOpt = null;
let afterUpdateOpt = null;
before(() => {
// Create a new knex object by wrapping session.knex so that we get a new
// instance instead of a cached one from `bindKnex`.
const knex = mockKnexFactory(session.knex, function (mock, oldImpl, args) {
return oldImpl.apply(this, args);
});
ModelOne = session.unboundModels.Model1.bindKnex(knex);
ModelTwo = ModelOne.getRelation('model1Relation2').relatedModelClass;
expect(ModelOne).to.not.equal(Model1);
expect(ModelTwo).to.not.equal(Model2);
expect(ModelOne).to.not.equal(session.unboundModels.Model1);
expect(ModelTwo).to.not.equal(session.unboundModels.Model2);
ModelOne.prototype.$beforeUpdate = function (opt, ctx) {
beforeUpdateCalled += 'ModelOne';
beforeUpdateOpt = _.cloneDeep(opt);
};
ModelOne.prototype.$afterUpdate = function (opt, ctx) {
afterUpdateCalled += 'ModelOne';
afterUpdateOpt = _.cloneDeep(opt);
};
ModelTwo.prototype.$beforeUpdate = function (opt, ctx) {
beforeUpdateCalled += 'ModelTwo';
beforeUpdateOpt = _.cloneDeep(opt);
};
ModelTwo.prototype.$afterUpdate = function (opt, ctx) {
afterUpdateCalled += 'ModelTwo';
afterUpdateOpt = _.cloneDeep(opt);
};
});
beforeEach(() => {
beforeUpdateCalled = '';
afterUpdateCalled = '';
beforeUpdateOpt = null;
afterUpdateOpt = null;
});
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'foo 1',
},
],
model1Relation3: [
{
idCol: 2,
model2Prop1: 'foo 2',
},
],
},
]);
});
it('.query().patch()', () => {
return ModelOne.query()
.findById(1)
.patch({ model1Prop1: 'updated text' })
.then(() => {
expect(beforeUpdateCalled).to.equal('ModelOne');
expect(beforeUpdateOpt).to.eql({ patch: true });
expect(afterUpdateCalled).to.equal('ModelOne');
expect(afterUpdateOpt).to.eql({ patch: true });
});
});
it('.$query().patch()', () => {
return ModelOne.query()
.findById(1)
.then((model) => {
return model.$query().patch({ model1Prop1: 'updated text' });
})
.then(() => {
expect(beforeUpdateCalled).to.equal('ModelOne');
expect(beforeUpdateOpt).to.eql({
patch: true,
old: {
$afterFindCalled: 1,
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
},
});
expect(afterUpdateCalled).to.equal('ModelOne');
expect(afterUpdateOpt).to.eql({
patch: true,
old: {
$afterFindCalled: 1,
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
},
});
});
});
describe('$relatedQuery().patch()', () => {
it('belongs to one relation', () => {
return ModelOne.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation1').patch({ model1Prop1: 'updated text' });
})
.then(() => {
expect(beforeUpdateCalled).to.equal('ModelOne');
expect(beforeUpdateOpt).to.eql({ patch: true });
expect(afterUpdateCalled).to.equal('ModelOne');
expect(afterUpdateOpt).to.eql({ patch: true });
});
});
it('has many relation', () => {
return ModelOne.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation2').patch({ model2Prop1: 'updated text' });
})
.then(() => {
expect(beforeUpdateCalled).to.equal('ModelTwo');
expect(beforeUpdateOpt).to.eql({ patch: true });
expect(afterUpdateCalled).to.equal('ModelTwo');
expect(afterUpdateOpt).to.eql({ patch: true });
});
});
it('many to many relation', () => {
return ModelOne.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation3').patch({ model2Prop1: 'updated text' });
})
.then(() => {
expect(beforeUpdateCalled).to.equal('ModelTwo');
expect(beforeUpdateOpt).to.eql({ patch: true });
expect(afterUpdateCalled).to.equal('ModelTwo');
expect(afterUpdateOpt).to.eql({ patch: true });
});
});
});
});
function subClassWithSchema(Model, schema) {
let SubModel = inheritModel(Model);
SubModel.jsonSchema = schema;
return SubModel;
}
});
};
================================================
FILE: tests/integration/queryContext.js
================================================
const _ = require('lodash');
const Promise = require('bluebird');
const utils = require('../../lib/utils/knexUtils');
const expect = require('expect.js');
const chai = require('chai');
const inheritModel = require('../../lib/model/inheritModel').inheritModel;
const knexMocker = require('../../testUtils/mockKnex');
module.exports = (session) => {
let Model1;
let Model2;
let mockKnex;
// This file tests only the query context feature. Query context feature is present in
// so many places that it is better to test it separately rather than add tests in
// multiple other test sets.
describe('Query context', () => {
before(() => {
mockKnex = knexMocker(session.knex, function (mock, origImpl, args) {
mock.executedQueries.push(this.toString());
if (mock.results.length) {
let result = mock.results.shift() || [];
let promise = Promise.resolve(result);
return promise.then.apply(promise, args);
} else {
return origImpl.apply(this, args);
}
});
mockKnex.reset = () => {
mockKnex.executedQueries = [];
mockKnex.results = [];
};
Model1 = session.models.Model1.bindKnex(mockKnex);
Model2 = session.models.Model2.bindKnex(mockKnex);
mockKnex.reset();
});
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
model1Relation1: {
id: 3,
model1Prop1: 'hello 3',
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'hejsan 1',
model2Prop2: 30,
model2Relation1: [
{
id: 4,
model1Prop1: 'hello 4',
},
],
},
{
idCol: 2,
model2Prop1: 'hejsan 2',
model2Prop2: 20,
},
],
},
},
]);
});
beforeEach(() => {
mockKnex.reset();
});
it('should get passed to the $afterFind method', () => {
let Model = inheritModel(Model1);
let context = { a: 1, b: '2' };
let called = false;
Model.prototype.$afterFind = (queryContext) => {
expect(queryContext).to.eql(context);
expect(context.transaction).to.equal(undefined);
expect(queryContext.transaction).to.equal(mockKnex);
expect(Object.keys(queryContext).indexOf('transaction')).to.equal(-1);
called = true;
};
return Model.query()
.context(context)
.where('id', 1)
.then(() => {
expect(called).to.equal(true);
});
});
it('should get passed to the $beforeUpdate method', () => {
let Model = inheritModel(Model1);
let context = { a: 1, b: '2' };
let called = false;
Model.prototype.$beforeUpdate = function (opt, queryContext) {
expect(queryContext).to.eql(context);
expect(context.transaction).to.equal(undefined);
expect(queryContext.transaction).to.equal(mockKnex);
expect(Object.keys(queryContext).indexOf('transaction')).to.equal(-1);
called = true;
};
return Model.query()
.context(context)
.update({ model1Prop1: 'updated' })
.where('id', 1)
.then(() => {
expect(called).to.equal(true);
});
});
it('should get passed to the $afterUpdate method', () => {
let Model = inheritModel(Model1);
let context = { a: 1, b: '2' };
let called = false;
Model.prototype.$afterUpdate = function (opt, queryContext) {
expect(queryContext).to.eql(context);
expect(context.transaction).to.equal(undefined);
expect(queryContext.transaction).to.equal(mockKnex);
expect(Object.keys(queryContext).indexOf('transaction')).to.equal(-1);
called = true;
};
return Model.query()
.context(context)
.update({ model1Prop1: 'updated' })
.where('id', 1)
.then(() => {
expect(called).to.equal(true);
});
});
it('should get passed to the $beforeInsert method', () => {
let Model = inheritModel(Model1);
let context = { a: 1, b: '2' };
let called = false;
Model.prototype.$beforeInsert = (queryContext) => {
expect(queryContext).to.eql(context);
expect(context.transaction).to.equal(undefined);
expect(queryContext.transaction).to.equal(mockKnex);
expect(Object.keys(queryContext).indexOf('transaction')).to.equal(-1);
called = true;
};
return Model.query()
.context(context)
.insert({ model1Prop1: 'new' })
.then(() => {
expect(called).to.equal(true);
});
});
it('should get passed to the $afterInsert method', () => {
let Model = inheritModel(Model1);
let context = { a: 1, b: '2' };
let called = false;
Model.prototype.$afterInsert = (queryContext) => {
expect(queryContext).to.eql(context);
expect(context.transaction).to.equal(undefined);
expect(queryContext.transaction).to.equal(mockKnex);
expect(Object.keys(queryContext).indexOf('transaction')).to.equal(-1);
called = true;
};
return Model.query()
.context(context)
.insert({ model1Prop1: 'new' })
.then(() => {
expect(called).to.equal(true);
});
});
it('should get passed to the $beforeDelete method', () => {
let Model = inheritModel(Model1);
let context = { a: 1, b: '2' };
let called = false;
Model.prototype.$beforeDelete = (queryContext) => {
expect(queryContext).to.eql(context);
expect(context.transaction).to.equal(undefined);
expect(queryContext.transaction).to.equal(mockKnex);
expect(Object.keys(queryContext).indexOf('transaction')).to.equal(-1);
called = true;
};
return Model.fromJson({ id: 1 })
.$query()
.context(context)
.delete()
.then(() => {
expect(called).to.equal(true);
});
});
it('should get passed to the $afterDelete method', () => {
let Model = inheritModel(Model1);
let context = { a: 1, b: '2' };
let called = false;
Model.prototype.$afterDelete = (queryContext) => {
expect(queryContext).to.eql(context);
expect(context.transaction).to.equal(undefined);
expect(queryContext.transaction).to.equal(mockKnex);
expect(Object.keys(queryContext).indexOf('transaction')).to.equal(-1);
called = true;
};
return Model.fromJson({ id: 1 })
.$query()
.context(context)
.delete()
.then(() => {
expect(called).to.equal(true);
});
});
it('mergeContex should merge values into the context', () => {
let Model = inheritModel(Model1);
let context = { a: 1, b: '2' };
let merge1 = { c: [10, 11] };
let merge2 = { d: false };
let called = false;
Model.prototype.$afterDelete = (queryContext) => {
expect(queryContext).to.eql(_.assign({}, context, merge1, merge2));
expect(context.transaction).to.equal(undefined);
expect(queryContext.transaction).to.equal(mockKnex);
expect(Object.keys(queryContext).indexOf('transaction')).to.equal(-1);
called = true;
};
return Model.fromJson({ id: 1 })
.$query()
.context(context)
.context(merge1)
.delete()
.context(merge2)
.then(() => {
expect(called).to.equal(true);
});
});
if (utils.isPostgres(session.knex)) {
// The following features work on all databases. We only test against postgres
// so that we can use postgres specific SQL to make the tests simpler.
it('both queries started by `insertAndFetch` should share the same context', () => {
let queries = [];
return (
Model1.query()
.insertAndFetch({ model1Prop1: 'new' })
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
builder.select('Model1.*').returning('*');
},
runBefore: (data, builder) => {
if (builder.isExecutable()) {
queries.push(builder.toKnexQuery().toString());
}
},
})
.then((model) => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'insert into "public"."Model1" ("model1Prop1") values (\'new\') returning *',
'select "Model1".* from "public"."Model1" where "Model1"."id" in (5)',
]);
expect(model.toJSON()).to.eql({
model1Prop1: 'new',
id: 5,
model1Id: null,
model1Prop2: null,
});
})
);
});
it('both queries started by `updateAndFetchById` should share the same context', () => {
let queries = [];
return (
Model1.query()
.updateAndFetchById(1, { model1Prop1: 'updated' })
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
builder.select('Model1.*').returning('*');
},
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
})
.then((model) => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'update "public"."Model1" set "model1Prop1" = \'updated\' where "Model1"."id" = 1 returning *',
'select "Model1".* from "public"."Model1" where "Model1"."id" = 1',
]);
expect(model.toJSON()).to.eql({
model1Prop1: 'updated',
id: 1,
model1Id: 2,
model1Prop2: null,
});
})
);
});
it('all queries created by insertWithRelated should share the same context', () => {
let queries = [];
// We create a query with `insertWithRelated` method that causes multiple queries to be executed.
// We install hooks using for the context object and check that the modifications made in those
// hooks are present in the result. This way we can be sure that the hooks were called for all
// queries.
return (
Model1.query()
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: [
(builder) => {
if (builder.modelClass() === Model1) {
// Add a property that is created by the database engine to make sure that the result
// actually comes from the database.
builder.returning([
'id',
Model1.raw('"model1Prop1" || \' computed1\' as computed'),
]);
}
},
(builder) => {
if (builder.modelClass() == Model2) {
// Add a property that is created by the database engine to make sure that the result
// actually comes from the database.
builder.returning([
'id_col',
Model1.raw('"model2_prop1" || \' computed2\' as computed'),
]);
}
},
],
runBefore: [
function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
],
runAfter: [
(models) => {
// Append text to the end of our computed property to make sure this function is called.
_.each(_.flatten([models]), (model) => {
model.computed += ' after';
});
return models;
},
],
})
.insertGraph({
model1Prop1: 'new 1',
model1Relation1: {
model1Prop1: 'new 2',
model1Relation2: [
{
model2Prop1: 'new 3',
model2Relation1: [
{
model1Prop1: 'new 4',
},
],
},
],
},
})
.then((model) => {
expect(mockKnex.executedQueries.length).to.equal(4);
expect(mockKnex.executedQueries.length).to.equal(queries.length);
chai.expect(mockKnex.executedQueries).to.containSubset(queries);
chai
.expect(mockKnex.executedQueries)
.to.containSubset([
'insert into "public"."Model1" ("model1Prop1") values (\'new 2\'), (\'new 4\') returning "id", "model1Prop1" || \' computed1\' as computed',
'insert into "public"."Model1" ("model1Id", "model1Prop1") values (5, \'new 1\') returning "id", "model1Prop1" || \' computed1\' as computed',
'insert into "public"."model2" ("model1_id", "model2_prop1") values (5, \'new 3\') returning "id_col", "model2_prop1" || \' computed2\' as computed',
'insert into "public"."Model1Model2" ("model1Id", "model2Id") values (6, 3) returning "model1Id"',
]);
expect(model.$toJson()).to.eql({
id: 7,
model1Id: 5,
model1Prop1: 'new 1',
// TODO: why is after twice here?
computed: 'new 1 computed1 after after',
model1Relation1: {
id: 5,
model1Prop1: 'new 2',
computed: 'new 2 computed1 after',
model1Relation2: [
{
idCol: 3,
model1Id: 5,
model2Prop1: 'new 3',
computed: 'new 3 computed2 after',
model2Relation1: [
{
model1Prop1: 'new 4',
id: 6,
computed: 'new 4 computed1 after',
},
],
},
],
},
});
})
);
});
it('all queries created by a eager query should share the same context', () => {
let queries = [];
// We create a query with `eager` method that causes multiple queries to be executed.
// We install hooks using for the context object and check that the modifications made in those
// hooks are present in the result. This way we can be sure that the hooks were called for all
// queries.
return (
Model1.query()
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
// Add a property that is created by the database engine to make sure that the result
// actually comes from the database.
if (builder.modelClass() === Model1) {
builder.select(
'Model1.*',
Model1.raw('"model1Prop1" || \' computed1\' as computed'),
);
} else {
builder.select(
'model2.*',
Model1.raw('"model2_prop1" || \' computed2\' as computed'),
);
}
},
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
runAfter: (models) => {
_.each(_.flatten([models]), (model) => {
model.computed += ' after';
});
return models;
},
})
.where('id', 1)
.withGraphFetched(
'[model1Relation1.[model1Relation1, model1Relation2.model2Relation1]]',
)
.modifyGraph('model1Relation1.model1Relation2', (builder) => {
builder.orderBy('id_col');
})
.then((models) => {
expect(queries).to.eql([
'select "Model1".*, "model1Prop1" || \' computed1\' as computed from "public"."Model1" where "id" = 1',
'select "Model1".*, "model1Prop1" || \' computed1\' as computed from "public"."Model1" where "Model1"."id" in (2)',
'select "Model1".*, "model1Prop1" || \' computed1\' as computed from "public"."Model1" where "Model1"."id" in (3)',
'select "model2".*, "model2_prop1" || \' computed2\' as computed from "public"."model2" where "model2"."model1_id" in (2) order by "id_col" asc',
'select "Model1Model2"."model2Id" as "objectiontmpjoin0", "Model1".*, "model1Prop1" || \' computed1\' as computed from "public"."Model1" inner join "public"."Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1, 2)',
]);
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
computed: 'hello 1 computed1 after',
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
computed: 'hello 2 computed1 after',
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: null,
model1Prop1: 'hello 3',
model1Prop2: null,
computed: 'hello 3 computed1 after',
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'hejsan 1',
model2Prop2: 30,
computed: 'hejsan 1 computed2 after',
$afterFindCalled: 1,
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'hello 4',
model1Prop2: null,
computed: 'hello 4 computed1 after',
$afterFindCalled: 1,
},
],
},
{
idCol: 2,
model1Id: 2,
model2Prop1: 'hejsan 2',
model2Prop2: 20,
computed: 'hejsan 2 computed2 after',
model2Relation1: [],
$afterFindCalled: 1,
},
],
},
},
]);
})
);
});
it('subquery should be able to override the context (1)', () => {
// Disable the actual database query because we use a schema that doesn't exists.
mockKnex.results.push([]);
let queries = [];
return Model1.query()
.withSchema('public')
.select(Model2.query().withSchema('someSchema').avg('model2Prop1').as('avg'))
.context({
runAfter: (res, builder) => {
if (builder.isExecutable()) {
queries.push(builder.toKnexQuery().toString());
}
},
})
.then(() => {
expect(queries).to.eql(mockKnex.executedQueries);
expect(queries).to.eql([
'select (select avg("model2Prop1") from "someSchema"."model2") as "avg" from "public"."Model1"',
]);
});
});
it('subquery should be able to override the context (2)', () => {
// Disable the actual database query because we use a schema that doesn't exists.
mockKnex.results.push([]);
let queries = [];
return Model1.query()
.select(Model2.query().withSchema('someSchema').avg('model2Prop1').as('avg'))
.withSchema('public')
.context({
runAfter: (res, builder) => {
if (builder.isExecutable()) {
queries.push(builder.toKnexQuery().toString());
}
},
})
.then(() => {
expect(queries).to.eql(mockKnex.executedQueries);
expect(queries).to.eql([
'select (select avg("model2Prop1") from "someSchema"."model2") as "avg" from "public"."Model1"',
]);
});
});
it('subquery should be able to override the context (3)', () => {
// Disable the actual database query because we use a schema that doesn't exists.
mockKnex.results.push([]);
let queries = [];
return Model1.query()
.withSchema('public')
.select((builder) => {
builder.avg('model2Prop1').from('model2').withSchema('someSchema').as('avg');
})
.context({
runAfter: (res, builder) => {
if (builder.isExecutable()) {
queries.push(builder.toKnexQuery().toString());
}
},
})
.then(() => {
expect(queries).to.eql(mockKnex.executedQueries);
expect(queries).to.eql([
'select (select avg("model2Prop1") from "someSchema"."model2") as "avg" from "public"."Model1"',
]);
});
});
it('subquery should be able to override the context (4)', () => {
// Disable the actual database query because we use a schema that doesn't exists.
mockKnex.results.push([]);
let queries = [];
return Model1.query()
.select((builder) => {
builder.avg('model2Prop1').from('model2').withSchema('someSchema').as('avg');
})
.withSchema('public')
.context({
runAfter: (res, builder) => {
if (builder.isExecutable()) {
queries.push(builder.toKnexQuery().toString());
}
},
})
.then(() => {
expect(queries).to.eql(mockKnex.executedQueries);
expect(queries).to.eql([
'select (select avg("model2Prop1") from "someSchema"."model2") as "avg" from "public"."Model1"',
]);
});
});
describe('$relatedQuery', () => {
describe('belongs to one relation', () => {
let model2;
let model4;
beforeEach(() => {
return Model1.query()
.whereIn('id', [2, 4])
.then((mod) => {
model2 = _.find(mod, { id: 2 });
model4 = _.find(mod, { id: 4 });
mockKnex.reset();
});
});
it('both queries created by an `insert` should share the same context', () => {
let queries = [];
return (
model4
.$relatedQuery('model1Relation1')
.insert({ model1Prop1: 'new' })
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
// Add a property that is created by the database engine to make sure that the result
// actually comes from the database.
if (builder.modelClass() === Model1) {
builder.returning([
'id',
Model1.raw('"model1Prop1" || \' computed1\' as computed'),
]);
} else if (builder.modelClass() === Model2) {
builder.returning([
'id_col',
Model1.raw('"model2_prop1" || \' computed2\' as computed'),
]);
}
},
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
})
.then((model) => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'insert into "public"."Model1" ("model1Prop1") values (\'new\') returning "id", "model1Prop1" || \' computed1\' as computed',
'update "public"."Model1" set "model1Id" = 5 where "Model1"."id" in (4) returning "id", "model1Prop1" || \' computed1\' as computed',
]);
expect(model.toJSON()).to.eql({
model1Prop1: 'new',
id: 5,
computed: 'new computed1',
});
})
);
});
it('the query created by `relate` should share the same context', () => {
let queries = [];
return (
model4
.$relatedQuery('model1Relation1')
.relate(1)
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
builder.returning('*');
},
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
})
.then(() => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'update "public"."Model1" set "model1Id" = 1 where "Model1"."id" in (4) returning *',
]);
return session.knex('Model1').where('id', 4);
})
.then((rows) => {
expect(rows[0].model1Id).to.eql(1);
})
);
});
it('the query created by `unrelate` should share the same context', () => {
let queries = [];
return (
model2
.$relatedQuery('model1Relation1')
.unrelate()
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
builder.returning('*');
},
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
})
.then(() => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'update "public"."Model1" set "model1Id" = NULL where "Model1"."id" in (2) returning *',
]);
return session.knex('Model1').where('id', 2);
})
.then((rows) => {
expect(rows[0].model1Id).to.eql(null);
})
);
});
});
describe('has many relation', () => {
let model;
let newModel;
beforeEach(() => {
return Model1.query()
.where('id', 2)
.first()
.then((mod) => {
model = mod;
return Model2.query().insert({ model2Prop1: 'new' });
})
.then((newMod) => {
newModel = newMod;
mockKnex.reset();
});
});
it('the query created by `relate` should share the same context', () => {
let queries = [];
return (
model
.$relatedQuery('model1Relation2')
.relate(newModel.idCol)
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
builder.returning('*');
},
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
})
.then(() => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'update "public"."model2" set "model1_id" = 2 where "model2"."id_col" in (3) returning *',
]);
return session.knex('model2').where('id_col', newModel.idCol);
})
.then((rows) => {
expect(rows[0].model1_id).to.eql(2);
})
);
});
it('the query created by `unrelate` should share the same context', () => {
let queries = [];
return (
model
.$relatedQuery('model1Relation2')
.unrelate()
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
builder.returning('*');
},
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
})
.then(() => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'update "public"."model2" set "model1_id" = NULL where "model2"."model1_id" in (2) returning *',
]);
return session.knex('model2');
})
.then((rows) => {
_.each(rows, (row) => {
expect(row.model1_id).to.equal(null);
});
})
);
});
});
describe('many to many relation', () => {
let model;
beforeEach(() => {
return Model2.query()
.where('id_col', 1)
.first()
.then((mod) => {
model = mod;
mockKnex.reset();
});
});
it('both queries created by an insert should share the same context', () => {
let queries = [];
return (
model
.$relatedQuery('model2Relation1')
.insert({ model1Prop1: 'new' })
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
// Add a property that is created by the database engine to make sure that the result
// actually comes from the database.
if (builder.modelClass() === Model1) {
builder.returning([
'id',
Model1.raw('"model1Prop1" || \' computed1\' as computed'),
]);
}
},
runBefore: function () {
queries.push(this.toKnexQuery().toString());
},
})
.then((model) => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'insert into "public"."Model1" ("model1Prop1") values (\'new\') returning "id", "model1Prop1" || \' computed1\' as computed',
'insert into "public"."Model1Model2" ("model1Id", "model2Id") values (5, 1) returning "model1Id"',
]);
expect(model.toJSON()).to.eql({
model1Prop1: 'new',
id: 5,
computed: 'new computed1',
});
})
);
});
it('the query created by `relate` should share the same context', () => {
let queries = [];
return (
model
.$relatedQuery('model2Relation1')
.relate(1)
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
onBuild: (builder) => {
builder.returning('*');
},
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
})
.then(() => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
'insert into "public"."Model1Model2" ("model1Id", "model2Id") values (1, 1) returning *',
]);
return session.knex('Model1Model2');
})
.then((rows) => {
expect(_.filter(rows, { model1Id: 1, model2Id: 1 }).length).to.equal(1);
})
);
});
it('the query created by `unrelate` should share the same context', () => {
let queries = [];
return (
model
.$relatedQuery('model2Relation1')
.unrelate()
.where('Model1.id', 4)
// withSchema uses the context to share the schema between all queries.
.withSchema('public')
.context({
runBefore: function () {
if (this.isExecutable()) {
queries.push(this.toKnexQuery().toString());
}
},
})
.then(() => {
expect(mockKnex.executedQueries).to.eql(queries);
expect(mockKnex.executedQueries).to.eql([
`delete from \"public\".\"Model1Model2\" where \"Model1Model2\".\"model1Id\" in (select \"Model1\".\"id\" from \"public\".\"Model1\" inner join \"public\".\"Model1Model2\" on \"Model1\".\"id\" = \"Model1Model2\".\"model1Id\" where \"Model1Model2\".\"model2Id\" in (1) and \"Model1\".\"id\" = 4) and \"Model1Model2\".\"model2Id\" in (1)`,
]);
return session.knex('Model1Model2');
})
.then((rows) => {
expect(rows).to.have.length(0);
})
);
});
});
});
}
});
};
================================================
FILE: tests/integration/relate.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const chai = require('chai');
module.exports = (session) => {
let Model1 = session.models.Model1;
let Model2 = session.models.Model2;
describe('Model relate queries', () => {
describe('.$query()', () => {
it('should reject the query because relate makes no sense in this context', (done) => {
Model1.fromJson({ id: 1 })
.$query()
.relate(1)
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
});
describe('.$relatedQuery().relate()', () => {
describe('belongs to one relation', () => {
let model1;
let model2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 3',
},
{
id: 3,
model1Prop1: 'hello 4',
},
]);
});
beforeEach(async () => {
model1 = await Model1.query().findById(1);
model2 = await Model1.query().findById(2);
});
it('should relate', () => {
return model1
.$relatedQuery('model1Relation1')
.relate(model2.id)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(model2.id);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(null);
});
});
it('should relate multiple', () => {
return model1
.$relatedQuery('model1Relation1')
.relate([model2.id])
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(model2.id);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(null);
});
});
it('should relate (object value)', () => {
return model1
.$relatedQuery('model1Relation1')
.relate({ id: model2.id })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(model2.id);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(null);
});
});
it('should relate (model value)', () => {
return model1
.$relatedQuery('model1Relation1')
.relate(model2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(model2.id);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(null);
});
});
it('should fail with invalid object value)', (done) => {
model1
.$relatedQuery('model1Relation1')
.relate({ wrongId: model2.id })
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
return session
.knex(Model1.getTableName())
.orderBy('id')
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(null);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(null);
done();
});
});
});
});
describe('has many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 4',
model2Prop2: 3,
},
],
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation2: [
{
idCol: 3,
model2Prop1: 'text 5',
model2Prop2: 2,
},
],
},
]);
});
it('should relate', () => {
return Model1.query()
.where('id', 1)
.first()
.then((model) => {
return model.$relatedQuery('model1Relation2').relate(2);
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1_id).to.equal(1);
expect(rows[1].model1_id).to.equal(1);
expect(rows[2].model1_id).to.equal(3);
});
});
it('should relate (multiple values)', () => {
return Model1.query()
.where('id', 1)
.first()
.then((model) => {
return model.$relatedQuery('model1Relation2').relate([2, 3]);
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1_id).to.equal(1);
expect(rows[1].model1_id).to.equal(1);
expect(rows[2].model1_id).to.equal(1);
});
});
it('should relate (object value)', () => {
return Model1.query()
.where('id', 1)
.first()
.then((model) => {
return model.$relatedQuery('model1Relation2').relate({ idCol: 2 });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1_id).to.equal(1);
expect(rows[1].model1_id).to.equal(1);
expect(rows[2].model1_id).to.equal(3);
});
});
it('should relate (multiple object values)', () => {
return Model1.query()
.where('id', 1)
.first()
.then((model) => {
return model.$relatedQuery('model1Relation2').relate([{ idCol: 2 }, { idCol: 3 }]);
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1_id).to.equal(1);
expect(rows[1].model1_id).to.equal(1);
expect(rows[2].model1_id).to.equal(1);
});
});
});
describe('many to many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 3,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 2,
},
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 1,
},
],
},
],
},
]);
});
it('should relate', () => {
return Model2.query()
.where('id_col', 1)
.first()
.then((model) => {
return model.$relatedQuery('model2Relation1').relate(5);
})
.then((res) => {
expect(res).to.equal(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.filter(rows, { model2Id: 1, model1Id: 3 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 5 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 4 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 5 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 6 })).to.have.length(1);
});
});
if (session.isPostgres()) {
it('should relate (multiple values)', () => {
return Model2.query()
.where('id_col', 1)
.first()
.then((model) => {
return model.$relatedQuery('model2Relation1').relate([5, 6]);
})
.then((res) => {
expect(res).to.equal(2);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(6);
expect(_.filter(rows, { model2Id: 1, model1Id: 3 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 5 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 6 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 4 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 5 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 6 })).to.have.length(1);
});
});
it('should relate with onConflict().ignore()', async () => {
const sql = await Model2.relatedQuery('model2Relation1')
.for(1)
.relate(3)
.onConflict('model1Id')
.ignore()
.toKnexQuery()
.toSQL().sql;
expect(sql).to.equal(
'insert into "Model1Model2" ("model1Id", "model2Id") values (?, ?) on conflict ("model1Id") do nothing returning "model1Id"',
);
});
}
it('should relate (object value)', () => {
return Model2.query()
.where('id_col', 1)
.first()
.then((model) => {
return model.$relatedQuery('model2Relation1').relate({ id: 5 });
})
.then((res) => {
expect(res).to.equal(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.filter(rows, { model2Id: 1, model1Id: 3 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 5 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 4 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 5 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 6 })).to.have.length(1);
});
});
it('should relate with extra properties', () => {
return Model2.query()
.where('id_col', 1)
.first()
.then((model) => {
return model
.$relatedQuery('model2Relation1')
.relate({ id: 5, aliasedExtra: 'foobar' });
})
.then((res) => {
expect(res).to.equal(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(5);
expect(_.filter(rows, { model2Id: 1, model1Id: 3 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 5, extra3: 'foobar' })).to.have.length(
1,
);
expect(_.filter(rows, { model2Id: 2, model1Id: 4 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 5 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 6 })).to.have.length(1);
});
});
});
describe('has one through relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation2: null,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should relate', () => {
return Model2.query()
.where('id_col', 1)
.first()
.then((model) => {
return model.$relatedQuery('model2Relation2').relate(2);
})
.then((res) => {
expect(res).to.equal(1);
return session.knex('Model1Model2One');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 2 })).to.have.length(1);
});
});
});
});
describe('.relatedQuery().relate()', () => {
describe('belongs to one relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 3',
},
{
id: 3,
model1Prop1: 'hello 4',
},
]);
});
it('should relate', () => {
return Model1.relatedQuery('model1Relation1')
.for(1)
.relate(2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(2);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(null);
});
});
it('should relate (object value)', () => {
return Model1.relatedQuery('model1Relation1')
.for(1)
.relate({ id: 2 })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(2);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(null);
});
});
it('should relate for multiple parents', () => {
return Model1.relatedQuery('model1Relation1')
.for([1, 3])
.relate(2)
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(2);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(2);
});
});
it('should relate for multiple parents using a subquery', () => {
return Model1.relatedQuery('model1Relation1')
.for(Model1.query().findByIds([1, 3]))
.relate(2)
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(2);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(2);
});
});
it('should fail with invalid object value)', (done) => {
Model1.relatedQuery('model1Relation1')
.for(1)
.relate({ wrongId: 2 })
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
return session
.knex(Model1.getTableName())
.orderBy('id')
.then((rows) => {
expect(rows).to.have.length(3);
expect(rows[0].model1Id).to.equal(null);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(null);
done();
});
});
});
});
describe('has many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
],
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation2: [
{
idCol: 5,
model2Prop1: 'text 5',
model2Prop2: 2,
},
{
idCol: 6,
model2Prop1: 'text 6',
model2Prop2: 1,
},
],
},
]);
});
it('should relate', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.relate(3)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: 1 },
{ id_col: 3, model1_id: 1 },
{ id_col: 4, model1_id: 2 },
{ id_col: 5, model1_id: 3 },
{ id_col: 6, model1_id: 3 },
]);
});
});
it('should relate using a subquery', () => {
return Model1.relatedQuery('model1Relation2')
.for(Model1.query().findByIds(1))
.relate(3)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: 1 },
{ id_col: 3, model1_id: 1 },
{ id_col: 4, model1_id: 2 },
{ id_col: 5, model1_id: 3 },
{ id_col: 6, model1_id: 3 },
]);
});
});
it('should fail with multiple values', (done) => {
Model1.relatedQuery('model1Relation2')
.for([1, 2])
.relate(3)
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.message).to.equal(
"Can only relate items for one parent at a time in case of HasManyRelation. Otherwise multiple update queries would need to be created. If you need to relate items for multiple parents, simply loop through them. That's the most performant way.",
);
done();
})
.catch(done);
});
it('should relate (multiple values)', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.relate([3, 5])
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: 1 },
{ id_col: 3, model1_id: 1 },
{ id_col: 4, model1_id: 2 },
{ id_col: 5, model1_id: 1 },
{ id_col: 6, model1_id: 3 },
]);
});
});
it('should relate (object value)', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.relate({ idCol: 3 })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: 1 },
{ id_col: 3, model1_id: 1 },
{ id_col: 4, model1_id: 2 },
{ id_col: 5, model1_id: 3 },
{ id_col: 6, model1_id: 3 },
]);
});
});
it('should relate (multiple object values)', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.relate([{ idCol: 3 }, { idCol: 5 }])
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: 1 },
{ id_col: 3, model1_id: 1 },
{ id_col: 4, model1_id: 2 },
{ id_col: 5, model1_id: 1 },
{ id_col: 6, model1_id: 3 },
]);
});
});
});
describe('many to many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 3,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 2,
},
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 1,
},
],
},
],
},
]);
});
it('should relate using one parent id', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.relate(5)
.then((res) => {
expect(res).to.equal(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(5);
chai.expect(rows).to.containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5 },
{ model2Id: 2, model1Id: 4 },
{ model2Id: 2, model1Id: 5 },
{ model2Id: 2, model1Id: 6 },
]);
});
});
it('should relate using a subquery', () => {
return Model2.relatedQuery('model2Relation1')
.for(Model2.query().findById(1))
.relate(5)
.then((res) => {
expect(res).to.equal(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(5);
chai.expect(rows).to.containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5 },
{ model2Id: 2, model1Id: 4 },
{ model2Id: 2, model1Id: 5 },
{ model2Id: 2, model1Id: 6 },
]);
});
});
if (session.isPostgres()) {
it('should relate multiple values', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.relate([5, 6])
.then((res) => {
expect(res).to.equal(2);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(6);
chai.expect(rows).to.containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5 },
{ model2Id: 1, model1Id: 6 },
{ model2Id: 2, model1Id: 4 },
{ model2Id: 2, model1Id: 5 },
{ model2Id: 2, model1Id: 6 },
]);
});
});
it('should relate multiple values using a subquery', () => {
return Model2.relatedQuery('model2Relation1')
.for(Model2.query().findById(1))
.relate([5, 6])
.then(() => {
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(6);
chai.expect(rows).to.containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5 },
{ model2Id: 1, model1Id: 6 },
{ model2Id: 2, model1Id: 4 },
{ model2Id: 2, model1Id: 5 },
{ model2Id: 2, model1Id: 6 },
]);
});
});
it('should relate multiple values for multiple parents', () => {
return Model2.relatedQuery('model2Relation1')
.for([1, 2])
.relate([1, 2])
.then(() => {
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(8);
chai.expect(rows).to.containSubset([
{ model2Id: 1, model1Id: 1 },
{ model2Id: 1, model1Id: 2 },
{ model2Id: 1, model1Id: 3 },
{ model2Id: 2, model1Id: 1 },
{ model2Id: 2, model1Id: 2 },
{ model2Id: 2, model1Id: 4 },
{ model2Id: 2, model1Id: 5 },
{ model2Id: 2, model1Id: 6 },
]);
});
});
}
it('should relate (object value)', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.relate({ id: 5 })
.then((res) => {
expect(res).to.eql(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(5);
chai.expect(rows).to.containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5 },
{ model2Id: 2, model1Id: 4 },
{ model2Id: 2, model1Id: 5 },
{ model2Id: 2, model1Id: 6 },
]);
});
});
it('should relate with extra properties', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.relate({ id: 5, aliasedExtra: 'foobar' })
.then((res) => {
expect(res).to.eql(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(5);
chai.expect(rows).to.containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5, extra3: 'foobar' },
{ model2Id: 2, model1Id: 4 },
{ model2Id: 2, model1Id: 5 },
{ model2Id: 2, model1Id: 6 },
]);
});
});
});
});
});
};
================================================
FILE: tests/integration/relationModify.js
================================================
const { Model } = require('../../');
const expect = require('chai').expect;
module.exports = (session) => {
describe('relation modify hooks', () => {
class Person extends Model {
static get tableName() {
return 'person';
}
static get relationMappings() {
return {
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
modify: (builder) => {
builder.modify(builder.context().belongsToOne);
},
join: {
from: 'person.parentId',
to: 'person.id',
},
},
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
modify: (builder) => {
builder.modify(builder.context().hasMany);
},
join: {
from: 'person.id',
to: 'animal.ownerId',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
modify: (builder) => {
builder.modify(builder.context().manyToMany);
},
join: {
from: 'person.id',
through: {
from: 'personMovie.personId',
to: 'personMovie.movieId',
},
to: 'movie.id',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'animal';
}
}
class Movie extends Model {
static get tableName() {
return 'movie';
}
}
before(() => {
return session.knex.schema
.dropTableIfExists('personMovie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person')
.createTable('person', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('parentId');
})
.createTable('animal', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('ownerId');
})
.createTable('movie', (table) => {
table.increments('id').primary();
table.string('name');
})
.createTable('personMovie', (table) => {
table.integer('personId');
table.integer('movieId');
});
});
before(() => {
Person.knex(session.knex);
Animal.knex(session.knex);
Movie.knex(session.knex);
});
beforeEach(() => {
return Person.query()
.delete()
.then(() => Animal.query().delete())
.then(() => Movie.query().delete())
.then(() => {
return Person.query().insertGraph([
{
name: 'Arnold',
parent: {
name: 'Gustav',
},
pets: [
{
name: 'Freud',
},
{
name: 'Stalin',
},
],
movies: [
{
name: 'Terminator',
},
{
name: 'Terminator 2',
},
],
},
{
name: 'Meinhard',
pets: [
{
name: 'Ruffus',
},
],
},
]);
});
});
describe('$relatedQuery', () => {
describe('belongs to one relation', () => {
it('find', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('parent')
.context(modifyBelongsToOne((qb) => qb.select('name')));
})
.then((gustav) => expect(gustav.name).to.eql('Gustav'));
});
it('update', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('parent')
.context(modifyBelongsToOne((qb) => qb.where('name', 'Not Gustav')))
.update({ name: 'Updated' });
})
.then((numUpdated) => expect(numUpdated).to.equal(0))
.then(findArnold)
.then((arnold) => {
return arnold
.$relatedQuery('parent')
.context(modifyBelongsToOne((qb) => qb.where('name', 'Gustav')))
.update({ name: 'Updated' });
})
.then((numUpdated) => expect(numUpdated).to.equal(1));
});
it('delete', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('parent')
.context(modifyBelongsToOne((qb) => qb.where('name', 'Not Gustav')))
.delete();
})
.then((numDeleted) => expect(numDeleted).to.equal(0))
.then(findArnold)
.then((arnold) => {
return arnold
.$relatedQuery('parent')
.context(modifyBelongsToOne((qb) => qb.where('name', 'Gustav')))
.delete();
})
.then((numDeleted) => expect(numDeleted).to.equal(1));
});
it('insert', () => {
return findArnold()
.then((arnold) => {
// The filter should not affect inserts.
return arnold
.$relatedQuery('parent')
.context(modifyBelongsToOne((qb) => qb.where('name', 'Not Gustav')))
.insert({ name: 'Gustav-neue' });
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('parent'))
.then((gustavNeue) => expect(gustavNeue.name).to.equal('Gustav-neue'));
});
it('relate', () => {
return Promise.all([findArnold(), findMeinhard()])
.then(([arnold, meinhard]) => {
// The filter should not affect relates because the relates
// query is directed at the owner, not the related model.
return arnold
.$relatedQuery('parent')
.context(modifyBelongsToOne((qb) => qb.where('name', 'Not Gustav')))
.relate(meinhard.id);
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('parent'))
.then((meinhard) => expect(meinhard.name).to.equal('Meinhard'));
});
it('unrelate', () => {
return findArnold()
.then((arnold) => {
// The filter should not affect unrelates because the unrelate
// query is directed at the owner, not the related model.
return arnold
.$relatedQuery('parent')
.context(modifyBelongsToOne((qb) => qb.where('name', 'Not Gustav')))
.unrelate();
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('parent'))
.then((parent) => expect(parent).to.eql(undefined));
});
});
describe('has many relation', () => {
it('find', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.select('name').orderBy('name')));
})
.then((pets) => expect(pets.map((it) => it.name)).to.eql(['Freud', 'Stalin']));
});
it('update', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'None of the pets')))
.update({ name: 'Updated' });
})
.then((numUpdated) => expect(numUpdated).to.equal(0))
.then(findArnold)
.then((arnold) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'Freud')))
.update({ name: 'Updated' });
})
.then((numUpdated) => expect(numUpdated).to.equal(1));
});
it('delete', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'None of the pets')))
.delete();
})
.then((numDeleted) => expect(numDeleted).to.equal(0))
.then(findArnold)
.then((arnold) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'Stalin')))
.delete();
})
.then((numDeleted) => expect(numDeleted).to.equal(1));
});
it('insert', () => {
return findArnold()
.then((arnold) => {
// The filter should not affect inserts.
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'None of the pets')))
.insert({ name: 'Cat' });
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('pets').orderBy('name').select('name'))
.then((pets) => expect(pets.map((it) => it.name)).to.eql(['Cat', 'Freud', 'Stalin']));
});
it('relate', () => {
return Promise.all([findArnold(), findRuffus()])
.then(([arnold, ruffus]) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'None of the pets')))
.relate(ruffus.id);
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('pets').orderBy('name').select('name'))
.then((pets) => expect(pets.map((it) => it.name)).to.eql(['Freud', 'Stalin']))
.then(() => Promise.all([findArnold(), findRuffus()]))
.then(([arnold, ruffus]) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'Ruffus')))
.relate(ruffus.id);
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('pets').orderBy('name').select('name'))
.then((pets) =>
expect(pets.map((it) => it.name)).to.eql(['Freud', 'Ruffus', 'Stalin']),
);
});
it('unrelate', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'None of the pets')))
.unrelate();
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('pets').orderBy('name').select('name'))
.then((pets) => expect(pets.map((it) => it.name)).to.eql(['Freud', 'Stalin']))
.then(findArnold)
.then((arnold) => {
return arnold
.$relatedQuery('pets')
.context(modifyHasMany((qb) => qb.where('name', 'Stalin')))
.unrelate();
})
.then(findArnold)
.then((arnold) => arnold.$relatedQuery('pets').orderBy('name').select('name'))
.then((pets) => expect(pets.map((it) => it.name)).to.eql(['Freud']));
});
});
describe('many to many relation', () => {
it('find', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('movies')
.context(modifyManyToMany((qb) => qb.select('name').orderBy('name')));
})
.then((movies) =>
expect(movies.map((it) => it.name)).to.eql(['Terminator', 'Terminator 2']),
);
});
describe('update', () => {
it('simple modifier', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('movies')
.context(modifyManyToMany((qb) => qb.where('name', 'None of the movies')))
.update({ name: 'Updated' });
})
.then((numUpdated) => expect(numUpdated).to.equal(0))
.then(findArnold)
.then((arnold) => {
return arnold
.$relatedQuery('movies')
.context(modifyManyToMany((qb) => qb.where('name', 'Terminator')))
.update({ name: 'Updated' });
})
.then((numUpdated) => expect(numUpdated).to.equal(1));
});
it('modifier with selects', () => {
return findArnold()
.then((arnold) => {
return arnold
.$relatedQuery('movies')
.context(modifyManyToMany((qb) => qb.where('name', 'None of the movies')))
.update({ name: 'Updated' });
})
.then((numUpdated) => expect(numUpdated).to.equal(0))
.then(findArnold)
.then((arnold) => {
return arnold
.$relatedQuery('movies')
.context(modifyManyToMany((qb) => qb.where('name', 'Terminator').select('name')))
.update({ name: 'Updated' });
})
.then((numUpdated) => expect(numUpdated).to.equal(1));
});
});
});
});
describe('eager', () => {
it('belongs to one relation', () => {});
it('has many relation', () => {});
it('many to many relation', () => {});
});
describe('joinEager', () => {
it('belongs to one relation', () => {});
it('has many relation', () => {});
it('many to many relation', () => {});
});
describe('joinRelated', () => {
it('belongs to one relation', () => {});
it('has many relation', () => {});
it('many to many relation', () => {});
});
after(() => {
return session.knex.schema
.dropTableIfExists('personMovie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person');
});
function findArnold() {
return Person.query().findOne('name', 'Arnold');
}
function findMeinhard() {
return Person.query().findOne('name', 'Meinhard');
}
function findRuffus() {
return Animal.query().findOne('name', 'Ruffus');
}
function modifyBelongsToOne(query) {
return {
belongsToOne: query,
};
}
function modifyHasMany(query) {
return {
hasMany: query,
};
}
function modifyManyToMany(query) {
return {
manyToMany: query,
};
}
});
};
================================================
FILE: tests/integration/schema.js
================================================
const expect = require('expect.js');
const Promise = require('bluebird');
const { Model } = require('../../');
module.exports = (session) => {
// TODO igor PR test
if (session.isPostgres()) {
describe('table names with postgres schema', () => {
class Person extends Model {
static get tableName() {
return 'public.Person';
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'public.Person.id',
to: 'public.Animal.ownerId',
},
},
parents: {
relation: Model.ManyToManyRelation,
modelClass: Person,
join: {
from: 'public.Person.id',
through: {
from: 'public.Relatives.childId',
to: 'public.Relatives.parentId',
},
to: 'public.Person.id',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'public.Animal';
}
}
before(() => {
return session.knex.schema
.dropTableIfExists('Relatives')
.dropTableIfExists('Animal')
.dropTableIfExists('Person')
.createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
})
.createTable('Animal', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('ownerId').references('Person.id');
})
.createTable('Relatives', (table) => {
table.increments('id').primary();
table.integer('parentId').references('Person.id').onDelete('CASCADE');
table.integer('childId').references('Person.id').onDelete('CASCADE');
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('Relatives')
.dropTableIfExists('Animal')
.dropTableIfExists('Person');
});
beforeEach(() => {
const knex = session.knex;
return Promise.coroutine(function* () {
yield Animal.query(knex).delete();
yield Person.query(knex).delete();
yield Person.query(knex).insertGraph({
id: 1,
name: 'Arnold',
pets: [
{
id: 1,
name: 'Fluffy',
},
],
parents: [
{
id: 2,
name: 'Mom',
},
{
id: 3,
name: 'Dad',
},
],
});
})();
});
it('simple find query', () => {
return Person.query(session.knex)
.orderBy('id')
.then((people) => {
expect(people).to.eql([
{
id: 1,
name: 'Arnold',
},
{
id: 2,
name: 'Mom',
},
{
id: 3,
name: 'Dad',
},
]);
});
});
it('join eager', () => {
return Person.query(session.knex)
.where('Person.name', 'Arnold')
.withGraphJoined({
pets: true,
parents: true,
})
.then((people) => {
expect(people).to.eql([
{
id: 1,
name: 'Arnold',
pets: [
{
id: 1,
name: 'Fluffy',
ownerId: 1,
},
],
parents: [
{
id: 2,
name: 'Mom',
},
{
id: 3,
name: 'Dad',
},
],
},
]);
});
});
it('columnInfo', () => {
return Person.query(session.knex)
.columnInfo()
.then((info) => {
expect(info instanceof Model).to.equal(false);
expect(info).to.eql({
id: {
type: 'integer',
maxLength: null,
nullable: false,
defaultValue: `nextval('"Person_id_seq"'::regclass)`,
},
name: {
type: 'character varying',
maxLength: 255,
nullable: true,
defaultValue: null,
},
});
});
});
it('many-to-many $relatedQuery', () => {
return Person.query(session.knex)
.findOne('name', 'Arnold')
.then((arnold) => {
return arnold.$relatedQuery('parents', session.knex);
})
.then((parents) => {
expect(parents).to.eql([
{
id: 2,
name: 'Mom',
},
{
id: 3,
name: 'Dad',
},
]);
});
});
});
describe('related tables across non-public postgres schemas', () => {
class Person extends Model {
static get tableName() {
return 'homoSapiens.Person';
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'homoSapiens.Person.id',
to: 'canisFamiliar.Animal.ownerId',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'canisFamiliar.Animal';
}
static get relationMappings() {
return {
owner: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'canisFamiliar.Animal.ownerId',
to: 'homoSapiens.Person.id',
},
},
};
}
}
before(() => {
return session.knex.schema
.createSchema('homoSapiens')
.then(() => {
return session.knex.schema.createSchema('canisFamiliar');
})
.then(() => {
return session.knex.schema
.withSchema('homoSapiens')
.dropTableIfExists('Person')
.createTable('Person', (table) => {
table.increments('id').primary();
table.string('name');
});
})
.then(() => {
return session.knex.schema
.withSchema('canisFamiliar')
.dropTableIfExists('Animal')
.createTable('Animal', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('ownerId').references('id').inTable('homoSapiens.Person');
});
});
});
after(() => {
return session.knex.schema
.withSchema('canisFamiliar')
.dropTableIfExists('Animal')
.then(() => {
return session.knex.schema.withSchema('homoSapiens').dropTableIfExists('Person');
})
.then(() => {
return session.knex.schema.dropSchema('canisFamiliar');
})
.then(() => {
return session.knex.schema.dropSchema('homoSapiens');
});
});
beforeEach(() => {
const knex = session.knex;
return Promise.coroutine(function* () {
yield Animal.query(knex).delete();
yield Person.query(knex).delete();
yield Person.query(knex).insertGraph({
id: 1,
name: 'Arnold',
pets: [
{
id: 1,
name: 'Fluffy',
},
],
});
})();
});
it('simple find query (parent)', () => {
return Person.query(session.knex).then((people) => {
expect(people).to.eql([
{
id: 1,
name: 'Arnold',
},
]);
});
});
it('simple find query (child)', () => {
return Animal.query(session.knex).then((animals) => {
expect(animals).to.eql([
{
id: 1,
name: 'Fluffy',
ownerId: 1,
},
]);
});
});
it('join eager', () => {
return Person.query(session.knex)
.withGraphJoined('pets')
.then((people) => {
expect(people).to.eql([
{
id: 1,
name: 'Arnold',
pets: [
{
id: 1,
name: 'Fluffy',
ownerId: 1,
},
],
},
]);
});
});
it('join eager (inverse)', () => {
return Animal.query(session.knex)
.withGraphJoined('owner')
.then((animals) => {
expect(animals).to.eql([
{
id: 1,
name: 'Fluffy',
ownerId: 1,
owner: {
id: 1,
name: 'Arnold',
},
},
]);
});
});
it('columnInfo', () => {
return Person.query(session.knex)
.columnInfo()
.then((info) => {
expect(info instanceof Model).to.equal(false);
expect(info).to.eql({
id: {
type: 'integer',
maxLength: null,
nullable: false,
defaultValue: `nextval('"homoSapiens"."Person_id_seq"'::regclass)`,
},
name: {
type: 'character varying',
maxLength: 255,
nullable: true,
defaultValue: null,
},
});
});
});
});
}
};
================================================
FILE: tests/integration/snakeCase.js
================================================
const { Model, snakeCaseMappers } = require('../../');
const Promise = require('bluebird');
const expect = require('chai').expect;
module.exports = (session) => {
describe('snakeCaseMappers', () => {
class Person extends Model {
$formatDatabaseJson(json) {
json = super.$formatDatabaseJson(json);
delete json.id;
return json;
}
static get tableName() {
return 'person';
}
static get columnNameMappers() {
return snakeCaseMappers();
}
static get jsonAttributes() {
return ['address'];
}
static get relationMappings() {
return {
parentPerson: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'person.parent_id',
to: 'person.id',
},
},
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'person.id',
to: 'animal.owner_id',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'person.id',
through: {
from: 'person_movie.person_id',
to: 'person_movie.movie_id',
},
to: 'movie.id',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'animal';
}
static get columnNameMappers() {
return snakeCaseMappers();
}
}
class Movie extends Model {
static get tableName() {
return 'movie';
}
static get columnNameMappers() {
return snakeCaseMappers();
}
}
before(() => {
return session.knex.schema
.dropTableIfExists('person_movie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person')
.createTable('person', (table) => {
table.increments('id').primary();
table.string('first_name');
table.integer('parent_id');
if (session.isPostgres()) {
table.jsonb('person_address');
}
})
.createTable('animal', (table) => {
table.increments('id').primary();
table.string('animal_name');
table.integer('owner_id');
})
.createTable('movie', (table) => {
table.increments('id').primary();
table.string('movie_name');
})
.createTable('person_movie', (table) => {
table.integer('person_id');
table.integer('movie_id');
});
});
describe('queries', () => {
beforeEach(() => {
function maybeWithAddress(obj, address) {
if (session.isPostgres()) {
obj.personAddress = address;
}
return obj;
}
return Person.query(session.knex).insertGraph({
firstName: 'Seppo',
parentPerson: {
firstName: 'Teppo',
parentPerson: maybeWithAddress(
{
firstName: 'Matti',
},
{
personCity: 'Jalasjärvi',
cityCoordinates: {
latitudeCoordinate: 61,
longitudeCoordinate: 23,
},
},
),
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit the movie',
},
{
movieName: 'Salkkarit 2, the low quality continues',
},
],
});
});
afterEach(() => {
return ['animal', 'person_movie', 'movie', 'person'].reduce((promise, table) => {
return promise.then(() => session.knex(table).delete());
}, Promise.resolve());
});
it('$relatedQuery', () => {
return Person.query(session.knex)
.findOne({ first_name: 'Seppo' })
.then((model) => {
return model.$relatedQuery('pets', session.knex).orderBy('animal_name');
})
.then((pets) => {
expect(pets).to.containSubset([
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
]);
});
});
it('joinRelated', () => {
return Person.query(session.knex)
.joinRelated('parentPerson.parentPerson')
.select('parentPerson:parentPerson.first_name as nestedRef')
.then((result) => {
expect(result).to.containSubset([{ nestedRef: 'Matti' }]);
});
});
if (session.isPostgres()) {
// TODO: Enable and fix after -> is used as a separator.
it.skip('update with json references', () => {
return Person.query(session.knex)
.where('first_name', 'Matti')
.patch({
'person_address:cityCoordinates.latitudeCoordinate': 30,
})
.returning('*')
.then((result) => {
expect(result).to.containSubset([
{
firstName: 'Matti',
parentId: null,
personAddress: {
personCity: 'Jalasjärvi',
cityCoordinates: {
latitudeCoordinate: 30,
longitudeCoordinate: 23,
},
},
},
]);
});
});
}
['withGraphFetched', 'withGraphJoined'].forEach((method) => {
it(`eager (${method})`, () => {
return Person.query(session.knex)
.select('person.first_name as rootFirstName')
.modifyGraph('parentPerson', (qb) => qb.select('first_name as parentFirstName'))
.modifyGraph('parentPerson.parentPerson', (qb) =>
qb.select('first_name as grandParentFirstName'),
)
[method]('[parentPerson.parentPerson, pets, movies]')
.orderBy('person.first_name')
.then((people) => {
expect(people.length).to.equal(3);
expect(people).to.containSubset([
{
rootFirstName: 'Seppo',
parentPerson: {
parentFirstName: 'Teppo',
parentPerson: {
grandParentFirstName: 'Matti',
},
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit 2, the low quality continues',
},
{
movieName: 'Salkkarit the movie',
},
],
},
{
rootFirstName: 'Teppo',
parentPerson: {
parentFirstName: 'Matti',
},
},
{
rootFirstName: 'Matti',
},
]);
});
});
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('person_movie')
.dropTableIfExists('animal')
.dropTableIfExists('movie')
.dropTableIfExists('person');
});
});
describe('snakeCaseMappers uppercase = true', () => {
class Person extends Model {
static get tableName() {
return 'PERSON';
}
static get idColumn() {
return 'ID';
}
static get columnNameMappers() {
return snakeCaseMappers({ upperCase: true });
}
static get relationMappings() {
return {
parentPerson: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'PERSON.PARENT_ID',
to: 'PERSON.ID',
},
},
pets: {
relation: Model.HasManyRelation,
modelClass: Animal,
join: {
from: 'PERSON.ID',
to: 'ANIMAL.OWNER_ID',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'PERSON.ID',
through: {
from: 'PERSON_MOVIE.PERSON_ID',
to: 'PERSON_MOVIE.MOVIE_ID',
},
to: 'MOVIE.ID',
},
},
};
}
}
class Animal extends Model {
static get tableName() {
return 'ANIMAL';
}
static get idColumn() {
return 'ID';
}
static get columnNameMappers() {
return snakeCaseMappers({ upperCase: true });
}
}
class Movie extends Model {
static get tableName() {
return 'MOVIE';
}
static get idColumn() {
return 'ID';
}
static get columnNameMappers() {
return snakeCaseMappers({ upperCase: true });
}
}
before(() => {
return session.knex.schema
.dropTableIfExists('PERSON_MOVIE')
.dropTableIfExists('ANIMAL')
.dropTableIfExists('MOVIE')
.dropTableIfExists('PERSON')
.createTable('PERSON', (table) => {
table.increments('ID').primary();
table.string('FIRST_NAME');
table.integer('PARENT_ID');
})
.createTable('ANIMAL', (table) => {
table.increments('ID').primary();
table.string('ANIMAL_NAME');
table.integer('OWNER_ID');
})
.createTable('MOVIE', (table) => {
table.increments('ID').primary();
table.string('MOVIE_NAME');
})
.createTable('PERSON_MOVIE', (table) => {
table.integer('PERSON_ID');
table.integer('MOVIE_ID');
});
});
describe('queries', () => {
beforeEach(() => {
return Person.query(session.knex).insertGraph({
firstName: 'Seppo',
parentPerson: {
firstName: 'Teppo',
parentPerson: {
firstName: 'Matti',
},
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit the movie',
},
{
movieName: 'Salkkarit 2, the low quality continues',
},
],
});
});
afterEach(() => {
return ['ANIMAL', 'PERSON_MOVIE', 'MOVIE', 'PERSON'].reduce((promise, table) => {
return promise.then(() => session.knex(table).delete());
}, Promise.resolve());
});
it('$relatedQuery', () => {
return Person.query(session.knex)
.findOne({ FIRST_NAME: 'Seppo' })
.then((model) => {
return model.$relatedQuery('pets', session.knex).orderBy('ANIMAL_NAME');
})
.then((pets) => {
expect(pets).to.containSubset([
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
]);
});
});
['withGraphFetched', 'withGraphJoined'].forEach((method) => {
it(`eager (${method})`, () => {
return Person.query(session.knex)
.select('PERSON.FIRST_NAME as rootFirstName')
.modifyGraph('parentPerson', (qb) => qb.select('FIRST_NAME as parentFirstName'))
.modifyGraph('parentPerson.parentPerson', (qb) =>
qb.select('FIRST_NAME as GRAND_PARENT_FIRST_NAME'),
)
[method]('[parentPerson.parentPerson, pets, movies]')
.orderBy('PERSON.FIRST_NAME')
.then((people) => {
expect(people.length).to.equal(3);
expect(people).to.containSubset([
{
rootFirstName: 'Seppo',
parentPerson: {
parentFirstName: 'Teppo',
parentPerson: {
grandParentFirstName: 'Matti',
},
},
pets: [
{
animalName: 'Hurtta',
},
{
animalName: 'Katti',
},
],
movies: [
{
movieName: 'Salkkarit 2, the low quality continues',
},
{
movieName: 'Salkkarit the movie',
},
],
},
{
rootFirstName: 'Teppo',
parentPerson: {
parentFirstName: 'Matti',
},
},
{
rootFirstName: 'Matti',
},
]);
});
});
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('PERSON_MOVIe')
.dropTableIfExists('ANIMAL')
.dropTableIfExists('MOVIE')
.dropTableIfExists('PERSON');
});
});
};
================================================
FILE: tests/integration/staticHooks.js
================================================
const expect = require('expect.js');
const { expect: chaiExpect } = require('chai');
const { Model } = require('../../');
const mockKnexFactory = require('../../testUtils/mockKnex');
module.exports = (session) => {
describe('static model hooks', () => {
let knex;
let queries = [];
let Person;
let Pet;
let Movie;
before(() => {
return session.knex.schema
.dropTableIfExists('actorsMovies')
.dropTableIfExists('movies')
.dropTableIfExists('pets')
.dropTableIfExists('people')
.createTable('people', (table) => {
table.increments('id').primary();
table.string('name');
})
.createTable('pets', (table) => {
table.increments('id').primary();
table.string('name');
table.string('species');
table.integer('ownerId').unsigned().references('people.id').onDelete('SET NULL');
})
.createTable('movies', (table) => {
table.increments('id').primary();
table.string('name');
})
.createTable('actorsMovies', (table) => {
table.increments('id').primary();
table.integer('personId').unsigned().references('people.id').onDelete('CASCADE');
table.integer('movieId').unsigned().references('movies.id').onDelete('CASCADE');
});
});
after(() => {
return session.knex.schema
.dropTableIfExists('actorsMovies')
.dropTableIfExists('movies')
.dropTableIfExists('pets')
.dropTableIfExists('people');
});
before(() => {
knex = mockKnexFactory(session.knex, function (_, oldImpl, args) {
queries.push(this.toSQL());
return oldImpl.apply(this, args);
});
});
beforeEach(() => {
Person = class extends Model {
static get tableName() {
return 'people';
}
static get relationMappings() {
return {
pets: {
relation: Model.HasManyRelation,
modelClass: Pet,
join: {
from: 'people.id',
to: 'pets.ownerId',
},
},
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'people.id',
through: {
from: 'actorsMovies.personId',
to: 'actorsMovies.movieId',
},
to: 'movies.id',
},
},
};
}
};
Pet = class extends Model {
static get tableName() {
return 'pets';
}
static get relationMappings() {
return {
owner: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'pets.ownerId',
to: 'people.id',
},
},
};
}
};
Movie = class extends Model {
static get tableName() {
return 'movies';
}
static get relationMappings() {
return {
actors: {
relation: Model.ManyToManyRelation,
modelClass: Person,
join: {
from: 'movies.id',
through: {
from: 'actorsMovies.movieId',
to: 'actorsMovies.personId',
},
to: 'people.id',
},
},
};
}
};
Person.knex(knex);
Pet.knex(knex);
Movie.knex(knex);
});
beforeEach(() => {
return Movie.query()
.delete()
.then(() => Pet.query().delete())
.then(() => Person.query().delete());
});
describe('onCreateQuery', () => {
describe('default selects', () => {
beforeEach(() => {
Person.onCreateQuery = (query) => {
query.select('people.name');
};
Pet.onCreateQuery = (query) => {
query.select('pets.name');
};
Movie.onCreateQuery = (query) => {
query.select('movies.name');
};
});
beforeEach(() => {
return Person.query().insertGraph({
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
name: 'Silver Linings Playbook',
},
],
});
});
it('should work with a simple query', () => {
return Person.query()
.findOne('name', 'Jennifer')
.then((result) => {
expect(result).to.eql({
name: 'Jennifer',
});
});
});
it('should work with updates', () => {
return Person.query()
.findOne('name', 'Jennifer')
.patch({ name: 'Jennier II' })
.then((result) => {
expect(result).to.eql(1);
});
});
it('should work with inserts', () => {
return Person.query()
.insert({ name: 'Jennier II' })
.then((result) => {
expect(result.id).to.be.a('number');
});
});
it('should work with deletes', () => {
return Person.query()
.findOne('name', 'Jennifer')
.delete()
.then((result) => {
expect(result).to.eql(1);
});
});
it('should work with eager', () => {
return Person.query()
.findOne('name', 'Jennifer')
.withGraphFetched({
movies: true,
pets: {
owner: true,
},
})
.modifyGraph('pets', (query) => query.orderBy('name', 'desc'))
.then((result) => {
expect(result).to.eql({
name: 'Jennifer',
pets: [
{
name: 'Doggo',
owner: {
name: 'Jennifer',
},
},
{
name: 'Cato',
owner: {
name: 'Jennifer',
},
},
],
movies: [
{
name: 'Silver Linings Playbook',
},
],
});
});
});
it('should work with joinEager', () => {
return Person.query()
.findOne('people.name', 'Jennifer')
.withGraphJoined({
movies: true,
pets: {
owner: true,
},
})
.orderBy('pets.name', 'desc')
.then((result) => {
expect(result).to.eql({
name: 'Jennifer',
pets: [
{
name: 'Doggo',
owner: {
name: 'Jennifer',
},
},
{
name: 'Cato',
owner: {
name: 'Jennifer',
},
},
],
movies: [
{
name: 'Silver Linings Playbook',
},
],
});
});
});
});
});
describe('beforeFind', () => {
beforeEach(() => {
return Person.query().insertGraph(
[
{
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
'#id': 'silver',
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
},
{
name: 'Brad',
pets: [
{
name: 'Jamie',
species: 'Lion',
},
{
name: 'Rob',
species: 'Deer',
},
],
movies: [
{
'#ref': 'silver',
},
],
},
],
{ allowRefs: true },
);
});
beforeEach(() => {
queries = [];
});
describe('query', () => {
it('should be called before normal queries', () => {
Movie.beforeFind = createHookSpy();
return Movie.query().then((movies) => {
expect(movies.length).to.equal(2);
chaiExpect(movies).to.containSubset([
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
]);
expect(Movie.beforeFind.calls.length).to.equal(1);
});
});
it('can be async', () => {
Movie.beforeFind = createHookSpy((_, call) => {
return delay(50).then(() => {
call.itWorked = true;
});
});
return Movie.query().then((movies) => {
expect(movies.length).to.equal(2);
chaiExpect(movies).to.containSubset([
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
]);
expect(Movie.beforeFind.calls.length).to.equal(1);
expect(Movie.beforeFind.calls[0].itWorked).to.equal(true);
});
});
it('should have access to `context`', () => {
Movie.beforeFind = createHookSpy(({ context }) => {
expect(context).to.eql({ a: 1 });
});
return Movie.query()
.context({ a: 1 })
.then(() => {
expect(Movie.beforeFind.calls.length).to.equal(1);
});
});
it('should have access to `transaction`', () => {
Movie.beforeFind = createHookSpy(({ transaction }) => {
expect(transaction).to.equal(Movie.knex());
});
return Movie.query().then(() => {
expect(Movie.beforeFind.calls.length).to.equal(1);
});
});
it('should be able to cancel the query', () => {
Movie.beforeFind = createHookSpy(({ cancelQuery }) => {
cancelQuery();
});
return Movie.query().then((result) => {
expect(result).to.eql([]);
expect(queries.length).to.equal(0);
});
});
it('should be able to cancel the query with a value', () => {
Movie.beforeFind = createHookSpy(({ cancelQuery }) => {
cancelQuery(['lol']);
});
return Movie.query().then((result) => {
expect(result).to.eql(['lol']);
expect(queries.length).to.equal(0);
});
});
});
describe('$query', () => {
it('should have access to `items`', () => {
return Movie.query()
.findOne({ name: 'Hungergames' })
.then((movie) => {
Movie.beforeFind = createHookSpy(({ items }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Hungergames',
},
]);
});
return movie.$query();
})
.then((result) => {
expect(result.name).to.equal('Hungergames');
expect(Movie.beforeFind.calls.length).to.equal(1);
});
});
});
describe('$relatedQuery', () => {
describe('many to many', () => {
it('should have access to `relation` and `items`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Movie.beforeFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return person.$relatedQuery('movies');
})
.then((movies) => {
expect(movies.length).to.equal(2);
chaiExpect(movies).to.containSubset([
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
]);
expect(Movie.beforeFind.calls.length).to.equal(1);
});
});
});
describe('has many', () => {
it('should have access to `relation` and `items`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Pet.beforeFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
return person.$relatedQuery('pets');
})
.then((pets) => {
expect(pets.length).to.equal(2);
chaiExpect(pets).to.containSubset([
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
]);
expect(Pet.beforeFind.calls.length).to.equal(1);
});
});
});
describe('belongs to one', () => {
it('should have access to `relation` and `items`', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
Person.beforeFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
return pet.$relatedQuery('owner');
})
.then((person) => {
chaiExpect(person).to.containSubset({
name: 'Jennifer',
});
expect(Person.beforeFind.calls.length).to.equal(1);
});
});
});
});
describe('eager', () => {
it('should have access to all parents and relation', () => {
Pet.beforeFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(2);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
{
name: 'Brad',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
Person.beforeFind = createHookSpy(({ items, relation }) => {
// Ignore the first call (root query).
if (Person.beforeFind.calls.length === 1) {
return;
}
expect(items.length).to.equal(4);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
{
name: 'Cato',
},
{
name: 'Jamie',
},
{
name: 'Rob',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
Movie.beforeFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(2);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
{
name: 'Brad',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return Person.query()
.withGraphFetched({
movies: true,
pets: {
owner: true,
},
})
.then(() => {
expect(Movie.beforeFind.calls.length).to.equal(1);
expect(Pet.beforeFind.calls.length).to.equal(1);
expect(Person.beforeFind.calls.length).to.equal(2);
});
});
});
});
describe('afterFind', () => {
beforeEach(() => {
return Person.query().insertGraph(
[
{
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
'#id': 'silver',
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
},
{
name: 'Brad',
pets: [
{
name: 'Jamie',
species: 'Lion',
},
{
name: 'Rob',
species: 'Deer',
},
],
movies: [
{
'#ref': 'silver',
},
],
},
],
{ allowRefs: true },
);
});
describe('query', () => {
it('should be called before normal queries', () => {
Movie.afterFind = createHookSpy();
return Movie.query().then((movies) => {
expect(movies.length).to.equal(2);
chaiExpect(movies).to.containSubset([
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
]);
expect(Movie.afterFind.calls.length).to.equal(1);
});
});
it('should be able to change the result', () => {
Movie.afterFind = createHookSpy(({ result }) => {
return ['some', 'crap', result];
});
return Movie.query().then((result) => {
chaiExpect(result).to.containSubset([
'some',
'crap',
[
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
]);
expect(Movie.afterFind.calls.length).to.equal(1);
});
});
it('can be async', () => {
Movie.afterFind = createHookSpy((_, call) => {
return delay(50).then(() => {
call.itWorked = true;
});
});
return Movie.query().then((movies) => {
expect(movies.length).to.equal(2);
chaiExpect(movies).to.containSubset([
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
]);
expect(Movie.afterFind.calls.length).to.equal(1);
expect(Movie.afterFind.calls[0].itWorked).to.equal(true);
});
});
it('should have access to `context`', () => {
Movie.afterFind = createHookSpy(({ context }) => {
expect(context).to.eql({ a: 1 });
});
return Movie.query()
.context({ a: 1 })
.then(() => {
expect(Movie.afterFind.calls.length).to.equal(1);
});
});
it('should have access to `transaction`', () => {
Movie.afterFind = createHookSpy(({ transaction }) => {
expect(transaction).to.equal(Movie.knex());
});
return Movie.query().then(() => {
expect(Movie.afterFind.calls.length).to.equal(1);
});
});
});
describe('$query', () => {
it('should have access to `items`', () => {
return Movie.query()
.findOne({ name: 'Hungergames' })
.then((movie) => {
Movie.afterFind = createHookSpy(({ items }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Hungergames',
},
]);
});
return movie.$query();
})
.then((result) => {
expect(result.name).to.equal('Hungergames');
expect(Movie.afterFind.calls.length).to.equal(1);
});
});
});
describe('$relatedQuery', () => {
describe('many to many', () => {
it('should have access to `relation` and `items`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Movie.afterFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return person.$relatedQuery('movies');
})
.then((movies) => {
expect(movies.length).to.equal(2);
chaiExpect(movies).to.containSubset([
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
]);
expect(Movie.afterFind.calls.length).to.equal(1);
});
});
});
describe('has many', () => {
it('should have access to `relation` and `items`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Pet.afterFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
return person.$relatedQuery('pets');
})
.then((pets) => {
expect(pets.length).to.equal(2);
chaiExpect(pets).to.containSubset([
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
]);
expect(Pet.afterFind.calls.length).to.equal(1);
});
});
});
describe('belongs to one', () => {
it('should have access to `relation` and `items`', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
Person.afterFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
return pet.$relatedQuery('owner');
})
.then((person) => {
chaiExpect(person).to.containSubset({
name: 'Jennifer',
});
expect(Person.afterFind.calls.length).to.equal(1);
});
});
});
});
describe('eager', () => {
it('should have access to all parents and relation', () => {
Pet.afterFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(2);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
{
name: 'Brad',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
Person.afterFind = createHookSpy(({ items, relation }) => {
// Ignore the last call (root query).
if (Person.afterFind.calls.length === 2) {
return;
}
expect(items.length).to.equal(4);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
{
name: 'Cato',
},
{
name: 'Jamie',
},
{
name: 'Rob',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
Movie.afterFind = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(2);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
{
name: 'Brad',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return Person.query()
.withGraphFetched({
movies: true,
pets: {
owner: true,
},
})
.then(() => {
expect(Movie.afterFind.calls.length).to.equal(1);
expect(Pet.afterFind.calls.length).to.equal(1);
expect(Person.afterFind.calls.length).to.equal(2);
});
});
});
});
describe('beforeUpdate', () => {
beforeEach(() => {
return Person.query().insertGraph(
[
{
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
'#id': 'silver',
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
},
{
name: 'Brad',
pets: [
{
name: 'Jamie',
species: 'Lion',
},
{
name: 'Rob',
species: 'Deer',
},
],
movies: [
{
'#ref': 'silver',
},
{
name: 'A Star is Born',
},
],
},
],
{ allowRefs: true },
);
});
beforeEach(() => {
queries = [];
});
describe('query', () => {
it('should be called before normal queries', () => {
Movie.beforeUpdate = createHookSpy();
return Movie.query()
.update({ name: 'Updated' })
.then((numUpdated) => {
expect(numUpdated).to.equal(3);
expect(Movie.beforeUpdate.calls.length).to.equal(1);
});
});
it('can be async', () => {
Movie.beforeUpdate = createHookSpy((_, call) => {
return delay(50).then(() => {
call.itWorked = true;
});
});
return Movie.query()
.update({ name: 'Updated' })
.then((numUpdated) => {
expect(numUpdated).to.equal(3);
expect(Movie.beforeUpdate.calls.length).to.equal(1);
expect(Movie.beforeUpdate.calls[0].itWorked).to.equal(true);
});
});
it('should have access to `context`', () => {
Movie.beforeUpdate = createHookSpy(({ context }) => {
expect(context).to.eql({ a: 1 });
});
return Movie.query()
.update({ name: 'Updated' })
.context({ a: 1 })
.then(() => {
expect(Movie.beforeUpdate.calls.length).to.equal(1);
});
});
it('should have access to `transaction`', () => {
Movie.beforeUpdate = createHookSpy(({ transaction }) => {
expect(transaction).to.equal(Movie.knex());
});
return Movie.query()
.update({ name: 'Updated' })
.then(() => {
expect(Movie.beforeUpdate.calls.length).to.equal(1);
});
});
it('should have access to `inputItems`', () => {
Movie.beforeUpdate = createHookSpy(({ inputItems }) => {
expect(inputItems.length).to.equal(1);
expect(inputItems[0] instanceof Movie).to.equal(true);
chaiExpect(inputItems).to.containSubset([
{
name: 'Updated',
},
]);
});
return Movie.query()
.update({ name: 'Updated' })
.then(() => {
expect(Movie.beforeUpdate.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be updated', () => {
Movie.beforeUpdate = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((moviesToBeUpdated) => {
chaiExpect(moviesToBeUpdated).to.have.length(1);
chaiExpect(moviesToBeUpdated).containSubset([
{
name: 'Hungergames',
},
]);
call.queryWasAwaited = true;
});
});
return Movie.query()
.update({ name: 'Updated' })
.where('name', 'like', '%gam%')
.then(() => {
expect(Movie.beforeUpdate.calls.length).to.equal(1);
expect(Movie.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
it('should be able to fetch the rows about to be updated when using pacthAndFetchById', async () => {
Movie.beforeUpdate = createHookSpy(async ({ asFindQuery }, call) => {
const moviesToBeUpdated = await asFindQuery().select('name').forUpdate();
chaiExpect(moviesToBeUpdated).to.have.length(1);
chaiExpect(moviesToBeUpdated).containSubset([
{
name: 'Hungergames',
},
]);
call.queryWasAwaited = true;
});
const hungerGames = await Movie.query().findOne('name', 'like', '%gam%');
expect(queries.length).to.equal(1);
await Movie.query().patchAndFetchById(hungerGames.id, { name: 'Updated' });
expect(Movie.beforeUpdate.calls.length).to.equal(1);
expect(Movie.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
// findOne + patch + fetch + asFindQuery()
expect(queries.length).to.equal(4);
});
it('should be able to access modelOptions in beforeUpdate when using patchAndFetchById', async () => {
Movie.beforeUpdate = createHookSpy(({ modelOptions }) => {
chaiExpect(modelOptions).to.deep.equal({ patch: true });
});
const hungerGames = await Movie.query().findOne('name', 'like', '%gam%');
expect(queries.length).to.equal(1);
await Movie.query().patchAndFetchById(hungerGames.id, { name: 'Updated' });
expect(Movie.beforeUpdate.calls.length).to.equal(1);
});
it('should populate modelOptions with old data when using upsertGraph', async () => {
Movie.beforeUpdate = createHookSpy(({ modelOptions }) => {
expect(modelOptions).to.have.property('old');
chaiExpect(modelOptions.old).containSubset({ name: 'Hungergames' });
});
const hungerGames = await Movie.query().findOne('name', 'like', '%gam%');
expect(queries.length).to.equal(1);
await Movie.query().upsertGraph({ id: hungerGames.id, name: 'Updated' });
expect(Movie.beforeUpdate.calls.length).to.equal(1);
});
it('should be able to cancel the query', () => {
Movie.beforeUpdate = createHookSpy(({ cancelQuery }) => {
cancelQuery();
});
return Movie.query()
.update({ name: 'Updated' })
.then((numUpdated) => {
expect(numUpdated).to.eql(0);
expect(queries.length).to.equal(0);
});
});
it('should be able to cancel the query with a value', () => {
Movie.beforeUpdate = createHookSpy(({ cancelQuery }) => {
cancelQuery(['lol']);
});
return Movie.query()
.update({ name: 'Updated' })
.then((result) => {
expect(result).to.eql(['lol']);
expect(queries.length).to.equal(0);
});
});
});
describe('$query', () => {
it('should have access to `items` and `inputItems`', () => {
return Movie.query()
.findOne({ name: 'Silver Linings Playbook' })
.then((movie) => {
Movie.beforeUpdate = createHookSpy(({ items, inputItems }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Silver Linings Playbook',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Updated',
},
]);
});
return movie.$query().patch({ name: 'Updated' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(Movie.beforeUpdate.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be updated`', () => {
return Movie.query()
.findOne({ name: 'Silver Linings Playbook' })
.then((movie) => {
queries = [];
Movie.beforeUpdate = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((moviesToBeUpdated) => {
chaiExpect(moviesToBeUpdated).to.have.length(1);
// Note: moviesToBeUpdated must be an array even though $query()
// would normally produce a single item.
chaiExpect(moviesToBeUpdated).containSubset([
{
name: 'Silver Linings Playbook',
},
]);
call.queryWasAwaited = true;
});
});
return movie.$query().patch({ name: 'Updated' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(Movie.beforeUpdate.calls.length).to.equal(1);
expect(Movie.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
});
describe('$relatedQuery', () => {
describe('many to many', () => {
it('should have access to `relation`, `items` and `inputItems`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Movie.beforeUpdate = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Updated',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return person.$relatedQuery('movies').update({ name: 'Updated' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
expect(Movie.beforeUpdate.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be updated`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
queries = [];
Movie.beforeUpdate = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((moviesToBeUpdated) => {
expect(moviesToBeUpdated.length).to.equal(2);
chaiExpect(moviesToBeUpdated).containSubset([
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
]);
call.queryWasAwaited = true;
});
});
return person.$relatedQuery('movies').patch({ name: 'Updated' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
expect(Movie.beforeUpdate.calls.length).to.equal(1);
expect(Movie.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
});
describe('has many', () => {
it('should have access to `relation`, `items` and `inputItems`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Pet.beforeUpdate = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
chaiExpect(inputItems).to.containSubset([
{
species: 'Frog',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
return person.$relatedQuery('pets').patch({ species: 'Frog' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
expect(Pet.beforeUpdate.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be updated`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
queries = [];
Pet.beforeUpdate = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((petsToBeUpdated) => {
expect(petsToBeUpdated.length).to.equal(2);
chaiExpect(petsToBeUpdated).containSubset([
{
name: 'Doggo',
},
{
name: 'Cato',
},
]);
call.queryWasAwaited = true;
});
});
return person.$relatedQuery('pets').patch({ name: 'Updated' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
expect(Pet.beforeUpdate.calls.length).to.equal(1);
expect(Pet.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
it('should be able to fetch the rows about to be updated when relating', async () => {
Pet.beforeUpdate = createHookSpy(async ({ asFindQuery }, call) => {
const petsToBeRelated = await asFindQuery().select('name').forUpdate();
chaiExpect(petsToBeRelated).to.have.length(2);
chaiExpect(petsToBeRelated).containSubset([
{
name: 'Hamsto',
},
{
name: 'Croco',
},
]);
call.queryWasAwaited = true;
});
const jennifer = await Person.query().findOne({ name: 'Jennifer' });
const hamsto = await Pet.query().insert({ name: 'Hamsto', species: 'Hamster' });
const croco = await Pet.query().insert({ name: 'Croco', species: 'Crocodile' });
queries = [];
await jennifer.$relatedQuery('pets').relate([hamsto.id, croco.id]);
expect(Pet.beforeUpdate.calls.length).to.equal(1);
expect(Pet.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
it('should be able to fetch the rows about to be updated when unrelating', async () => {
Pet.beforeUpdate = createHookSpy(async ({ asFindQuery }, call) => {
const petsToBeUnrelated = await asFindQuery().select('name').forUpdate();
chaiExpect(petsToBeUnrelated).to.have.length(2);
chaiExpect(petsToBeUnrelated).containSubset([
{
name: 'Doggo',
},
{
name: 'Cato',
},
]);
call.queryWasAwaited = true;
});
const jennifer = await Person.query().findOne({ name: 'Jennifer' });
queries = [];
await jennifer.$relatedQuery('pets').unrelate();
expect(Pet.beforeUpdate.calls.length).to.equal(1);
expect(Pet.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
describe('belongs to one', () => {
it('should have access to `relation`, `items` and `inputItems', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
Person.beforeUpdate = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'New Owner',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
return pet.$relatedQuery('owner').patch({ name: 'New Owner' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(Person.beforeUpdate.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be updated`', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
queries = [];
Person.beforeUpdate = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((peopleToBeUpdated) => {
expect(peopleToBeUpdated.length).to.equal(1);
chaiExpect(peopleToBeUpdated).containSubset([
{
name: 'Jennifer',
},
]);
call.queryWasAwaited = true;
});
});
return pet.$relatedQuery('owner').patch({ name: 'Updated' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(Person.beforeUpdate.calls.length).to.equal(1);
expect(Person.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
});
});
});
describe('afterUpdate', () => {
beforeEach(() => {
return Person.query().insertGraph(
[
{
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
'#id': 'silver',
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
},
{
name: 'Brad',
pets: [
{
name: 'Jamie',
species: 'Lion',
},
{
name: 'Rob',
species: 'Deer',
},
],
movies: [
{
'#ref': 'silver',
},
{
name: 'A Star is Born',
},
],
},
],
{ allowRefs: true },
);
});
beforeEach(() => {
queries = [];
});
describe('query', () => {
it('should be called after normal queries', () => {
Movie.afterUpdate = createHookSpy();
return Movie.query()
.update({ name: 'Updated' })
.then((numUpdated) => {
expect(numUpdated).to.equal(3);
expect(Movie.afterUpdate.calls.length).to.equal(1);
});
});
it('should be able to change the result', () => {
Movie.afterUpdate = createHookSpy(({ result }) => {
return {
numUpdated: result[0],
};
});
return Movie.query()
.update({ name: 'Updated' })
.then((result) => {
expect(result).to.eql({ numUpdated: 3 });
});
});
it('can be async', () => {
Movie.afterUpdate = createHookSpy((_, call) => {
return delay(50).then(() => {
call.itWorked = true;
});
});
return Movie.query()
.update({ name: 'Updated' })
.then((numUpdated) => {
expect(numUpdated).to.equal(3);
expect(Movie.afterUpdate.calls.length).to.equal(1);
expect(Movie.afterUpdate.calls[0].itWorked).to.equal(true);
});
});
it('should have access to `context`', () => {
Movie.afterUpdate = createHookSpy(({ context }) => {
expect(context).to.eql({ a: 1 });
});
return Movie.query()
.update({ name: 'Updated' })
.context({ a: 1 })
.then(() => {
expect(Movie.afterUpdate.calls.length).to.equal(1);
});
});
it('should have access to `transaction`', () => {
Movie.afterUpdate = createHookSpy(({ transaction }) => {
expect(transaction).to.equal(Movie.knex());
});
return Movie.query()
.update({ name: 'Updated' })
.then(() => {
expect(Movie.afterUpdate.calls.length).to.equal(1);
});
});
it('should have access to `inputItems`', () => {
Movie.afterUpdate = createHookSpy(({ inputItems }) => {
expect(inputItems.length).to.equal(1);
expect(inputItems[0] instanceof Movie).to.equal(true);
chaiExpect(inputItems).to.containSubset([
{
name: 'Updated',
},
]);
});
return Movie.query()
.update({ name: 'Updated' })
.then(() => {
expect(Movie.afterUpdate.calls.length).to.equal(1);
});
});
});
describe('$query', () => {
it('should have access to `items` and `inputItems`', () => {
return Movie.query()
.findOne({ name: 'Silver Linings Playbook' })
.then((movie) => {
Movie.afterUpdate = createHookSpy(({ items, inputItems }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Silver Linings Playbook',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Updated',
},
]);
});
return movie.$query().patch({ name: 'Updated' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(Movie.afterUpdate.calls.length).to.equal(1);
});
});
});
describe('$relatedQuery', () => {
describe('many to many', () => {
it('should have access to `relation`, `items` and `inputItems`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Movie.afterUpdate = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Updated',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return person.$relatedQuery('movies').update({ name: 'Updated' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
expect(Movie.afterUpdate.calls.length).to.equal(1);
});
});
});
describe('has many', () => {
it('should have access to `relation`, `items` and `inputItems`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Pet.afterUpdate = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
chaiExpect(inputItems).to.containSubset([
{
species: 'Frog',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
return person.$relatedQuery('pets').patch({ species: 'Frog' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
expect(Pet.afterUpdate.calls.length).to.equal(1);
});
});
});
describe('belongs to one', () => {
it('should have access to `relation`, `items` and `inputItems', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
Person.afterUpdate = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'New Owner',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
return pet.$relatedQuery('owner').patch({ name: 'New Owner' });
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(Person.afterUpdate.calls.length).to.equal(1);
});
});
});
});
});
describe('beforeDelete', () => {
beforeEach(() => {
return Person.query().insertGraph(
[
{
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
'#id': 'silver',
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
},
{
name: 'Brad',
pets: [
{
name: 'Jamie',
species: 'Lion',
},
{
name: 'Rob',
species: 'Deer',
},
],
movies: [
{
'#ref': 'silver',
},
{
name: 'A Star is Born',
},
],
},
],
{ allowRefs: true },
);
});
beforeEach(() => {
queries = [];
});
describe('query', () => {
it('should be called before normal queries', () => {
Movie.beforeDelete = createHookSpy();
return Movie.query()
.delete()
.where('name', 'A Star is Born')
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(Movie.beforeDelete.calls.length).to.equal(1);
});
});
it('can be async', () => {
Movie.beforeDelete = createHookSpy((_, call) => {
return delay(50).then(() => {
call.itWorked = true;
});
});
return Movie.query()
.delete()
.where('name', 'A Star is Born')
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(Movie.beforeDelete.calls.length).to.equal(1);
expect(Movie.beforeDelete.calls[0].itWorked).to.equal(true);
});
});
it('should have access to `context`', () => {
Movie.beforeDelete = createHookSpy(({ context }) => {
expect(context).to.eql({ a: 1 });
});
return Movie.query()
.delete()
.where('name', 'A Star is Born')
.context({ a: 1 })
.then(() => {
expect(Movie.beforeDelete.calls.length).to.equal(1);
});
});
it('should have access to `transaction`', () => {
Movie.beforeDelete = createHookSpy(({ transaction }) => {
expect(transaction).to.equal(Movie.knex());
});
return Movie.query()
.delete()
.where('name', 'A Star is Born')
.then(() => {
expect(Movie.beforeDelete.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be deleted', () => {
Movie.beforeDelete = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((moviesToBeDeleted) => {
chaiExpect(moviesToBeDeleted).to.have.length(1);
chaiExpect(moviesToBeDeleted).containSubset([
{
name: 'A Star is Born',
},
]);
call.queryWasAwaited = true;
});
});
return Movie.query()
.delete()
.where('name', 'A Star is Born')
.then(() => {
expect(Movie.beforeDelete.calls.length).to.equal(1);
expect(Movie.beforeDelete.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
it('should be able to cancel the query', () => {
Movie.beforeDelete = createHookSpy(({ cancelQuery }) => {
cancelQuery();
});
return Movie.query()
.delete()
.where('name', 'A Star is Born')
.then((numDeleted) => {
expect(numDeleted).to.eql(0);
expect(queries.length).to.equal(0);
});
});
it('should be able to cancel the query with a value', () => {
Movie.beforeDelete = createHookSpy(({ cancelQuery }) => {
cancelQuery(['lol']);
});
return Movie.query()
.delete()
.where('name', 'A Star is Born')
.then((result) => {
expect(result).to.eql(['lol']);
expect(queries.length).to.equal(0);
});
});
it('should be able to fetch the rows about to be deleted`', async () => {
queries = [];
Movie.beforeDelete = createHookSpy(async ({ asFindQuery, cancelQuery }) => {
const numUpdated = await asFindQuery().patch({ name: 'deleted' });
cancelQuery(numUpdated);
});
const numPatched = await Movie.query()
.findOne({ name: 'Silver Linings Playbook' })
.delete();
expect(numPatched).to.equal(1);
expect(queries.length).to.equal(1);
expect(queries[0].bindings).to.eql(['deleted', 'Silver Linings Playbook']);
if (session.isMySql()) {
expect(queries[0].sql).to.equal('update `movies` set `name` = ? where `name` = ?');
} else if (session.isPostgres()) {
expect(queries[0].sql).to.equal('update "movies" set "name" = ? where "name" = ?');
}
});
});
describe('$query', () => {
it('should have access to `items`', () => {
return Movie.query()
.findOne({ name: 'Silver Linings Playbook' })
.then((movie) => {
Movie.beforeDelete = createHookSpy(({ items, inputItems }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Silver Linings Playbook',
},
]);
});
return movie.$query().delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(Movie.beforeDelete.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be deleted`', () => {
return Movie.query()
.findOne({ name: 'Silver Linings Playbook' })
.then((movie) => {
queries = [];
Movie.beforeDelete = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((moviesToBeDeleted) => {
chaiExpect(moviesToBeDeleted).to.have.length(1);
// Note: moviesToBeDeleted must be an array even though $query()
// would normally produce a single item.
chaiExpect(moviesToBeDeleted).containSubset([
{
name: 'Silver Linings Playbook',
},
]);
call.queryWasAwaited = true;
});
});
return movie.$query().delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(Movie.beforeDelete.calls.length).to.equal(1);
expect(Movie.beforeDelete.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
});
describe('$relatedQuery', () => {
describe('many to many', () => {
it('should have access to `relation` and `items`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Movie.beforeDelete = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return person.$relatedQuery('movies').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
expect(Movie.beforeDelete.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be deleted`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
queries = [];
Movie.beforeDelete = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((moviesToBeDeleted) => {
expect(moviesToBeDeleted.length).to.equal(2);
chaiExpect(moviesToBeDeleted).containSubset([
{
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
]);
call.queryWasAwaited = true;
});
});
return person.$relatedQuery('movies').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
expect(Movie.beforeDelete.calls.length).to.equal(1);
expect(Movie.beforeDelete.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
});
describe('has many', () => {
it('should have access to `relation` and `items`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Pet.beforeDelete = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
return person.$relatedQuery('pets').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
expect(Pet.beforeDelete.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be deleted`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
queries = [];
Pet.beforeDelete = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((moviesToBeDeleted) => {
expect(moviesToBeDeleted.length).to.equal(2);
chaiExpect(moviesToBeDeleted).containSubset([
{
name: 'Doggo',
},
{
name: 'Cato',
},
]);
call.queryWasAwaited = true;
});
});
return person.$relatedQuery('pets').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
expect(Pet.beforeDelete.calls.length).to.equal(1);
expect(Pet.beforeDelete.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
});
describe('belongs to one', () => {
it('should have access to `relation` and `items`', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
Person.beforeDelete = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
return pet.$relatedQuery('owner').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(Person.beforeDelete.calls.length).to.equal(1);
});
});
it('should be able to fetch the rows about to be deleted`', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
queries = [];
Person.beforeDelete = createHookSpy(({ asFindQuery }, call) => {
return asFindQuery()
.select('name')
.forUpdate()
.then((peopleToBeDeleted) => {
expect(peopleToBeDeleted.length).to.equal(1);
chaiExpect(peopleToBeDeleted).containSubset([
{
name: 'Jennifer',
},
]);
call.queryWasAwaited = true;
});
});
return pet.$relatedQuery('owner').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(Person.beforeDelete.calls.length).to.equal(1);
expect(Person.beforeDelete.calls[0].queryWasAwaited).to.equal(true);
expect(queries.length).to.equal(2);
});
});
});
});
});
describe('afterDelete', () => {
beforeEach(() => {
return Person.query().insertGraph(
[
{
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
'#id': 'silver',
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
},
{
name: 'Brad',
pets: [
{
name: 'Jamie',
species: 'Lion',
},
{
name: 'Rob',
species: 'Deer',
},
],
movies: [
{
'#ref': 'silver',
},
{
name: 'A Star is Born',
},
],
},
],
{ allowRefs: true },
);
});
beforeEach(() => {
queries = [];
});
describe('query', () => {
it('should be called after normal queries', () => {
Movie.afterDelete = createHookSpy();
return Movie.query()
.delete()
.then((numDeleted) => {
expect(numDeleted).to.equal(3);
expect(Movie.afterDelete.calls.length).to.equal(1);
});
});
it('should be able to change the result', () => {
Movie.afterDelete = createHookSpy(({ result }) => {
return {
numDeleted: result[0],
};
});
return Movie.query()
.delete()
.where('name', 'Hungergames')
.then((result) => {
expect(result).to.eql({ numDeleted: 1 });
});
});
it('can be async', () => {
Movie.afterDelete = createHookSpy((_, call) => {
return delay(50).then(() => {
call.itWorked = true;
});
});
return Movie.query()
.delete()
.where('name', 'Hungergames')
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(Movie.afterDelete.calls.length).to.equal(1);
expect(Movie.afterDelete.calls[0].itWorked).to.equal(true);
});
});
it('should have access to `context`', () => {
Movie.afterDelete = createHookSpy(({ context }) => {
expect(context).to.eql({ a: 1 });
});
return Movie.query()
.delete()
.where('name', 'Hungergames')
.context({ a: 1 })
.then(() => {
expect(Movie.afterDelete.calls.length).to.equal(1);
});
});
it('should have access to `transaction`', () => {
Movie.afterDelete = createHookSpy(({ transaction }) => {
expect(transaction).to.equal(Movie.knex());
});
return Movie.query()
.delete()
.where('name', 'Hungergames')
.then(() => {
expect(Movie.afterDelete.calls.length).to.equal(1);
});
});
});
describe('$query', () => {
it('should have access to `items`', () => {
return Movie.query()
.findOne({ name: 'Silver Linings Playbook' })
.then((movie) => {
Movie.afterDelete = createHookSpy(({ items }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Silver Linings Playbook',
},
]);
});
return movie.$query().delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(Movie.afterDelete.calls.length).to.equal(1);
});
});
});
describe('$relatedQuery', () => {
describe('many to many', () => {
it('should have access to `relation` and `items`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Movie.afterDelete = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return person.$relatedQuery('movies').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
expect(Movie.afterDelete.calls.length).to.equal(1);
});
});
});
describe('has many', () => {
it('should have access to `relation` and `items`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Pet.afterDelete = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
return person.$relatedQuery('pets').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
expect(Pet.afterDelete.calls.length).to.equal(1);
});
});
});
describe('belongs to one', () => {
it('should have access to `relation` and `items`', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
Person.afterDelete = createHookSpy(({ items, relation }) => {
expect(items.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
return pet.$relatedQuery('owner').delete();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
expect(Person.afterDelete.calls.length).to.equal(1);
});
});
});
});
});
describe('beforeInsert', () => {
beforeEach(() => {
return Person.query().insertGraph(
[
{
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
'#id': 'silver',
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
},
{
name: 'Brad',
pets: [
{
name: 'Jamie',
species: 'Lion',
},
{
name: 'Rob',
species: 'Deer',
},
],
movies: [
{
'#ref': 'silver',
},
{
name: 'A Star is Born',
},
],
},
],
{ allowRefs: true },
);
});
beforeEach(() => {
queries = [];
});
describe('query', () => {
it('should be called before normal queries', () => {
Movie.beforeInsert = createHookSpy();
return Movie.query()
.insert({ name: 'Inserted' })
.then((movie) => {
expect(movie.id).to.be.a('number');
expect(Movie.beforeInsert.calls.length).to.equal(1);
});
});
it('can be async', () => {
Movie.beforeInsert = createHookSpy((_, call) => {
return delay(50).then(() => {
call.itWorked = true;
});
});
return Movie.query()
.insert({ name: 'Inserted' })
.then((movie) => {
expect(movie.id).to.be.a('number');
expect(Movie.beforeInsert.calls.length).to.equal(1);
expect(Movie.beforeInsert.calls[0].itWorked).to.equal(true);
});
});
it('should have access to `context`', () => {
Movie.beforeInsert = createHookSpy(({ context }) => {
expect(context).to.eql({ a: 1 });
});
return Movie.query()
.insert({ name: 'Inserted' })
.context({ a: 1 })
.then(() => {
expect(Movie.beforeInsert.calls.length).to.equal(1);
});
});
it('should have access to `transaction`', () => {
Movie.beforeInsert = createHookSpy(({ transaction }) => {
expect(transaction).to.equal(Movie.knex());
});
return Movie.query()
.insert({ name: 'Inserted' })
.then(() => {
expect(Movie.beforeInsert.calls.length).to.equal(1);
});
});
it('should have access to `inputItems`', async () => {
Movie.beforeInsert = createHookSpy(({ inputItems }) => {
expect(inputItems.length).to.equal(1);
expect(inputItems[0] instanceof Movie).to.equal(true);
chaiExpect(inputItems).to.containSubset([
{
name: 'Inserted',
},
]);
});
await Movie.query().insert({ name: 'Inserted' });
await Movie.query().insertAndFetch({ name: 'Inserted' });
expect(Movie.beforeInsert.calls.length).to.equal(2);
});
it('should be able to cancel the query', () => {
Movie.beforeInsert = createHookSpy(({ cancelQuery }) => {
cancelQuery();
});
return Movie.query()
.insert({ name: 'Inserted' })
.then((result) => {
expect(result.name).to.equal('Inserted');
expect(result.id).to.equal(undefined);
expect(queries.length).to.equal(0);
});
});
it('should be able to cancel the query with a value', () => {
Movie.beforeInsert = createHookSpy(({ cancelQuery }) => {
cancelQuery([{ lol: true }]);
});
return Movie.query()
.insert({ name: 'Inserted' })
.then((result) => {
expect(result.lol).to.equal(true);
expect(queries.length).to.equal(0);
});
});
});
describe('$query', () => {
it('should have access to `items` and `inputItems`', () => {
const movie = Movie.fromJson({ name: 'Inserted' });
Movie.beforeInsert = createHookSpy(({ items, inputItems }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Inserted',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Inserted',
},
]);
});
return movie
.$query()
.insert()
.then((result) => {
expect(result.id).to.be.a('number');
expect(Movie.beforeInsert.calls.length).to.equal(1);
});
});
});
describe('$relatedQuery', () => {
describe('many to many', () => {
it('should have access to `relation`, `items` and `inputItems`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Movie.beforeInsert = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Inserted',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return person.$relatedQuery('movies').insert({ name: 'Inserted' });
})
.then((inserted) => {
expect(inserted.id).to.be.a('number');
expect(Movie.beforeInsert.calls.length).to.equal(1);
});
});
it('should be able to cancel the query', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
queries = [];
Movie.beforeInsert = createHookSpy(({ cancelQuery }) => {
cancelQuery();
});
return person.$relatedQuery('movies').insert({ name: 'Inserted' });
})
.then((inserted) => {
expect(inserted.id).to.equal(undefined);
expect(Movie.beforeInsert.calls.length).to.equal(1);
expect(queries.length).to.equal(0);
});
});
});
describe('has many', () => {
it('should have access to `relation`, `items` and `inputItems`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Pet.beforeInsert = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
chaiExpect(inputItems).to.containSubset([
{
species: 'Frog',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
return person.$relatedQuery('pets').insert({ species: 'Frog' });
})
.then((inserted) => {
expect(inserted.id).to.be.a('number');
expect(inserted.species).to.equal('Frog');
expect(Pet.beforeInsert.calls.length).to.equal(1);
});
});
});
describe('belongs to one', () => {
it('should have access to `relation`, `items` and `inputItems', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
Pet.beforeUpdate = createHookSpy(async ({ asFindQuery }, call) => {
const pets = await asFindQuery().select('name');
expect(pets).to.have.length(1);
expect(pets[0].name).to.equal('Doggo');
call.queryWasAwaited = true;
});
Person.beforeInsert = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'New Owner',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
return pet.$relatedQuery('owner').insert({ name: 'New Owner' });
})
.then((inserted) => {
expect(inserted.id).to.be.a('number');
expect(inserted.name).to.equal('New Owner');
expect(Person.beforeInsert.calls.length).to.equal(1);
expect(Pet.beforeUpdate.calls.length).to.equal(1);
expect(Pet.beforeUpdate.calls[0].queryWasAwaited).to.equal(true);
});
});
it('should be able to cancel the query', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
queries = [];
Person.beforeInsert = createHookSpy(({ cancelQuery }) => {
cancelQuery();
});
return pet.$relatedQuery('owner').insert({ name: 'New Owner' });
})
.then((inserted) => {
expect(inserted.id).to.equal(undefined);
expect(inserted.name).to.equal('New Owner');
expect(Person.beforeInsert.calls.length).to.equal(1);
expect(queries.length).to.equal(0);
});
});
});
});
});
describe('afterInsert', () => {
beforeEach(() => {
return Person.query().insertGraph(
[
{
name: 'Jennifer',
pets: [
{
name: 'Doggo',
species: 'dog',
},
{
name: 'Cato',
species: 'cat',
},
],
movies: [
{
'#id': 'silver',
name: 'Silver Linings Playbook',
},
{
name: 'Hungergames',
},
],
},
{
name: 'Brad',
pets: [
{
name: 'Jamie',
species: 'Lion',
},
{
name: 'Rob',
species: 'Deer',
},
],
movies: [
{
'#ref': 'silver',
},
{
name: 'A Star is Born',
},
],
},
],
{ allowRefs: true },
);
});
beforeEach(() => {
queries = [];
});
describe('query', () => {
it('should be called after normal queries', () => {
Movie.afterInsert = createHookSpy();
return Movie.query()
.insert({ name: 'Inserted' })
.then((movie) => {
expect(movie.id).to.be.a('number');
expect(Movie.afterInsert.calls.length).to.equal(1);
});
});
it('should be able to change the result', () => {
Movie.afterInsert = createHookSpy(({ result }) => {
return {
someId: result[0].id,
someName: result[0].name,
};
});
return Movie.query()
.insert({ name: 'Inserted' })
.then((result) => {
expect(result.someId).to.be.a('number');
expect(result.someName).to.equal('Inserted');
});
});
it('can be async', () => {
Movie.afterInsert = createHookSpy((_, call) => {
return delay(50).then(() => {
call.itWorked = true;
});
});
return Movie.query()
.insert({ name: 'Inserted' })
.then(() => {
expect(Movie.afterInsert.calls.length).to.equal(1);
expect(Movie.afterInsert.calls[0].itWorked).to.equal(true);
});
});
it('should have access to `context`', () => {
Movie.afterInsert = createHookSpy(({ context }) => {
expect(context).to.eql({ a: 1 });
});
return Movie.query()
.insert({ name: 'Inserted' })
.context({ a: 1 })
.then(() => {
expect(Movie.afterInsert.calls.length).to.equal(1);
});
});
it('should have access to `transaction`', () => {
Movie.afterInsert = createHookSpy(({ transaction }) => {
expect(transaction).to.equal(Movie.knex());
});
return Movie.query()
.insert({ name: 'Inserted' })
.then(() => {
expect(Movie.afterInsert.calls.length).to.equal(1);
});
});
it('should have access to `inputItems`', () => {
Movie.afterInsert = createHookSpy(({ inputItems }) => {
expect(inputItems.length).to.equal(1);
expect(inputItems[0] instanceof Movie).to.equal(true);
chaiExpect(inputItems).to.containSubset([
{
name: 'Inserted',
},
]);
});
return Movie.query()
.insert({ name: 'Inserted' })
.then(() => {
expect(Movie.afterInsert.calls.length).to.equal(1);
});
});
});
describe('$query', () => {
it('should have access to `items` and `inputItems`', () => {
const movie = Movie.fromJson({ name: 'Inserted' });
Movie.afterInsert = createHookSpy(({ items, inputItems }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Inserted',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Inserted',
},
]);
});
return movie
.$query()
.insert()
.then((movie) => {
expect(movie.id).to.be.a('number');
expect(Movie.afterInsert.calls.length).to.equal(1);
});
});
});
describe('$relatedQuery', () => {
describe('many to many', () => {
it('should have access to `relation`, `items` and `inputItems`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Movie.afterInsert = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Inserted',
},
]);
expect(relation).to.equal(Person.getRelation('movies'));
});
return person.$relatedQuery('movies').insert({ name: 'Inserted' });
})
.then((inserted) => {
expect(inserted.id).to.be.a('number');
expect(Movie.afterInsert.calls.length).to.equal(1);
});
});
});
describe('has many', () => {
it('should have access to `relation`, `items` and `inputItems`', () => {
return Person.query()
.findOne({ name: 'Jennifer' })
.then((person) => {
Pet.afterInsert = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Jennifer',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'Lol',
species: 'Frog',
},
]);
expect(relation).to.equal(Person.getRelation('pets'));
});
return person.$relatedQuery('pets').insert({ name: 'Lol', species: 'Frog' });
})
.then((inserted) => {
expect(inserted.id).to.be.a('number');
expect(Pet.afterInsert.calls.length).to.equal(1);
});
});
});
describe('belongs to one', () => {
it('should have access to `relation`, `items` and `inputItems', () => {
return Pet.query()
.findOne({ name: 'Doggo' })
.then((pet) => {
Person.afterInsert = createHookSpy(({ items, inputItems, relation }) => {
expect(items.length).to.equal(1);
expect(inputItems.length).to.equal(1);
chaiExpect(items).to.containSubset([
{
name: 'Doggo',
},
]);
chaiExpect(inputItems).to.containSubset([
{
name: 'New Owner',
},
]);
expect(relation).to.equal(Pet.getRelation('owner'));
});
return pet.$relatedQuery('owner').insert({ name: 'New Owner' });
})
.then((inserted) => {
expect(inserted.id).to.be.a('number');
expect(Person.afterInsert.calls.length).to.equal(1);
});
});
});
});
});
});
};
function createHookSpy(hook = () => {}) {
const spy = (args) => {
const call = { args };
spy.calls.push(call);
return hook(args, call);
};
spy.calls = [];
return spy;
}
function delay(millis) {
return new Promise((resolve) => setTimeout(resolve, millis));
}
================================================
FILE: tests/integration/toKnexQuery.js
================================================
const expectJs = require('expect.js');
const { expect } = require('chai');
const { Model, val, raw, initialize } = require('../../');
module.exports = (session) => {
describe(`toKnexQuery`, () => {
const { knex } = session;
let Person;
before(() => {
return knex.schema.dropTableIfExists('persons').createTable('persons', (table) => {
table.increments('id').primary();
table.string('name');
table.integer('parentId');
});
});
after(() => {
return knex.schema.dropTableIfExists('persons');
});
beforeEach(() => {
Person = class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: 'persons.id',
to: 'persons.parentId',
},
},
};
}
};
});
beforeEach(async () => {
await Person.query(knex).delete();
await Person.query(knex).insertGraph({
id: 1,
name: 'parent',
children: [
{
id: 2,
name: 'child 1',
},
{
id: 3,
name: 'child 2',
},
],
});
});
describe('should compile a query into a knex query', () => {
it("where('name', 'child 1')", () => {
testSql({
query: Person.query(knex).where('name', 'child 1'),
sql: 'select "persons".* from "persons" where "name" = ?',
bindings: ['child 1'],
});
});
it("select('id', 'name')", () => {
testSql({
query: Person.query(knex).select('id', 'name'),
sql: 'select "id", "name" from "persons"',
bindings: [],
});
});
it("where(raw('?', raw('?', val(1))), Person.relatedQuery('children').select('id').limit(1))", () => {
testSql({
query: Person.query(knex).where(
raw('?', raw('?', val(1))),
Person.relatedQuery('children').select('id').limit(1),
),
sql: 'select "persons".* from "persons" where ? = (select "id" from "persons" as "children" where "children"."parentId" = "persons"."id" limit ?)',
bindings: [1, 1],
});
});
it('should fail with an informational error when withGraphJoined is used before warm up', () => {
expectJs(() => {
Person.query(knex).withGraphJoined('children').toKnexQuery();
}).to.throwException((err) => {
expect(err.message).to.equal(
`table metadata has not been fetched for table 'persons'. Are you trying to call toKnexQuery() for a withGraphJoined query? To make sure the table metadata is fetched see the objection.initialize function.`,
);
});
});
it('should fail with a informational error when withGraphJoined is used before warm up', async () => {
await initialize(knex, [Person]);
testSql({
query: Person.query(knex).withGraphJoined('children'),
sql: 'select "persons"."id" as "id", "persons"."name" as "name", "persons"."parentId" as "parentId", "children"."id" as "children:id", "children"."name" as "children:name", "children"."parentId" as "children:parentId" from "persons" left join "persons" as "children" on "children"."parentId" = "persons"."id"',
bindings: [],
});
});
it('should fail with a informational error when withGraphJoined is used before warm up (2)', async () => {
Person.knex(knex);
await initialize([Person]);
testSql({
query: Person.query(knex).withGraphJoined('children'),
sql: 'select "persons"."id" as "id", "persons"."name" as "name", "persons"."parentId" as "parentId", "children"."id" as "children:id", "children"."name" as "children:name", "children"."parentId" as "children:parentId" from "persons" left join "persons" as "children" on "children"."parentId" = "persons"."id"',
bindings: [],
});
});
});
});
};
function testSql({ query, sql, bindings }) {
const result = query.toKnexQuery().toSQL();
expect(normalizeSql(result.sql)).to.equal(sql);
expect(result.bindings).to.eql(bindings);
}
function normalizeSql(sql) {
return sql.replace(/`/g, '"');
}
================================================
FILE: tests/integration/transactions.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const Promise = require('bluebird');
const transaction = require('../../').transaction;
const knexUtils = require('../../lib/utils/knexUtils');
module.exports = (session) => {
let Model1 = session.models.Model1;
let Model2 = session.models.Model2;
describe('transaction', () => {
beforeEach(() => {
return session.populate([]);
});
before(() => {
// Disable unhandled exception logging. Some of the tests _should_ leak an exception
// but we don't want them to appear in the log.
session.addUnhandledRejectionHandler(_.noop);
});
after(() => {
session.removeUnhandledRejectionHandler(_.noop);
});
it('should resolve an empty transaction', (done) => {
transaction(Model1, Model2, () => {
return { a: 1 };
}).then((result) => {
expect(result).to.eql({ a: 1 });
done();
});
});
it('should fail without models', (done) => {
transaction(() => {
return { a: 1 };
})
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
it('should fail if one of the model classes is not a subclass of Model', (done) => {
transaction(
Model1,
function () {},
() => {
return { a: 1 };
},
)
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
it('should fail if all ModelClasses are not bound to the same knex connection', (done) => {
transaction(Model1, Model2.bindKnex({}), () => {
return { a: 1 };
})
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
it('should commit transaction if no errors occur (1)', (done) => {
transaction(Model1, Model2, (Model1, Model2) => {
return Model1.query()
.insert({ model1Prop1: 'test 1' })
.then(() => {
return Model1.query().insert({ model1Prop1: 'test 2' });
})
.then(() => {
return Model2.query().insert({ model2Prop1: 'test 3' });
});
})
.then((result) => {
expect(result.model2Prop1).to.equal('test 3');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(2);
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['test 1', 'test 2']);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0].model2_prop1).to.equal('test 3');
done();
})
.catch(done);
});
it('should commit transaction if no errors occur (Model.transaction)', async () => {
const result = await Model1.transaction(async (trx) => {
await Model1.query(trx).insert({ model1Prop1: 'test 1' });
await Model1.query(trx).insert({ model1Prop1: 'test 2' });
return Model2.query(trx).insert({ model2Prop1: 'test 3' });
});
expect(result.model2Prop1).to.equal('test 3');
let rows = await session.knex('Model1');
expect(rows).to.have.length(2);
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['test 1', 'test 2']);
rows = await session.knex('model2');
expect(rows).to.have.length(1);
expect(rows[0].model2_prop1).to.equal('test 3');
});
it('should commit transaction if no errors occur (Model.transaction with two args)', async () => {
const result = await Model1.transaction(Model1.knex(), async (trx) => {
await Model1.query(trx).insert({ model1Prop1: 'test 1' });
await Model1.query(trx).insert({ model1Prop1: 'test 2' });
return Model2.query(trx).insert({ model2Prop1: 'test 3' });
});
expect(result.model2Prop1).to.equal('test 3');
let rows = await session.knex('Model1');
expect(rows).to.have.length(2);
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['test 1', 'test 2']);
rows = await session.knex('model2');
expect(rows).to.have.length(1);
expect(rows[0].model2_prop1).to.equal('test 3');
});
it('should commit transaction if no errors occur (2)', (done) => {
transaction(Model1, (Model1) => {
return Model1.query().insertGraph([
{
model1Prop1: 'a',
model1Relation1: {
model1Prop1: 'b',
},
model1Relation2: [
{
model2Prop1: 'c',
model2Relation1: [
{
model1Prop1: 'd',
},
],
},
],
},
]);
})
.then(() => {
return Promise.all([
session.knex('Model1').orderBy('model1Prop1'),
session.knex('model2'),
session.knex('Model1Model2'),
]);
})
.then(([rows1, rows2, rows3]) => {
expect(rows1).to.have.length(3);
expect(_.map(rows1, 'model1Prop1')).to.eql(['a', 'b', 'd']);
expect(rows2).to.have.length(1);
expect(rows2[0].model2_prop1).to.equal('c');
expect(rows3).to.have.length(1);
done();
})
.catch(done);
});
it('should commit transaction if no errors occur (3)', (done) => {
Model1.knex()
.transaction((trx) => {
return Model1.query(trx).insertGraph([
{
model1Prop1: 'a',
model1Relation1: {
model1Prop1: 'b',
},
model1Relation2: [
{
model2Prop1: 'c',
model2Relation1: [
{
model1Prop1: 'd',
},
],
},
],
},
]);
})
.then(() => {
return Promise.all([
session.knex('Model1').orderBy('model1Prop1'),
session.knex('model2'),
session.knex('Model1Model2'),
]);
})
.then(([rows1, rows2, rows3]) => {
expect(rows1).to.have.length(3);
expect(_.map(rows1, 'model1Prop1')).to.eql(['a', 'b', 'd']);
expect(rows2).to.have.length(1);
expect(rows2[0].model2_prop1).to.equal('c');
expect(rows3).to.have.length(1);
done();
})
.catch(done);
});
it('should rollback if an error occurs (1)', (done) => {
transaction(Model1, Model2, (Model1, Model2) => {
return Model1.query()
.insert({ model1Prop1: 'test 1' })
.then(() => {
return Model1.query().insert({ model1Prop1: 'test 2' });
})
.then(() => {
return Model2.query().insert({ model2Prop1: 'test 3' });
})
.then(() => {
throw new Error('whoops');
});
})
.catch((err) => {
expect(err.message).to.equal('whoops');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(0);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(0);
done();
})
.catch(done);
});
it('should rollback if an error occurs (Model.transaction)', async () => {
try {
await Model1.transaction(async (trx) => {
await Model1.query(trx).insert({ model1Prop1: 'test 1' });
await Model1.query(trx).insert({ model1Prop1: 'test 2' });
await Model2.query(trx).insert({ model2Prop1: 'test 3' });
throw new Error('whoops');
});
throw new Error('should not get here');
} catch (err) {
expect(err.message).to.equal('whoops');
let rows = await session.knex('Model1');
expect(rows).to.have.length(0);
rows = await session.knex('model2');
expect(rows).to.have.length(0);
}
});
it('should rollback if an error occurs (2)', (done) => {
transaction(Model1, (Model1) => {
return Model1.query()
.insert({ model1Prop1: 'test 1' })
.then((model) => {
return model.$relatedQuery('model1Relation2').insert({ model2Prop2: 1000 });
})
.then(() => {
throw new Error('whoops');
});
})
.catch((err) => {
expect(err.message).to.equal('whoops');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(0);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(0);
done();
})
.catch(done);
});
it('should rollback if an error occurs (3)', (done) => {
transaction(Model1, (Model1) => {
return Model1.query()
.insertGraph([
{
model1Prop1: 'a',
model1Relation1: {
model1Prop1: 'b',
},
model1Relation2: [
{
model2Prop1: 'c',
model2Relation1: [
{
model1Prop1: 'd',
},
],
},
],
},
])
.then(() => {
throw new Error('whoops');
});
})
.catch((err) => {
expect(err.message).to.equal('whoops');
return Promise.all([
session.knex('Model1'),
session.knex('model2'),
session.knex('Model1Model2'),
]);
})
.then(([rows1, rows2, rows3]) => {
expect(rows1).to.have.length(0);
expect(rows2).to.have.length(0);
expect(rows3).to.have.length(0);
done();
})
.catch(done);
});
it('should rollback if an error occurs (4)', (done) => {
Model1.knex()
.transaction((trx) => {
return Model1.query(trx)
.insertGraph([
{
model1Prop1: 'a',
model1Relation1: {
model1Prop1: 'b',
},
model1Relation2: [
{
model2Prop1: 'c',
model2Relation1: [
{
model1Prop1: 'd',
},
],
},
],
},
])
.then((models) => {
return models[0]
.$relatedQuery('model1Relation2', trx)
.insert({ model2Prop1: 'e' })
.then(() => models);
})
.then((models) => {
return models[0]
.$relatedQuery('model1Relation2')
.transacting(trx)
.insert({ model2Prop1: 'f' })
.then(() => models);
})
.then((models) => {
return Model1.query(trx)
.findById(models[0].id)
.then((it) => it.$fetchGraph('model1Relation1', { transaction: trx }))
.then((it) => expect(it.model1Relation1.model1Prop1).to.equal('b'))
.then(() => models);
})
.then((models) => {
expect(models[0].$query(trx).knex() === trx);
})
.then(() => {
throw new Error('whoops');
});
})
.catch((err) => {
console.log(err);
expect(err.message).to.equal('whoops');
return Promise.all([
session.knex('Model1'),
session.knex('model2'),
session.knex('Model1Model2'),
]);
})
.then(([rows1, rows2, rows3]) => {
expect(rows1).to.have.length(0);
expect(rows2).to.have.length(0);
expect(rows3).to.have.length(0);
done();
})
.catch(done);
});
it('should rollback if an error occurs (5)', (done) => {
transaction(Model1.knex(), (trx) => {
return Model1.query(trx)
.insertGraph([
{
model1Prop1: 'a',
model1Relation1: {
model1Prop1: 'b',
},
model1Relation2: [
{
model2Prop1: 'c',
model2Relation1: [
{
model1Prop1: 'd',
},
],
},
],
},
])
.then((models) => {
return models[0]
.$relatedQuery('model1Relation2', trx)
.insert({ model2Prop1: 'e' })
.then(() => models);
})
.then((models) => {
return models[0]
.$relatedQuery('model1Relation2')
.transacting(trx)
.insert({ model2Prop1: 'f' })
.then(() => models);
})
.then((models) => {
return Model1.query(trx)
.findById(models[0].id)
.then((it) => it.$fetchGraph('model1Relation1', { transaction: trx }))
.then((it) => expect(it.model1Relation1.model1Prop1).to.equal('b'))
.then(() => models);
})
.then((models) => {
expect(models[0].$query(trx).knex() === trx);
})
.then(() => {
throw new Error('whoops');
});
})
.catch((err) => {
expect(err.message).to.equal('whoops');
return Promise.all([
session.knex('Model1'),
session.knex('model2'),
session.knex('Model1Model2'),
]);
})
.then(([rows1, rows2, rows3]) => {
expect(rows1).to.have.length(0);
expect(rows2).to.have.length(0);
expect(rows3).to.have.length(0);
done();
})
.catch(done);
});
it('should rollback if the rollback method is called (no return)', (done) => {
transaction(Model1.knex(), (trx) => {
Model1.query(trx)
.insertGraph([
{
model1Prop1: 'a',
model1Relation1: {
model1Prop1: 'b',
},
model1Relation2: [
{
model2Prop1: 'c',
model2Relation1: [
{
model1Prop1: 'd',
},
],
},
],
},
])
.then((models) => {
return models[0]
.$relatedQuery('model1Relation2', trx)
.insert({ model2Prop1: 'e' })
.then(() => models);
})
.then((models) => {
return models[0]
.$relatedQuery('model1Relation2')
.transacting(trx)
.insert({ model2Prop1: 'f' })
.then(() => models);
})
.then((models) => {
expect(models[0].$query(trx).knex() === trx);
})
.then(() => {
trx.rollback(new Error('whoops'));
});
})
.catch((err) => {
expect(err.message).to.equal('whoops');
return Promise.all([
session.knex('Model1'),
session.knex('model2'),
session.knex('Model1Model2'),
]);
})
.then(([rows1, rows2, rows3]) => {
expect(rows1).to.have.length(0);
expect(rows2).to.have.length(0);
expect(rows3).to.have.length(0);
done();
})
.catch(done);
});
it('should rollback if the rollback method is called (with return)', (done) => {
transaction(Model1.knex(), (trx) => {
return Model1.query(trx)
.insertGraph([
{
model1Prop1: 'a',
model1Relation1: {
model1Prop1: 'b',
},
model1Relation2: [
{
model2Prop1: 'c',
model2Relation1: [
{
model1Prop1: 'd',
},
],
},
],
},
])
.then(() => {
return trx.rollback(new Error('whoops'));
});
})
.catch((err) => {
expect(err.message).to.equal('whoops');
return Promise.all([
session.knex('Model1'),
session.knex('model2'),
session.knex('Model1Model2'),
]);
})
.then(([rows1, rows2, rows3]) => {
expect(rows1).to.have.length(0);
expect(rows2).to.have.length(0);
expect(rows3).to.have.length(0);
done();
})
.catch(done);
});
it('should skip queries after rollback', (done) => {
transaction(Model1, (Model1) => {
return Model1.query()
.insert({ model1Prop1: '123' })
.then(() => {
return Promise.all(
_.map(_.range(2), (i) => {
if (i === 1) {
throw new Error();
}
return Model1.query().insert({ model1Prop1: i.toString() }).then();
}),
);
});
})
.catch(() => {
return Promise.delay(5).then(() => {
return session.knex('Model1');
});
})
.then((rows) => {
expect(rows).to.have.length(0);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(0);
done();
})
.catch(done);
});
it('bound model class should accept unbound model instances', (done) => {
let unboundModel = Model1.fromJson({ model1Prop1: '123' });
transaction(Model1, (Model1) => {
return Model1.query().insert(unboundModel);
})
.then((inserted) => {
expect(inserted.model1Prop1).to.equal('123');
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0].model1Prop1).to.equal('123');
done();
})
.catch(done);
});
it('last argument should be the knex transaction object', (done) => {
transaction(Model1, Model2, (Model1, Model2, trx) => {
expect(trx).to.equal(Model1.knex());
})
.then(() => {
done();
})
.catch(done);
});
it('if knex instance is passed, should be equivalent to knex.transaction()', (done) => {
transaction(Model1.knex(), (trx) => {
return trx('Model1').insert({ model1Prop1: '1' });
})
.then(() => {
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0].model1Prop1).to.equal('1');
done();
})
.catch(done);
});
describe('transaction.start() / Model.startTransaction()', () => {
it('should commit transaction when the commit method is called', (done) => {
let trx;
transaction
.start(Model1)
.then((trans) => {
trx = trans;
return Model1.bindKnex(trx).query().insert({ model1Prop1: 'test 1' });
})
.then(() => {
return Model1.bindKnex(trx).query().insert({ model1Prop1: 'test 2' });
})
.then(() => {
return Model2.bindKnex(trx).query().insert({ model2Prop1: 'test 3' });
})
.then(() => {
return trx.commit();
})
.then(() => {
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(2);
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['test 1', 'test 2']);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0].model2_prop1).to.equal('test 3');
done();
})
.catch(done);
});
it('should commit transaction when the commit method is called (Model.startTransaction())', (done) => {
let trx;
Model1.startTransaction()
.then((trans) => {
trx = trans;
return Model1.bindKnex(trx).query().insert({ model1Prop1: 'test 1' });
})
.then(() => {
return Model1.bindKnex(trx).query().insert({ model1Prop1: 'test 2' });
})
.then(() => {
return Model2.bindKnex(trx).query().insert({ model2Prop1: 'test 3' });
})
.then(() => {
return trx.commit();
})
.then(() => {
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(2);
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['test 1', 'test 2']);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0].model2_prop1).to.equal('test 3');
done();
})
.catch(done);
});
it(
'commit should work with yield (and thus async/await)',
Promise.coroutine(function* () {
const trx = yield transaction.start(Model1.knex());
yield Model1.query(trx).insert({ model1Prop1: 'test 1' });
yield Model1.query(trx).insert({ model1Prop1: 'test 2' });
yield Model2.query(trx).insert({ model2Prop1: 'test 3' });
yield trx.commit();
const model1Rows = yield session.knex('Model1');
const model2Rows = yield session.knex('model2');
expect(model1Rows).to.have.length(2);
expect(_.map(model1Rows, 'model1Prop1').sort()).to.eql(['test 1', 'test 2']);
expect(model2Rows).to.have.length(1);
expect(model2Rows[0].model2_prop1).to.equal('test 3');
}),
);
it(
'commit should work with yield (and thus async/await) (Model.startTransaction())',
Promise.coroutine(function* () {
const trx = yield Model1.startTransaction();
yield Model1.query(trx).insert({ model1Prop1: 'test 1' });
yield Model1.query(trx).insert({ model1Prop1: 'test 2' });
yield Model2.query(trx).insert({ model2Prop1: 'test 3' });
yield trx.commit();
const model1Rows = yield session.knex('Model1');
const model2Rows = yield session.knex('model2');
expect(model1Rows).to.have.length(2);
expect(_.map(model1Rows, 'model1Prop1').sort()).to.eql(['test 1', 'test 2']);
expect(model2Rows).to.have.length(1);
expect(model2Rows[0].model2_prop1).to.equal('test 3');
}),
);
it(
'rollback should work with yield (and thus async/await)',
Promise.coroutine(function* () {
const trx = yield transaction.start(Model1.knex());
yield Model1.query(trx).insert({ model1Prop1: 'test 1' });
yield Model1.query(trx).insert({ model1Prop1: 'test 2' });
yield Model2.query(trx).insert({ model2Prop1: 'test 3' });
yield trx.rollback();
const model1Rows = yield session.knex('Model1');
const model2Rows = yield session.knex('model2');
expect(model1Rows).to.have.length(0);
expect(model2Rows).to.have.length(0);
}),
);
it('should work when a knex connection is passed instead of a model', (done) => {
let trx;
transaction
.start(Model1.knex())
.then((trans) => {
trx = trans;
return Model1.bindTransaction(trx).query().insert({ model1Prop1: 'test 1' });
})
.then(() => {
return Model1.bindTransaction(trx).query().insert({ model1Prop1: 'test 2' });
})
.then(() => {
return Model2.bindTransaction(trx).query().insert({ model2Prop1: 'test 3' });
})
.then(() => {
return trx.commit();
})
.then(() => {
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(2);
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['test 1', 'test 2']);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(rows[0].model2_prop1).to.equal('test 3');
done();
})
.catch(done);
});
it('should rollback transaction when the rollback method is called', (done) => {
let trx;
transaction
.start(Model1)
.then((trans) => {
trx = trans;
return Model1.bindTransaction(trx).query().insert({ model1Prop1: 'test 1' });
})
.then(() => {
return Model1.bindTransaction(trx).query().insert({ model1Prop1: 'test 2' });
})
.then(() => {
return Model2.bindTransaction(trx).query().insert({ model2Prop1: 'test 3' });
})
.then(() => {
return trx.rollback();
})
.then(() => {
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(0);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(0);
done();
})
.catch(done);
});
it('should fail if neither a model or a knex connection is passed', (done) => {
transaction
.start({})
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
});
describe('model.$knex()', () => {
it("model.$knex() methods should return the model's transaction", (done) => {
transaction
.start(Model1)
.then((trx) => {
return Model1.bindTransaction(trx).query().insert({ model1Prop1: 'test 1' });
})
.then((model) => {
return Model1.bindTransaction(model.$knex()).query().insert({ model1Prop1: 'test 2' });
})
.then((model) => {
return Model2.bindTransaction(model.$knex()).query().insert({ model2Prop1: 'test 3' });
})
.then((model) => {
return model.$knex().rollback();
})
.then(() => {
return session.knex('Model1');
})
.then((rows) => {
expect(rows).to.have.length(0);
return session.knex('model2');
})
.then((rows) => {
expect(rows).to.have.length(0);
done();
})
.catch(done);
});
});
});
};
================================================
FILE: tests/integration/unrelate.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const chai = require('chai');
module.exports = (session) => {
let Model1 = session.models.Model1;
let Model2 = session.models.Model2;
describe('Model unrelate queries', () => {
describe('.$query()', () => {
it('should reject the query', (done) => {
Model1.fromJson({ id: 1 })
.$query()
.unrelate()
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
});
describe('.$relatedQuery().unrelate()', () => {
describe('belongs to one relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'foo',
},
],
},
]);
});
it('should unrelate', () => {
return Model1.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation1').unrelate();
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(rows[0].model1Id).to.equal(null);
expect(rows[1].model1Id).to.equal(null);
expect(rows[2].model1Id).to.equal(4);
expect(rows[3].model1Id).to.equal(null);
});
});
it('should fail if arguments are given', (done) => {
Model1.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation1').unrelate(1);
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`Don't pass arguments to unrelate(). You should use it like this: unrelate().where('foo', 'bar').andWhere(...)`,
);
done();
})
.catch(done);
});
});
describe('has many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
],
},
]);
});
it('should unrelate', () => {
return Model1.query()
.where('id', 1)
.first()
.then((model) => {
return model.$relatedQuery('model1Relation2').unrelate().where('id_col', 2);
})
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(rows[0].model1_id).to.equal(1);
expect(rows[1].model1_id).to.equal(null);
expect(rows[2].model1_id).to.equal(1);
expect(rows[3].model1_id).to.equal(2);
});
});
it('should unrelate multiple', () => {
return Model1.query()
.where('id', 1)
.first()
.then((model) => {
return model.$relatedQuery('model1Relation2').unrelate().where('id_col', '>', 1);
})
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
expect(rows[0].model1_id).to.equal(1);
expect(rows[1].model1_id).to.equal(null);
expect(rows[2].model1_id).to.equal(null);
expect(rows[3].model1_id).to.equal(2);
});
});
it('should fail if arguments are given', (done) => {
Model1.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('model1Relation2').unrelate([1, 2]);
})
.then((numUpdated) => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`Don't pass arguments to unrelate(). You should use it like this: unrelate().where('foo', 'bar').andWhere(...)`,
);
done();
})
.catch(done);
});
});
describe('many to many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 5,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
},
],
},
],
},
]);
});
it('should unrelate', () => {
return Model2.query()
.where('id_col', 1)
.first()
.then((model) => {
return model.$relatedQuery('model2Relation1').unrelate().where('Model1.id', 4);
})
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expect(_.filter(rows, { model2Id: 1, model1Id: 3 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 4 })).to.have.length(0);
expect(_.filter(rows, { model2Id: 1, model1Id: 5 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 2, model1Id: 6 })).to.have.length(1);
});
});
it('should unrelate multiple', () => {
return Model2.query()
.findById(1)
.then((model) => {
return model
.$relatedQuery('model2Relation1')
.unrelate()
.where('model1Prop1', '>', 'blaa 1');
})
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expect(_.filter(rows, { model2Id: 1, model1Id: 3 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 4 })).to.have.length(0);
expect(_.filter(rows, { model2Id: 1, model1Id: 5 })).to.have.length(0);
expect(_.filter(rows, { model2Id: 2, model1Id: 6 })).to.have.length(1);
});
});
it('should fail if arguments are given', (done) => {
Model2.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('model2Relation1').unrelate([1, 2]);
})
.then((numUpdated) => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`Don't pass arguments to unrelate(). You should use it like this: unrelate().where('foo', 'bar').andWhere(...)`,
);
done();
})
.catch(done);
});
});
describe('has one through relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation2: {
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation2: {
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 5,
},
model2Relation1: [
{
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 4,
},
],
},
],
},
]);
});
it('should unrelate', () => {
return Model2.query()
.where('id_col', 2)
.first()
.then((model) => {
return model.$relatedQuery('model2Relation2').unrelate();
})
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1Model2One');
})
.then((rows) => {
expect(rows).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 5 })).to.have.length(1);
});
});
});
});
describe('.relatedQuery().unrelate()', () => {
describe('belongs to one relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'foo',
},
],
},
]);
});
it('should unrelate using one id', () => {
return Model1.relatedQuery('model1Relation1')
.for(1)
.unrelate()
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id: 1, model1Id: null },
{ id: 2, model1Id: null },
{ id: 3, model1Id: 4 },
{ id: 4, model1Id: null },
]);
});
});
it('should unrelate using multiple ids', () => {
return Model1.relatedQuery('model1Relation1')
.for([1, 3])
.unrelate()
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id: 1, model1Id: null },
{ id: 2, model1Id: null },
{ id: 3, model1Id: null },
{ id: 4, model1Id: null },
]);
});
});
it('should unrelate using subquery', () => {
return Model1.relatedQuery('model1Relation1')
.for(Model1.query().findByIds([1, 3]))
.unrelate()
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id: 1, model1Id: null },
{ id: 2, model1Id: null },
{ id: 3, model1Id: null },
{ id: 4, model1Id: null },
]);
});
});
// Filters don't work on mysql when the related table and the
// owner table are the same.
if (!session.isMySql()) {
it('should unrelate using multiple ids and a filter', () => {
return Model1.relatedQuery('model1Relation1')
.for([1, 3])
.unrelate()
.where('model1Prop1', '!=', 'hello 2')
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model1.getTableName()).orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id: 1, model1Id: 2 },
{ id: 2, model1Id: null },
{ id: 3, model1Id: null },
{ id: 4, model1Id: null },
]);
});
});
}
it('should fail if arguments are given', (done) => {
Model1.relatedQuery('model1Relation1')
.for(1)
.unrelate(1)
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`Don't pass arguments to unrelate(). You should use it like this: unrelate().where('foo', 'bar').andWhere(...)`,
);
done();
})
.catch(done);
});
});
describe('has many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
],
},
]);
});
it('should unrelate for one parent', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.unrelate()
.whereIn('id_col', [2, 4])
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: null },
{ id_col: 3, model1_id: 1 },
{ id_col: 4, model1_id: 2 },
]);
});
});
it('should unrelate for one parent using a subquery', () => {
return Model1.relatedQuery('model1Relation2')
.for(Model1.query().findById(1))
.unrelate()
.whereIn('id_col', [2, 4])
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: null },
{ id_col: 3, model1_id: 1 },
{ id_col: 4, model1_id: 2 },
]);
});
});
it('should unrelate for two parents', () => {
return Model1.relatedQuery('model1Relation2')
.for([1, 2])
.unrelate()
.whereIn('id_col', [2, 4])
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: null },
{ id_col: 3, model1_id: 1 },
{ id_col: 4, model1_id: null },
]);
});
});
it('should unrelate multiple', () => {
return Model1.relatedQuery('model1Relation2')
.for(1)
.unrelate()
.where('id_col', '>', 1)
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex(Model2.getTableName()).orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(4);
chai.expect(rows).containSubset([
{ id_col: 1, model1_id: 1 },
{ id_col: 2, model1_id: null },
{ id_col: 3, model1_id: null },
{ id_col: 4, model1_id: 2 },
]);
});
});
it('should fail if arguments are given', (done) => {
Model1.relatedQuery('model1Relation2')
.for(1)
.unrelate([1, 2])
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`Don't pass arguments to unrelate(). You should use it like this: unrelate().where('foo', 'bar').andWhere(...)`,
);
done();
})
.catch(done);
});
});
describe('many to many relation', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 5,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
},
],
},
],
},
]);
});
it('should unrelate using one parent id', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.unrelate()
.whereIn('Model1.id', [4, 6])
.then((numDeleted) => {
expect(numDeleted).to.equal(1);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
chai.expect(rows).containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5 },
{ model2Id: 2, model1Id: 6 },
]);
});
});
it('should unrelate using two parent ids', () => {
return Model2.relatedQuery('model2Relation1')
.for([1, 2])
.unrelate()
.whereIn('Model1.id', [4, 6])
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
chai.expect(rows).containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5 },
]);
});
});
it('should unrelate using two parents and a subquery', () => {
return Model2.relatedQuery('model2Relation1')
.for(Model2.query().findByIds([1, 2]))
.unrelate()
.whereIn('Model1.id', [4, 6])
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
chai.expect(rows).containSubset([
{ model2Id: 1, model1Id: 3 },
{ model2Id: 1, model1Id: 5 },
]);
});
});
it('should unrelate multiple', () => {
return Model2.relatedQuery('model2Relation1')
.for(1)
.unrelate()
.where('model1Prop1', '>', 'blaa 1')
.then((numDeleted) => {
expect(numDeleted).to.equal(2);
return session.knex('Model1Model2').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expect(_.filter(rows, { model2Id: 1, model1Id: 3 })).to.have.length(1);
expect(_.filter(rows, { model2Id: 1, model1Id: 4 })).to.have.length(0);
expect(_.filter(rows, { model2Id: 1, model1Id: 5 })).to.have.length(0);
expect(_.filter(rows, { model2Id: 2, model1Id: 6 })).to.have.length(1);
});
});
it('should fail if arguments are given', (done) => {
Model2.query()
.findById(1)
.then((model) => {
return model.$relatedQuery('model2Relation1').unrelate([1, 2]);
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
`Don't pass arguments to unrelate(). You should use it like this: unrelate().where('foo', 'bar').andWhere(...)`,
);
done();
})
.catch(done);
});
});
});
});
};
================================================
FILE: tests/integration/update.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const Promise = require('bluebird');
const { inheritModel } = require('../../lib/model/inheritModel');
const { expectPartialEqual: expectPartEql } = require('./../../testUtils/testUtils');
const { ValidationError, raw } = require('../../');
module.exports = (session) => {
let Model1 = session.models.Model1;
let Model2 = session.models.Model2;
describe('Model update queries', () => {
describe('.query().update()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 2,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 1,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
{
id: 3,
model1Prop1: 'hello 3',
},
]);
});
it('should update a model (1)', () => {
let model = Model1.fromJson({ model1Prop1: 'updated text' });
return Model1.query()
.update(model)
.where('id', '=', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$afterUpdateCalled).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should be able to update to null value', () => {
return Model1.query()
.update({ model1Prop1: null, model1Prop2: 100 })
.where('id', '=', 1)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: null });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should be able to update to an empty string', () => {
return Model1.query()
.update({ model1Prop1: '', model1Prop2: 100 })
.where('id', '=', 1)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: '' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should accept json', () => {
return Model1.query()
.update({ model1Prop1: 'updated text' })
.where('id', '=', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should update a model (2)', () => {
let model = Model2.fromJson({ model2Prop1: 'updated text' });
return Model2.query()
.update(model)
.where('id_col', '=', 1)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'updated text', model2_prop2: 2 });
expectPartEql(rows[1], { id_col: 2, model2_prop1: 'text 2', model2_prop2: 1 });
});
});
it('should update multiple', () => {
return Model1.query()
.update({ model1Prop1: 'updated text' })
.where('model1Prop1', '<', 'hello 3')
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
it('should validate', (done) => {
let ModelWithSchema = subClassWithSchema(Model1, {
type: 'object',
properties: {
id: { type: ['number', 'null'] },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
});
ModelWithSchema.query()
.update({ model1Prop1: 666 })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(ValidationError);
expect(err.type).to.equal('ModelValidation');
expect(err.data).to.eql({
model1Prop1: [
{
message: 'must be string',
keyword: 'type',
params: {
type: 'string',
},
},
],
});
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
done();
})
.catch(done);
});
it('should validate required properties', (done) => {
let ModelWithSchema = subClassWithSchema(Model1, {
type: 'object',
required: ['model1Prop2'],
properties: {
id: { type: ['number', 'null'] },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
});
ModelWithSchema.query()
.update({ model1Prop1: 'text' })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(ValidationError);
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
done();
})
.catch(done);
});
it.skip('should pass validation if query properties are passed in for required', () => {
const ModelWithSchema = subClassWithSchema(Model1, {
type: 'object',
required: ['model1Prop1'],
properties: {
id: { type: ['number', 'null'] },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
});
return ModelWithSchema.query()
.update({ model1Prop1: raw(`'text'`) })
.where('model1Prop1', 'hello 2')
.then(() => {
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
});
});
it('should use `Model.createValidationError` to create the error', (done) => {
class MyError extends Error {
constructor({ data }) {
super('MyError');
this.errors = data;
}
}
let ModelWithSchema = subClassWithSchema(Model1, {
type: 'object',
properties: {
id: { type: ['number', 'null'] },
model1Prop1: { type: 'string' },
model1Prop2: { type: 'number' },
},
});
ModelWithSchema.createValidationError = (props) => {
return new MyError(props);
};
ModelWithSchema.query()
.update({ model1Prop1: 666 })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(MyError);
expect(err.errors).to.eql({
model1Prop1: [
{
message: 'must be string',
keyword: 'type',
params: {
type: 'string',
},
},
],
});
return session.knex(Model1.getTableName());
})
.then((rows) => {
expect(_.map(rows, 'model1Prop1').sort()).to.eql(['hello 1', 'hello 2', 'hello 3']);
done();
})
.catch(done);
});
});
describe('.query().updateAndFetchById()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 2,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 1,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
},
{
id: 3,
model1Prop1: 'hello 3',
},
]);
});
it('should update and fetch a model', () => {
let model = Model1.fromJson({ model1Prop1: 'updated text' });
return Model1.query()
.updateAndFetchById(2, model)
.then((fetchedModel) => {
expect(fetchedModel).to.equal(model);
expect(fetchedModel).eql({
id: 2,
model1Prop1: 'updated text',
model1Prop2: null,
model1Id: null,
$beforeUpdateCalled: 1,
$beforeUpdateOptions: {},
$afterUpdateCalled: 1,
$afterUpdateOptions: {},
});
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(3);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
});
});
});
describe('.$query().update()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should update a model (1)', () => {
const model = Model1.fromJson({ id: 1 });
return model
.$query()
.update({ model1Prop1: 'updated text' })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.model1Prop1).to.eql('updated text');
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
it('should update a model (2)', () => {
const model = Model1.fromJson({ id: 1, model1Prop1: 'updated text' });
return model
.$query()
.update()
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$afterUpdateCalled).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
it('should pass the old values to $beforeUpdate and $afterUpdate hooks in options.old', () => {
let model = Model1.fromJson({ id: 1, model1Prop1: 'updated text' });
return Model1.fromJson({ id: 1 })
.$query()
.update(model)
.then(() => {
expect(model.$beforeUpdateCalled).to.equal(1);
expect(model.$beforeUpdateOptions).to.eql({ old: { id: 1 } });
expect(model.$afterUpdateCalled).to.equal(1);
expect(model.$afterUpdateOptions).to.eql({ old: { id: 1 } });
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
it('should pass the old values to $beforeValidate and $afterValidate hooks in options.old', () => {
let TestModel = inheritModel(Model1);
TestModel.pickJsonSchemaProperties = false;
TestModel.jsonSchema = {
type: 'object',
properties: {
id: { type: 'integer' },
},
};
let before;
let after;
let model = TestModel.fromJson({ id: 1, model1Prop1: 'text' });
TestModel.prototype.$beforeValidate = (schema, json, options) => {
before = options.old.toJSON();
return schema;
};
TestModel.prototype.$afterValidate = function (json, options) {
after = options.old.toJSON();
};
return model
.$query()
.update({ model1Prop1: 'updated text' })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
expect(before.id).to.equal(1);
expect(after.id).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
it('model edits in $beforeUpdate should get into database query', () => {
let model = Model1.fromJson({ id: 1 });
model.$beforeUpdate = function () {
let self = this;
return Promise.delay(1).then(() => {
self.model1Prop1 = 'updated text';
});
};
return model
.$query()
.update()
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'updated text' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
});
});
});
describe('.$query().updateAndFetch()', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
},
{
id: 2,
model1Prop1: 'hello 2',
},
]);
});
it('should update and fetch a model', () => {
let model = Model1.fromJson({ id: 1 });
return model
.$query()
.updateAndFetch({ model1Prop2: 10, undefinedShouldBeIgnored: undefined })
.then((updated) => {
expect(updated.id).to.equal(1);
expect(updated.model1Id).to.equal(null);
expect(updated.model1Prop1).to.equal('hello 1');
expect(updated.model1Prop2).to.equal(10);
expectPartEql(model, {
id: 1,
model1Prop1: 'hello 1',
model1Prop2: 10,
model1Id: null,
});
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(2);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1', model1Prop2: 10 });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2', model1Prop2: null });
});
});
});
describe('.$relatedQuery().update()', () => {
describe('belongs to one relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
},
},
{
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
},
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 3 });
});
});
it('should update a related object (1)', () => {
return parent1
.$relatedQuery('model1Relation1')
.update({ model1Prop1: 'updated text' })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'updated text' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'hello 4' });
});
});
it('should update a related object (2)', () => {
return parent2
.$relatedQuery('model1Relation1')
.update({ model1Prop1: 'updated text', model1Prop2: 1000 })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'hello 3' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'updated text', model1Prop2: 1000 });
});
});
});
describe('has many relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Prop2: 6,
},
{
idCol: 2,
model2Prop1: 'text 2',
model2Prop2: 5,
},
{
idCol: 3,
model2Prop1: 'text 3',
model2Prop2: 4,
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'text 4',
model2Prop2: 3,
},
{
idCol: 5,
model2Prop1: 'text 5',
model2Prop2: 2,
},
{
idCol: 6,
model2Prop1: 'text 6',
model2Prop2: 1,
},
],
},
]);
});
beforeEach(() => {
return Model1.query().then((parents) => {
parent1 = _.find(parents, { id: 1 });
parent2 = _.find(parents, { id: 2 });
});
});
it('should update a related object', () => {
return parent1
.$relatedQuery('model1Relation2')
.update({ model2Prop1: 'updated text' })
.where('id_col', 2)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], {
id_col: 2,
model2_prop1: 'updated text',
model2_prop2: 5,
});
expectPartEql(rows[2], { id_col: 3, model2_prop1: 'text 3' });
expectPartEql(rows[3], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[4], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[5], { id_col: 6, model2_prop1: 'text 6' });
});
});
it('should update multiple related objects', () => {
return parent1
.$relatedQuery('model1Relation2')
.update({ model2Prop1: 'updated text' })
.where('model2_prop2', '<', 6)
.where('model2_prop1', 'like', 'text %')
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('model2').orderBy('id_col');
})
.then((rows) => {
expect(rows).to.have.length(6);
expectPartEql(rows[0], { id_col: 1, model2_prop1: 'text 1' });
expectPartEql(rows[1], {
id_col: 2,
model2_prop1: 'updated text',
model2_prop2: 5,
});
expectPartEql(rows[2], {
id_col: 3,
model2_prop1: 'updated text',
model2_prop2: 4,
});
expectPartEql(rows[3], { id_col: 4, model2_prop1: 'text 4' });
expectPartEql(rows[4], { id_col: 5, model2_prop1: 'text 5' });
expectPartEql(rows[5], { id_col: 6, model2_prop1: 'text 6' });
});
});
});
describe('many to many relation', () => {
let parent1;
let parent2;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation1: [
{
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
{
id: 4,
model1Prop1: 'blaa 2',
model1Prop2: 5,
},
{
id: 5,
model1Prop1: 'blaa 3',
model1Prop2: 4,
},
],
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation1: [
{
id: 6,
model1Prop1: 'blaa 4',
model1Prop2: 3,
},
{
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 2,
},
{
id: 8,
model1Prop1: 'blaa 6',
model1Prop2: 1,
},
],
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent1 = _.find(parents, { idCol: 1 });
parent2 = _.find(parents, { idCol: 2 });
});
});
it('should update a related object', () => {
return parent1
.$relatedQuery('model2Relation1')
.update({ model1Prop1: 'updated text' })
.where('Model1.id', 5)
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(8);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'updated text' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'blaa 6' });
});
});
it('should update multiple objects (1)', () => {
return parent2
.$relatedQuery('model2Relation1')
.update({ model1Prop1: 'updated text', model1Prop2: 123 })
.where('model1Prop1', 'like', 'blaa 4')
.orWhere('model1Prop1', 'like', 'blaa 6')
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(8);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'blaa 2' });
expectPartEql(rows[4], { id: 5, model1Prop1: 'blaa 3' });
expectPartEql(rows[5], { id: 6, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'updated text', model1Prop2: 123 });
});
});
it('should update multiple objects (2)', () => {
return parent1
.$relatedQuery('model2Relation1')
.update({ model1Prop1: 'updated text', model1Prop2: 123 })
.where('model1Prop2', '<', 6)
.then((numUpdated) => {
expect(numUpdated).to.equal(2);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(8);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'blaa 1' });
expectPartEql(rows[3], { id: 4, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[4], { id: 5, model1Prop1: 'updated text', model1Prop2: 123 });
expectPartEql(rows[5], { id: 6, model1Prop1: 'blaa 4' });
expectPartEql(rows[6], { id: 7, model1Prop1: 'blaa 5' });
expectPartEql(rows[7], { id: 8, model1Prop1: 'blaa 6' });
});
});
});
describe('has one through relation', () => {
let parent;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation2: [
{
idCol: 1,
model2Prop1: 'text 1',
model2Relation2: {
id: 3,
model1Prop1: 'blaa 1',
model1Prop2: 6,
},
},
],
},
{
id: 2,
model1Prop1: 'hello 2',
model1Relation2: [
{
idCol: 2,
model2Prop1: 'text 2',
model2Relation2: {
id: 7,
model1Prop1: 'blaa 5',
model1Prop2: 2,
},
},
],
},
]);
});
beforeEach(() => {
return Model2.query().then((parents) => {
parent = _.find(parents, { idCol: 1 });
});
});
it('should update the related object', () => {
return parent
.$relatedQuery('model2Relation2')
.update({ model1Prop1: 'updated text' })
.then((numUpdated) => {
expect(numUpdated).to.equal(1);
return session.knex('Model1').orderBy('Model1.id');
})
.then((rows) => {
expect(rows).to.have.length(4);
expectPartEql(rows[0], { id: 1, model1Prop1: 'hello 1' });
expectPartEql(rows[1], { id: 2, model1Prop1: 'hello 2' });
expectPartEql(rows[2], { id: 3, model1Prop1: 'updated text' });
expectPartEql(rows[3], { id: 7, model1Prop1: 'blaa 5' });
});
});
});
});
function subClassWithSchema(Model, schema) {
let SubModel = inheritModel(Model);
SubModel.jsonSchema = schema;
return SubModel;
}
});
};
================================================
FILE: tests/integration/upsertGraph.js
================================================
const expect = require('expect.js');
const chai = require('chai');
const Promise = require('bluebird');
const { raw, transaction, ValidationError } = require('../../');
const { createRejectionReflection } = require('../../testUtils/testUtils');
const { FetchStrategy } = require('../../lib/queryBuilder/graph/GraphOptions');
const mockKnexFactory = require('../../testUtils/mockKnex');
module.exports = (session) => {
const Model1 = session.unboundModels.Model1;
const Model2 = session.unboundModels.Model2;
const NONEXISTENT_ID = 1000;
for (const fetchStrategy of Object.keys(FetchStrategy)) {
describe(`upsertGraph (fetchStrategy: ${fetchStrategy})`, () => {
let population;
beforeEach(() => {
population = [
{
id: 1,
model1Id: null,
model1Prop1: 'root 1',
model1Relation1: null,
model1Relation2: [],
},
{
id: 2,
model1Id: 3,
model1Prop1: 'root 2',
// This is a BelongsToOneRelation
model1Relation1: {
id: 3,
model1Id: null,
model1Prop1: 'belongsToOne',
},
// This is a HasManyRelation
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'hasMany 1',
// This is a ManyToManyRelation
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'manyToMany 1',
},
{
id: 5,
model1Id: null,
model1Prop1: 'manyToMany 2',
},
],
},
{
idCol: 2,
model1Id: 2,
model2Prop1: 'hasMany 2',
// This is a ManyToManyRelation
model2Relation1: [
{
id: 6,
model1Id: null,
model1Prop1: 'manyToMany 3',
},
{
id: 7,
model1Id: null,
model1Prop1: 'manyToMany 4',
},
],
},
],
},
];
return session.populate(population);
});
it('should do nothing if an empty array is given', () => {
return Promise.all([
Model1.query(session.knex).upsertGraph([]),
Model1.query(session.knex).upsertGraphAndFetch([]),
]);
});
for (const passthroughMethodCall of [
null,
'forUpdate',
'forShare',
'forNoKeyUpdate',
'forKeyShare',
]) {
const passthroughMethodCallSql = {
null: '',
forUpdate: ' for update',
forShare: ' for share',
forNoKeyUpdate: ' for no key update',
forKeyShare: ' for key share',
};
if (
!session.isPostgres() &&
['forNoKeyUpdate', 'forKeyShare'].includes(passthroughMethodCall)
) {
continue;
}
it(
'by default, should insert new, update existing and delete missing' +
(passthroughMethodCall ? ` (${passthroughMethodCall})` : ''),
() => {
const upsert = {
// Nothing is done for the root since it only has an ids.
id: 2,
model1Id: 3,
// update
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
},
// update idCol=1
// delete idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// delete id=5
// and insert one new
model2Relation1: [
{
// This is a string instead of a number on purpose to test
// that no id update is generated even if they only match
// non-strictly.
id: '4',
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
return transaction(session.knex, (trx) => {
const sql = [];
// Wrap the transaction to catch the executed sql.
trx = mockKnexFactory(trx, function (mock, oldImpl, args) {
sql.push(this.toString());
return oldImpl.apply(this, args);
});
return (
Model1.query(trx)
.upsertGraph(upsert, { fetchStrategy })
.modify((builder) => {
if (passthroughMethodCall) {
builder[passthroughMethodCall]();
}
})
// Sort all result by id to make the SQL we test below consistent.
.context({
onBuild(builder) {
if (!builder.isFind()) {
return;
}
if (builder.modelClass().getTableName() === 'Model1') {
builder.orderBy('Model1.id');
} else if (builder.modelClass().getTableName() === 'model2') {
builder.orderBy('model2.id_col');
}
},
})
.then((result) => {
expect(sql.length).to.equal(12);
if (session.isPostgres()) {
if (fetchStrategy === FetchStrategy.OnlyIdentifiers) {
chai
.expect(sql)
.to.containSubset([
'select "Model1"."id", "Model1"."model1Id" from "Model1" where "Model1"."id" in (2) order by "Model1"."id" asc' +
passthroughMethodCallSql[passthroughMethodCall],
'select "Model1"."id" from "Model1" where "Model1"."id" in (3) order by "Model1"."id" asc',
'select "model2"."model1_id", "model2"."id_col" from "model2" where "model2"."model1_id" in (2) order by "model2"."id_col" asc',
'select "Model1Model2"."model2Id" as "objectiontmpjoin0", "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1, 2) order by "Model1"."id" asc',
'delete from "model2" where "model2"."id_col" in (2) and "model2"."model1_id" in (2)',
'delete from "Model1" where "Model1"."id" in (select "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1) and "Model1"."id" in (5) order by "Model1"."id" asc)',
'insert into "Model1" ("model1Prop1") values (\'inserted manyToMany\') returning "id"',
'insert into "model2" ("model1_id", "model2_prop1") values (2, \'inserted hasMany\') returning "id_col"',
'insert into "Model1Model2" ("model1Id", "model2Id") values (8, 1) returning "model1Id"',
'update "Model1" set "model1Prop1" = \'updated belongsToOne\' where "Model1"."id" = 3 and "Model1"."id" in (3)',
'update "Model1" set "model1Prop1" = \'updated manyToMany 1\' where "Model1"."id" in (select "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1) and "Model1"."id" = \'4\' order by "Model1"."id" asc)',
'update "model2" set "model2_prop1" = \'updated hasMany 1\' where "model2"."id_col" = 1',
]);
} else if (fetchStrategy === FetchStrategy.Everything) {
chai
.expect(sql)
.to.containSubset([
'select "Model1".* from "Model1" where "Model1"."id" in (2) order by "Model1"."id" asc' +
passthroughMethodCallSql[passthroughMethodCall],
'select "Model1".* from "Model1" where "Model1"."id" in (3) order by "Model1"."id" asc',
'select "model2".* from "model2" where "model2"."model1_id" in (2) order by "model2"."id_col" asc',
'select "Model1".*, "Model1Model2"."extra3" as "aliasedExtra", "Model1Model2"."model2Id" as "objectiontmpjoin0" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1, 2) order by "Model1"."id" asc',
'delete from "model2" where "model2"."id_col" in (2) and "model2"."model1_id" in (2)',
'delete from "Model1" where "Model1"."id" in (select "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1) and "Model1"."id" in (5) order by "Model1"."id" asc)',
'insert into "Model1" ("model1Prop1") values (\'inserted manyToMany\') returning "id"',
'insert into "model2" ("model1_id", "model2_prop1") values (2, \'inserted hasMany\') returning "id_col"',
'insert into "Model1Model2" ("model1Id", "model2Id") values (8, 1) returning "model1Id"',
'update "Model1" set "model1Prop1" = \'updated belongsToOne\' where "Model1"."id" = 3 and "Model1"."id" in (3)',
'update "Model1" set "model1Prop1" = \'updated manyToMany 1\' where "Model1"."id" in (select "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1) and "Model1"."id" = \'4\' order by "Model1"."id" asc)',
'update "model2" set "model2_prop1" = \'updated hasMany 1\' where "model2"."id_col" = 1',
]);
} else if (fetchStrategy === FetchStrategy.OnlyNeeded) {
chai
.expect(sql)
.to.containSubset([
'select "Model1"."id", "Model1"."model1Id" from "Model1" where "Model1"."id" in (2) order by "Model1"."id" asc' +
passthroughMethodCallSql[passthroughMethodCall],
'select "Model1"."id", "Model1"."model1Prop1" from "Model1" where "Model1"."id" in (3) order by "Model1"."id" asc',
'select "model2"."model1_id", "model2"."id_col", "model2"."model2_prop1" from "model2" where "model2"."model1_id" in (2) order by "model2"."id_col" asc',
'select "Model1Model2"."model2Id" as "objectiontmpjoin0", "Model1"."id", "Model1"."model1Prop1" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1, 2) order by "Model1"."id" asc',
'delete from "model2" where "model2"."id_col" in (2) and "model2"."model1_id" in (2)',
'delete from "Model1" where "Model1"."id" in (select "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1) and "Model1"."id" in (5) order by "Model1"."id" asc)',
'insert into "Model1" ("model1Prop1") values (\'inserted manyToMany\') returning "id"',
'insert into "model2" ("model1_id", "model2_prop1") values (2, \'inserted hasMany\') returning "id_col"',
'insert into "Model1Model2" ("model1Id", "model2Id") values (8, 1) returning "model1Id"',
'update "Model1" set "model1Prop1" = \'updated belongsToOne\' where "Model1"."id" = 3 and "Model1"."id" in (3)',
'update "Model1" set "model1Prop1" = \'updated manyToMany 1\' where "Model1"."id" in (select "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1) and "Model1"."id" = \'4\' order by "Model1"."id" asc)',
'update "model2" set "model2_prop1" = \'updated hasMany 1\' where "model2"."id_col" = 1',
]);
}
}
expect(result.$beforeUpdateCalled).to.equal(undefined);
expect(result.$afterUpdateCalled).to.equal(undefined);
expect(result.model1Relation1.$beforeUpdateCalled).to.equal(1);
expect(result.model1Relation1.$afterUpdateCalled).to.equal(1);
expect(result.model1Relation2[0].$beforeUpdateCalled).to.equal(1);
expect(result.model1Relation2[0].$afterUpdateCalled).to.equal(1);
expect(result.model1Relation2[1].$beforeUpdateCalled).to.equal(undefined);
expect(result.model1Relation2[1].$afterUpdateCalled).to.equal(undefined);
expect(result.model1Relation2[1].$beforeInsertCalled).to.equal(1);
expect(result.model1Relation2[1].$afterInsertCalled).to.equal(1);
expect(
result.model1Relation2[0].model2Relation1[0].$beforeUpdateCalled,
).to.equal(1);
expect(
result.model1Relation2[0].model2Relation1[0].$afterUpdateCalled,
).to.equal(1);
expect(
result.model1Relation2[0].model2Relation1[1].$beforeInsertCalled,
).to.equal(1);
expect(
result.model1Relation2[0].model2Relation1[1].$afterInsertCalled,
).to.equal(1);
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 3,
model1Prop1: 'root 2',
model1Relation1: {
id: 3,
model1Id: null,
model1Prop1: 'updated belongsToOne',
},
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
id: 8,
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
idCol: 3,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 5 should be deleted.
expect(model1Rows.find((it) => it.id == 5)).to.equal(undefined);
// Row 6 should NOT be deleted even thought its parent is.
expect(model1Rows.find((it) => it.id == 6)).to.be.an(Object);
// Row 7 should NOT be deleted even thought its parent is.
expect(model1Rows.find((it) => it.id == 7)).to.be.an(Object);
// Row 2 should be deleted.
expect(model2Rows.find((it) => it.id_col == 2)).to.equal(undefined);
},
);
})
);
});
},
);
}
it('should respect noDelete, noInsert and noUpdate flags', () => {
const upsert = {
// Nothing is done for the root since it only has an ids.
id: 2,
model1Id: 3,
// don't update because of `noUpdate`
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
},
// update idCol=1
// don't delete idCol=2 because of `noDelete`
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// delete id=5
// don't insert new row because `noInsert`
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, {
fetchStrategy,
noUpdate: ['model1Relation1'],
noDelete: ['model1Relation2'],
noInsert: ['model1Relation2.model2Relation1'],
})
.then(() => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 3,
model1Prop1: 'root 2',
model1Relation1: {
// Not updated.
id: 3,
model1Id: null,
model1Prop1: 'belongsToOne',
},
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
],
},
{
// Not deleted.
idCol: 2,
model1Id: 2,
model2Prop1: 'hasMany 2',
model2Relation1: [
{
id: 6,
model1Id: null,
model1Prop1: 'manyToMany 3',
},
{
id: 7,
model1Id: null,
model1Prop1: 'manyToMany 4',
},
],
},
{
idCol: 3,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 5 should be deleted.
expect(model1Rows.find((it) => it.id == 5)).to.equal(undefined);
// Row 6 should NOT be deleted even thought its parent is.
expect(model1Rows.find((it) => it.id == 6)).to.be.an(Object);
// Row 7 should NOT be deleted even thought its parent is.
expect(model1Rows.find((it) => it.id == 7)).to.be.an(Object);
// Row 2 should NOT be deleted because of `noDelete`.
expect(model2Rows.find((it) => it.id_col == 2)).to.be.an(Object);
},
);
});
});
});
it('should update model if belongsToOne relation changes', () => {
const upsert = {
id: 1,
// This causes the parent model's model1Id to change
// which in turn should cause the parent to get updated.
model1Relation1: { id: 3 },
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { relate: true, fetchStrategy })
.then((result) => {
expect(result.$beforeUpdateCalled).to.equal(1);
expect(result.$afterUpdateCalled).to.equal(1);
expect(result.model1Relation1.$beforeUpdateCalled).to.equal(undefined);
expect(result.model1Relation1.$afterUpdateCalled).to.equal(undefined);
});
})
.then(() => {
return Model1.query(session.knex).findById(1);
})
.then((model) => {
expect(model.model1Id).to.equal(3);
});
});
it('should work with an empty object in belongsToOne relation', () => {
const upsert = {
model1Relation1: {},
};
return transaction(session.knex, (trx) =>
Model1.query(trx).upsertGraph(upsert, { fetchStrategy }),
)
.then((inserted) =>
Model1.query(session.knex).findById(inserted.id).withGraphFetched('model1Relation1'),
)
.then((model) => {
chai.expect(model).to.containSubset({
model1Prop1: null,
model1Prop2: null,
model1Relation1: {
model1Prop1: null,
model1Prop2: null,
},
});
});
});
it('should work with an empty object in hasOne relation', () => {
const upsert = {
model1Relation1Inverse: {},
};
return transaction(session.knex, (trx) =>
Model1.query(trx).upsertGraph(upsert, { fetchStrategy }),
)
.then((inserted) =>
Model1.query(session.knex)
.findById(inserted.id)
.withGraphFetched('model1Relation1Inverse'),
)
.then((model) => {
chai.expect(model).to.containSubset({
model1Prop1: null,
model1Prop2: null,
model1Relation1Inverse: {
model1Prop1: null,
model1Prop2: null,
},
});
});
});
it('should update model if the model changes and a belongsToOne relation changes', () => {
const upsert = {
id: 1,
model1Prop1: 'updated',
// This causes the parent model's model1Id to change
// which in turn should cause the parent to get updated.
model1Relation1: { id: 3 },
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { relate: true, fetchStrategy })
.then((result) => {
expect(result.$beforeUpdateCalled).to.equal(1);
expect(result.$afterUpdateCalled).to.equal(1);
expect(result.model1Relation1.$beforeUpdateCalled).to.equal(undefined);
expect(result.model1Relation1.$afterUpdateCalled).to.equal(undefined);
});
})
.then(() => {
return Model1.query(session.knex).findById(1);
})
.then((model) => {
expect(model.model1Id).to.equal(3);
expect(model.model1Prop1).to.equal('updated');
});
});
it('should work like insertGraph if root is an insert', () => {
const upsert = {
model1Prop1: 'new',
model1Relation1: {
model1Prop1: 'new belongsToOne',
},
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { fetchStrategy })
.then((result) => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(result.id)
.withGraphFetched('model1Relation1')
.select('model1Prop1')
.modifyGraph('model1Relation1', (qb) => qb.select('model1Prop1'));
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
model1Prop1: 'new',
model1Relation1: {
model1Prop1: 'new belongsToOne',
},
});
});
});
});
it('should upsert a model with relations and fetch the upserted graph', () => {
const upsert = {
id: 2,
model1Id: 3,
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
model1Prop1: 'inserted manyToMany',
},
],
},
{
model2Prop1: 'inserted hasMany',
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx).upsertGraphAndFetch(upsert, { fetchStrategy });
}).then((upserted) => {
return Model1.query(session.knex)
.withGraphFetched('[model1Relation1, model1Relation2.model2Relation1]')
.findById(upserted.id)
.then((fetched) => {
expect(upserted.$toJson()).to.eql(fetched.$toJson());
});
});
});
it('should insert new, update existing relate unrelated and unrelate missing if `unrelate` and `relate` options are true', () => {
const upsert = {
// the root gets updated because it has an id
id: 2,
model1Prop1: 'updated root 2',
// unrelate
model1Relation1: null,
// update idCol=1
// unrelate idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// unrelate id=5
// relate id=6
// and insert one new
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
},
{
// This will get related because it has an id
// that doesn't currently exist in the relation.
id: 6,
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
return transaction(session.knex, (trx) => {
const sql = [];
// Wrap the transaction to catch the executed sql.
trx = mockKnexFactory(trx, function (mock, oldImpl, args) {
sql.push(this.toString());
return oldImpl.apply(this, args);
});
return (
Model1.query(trx)
.upsertGraph(upsert, { unrelate: true, relate: true, fetchStrategy })
// Sort all result by id to make the SQL we test below consistent.
.context({
onBuild(builder) {
if (!builder.isFind()) {
return;
}
if (builder.modelClass().getTableName() === 'Model1') {
builder.orderBy('Model1.id');
} else if (builder.modelClass().getTableName() === 'model2') {
builder.orderBy('model2.id_col');
}
},
})
.then((result) => {
expect(result.model1Relation2[0].model2Relation1[2].$beforeUpdateCalled).to.equal(
undefined,
);
if (session.isPostgres()) {
expect(sql.length).to.equal(12);
if (fetchStrategy === FetchStrategy.OnlyIdentifiers) {
chai
.expect(sql)
.to.containSubset([
'select "Model1"."id", "Model1"."model1Id" from "Model1" where "Model1"."id" in (2) order by "Model1"."id" asc',
'select "Model1"."id" from "Model1" where "Model1"."id" in (3) order by "Model1"."id" asc',
'select "model2"."model1_id", "model2"."id_col" from "model2" where "model2"."model1_id" in (2) order by "model2"."id_col" asc',
'select "Model1Model2"."model2Id" as "objectiontmpjoin0", "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1, 2) order by "Model1"."id" asc',
'delete from "Model1Model2" where "Model1Model2"."model1Id" in (select "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1) and "Model1"."id" in (5) order by "Model1"."id" asc) and "Model1Model2"."model2Id" in (1)',
'update "model2" set "model1_id" = NULL where "model2"."id_col" in (2) and "model2"."model1_id" in (2)',
'insert into "Model1" ("model1Prop1") values (\'inserted manyToMany\') returning "id"',
'insert into "model2" ("model1_id", "model2_prop1") values (2, \'inserted hasMany\') returning "id_col"',
'insert into "Model1Model2" ("model1Id", "model2Id") values (8, 1), (6, 1) returning "model1Id"',
'update "Model1" set "model1Prop1" = \'updated root 2\', "model1Id" = NULL where "Model1"."id" = 2',
'update "Model1" set "model1Prop1" = \'updated manyToMany 1\' where "Model1"."id" in (select "Model1"."id" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1) and "Model1"."id" = 4 order by "Model1"."id" asc)',
'update "model2" set "model2_prop1" = \'updated hasMany 1\' where "model2"."id_col" = 1',
]);
}
}
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'updated root 2',
model1Relation1: null,
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
id: 6,
model1Id: null,
model1Prop1: 'manyToMany 3',
},
{
id: 8,
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
idCol: 3,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 3 should NOT be deleted.
expect(model1Rows.find((it) => it.id == 3)).to.eql({
id: 3,
model1Id: null,
model1Prop1: 'belongsToOne',
model1Prop2: null,
});
// Row 5 should NOT be deleted.
expect(model1Rows.find((it) => it.id == 5)).to.eql({
id: 5,
model1Id: null,
model1Prop1: 'manyToMany 2',
model1Prop2: null,
});
// Row 2 should NOT be deleted.
expect(model2Rows.find((it) => it.id_col == 2)).to.eql({
id_col: 2,
model1_id: null,
model2_prop1: 'hasMany 2',
model2_prop2: null,
});
},
);
})
);
});
});
it('should relate a HasManyRelation if `relate` option is true', () => {
const BoundModel1 = Model1.bindKnex(session.knex);
const upsert = {
id: 1,
// relate 1, 2
// insert 'new'
model1Relation2: [
{
idCol: 1,
model2Prop1: 'also update',
},
{
idCol: 2,
},
{
model2Prop1: 'new',
},
],
};
return BoundModel1.query()
.upsertGraph(upsert, { relate: true, fetchStrategy })
.then(() => {
return BoundModel1.query().findById(1).withGraphFetched('model1Relation2');
})
.then((result) => {
expect(result.model1Relation2).to.have.length(3);
chai.expect(result).to.containSubset({
id: 1,
model1Id: null,
model1Prop1: 'root 1',
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'also update',
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hasMany 2',
},
{
model1Id: 1,
model2Prop1: 'new',
},
],
});
});
});
it('should relate a HasManyRelation if #dbRef is used', () => {
const BoundModel1 = Model1.bindKnex(session.knex);
const upsert = {
id: 1,
// relate 1, 2
// insert 'new'
model1Relation2: [
{
'#dbRef': 1,
model2Prop1: 'also update',
},
{
'#dbRef': 2,
},
{
model2Prop1: 'new',
},
],
};
return BoundModel1.query()
.upsertGraph(upsert, { fetchStrategy, allowRefs: true })
.then(() => {
return BoundModel1.query().findById(1).withGraphFetched('model1Relation2');
})
.then((result) => {
expect(result.model1Relation2).to.have.length(3);
chai.expect(result).to.containSubset({
id: 1,
model1Id: null,
model1Prop1: 'root 1',
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'also update',
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hasMany 2',
},
{
model1Id: 1,
model2Prop1: 'new',
},
],
});
});
});
it('should also update if relate model has other properties than id', () => {
const upsert = {
id: 2,
// unrelate idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
// update id=4
// unrelate id=5
// relate id=6
// and insert one new
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
},
{
// This will get related because it has an id
// that doesn't currently exist in the relation.
// This should also get updated.
id: 6,
model1Prop1: 'related and updated manyToMany',
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { unrelate: true, relate: true, fetchStrategy })
.then((result) => {
expect(result.model1Relation2[0].model2Relation1[2].$beforeUpdateCalled).to.equal(1);
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched('[model1Relation2(orderById).model2Relation1(orderById)]');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 3,
model1Prop1: 'root 2',
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
id: 6,
model1Id: null,
model1Prop1: 'related and updated manyToMany',
},
{
id: 8,
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
idCol: 3,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
});
});
});
it('should be able to modify previously set properties to be null', () => {
const upsert = {
id: 2,
model1Prop1: null,
model1Relation2: [
{
idCol: 1,
model2Relation1: [
{
id: 4,
model1Prop1: null,
},
],
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { unrelate: true, relate: true, fetchStrategy })
.then((result) => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched('[model1Relation2(orderById).model2Relation1(orderById)]');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 3,
model1Prop1: null,
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: null,
},
],
},
],
});
});
});
});
it('should be able to automatically convert children that are plain JS objects into model instances', () => {
const parent = Model1.fromJson({
id: 2,
model1Prop1: null,
});
parent.model1Relation2 = [
{
idCol: 1,
model2Relation1: [
{
id: 4,
model1Prop1: null,
},
],
},
];
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(parent, { unrelate: true, relate: true, fetchStrategy })
.then((result) => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched('[model1Relation2(orderById).model2Relation1(orderById)]');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 3,
model1Prop1: null,
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: null,
},
],
},
],
});
});
});
});
it('should respect noRelate and noUnrelate flags', () => {
const upsert = {
// the root gets updated because it has an id
id: 2,
model1Prop1: 'updated root 2',
// unrelate
model1Relation1: null,
// update idCol=1
// don't unrelate idCol=2 because of `noUnrelate`
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// unrelate id=5
// don't relate id=6 because of `noRelate`
// and insert one new
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
},
{
id: 6,
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, {
fetchStrategy,
unrelate: true,
relate: true,
noUnrelate: ['model1Relation2'],
noRelate: ['model1Relation2.model2Relation1'],
})
.then((result) => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'updated root 2',
model1Relation1: null,
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
id: 8,
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
idCol: 2,
model1Id: 2,
model2Prop1: 'hasMany 2',
model2Relation1: [
{
id: 6,
model1Id: null,
model1Prop1: 'manyToMany 3',
},
{
id: 7,
model1Id: null,
model1Prop1: 'manyToMany 4',
},
],
},
{
idCol: 3,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 3 should NOT be deleted.
expect(model1Rows.find((it) => it.id == 3)).to.eql({
id: 3,
model1Id: null,
model1Prop1: 'belongsToOne',
model1Prop2: null,
});
// Row 5 should NOT be deleted.
expect(model1Rows.find((it) => it.id == 5)).to.eql({
id: 5,
model1Id: null,
model1Prop1: 'manyToMany 2',
model1Prop2: null,
});
// Row 2 should NOT be deleted.
expect(model2Rows.find((it) => it.id_col == 2)).to.eql({
id_col: 2,
model1_id: 2,
model2_prop1: 'hasMany 2',
model2_prop2: null,
});
},
);
});
});
});
it('should relate and unrelate some models if `unrelate` and `relate` are arrays of relation paths', () => {
const upsert = {
// the root gets updated because it has an id
id: 2,
model1Prop1: 'updated root 2',
// unrelate
model1Relation1: null,
// update idCol=1
// delete idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// unrelate id=5
// relate id=6
// and insert one new
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
},
{
// This will get related because it has an id
// that doesn't currently exist in the relation.
id: 6,
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, {
fetchStrategy,
unrelate: ['model1Relation1', 'model1Relation2.model2Relation1'],
relate: ['model1Relation2.model2Relation1'],
})
.then((result) => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'updated root 2',
model1Relation1: null,
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
id: 6,
model1Id: null,
model1Prop1: 'manyToMany 3',
},
{
id: 8,
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
idCol: 3,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 3 should NOT be deleted.
expect(model1Rows.find((it) => it.id == 3)).to.eql({
id: 3,
model1Id: null,
model1Prop1: 'belongsToOne',
model1Prop2: null,
});
// Row 5 should NOT be deleted.
expect(model1Rows.find((it) => it.id == 5)).to.eql({
id: 5,
model1Id: null,
model1Prop1: 'manyToMany 2',
model1Prop2: null,
});
// Row 2 should be deleted.
expect(model2Rows.find((it) => it.id_col == 2)).to.equal(undefined);
},
);
});
});
});
it('should update parent if a `BelongsToOne` relation changes (because the relation propery is in the parent)', () => {
const upsert = {
id: 1,
// This is a BelongsToOneRelation
model1Relation1: { id: 3 },
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { relate: true, unrelate: true, fetchStrategy })
.then((result) => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(result.id)
.withGraphFetched('model1Relation1')
.select('id')
.modifyGraph('model1Relation1', (qb) => qb.select('id'));
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 1,
model1Relation1: {
id: 3,
},
});
});
});
});
it('should update parent if a new `BelongsToOne` relation is inserted (because the relation propery is in the parent)', () => {
const model1Prop1 = 'new';
const upsert = {
id: 1,
// This is a BelongsToOneRelation
model1Relation1: { model1Prop1 },
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { relate: true, unrelate: true, fetchStrategy })
.then((result) => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(result.id)
.withGraphFetched('model1Relation1')
.select('id')
.modifyGraph('model1Relation1', (qb) => qb.select('model1Prop1'));
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 1,
model1Relation1: {
model1Prop1,
},
});
});
});
});
it('should delete and insert belongsToOneRelation', () => {
const upsert = {
// the root gets updated because it has an id
id: 2,
model1Prop1: 'updated root 2',
// The model with id 3 should get deleted and this new one inserted.
model1Relation1: {
model1Prop1: 'inserted belongsToOne',
},
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { fetchStrategy })
.then(() => {
// Fetch the graph from the database.
return Model1.query(trx).findById(2).withGraphFetched('model1Relation1');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 8,
model1Prop1: 'updated root 2',
model1Relation1: {
id: 8,
model1Id: null,
model1Prop1: 'inserted belongsToOne',
},
});
return Promise.all([trx('Model1'), trx('model2')]).then(([model1Rows]) => {
// Row 3 should be deleted.
expect(model1Rows.find((it) => it.id == 3)).to.equal(undefined);
});
});
});
});
it("should insert belongsToOneRelation if it's an array", () => {
const upsert = {
id: 2,
// The model with id 3 should get deleted and this new one inserted.
model1Relation1: [
{
model1Prop1: 'inserted belongsToOne',
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { fetchStrategy })
.then(() => {
// Fetch the graph from the database.
return Model1.query(trx).findById(2).withGraphFetched('model1Relation1');
})
.then(omitIrrelevantProps)
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Relation1: {
model1Prop1: 'inserted belongsToOne',
},
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 3 should be deleted.
expect(model1Rows.find((it) => it.id == 3)).to.equal(undefined);
},
);
});
});
});
it("should insert hasManyRelation if it's not an array", () => {
const upsert = {
id: 2,
// Should delete idCol = 2
// Should update idCol = 1
model1Relation2: {
idCol: 1,
model2Prop1: 'updated',
},
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { fetchStrategy })
.then(() => {
// Fetch the graph from the database.
return Model1.query(trx).findById(2).withGraphFetched('model1Relation2');
})
.then(omitIrrelevantProps)
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated',
},
],
});
return trx('model2').then((model2Rows) => {
// Row 2 should be deleted.
expect(model2Rows.find((it) => it.idCol == 2)).to.equal(undefined);
});
});
});
});
it('should unrelate and relate belongsToOneRelation', () => {
const upsert = {
id: 2,
// The model with id 3 should get unrelated and this new one related.
model1Relation1: {
id: 4,
},
};
const options = {
fetchStrategy,
unrelate: true,
relate: true,
};
return transaction(session.knex, (trx) => {
const sql = [];
// Wrap the transaction to catch the executed sql.
trx = mockKnexFactory(trx, function (mock, oldImpl, args) {
sql.push(this.toString());
return oldImpl.apply(this, args);
});
return Model1.query(trx)
.upsertGraph(upsert, options)
.then(() => {
if (fetchStrategy === FetchStrategy.OnlyIdentifiers) {
expect(sql.length).to.equal(3);
}
if (session.isPostgres()) {
if (fetchStrategy === FetchStrategy.OnlyIdentifiers) {
chai.expect(sql).to.containSubset([
'select "Model1"."id", "Model1"."model1Id" from "Model1" where "Model1"."id" in (2)',
'select "Model1"."id" from "Model1" where "Model1"."id" in (3)',
// There should only be one `model1Id` update here. If you see two, something is broken.
'update "Model1" set "model1Id" = 4 where "Model1"."id" = 2',
]);
}
}
// Fetch the graph from the database.
return Model1.query(trx).findById(2).withGraphFetched('model1Relation1');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 4,
model1Prop1: 'root 2',
model1Relation1: {
id: 4,
model1Id: null,
model1Prop1: 'manyToMany 1',
},
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 3 should not be deleted.
expect(model1Rows.find((it) => it.id == 3)).to.not.equal(undefined);
},
);
});
});
});
it('should not update other than the relation properties when belongsToOneRelation is inserted but the parent has noUpdate: true', () => {
const upsert = {
id: 2,
model1Relation1: {
id: 3,
model1Prop1: 'this should not be written to db',
// This should cause the id=3 to be updated with the new
// model1Id property.
model1Relation1: {
model1Prop1: 'inserted',
},
},
};
return Model1.query(session.knex)
.upsertGraph(upsert, {
fetchStrategy,
noUpdate: ['model1Relation1'],
})
.then(() => {
// Fetch the graph from the database.
return Model1.query(session.knex)
.findById(2)
.withGraphFetched('model1Relation1.model1Relation1');
})
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Relation1: {
id: 3,
model1Prop1: 'belongsToOne',
model1Relation1: {
model1Prop1: 'inserted',
},
},
});
});
});
it('should not update other than the relation properties when belongsToOneRelation is related but the parent has noUpdate: true', () => {
const upsert = {
id: 2,
model1Relation1: {
id: 3,
model1Prop1: 'this should not be written to db',
// This should cause the id=3 to be updated with the new
// model1Id property.
model1Relation1: {
id: 1,
},
},
};
return Model1.query(session.knex)
.upsertGraph(upsert, {
fetchStrategy,
noUpdate: ['model1Relation1'],
relate: ['model1Relation1.model1Relation1'],
})
.then(() => {
// Fetch the graph from the database.
return Model1.query(session.knex)
.findById(2)
.withGraphFetched('model1Relation1.model1Relation1');
})
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Relation1: {
id: 3,
model1Prop1: 'belongsToOne',
model1Relation1: {
id: 1,
model1Prop1: 'root 1',
},
},
});
});
});
it('should not update other than the relation properties when belongsToOneRelation is unrelated but the parent has noUpdate: true', () => {
const upsert1 = {
id: 2,
model1Relation1: {
id: 3,
model1Relation1: {
id: 1,
},
},
};
const upsert2 = {
id: 2,
model1Relation1: {
id: 3,
model1Prop1: 'this should not be written to db',
model1Relation1: null,
},
};
return Model1.query(session.knex)
.upsertGraph(upsert1, {
fetchStrategy,
noUpdate: ['model1Relation1'],
relate: ['model1Relation1.model1Relation1'],
})
.then(() => {
return Model1.query(session.knex).upsertGraph(upsert2, {
fetchStrategy,
noUpdate: ['model1Relation1'],
unrelate: ['model1Relation1.model1Relation1'],
});
})
.then(() => {
// Fetch the graph from the database.
return Model1.query(session.knex)
.findById(2)
.withGraphFetched('model1Relation1.model1Relation1');
})
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Relation1: {
id: 3,
model1Prop1: 'belongsToOne',
model1Id: null,
model1Relation1: null,
},
});
});
});
it('should insert with an id instead of throwing an error if `insertMissing` option is true', () => {
const upsert = {
id: 2,
// update idCol=1
// delete idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// delete id=5
// and insert one new
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row with an id.
id: 1000,
model1Prop1: 'inserted manyToMany',
},
],
},
{
// This is the new row with an id.
idCol: 1000,
model2Prop1: 'inserted hasMany',
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx).upsertGraph(upsert, { insertMissing: true, fetchStrategy });
})
.then(() => {
// Fetch the graph from the database.
return Model1.query(session.knex)
.findById(2)
.withGraphFetched('model1Relation2(orderById).model2Relation1(orderById)');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 3,
model1Prop1: 'root 2',
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
id: 1000,
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
idCol: 1000,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
});
});
it('should insert with an id instead of relating if `insertMissing` option is true and the item doesnt exist int the db', () => {
const upsert = {
id: 2,
// update idCol=1
// delete idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// delete id=5
// and insert one new
model2Relation1: [
{
// Has an id and exist in db --> relate
id: 1,
},
{
// Has an id and exists in the relation --> update
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// Has an id and doesn't exist in db --> insert
id: 1000,
model1Prop1: 'inserted manyToMany',
},
],
},
{
// Has an id and doesn't exist in db --> insert
idCol: 1000,
model2Prop1: 'inserted hasMany',
},
],
};
const options = {
relate: true,
unrelate: true,
insertMissing: true,
fetchStrategy,
};
return Model1.query(session.knex)
.upsertGraph(upsert, options)
.then(() => {
// Fetch the graph from the database.
return Model1.query(session.knex)
.findById(2)
.withGraphFetched('model1Relation2(orderById).model2Relation1(orderById)');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 3,
model1Prop1: 'root 2',
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 1,
model1Id: null,
model1Prop1: 'root 1',
},
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
id: 1000,
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
idCol: 1000,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
});
});
it('should insert root model with an id instead of throwing an error if `insertMissing` option is true', () => {
let upsert = {
// This doesn't exist.
id: NONEXISTENT_ID,
model1Prop1: `updated root ${NONEXISTENT_ID}`,
model1Relation1: {
model1Prop1: 'inserted belongsToOne',
},
};
const upsertAndCompare = () => {
return transaction(session.knex, (trx) => {
return Model1.query(trx).upsertGraph(upsert, { insertMissing: true, fetchStrategy });
})
.then((result) => {
// Fetch the graph from the database.
return Model1.query(session.knex)
.findById(NONEXISTENT_ID)
.withGraphFetched('model1Relation1');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: NONEXISTENT_ID,
model1Prop1: `updated root ${NONEXISTENT_ID}`,
model1Id: 8,
model1Relation1: {
id: 8,
model1Id: null,
model1Prop1: 'inserted belongsToOne',
},
});
// Change upsert to the result, for the 2nd upsertAndCompare()
upsert = result;
});
};
// Execute upsertAndCompare() twice, first to insert, then to update
return upsertAndCompare().then(() => upsertAndCompare());
});
it('should fail if given nonexistent id in root', (done) => {
const upsert = {
// This doesn't exist.
id: NONEXISTENT_ID,
model1Prop1: 'updated root 2',
model1Relation1: {
model1Prop1: 'inserted belongsToOne',
},
};
transaction(session.knex, (trx) => {
return Model1.query(trx).upsertGraph(upsert, { fetchStrategy });
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err instanceof Model1.NotFoundError).to.equal(true);
expect(err.message).to.equal(
'root model (id=1000) does not exist. If you want to insert it with an id, use the insertMissing option',
);
expect(err.data.dataPath).to.eql([]);
return session
.knex('Model1')
.whereIn('model1Prop1', ['updated root 2', 'inserted belongsToOne']);
})
.then((rows) => {
expect(rows).to.have.length(0);
done();
})
.catch(done);
});
it('should fail if given nonexistent id in a relation (without relate: true option)', (done) => {
const upsert = {
id: 2,
model1Prop1: 'updated root 2',
// id 1000 is not related to id 2. This will thrown an error.
model1Relation1: {
id: NONEXISTENT_ID,
model1Prop1: 'inserted belongsToOne',
},
};
transaction(session.knex, (trx) => {
return Model1.query(trx).upsertGraph(upsert, { fetchStrategy });
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err instanceof Model1.NotFoundError).to.equal(true);
expect(err.type).to.equal('NotFound');
expect(err.message).to.equal(
'model (id=1000) is not a child of model (id=2). If you want to relate it, use the relate option. If you want to insert it with an id, use the insertMissing option',
);
expect(err.data.dataPath).to.eql(['model1Relation1']);
return session
.knex('Model1')
.whereIn('model1Prop1', ['updated root 2', 'inserted belongsToOne']);
})
.then((rows) => {
expect(rows).to.have.length(0);
done();
})
.catch(done);
});
it('allowGraph should limit the relations that can be upserted', () => {
const errors = [];
const upsert = {
// the root gets updated because it has an id
id: 2,
model1Prop1: 'updated root 2',
// unrelate
model1Relation1: null,
// update idCol=1
// unrelate idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// unrelate id=5
// relate id=6
// and insert one new
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
},
{
// This will get related because it has an id
// that doesn't currently exist in the relation.
id: 6,
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
// This should fail.
return Model1.query(session.knex)
.upsertGraph(upsert, { unrelate: true, relate: true, fetchStrategy })
.allowGraph('[model1Relation1, model1Relation2]')
.catch((err) => {
errors.push(err);
// This should also fail.
return Model1.query(session.knex)
.upsertGraph(upsert, { unrelate: true, relate: true, fetchStrategy })
.allowGraph('[model1Relation2.model2Relation1]');
})
.catch((err) => {
errors.push(err);
// This should succeed.
return Model1.query(session.knex)
.upsertGraph(upsert, { unrelate: true, relate: true, fetchStrategy })
.allowGraph('[model1Relation1, model1Relation2.model2Relation1]');
})
.then(() => {
// Fetch the graph from the database.
return Model1.query(session.knex)
.findById(2)
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(errors.length).to.equal(2);
errors.forEach((error) => {
expect(error).to.be.a(ValidationError);
expect(error.type).to.equal('UnallowedRelation');
expect(error.message).to.equal('trying to upsert an unallowed relation');
});
expect(result).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'updated root 2',
model1Relation1: null,
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
id: 6,
model1Id: null,
model1Prop1: 'manyToMany 3',
},
{
id: 8,
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
idCol: 3,
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
return Promise.all([session.knex('Model1'), session.knex('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 3 should NOT be deleted.
expect(model1Rows.find((it) => it.id == 3)).to.eql({
id: 3,
model1Id: null,
model1Prop1: 'belongsToOne',
model1Prop2: null,
});
// Row 5 should NOT be deleted.
expect(model1Rows.find((it) => it.id == 5)).to.eql({
id: 5,
model1Id: null,
model1Prop1: 'manyToMany 2',
model1Prop2: null,
});
// Row 2 should NOT be deleted.
expect(model2Rows.find((it) => it.id_col == 2)).to.eql({
id_col: 2,
model1_id: null,
model2_prop1: 'hasMany 2',
model2_prop2: null,
});
},
);
});
});
it('raw sql and subqueries should work', () => {
const upsert = {
// the root gets updated because it has an id
id: 2,
model1Prop1: raw('10 + 20'),
// update
model1Relation1: {
id: 3,
model1Prop1: Model2.query(session.knex).min('id_col'),
},
// update idCol=1
// delete idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: session.knex.raw('50 * 100'),
// update id=4
// delete id=5
// and insert one new
model2Relation1: [
{
id: 4,
model1Prop1: session.knex.raw('30 * 100'),
},
{
// This is the new row.
model1Prop1: Model2.query(session.knex).min('id_col'),
},
],
},
{
// This is the new row.
model2Prop1: session.knex('Model1').min('id').where('id', '>', 1),
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { fetchStrategy })
.then(() => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 3,
model1Prop1: '30',
model1Relation1: {
id: 3,
model1Id: null,
model1Prop1: '1',
},
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: '5000',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: '3000',
},
{
id: 8,
model1Id: null,
model1Prop1: '1',
},
],
},
{
idCol: 3,
model1Id: 2,
model2Prop1: '2',
model2Relation1: [],
},
],
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 5 should be deleted.
expect(model1Rows.find((it) => it.id == 5)).to.equal(undefined);
// Row 6 should NOT be deleted even thought its parent is.
expect(model1Rows.find((it) => it.id == 6)).to.be.an(Object);
// Row 7 should NOT be deleted even thought its parent is.
expect(model1Rows.find((it) => it.id == 7)).to.be.an(Object);
// Row 2 should be deleted.
expect(model2Rows.find((it) => it.id_col == 2)).to.equal(undefined);
},
);
});
});
});
it('should delete belongsToOne relation and succesfully update parent after that', () => {
// This tests that the parent update doesn't try to set
// the foreign key back.
const upsert = {
id: 2,
model1Prop1: 'update',
model1Relation1: null,
};
return Model1.query(session.knex)
.upsertGraph(upsert, { fetchStrategy })
.then(() => {
return Model1.query(session.knex).findById(2).withGraphFetched('model1Relation1');
})
.then((result) => {
expect(result.model1Relation1).to.equal(null);
return Model1.query(session.knex).findById(3);
})
.then((result) => {
expect(result).to.equal(undefined);
});
});
it('The internal select queries should return true from `isInternal`', () => {
const upsert = Model1.fromJson({
id: 2,
model1Prop1: 'update',
model1Relation1: null,
});
let findQueryCount = 0;
return Model1.query(session.knex)
.upsertGraph(upsert, { fetchStrategy })
.context({
runBefore(_, builder) {
if (builder.isFind() && builder.isExecutable()) {
findQueryCount++;
expect(builder.isInternal()).to.equal(true);
}
},
})
.then(() => {
const fetchQuery = Model1.query(session.knex)
.findById(2)
.withGraphFetched('model1Relation1');
expect(findQueryCount).to.equal(2);
expect(fetchQuery.isInternal()).to.equal(false);
return fetchQuery;
});
});
it('should throw a sensible error if a non-object is passed in as the root', (done) => {
Model1.bindKnex(session.knex)
.query()
.upsertGraph('not a model')
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.type).to.equal('InvalidGraph');
expect(err.message).to.equal(
'expected value "not a model" to be an instance of Model1',
);
done();
})
.catch(done);
});
it('should throw a sensible error if a non-object is passed in a belongs to one relation', (done) => {
Model1.bindKnex(session.knex)
.query()
.upsertGraph(
{
id: 1,
model1Relation1: 'not an object',
},
{
fetchStrategy,
},
)
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.type).to.equal('InvalidGraph');
expect(err.message).to.equal(
'expected value "not an object" to be an instance of Model1',
);
done();
})
.catch(done);
});
it('should throw a sensible error if a non-object is passed in a has many relation', (done) => {
Model1.bindKnex(session.knex)
.query()
.upsertGraph(
{
id: 1,
model1Relation2: ['not an object'],
},
{
fetchStrategy,
},
)
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.type).to.equal('InvalidGraph');
expect(err.message).to.equal(
'expected value "not an object" to be an instance of Model2',
);
done();
})
.catch(done);
});
it('should throw if any `where` calls are added to the query', (done) => {
Model1.bindKnex(session.knex)
.query()
.where('id', 1)
.upsertGraph(
{
id: 1,
},
{
fetchStrategy,
},
)
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.message).to.equal(
'upsertGraph query should contain no other query builder calls like `findById`, `where` or `$relatedQuery` that would affect the SQL. They have no effect.',
);
done();
})
.catch(done);
});
it('should throw if any `findById` call is added to the query', (done) => {
Model1.bindKnex(session.knex)
.query()
.findById(1)
.upsertGraph(
{
id: 1,
},
{
fetchStrategy,
},
)
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.message).to.equal(
'upsertGraph query should contain no other query builder calls like `findById`, `where` or `$relatedQuery` that would affect the SQL. They have no effect.',
);
done();
})
.catch(done);
});
it('should throw if any `findOne` call is added to the query', (done) => {
Model1.bindKnex(session.knex)
.query()
.findOne({ id: 1 })
.upsertGraph(
{
id: 1,
},
{
fetchStrategy,
},
)
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.message).to.equal(
'upsertGraph query should contain no other query builder calls like `findById`, `where` or `$relatedQuery` that would affect the SQL. They have no effect.',
);
done();
})
.catch(done);
});
it('should throw if `upsertGraph` is used with `$relatedQuery`', (done) => {
Model1.fromJson({ id: 1 })
.$relatedQuery('model1Relation1', session.knex)
.upsertGraph(
{
id: 1,
},
{
fetchStrategy,
},
)
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.message).to.equal(
'upsertGraph query should contain no other query builder calls like `findById`, `where` or `$relatedQuery` that would affect the SQL. They have no effect.',
);
done();
})
.catch(done);
});
if (fetchStrategy !== FetchStrategy.OnlyIdentifiers) {
describe('fetchStrategy != OnlyIdentifiers', () => {
it('should fetch all properties and avoid useless update operations if fetchStrategy != OnlyIdentifiers', () => {
// This is exactly the current graph, except for the couple of commented changes.
const graph = {
id: 2,
model1Id: 3,
model1Prop1: 'root 2',
model1Relation1: {
id: 3,
model1Id: null,
model1Prop1: 'belongsToOne',
},
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'hasMany 1',
model2Relation1: [
{
id: 4,
model1Id: null,
model1Prop1: 'manyToMany 1',
},
{
id: 5,
model1Id: null,
model1Prop1: 'manyToMany 2',
aliasedExtra: null,
},
],
// This should get inserted.
model2Relation3: [
{
model3Prop1: 'hello',
model3JsonProp: {
foo: 'heps',
bar: [
{
spam: true,
},
{
spam: false,
},
],
},
},
],
},
{
idCol: 2,
model1Id: 2,
model2Prop1: 'hasMany 2',
model2Relation1: [
// This should get updated.
{
id: 6,
model1Id: null,
model1Prop1: 'manyToMany 33',
},
{
id: 7,
model1Id: null,
model1Prop1: 'manyToMany 4',
},
],
},
],
};
return transaction(session.knex, (trx) => {
let sql = [];
// Wrap the transaction to catch the executed sql.
trx = mockKnexFactory(trx, function (mock, oldImpl, args) {
sql.push(this.toString());
return oldImpl.apply(this, args);
});
return Model1.query(trx)
.upsertGraph(graph, {
fetchStrategy,
})
.then(() => {
// There should only be the selects, one update and a m2m insert.
// 5 selects, 1 update, 2 inserts (row and pivot row).
expect(sql.length).to.equal(8);
return Model1.query(trx)
.findById(2)
.withGraphFetched({
model1Relation1: true,
model1Relation2: {
model2Relation1: true,
model2Relation3: true,
},
});
})
.then((fetchedGraph) => {
sql = [];
return Model1.query(trx).upsertGraph(fetchedGraph, { fetchStrategy });
})
.then(() => {
// There should only be the selects since we patched using
// the current state.
expect(sql.length).to.equal(5);
return Model1.query(trx)
.findById(2)
.withGraphFetched({
model1Relation1: true,
model1Relation2: {
model2Relation1: true,
model2Relation3: true,
},
})
.then((fetchedGraph) => {
sql = [];
const model2 = fetchedGraph.model1Relation2.find(
(it) => it.model2Relation3.length > 0,
);
const model3 = model2.model2Relation3[0];
model3.model3JsonProp.bar[1].spam = true;
return Model1.query(trx).upsertGraph(fetchedGraph, { fetchStrategy });
})
.then(() => {
// There should only be the selects and the json field update.
expect(sql.length).to.equal(6);
});
});
});
});
});
it('should throw a sensible error if an option with an invalid type is passed', (done) => {
Model1.bindKnex(session.knex)
.query()
.upsertGraph(
{
id: 1,
},
{
noRelate: 'model1Relation2',
},
)
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err.message).to.equal(
'expected noRelate option value "model1Relation2" to be an instance of boolean or array of strings',
);
done();
})
.catch(done);
});
}
describe('relate with children => upsertGraph recursively called', () => {
beforeEach(() => {
population = [
{
id: 1,
model1Prop1: 'root 1',
model1Relation3: [
{
idCol: 1,
model2Prop1: 'manyToMany 1',
// This is a ManyToManyRelation
model2Relation3: [
{
id: 1,
model3Prop1: 'model3Prop1 1',
},
{
id: 3,
model3Prop1: 'model3Prop1 3',
},
],
model2Relation2: {
id: 3,
model1Prop1: 'hasOne 3',
},
},
],
},
{
id: 2,
model1Prop1: 'root 2',
// This is a ManyToManyRelation
model1Relation3: [
{
idCol: 2,
model2Prop1: 'manyToMany 2',
// This is a ManyToManyRelation
model2Relation3: [
{
id: 2,
model3Prop1: 'model3Prop1 2',
},
{
id: 4,
model3Prop1: 'model3Prop1 4',
},
],
model2Relation2: {
id: 4,
model1Prop1: 'hasOne 4',
},
},
],
},
{
id: 5,
model1Prop1: 'root 5',
},
{
id: 6,
model1Prop1: 'root 6',
},
{
id: 7,
model1Prop1: 'root 7',
},
];
return session.populate(population);
});
it('should relate BelongsToOne relation and nested children as expected', () => {
const upsert = {
id: 2,
model1Prop1: 'updated root 2',
// Relate new BelongsToOne relation
model1Relation1: {
id: 6,
model1Prop1: 'belongs to one 6',
// Relate new and update ManyToMany relation
model1Relation3: [
{
idCol: 2,
// Relate new and update ManyToMany relation
model2Relation3: [{ id: 1 }],
},
],
},
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { relate: true, unrelate: true, fetchStrategy })
.then(() => {
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation1.[model1Relation3(orderById).model2Relation3(orderById)]]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: 6,
model1Relation1: {
id: 6,
model1Prop1: 'belongs to one 6',
model1Id: null,
model1Relation3: [
{
extra1: null,
extra2: null,
idCol: 2,
model1Id: null,
model2Prop1: 'manyToMany 2',
model2Relation3: [
{ id: 1, model3Prop1: 'model3Prop1 1', model3JsonProp: null },
],
},
],
},
model1Prop1: 'updated root 2',
});
});
});
});
it('should relate ManyToMany relations and children as expected', () => {
const upsert = {
id: 2,
model1Prop1: 'updated root 2',
// Relate new and update ManyToMany relation
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
// Relate new and update Has Many relation
model2Relation2: {
id: 5,
model1Prop1: 'updated root 5',
// Update BelongsToOne
model1Relation1: {
id: 1,
},
},
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { relate: true, unrelate: true, fetchStrategy })
.then(() => {
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation3(orderById).[model2Relation2(orderById).model1Relation1]]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'updated root 2',
model1Relation3: [
{
extra1: null,
extra2: null,
idCol: 1,
model1Id: null,
model2Prop1: 'updated model2Prop1',
model2Relation2: {
id: 5,
model1Id: 1,
model1Prop1: 'updated root 5',
model1Relation1: {
id: 1,
model1Id: null,
model1Prop1: 'root 1',
},
},
},
],
});
});
});
});
it('should relate HasMany relations and children as expected', () => {
const upsert = {
id: 2,
model1Prop1: 'updated root 2',
// Relate new and update ManyToMany relation
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
// Relate new and update Has Many relation
model2Relation2: {
id: 5,
model1Prop1: 'updated root 5',
// Update BelongsToOne
model1Relation1: {
id: 1,
},
},
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { relate: true, unrelate: true, fetchStrategy })
.then(() => {
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation2(orderById).[model2Relation2(orderById).model1Relation1]]',
);
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'updated root 2',
model1Relation2: [
{
idCol: 1,
model1Id: 2,
model2Prop1: 'updated model2Prop1',
model2Relation2: {
id: 5,
model1Id: 1,
model1Prop1: 'updated root 5',
model1Relation1: {
id: 1,
model1Id: null,
model1Prop1: 'root 1',
},
},
},
],
});
});
});
});
it('should upsert recursively and respect options', () => {
const upsert = {
id: 2,
model1Prop1: 'updated root 2',
// Relate new and update ManyToMany relation
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
// Relate new and update ManyToMany relation
model2Relation3: [{ id: 2 }],
},
],
};
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, {
fetchStrategy,
relate: ['model1Relation3', 'model1Relation3.model2Relation3'],
noUnrelate: ['model1Relation3.model2Relation3'],
noDelete: ['model1Relation3.model2Relation3'],
})
.then(() => {
return Model1.query(trx)
.findById(2)
.withGraphFetched('model1Relation3(orderById).model2Relation3(orderById)');
})
.then(omitIrrelevantProps)
.then((result) => {
expect(result).to.eql({
id: 2,
model1Id: null,
model1Prop1: 'updated root 2',
model1Relation3: [
{
extra1: null,
extra2: null,
idCol: 1,
model1Id: null,
model2Prop1: 'updated model2Prop1',
model2Relation3: [
// Existing, but not removed
{
id: 1,
model3Prop1: 'model3Prop1 1',
model3JsonProp: null,
},
// Related
{
id: 2,
model3Prop1: 'model3Prop1 2',
model3JsonProp: null,
},
// Existing, but not removed
{
id: 3,
model3Prop1: 'model3Prop1 3',
model3JsonProp: null,
},
],
},
],
});
});
});
});
it('references to parent graph should produce an error in recursive upsert by default', (done) => {
const upsert = {
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
'#id': 'inserted',
model1Prop1: 'foo',
},
// This will get related.
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
// This will also get related.
model2Relation2: {
'#ref': 'inserted',
},
},
],
};
const options = {
relate: true,
unrelate: true,
fetchStrategy,
};
Model1.query(session.knex)
.upsertGraph(upsert, options)
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal(
'#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option',
);
done();
});
});
it('references to parent graph should work in recursive upsert', () => {
const upsert = {
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
'#id': 'inserted',
model1Prop1: 'foo',
},
// This will get related.
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
// This will also get related.
model2Relation2: {
'#ref': 'inserted',
},
},
],
};
const options = {
relate: true,
unrelate: true,
allowRefs: true,
fetchStrategy,
};
return Model1.query(session.knex)
.upsertGraph(upsert, options)
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
model1Prop1: 'foo',
},
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
model2Relation2: {
model1Prop1: 'foo',
},
},
],
});
expect(result.model1Relation1.id).to.equal(
result.model1Relation3[0].model2Relation2.id,
);
return Model1.query(session.knex)
.findById(2)
.withGraphFetched({
model1Relation1: true,
model1Relation3: {
model2Relation2: true,
},
});
})
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
model1Prop1: 'foo',
},
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
model2Relation2: {
model1Prop1: 'foo',
},
},
],
});
expect(result.model1Relation1.id).to.equal(
result.model1Relation3[0].model2Relation2.id,
);
});
});
it('property references to parent graph should work in recursive upsert', () => {
const upsert = {
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
'#id': 'inserted',
model1Prop1: 'foo',
model1Prop2: 101,
},
// This will get related.
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
// This will get inserted.
model2Relation2: {
model1Prop1: 'hello #ref{inserted.model1Prop1} #ref{inserted.model1Prop2}',
model1Prop2: '#ref{inserted.model1Prop2}',
},
},
],
};
const options = {
relate: true,
unrelate: true,
allowRefs: true,
fetchStrategy,
};
return Model1.query(session.knex)
.upsertGraph(upsert, options)
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
model1Prop1: 'foo',
},
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
model2Relation2: {
model1Prop1: 'hello foo 101',
model1Prop2: 101,
},
},
],
});
expect(result.model1Relation1.id).to.not.equal(
result.model1Relation3[0].model2Relation2.id,
);
return Model1.query(session.knex)
.findById(2)
.withGraphFetched({
model1Relation1: true,
model1Relation3: {
model2Relation2: true,
},
});
})
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
model1Prop1: 'foo',
},
model1Relation3: [
{
idCol: 1,
model2Prop1: 'updated model2Prop1',
model2Relation2: {
model1Prop1: 'hello foo 101',
model1Prop2: 101,
},
},
],
});
expect(result.model1Relation1.id).to.not.equal(
result.model1Relation3[0].model2Relation2.id,
);
});
});
});
describe('validation and transactions', () => {
before(() => {
Model1.$$jsonSchema = {
type: 'object',
required: ['model1Prop1', 'model1Prop2'],
properties: {
model1Prop1: { type: ['string', 'null'] },
model1Prop2: { type: ['integer', 'null'] },
},
};
Model2.$$jsonSchema = {
type: 'object',
required: ['model2Prop1'],
properties: {
model2Prop1: { type: ['string', 'null'] },
},
};
});
after(() => {
delete Model1.$$jsonSchema;
delete Model1.$$validator;
delete Model2.$$jsonSchema;
delete Model2.$$validator;
});
it('should validate (also tests transactions)', () => {
const fails = [
{
id: 2,
// This fails because of invalid type.
model1Prop1: 100,
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
model1Prop1: 'inserted manyToMany',
model1Prop2: 10,
},
],
},
{
model2Prop1: 'inserted hasMany',
},
],
},
{
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
id: 3,
// This fails because of invalid type.
model1Prop1: 100,
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
model1Prop1: 'inserted manyToMany',
model1Prop2: 10,
},
],
},
{
model2Prop1: 'inserted hasMany',
},
],
},
{
id: 2,
model1Prop1: 'updated root 2',
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row that fails because of invalid type.
model1Prop1: 100,
model1Prop2: 10,
},
],
},
{
model2Prop1: 'inserted hasMany',
},
],
},
];
const success = {
// the root gets updated because it has an id
id: 2,
model1Prop1: 'updated root 2',
// update
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
},
// update idCol=1
// delete idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// delete id=5
// and insert one new
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
model1Prop2: 10,
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
const errorKeys = [
'model1Prop1',
'model1Relation1.model1Prop1',
'model1Relation2[0].model2Relation1[1].model1Prop1',
];
return Promise.map(fails, (fail) => {
return transaction(session.knex, (trx) =>
Model1.query(trx).upsertGraph(fail, { fetchStrategy }),
).catch((err) => createRejectionReflection(err));
})
.then((results) => {
// Check that all transactions have failed because of a validation error.
results.forEach((res, index) => {
expect(res.isRejected()).to.equal(true);
expect(res.reason().data[errorKeys[index]][0].message).to.equal(
'must be string,null',
);
});
return Model1.query(session.knex)
.orderBy('id')
.whereIn('id', [1, 2])
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then((db) => {
// Check that the transactions worked and the database was in no way modified.
expect(omitIrrelevantProps(db)).to.eql(population);
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(success, { fetchStrategy })
.then(() => {
// Fetch the graph from the database.
return Model1.query(trx)
.findById(2)
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById).model2Relation1(orderById)]',
);
})
.then(omitIrrelevantProps)
.then(omitIds)
.then((result) => {
expect(result).to.eql({
model1Id: 3,
model1Prop1: 'updated root 2',
model1Relation1: {
model1Id: null,
model1Prop1: 'updated belongsToOne',
},
model1Relation2: [
{
model1Id: 2,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
model1Id: null,
model1Prop1: 'updated manyToMany 1',
},
{
model1Id: null,
model1Prop1: 'inserted manyToMany',
},
],
},
{
model1Id: 2,
model2Prop1: 'inserted hasMany',
model2Relation1: [],
},
],
});
return Promise.all([trx('Model1'), trx('model2')]).then(
([model1Rows, model2Rows]) => {
// Row 5 should be deleted.
expect(model1Rows.find((it) => it.id == 5)).to.equal(undefined);
// Row 6 should NOT be deleted even thought its parent is.
expect(model1Rows.find((it) => it.id == 6)).to.be.an(Object);
// Row 7 should NOT be deleted even thought its parent is.
expect(model1Rows.find((it) => it.id == 7)).to.be.an(Object);
// Row 2 should be deleted.
expect(model2Rows.find((it) => it.id_col == 2)).to.equal(undefined);
},
);
});
});
});
});
it('should always patch-validate #dbRef reference objects (ignore required)', () => {
const upsert = [
{
id: 1000,
model1Prop1: 'foo',
model1Prop2: 1,
model1Relation2: [
{
'#dbRef': 1,
},
],
},
{
id: 1001,
model1Prop1: 'bar',
model1Prop2: 2,
model1Relation1: {
'#dbRef': 2,
},
model1Relation3: [
{
'#dbRef': 2,
},
],
},
];
const options = {
// Insert missing from the root.
insertMissing: [''],
fetchStrategy,
allowRefs: true,
};
return Model1.query(session.knex)
.upsertGraph(upsert, options)
.then(() => {
return Model1.query(session.knex)
.findByIds([1000, 1001])
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById), model1Relation3(orderById)]',
);
})
.then((result) => {
chai.expect(result).to.containSubset([
{
id: 1000,
model1Relation2: [
{
idCol: 1,
model2Prop1: 'hasMany 1',
},
],
},
{
id: 1001,
model1Relation1: {
id: 2,
model1Prop1: 'root 2',
},
model1Relation3: [
{
idCol: 2,
model2Prop1: 'hasMany 2',
},
],
},
]);
});
});
it('should always patch-validate #dbRef reference objects (does update)', () => {
const upsert = [
{
id: 1000,
model1Prop1: 'foo',
model1Prop2: 1,
model1Relation2: [
{
'#dbRef': 1,
model2Prop1: 'updated 1',
},
],
},
{
id: 1001,
model1Prop1: 'bar',
model1Prop2: 2,
model1Relation1: {
'#dbRef': 2,
model1Prop1: 'updated 2',
},
model1Relation3: [
{
'#dbRef': 2,
model2Prop1: 'updated 3',
},
],
},
];
const options = {
// Insert missing from the root.
insertMissing: [''],
fetchStrategy,
allowRefs: true,
};
return Model1.query(session.knex)
.upsertGraph(upsert, options)
.then(() => {
return Model1.query(session.knex)
.findByIds([1000, 1001])
.withGraphFetched(
'[model1Relation1, model1Relation2(orderById), model1Relation3(orderById)]',
);
})
.then((result) => {
chai.expect(result).to.containSubset([
{
id: 1000,
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated 1',
},
],
},
{
id: 1001,
model1Relation1: {
id: 2,
model1Prop1: 'updated 2',
},
model1Relation3: [
{
idCol: 2,
model2Prop1: 'updated 3',
},
],
},
]);
});
});
it('should always patch-validate #dbRef reference objects (does validate)', (done) => {
const upsert = [
{
id: 1000,
model1Prop1: 'foo',
model1Prop2: 1,
model1Relation2: [
{
'#dbRef': 1,
model2Prop1: 1,
},
],
},
{
id: 1001,
model1Prop1: 'bar',
model1Prop2: 2,
model1Relation1: {
'#dbRef': 2,
model2Prop1: 'updated 2',
},
model1Relation3: [
{
'#dbRef': 2,
model2Prop1: 'updated 3',
},
],
},
];
const options = {
// Insert missing from the root.
insertMissing: [''],
fetchStrategy,
allowRefs: true,
};
Model1.query(session.knex)
.upsertGraph(upsert, options)
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.data['model1Relation2[0].model2Prop1'][0].message).to.equal(
'must be string,null',
);
done();
})
.catch(done);
});
});
describe('upserts with update: true option', () => {
before(() => {
Model1.$$jsonSchema = {
type: 'object',
required: ['model1Prop1', 'model1Prop2'],
properties: {
model1Prop1: { type: 'string' },
model1Prop2: { type: 'integer' },
},
};
});
after(() => {
delete Model1.$$jsonSchema;
delete Model1.$$validator;
});
it('should fail to do an incomplete upsert', async () => {
const fails = [
{
id: 2,
model1Prop1: 'updated root 2',
// This fails because of missing property.
// model1Prop2: 10,
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
model1Prop2: 100,
},
},
{
id: 2,
model1Prop1: 'updated root 2',
model1Prop2: 10,
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
// This fails because of missing property.
// model2Prop2: 100
},
},
];
const success = [
{
id: 2,
model1Prop1: 'updated root 2',
model1Prop2: 10,
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
model1Prop2: 100,
},
},
];
const errorKeys = ['model1Prop2', 'model1Relation1.model1Prop2'];
return Promise.map(fails, (fail) => {
return transaction(session.knex, (trx) =>
Model1.query(trx).upsertGraph(fail, { update: true, fetchStrategy }),
).catch((err) => createRejectionReflection(err));
})
.then((results) => {
// Check that all transactions have failed because of a validation error.
results.forEach((res, index) => {
expect(res.isRejected()).to.equal(true);
expect(res.reason().data[errorKeys[index]][0].message).to.equal(
"must have required property 'model1Prop2'",
);
});
})
.then(() => {
return transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(success, { update: true, fetchStrategy })
.then((result) => {
// Fetch the graph from the database.
return Model1.query(trx).findById(2).withGraphFetched('model1Relation1');
})
.then(omitIrrelevantProps)
.then(omitIds)
.then((result) => {
expect(result).to.eql({
model1Id: 3,
model1Prop1: 'updated root 2',
model1Relation1: {
model1Id: null,
model1Prop1: 'updated belongsToOne',
},
});
});
});
});
});
});
describe('cyclic references', () => {
it('should detect cycles in the graph', (done) => {
const upsert = {
id: 1,
model1Relation1: {
'#id': 'root',
model1Relation1: {
'#ref': 'root',
},
},
};
Model1.bindKnex(session.knex)
.query()
.upsertGraph(upsert, { fetchStrategy, allowRefs: true })
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal('the object graph contains cyclic references');
done();
})
.catch(done);
});
it('cycle detection should consider already inserted nodes', () => {
// There are no cycles in this graph because `id=2` has already
// been inserted.
const upsert = {
id: 2,
model1Relation1: {
id: 3,
model1Prop1: 'also updated',
model1Relation1: {
'#ref': '@1',
},
},
model1Relation1Inverse: {
'#id': '@1',
model1Prop1: 'hello',
},
};
return Model1.bindKnex(session.knex)
.query()
.upsertGraph(upsert, { fetchStrategy, allowRefs: true })
.then(() => {
return Model1.query(session.knex)
.findById(2)
.withGraphFetched({
model1Relation1: {
model1Relation1: true,
},
model1Relation1Inverse: true,
});
})
.then((result) => {
const id = result.model1Relation1.model1Relation1.id;
chai.expect(result).containSubset({
id: 2,
model1Relation1: {
id: 3,
model1Relation1: {
id,
},
},
model1Relation1Inverse: {
id,
},
});
});
});
});
describe('manytoManyRelation extra properties', () => {
it('insert', () => {
const upsert = {
idCol: 2,
model2Relation1: [
// Do nothing.
{
id: 6,
},
// Do nothing.
{
id: 7,
},
// Insert.
{
aliasedExtra: 'foo',
},
],
};
return transaction(session.knex, (trx) => {
return Model2.query(trx)
.upsertGraph(upsert, { fetchStrategy })
.then((result) => {
expect(result.model2Relation1[2].aliasedExtra).to.equal('foo');
});
})
.then(() => {
return Model2.query(session.knex)
.findById(2)
.withGraphFetched('model2Relation1(orderById)');
})
.then((model) => {
expect(model.model2Relation1[2].aliasedExtra).to.equal('foo');
});
});
it('relate', () => {
const upsert = {
idCol: 2,
// delete 6
model2Relation1: [
// relate
{
id: 5,
aliasedExtra: 'foo',
},
// do nothing.
{
id: 7,
},
],
};
return transaction(session.knex, (trx) => {
return Model2.query(trx)
.upsertGraph(upsert, { relate: true, fetchStrategy })
.then((result) => {
expect(result.model2Relation1[0].id).to.equal(5);
expect(result.model2Relation1[0].aliasedExtra).to.equal('foo');
});
})
.then(() => {
return Model2.query(session.knex)
.findById(2)
.withGraphFetched('model2Relation1(orderById)');
})
.then((model) => {
expect(model.model2Relation1[0].id).to.equal(5);
expect(model.model2Relation1[0].aliasedExtra).to.equal('foo');
});
});
it('update', () => {
const upsert = {
idCol: 2,
model2Relation1: [
{
id: 6,
aliasedExtra: 'hello extra 1',
},
{
id: 7,
aliasedExtra: 'hello extra 2',
},
],
};
return transaction(session.knex, (trx) => {
return Model2.query(trx)
.upsertGraph(upsert, { fetchStrategy })
.then((result) => {
expect(result.model2Relation1[0].aliasedExtra).to.equal('hello extra 1');
expect(result.model2Relation1[1].aliasedExtra).to.equal('hello extra 2');
});
})
.then(() => {
return Model2.query(session.knex)
.findById(2)
.withGraphFetched('model2Relation1(orderById)');
})
.then((model) => {
expect(model.model2Relation1[0].aliasedExtra).to.equal('hello extra 1');
expect(model.model2Relation1[1].aliasedExtra).to.equal('hello extra 2');
});
});
});
describe('modifying properties in $beforeUpdate (#2233)', () => {
let $beforeUpdate;
before(() => {
$beforeUpdate = Model1.prototype.$beforeUpdate;
Model1.prototype.$beforeUpdate = function () {
this.model1Prop1 = 'updated in before update';
};
});
after(() => {
Model1.prototype.$beforeUpdate = $beforeUpdate;
});
it('should include modified properties in update', () => {
const upsert = {
id: 1,
model1Prop2: 101,
};
return transaction(session.knex, (trx) => {
return Model1.query(trx).upsertGraph(upsert, { fetchStrategy });
})
.then((res) => {
expect(res.model1Prop1).to.equal('updated in before update');
expect(res.model1Prop2).to.equal(101);
return Model1.query(session.knex).findById(1);
})
.then((model) => {
expect(model.model1Prop1).to.equal('updated in before update');
expect(model.model1Prop2).to.equal(101);
});
});
});
describe('should not call onError() with internal exception (#2603)', () => {
let query;
before(() => {
query = Model1.query;
});
after(() => {
Model1.query = query;
});
it('should not call onError() with internal exception', async () => {
const upsert = { id: 2 };
let error = null;
Model1.query = function (trx) {
return query.call(this, trx).onError((err) => {
error = err;
});
};
await transaction(session.knex, (trx) => {
return Model1.query(trx).upsertGraph(upsert, { fetchStrategy });
});
expect(error).to.equal(null);
});
});
describe('should not call $beforeUpdate() on empty relates (#2605)', () => {
it('should not call $beforeUpdate() on empty relates', async () => {
const upsert = {
id: 2,
model1Relation1: { id: 3 },
};
await transaction(session.knex, (trx) => {
return Model1.query(trx)
.upsertGraph(upsert, { relate: true, fetchStrategy })
.then((result) => {
expect(result.$beforeUpdateCalled).to.equal(undefined);
expect(result.model1Relation1.$beforeUpdateCalled).to.equal(undefined);
});
});
});
});
if (session.isPostgres()) {
describe('returning', () => {
it('should propagate returning(*) to all update an insert operations', () => {
const upsert = {
// Nothing is done for the root since it only has an ids.
id: 2,
model1Id: 3,
// This should get updated.
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
},
// update idCol=1
// delete idCol=2
// and insert one new
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
// update id=4
// delete id=5
// insert new row
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
// relate id=1
model1Relation1: {
id: 1,
},
},
{
// This is the new row.
model1Prop1: 'inserted manyToMany',
},
],
},
{
// This is the new row.
model2Prop1: 'inserted hasMany',
},
],
};
return Model1.query(session.knex)
.upsertGraph(upsert, {
fetchStrategy,
relate: ['model1Relation2.model2Relation1.model1Relation1'],
})
.returning('*')
.then((result) => {
chai.expect(result).to.containSubset({
id: 2,
model1Id: 3,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Prop1: 'updated belongsToOne',
$beforeUpdateCalled: 1,
$beforeUpdateOptions: { patch: true },
$afterUpdateCalled: 1,
$afterUpdateOptions: { patch: true },
model1Id: null,
model1Prop2: null,
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'updated hasMany 1',
model2Relation1: [
{
id: 4,
model1Prop1: 'updated manyToMany 1',
model1Relation1: { id: 1 },
model1Id: 1,
$beforeUpdateCalled: 1,
$beforeUpdateOptions: { patch: true },
$afterUpdateCalled: 1,
$afterUpdateOptions: { patch: true },
model1Prop2: null,
},
{
model1Prop1: 'inserted manyToMany',
$beforeInsertCalled: 1,
id: 8,
model1Id: null,
model1Prop2: null,
$afterInsertCalled: 1,
},
],
model1Id: 2,
$beforeUpdateCalled: 1,
$beforeUpdateOptions: { patch: true },
$afterUpdateCalled: 1,
$afterUpdateOptions: { patch: true },
model2Prop2: null,
},
{
model2Prop1: 'inserted hasMany',
model1Id: 2,
$beforeInsertCalled: 1,
idCol: 3,
model2Prop2: null,
$afterInsertCalled: 1,
},
],
});
});
});
});
}
});
}
function omitIrrelevantProps(model) {
const delProps = ['model1Prop2', 'model2Prop2', 'aliasedExtra', '$afterFindCalled'];
Model1.traverse(model, (model) => {
delProps.forEach((prop) => delete model[prop]);
});
return model;
}
function omitIds(model) {
const delProps = ['id', 'idCol'];
Model1.traverse(model, (model) => {
delProps.forEach((prop) => delete model[prop]);
});
return model;
}
};
================================================
FILE: tests/integration/viewsAndAliases.js
================================================
const _ = require('lodash');
const utils = require('../../lib/utils/knexUtils');
const expect = require('expect.js');
const mockKnexFactory = require('../../testUtils/mockKnex');
// This is another one of those features that need a separate test suite
// because they are so pervasive.
module.exports = (session) => {
const queries = [];
const knex = mockKnexFactory(session.knex, function (mock, oldImpl, args) {
queries.push(this.toString());
return oldImpl.apply(this, args);
});
const Model1 = session.unboundModels.Model1.bindKnex(knex);
describe('views and aliases', () => {
let fullEager;
let fullEagerResult;
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
model1Relation1: {
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'hejsan 4',
},
],
},
},
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'hejsan 1',
model2Relation2: {
id: 8,
model1Prop1: 'hello 8',
model1Relation1: {
id: 9,
model1Prop1: 'hello 9',
},
},
},
{
idCol: 2,
model2Prop1: 'hejsan 2',
model2Relation1: [
{
id: 5,
model1Prop1: 'hello 5',
aliasedExtra: 'extra 5',
},
{
id: 6,
model1Prop1: 'hello 6',
aliasedExtra: 'extra 6',
model1Relation1: {
id: 7,
model1Prop1: 'hello 7',
},
model1Relation2: [
{
idCol: 3,
model2Prop1: 'hejsan 3',
},
],
},
],
},
],
},
]);
});
beforeEach(() => {
queries.splice(0, queries.length);
fullEager = `[
model1Relation1,
model1Relation2.model2Relation1.[
model1Relation1,
model1Relation2
]
]`;
// The result we should get for `fullEager`.
fullEagerResult = [
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
aliasedExtra: 'extra 5',
model1Relation1: null,
model1Relation2: [],
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
model1Relation1: {
id: 7,
model1Id: null,
model1Prop1: 'hello 7',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
],
},
],
},
];
});
before(() => {
// This makes sure the columnInfo cache is populated.
return Model1.query().findById(1).withGraphJoined('[model1Relation1, model1Relation2]');
});
describe('aliases', () => {
it('should use alias in joinRelated', () => {
return Model1.query()
.findById(1)
.table('Model1 as someAlias')
.joinRelated('[model1Relation1, model1Relation2, model1Relation3]')
.then((models) => {
if (utils.isPostgres(session.knex)) {
expect(queries[0].replace(/\s/g, '')).to.equal(
`
select "someAlias".*
from "Model1" as "someAlias"
inner join "Model1" as "model1Relation1" on "model1Relation1"."id" = "someAlias"."model1Id"
inner join "model2" as "model1Relation2" on "model1Relation2"."model1_id" = "someAlias"."id"
inner join "Model1Model2" as "model1Relation3_join" on "model1Relation3_join"."model1Id" = "someAlias"."id"
inner join "model2" as "model1Relation3" on "model1Relation3_join"."model2Id" = "model1Relation3"."id_col"
where "someAlias"."id" = 1
`.replace(/\s/g, ''),
);
}
});
});
it('should use alias with an instance query', () => {
return Model1.query()
.findById(1)
.then((model) => {
queries.splice(0, queries.length);
return model.$query().alias('foo').joinRelated('model1Relation1.model1Relation1');
})
.then((model) => {
if (session.isPostgres()) {
expect(queries).to.eql([
'select "foo".* from "Model1" as "foo" inner join "Model1" as "model1Relation1" on "model1Relation1"."id" = "foo"."model1Id" inner join "Model1" as "model1Relation1:model1Relation1" on "model1Relation1:model1Relation1"."id" = "model1Relation1"."model1Id" where "foo"."id" = 1',
]);
}
});
});
it('should use alias for eager queries (WhereInEagerOperation)', () => {
return Model1.query()
.findById(1)
.table('Model1 as someAlias')
.withGraphFetched(fullEager)
.then(sortEager)
.then((model) => {
if (utils.isPostgres(session.knex)) {
queries.sort();
const expectedQueries = [
/select "model2"\.\* from "model2" where "model2"\."model1_id" in \(1\)/,
/select "model2"\.\* from "model2" where "model2"\."model1_id" in (\(5, 6\)|\(6, 5\))/,
/select "someAlias"\.\* from "Model1" as "someAlias" where "someAlias"\."id" = 1/,
/select "someAlias"\.\* from "Model1" as "someAlias" where "someAlias"\."id" in \(2\)/,
/select "someAlias"\.\* from "Model1" as "someAlias" where "someAlias"\."id" in \(7\)/,
/select "someAlias"\.\*, "Model1Model2"\."extra3" as "aliasedExtra", "Model1Model2"\."model2Id" as "objectiontmpjoin0" from "Model1" as "someAlias" inner join "Model1Model2" on "someAlias"\."id" = "Model1Model2"\."model1Id" where "Model1Model2"\."model2Id" in (\(1, 2\)|\(2, 1\))/,
];
expectedQueries.forEach((expectedQuery, i) => {
expect(queries[i]).to.match(expectedQuery);
});
}
expect(model).to.eql(fullEagerResult[0]);
});
});
it('should use alias for eager queries (WhereInEagerOperation) (alias method)', () => {
return Model1.query()
.findById(1)
.alias('someAlias')
.withGraphFetched(fullEager)
.then(sortEager)
.then((model) => {
if (utils.isPostgres(session.knex)) {
queries.sort();
const expectedQueries = [
/select "model2"\.\* from "model2" where "model2"\."model1_id" in \(1\)/,
/select "model2"\.\* from "model2" where "model2"\."model1_id" in (\(5, 6\)|\(6, 5\))/,
/select "someAlias"\.\* from "Model1" as "someAlias" where "someAlias"\."id" = 1/,
/select "someAlias"\.\* from "Model1" as "someAlias" where "someAlias"\."id" in \(2\)/,
/select "someAlias"\.\* from "Model1" as "someAlias" where "someAlias"\."id" in \(7\)/,
/select "someAlias"\.\*, "Model1Model2"\."extra3" as "aliasedExtra", "Model1Model2"\."model2Id" as "objectiontmpjoin0" from "Model1" as "someAlias" inner join "Model1Model2" on "someAlias"\."id" = "Model1Model2"\."model1Id" where "Model1Model2"\."model2Id" in (\(1, 2\)|\(2, 1\))/,
];
expectedQueries.forEach((expectedQuery, i) => {
expect(queries[i]).to.match(expectedQuery);
});
}
expect(model).to.eql(fullEagerResult[0]);
});
});
it('should use alias for eager queries (withGraphJoined)', () => {
return Model1.query()
.findById(1)
.table('Model1 as someAlias')
.withGraphJoined(fullEager)
.then(sortEager)
.then((model) => {
if (utils.isPostgres(session.knex)) {
expect(queries.length).to.equal(1);
expect(queries[0].replace(/\s/g, '')).to.equal(
`
select
"someAlias"."id" as "id",
"someAlias"."model1Id" as "model1Id",
"someAlias"."model1Prop1" as "model1Prop1",
"someAlias"."model1Prop2" as "model1Prop2",
"model1Relation1"."id" as "model1Relation1:id",
"model1Relation1"."model1Id" as "model1Relation1:model1Id",
"model1Relation1"."model1Prop1" as "model1Relation1:model1Prop1",
"model1Relation1"."model1Prop2" as "model1Relation1:model1Prop2",
"model1Relation2"."id_col" as "model1Relation2:id_col",
"model1Relation2"."model1_id" as "model1Relation2:model1_id",
"model1Relation2"."model2_prop1" as "model1Relation2:model2_prop1",
"model1Relation2"."model2_prop2" as "model1Relation2:model2_prop2",
"model1Relation2:model2Relation1"."id" as "model1Relation2:model2Relation1:id",
"model1Relation2:model2Relation1"."model1Id" as "model1Relation2:model2Relation1:model1Id",
"model1Relation2:model2Relation1"."model1Prop1" as "model1Relation2:model2Relation1:model1Prop1",
"model1Relation2:model2Relation1"."model1Prop2" as "model1Relation2:model2Relation1:model1Prop2",
"model1Relation2:model2Relation1_join"."extra3" as "model1Relation2:model2Relation1:aliasedExtra",
"model1Relation2:model2Relation1:model1Relation1"."id" as "model1Relation2:model2Relation1:model1Relation1:id",
"model1Relation2:model2Relation1:model1Relation1"."model1Id" as "model1Relation2:model2Relation1:model1Relation1:model1Id",
"model1Relation2:model2Relation1:model1Relation1"."model1Prop1" as "model1Relation2:model2Relation1:model1Relation1:model1Prop1",
"model1Relation2:model2Relation1:model1Relation1"."model1Prop2" as "model1Relation2:model2Relation1:model1Relation1:model1Prop2",
"model1Relation2:model2Relation1:model1Relation2"."id_col" as "model1Relation2:model2Relation1:model1Relation2:id_col",
"model1Relation2:model2Relation1:model1Relation2"."model1_id" as "model1Relation2:model2Relation1:model1Relation2:model1_id",
"model1Relation2:model2Relation1:model1Relation2"."model2_prop1" as "model1Relation2:model2Relation1:model1Relation2:model2_prop1",
"model1Relation2:model2Relation1:model1Relation2"."model2_prop2" as "model1Relation2:model2Relation1:model1Relation2:model2_prop2"
from
"Model1" as "someAlias"
left join
"Model1" as "model1Relation1" on "model1Relation1"."id" = "someAlias"."model1Id"
left join
"model2" as "model1Relation2" on "model1Relation2"."model1_id" = "someAlias"."id"
left join
"Model1Model2" as "model1Relation2:model2Relation1_join" on "model1Relation2:model2Relation1_join"."model2Id" = "model1Relation2"."id_col"
left join
"Model1" as "model1Relation2:model2Relation1" on "model1Relation2:model2Relation1_join"."model1Id" = "model1Relation2:model2Relation1"."id"
left join
"Model1" as "model1Relation2:model2Relation1:model1Relation1" on "model1Relation2:model2Relation1:model1Relation1"."id" = "model1Relation2:model2Relation1"."model1Id"
left join
"model2" as "model1Relation2:model2Relation1:model1Relation2" on "model1Relation2:model2Relation1:model1Relation2"."model1_id" = "model1Relation2:model2Relation1"."id"
where
"someAlias"."id" = 1
`.replace(/\s/g, ''),
);
}
expect(model).to.eql(fullEagerResult[0]);
});
});
});
if (utils.isPostgres(session.knex)) {
describe('views', () => {
before(() => {
return session.knex.schema.raw(`
create view "someView" as (select "Model1".*, "Model1"."model1Prop1" as "viewProp" from "Model1")
`);
});
after(() => {
return session.knex.schema.raw(`
drop view "someView"
`);
});
before(() => {
// This makes sure the columnInfo cache is populated.
return Model1.query()
.findById(1)
.table('someView')
.withGraphJoined('[model1Relation1, model1Relation2]');
});
it('swapping table into a view for a joinRelated query should work', () => {
return Model1.query()
.findById(1)
.table('someView')
.joinRelated('[model1Relation1, model1Relation2, model1Relation3]')
.then((models) => {
if (utils.isPostgres(session.knex)) {
expect(queries[0].replace(/\s/g, '')).to.equal(
`
select "someView".*
from "someView"
inner join "someView" as "model1Relation1" on "model1Relation1"."id" = "someView"."model1Id"
inner join "model2" as "model1Relation2" on "model1Relation2"."model1_id" = "someView"."id"
inner join "Model1Model2" as "model1Relation3_join" on "model1Relation3_join"."model1Id" = "someView"."id"
inner join "model2" as "model1Relation3" on "model1Relation3_join"."model2Id" = "model1Relation3"."id_col"
where "someView"."id" = 1
`.replace(/\s/g, ''),
);
}
});
});
it('swapping table into a view for an eager query should work (WhereInEagerOperation)', () => {
return Model1.query()
.where('someView.id', 1)
.table('someView')
.withGraphFetched(fullEager)
.then(sortEager)
.then((model) => {
queries.sort();
const expectedQueries = [
/select "model2"\.\* from "model2" where "model2"\."model1_id" in \(1\)/,
/select "model2"\.\* from "model2" where "model2"\."model1_id" in (\(5, 6\)|\(6, 5\))/,
/select "someView"\.\* from "someView" where "someView"\."id" = 1/,
/select "someView"\.\* from "someView" where "someView"\."id" in \(2\)/,
/select "someView"\.\* from "someView" where "someView"\."id" in \(7\)/,
/select "someView"\.\*, "Model1Model2"\."extra3" as "aliasedExtra", "Model1Model2"\."model2Id" as "objectiontmpjoin0" from "someView" inner join "Model1Model2" on "someView"\."id" = "Model1Model2"\."model1Id" where "Model1Model2"\."model2Id" in (\(1, 2\)|\(2, 1\))/,
];
expectedQueries.forEach((expectedQuery, i) => {
expect(queries[i]).to.match(expectedQuery);
});
expect(model).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
viewProp: 'hello 1',
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
viewProp: 'hello 2',
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
viewProp: 'hello 5',
aliasedExtra: 'extra 5',
model1Relation1: null,
model1Relation2: [],
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
viewProp: 'hello 6',
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
model1Relation1: {
id: 7,
model1Id: null,
model1Prop1: 'hello 7',
model1Prop2: null,
viewProp: 'hello 7',
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
],
},
],
},
]);
});
});
it('swapping table into a view for an eager query should work (withGraphJoined)', () => {
return Model1.query()
.where('someView.id', 1)
.table('someView')
.withGraphJoined(fullEager)
.then(sortEager)
.then((model) => {
expect(queries.length).to.equal(1);
expect(queries[0].replace(/\s/g, '')).to.equal(
`
select
"someView"."id" as "id",
"someView"."model1Id" as "model1Id",
"someView"."model1Prop1" as "model1Prop1",
"someView"."model1Prop2" as "model1Prop2",
"someView"."viewProp" as "viewProp",
"model1Relation1"."id" as "model1Relation1:id",
"model1Relation1"."model1Id" as "model1Relation1:model1Id",
"model1Relation1"."model1Prop1" as "model1Relation1:model1Prop1",
"model1Relation1"."model1Prop2" as "model1Relation1:model1Prop2",
"model1Relation1"."viewProp" as "model1Relation1:viewProp",
"model1Relation2"."id_col" as "model1Relation2:id_col",
"model1Relation2"."model1_id" as "model1Relation2:model1_id",
"model1Relation2"."model2_prop1" as "model1Relation2:model2_prop1",
"model1Relation2"."model2_prop2" as "model1Relation2:model2_prop2",
"model1Relation2:model2Relation1"."id" as "model1Relation2:model2Relation1:id",
"model1Relation2:model2Relation1"."model1Id" as "model1Relation2:model2Relation1:model1Id",
"model1Relation2:model2Relation1"."model1Prop1" as "model1Relation2:model2Relation1:model1Prop1",
"model1Relation2:model2Relation1"."model1Prop2" as "model1Relation2:model2Relation1:model1Prop2",
"model1Relation2:model2Relation1"."viewProp" as "model1Relation2:model2Relation1:viewProp",
"model1Relation2:model2Relation1_join"."extra3" as "model1Relation2:model2Relation1:aliasedExtra",
"model1Relation2:model2Relation1:model1Relation1"."id" as "model1Relation2:model2Relation1:model1Relation1:id",
"model1Relation2:model2Relation1:model1Relation1"."model1Id" as "model1Relation2:model2Relation1:model1Relation1:model1Id",
"model1Relation2:model2Relation1:model1Relation1"."model1Prop1" as "model1Relation2:model2Relation1:model1Relation1:model1Prop1",
"model1Relation2:model2Relation1:model1Relation1"."model1Prop2" as "model1Relation2:model2Relation1:model1Relation1:model1Prop2",
"model1Relation2:model2Relation1:model1Relation1"."viewProp" as "model1Relation2:model2Relation1:model1Relation1:viewProp",
"model1Relation2:model2Relation1:model1Relation2"."id_col" as "model1Relation2:model2Relation1:model1Relation2:id_col",
"model1Relation2:model2Relation1:model1Relation2"."model1_id" as "model1Relation2:model2Relation1:model1Relation2:model1_id",
"model1Relation2:model2Relation1:model1Relation2"."model2_prop1" as "model1Relation2:model2Relation1:model1Relation2:model2_prop1",
"model1Relation2:model2Relation1:model1Relation2"."model2_prop2" as "model1Relation2:model2Relation1:model1Relation2:model2_prop2"
from
"someView"
left join
"someView" as "model1Relation1" on "model1Relation1"."id" = "someView"."model1Id"
left join
"model2" as "model1Relation2" on "model1Relation2"."model1_id" = "someView"."id"
left join
"Model1Model2" as "model1Relation2:model2Relation1_join" on "model1Relation2:model2Relation1_join"."model2Id" = "model1Relation2"."id_col"
left join
"someView" as "model1Relation2:model2Relation1" on "model1Relation2:model2Relation1_join"."model1Id" = "model1Relation2:model2Relation1"."id"
left join
"someView" as "model1Relation2:model2Relation1:model1Relation1" on "model1Relation2:model2Relation1:model1Relation1"."id" = "model1Relation2:model2Relation1"."model1Id"
left join
"model2" as "model1Relation2:model2Relation1:model1Relation2" on "model1Relation2:model2Relation1:model1Relation2"."model1_id" = "model1Relation2:model2Relation1"."id"
where
"someView"."id" = 1
`.replace(/\s/g, ''),
);
// This makes sure, `Model1` and `someView` have different metadata.
expect(Array.from(Model1.$$tableMetadata.keys()).sort()).to.eql([
'Model1',
'someView',
]);
expect(model).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
viewProp: 'hello 1',
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
viewProp: 'hello 2',
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
viewProp: 'hello 5',
aliasedExtra: 'extra 5',
model1Relation1: null,
model1Relation2: [],
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
viewProp: 'hello 6',
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
model1Relation1: {
id: 7,
model1Id: null,
model1Prop1: 'hello 7',
model1Prop2: null,
viewProp: 'hello 7',
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
],
},
],
},
]);
});
});
it('swapping table into a view for an eager query with filters should work (withGraphJoined)', () => {
return Model1.query()
.where('someView.id', 1)
.table('someView')
.withGraphJoined(fullEager)
.modifyGraph('model1Relation1', (builder) => builder.select('someView.id'))
.modifyGraph('model1Relation2.model2Relation1', (builder) =>
builder.select('someView.id'),
)
.then(sortEager)
.then(() => {
expect(queries.length).to.equal(1);
expect(queries[0].replace(/\s/g, '')).to.equal(
`
select
"someView"."id" as "id",
"someView"."model1Id" as "model1Id",
"someView"."model1Prop1" as "model1Prop1",
"someView"."model1Prop2" as "model1Prop2",
"someView"."viewProp" as "viewProp",
"model1Relation1"."id" as "model1Relation1:id",
"model1Relation2"."id_col" as "model1Relation2:id_col",
"model1Relation2"."model1_id" as "model1Relation2:model1_id",
"model1Relation2"."model2_prop1" as "model1Relation2:model2_prop1",
"model1Relation2"."model2_prop2" as "model1Relation2:model2_prop2",
"model1Relation2:model2Relation1"."id" as "model1Relation2:model2Relation1:id",
"model1Relation2:model2Relation1:model1Relation1"."id" as "model1Relation2:model2Relation1:model1Relation1:id",
"model1Relation2:model2Relation1:model1Relation1"."model1Id" as "model1Relation2:model2Relation1:model1Relation1:model1Id",
"model1Relation2:model2Relation1:model1Relation1"."model1Prop1" as "model1Relation2:model2Relation1:model1Relation1:model1Prop1",
"model1Relation2:model2Relation1:model1Relation1"."model1Prop2" as "model1Relation2:model2Relation1:model1Relation1:model1Prop2",
"model1Relation2:model2Relation1:model1Relation1"."viewProp" as "model1Relation2:model2Relation1:model1Relation1:viewProp",
"model1Relation2:model2Relation1:model1Relation2"."id_col" as "model1Relation2:model2Relation1:model1Relation2:id_col",
"model1Relation2:model2Relation1:model1Relation2"."model1_id" as "model1Relation2:model2Relation1:model1Relation2:model1_id",
"model1Relation2:model2Relation1:model1Relation2"."model2_prop1" as "model1Relation2:model2Relation1:model1Relation2:model2_prop1",
"model1Relation2:model2Relation1:model1Relation2"."model2_prop2" as "model1Relation2:model2Relation1:model1Relation2:model2_prop2"
from
"someView"
left join
(select "someView"."id" from "someView") as "model1Relation1" on "model1Relation1"."id" = "someView"."model1Id"
left join
"model2" as "model1Relation2" on "model1Relation2"."model1_id" = "someView"."id"
left join
"Model1Model2" as "model1Relation2:model2Relation1_join" on "model1Relation2:model2Relation1_join"."model2Id" = "model1Relation2"."id_col"
left join
(select "someView"."id", "someView"."model1Id" from "someView") as "model1Relation2:model2Relation1" on "model1Relation2:model2Relation1_join"."model1Id" = "model1Relation2:model2Relation1"."id"
left join
"someView" as "model1Relation2:model2Relation1:model1Relation1" on "model1Relation2:model2Relation1:model1Relation1"."id" = "model1Relation2:model2Relation1"."model1Id"
left join
"model2" as "model1Relation2:model2Relation1:model1Relation2" on "model1Relation2:model2Relation1:model1Relation2"."model1_id" = "model1Relation2:model2Relation1"."id"
where
"someView"."id" = 1
`.replace(/\s/g, ''),
);
});
});
});
}
});
};
function sortEager(models) {
let mods = models;
if (!Array.isArray(mods)) {
mods = [mods];
}
mods.forEach((model) => {
if (model.model1Relation2) {
model.model1Relation2 = _.sortBy(model.model1Relation2, 'idCol');
}
if (model.model1Relation2[1].model2Relation1) {
model.model1Relation2[1].model2Relation1 = _.sortBy(
model.model1Relation2[1].model2Relation1,
'id',
);
}
});
return models;
}
================================================
FILE: tests/integration/withGraph.js
================================================
const _ = require('lodash');
const chai = require('chai');
const expect = require('expect.js');
const Promise = require('bluebird');
const { ValidationError, raw } = require('../..');
const mockKnexFactory = require('../../testUtils/mockKnex');
module.exports = (session) => {
const Model1 = session.models.Model1;
const Model2 = session.models.Model2;
describe('Model withGraph queries', () => {
beforeEach(() => {
return session.populate([
{
id: 1,
model1Prop1: 'hello 1',
model1Relation1: {
id: 2,
model1Prop1: 'hello 2',
model1Relation1: {
id: 3,
model1Prop1: 'hello 3',
model1Relation1: {
id: 4,
model1Prop1: 'hello 4',
model1Relation2: [
{
idCol: 4,
model2Prop1: 'hejsan 4',
},
],
},
},
},
model1Relation2: [
{
idCol: 1,
model2Prop1: 'hejsan 1',
model2Relation2: {
id: 8,
model1Prop1: 'hello 8',
model1Relation1: {
id: 9,
model1Prop1: 'hello 9',
},
},
},
{
idCol: 2,
model2Prop1: 'hejsan 2',
model2Relation1: [
{
id: 5,
model1Prop1: 'hello 5',
aliasedExtra: 'extra 5',
},
{
id: 6,
model1Prop1: 'hello 6',
aliasedExtra: 'extra 6',
model1Relation1: {
id: 7,
model1Prop1: 'hello 7',
},
model1Relation2: [
{
idCol: 3,
model2Prop1: 'hejsan 3',
},
],
},
],
},
],
},
]);
});
test('model1Relation1', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
},
]);
expect(models[0]).to.be.a(Model1);
expect(models[0].model1Relation1).to.be.a(Model1);
});
test({ model1Relation1: true }, (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
},
]);
expect(models[0]).to.be.a(Model1);
expect(models[0].model1Relation1).to.be.a(Model1);
});
test('model1Relation1(select:model1Prop1)', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
model1Prop1: 'hello 2',
$afterFindCalled: 1,
},
},
]);
expect(models[0]).to.be.a(Model1);
expect(models[0].model1Relation1).to.be.a(Model1);
});
test(
{
model1Relation1: {
$modify: ['select:model1Prop1'],
},
},
(models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
model1Prop1: 'hello 2',
$afterFindCalled: 1,
},
},
]);
expect(models[0]).to.be.a(Model1);
expect(models[0].model1Relation1).to.be.a(Model1);
},
);
test('model1Relation1.model1Relation1', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
model1Prop2: null,
$afterFindCalled: 1,
},
},
},
]);
});
test({ model1Relation1: { model1Relation1: {} } }, (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
model1Prop2: null,
$afterFindCalled: 1,
},
},
},
]);
});
test('model1Relation1.model1Relation1Inverse', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1Inverse: {
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
},
},
},
]);
});
test(
'model1Relation1.^',
(models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 4,
model1Id: null,
model1Prop1: 'hello 4',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: null,
},
},
},
},
]);
},
{ disableJoin: true },
);
test('model1Relation1.^2', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
model1Prop2: null,
$afterFindCalled: 1,
},
},
},
]);
});
test(
{
aliased1: {
$relation: 'model1Relation1',
$recursive: 2,
},
},
(models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
aliased1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
aliased1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
model1Prop2: null,
$afterFindCalled: 1,
},
},
},
]);
},
);
test(
'model1Relation1(selectId).^',
(models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
$afterFindCalled: 1,
model1Relation1: {
id: 4,
$afterFindCalled: 1,
model1Id: null,
model1Relation1: null,
},
},
},
},
]);
},
{
filters: {
selectId: (builder) => {
builder.select('id', 'model1Id');
},
},
disableJoin: true,
},
);
test(
'model1Relation1(selectId).^4',
(models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
model1Prop1: 'hello 2',
$afterFindCalled: 1,
model1Relation1: {
model1Prop1: 'hello 3',
$afterFindCalled: 1,
model1Relation1: {
model1Prop1: 'hello 4',
$afterFindCalled: 1,
model1Relation1: null,
},
},
},
},
]);
},
{
filters: {
selectId: (builder) => {
builder.select('model1Prop1');
},
},
disableWhereIn: true,
eagerOptions: { minimize: true },
},
);
test('model1Relation2.model2Relation2', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation2: {
id: 8,
model1Id: 9,
model1Prop1: 'hello 8',
model1Prop2: null,
$afterFindCalled: 1,
},
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation2: null,
},
],
},
]);
expect(models[0]).to.be.a(Model1);
expect(models[0].model1Relation2[0].model2Relation2).to.be.a(Model1);
});
test('model1Relation2.model2Relation2.model1Relation1', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation2: {
id: 8,
model1Id: 9,
model1Prop1: 'hello 8',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 9,
model1Id: null,
model1Prop1: 'hello 9',
model1Prop2: null,
$afterFindCalled: 1,
},
},
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation2: null,
},
],
},
]);
expect(models[0]).to.be.a(Model1);
expect(models[0].model1Relation2[0].model2Relation2.model1Relation1).to.be.a(Model1);
});
test('[model1Relation1, model1Relation2]', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
]);
expect(models[0]).to.be.a(Model1);
expect(models[0].model1Relation2[0]).to.be.a(Model2);
});
test(
'[model1Relation1, model1Relation2(orderByDesc, selectProps)]',
(models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
$afterFindCalled: 1,
},
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
$afterFindCalled: 1,
},
],
},
]);
},
{
filters: {
selectProps: (builder) => {
builder.select('id_col', 'model1_id', 'model2_prop1');
},
orderByDesc: (builder) => {
builder.orderBy('model2_prop1', 'desc');
},
},
disableJoin: true,
disableSort: true,
},
);
test('[model1Relation1, model1Relation2.model2Relation1]', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
aliasedExtra: 'extra 5',
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
test('[model1Relation2.model2Relation1, model1Relation1]', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
aliasedExtra: 'extra 5',
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
test('[model1Relation1, model1Relation2.model2Relation1.[model1Relation1, model1Relation2]]', (models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
aliasedExtra: 'extra 5',
model1Relation1: null,
model1Relation2: [],
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
model1Relation1: {
id: 7,
model1Id: null,
model1Prop1: 'hello 7',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
],
},
],
},
]);
});
// This tests the Model.modifiers feature.
test(
`[
model1Relation1(select:id, localModifier),
model1Relation2.[
model2Relation1(select:model1Prop1).[
model1Relation1(select:id, select:model1Prop1, select:model1Prop1Aliased),
model1Relation2
]
]
]`,
(models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
model1Prop1: 'hello 5',
$afterFindCalled: 1,
model1Relation1: null,
model1Relation2: [],
},
{
model1Prop1: 'hello 6',
$afterFindCalled: 1,
model1Relation1: {
id: 7,
model1Prop1: 'hello 7',
aliasedInFilter: 'hello 7',
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
],
},
],
},
]);
},
{
filters: {
localModifier: (builder) => builder.select('model1Prop2'),
},
},
);
it('should fail fast on incorrect table name', function (done) {
Model1.query()
.findById(1)
.withGraphJoined('model1Relation111')
.then(_.noop)
.catch((err) => {
expect(err.message).to.equal(
'unknown relation "model1Relation111" in a relation expression',
);
done();
});
});
it('setting maxBatchSize option to 1 should cause relations to be fetched naively to each parent separately', () => {
return Model1.query()
.withGraphFetched('model1Relation2', { maxBatchSize: 1 })
.whereExists(Model1.relatedQuery('model1Relation2'))
.modifyGraph('model1Relation2', (query) => {
// This works only because we set `maxBatchSize` to 1.
query.limit(1).orderBy('id_col');
})
.orderBy('id')
.then((result) => {
expect(result).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
},
],
$afterFindCalled: 1,
},
{
id: 4,
model1Id: null,
model1Prop1: 'hello 4',
model1Prop2: null,
model1Relation2: [
{
idCol: 4,
model1Id: 4,
model2Prop1: 'hejsan 4',
model2Prop2: null,
$afterFindCalled: 1,
},
],
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
$afterFindCalled: 1,
},
]);
});
});
it('should work with zero id', () => {
return Promise.map(
['withGraphFetched', 'withGraphJoined'],
(method) => {
return session
.populate([
{
id: 0,
model1Prop1: 'hello 0',
model1Relation1: {
id: 1,
model1Prop1: 'hello 1',
},
model1Relation2: [
{
idCol: 0,
model2Prop1: 'hejsan 1',
model2Relation1: [
{
id: 2,
model1Prop1: 'hello 2',
},
],
},
],
},
])
.then(() => {
return Model1.query()
.where('Model1.id', 0)
[method]('[model1Relation1, model1Relation2.model2Relation1]');
})
.then((models) => {
expect(models).to.eql([
{
id: 0,
model1Prop1: 'hello 0',
model1Prop2: null,
model1Id: 1,
$afterFindCalled: 1,
model1Relation1: {
id: 1,
model1Prop1: 'hello 1',
model1Prop2: null,
model1Id: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 0,
model2Prop1: 'hejsan 1',
model2Prop2: null,
model1Id: 0,
$afterFindCalled: 1,
model2Relation1: [
{
id: 2,
model1Prop1: 'hello 2',
model1Prop2: null,
aliasedExtra: null,
model1Id: null,
$afterFindCalled: 1,
},
],
},
],
},
]);
});
},
{ concurrency: 1 },
);
});
it('keepImplicitJoinProps', () => {
return Model1.query()
.select('id')
.findById(1)
.withGraphFetched('[model1Relation1, model1Relation2.model2Relation1]')
.internalOptions({ keepImplicitJoinProps: true })
.modifyGraph('model1Relation1', (qb) => qb.select('Model1.id'))
.modifyGraph('model1Relation2', (qb) => qb.select('model2.id_col').orderBy('model2.id_col'))
.modifyGraph('model1Relation2.model2Relation1', (qb) =>
qb.select('Model1.id').orderBy('Model1.id'),
)
.then((res) => {
expect(res).to.eql({
id: 1,
model1Id: 2,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
$afterFindCalled: 1,
model2Relation1: [],
},
{
idCol: 2,
model1Id: 1,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
$afterFindCalled: 1,
objectiontmpjoin0: 2,
},
{
id: 6,
$afterFindCalled: 1,
objectiontmpjoin0: 2,
},
],
},
],
});
});
});
it('range should work', () => {
return Model1.query()
.where('id', 1)
.withGraphFetched('[model1Relation1, model1Relation2]')
.range(0, 0)
.then((res) => {
expect(res.results[0].model1Relation1.id).to.equal(2);
expect(res.results[0].model1Relation2).to.have.length(2);
});
});
it('range should work with joinEager', () => {
return Model1.query()
.where('Model1.id', 1)
.withGraphJoined('model1Relation1')
.range(0, 0)
.then((res) => {
expect(res.results[0].model1Relation1.id).to.equal(2);
});
});
it('eager should not blow up');
it('should be able to call eager from runBefore hook', () => {
return Model1.query()
.runBefore((_, builder) => {
builder.withGraphFetched('model1Relation1');
})
.findOne({ model1Prop1: 'hello 1' })
.then((result) => {
chai.expect(result).to.containSubset({
model1Prop1: 'hello 1',
model1Relation1: {
model1Prop1: 'hello 2',
},
});
});
});
it('should be able to call joinEager from runBefore hook', () => {
return Model1.query()
.runBefore((_, builder) => {
builder.withGraphJoined('model1Relation1');
})
.where('Model1.model1Prop1', 'hello 1')
.first()
.then((result) => {
chai.expect(result).to.containSubset({
model1Prop1: 'hello 1',
model1Relation1: {
model1Prop1: 'hello 2',
},
});
});
});
it('eager should not blow up with an empty eager operation', () => {
return Model1.query()
.modifyGraph('foo', () => {})
.findOne({ model1Prop1: 'hello 1' })
.then((result) => {
expect(result.model1Prop1).to.equal('hello 1');
});
});
it('should be able to order by ambiguous column (issue #1287 regression)', () => {
return Model1.query()
.findOne('Model1.model1Prop1', 'hello 1')
.withGraphJoined('model1Relation1')
.orderBy('id')
.execute();
});
if (session.isPostgres()) {
it('should be able to use a distinctOn trick to fetch one of each related item', async () => {
await session.populate({
id: 1,
model1Prop1: 'root',
model1Relation2: [
{
idCol: 1,
model2Prop1: '1',
model2Relation1: [
{
id: 2,
model1Prop1: '11',
},
{
id: 3,
model1Prop1: '12',
},
],
},
{
idCol: 2,
model2Prop1: '2',
model2Relation1: [
{
id: 4,
model1Prop1: '21',
},
{
id: 5,
model1Prop1: '22',
},
],
},
{
idCol: 3,
model2Prop1: '3',
model2Relation1: [
{
id: 6,
model1Prop1: '31',
},
{
id: 7,
model1Prop1: '32',
},
],
},
],
});
const result = await Model2.query()
.withGraphFetched('model2Relation1(onlyFirst)')
.orderBy('id_col')
.modifiers({
onlyFirst(query) {
query
.orderBy(['model2Id', { column: 'model1Prop1', order: 'desc' }])
.distinctOn('model2Id');
},
});
expect(result).to.eql([
{
idCol: 1,
model1Id: 1,
model2Prop1: '1',
model2Prop2: null,
model2Relation1: [
{
id: 3,
model1Id: null,
model1Prop1: '12',
model1Prop2: null,
aliasedExtra: null,
$afterFindCalled: 1,
},
],
$afterFindCalled: 1,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: '2',
model2Prop2: null,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: '22',
model1Prop2: null,
aliasedExtra: null,
$afterFindCalled: 1,
},
],
$afterFindCalled: 1,
},
{
idCol: 3,
model1Id: 1,
model2Prop1: '3',
model2Prop2: null,
model2Relation1: [
{
id: 7,
model1Id: null,
model1Prop1: '32',
model1Prop2: null,
aliasedExtra: null,
$afterFindCalled: 1,
},
],
$afterFindCalled: 1,
},
]);
});
}
// TODO: enable for v2.0.
it.skip('should fail with a clear error when a duplicate relation is detected', () => {
expect(() => {
Model1.query().withGraphFetched('[model1Relation1, model1Relation1.model1Relation2]');
}).to.throwException((err) => {
expect(err.message).to.equal(
`Duplicate relation name "model1Relation1" in relation expression "[model1Relation1, model1Relation1.model1Relation2]". Use "a.[b, c]" instead of "[a.b, a.c]".`,
);
});
});
describe('skipFetched option', () => {
let TestModel;
let queries;
beforeEach(() => {
queries = [];
// Create a dummy mock so that we can bind Model1 to it.
TestModel = Model1.bindKnex(
mockKnexFactory(session.knex, function (mock, oldImpl, args) {
queries.push(this.toSQL());
return oldImpl.apply(this, args);
}),
);
});
it('should not fetch an existing relation when `skipFetched` option is true', async () => {
const models = await TestModel.query().withGraphFetched('model1Relation1');
queries = [];
const result = await TestModel.fetchGraph(models, 'model1Relation1', { skipFetched: true });
expect(queries).to.have.length(0);
expect(models).to.eql(result);
});
it('should not fetch an existing nested relation when `skipFetched` option is true', async () => {
let result = await TestModel.query()
.withGraphFetched('model1Relation1')
.whereIn('id', [1, 2, 3]);
queries = [];
result = await TestModel.fetchGraph(result, 'model1Relation1.model1Relation1', {
skipFetched: true,
});
expect(queries).to.have.length(1);
});
it('should fetch an existing relation when `skipFetched` option is true if not all needed relation props exist', async () => {
const model = await TestModel.query().withGraphFetched('model1Relation1').findById(1);
// Deleting this will cause `fetchGraph` to reload model.model1Relation1
// because it needs it to have the `model1Id` present for the next level
// of fetching.
delete model.model1Relation1.model1Id;
queries = [];
await model.$fetchGraph('model1Relation1.model1Relation1', { skipFetched: true });
expect(queries).to.have.length(2);
expect(model).to.eql({
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
$afterFindCalled: 1,
model1Prop2: null,
},
},
});
});
it('should fetch an existing relation when `skipFetched` option is true if not all needed relation props exist (2)', async () => {
const model = await TestModel.query().withGraphFetched('model1Relation1').findById(1);
// Deleting this will cause `fetchGraph` to reload model.model1Relation1
// because it needs it to have the `id` present for the next level
// of fetching.
delete model.model1Relation1.id;
queries = [];
await model.$fetchGraph('model1Relation1.model1Relation2', { skipFetched: true });
expect(queries).to.have.length(2);
});
it('should fetch other relations when `skipFetched` option is true', async () => {
const models = await TestModel.query().withGraphFetched('model1Relation1').where('id', 1);
queries = [];
const result = await TestModel.fetchGraph(
models,
'[model1Relation1.model1Relation1, model1Relation2(orderById)]',
{ skipFetched: true },
);
expect(queries).to.have.length(2);
expect(result).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
$afterFindCalled: 1,
model1Prop2: null,
},
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
$afterFindCalled: 1,
model2Prop2: null,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
$afterFindCalled: 1,
model2Prop2: null,
},
],
},
]);
});
it('should work with nested relations', async () => {
const models = await TestModel.query()
.withGraphFetched('model1Relation1.model1Relation1')
.where('id', 1);
queries = [];
const result = await TestModel.fetchGraph(
models,
'[model1Relation1.model1Relation1, model1Relation2(orderById)]',
{ skipFetched: true },
);
expect(queries).to.have.length(1);
expect(result).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
$afterFindCalled: 1,
model1Prop2: null,
},
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
$afterFindCalled: 1,
model2Prop2: null,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
$afterFindCalled: 1,
model2Prop2: null,
},
],
},
]);
});
it('should work with nested and parallel relations', async () => {
const models = await TestModel.query()
.withGraphFetched('[model1Relation1.model1Relation1, model1Relation2(orderById)]')
.where('id', 1);
queries = [];
const result = await TestModel.fetchGraph(
models,
'[model1Relation1.model1Relation1, model1Relation2]',
{ skipFetched: true },
);
expect(queries).to.have.length(0);
expect(result).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
$afterFindCalled: 1,
model1Prop2: null,
},
},
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
$afterFindCalled: 1,
model2Prop2: null,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
$afterFindCalled: 1,
model2Prop2: null,
},
],
},
]);
});
});
describe('QueryBuilder.withGraphJoined', () => {
it('select should work', () => {
return Model1.query()
.select('Model1.id', 'Model1.model1Prop1')
.where('Model1.id', 1)
.where('model1Relation2.id_col', 2)
.where('model1Relation2:model2Relation1.id', 6)
.withGraphJoined('[model1Relation1, model1Relation2.model2Relation1]')
.then((models) => {
expect(models).to.eql([
{
id: 1,
model1Prop1: 'hello 1',
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
});
// Disabled for sqlite because it doesn't have `concat` function :)
if (!session.isSqlite()) {
it('select * + raw should work', () => {
return Model1.query()
.select(
'Model1.*',
raw(`concat(??, ' - ', ??) as "rawThingy"`, 'Model1.model1Prop1', 'Model1.id'),
)
.where('Model1.id', 1)
.where('model1Relation2.id_col', 2)
.where('model1Relation2:model2Relation1.id', 6)
.withGraphJoined('[model1Relation1, model1Relation2.model2Relation1]')
.then((models) => {
expect(models).to.eql([
{
id: 1,
model1Prop1: 'hello 1',
model1Prop2: null,
model1Id: 2,
rawThingy: 'hello 1 - 1',
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
});
it('raw select should work in modifier', () => {
return Model1.query()
.select('Model1.id')
.where('Model1.id', 1)
.where('model1Relation2.id_col', 2)
.where('model1Relation2:model2Relation1.id', 6)
.withGraphJoined('[model1Relation1(rawStuff), model1Relation2.model2Relation1]')
.modifiers({
rawStuff(builder) {
builder.select(
raw(`concat(??, ' - ', ?? * 2)`, 'model1Prop1', 'id').as('rawThingy'),
);
},
})
.then((models) => {
expect(models).to.eql([
{
id: 1,
$afterFindCalled: 1,
model1Relation1: {
rawThingy: 'hello 2 - 4',
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
});
}
it('select should work with alias', () => {
return Model1.query()
.select('Model1.id as theId', 'Model1.model1Prop1 as leProp')
.where('Model1.id', 1)
.where('model1Relation2.id_col', 2)
.where('model1Relation2:model2Relation1.id', 6)
.withGraphJoined('[model1Relation1, model1Relation2.model2Relation1]')
.then((models) => {
expect(models).to.eql([
{
theId: 1,
leProp: 'hello 1',
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
});
it('should be able to change the join type', () => {
return Model1.query()
.select('Model1.id', 'Model1.model1Prop1')
.withGraphJoined('model1Relation2', { joinOperation: 'innerJoin' })
.orderBy(['Model1.id', 'model1Relation2.id_col'])
.then((models) => {
// With innerJoin we should only get `Model1` instances that have one
// or more `model2Relation2` relations.
expect(models).to.eql([
{
id: 1,
model1Prop1: 'hello 1',
$afterFindCalled: 1,
model1Relation2: [
{
idCol: 1,
model1Id: 1,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
},
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
{
id: 4,
model1Prop1: 'hello 4',
$afterFindCalled: 1,
model1Relation2: [
{
idCol: 4,
model1Id: 4,
model2Prop1: 'hejsan 4',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
{
id: 6,
model1Prop1: 'hello 6',
$afterFindCalled: 1,
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
]);
});
});
it('should be able to change the separator', () => {
return Model1.query()
.select('Model1.id', 'Model1.model1Prop1')
.where('Model1.id', 1)
.where('model1Relation2.id_col', 2)
.where('model1Relation2->model2Relation1.id', 6)
.withGraphJoined('[model1Relation1, model1Relation2.model2Relation1]', {
separator: '->',
})
.then((models) => {
expect(models).to.eql([
{
id: 1,
model1Prop1: 'hello 1',
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
});
it('should be able to refer to joined relations with syntax Table:rel1:rel2.col', () => {
return Model1.query()
.where('Model1.id', 1)
.where('model1Relation2.id_col', 2)
.withGraphJoined('[model1Relation1, model1Relation2.model2Relation1]')
.orderBy(['Model1.id', 'model1Relation2:model2Relation1.id'])
.then((models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
aliasedExtra: 'extra 5',
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
});
it('should be able to give aliases for relations', () => {
return Model1.query()
.where('Model1.id', 1)
.where('mr2.id_col', 2)
.withGraphJoined('[model1Relation1, model1Relation2.model2Relation1]', {
aliases: {
model1Relation2: 'mr2',
},
})
.orderBy(['Model1.id', 'mr2:model2Relation1.id'])
.then((models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
id: 5,
model1Id: null,
model1Prop1: 'hello 5',
model1Prop2: null,
aliasedExtra: 'extra 5',
$afterFindCalled: 1,
},
{
id: 6,
model1Id: 7,
model1Prop1: 'hello 6',
model1Prop2: null,
aliasedExtra: 'extra 6',
$afterFindCalled: 1,
},
],
},
],
},
]);
});
});
it('relation references longer that 63 chars should throw an exception', (done) => {
Model1.query()
.where('Model1.id', 1)
.withGraphJoined('[model1Relation1.model1Relation1.model1Relation1.model1Relation1]')
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(ValidationError);
expect(err.type).to.equal('RelationExpression');
expect(err.modelClass).to.equal(Model1);
expect(err.message).to.equal(
'identifier model1Relation1:model1Relation1:model1Relation1:model1Relation1:id is over 63 characters long and would be truncated by the database engine.',
);
done();
})
.catch(done);
});
it('relation references longer that 63 chars should NOT throw an exception if minimize: true option is given', (done) => {
Model1.query()
.where('Model1.id', 1)
.withGraphJoined('[model1Relation1.model1Relation1.model1Relation1.model1Relation1]', {
minimize: true,
})
.then((models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 2,
model1Id: 3,
model1Prop1: 'hello 2',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 3,
model1Id: 4,
model1Prop1: 'hello 3',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: {
id: 4,
model1Id: null,
model1Prop1: 'hello 4',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation1: null,
},
},
},
},
]);
done();
})
.catch(done);
});
it('infinitely recursive expressions should fail', (done) => {
Model1.query()
.where('Model1.id', 1)
.withGraphJoined('model1Relation1.^')
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.type).to.equal('RelationExpression');
expect(err.message).to.equal(
'recursion depth of eager expression model1Relation1.^ too big for JoinEagerAlgorithm',
);
done();
})
.catch(done);
});
it('should fail if given missing filter', (done) => {
Model1.query()
.where('id', 1)
.withGraphFetched('model1Relation2(missingFilter)')
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err).to.be.a(ValidationError);
expect(err.type).to.equal('RelationExpression');
expect(err.message).to.equal(
'could not find modifier "missingFilter" for relation "model1Relation2"',
);
done();
})
.catch(done);
});
it('should fail if given missing relation', (done) => {
Model1.query()
.where('id', 1)
.withGraphFetched('invalidRelation')
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err).to.be.a(ValidationError);
expect(err.type).to.equal('RelationExpression');
expect(err.message).to.equal(
'unknown relation "invalidRelation" in an eager expression',
);
done();
})
.catch(done);
});
it('should fail if given invalid relation expression', (done) => {
Model1.query()
.where('id', 1)
.withGraphFetched('invalidRelation')
.then(() => {
throw new Error('should not get here');
})
.catch((err) => {
expect(err).to.be.a(ValidationError);
expect(err.type).to.equal('RelationExpression');
expect(err.message).to.equal(
'unknown relation "invalidRelation" in an eager expression',
);
done();
})
.catch(done);
});
});
describe('QueryBuilder.modifyGraph', () => {
it('should filter the eager query using relation expressions as paths', () => {
return Promise.all(
['withGraphFetched', 'withGraphJoined'].map((method) => {
return Model1.query()
.where('Model1.id', 1)
.modifyGraph('model1Relation2.model2Relation1', (builder) => {
builder.where('Model1.id', 6);
})
[method]('model1Relation2.model2Relation1.[model1Relation1, model1Relation2]')
.modifyGraph('model1Relation2', (builder) => {
builder.where('model2_prop1', 'hejsan 2');
})
.then((models) => {
expect(models[0].model1Relation2).to.have.length(1);
expect(models[0].model1Relation2[0].model2Prop1).to.equal('hejsan 2');
expect(models[0].model1Relation2[0].model2Relation1).to.have.length(1);
expect(models[0].model1Relation2[0].model2Relation1[0].id).to.equal(6);
});
}),
);
});
it('should accept a modifier name', () => {
return Promise.all(
['withGraphFetched', 'withGraphJoined'].map((method) => {
return Model1.query()
.where('Model1.id', 1)
[method]('model1Relation2.model2Relation1')
.modifyGraph('model1Relation2.model2Relation1', 'select:model1Prop1')
.then((models) => {
const model2 = models[0].model1Relation2.find((it) => it.idCol === 2);
expect(Object.keys(model2.model2Relation1[0])).to.eql([
'model1Prop1',
'$afterFindCalled',
]);
});
}),
);
});
it('should accept a list of modifier names', () => {
return Promise.all(
['withGraphFetched', 'withGraphJoined'].map((method) => {
return Model1.query()
.where('Model1.id', 1)
[method]('model1Relation1')
.modifyGraph('model1Relation1', ['select:id', 'select:model1Prop1'])
.then((models) => {
expect(Object.keys(models[0].model1Relation1)).to.eql([
'id',
'model1Prop1',
'$afterFindCalled',
]);
});
}),
);
});
it('should implicitly add selects for join columns if they are omitted in modifyGraph', () => {
return Promise.all(
['withGraphFetched', 'withGraphJoined'].map((method) => {
return Model1.query()
.where('Model1.id', 1)
.column('Model1.model1Prop1')
[method]('model1Relation2.model2Relation1.[model1Relation1, model1Relation2]')
.modifyGraph('model1Relation2', (builder) => {
builder.select('model2_prop1');
})
.modifyGraph('model1Relation2.model2Relation1', (builder) => {
builder.distinct('model1Prop1');
})
.modifyGraph('model1Relation2.model2Relation1.model1Relation1', (builder) => {
builder.select('model1Prop1');
})
.modifyGraph('model1Relation2.model2Relation1.model1Relation2', (builder) => {
builder.select('model2_prop1');
})
.then((models) => {
models[0].model1Relation2 = _.sortBy(models[0].model1Relation2, 'model2Prop1');
models[0].model1Relation2[1].model2Relation1 = _.sortBy(
models[0].model1Relation2[1].model2Relation1,
'model1Prop1',
);
expect(models).to.eql([
{
model1Prop1: 'hello 1',
$afterFindCalled: 1,
model1Relation2: [
{
model2Prop1: 'hejsan 1',
$afterFindCalled: 1,
model2Relation1: [],
},
{
model2Prop1: 'hejsan 2',
$afterFindCalled: 1,
model2Relation1: [
{
model1Prop1: 'hello 5',
$afterFindCalled: 1,
model1Relation1: null,
model1Relation2: [],
},
{
model1Prop1: 'hello 6',
$afterFindCalled: 1,
model1Relation1: {
model1Prop1: 'hello 7',
$afterFindCalled: 1,
},
model1Relation2: [
{
model2Prop1: 'hejsan 3',
$afterFindCalled: 1,
},
],
},
],
},
],
},
]);
});
}),
);
});
it('should implicitly add selects for join columns if they are aliased in modifyGraph', () => {
return Model1.query()
.where('Model1.id', 1)
.column('Model1.model1Prop1')
.withGraphFetched('model1Relation2.model2Relation1.[model1Relation1, model1Relation2]')
.modifyGraph('model1Relation2', (builder) => {
builder.select('model2_prop1', 'id_col as x1', 'model1_id as x2');
})
.modifyGraph('model1Relation2.model2Relation1', (builder) => {
builder.select('model1Prop1', 'Model1.id as y1', 'Model1.model1Id as y2');
})
.modifyGraph('model1Relation2.model2Relation1.model1Relation1', (builder) => {
builder.select('model1Prop1', 'Model1.id as y1', 'Model1.model1Id as y2');
})
.modifyGraph('model1Relation2.model2Relation1.model1Relation2', (builder) => {
builder.select('model2_prop1', 'id_col as x1', 'model1_id as x2');
})
.then((models) => {
models[0].model1Relation2 = _.sortBy(models[0].model1Relation2, 'model2Prop1');
models[0].model1Relation2[1].model2Relation1 = _.sortBy(
models[0].model1Relation2[1].model2Relation1,
'model1Prop1',
);
expect(models).to.eql([
{
model1Prop1: 'hello 1',
$afterFindCalled: 1,
model1Relation2: [
{
model2Prop1: 'hejsan 1',
$afterFindCalled: 1,
x1: 1,
x2: 1,
model2Relation1: [],
},
{
model2Prop1: 'hejsan 2',
$afterFindCalled: 1,
x1: 2,
x2: 1,
model2Relation1: [
{
model1Prop1: 'hello 5',
$afterFindCalled: 1,
y1: 5,
y2: null,
model1Relation1: null,
model1Relation2: [],
},
{
model1Prop1: 'hello 6',
$afterFindCalled: 1,
y1: 6,
y2: 7,
model1Relation1: {
model1Prop1: 'hello 7',
$afterFindCalled: 1,
y1: 7,
y2: null,
},
model1Relation2: [
{
model2Prop1: 'hejsan 3',
$afterFindCalled: 1,
x1: 3,
x2: 6,
},
],
},
],
},
],
},
]);
});
});
it('should filter the eager query using relation expressions as paths (withGraphJoined)', () => {
return Model1.query()
.where('Model1.id', 1)
.modifyGraph('model1Relation2.model2Relation1', (builder) => {
builder.where('id', 6);
})
.withGraphJoined('model1Relation2.model2Relation1.[model1Relation1, model1Relation2]')
.modifyGraph('model1Relation2', (builder) => {
builder.where('model2_prop1', 'hejsan 2');
})
.modifyGraph('model1Relation2.model2Relation1', (builder) => {
builder.select('model1Prop1');
})
.then((models) => {
expect(models).to.eql([
{
id: 1,
model1Id: 2,
model1Prop1: 'hello 1',
model1Prop2: null,
$afterFindCalled: 1,
model1Relation2: [
{
idCol: 2,
model1Id: 1,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
model1Prop1: 'hello 6',
$afterFindCalled: 1,
model1Relation1: {
id: 7,
model1Id: null,
model1Prop1: 'hello 7',
model1Prop2: null,
$afterFindCalled: 1,
},
model1Relation2: [
{
idCol: 3,
model1Id: 6,
model2Prop1: 'hejsan 3',
model2Prop2: null,
$afterFindCalled: 1,
},
],
},
],
},
],
},
]);
});
});
});
it('should merge eager expressions', () => {
return Model1.query()
.where('id', 1)
.withGraphFetched('model1Relation2')
.withGraphFetched('model1Relation2.model2Relation1.model1Relation1')
.withGraphFetched('model1Relation2.model2Relation1.model1Relation2')
.first()
.modifyGraph('model1Relation2', (builder) => {
builder.orderBy('id_col');
})
.modifyGraph('model1Relation2.model2Relation1', (builder) => {
builder.orderBy('id');
})
.then((model) => {
chai.expect(model.toJSON()).to.containSubset({
id: 1,
model1Relation2: [
{
idCol: 1,
model2Relation1: [],
},
{
idCol: 2,
model2Relation1: [
{
id: 5,
aliasedExtra: 'extra 5',
model1Relation1: null,
model1Relation2: [],
},
{
id: 6,
aliasedExtra: 'extra 6',
model1Relation1: {
id: 7,
},
model1Relation2: [
{
idCol: 3,
},
],
},
],
},
],
});
});
});
it('should merge eager expressions and modifiers', () => {
return Model1.query()
.where('id', 1)
.withGraphFetched('model1Relation2')
.withGraphFetched('model1Relation2(f1).model2Relation1.model1Relation1')
.modifiers({
f1: (builder) => {
builder.orderBy('id_col');
},
})
.withGraphFetched('model1Relation2.model2Relation1(f2).model1Relation2')
.modifiers({
f2: (builder) => {
builder.orderBy('id');
},
})
.first()
.then((model) => {
chai.expect(model.toJSON()).to.containSubset({
id: 1,
model1Relation2: [
{
idCol: 1,
model2Relation1: [],
},
{
idCol: 2,
model2Relation1: [
{
id: 5,
aliasedExtra: 'extra 5',
model1Relation1: null,
model1Relation2: [],
},
{
id: 6,
aliasedExtra: 'extra 6',
model1Relation1: {
id: 7,
},
model1Relation2: [
{
idCol: 3,
},
],
},
],
},
],
});
});
});
describe('QueryBuilder.orderBy', () => {
it('orderBy should work for the root query', () => {
return Promise.map(['withGraphFetched', 'withGraphJoined'], (method) => {
return Model1.query()
.select('Model1.model1Prop1')
.modifyGraph('model1Relation1', (builder) => {
builder.select('model1Prop1');
})
[method]('model1Relation1')
.orderBy('Model1.model1Prop1', 'DESC')
.whereNotNull('Model1.model1Id')
.then((models) => {
expect(models).to.eql([
{
model1Prop1: 'hello 8',
model1Relation1: { model1Prop1: 'hello 9', $afterFindCalled: 1 },
$afterFindCalled: 1,
},
{
model1Prop1: 'hello 6',
model1Relation1: { model1Prop1: 'hello 7', $afterFindCalled: 1 },
$afterFindCalled: 1,
},
{
model1Prop1: 'hello 3',
model1Relation1: { model1Prop1: 'hello 4', $afterFindCalled: 1 },
$afterFindCalled: 1,
},
{
model1Prop1: 'hello 2',
model1Relation1: { model1Prop1: 'hello 3', $afterFindCalled: 1 },
$afterFindCalled: 1,
},
{
model1Prop1: 'hello 1',
model1Relation1: { model1Prop1: 'hello 2', $afterFindCalled: 1 },
$afterFindCalled: 1,
},
]);
});
});
});
});
describe('Multiple parents + ManyToManyRelation', () => {
beforeEach(() => {
return Model2.query().insertGraph([
{
idCol: 100,
model2Prop1: 'hejsan 1',
model2Relation1: [
{
id: 500,
model1Prop1: 'hello 5',
},
{
id: 600,
model1Prop1: 'hello 6',
},
],
},
{
idCol: 200,
model2Prop1: 'hejsan 2',
model2Relation1: [
{
id: 700,
model1Prop1: 'hello 7',
},
{
id: 800,
model1Prop1: 'hello 8',
},
],
},
]);
});
it('should work with withGraphFetched', () => {
return Model2.query()
.whereIn('id_col', [100, 200])
.orderBy('id_col')
.withGraphFetched('model2Relation1(select, orderById)')
.modifiers({
select: (b) => b.select('model1Prop1'),
})
.then((models) => {
expect(models).to.eql([
{
idCol: 100,
model1Id: null,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
model1Prop1: 'hello 5',
$afterFindCalled: 1,
},
{
model1Prop1: 'hello 6',
$afterFindCalled: 1,
},
],
},
{
idCol: 200,
model1Id: null,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
model1Prop1: 'hello 7',
$afterFindCalled: 1,
},
{
model1Prop1: 'hello 8',
$afterFindCalled: 1,
},
],
},
]);
});
});
it('should work with withGraphJoined', () => {
return Model2.query()
.whereIn('id_col', [100, 200])
.orderBy(['id_col', 'model2Relation1.model1Prop1'])
.modifiers({
select: (b) => b.select('model1Prop1'),
})
.withGraphJoined('model2Relation1(select)')
.then((models) => {
expect(models).to.eql([
{
idCol: 100,
model1Id: null,
model2Prop1: 'hejsan 1',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
model1Prop1: 'hello 5',
$afterFindCalled: 1,
},
{
model1Prop1: 'hello 6',
$afterFindCalled: 1,
},
],
},
{
idCol: 200,
model1Id: null,
model2Prop1: 'hejsan 2',
model2Prop2: null,
$afterFindCalled: 1,
model2Relation1: [
{
model1Prop1: 'hello 7',
$afterFindCalled: 1,
},
{
model1Prop1: 'hello 8',
$afterFindCalled: 1,
},
],
},
]);
});
});
});
describe('Same ManyToMany child for multiple parents + extras', () => {
beforeEach(() => {
return Model2.query().insertGraph(
[
{
idCol: 100,
model2Prop1: 'hejsan 1',
model2Relation1: [
{
id: 500,
model1Prop1: 'hello 5',
},
{
'#id': 'shared',
id: 600,
model1Prop1: 'hello 6',
aliasedExtra: 'lol1',
},
],
},
{
idCol: 200,
model2Prop1: 'hejsan 2',
model2Relation1: [
{
'#ref': 'shared',
aliasedExtra: 'lol2',
},
{
id: 700,
model1Prop1: 'hello 7',
},
],
},
],
{ allowRefs: true },
);
});
it('test', () => {
return Model2.query()
.whereIn('id_col', [100, 200])
.orderBy('id_col')
.withGraphFetched('model2Relation1(orderById)')
.then((result) => {
chai.expect(result).to.containSubset([
{
idCol: 100,
model2Relation1: [
{
id: 500,
aliasedExtra: null,
},
{
id: 600,
aliasedExtra: 'lol1',
},
],
},
{
idCol: 200,
model2Relation1: [
{
id: 600,
aliasedExtra: 'lol2',
},
{
id: 700,
aliasedExtra: null,
},
],
},
]);
});
});
});
describe('aliases', () => {
it('aliases in eager expressions should work', () => {
return Promise.map(
['withGraphFetched', 'withGraphJoined'],
(method) => {
return Model1.query()
.where('Model1.id', 1)
.select('Model1.id')
[method](
`[
model1Relation1(f1) as a,
model1Relation2(f2) as b .[
model2Relation1(f1) as c,
model2Relation1(f1) as d
]
]`,
)
.modifiers({
f1: (builder) => builder.select('Model1.id'),
f2: (builder) => builder.select('model2.id_col'),
})
.first()
.then((model) => {
model.b = _.sortBy(model.b, 'idCol');
model.b[1].c = _.sortBy(model.b[1].c, 'id');
model.b[1].d = _.sortBy(model.b[1].d, 'id');
expect(model).to.eql({
id: 1,
$afterFindCalled: 1,
a: {
id: 2,
$afterFindCalled: 1,
},
b: [
{
idCol: 1,
$afterFindCalled: 1,
c: [],
d: [],
},
{
idCol: 2,
$afterFindCalled: 1,
c: [
{
id: 5,
$afterFindCalled: 1,
},
{
id: 6,
$afterFindCalled: 1,
},
],
d: [
{
id: 5,
$afterFindCalled: 1,
},
{
id: 6,
$afterFindCalled: 1,
},
],
},
],
});
});
},
{ concurrency: 1 },
);
});
it('aliases should alias the joined tables when using withGraphJoined', () => {
return Model1.query()
.findById(1)
.select('Model1.id')
.withGraphJoined(
`[
model1Relation1(f1) as a,
model1Relation2(f2) as b .[
model2Relation1(f1) as c,
model2Relation1(f1) as d
]
]`,
)
.modifiers({
f1: (builder) => builder.select('Model1.id'),
f2: (builder) => builder.select('model2.id_col'),
})
.where('b:d.id', 6)
.then((model) => {
model.b = _.sortBy(model.b, 'idCol');
model.b[0].c = _.sortBy(model.b[0].c, 'id');
model.b[0].d = _.sortBy(model.b[0].d, 'id');
expect(model).to.eql({
id: 1,
$afterFindCalled: 1,
a: {
id: 2,
$afterFindCalled: 1,
},
b: [
{
idCol: 2,
$afterFindCalled: 1,
c: [
{
id: 5,
$afterFindCalled: 1,
},
{
id: 6,
$afterFindCalled: 1,
},
],
d: [
{
id: 6,
$afterFindCalled: 1,
},
],
},
],
});
});
});
it('alias method should work', () => {
return Promise.map(
['withGraphFetched', 'withGraphJoined'],
(method) => {
return Model1.query()
.alias('m1')
.select('m1.id')
[method](`[model1Relation1(f1) as a]`)
.modifiers({
f1: (builder) => builder.select('id'),
})
.findOne({ 'm1.id': 1 })
.then((model) => {
expect(model).to.eql({
id: 1,
$afterFindCalled: 1,
a: {
id: 2,
$afterFindCalled: 1,
},
});
});
},
{ concurrency: 1 },
);
});
});
if (session.isPostgres()) {
describe('generated sql', () => {
let sql = [];
let mockKnex = null;
before(() => {
mockKnex = mockKnexFactory(session.knex, function (mock, then, args) {
sql.push(this.toString());
return then.apply(this, args);
});
});
beforeEach(() => {
sql = [];
});
it('check withGraphFetched generated SQL', () => {
return Model1.bindKnex(mockKnex)
.query()
.withGraphFetched(
'[model1Relation1, model1Relation1Inverse, model1Relation2.[model2Relation1, model2Relation2], model1Relation3]',
)
.context({
onBuild(builder) {
if (builder.modelClass().name === 'Model2') {
builder.orderBy('id_col');
} else {
builder.orderBy('id');
}
},
})
.then(() => {
expect(sql).to.eql([
'select "Model1".* from "Model1" order by "id" asc',
'select "Model1".* from "Model1" where "Model1"."id" in (2, 3, 4, 7, 9) order by "id" asc',
'select "Model1".* from "Model1" where "Model1"."model1Id" in (1, 2, 3, 4, 5, 6, 7, 8, 9) order by "id" asc',
'select "model2".* from "model2" where "model2"."model1_id" in (1, 2, 3, 4, 5, 6, 7, 8, 9) order by "id_col" asc',
'select "model2".*, "Model1Model2"."extra1" as "extra1", "Model1Model2"."extra2" as "extra2", "Model1Model2"."model1Id" as "objectiontmpjoin0" from "model2" inner join "Model1Model2" on "model2"."id_col" = "Model1Model2"."model2Id" where "Model1Model2"."model1Id" in (1, 2, 3, 4, 5, 6, 7, 8, 9) order by "id_col" asc',
'select "Model1".*, "Model1Model2"."extra3" as "aliasedExtra", "Model1Model2"."model2Id" as "objectiontmpjoin0" from "Model1" inner join "Model1Model2" on "Model1"."id" = "Model1Model2"."model1Id" where "Model1Model2"."model2Id" in (1, 2, 3, 4) order by "id" asc',
'select "Model1".*, "Model1Model2One"."model2Id" as "objectiontmpjoin0" from "Model1" inner join "Model1Model2One" on "Model1"."id" = "Model1Model2One"."model1Id" where "Model1Model2One"."model2Id" in (1, 2, 3, 4) order by "id" asc',
]);
});
});
it('should not execute queries when an empty relation set is encoutered', () => {
return Model1.bindKnex(mockKnex)
.query()
.findById(4)
.withGraphFetched('model1Relation1')
.then((res) => {
expect(sql).to.have.length(1);
expect(res.id).to.equal(4);
});
});
it('check withGraphJoined generated SQL', () => {
return Model1.bindKnex(mockKnex)
.query()
.withGraphJoined(
'[model1Relation1, model1Relation1Inverse, model1Relation2.[model2Relation1, model2Relation2], model1Relation3]',
)
.then(() => {
expect(_.last(sql).replace(/\s/g, '')).to.equal(
`
select
"Model1"."id" as "id",
"Model1"."model1Id" as "model1Id",
"Model1"."model1Prop1" as "model1Prop1",
"Model1"."model1Prop2" as "model1Prop2",
"model1Relation1"."id" as "model1Relation1:id",
"model1Relation1"."model1Id" as "model1Relation1:model1Id",
"model1Relation1"."model1Prop1" as "model1Relation1:model1Prop1",
"model1Relation1"."model1Prop2" as "model1Relation1:model1Prop2",
"model1Relation1Inverse"."id" as "model1Relation1Inverse:id",
"model1Relation1Inverse"."model1Id" as "model1Relation1Inverse:model1Id",
"model1Relation1Inverse"."model1Prop1" as "model1Relation1Inverse:model1Prop1",
"model1Relation1Inverse"."model1Prop2" as "model1Relation1Inverse:model1Prop2",
"model1Relation2"."id_col" as "model1Relation2:id_col",
"model1Relation2"."model1_id" as "model1Relation2:model1_id",
"model1Relation2"."model2_prop1" as "model1Relation2:model2_prop1",
"model1Relation2"."model2_prop2" as "model1Relation2:model2_prop2",
"model1Relation2:model2Relation1"."id" as "model1Relation2:model2Relation1:id",
"model1Relation2:model2Relation1"."model1Id" as "model1Relation2:model2Relation1:model1Id",
"model1Relation2:model2Relation1"."model1Prop1" as "model1Relation2:model2Relation1:model1Prop1",
"model1Relation2:model2Relation1"."model1Prop2" as "model1Relation2:model2Relation1:model1Prop2",
"model1Relation2:model2Relation1_join"."extra3" as "model1Relation2:model2Relation1:aliasedExtra",
"model1Relation2:model2Relation2"."id" as "model1Relation2:model2Relation2:id",
"model1Relation2:model2Relation2"."model1Id" as "model1Relation2:model2Relation2:model1Id",
"model1Relation2:model2Relation2"."model1Prop1" as "model1Relation2:model2Relation2:model1Prop1",
"model1Relation2:model2Relation2"."model1Prop2" as "model1Relation2:model2Relation2:model1Prop2",
"model1Relation3"."id_col" as "model1Relation3:id_col",
"model1Relation3"."model1_id" as "model1Relation3:model1_id",
"model1Relation3"."model2_prop1" as "model1Relation3:model2_prop1",
"model1Relation3"."model2_prop2" as "model1Relation3:model2_prop2",
"model1Relation3_join"."extra1" as "model1Relation3:extra1",
"model1Relation3_join"."extra2" as "model1Relation3:extra2"
from
"Model1"
left join
"Model1" as "model1Relation1" on "model1Relation1"."id" = "Model1"."model1Id"
left join
"Model1" as "model1Relation1Inverse" on "model1Relation1Inverse"."model1Id" = "Model1"."id"
left join
"model2" as "model1Relation2" on "model1Relation2"."model1_id" = "Model1"."id"
left join
"Model1Model2" as "model1Relation2:model2Relation1_join" on "model1Relation2:model2Relation1_join"."model2Id" = "model1Relation2"."id_col"
left join
"Model1" as "model1Relation2:model2Relation1" on "model1Relation2:model2Relation1_join"."model1Id" = "model1Relation2:model2Relation1"."id"
left join
"Model1Model2One" as "model1Relation2:model2Relation2_join" on "model1Relation2:model2Relation2_join"."model2Id" = "model1Relation2"."id_col"
left join
"Model1" as "model1Relation2:model2Relation2" on "model1Relation2:model2Relation2_join"."model1Id" = "model1Relation2:model2Relation2"."id"
left join
"Model1Model2" as "model1Relation3_join" on "model1Relation3_join"."model1Id" = "Model1"."id"
left join
"model2" as "model1Relation3" on "model1Relation3_join"."model2Id" = "model1Relation3"."id_col"
`.replace(/\s/g, ''),
);
});
});
});
}
if (session.isPostgres())
describe.skip('big data', () => {
let graph = null;
before(function () {
this.timeout(30000);
let n = 0;
graph = _.range(100).map(() => {
return {
model1Prop1: 'hello ' + n++,
model1Relation1: {
model1Prop1: 'hi ' + n++,
model1Relation1: {
model1Prop1: 'howdy ' + n++,
},
},
model1Relation1Inverse: {
model1Prop1: 'quux ' + n++,
},
model1Relation2: _.range(10).map(() => {
return {
model2Prop1: 'foo ' + n++,
model2Relation1: _.range(10).map(() => {
return {
model1Prop1: 'bar ' + n++,
};
}),
model2Relation2: {
model1Prop1: 'baz ' + n++,
},
};
}),
model1Relation3: _.range(10).map(() => {
return {
model2Prop1: 'spam ' + n++,
};
}),
};
});
return session
.populate([])
.then(() => {
return Model1.query().insertGraph(graph);
})
.then((inserted) => {
graph = inserted;
});
});
it('should work with a lot of data', function () {
this.timeout(30000);
return Promise.map(
['withGraphFecthed', 'withGraphJoined'],
(method) => {
let t1 = Date.now();
return Model1.query()
.where('Model1.model1Prop1', 'like', 'hello%')
[method](
'[model1Relation1.model1Relation1, model1Relation1Inverse, model1Relation2.[model2Relation1, model2Relation2], model1Relation3]',
)
.then((res) => {
console.log('query time', Date.now() - t1);
graph = _.sortBy(graph, 'id');
res = _.sortBy(res, 'id');
Model1.traverse(graph, traverser);
Model1.traverse(res, traverser);
let expected = _.invokeMap(graph, 'toJSON');
let got = _.invokeMap(res, 'toJSON');
expect(got).to.eql(expected);
});
},
{ concurrency: 1 },
);
});
function traverser(model) {
['extra1', 'extra2', 'aliasedExtra', 'model1Id', 'model1Prop2', 'model2Prop2'].forEach(
(key) => {
delete model[key];
},
);
['model1Relation2', 'model1Relation3'].map((rel) => {
if (model[rel]) {
model[rel] = _.sortBy(model[rel], 'idCol');
}
});
['model2Relation1'].map((rel) => {
if (model[rel]) {
model[rel] = _.sortBy(model[rel], 'id');
}
});
}
});
});
// Tests all ways to fetch eagerly.
function test(expr, tester, opt) {
let testName;
if (typeof expr === 'object') {
testName = JSON.stringify(expr);
} else {
testName = expr.replace(/\s/g, '');
}
opt = _.defaults(opt || {}, {
Model: Model1,
filters: {},
id: 1,
});
let idCol = opt.Model.query().fullIdColumnFor(opt.Model);
let testFn = opt.only ? it.only.bind(it) : it;
if (!opt.disableWhereIn) {
testFn(testName + ' (QueryBuilder.withGraphFetched)', () => {
return opt.Model.query()
.where(idCol, opt.id)
.withGraphFetched(expr)
.modifiers(opt.filters)
.then(sortRelations(opt.disableSort))
.then(tester);
});
testFn(testName + ' (Model.$fetchGraph)', () => {
return opt.Model.query()
.where(idCol, opt.id)
.then((models) => {
return models[0].$fetchGraph(expr).modifiers(opt.filters);
})
.then(sortRelations(opt.disableSort))
.then((result) => {
tester([result]);
});
});
}
if (!opt.disableJoin) {
testFn(testName + ' (QueryBuilder.withGraphJoined)', () => {
return opt.Model.query()
.where(idCol, opt.id)
.withGraphJoined(expr, opt.eagerOptions)
.modifiers(opt.filters)
.then(sortRelations(opt.disableSort))
.then(tester);
});
}
}
function sortRelations(disable) {
if (disable) {
return (models) => {
return models;
};
}
return (models) => {
Model1.traverse(models, (model) => {
if (model.model1Relation2) {
model.model1Relation2 = _.sortBy(model.model1Relation2, ['idCol', 'model2Prop1']);
}
if (model.model2Relation1) {
model.model2Relation1 = _.sortBy(model.model2Relation1, ['id', 'model1Prop1']);
}
});
return models;
};
}
};
================================================
FILE: tests/main.js
================================================
const expect = require('expect.js');
describe('main module', () => {
it('should be able to load using require', () => {
let objection = require('../');
expect(objection.QueryBuilderBase).to.equal(
require('../lib/queryBuilder/QueryBuilderBase').QueryBuilderBase,
);
expect(objection.QueryBuilderOperation).to.equal(
require('../lib/queryBuilder/operations/QueryBuilderOperation').QueryBuilderOperation,
);
expect(objection.RelationExpression).to.equal(
require('../lib/queryBuilder/RelationExpression').RelationExpression,
);
expect(objection.ValidationError).to.equal(
require('../lib/model/ValidationError').ValidationError,
);
expect(objection.NotFoundError).to.equal(require('../lib/model/NotFoundError').NotFoundError);
expect(objection.Relation).to.equal(require('../lib/relations/Relation').Relation);
expect(objection.HasManyRelation).to.equal(
require('../lib/relations/hasMany/HasManyRelation').HasManyRelation,
);
expect(objection.HasOneRelation).to.equal(
require('../lib/relations/hasOne/HasOneRelation').HasOneRelation,
);
expect(objection.BelongsToOneRelation).to.equal(
require('../lib/relations/belongsToOne/BelongsToOneRelation').BelongsToOneRelation,
);
expect(objection.HasOneThroughRelation).to.equal(
require('../lib/relations/hasOneThrough/HasOneThroughRelation').HasOneThroughRelation,
);
expect(objection.ManyToManyRelation).to.equal(
require('../lib/relations/manyToMany/ManyToManyRelation').ManyToManyRelation,
);
expect(objection.transaction).to.equal(require('../lib/transaction').transaction);
expect(objection.transaction.start).to.equal(require('../lib/transaction').transaction.start);
expect(objection.ref).to.equal(require('../lib/queryBuilder/ReferenceBuilder').ref);
expect(objection.raw).to.equal(require('../lib/queryBuilder/RawBuilder').raw);
expect(objection.val).to.equal(require('../lib/queryBuilder/ValueBuilder').val);
expect(objection.mixin).to.equal(require('../lib/utils/mixin').mixin);
expect(objection.compose).to.equal(require('../lib/utils/mixin').compose);
expect(Object.getPrototypeOf(objection.Validator)).to.equal(
require('../lib/model/Validator').Validator,
);
expect(Object.getPrototypeOf(objection.AjvValidator)).to.equal(
require('../lib/model/AjvValidator').AjvValidator,
);
expect(Object.getPrototypeOf(objection.Model)).to.equal(require('../lib/model/Model').Model);
expect(Object.getPrototypeOf(objection.QueryBuilder)).to.equal(
require('../lib/queryBuilder/QueryBuilder').QueryBuilder,
);
expect(objection.DBError).to.equal(require('db-errors').DBError);
expect(objection.UniqueViolationError).to.equal(require('db-errors').UniqueViolationError);
expect(objection.ConstraintViolationError).to.equal(
require('db-errors').ConstraintViolationError,
);
expect(objection.ForeignKeyViolationError).to.equal(
require('db-errors').ForeignKeyViolationError,
);
expect(objection.NotNullViolationError).to.equal(require('db-errors').NotNullViolationError);
expect(objection.DataError).to.equal(require('db-errors').DataError);
});
});
================================================
FILE: tests/ts/custom-query-builder.ts
================================================
import { Model, QueryBuilder, Page, TransactionOrKnex } from '../../';
class CustomQueryBuilder extends QueryBuilder {
ArrayQueryBuilderType!: CustomQueryBuilder;
SingleQueryBuilderType!: CustomQueryBuilder;
MaybeSingleQueryBuilderType!: CustomQueryBuilder;
NumberQueryBuilderType!: CustomQueryBuilder;
PageQueryBuilderType!: CustomQueryBuilder>;
someCustomMethod(): this {
return this;
}
delete() {
return super.delete();
}
}
class BaseModel extends Model {
QueryBuilderType!: CustomQueryBuilder;
$query(trxOrKnex?: TransactionOrKnex) {
return super.$query(trxOrKnex);
}
}
class Animal extends BaseModel {
id!: number;
name!: string;
owner!: Person;
}
class Person extends BaseModel {
firstName!: string;
pets!: Animal[];
}
const people: CustomQueryBuilder = Person.query()
.someCustomMethod()
.where('firstName', 'lol')
.someCustomMethod()
.with('someAlias', (qb) => qb.someCustomMethod().from('lol').select('id'))
.modifyGraph('pets', (qb) => qb.someCustomMethod().where('id', 1).someCustomMethod());
const pets: CustomQueryBuilder = new Person()
.$relatedQuery('pets')
.someCustomMethod()
.where('id', 1)
.first()
.someCustomMethod();
const numUpdated: CustomQueryBuilder = Person.query()
.someCustomMethod()
.patch({ firstName: 'test' })
.someCustomMethod();
const allPets: PromiseLike = Person.relatedQuery('pets')
.for(Person.query().select('id'))
.someCustomMethod();
================================================
FILE: tests/ts/documents.ts
================================================
import { Person } from './fixtures/person';
(async () => {
const jennifer = await Person.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence',
age: 24,
address: {
street: 'Somestreet 10',
zipCode: '123456',
city: 'Tampere',
},
});
const jenniferFromDb = await Person.query().findById(jennifer.id);
console.log(jennifer.address.city); // --> Tampere
console.log(jenniferFromDb?.address?.city); // --> Tampere
})();
================================================
FILE: tests/ts/examples.ts
================================================
import Ajv from 'ajv';
import { Knex, knex } from 'knex';
import * as objection from '../../';
import {
DBError,
fn,
QueryBuilder,
raw,
ref,
RelationMapping,
RelationMappings,
StaticHookArguments,
val,
} from '../../';
// This file exercises the Objection.js typings.
// These calls are WHOLLY NONSENSICAL and are for TypeScript testing only. If
// you're new to Objection, and want to see how to use TypeScript, please look
// at the code in ../examples/express-ts.
// These "tests" pass if the TypeScript compiler is satisfied.
class CustomValidationError extends Error {}
class CustomValidator extends objection.Validator {
beforeValidate(args: objection.ValidatorArgs): void {
if (!args.options.skipValidation) {
args.ctx.whatever = 'anything';
args.ctx.foo = args.json.required;
const id = args.model.$id;
}
}
validate(args: objection.ValidatorArgs): objection.Pojo {
if (args.options.patch) {
args.json.required = [];
}
return args.json;
}
afterValidate(args: objection.ValidatorArgs): void {
args.json.required = args.ctx.foo;
}
}
class Person extends objection.Model {
// With TypeScript 2.7, fields in models need either optionality:
firstName?: string;
// Or for not-null fields that are always initialized, you can use the new ! syntax:
// prettier-ignore
lastName!: string;
mom?: Person;
children?: Person[];
// Note that $relatedQuery won't work for optional fields (at least until TS 2.8), so this gets a !:
// prettier-ignore
pets!: Animal[];
comments?: Comment[];
movies?: Movie[];
static columnNameMappers = objection.snakeCaseMappers();
static jsonSchema = {
type: 'object',
properties: {
firstName: {
type: 'string',
},
},
};
static async beforeFind({
asFindQuery,
cancelQuery,
}: StaticHookArguments): Promise {
takesPeople(await asFindQuery());
cancelQuery([]);
}
static async afterUpdate({
asFindQuery,
result,
items,
}: StaticHookArguments): Promise {
takesPeople(await asFindQuery());
takesNumber(result!);
takesPeople(items as Person[]);
}
examplePersonMethod = (arg: string) => 1;
static staticExamplePersonMethod() {
return 100;
}
petsWithId(petId: number): PromiseLike {
return this.$relatedQuery('pets').where('id', petId);
}
commentsWithId(commentId: number): Promise {
return this.$relatedQuery('comments').where('id', commentId).execute();
}
fetchMom(): PromiseLike {
return this.$relatedQuery('mom');
}
async $beforeInsert(queryContext: objection.QueryContext) {
console.log(queryContext.someCustomValue);
}
$formatDatabaseJson(json: objection.Pojo) {
// Test that any property can be accessed and set.
json.bar = json.foo;
return json;
}
$parseDatabaseJson(json: objection.Pojo) {
// Test that any property can be accessed and set.
json.foo = json.bar;
return json;
}
static createValidator() {
return new objection.AjvValidator({
onCreateAjv(ajv: Ajv) {
// modify ajv
},
options: {
allErrors: false,
},
});
}
static createValidationError(args: objection.CreateValidationErrorArgs) {
const { message, type, data } = args;
const errorItem: objection.ValidationErrorItem = data['someProp'];
const itemMessage: string = errorItem.message;
return new CustomValidationError('my custom error: ' + message + ' ' + itemMessage);
}
static modifiers = {
myFilter(query: QueryBuilder) {
query.where('something', 'something');
},
};
static relationMappings = () => ({
fancyPets: {
modelClass: Animal,
relation: objection.Model.HasManyRelation,
beforeInsert(pet) {
pet.name = pet.name + ' Fancy Pants';
},
modify(query) {
query.where('name', 'like', '% Fancy Pants');
},
join: {
from: 'person.ownerId',
to: 'pet.id',
},
} as RelationMapping,
});
}
function takesModelSubclass(m: M) {}
function takesModel(m: objection.Model) {}
function takesModelClass(m: objection.ModelClass) {}
// Borrowed from https://github.com/TypeStrong/ts-expect/blob/39f04b5/src/index.ts
type TypeEqual =
Exclude extends never ? (Exclude extends never ? true : false) : false;
const expectsTrue = () => 1;
const takesPerson = (person: Person) => {
person.examplePersonMethod('');
};
const takesMaybePerson = (_: Person | undefined) => 1;
const takesPeople = (_: Person[]) => 1;
const takesNumber = (_: number) => 1;
async function takesPersonClass(PersonClass: typeof Person) {
takesPerson(new PersonClass());
takesMaybePerson(await PersonClass.query().findById(123));
}
function takesPersonQueryBuilder(qb: objection.QueryBuilder): Promise {
return qb.execute();
}
const lastName = 'Lawrence';
// Note that at least with TypeScript 2.3 or earlier, type assertions made
// on an instance will coerce the assignment to the instance type, which
// means `const p: Person = somethingThatReturnsAny()` will compile.
// It also seems that Promise types are not as rigorously asserted as their
// resolved types, hence these async/await blocks:
takesPersonQueryBuilder(Person.query());
async () => {
takesPeople(await Person.query().where('lastName', lastName));
takesPeople(await Person.query().where({ lastName }));
const person = await Person.query().findById(123);
expectsTrue>();
takesMaybePerson(person);
takesMaybePerson(await Person.query().findById('uid'));
};
// .where().first is equivalent to .findOne:
async () => {
takesMaybePerson(await Person.query().where(raw('raw SQL constraint')).first());
takesMaybePerson(await Person.query().where('lastName', lastName).first());
takesMaybePerson(await Person.query().where('lastName', '>', lastName).first());
const person = await Person.query().where({ lastName }).first();
expectsTrue>();
takesMaybePerson(person);
takesMaybePerson(await Person.query().findOne(raw('raw SQL constraint')));
takesMaybePerson(await Person.query().findOne('lastName', lastName));
takesMaybePerson(await Person.query().findOne('lastName', '>', lastName));
const person2 = await Person.query().findOne({ lastName });
expectsTrue>();
takesMaybePerson(person2);
};
// union/unionAll types
async () => {
await Person.query()
.where({ lastName: 'finnigan' })
.union(
// supports callbacks, or querybuilders along-side each other.
Person.query().where({ lastName: 'doe' }),
(qb) => qb.table(Person.tableName).where({ lastName: 'black' }),
);
await Person.query()
.where({ lastName: 'finnigan' })
.union(
// multiple query builders
Person.query().where({ lastName: 'doe' }),
Person.query().where({ lastName: 'black' }),
);
await Person.query()
.where({ lastName: 'finnigan' })
.union(
// supports callbacks, or querybuilders along-side each other.
(qb) => qb.table(Person.tableName).where({ lastName: 'doe' }),
(qb) => qb.table(Person.tableName).where({ lastName: 'black' }),
);
// checks for unions that include wrap options
await Person.query()
.where({ lastName: 'finnigan' })
.union(
[
(qb) => qb.table(Person.tableName).where({ lastName: 'doe' }),
(qb) => qb.table(Person.tableName).where({ lastName: 'black' }),
],
true,
);
await Person.query()
.where({ lastName: 'finnigan' })
.union((qb) => qb.table(Person.tableName).where({ lastName: 'black' }), true);
await Person.query()
.where({ lastName: 'finnigan' })
.union(
// allows `wrap` to be passed as the last argument alongside
// other forms of unions. supports up to 7 union args before wrap arg.
Person.query().where({ lastName: 'doe' }),
(qb) => qb.table(Person.tableName).where({ lastName: 'doe' }),
(qb) => qb.table(Person.tableName).where({ lastName: 'black' }),
true,
);
await Person.query().intersect(Person.query().where({ lastName: 'doe' }), (qb) =>
qb.table(Person.tableName).where({ lastName: 'black' }),
);
};
// .query().castTo()
async () => {
const animals = await Person.query()
.joinRelated('children.children.pets')
.select('children:children:pets.*')
.castTo(Animal);
takesAnimals(animals);
};
// instance methods:
async () => {
const person = new Person();
takesPerson(await person.$fetchGraph('movies'));
takesPerson(await person.$query());
takesPerson(
await person.$query().patchAndFetch({
firstName: 'Test',
lastName: 'Name',
}),
);
};
class Movie extends objection.Model {
// prettier-ignore
title!: string;
// prettier-ignore
actors!: Person[];
// prettier-ignore
director!: Person;
fetchDirector(): PromiseLike {
return this.$relatedQuery('director');
}
/**
* 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: RelationMappings = {
actors: {
relation: objection.Model.ManyToManyRelation,
modelClass: Person,
join: {
from: ['Movie.id1', 'Model.id2'],
through: {
from: 'Actors.movieId',
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',
},
},
};
}
const cols1: string[] = Person.tableMetadata().columns;
const cols2: Promise = Person.fetchTableMetadata();
function takesMovie(m: Movie) {
m.title = '';
}
async () => {
// Another example of strongly-typed $relatedQuery without a cast:
takesPeople(await new Movie().$relatedQuery('actors'));
takesPerson(await new Movie().$relatedQuery('director'));
takesMaybePerson(await new Movie().$relatedQuery('actors').first());
takesMaybePerson(await new Movie().$relatedQuery('director').where('age', '>', 32));
};
const relatedPersons: PromiseLike = new Person().$relatedQuery('children');
const relatedMovies: PromiseLike = new Movie().$relatedQuery('actors');
class Animal extends objection.Model {
// prettier-ignore
species!: string;
name?: string;
owner?: Person | null;
// Tests the ColumnNameMappers interface.
static columnNameMappers = {
parse(json: objection.Pojo) {
return json;
},
format(json: objection.Pojo) {
return json;
},
};
}
const takesAnimal = (animal: Animal) => {
animal.species = 'dog';
};
const takesMaybeAnimal = (_: Animal | undefined) => 1;
const takesAnimals = (_: Animal[]) => 1;
class Comment extends objection.Model {
// prettier-ignore
comment!: string;
}
// !!! see examples/express-ts/src/app.ts for a valid knex setup. The following is bogus:
const k: Knex = knex({});
// bindKnex returns the proper Model subclass:
const BoundPerson = Person.bindKnex(k);
takesPersonClass(BoundPerson);
// The Model subclass is interpreted correctly to be constructable
const examplePerson = new BoundPerson();
// and inherited methods from Model
const personId = examplePerson.$id();
const exampleJsonPerson1: Person = examplePerson.$setJson({ id: 'hello' });
const exampleJsonPerson2: Person = examplePerson.$set({ id: 'hello' });
const exampleDatabaseJsonPerson: Person = examplePerson.$setDatabaseJson({
id: 'hello',
});
const omitPersonFromKey: Person = examplePerson.$omitFromJson('lastName');
const omitPersonFromObj: Person = examplePerson.$omitFromJson({ firstName: true });
const clonePerson: Person = examplePerson.$clone();
const setRelatedPerson: Person = examplePerson.$setRelated(
'parent',
Person.fromJson({ firstName: 'parent' }),
);
const appendRelatedPerson: Person = examplePerson.$appendRelated('pets', [
Animal.fromJson({ firstName: 'pet 1' }),
Animal.fromJson({ firstName: 'pet 2' }),
]);
// static methods from Model should return the subclass type
const personQB: objection.QueryBuilder = Person.fetchGraph(new Person(), 'movies');
const peopleQB: objection.QueryBuilder = Person.fetchGraph([new Person()], 'movies');
const person: PromiseLike = personQB;
const people: PromiseLike = peopleQB;
class Actor {
canAct?: boolean;
}
// Optional typing for findById():
function byId(id: number): Promise {
return Person.query().findById(id).execute();
}
// Person[] typing for findByIds():
function byIds(ids: number[] | number[][]): PromiseLike {
return Person.query().findByIds(ids);
}
// Person[] typing for where():
function whereSpecies(species: string): PromiseLike {
return Animal.query().where('species', species);
}
const pqb: objection.QueryBuilder = objection.QueryBuilder.forClass(Person);
const personPromise: PromiseLike = pqb.findById(1);
// QueryBuilder.findById accepts single and array values:
let qb: objection.QueryBuilder = BoundPerson.query().where('name', 'foo');
// QueryBuilder.throwIfNotFound makes an option query return exactly one:
async () => {
const q = () => Person.query().findOne({ lastName });
const person = await q().throwIfNotFound();
takesPerson(person);
};
// QueryBuilder.throwIfNotFound does nothing for array results:
async () => {
const q = () => Person.query().where({ lastName });
takesPeople(await q());
takesPeople(await q().throwIfNotFound());
};
// Note that the QueryBuilder chaining done in this file
// is done to verify that the return value is assignable to a QueryBuilder
// (fewer characters than having each line `const qbNNN: QueryBuilder =`):
const maybePerson: PromiseLike = qb.findById(1);
const maybePerson2: PromiseLike = qb.findById([1, 2, 3]);
// query builder knex-wrapping methods:
qb = qb.increment('column_name');
qb = qb.increment('column_name', 2);
qb = qb.decrement('column_name', 1);
qb = qb.select('column1');
qb = qb.select('column1', 'column2', 'column3');
qb = qb.select(['column1', 'column2']);
qb = qb.forUpdate();
qb = qb.as('column_name');
qb = qb.column('column_name');
qb = qb.columns('column_name', 'column_name_2');
qb = qb.withSchema('schema_name');
qb = qb.distinct('column1', 'column2', 'column3');
qb = qb.join('tablename', 'column1', '=', 'column2');
qb = qb.outerJoin('tablename', 'column1', '=', 'column2');
qb = qb.joinRelated('table');
qb = qb.joinRelated('table', { alias: false });
qb = qb.where(raw('random()', 1, '2'));
qb = qb.where(raw('random()', 1, '2'), '=', Person.knex().raw('foo'));
qb = qb.where(Person.raw('random()', 1, '2', raw('3')));
qb = qb.where(fn.coalesce(null, fn('random')), '=', 1);
qb = qb.where(Person.ref('column1'), 1);
qb = qb.alias('someAlias');
qb = qb.with('alias', Movie.query());
qb = qb.with('alias', (qb) => qb.from('someTable').select('id'));
qb = qb.whereColumn('firstName', 'lastName');
qb = qb.groupBy('firstName');
qb = qb.groupBy(['firstName', 'lastName']);
qb = qb.orderBy('firstName');
qb = qb.whereComposite('id', 1);
qb = qb.whereComposite('id', '>', 1);
qb = qb.whereComposite(['id1', 'id2'], [1, '2']);
qb = qb.whereComposite(['id1', 'id2'], Person.query());
qb = qb.whereInComposite('id', [1, 2]);
qb = qb.whereInComposite(
['id1', 'id2'],
[
[1, '2'],
[1, '2'],
],
);
qb = qb.whereInComposite(['id1', 'id2'], Person.query().select('firstName', 'lastName'));
// Query builder hooks. runBefore() and runAfter() don't immediately affect the result.
const runBeforePerson: PromiseLike = qb
.first()
.throwIfNotFound()
.runBefore(async (result: any, builder: objection.QueryBuilder) => 88);
const runBeforePersons: PromiseLike = qb.runBefore(
async (result: any, builder: objection.QueryBuilder) => 88,
);
const runAfterPerson: PromiseLike = qb
.first()
.throwIfNotFound()
.runAfter(async (result: any, builder: objection.QueryBuilder) => 88);
const runAfterPersons: PromiseLike = qb.runAfter(
async (result: any, builder: objection.QueryBuilder) => 88,
);
// signature-changing QueryBuilder methods:
const rowInserted: PromiseLike = qb.insert({ firstName: 'bob' });
const rowsInserted: PromiseLike = qb.insert([
{ firstName: 'alice' },
{ firstName: 'bob' },
]);
const rowsInsertedWithRelated: PromiseLike = qb.insertGraph({});
const rowsInsertGraph1: PromiseLike = qb.insertGraph({
'#id': 'root',
firstName: 'Name',
mom: {
lastName: 'Hello',
},
movies: [
{
director: {
firstName: 'Hans',
},
},
{
'#dbRef': 1,
},
],
pets: [
{
name: 'Pet',
},
{
species: 'Doggo',
},
{
name: 'Catto',
owner: {
'#ref': 'root',
},
},
],
});
const rowsInsertGraph2: PromiseLike = qb.insertGraph([
{
'#id': 'person',
firstName: 'Name',
pets: [
{
'#id': 'pet',
name: 'Pet',
},
{
species: 'Doggo',
owner: {
'#ref': 'person',
},
},
],
},
]);
const rowsInsertGraph3: PromiseLike = qb.insertGraph({}, { relate: true });
const rowsUpdated: PromiseLike = qb.update({});
const rowsPatched: PromiseLike = qb.patch({});
const rowsDeleted: PromiseLike = qb.delete();
const rowsDeletedById: PromiseLike = qb.deleteById(123);
const rowsDeletedByIds: PromiseLike = qb.deleteById([123, 456]);
const rowsUpdatedWithData: PromiseLike[] = [
qb.update({ firstName: 'name' }),
qb.update({ firstName: ref('last_name') }),
qb.update({ firstName: raw('"name"') }),
qb.update({ firstName: qb.select('lastname') }),
];
const rowsPatchedWithData: PromiseLike[] = [
qb.patch({ firstName: 'name' }),
qb.patch({ firstName: ref('last_name') }),
qb.patch({ firstName: raw('"name"') }),
qb.patch({ firstName: qb.select('lastname') }),
];
const insertedModel: PromiseLike = Person.query().insertAndFetch({});
const insertedModels1: PromiseLike = Person.query().insertGraphAndFetch([
new Person(),
new Person(),
]);
const insertedModels2: PromiseLike = Person.query().insertGraphAndFetch(
[new Person(), new Person()],
{
relate: true,
},
);
const upsertModel1: PromiseLike = Person.query().upsertGraph({});
const upsertModel2: PromiseLike = Person.query().upsertGraph({}, { relate: true });
const upsertModels1: PromiseLike = Person.query().upsertGraph([]);
const upsertModels2: PromiseLike = Person.query().upsertGraph([], {
unrelate: true,
});
const insertedGraphAndFetchOne: PromiseLike = Person.query().insertGraphAndFetch(
new Person(),
);
const insertedGraphAndFetchSome: PromiseLike = Person.query().insertGraphAndFetch([
new Person(),
new Person(),
]);
const updatedModel: PromiseLike = Person.query().updateAndFetch({});
const updatedModelById: PromiseLike = Person.query().updateAndFetchById(123, {});
const patchedModel: PromiseLike = Person.query().patchAndFetch({});
const patchedModelById: PromiseLike = Person.query().patchAndFetchById(123, {});
const updatedModels: PromiseLike[] = [
qb.updateAndFetch({ firstName: 'name' }),
qb.updateAndFetch({ firstName: ref('last_name') }),
qb.updateAndFetch({ firstName: raw('"name"') }),
qb.updateAndFetch({ firstName: qb.select('lastname') }),
qb.updateAndFetchById(123, { firstName: 'name' }),
qb.updateAndFetchById(123, { firstName: ref('last_name') }),
qb.updateAndFetchById(123, { firstName: raw('"name"') }),
qb.updateAndFetchById(123, { firstName: qb.select('lastname') }),
];
const patchedModels: PromiseLike[] = [
qb.patchAndFetch({ firstName: 'name' }),
qb.patchAndFetch({ firstName: ref('last_name') }),
qb.patchAndFetch({ firstName: raw('"name"') }),
qb.patchAndFetch({ firstName: qb.select('lastname') }),
qb.patchAndFetchById(123, { firstName: 'name' }),
qb.patchAndFetchById(123, { firstName: ref('last_name') }),
qb.patchAndFetchById(123, { firstName: raw('"name"') }),
qb.patchAndFetchById(123, { firstName: qb.select('lastname') }),
];
const rowsEager: PromiseLike = Person.query().withGraphFetched('foo.bar', {
joinOperation: 'innerJoin',
});
const rowsEager2: PromiseLike = Person.query().withGraphFetched({
pets: {
owner: {
movies: {
director: true,
},
},
},
});
const rowsEager3: PromiseLike = Person.query().withGraphFetched({
foo: {
bar: true,
},
});
const children: PromiseLike = Person.query()
.skipUndefined()
.allowGraph('[pets, parent, children.[pets, movies.actors], movies.actors.pets]')
.allowGraph({ pets: true })
.allowGraph({ parent: true })
.withGraphFetched('children')
.where('age', '>=', 42);
const childrenAndPets: PromiseLike = Person.query()
.withGraphFetched('children')
.where('age', '>=', 42)
.modifyGraph('[pets, children.pets]', (qb) => qb.orderBy('name'))
.modifyGraph('[pets, children.pets]', 'orderByName')
.modifyGraph('[pets, children.pets]', ['orderByName', 'orderBySomethingElse']);
const childrenAndPets2: PromiseLike = Person.query()
.withGraphFetched('children')
.where('age', '>=', 42)
.modifyGraph('[pets, children.pets]', (qb) => qb.orderBy('name'))
.modifyGraph('[pets, children.pets]', 'orderByName')
.modifyGraph('[pets, children.pets]', ['orderByName', 'orderBySomethingElse']);
const childrenAndPets3: PromiseLike = Person.query()
.withGraphJoined('children')
.where('age', '>=', 42)
.modifyGraph('[pets, children.pets]', (qb) => qb.orderBy('name'))
.modifyGraph('[pets, children.pets]', 'orderByName')
.modifyGraph('[pets, children.pets]', ['orderByName', 'orderBySomethingElse']);
const rowsPage: PromiseLike<{
total: number;
results: Person[];
}> = Person.query().page(1, 10);
const rowsRange: PromiseLike> = Person.query().range(1, 10);
const rowsPageRunAfter: PromiseLike> = 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: PromiseLike = Person.query().update({}).returning('*');
const rowPatchReturningFirst: PromiseLike = Person.query()
.patch({})
.returning('*')
.first();
// `retuning` should change the return value from number to T[]
const rowsDeleteReturning: PromiseLike = Person.query().delete().returning('*');
const rowsDeleteReturningFirst: PromiseLike = Person.query()
.delete()
.returning('*')
.first();
const rowInsertReturning: PromiseLike = Person.query()
.insert({})
.returning('*');
const rowsInsertReturning: PromiseLike = Person.query()
.insert([{ firstName: 'Jack' }])
.returning('*');
// Executing a query builder should be equivalent to treating it
// as a promise directly, regardless of query builder return type:
const maybePersonQb = Person.query().findById(1);
let maybePersonPromise: PromiseLike = maybePersonQb;
maybePersonPromise = maybePersonQb.execute();
const peopleQb = Person.query();
let peoplePromise: PromiseLike = peopleQb;
peoplePromise = peopleQb.execute();
const insertQb = Person.query().insert({});
let insertPromise: PromiseLike = insertQb;
insertPromise = insertQb.execute();
const insertConfclitQb = Person.query().insert({}).onConflict('id').ignore();
let insertConflictPromise: PromiseLike = insertConfclitQb;
insertConflictPromise = insertConfclitQb.execute();
const insertConfclitMergeQb = Person.query()
.insert({})
.onConflict('id')
.merge({ firstName: 'foo' })
.merge(['foo']);
let insertConflictMergePromise: PromiseLike = insertConfclitMergeQb;
insertConflictMergePromise = insertConfclitMergeQb.execute();
const deleteQb = Person.query().delete();
let deletePromise: PromiseLike = deleteQb;
deletePromise = deleteQb.execute();
const pageQb = Person.query().page(1, 10);
let pagePromise: PromiseLike> = pageQb;
pagePromise = pageQb.execute();
Person.query()
.modify('someModifier')
.modify('someModifier', 1, 'foo', { bar: true })
.modify(['someModifier', 'someOtherModifier'])
.modify((qb) => qb.where('firstName', 'lol'));
// non-wrapped methods:
const modelFromQuery = qb.modelClass();
const knexQuery = qb.toKnexQuery().toSQL();
const tableName: string = qb.tableNameFor(Person);
const tableRef: string = qb.tableRefFor(Person);
const tableRefModelClass: string = qb.tableRefFor(modelFromQuery);
function noop() {
// no-op
}
const qbcb = (ea: objection.QueryBuilder) => noop();
qb = qb.context({
runAfter: qbcb,
runBefore: qbcb,
onBuild: qbcb,
});
const trx: objection.Transaction = qb.context().transaction;
qb = qb.context({
foo: 'bar',
});
qb = qb.clearContext();
qb = qb.runBefore(qbcb);
qb = qb.onBuild(qbcb);
qb = qb.onBuildKnex((knexBuilder: Knex.QueryBuilder, builder: objection.QueryBuilder) => {
if (builder.hasWheres()) {
knexBuilder.where('foo', 'bar');
}
});
qb = qb.reject('fail');
qb = qb.resolve('success');
const trxRes1: Promise = Person.transaction(async (trx) => {
const person = await Person.query(trx).findById(1);
return person;
});
const trxRes2: Promise<(Person | undefined)[]> = Person.transaction(Person.knex(), async (trx) => {
const person = await Person.query(trx).findById(1);
return [person];
});
const trxRes3: Promise = objection.transaction(Person, (TxPerson) => {
const n: number = new TxPerson().examplePersonMethod('hello');
return Promise.resolve('yay');
});
objection.transaction(Movie, Person, async (TxMovie, TxPerson) => {
const s: string = new TxMovie().title;
const n: number = new TxPerson().examplePersonMethod('hello');
});
objection.transaction(Movie, Person, Animal, async (TxMovie, TxPerson, TxAnimal) => {
const t: string = new TxMovie().title;
const n: number = new TxPerson().examplePersonMethod('hello');
const s: string = new TxAnimal().species;
});
objection.transaction(
Movie,
Person,
Animal,
Comment,
async (TxMovie, TxPerson, TxAnimal, TxComment) => {
const t: string = new TxMovie().title;
const n: number = new TxPerson().examplePersonMethod('hello');
const s: string = new TxAnimal().species;
const c: string = new TxComment().comment;
},
);
objection.transaction(
Movie,
Person,
Animal,
Comment,
async (TxMovie, TxPerson, TxAnimal, TxComment, trx) => {
const t: string = new TxMovie().title;
const n: number = new TxPerson().examplePersonMethod('hello');
const s: string = new TxAnimal().species;
const c: string = new TxComment().comment;
Movie.query(trx);
},
);
objection.transaction.start(Person).then((trx) => {
const TxPerson: typeof Person = Person.bindTransaction(trx);
TxPerson.query()
.then(() => trx.commit())
.catch(() => trx.rollback());
Person.query(trx).where('age', '<', 90);
});
// Verify QueryBuilders are thenable:
const p: Promise = qb.then(() => 'done');
// Verify we can call `.insert` with a Partial:
Person.query().insert({ firstName: 'Chuck' });
// Verify we can call `.insert` via $relatedQuery
async () => {
const m = await new Person().$relatedQuery('movies').insert({ title: 'Total Recall' });
takesModel(m);
takesMovie(m);
};
// Verify if is possible transaction class can be shared across models
objection.transaction(Person.knex(), async (trx) => {
await Person.query(trx).insert({ firstName: 'Name' });
await Movie.query(trx).insert({ title: 'Total Recall' });
});
objection.transaction(Person.knex(), async (trx) => {
const person = await Person.query(trx).insert({ firstName: 'Name' });
await Movie.query(trx).insert({ title: 'Total Recall' });
await person.$fetchGraph('movies', { transaction: trx });
return person;
});
objection.transaction.start(Person).then((trx) => {
Movie.query(trx)
.then(() => trx.commit())
.catch(() => trx.rollback());
});
// Verify where methods take a queryBuilder of any.
const whereSubQuery = Movie.query().select('name');
Person.query().whereIn('firstName', whereSubQuery);
Person.query().whereIn(['firstName', 'lastName'], whereSubQuery);
Person.query().where('foo', whereSubQuery);
Person.query().whereExists(whereSubQuery);
Person.query().whereExists(Person.relatedQuery('pets'));
Person.query().select([Person.relatedQuery('pets').count().as('petCount')]);
Person.query().select('id', Person.relatedQuery('pets').count().as('petCount'));
Person.query().where((builder) => {
builder.whereBetween('age', [30, 40]).orWhereIn('lastName', whereSubQuery);
});
const relQueryResult1: PromiseLike = Person.relatedQuery('pets').for(1);
const relQueryResult2: PromiseLike = Person.relatedQuery('pets').for([1, 2]);
const relQueryResult3: PromiseLike = Person.relatedQuery('pets').for('something');
const relQueryResult4: PromiseLike = Person.relatedQuery('pets').for([
'something',
'eles',
]);
const relQueryResult5: PromiseLike = Person.relatedQuery('pets').for([
[1, 2],
[3, 4],
]);
const relQueryResult6: PromiseLike = Person.relatedQuery('pets').for(
Movie.query().select('id'),
);
const relQueryResult7: PromiseLike = Person.relatedQuery('movies').for(1);
const relQueryResult8: PromiseLike = Person.relatedQuery('mom').for(1);
const relQueryResult9: PromiseLike = Person.relatedQuery('children').for(1);
const relQueryResult10: PromiseLike =
Person.relatedQuery('nonExistentRelation').for(1);
/**
* https://knexjs.org/guide/query-builder.html#count
*/
Person.query().count('active', { as: 'a' });
Person.query().count('active as a');
Person.query().count({ a: 'active' });
Person.query().count({ a: 'active', v: 'valid' });
Person.query().count('id', 'active');
Person.query().count({ count: ['id', 'active'] });
Person.query().count(raw('??', ['active']));
// RawBuilder:
Person.query()
.select(raw('coalesce(sum(??), 0) as ??', ['age', 'childAgeSum']))
.where(raw(`?? || ' ' || ??`, 'firstName', 'lastName'), 'Arnold Schwarzenegger')
.orderBy(raw('random()'));
// ReferenceBuilder:
// @see http://vincit.github.io/objection.js/#ref75
// https://github.com/Vincit/objection.js/blob/main/doc/includes/API.md#global-query-building-helpers
Person.query()
.select([
'id',
ref('Model.jsonColumn:details.name').castText().as('name'),
ref('Model.jsonColumn:details.age').castInt().as('age'),
])
.join('OtherModel', ref('Model.jsonColumn:details.name').castText(), '=', ref('OtherModel.name'))
.where('age', '>', ref('OtherModel.ageLimit'));
// LiteralBuilder:
Person.query().where(ref('Model.jsonColumn:details'), '=', val({ name: 'Jennifer', age: 29 }));
Person.query().where('age', '>', val(10));
Person.query().where('firstName', val('Jennifer').castText());
// Preserving result type after result type changing methods.
qb = Person.query();
const findByIdSelect: PromiseLike = qb.findById(32).select('firstName');
const findByIdSelectThrow: PromiseLike = qb
.findById(32)
.select('firstName')
.throwIfNotFound();
const findByIdJoin: PromiseLike = qb
.findById(32)
.join('tablename', 'column1', '=', 'column2');
const findByIdJoinThrow: PromiseLike = qb
.findById(32)
.join('tablename', 'column1', '=', 'column2')
.throwIfNotFound();
const findByIdJoinRaw: PromiseLike = qb.findById(32).joinRaw('raw sql');
const findByIdJoinRawThrow: PromiseLike = qb
.findById(32)
.joinRaw('raw sql')
.throwIfNotFound();
const findOneWhere: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.where('lastName', 'like', 'Mac%');
const findOneWhereThrow: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.where('lastName', 'like', 'Mac%')
.throwIfNotFound();
const findOneSelect: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.select('firstName');
const findOneSelectThrow: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.select('firstName')
.throwIfNotFound();
const findOneWhereIn: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.whereIn('status', ['active', 'pending']);
const findOneWhereInThrow: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.whereIn('status', ['active', 'pending'])
.throwIfNotFound();
const findOneWhereJson: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.whereJsonSupersetOf('x:y', 'abc');
const findOneWhereJsonThrow: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.whereJsonSupersetOf('x:y', 'abc')
.throwIfNotFound();
const findOneWhereJsonIsArray: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.whereJsonIsArray('x:y');
const findOneWhereJsonIsArrayThrow: PromiseLike = qb
.findOne({ firstName: 'Mo' })
.whereJsonIsArray('x:y')
.throwIfNotFound();
const patchWhere: PromiseLike = qb.patch({ firstName: 'Mo' }).where('id', 32);
const patchWhereIn: PromiseLike = qb.patch({ firstName: 'Mo' }).whereIn('id', [1, 2, 3]);
const patchWhereJson: PromiseLike = qb
.patch({ firstName: 'Mo' })
.whereJsonSupersetOf('x:y', 'abc');
const patchWhereJsonIsArray: PromiseLike = qb
.patch({ firstName: 'Mo' })
.whereJsonIsArray('x:y');
const patchThrow: PromiseLike = qb.patch({ firstName: 'Mo' }).throwIfNotFound();
const updateWhere: PromiseLike = qb.update({ firstName: 'Mo' }).where('id', 32);
const updateWhereIn: PromiseLike = qb.update({ firstName: 'Mo' }).whereIn('id', [1, 2, 3]);
const updateWhereJson: PromiseLike = qb
.update({ firstName: 'Mo' })
.whereJsonSupersetOf('x:y', 'abc');
const updateWhereJsonIsArray: PromiseLike = qb
.update({ firstName: 'Mo' })
.whereJsonIsArray('x:y');
const updateThrow: PromiseLike = qb.update({ firstName: 'Mo' }).throwIfNotFound();
const deleteWhere: PromiseLike = qb.delete().where('lastName', 'like', 'Mac%');
const deleteWhereIn: PromiseLike = qb.delete().whereIn('id', [1, 2, 3]);
const deleteThrow: PromiseLike = qb.delete().throwIfNotFound();
const deleteByIDThrow: PromiseLike = qb.deleteById(32).throwIfNotFound();
// The location of `first` doesn't matter.
const whereFirst: PromiseLike = qb.where({ firstName: 'Mo' }).first();
const firstWhere: PromiseLike = qb.first().where({ firstName: 'Mo' });
const updateFirst: PromiseLike = qb.update({}).first();
const updateReturningFirst: PromiseLike = qb.update({}).returning('*').first();
// Returning restores the result to Model or Model[].
const whereInsertRet: PromiseLike = qb
.where({ lastName: 'MacMoo' })
.insert({ firstName: 'Mo' })
.returning('dbGeneratedColumn');
const whereMultiInsertRet: PromiseLike = qb
.where({ lastName: 'MacMoo' })
.insert([{ firstName: 'Mo' }, { firstName: 'Bob' }])
.returning('dbGeneratedColumn');
const whereUpdateRet: PromiseLike = qb
.where({ lastName: 'MacMoo' })
.update({ firstName: 'Bob' })
.returning('dbGeneratedColumn');
const wherePatchRet: PromiseLike = qb
.where({ lastName: 'MacMoo' })
.patch({ firstName: 'Mo' })
.returning('age');
const whereDelRetFirstWhere: PromiseLike = qb
.delete()
.returning('lastName')
.first()
.where({ firstName: 'Mo' });
const orderByColumn: PromiseLike = qb.orderBy('firstName', 'asc');
const orderByColumns: PromiseLike = qb.orderBy([
'email',
{ column: 'firstName', order: 'asc', nulls: 'first' },
{ column: 'lastName' },
]);
const someModel1: typeof objection.Model = Person.getRelations()['pets'].joinModelClass;
const someModel2: typeof objection.Model = Person.getRelation('pets').ownerModelClass;
// 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
const peep123: PromiseLike = BoundPerson.query(k).findById(123);
new Person().$query(k).execute();
new Person().$relatedQuery('pets', k).execute();
takesPerson(Person.fromJson({ firstName: 'jennifer', lastName: 'Lawrence' }));
takesPerson(Person.fromDatabaseJson({ firstName: 'jennifer', lastName: 'Lawrence' }));
// plugin tests for mixin and compose:
const plugin1 = {} as any as objection.Plugin;
const plugin2 = {} as any as objection.Plugin;
// DB errors
() => {
const e = new DBError('message');
if (e instanceof DBError) {
}
if (e instanceof objection.ConstraintViolationError) {
}
};
() => {
const BaseModel = objection.mixin(objection.Model, [plugin1, plugin2]);
takesModelClass(BaseModel);
takesModelSubclass(new BaseModel());
takesModel(new BaseModel());
};
() => {
const BaseModel = objection.mixin(objection.Model, plugin1, plugin2);
takesModelClass(BaseModel);
takesModelSubclass(new BaseModel());
takesModel(new BaseModel());
};
() => {
const PersonModel = objection.mixin(Person, plugin1, plugin2);
takesModelClass(PersonModel);
takesPersonClass(PersonModel);
takesModelSubclass(new PersonModel());
};
() => {
const plugin = objection.compose([plugin1, plugin2]);
const BaseModel = objection.mixin(objection.Model, plugin);
takesModelClass(BaseModel);
takesModelSubclass(new BaseModel());
takesModel(new BaseModel());
};
() => {
const plugin = objection.compose(plugin1, plugin2);
const BaseModel = objection.mixin(objection.Model, plugin);
takesModelClass(BaseModel);
takesModelSubclass(new BaseModel());
takesModel(new BaseModel());
};
// .mixin example with Model:
() => {
class MyModel extends objection.mixin(objection.Model, plugin1) {
readonly myModelMethod = true;
}
takesModelClass(MyModel);
takesModelSubclass(new MyModel());
};
// Example with subclass of Model:
() => {
class MyPerson extends objection.mixin(Person, plugin1) {}
takesModelClass(MyPerson);
takesPersonClass(MyPerson);
takesModelSubclass(new MyPerson());
};
// Examples with composite key
class Interview extends objection.Model {
interviewer!: number;
interviewee!: number;
interviewDate?: number;
static columnNameMappers = objection.snakeCaseMappers();
static idColumn = ['interviewer', 'interviewee'];
}
async () => {
// findById with composite key
const interview: Interview | undefined = await Interview.query().findById([10, 11]);
// findById with composite key, chained with other query builder methods
const interviewWithAssignedDate: Interview | undefined = await Interview.query()
.findById([10, 11])
.whereNotNull('interviewDate');
// findByIds with sets of composite key
const interviews: Interview[] = await Interview.query().findByIds([
[10, 11],
[11, 12],
[12, 13],
]);
// findByIds with sets of composite key, chained with other query builder methods
const interviewsWithAssignedDate: Interview[] = await Interview.query()
.findByIds([
[10, 11],
[11, 12],
[12, 13],
])
.whereNotNull('interviewDate');
};
Person.fromJson({ id: 1 }).$modelClass.query();
================================================
FILE: tests/ts/fixtures/animal.ts
================================================
import * as objection from '../../../';
import { Person } from './person';
export class Animal extends objection.Model {
id!: number;
species!: string;
name?: string;
owner?: Person;
// Tests the ColumnNameMappers interface.
static columnNameMappers = {
parse(json: objection.Pojo) {
return json;
},
format(json: objection.Pojo) {
return json;
},
};
static get modifiers() {
return {
orderByName(builder: objection.QueryBuilder) {
builder.orderBy('name');
},
onlyDogs(builder: objection.QueryBuilder) {
builder.where('species', 'dog');
},
};
}
}
================================================
FILE: tests/ts/fixtures/movie.ts
================================================
import * as objection from '../../../';
import { ref, RelationMappings } from '../../../';
import { Person } from './person';
import { Review } from './review';
export class Movie extends objection.Model {
id!: number;
duration!: number;
title!: string;
actors!: Person[];
director!: Person;
reviews!: Review[];
// Needed for testing `relate({ foo: 50, bar: 20, baz: 10 })`
foo!: number;
bar!: number;
baz!: number;
/**
* 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: RelationMappings = {
actors: {
relation: objection.Model.ManyToManyRelation,
modelClass: Person,
join: {
from: ['Movie.id1', 'Model.id2'],
through: {
from: 'Actors.movieId',
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',
},
},
};
}
================================================
FILE: tests/ts/fixtures/person.ts
================================================
import Ajv from 'ajv';
import * as objection from '../../../';
import { Animal } from './animal';
import { Movie } from './movie';
class CustomValidationError extends Error {}
export class Person extends objection.Model {
id!: number;
// With TypeScript 2.7, fields in models need either optionality:
firstName?: string;
// Or for not-null fields that are always initialized, you can use the new ! syntax:
lastName!: string;
mom?: Person;
children?: Person[];
// Note that $relatedQuery won't work for optional fields (at least until TS 2.8), so this gets a !:
pets!: Animal[];
comments?: Comment[];
movies?: Movie[];
age!: number;
parent?: Partial | null;
oldLastName?: string;
detailsJsonColumn!: objection.Pojo;
address!: objection.Pojo;
// fields marked as extras in relationMappings
someExtra!: string;
static columnNameMappers = objection.snakeCaseMappers();
examplePersonMethod = (arg: string) => 1;
static staticExamplePersonMethod() {
return 100;
}
petsWithId(petId: number): PromiseLike {
return this.$relatedQuery('pets').where('id', petId);
}
fetchMom(): PromiseLike {
return this.$relatedQuery('mom');
}
async $beforeInsert(queryContext: objection.QueryContext) {
console.log(queryContext.someCustomValue);
}
$formatDatabaseJson(json: objection.Pojo) {
// Test that any property can be accessed and set.
json.bar = json.foo;
return json;
}
$parseDatabaseJson(json: objection.Pojo) {
// Test that any property can be accessed and set.
json.foo = json.bar;
return json;
}
static createValidator() {
return new objection.AjvValidator({
onCreateAjv(ajv: Ajv) {
// modify ajv
},
options: {
allErrors: false,
},
});
}
static createValidationError(args: objection.CreateValidationErrorArgs) {
const { message, type, data } = args;
const errorItem: objection.ValidationErrorItem = data['someProp'];
const itemMessage: string = errorItem.message;
return new CustomValidationError('my custom error: ' + message + ' ' + itemMessage);
}
static get modifiers() {
return {
defaultSelects(builder: objection.QueryBuilder) {
builder.select('id', 'firstName');
},
orderByAge(builder: objection.QueryBuilder) {
builder.orderBy('age');
},
};
}
}
================================================
FILE: tests/ts/fixtures/review.ts
================================================
import * as objection from '../../../';
export class Review extends objection.Model {
id!: number;
title?: string;
stars!: number;
text!: string;
}
================================================
FILE: tests/ts/model/instance-methods.ts
================================================
import { Person } from '../fixtures/person';
import { ModelObject } from '../../../typings/objection';
const takesPersonPojo = (person: ModelObject) => true;
const person = Person.fromJson({ firstName: 'Jennifer' });
const personPojo = person.toJSON();
takesPersonPojo(personPojo);
================================================
FILE: tests/ts/model-class.ts
================================================
import { Person } from './fixtures/person';
import { Animal } from './fixtures/animal';
import { ModelClass } from '../../';
(async () => {
const query = Person.query();
const modelClass = query.modelClass();
const persons: Person[] = await modelClass.query();
const pets: Animal[] = await modelClass.relatedQuery('pets');
})();
(async () => {
const modelClass: ModelClass = Person;
const tableName: string = modelClass.tableName;
const persons = await modelClass.query().where('firstName', 'Jennifer');
const persons2: Person[] = await modelClass.fetchGraph(persons, 'pets');
})();
================================================
FILE: tests/ts/query-builder-api/eager-loading-methods.ts
================================================
import { Person } from '../fixtures/person';
(async () => {
await Person.query().where('firstName', 'Arnold').withGraphFetched('pets');
await Person.query().withGraphFetched('children.[pets, movies]');
await Person.query().withGraphFetched({
children: {
pets: true,
movies: true,
},
});
await Person.query()
.withGraphFetched('children(selectNameAndId).[pets(onlyDogs, orderByName), movies]')
.modifiers({
selectNameAndId(builder) {
builder.select('name', 'id');
},
orderByName(builder) {
builder.orderBy('name');
},
onlyDogs(builder) {
builder.where('species', 'dog');
},
});
await Person.query().modifiers({
// You can bind arguments to Model modifiers like this
filterFemale(builder) {
builder.modify('filterGender', 'female');
},
filterDogs(builder) {
builder.modify('filterSpecies', 'dog');
},
}).withGraphFetched(`
children(defaultSelects, orderByAge, filterFemale).[
pets(filterDogs, orderByName),
movies
]
`);
await Person.query()
.withGraphFetched('children.[pets, movies]')
.modifyGraph('children', (builder) => {
// Order children by age and only select id.
builder.orderBy('age').select('id');
})
.modifyGraph('children.[pets, movies]', (builder) => {
// Only select `pets` and `movies` whose id > 10 for the children.
builder.where('id', '>', 10);
});
await Person.query().withGraphFetched(`[
children(orderByAge) as kids .[
pets(filterDogs) as dogs,
pets(filterCats) as cats
movies.[
actors
]
]
]`);
await Person.query().where('id', 1).withGraphFetched('children.children');
await Person.query()
.withGraphJoined('children.[pets, movies]')
.whereIn('children.firstName', ['Arnold', 'Jennifer'])
.where('children:pets.name', 'Fluffy')
.where('children:movies.name', 'like', 'Terminator%');
await Person.query().withGraphJoined('pets').where('persons.id', '>', 100);
const builder = Person.query().withGraphFetched('children.pets(onlyId)');
const expr = builder.graphExpressionObject();
expr.children.movies = true;
builder.withGraphFetched(expr);
await Person.query().allowGraph('[children.pets, movies]').withGraphFetched('movies.actors');
await Person.query().allowGraph('[children.pets, movies]').withGraphFetched('children.pets');
await Person.query()
.allowGraph('[children.pets, movies]')
.insertGraph({
firstName: 'Sylvester',
children: [
{
firstName: 'Sage',
pets: [
{
name: 'Fluffy',
species: 'dog',
},
{
name: 'Scrappy',
species: 'dog',
},
],
},
],
});
await Person.query()
.allowGraph('[children.pets, movies]')
.upsertGraph({
firstName: 'Sylvester',
children: [
{
firstName: 'Sage',
pets: [
{
name: 'Fluffy',
species: 'dog',
},
{
name: 'Scrappy',
species: 'dog',
},
],
},
],
});
await Person.query().clearAllowGraph();
await Person.query().clearWithGraph();
await Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('children.pets', (builder) => {
builder.where('age', '>', 10);
});
await Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('children.[pets, movies]', (builder) => {
builder.orderBy('id');
});
await Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('[children.movies, movies]', (builder) => {
builder.where('name', 'like', '%Predator%');
});
await Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('children.movies', 'selectId');
})();
================================================
FILE: tests/ts/query-builder-api/find-methods.ts
================================================
import { raw, ref } from '../../..';
import { Movie } from '../fixtures/movie';
import { Person } from '../fixtures/person';
(async () => {
const person = await Person.query().findById(1);
await Person.query().findById([1, '10']);
await Person.query().findById(1).patch({ firstName: 'Jennifer' });
const [person1, person2] = await Person.query().findByIds([1, 2]);
const [person3, person4] = await Person.query().findByIds([
[1, '10'],
[2, '10'],
]);
await Person.query().findOne({
firstName: 'Jennifer',
lastName: 'Lawrence',
});
await Person.query().findOne('age', '>', 20);
await Person.query().findOne(raw('random() < 0.5'));
await Person.query()
.alias('p')
.where('p.id', 1)
.join('persons as parent', 'parent.id', 'p.parentId');
await Person.query()
.aliasFor('persons_movies', 'pm')
.joinRelated('movies')
.where('pm.someProp', 100);
await Person.query().aliasFor(Movie, 'm').joinRelated('movies').where('m.name', 'The Room');
await Person.query().select('id', 'name');
await Person.query().transacting(Person.knex()).select('*');
await Person.query().forShare().select('*');
await Person.query().forNoKeyUpdate().select('*');
await Person.query().forKeyShare().select('*');
await Person.query().select('name').as('person_name');
await Person.query().columns('firstName', 'lastName').select();
await Person.query().columns(['firstName', 'lastName']).select();
await Person.query().columns('firstName', { last: 'lastName' }, 'age').select();
await Person.query().column('firstName', 'lastName').select();
await Person.query().column(['firstName', 'lastName']).select();
await Person.query().column('firstName', { last: 'lastName' }, 'age').select();
await Person.query().select('*').from('employees');
// No example available in Knex documentation
await Person.query().with('young_adults', Person.query().where('age', '<', 30));
await Person.query().withSchema('legacy').select('*');
// No example available in Knex documentation
await Person.query().distinct('firstName', 'lastName');
await Person.query().where('firstName', 'Will');
await Person.query().where({
firstName: 'Will',
});
await Person.query()
.where((builder) => {
builder.whereIn('id', [1, 11, 15]).whereNotIn('id', [17, 19]);
})
.andWhere(function () {
this.where('id', '>', 10);
});
await Person.query()
.where(function () {
this.where('id', 1).orWhere('id', '>', 10);
})
.orWhere({ name: 'Tester' });
await Person.query().where('firstName', 'like', '%mark%');
await Person.query().where('votes', '>', 100);
let subquery = Person.query()
.where('votes', '>', 100)
.andWhere('status', 'active')
.orWhere('name', 'John')
.select('id');
await Person.query().where('id', 'in', subquery);
await Person.query().where('id', 1).orWhere({ votes: 100, user: 'knex' });
await Person.query()
.whereNot({
firstName: 'Test',
lastName: 'User',
})
.select('id');
await Person.query().whereNot('id', 1);
await Person.query()
.whereNot(function () {
this.where('id', 1).orWhereNot('id', '>', 10);
})
.orWhereNot({ name: 'Tester' });
await Person.query().whereNot('votes', '>', 100);
subquery = Person.query()
.whereNot('votes', '>', 100)
.andWhere('status', 'active')
.orWhere('name', 'John')
.select('id');
await Person.query().where('id', 'not in', subquery);
await Person.query().whereRaw('id = ?', [1]);
// No example available in Knex documentation
await Person.query().groupBy('count').orderBy('name', 'desc').having('count', '>', 100);
await Person.query().whereExists(function () {
this.select('*').from('accounts').whereRaw('users.account_id = accounts.id');
});
await Person.query().whereNotExists(function () {
this.select('*').from('accounts').whereRaw('users.account_id = accounts.id');
});
await Person.query().orWhereNotExists(function () {
this.select('*').from('accounts').whereRaw('users.account_id = accounts.id');
});
await Person.query().whereIn('id', [1, 2, 3]).orWhereIn('id', [4, 5, 6]);
await Person.query().whereIn('account_id', function () {
this.select('id').from('accounts');
});
await Person.query().whereNotIn('id', [1, 2, 3]);
await Person.query().where('name', 'like', '%Test%').orWhereNotIn('id', [1, 2, 3]);
await Person.query().whereNull('updated_at');
await Person.query().whereNotNull('created_at');
await Person.query().whereExists(function () {
this.select('*').from('accounts').whereRaw('users.account_id = accounts.id');
});
await Person.query().whereNotExists(function () {
this.select('*').from('accounts').whereRaw('users.account_id = accounts.id');
});
await Person.query().whereBetween('votes', [1, 100]);
await Person.query().whereNotBetween('votes', [1, 100]);
await Person.query().whereColumn('firstName', 'like', ref('foo'));
await Person.query()
.andWhereColumn('firstName', 'like', ref('foo'))
.orWhereColumn('firstName', 'like', ref('bar'));
await Person.query()
.whereNotColumn('firstName', 'like', 'foo')
.andWhereNotColumn('lastName', 'like', 'bar')
.orWhereNotColumn('age', '>', '35');
await Person.query().groupBy('age');
await Person.query().groupByRaw('age');
await Person.query().orderBy('email');
await Person.query().orderBy('email', 'ASC');
await Person.query().orderBy('email', 'desc');
await Person.query().orderBy('email', 'desc', 'last');
await Person.query().orderByRaw('? ASC', ['email']);
await Person.query().orderByRaw('email desc');
await Person.query()
.whereNull('last_name')
.union(function () {
this.select('*').from('users').whereNull('first_name');
});
await Person.query()
.whereNull('last_name')
.union([Person.query().whereNull('first_name')]);
await Person.query().unionAll(function () {
this.select('*').from('users').whereNull('first_name');
});
await Person.query()
.whereNull('last_name')
.unionAll([Person.query().whereNull('first_name')]);
await Person.query().groupBy('age').orderBy('firstName', 'desc').having('age', '>', 18);
await Person.query().havingIn('id', [5, 3, 10, 17]);
await Person.query()
.groupBy('count')
.orderBy('name', 'desc')
.havingRaw('count > ?', [100])
.orHaving('count', '<', 50)
.orHavingRaw('count = ?', [15]);
await Person.query().offset(10);
await Person.query().limit(100).offset(200);
await Person.query().count();
await Person.query().resultSize();
await Person.query().countDistinct('firstName');
await Person.query().min('age');
await Person.query().max('age');
await Person.query().sum('age');
await Person.query().avg('age');
await Person.query().avgDistinct('age');
await Person.query()
.insert({
firstName: 'Foo',
})
.returning('*');
const info = Person.query().columnInfo();
await Person.query().whereComposite(['id', 'name'], '=', [1, 'Jennifer']);
await Person.query().whereComposite('id', 1);
await Person.query().whereInComposite(
['a', 'b'],
[
[1, 2],
[3, 4],
[1, 4],
],
);
await Person.query().whereInComposite('a', [[1], [3], [1]]);
await Person.query().whereInComposite('a', [1, 3, 1]);
await Person.query().whereInComposite(['a', 'b'], Person.query().select('a', 'b'));
await Person.query().whereJsonSupersetOf('additionalData:myDogs', 'additionalData:dogsAtHome');
await Person.query().whereJsonSupersetOf('additionalData:myDogs[0]', { name: 'peter' });
await Person.query().orWhereJsonSupersetOf('additionalData:myDogs', 'additionalData:dogsAtHome');
await Person.query().orWhereJsonSupersetOf('additionalData:myDogs[0]', { name: 'peter' });
await Person.query().whereJsonNotSupersetOf('additionalData:myDogs', 'additionalData:dogsAtHome');
await Person.query().whereJsonNotSupersetOf('additionalData:myDogs[0]', { name: 'peter' });
await Person.query().orWhereJsonNotSupersetOf(
'additionalData:myDogs',
'additionalData:dogsAtHome',
);
await Person.query().orWhereJsonNotSupersetOf('additionalData:myDogs[0]', { name: 'peter' });
await Person.query().whereJsonSubsetOf('additionalData:myDogs', 'additionalData:dogsAtHome');
await Person.query().whereJsonSubsetOf('additionalData:myDogs[0]', { name: 'peter' });
await Person.query().orWhereJsonSubsetOf('additionalData:myDogs', 'additionalData:dogsAtHome');
await Person.query().orWhereJsonSubsetOf('additionalData:myDogs[0]', { name: 'peter' });
await Person.query().whereJsonNotSubsetOf('additionalData:myDogs', 'additionalData:dogsAtHome');
await Person.query().whereJsonNotSubsetOf('additionalData:myDogs[0]', { name: 'peter' });
await Person.query().orWhereJsonNotSubsetOf('additionalData:myDogs', 'additionalData:dogsAtHome');
await Person.query().orWhereJsonNotSubsetOf('additionalData:myDogs[0]', { name: 'peter' });
await Person.query().whereJsonIsArray('additionalData');
await Person.query().orWhereJsonIsArray('additionalData');
await Person.query().whereJsonNotArray('additionalData');
await Person.query().orWhereJsonNotArray('additionalData');
await Person.query().whereJsonIsObject('additionalData');
await Person.query().orWhereJsonIsObject('additionalData');
await Person.query().whereJsonNotObject('additionalData');
await Person.query().orWhereJsonNotObject('additionalData');
await Person.query().whereJsonHasAny('additionalData', 'foo');
await Person.query().whereJsonHasAny('additionalData', ['foo', 'bar']);
await Person.query().orWhereJsonHasAny('additionalData', 'foo');
await Person.query().orWhereJsonHasAny('additionalData', ['foo', 'bar']);
await Person.query().whereJsonHasAll('additionalData', 'foo');
await Person.query().whereJsonHasAll('additionalData', ['foo', 'bar']);
await Person.query().orWhereJsonHasAll('additionalData', 'foo');
await Person.query().orWhereJsonHasAll('additionalData', ['foo', 'bar']);
})();
================================================
FILE: tests/ts/query-builder-api/join-methods.ts
================================================
import { Person } from '../fixtures/person';
import { raw } from '../../..';
(async () => {
await Person.query().joinRelated('pets').where('pets.species', 'dog');
await Person.query().joinRelated('pets', { alias: 'p' }).where('p.species', 'dog');
await Person.query()
.joinRelated('[pets, parent]')
.where('pets.species', 'dog')
.where('parent.name', 'Arnold');
await Person.query()
.joinRelated({
pets: true,
parent: true,
})
.where('pets.species', 'dog')
.where('parent.name', 'Arnold');
await Person.query()
.select('persons.id', 'parent:parent.name as grandParentName')
.joinRelated('[pets, parent.[pets, parent]]')
.where('parent:pets.species', 'dog');
await Person.query()
.select('persons.id', 'pr:pr.name as grandParentName')
.joinRelated('[pets, parent.[pets, parent]]', {
aliases: {
parent: 'pr',
pets: 'pt',
},
})
.where('pr:pt.species', 'dog');
await Person.query()
.select('persons.id', 'pr:pr.name as grandParentName')
.joinRelated('[pets as pt, parent as pr.[pets as pt, parent as pr]]')
.where('pr:pt.species', 'dog');
await Person.query().innerJoinRelated('pets');
await Person.query().outerJoinRelated('pets');
await Person.query().leftJoinRelated('pets');
await Person.query().leftOuterJoinRelated('pets');
await Person.query().rightJoinRelated('pets');
await Person.query().rightOuterJoinRelated('pets');
await Person.query().fullOuterJoinRelated('pets');
await Person.query().join(raw('pets'));
await Person.query().joinRaw('pets');
await Person.query().innerJoin(raw('pets'));
await Person.query().leftJoin(raw('pets'));
await Person.query().leftOuterJoin(raw('pets'));
await Person.query().rightJoin(raw('pets'));
await Person.query().rightOuterJoin(raw('pets'));
await Person.query().outerJoin(raw('pets'));
await Person.query().fullOuterJoin(raw('pets'));
await Person.query().crossJoin(raw('pets'));
await Person.query().innerJoin('pets', 'pets.foo', 'persons.bar');
await Person.query().innerJoin(Person.query(), 'persons.foo', 'persons.bar');
await Person.query().innerJoin((qb) => qb.from('pets').as('pets'), 'pets.foo', 'persons.bar');
})();
================================================
FILE: tests/ts/query-builder-api/mutating-methods.ts
================================================
import { raw, ref } from '../../../';
import { Movie } from '../fixtures/movie';
import { Person } from '../fixtures/person';
(async () => {
await Person.query().insert({ firstName: 'Jennifer', lastName: 'Lawrence' });
await Movie.relatedQuery('actors')
.for(1)
.insert([
{ firstName: 'Jennifer', lastName: 'Lawrence' },
{ firstName: 'Bradley', lastName: 'Cooper' },
]);
await Person.query().insert({
age: Person.query().avg('age'),
firstName: raw("'Jenni' || 'fer'"),
});
await Person.query()
.insert({
age: Person.query().avg('age'),
firstName: raw("'Jenni' || 'fer'"),
})
.returning('*');
await Movie.relatedQuery('actors').for(1).insert({
firstName: 'Jennifer',
lastName: 'Lawrence',
someExtra: "I'll be written to the join table",
});
await Person.query().insertAndFetch({ firstName: 'Jennifer', lastName: 'Lawrence' });
await Movie.relatedQuery('actors')
.for(1)
.insertAndFetch([
{ firstName: 'Jennifer', lastName: 'Lawrence' },
{ firstName: 'Bradley', lastName: 'Cooper' },
]);
await Person.query().insertAndFetch({
age: Person.query().avg('age'),
firstName: raw("'Jenni' || 'fer'"),
});
await Movie.relatedQuery('actors').for(1).insertAndFetch({
firstName: 'Jennifer',
lastName: 'Lawrence',
someExtra: "I'll be written to the join table",
});
await Person.query().insertGraphAndFetch({ firstName: 'Jennifer', lastName: 'Lawrence' });
await Person.query().patch({ age: 24 }).findById(1);
await Person.query().patch({ age: 20 }).where('age', '<', 50);
await Person.query()
.patch({ age: raw('age + 1') })
.where('age', '<', 50);
await Person.query().patch({
age: Person.query().avg('age'),
firstName: raw("'Jenni' || 'fer'"),
oldLastName: ref('lastName'),
// Unable to support with TypeScript as of Sep 26, 2019 and typescript 3.5.3
// 'detailsJsonColumn:address.street': 'Elm street'
});
await Person.query().patchAndFetchById(134, { age: 24 });
let jennifer = await Person.query().findOne({ firstName: 'Jennifer' });
let updatedJennifer = await jennifer!.$query().patchAndFetch({ age: 24 });
await Person.query()
.update({ firstName: 'Jennifer', lastName: 'Lawrence', age: 24 })
.where('id', 134);
await Person.query().update({
firstName: raw("'Jenni' || 'fer'"),
lastName: 'Lawrence',
age: Person.query().avg('age'),
oldLastName: ref('lastName'), // same as knex.raw('??', ['lastName'])
});
await Person.query().update({
lastName: ref('someJsonColumn:mother.lastName').castText(),
// Unable to support with TypeScript as of Sep 26, 2019 and typescript 3.5.3
// 'detailsJsonColumn:address.street': 'Elm street'
});
await Person.query().updateAndFetchById(134, {
firstName: 'Christine',
});
jennifer = await Person.query().findOne({ firstName: 'Jennifer' });
updatedJennifer = await jennifer!.$query().updateAndFetch({ age: 24 });
await Person.query().delete().where('age', '>', 100);
await Person.query()
.delete()
.whereIn(
'id',
Person.query().select('persons.id').joinRelated('pets').where('pets.name', 'Fluffy'),
);
await Person.query()
.delete()
.whereExists(Person.relatedQuery('pets').where('pets.name', 'Fluffy'));
let person = await Person.query().findById(1);
await person!.$relatedQuery('pets').delete().whereNotIn('species', ['cat', 'dog']);
await person!.$relatedQuery('pets').delete();
await Person.query().deleteById(1);
await Person.query().deleteById([10, '20', 46]);
let actor = await Person.query().findById(100);
let movie = await Movie.query().findById(200);
await actor!.$relatedQuery('movies').relate(movie!);
await Person.relatedQuery('movies').for(100).relate(200);
await Person.relatedQuery('movies')
.for(Person.query().where('firstName', 'Arnold').limit(1))
.relate([100, 200, 300, 400]);
await Person.relatedQuery('movies').for(123).relate(50);
await Person.relatedQuery('movies').for(123).relate([50, 60, 70]);
await Person.relatedQuery('movies').for(123).relate({ foo: 50, bar: 20, baz: 10 });
await Movie.relatedQuery('actors').for(1).relate({
id: 50,
someExtra: "I'll be written to the join table",
});
actor = await Person.query().findById(100);
await actor!.$relatedQuery('movies').unrelate().where('name', 'like', 'Terminator%');
await Person.relatedQuery('movies').for(100).unrelate().where('name', 'like', 'Terminator%');
const arnold = Person.query().findOne({
firstName: 'Arnold',
lastName: 'Schwarzenegger',
});
await Person.relatedQuery('movies').for(arnold).unrelate().where('name', 'like', 'Terminator%');
person = await Person.query().findById(123);
const numUnrelatedRows = await person!.$relatedQuery('movies').unrelate().where('id', 50);
await Person.query().increment('age', 1);
await Person.query().decrement('age', 1);
await Person.query().truncate();
})();
================================================
FILE: tests/ts/query-builder-api/other-methods.ts
================================================
import { Person } from '../fixtures/person';
import { UniqueViolationError } from 'db-errors';
import { Animal } from '../fixtures/animal';
import { Model, transaction } from '../../../typings/objection';
(async () => {
const debugResult = Person.query().joinRelated('children').where('age', '>', '21').debug();
const personId = 1;
const pets = await Person.relatedQuery('pets').for(personId);
const builder = Person.query().context({ something: 'hello' });
const context = builder.context();
Person.query().context({
runBefore(result: any, builder: any) {
return result;
},
runAfter(result: any, builder: any) {
return result;
},
onBuild(builder: any) {},
});
Person.query()
.withGraphFetched('[movies, children.movies]')
.context({
onBuild(builder: any) {
builder.withSchema('someSchema');
},
});
builder.context({
foo: 'bar',
});
builder.tableNameFor(Person);
builder.tableRefFor(Person);
builder.reject('something went wrong');
builder.resolve({});
builder.isExecutable();
builder.isFind();
builder.isInsert();
builder.isUpdate();
builder.isDelete();
builder.isRelate();
builder.isUnrelate();
builder.isInternal();
builder.hasWheres();
builder.hasSelects();
builder.hasWithGraph();
builder.has('range');
builder.clear('orderBy').has('orderBy');
Person.query()
.runBefore(async (result) => {
console.log('hello 1');
console.log('hello 2');
return result;
})
.runBefore((result) => {
console.log('hello 3');
return result;
});
Person.query()
.onBuild((builder) => {
builder.where('id', 1);
})
.onBuild((builder) => {
builder.orWhere('id', 2);
});
Person.query().onBuildKnex((knexBuilder, objectionBuilder) => {
knexBuilder.where('id', 1);
});
Person.query()
.runAfter(async (models, queryBuilder) => {
return models;
})
.runAfter(async (models, queryBuilder) => {
models.push(Person.fromJson({ firstName: 'Jennifer' }));
return models;
});
Person.query()
.onError(async (error, queryBuilder) => {
if (error instanceof UniqueViolationError) {
return { error: 'some error occurred' };
} else {
return Promise.reject(error);
}
})
.where('age', '>', 30);
await Person.query()
.joinRelated('children.children.pets')
.select('children:children:pets.*')
.castTo(Animal);
await Person.query()
.joinRelated('children.pets')
.select(['children:pets.id as animalId', 'children.firstName as childFirstName'])
.castTo(Model);
const modelClass = builder.modelClass();
builder.toString();
Person.query().skipUndefined().where('firstName', 'something');
const trx = await transaction.start(Person);
builder.transacting(trx);
builder.clone();
builder.execute();
builder.then(
() => {
console.log('success');
},
() => {
console.log('error');
},
);
// builder.map((obj) => obj);
// builder.reduce()
builder.catch((error) => {
console.log(error);
});
// builder.bind()
// builder.asCallback();
// builder. nodeify()
const query = Person.query().where('age', '>', 20);
const [total, models] = await Promise.all([query.resultSize(), query.offset(100).limit(50)]);
await Person.query().where('age', '>', 20).page(5, 100);
await Person.query().where('age', '>', 20).range(0, 100);
await Person.query().where('age', '>', 20).limit(10).range();
await Person.query().first();
await Person.query().where('name', 'Java').andWhere('isModern', true).throwIfNotFound();
builder.timeout(2000, {
cancel: true,
});
builder.connection(Person.knex());
Person.query().modify('someModifier', 'foo', 1);
Person.query().modify(['someModifier', 'someOtherModifier'], 'foo', 1);
function modifierFunc(query: any, arg1: any, arg2: any) {
query.where(arg1, arg2);
}
Person.query().modify(modifierFunc, 'foo', 1);
await Person.query()
.modifiers({
selectFields: (query) => query.select('id', 'name'),
// In the following modifier, `filterGender` is a modifier
// registered in Person.modifiers object. Query modifiers
// can be used to bind arguments to model modifiers like this.
filterWomen: (query) => query.modify('filterGender', 'female'),
})
.modify('selectFields')
.withGraphFetched('children(selectFields, filterWomen)');
Person.query().modifiers();
})();
================================================
FILE: tests/ts/query-examples/basic-queries/delete.ts
================================================
import { raw } from '../../../../';
import { Person } from '../../fixtures/person';
(async () => {
await Person.query().deleteById(1);
const numDeleted: number = await Person.query()
.delete()
.where(raw('lower("firstName")'), 'like', '%ennif%');
console.log(numDeleted, 'people were deleted');
await Person.query()
.delete()
.whereIn(
'id',
Person.query().select('persons.id').joinRelated('pets').where('pets.name', 'Fluffy'),
);
await Person.query()
.delete()
.whereExists(Person.relatedQuery('pets').where('pets.name', 'Fluffy'));
await Person.fromJson({ firstName: 'Jennifer' }).$query().delete();
})();
================================================
FILE: tests/ts/query-examples/basic-queries/find.ts
================================================
import { Animal } from '../../fixtures/animal';
import { Person } from '../../fixtures/person';
(async () => {
const person = await Person.query().findById(1);
console.log(person!.firstName);
console.log(person instanceof Person); // --> true
const people = await Person.query();
console.log(people[0] instanceof Person); // --> true
console.log('there are', people.length, 'People in total');
const middleAgedJennifers = await Person.query()
.where('age', '>', 40)
.where('age', '<', 60)
.where('firstName', 'Jennifer')
.orderBy('lastName');
console.log('The last name of the first middle aged Jennifer is');
console.log(middleAgedJennifers[0].lastName);
})();
(async () => {
const people = await Person.query()
.select('persons.*', 'Parent.firstName as parentFirstName')
.join('persons as parent', 'persons.parentId', 'parent.id')
.where('persons.age', '<', Person.query().avg('persons.age'))
.whereExists(Animal.query().select(1).whereColumn('persons.id', 'animals.ownerId'))
.orderBy('persons.lastName');
})();
(async () => {
const people = await Person.query()
.select('parent:parent.name as grandParentName')
.joinRelated('parent.parent');
})();
(async () => {
const nonMiddleAgedJennifers = await Person.query()
.where((builder) => builder.where('age', '<', 4).orWhere('age', '>', 60))
.where('firstName', 'Jennifer')
.orderBy('lastName');
})();
================================================
FILE: tests/ts/query-examples/basic-queries/insert.ts
================================================
import { Person } from '../../fixtures/person';
(async () => {
const jennifer = await Person.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence',
});
const personPromise: PromiseLike = Person.fromJson({ firstName: 'Jennifer' })
.$query()
.insert();
})();
================================================
FILE: tests/ts/query-examples/basic-queries/update.ts
================================================
import { Person } from '../../fixtures/person';
(async () => {
let numUpdated = await Person.query().findById(1).patch({
firstName: 'Jennifer',
});
numUpdated = await Person.query().patch({ lastName: 'Dinosaur' }).where('age', '>', 60);
const updatedPerson = await Person.query().patchAndFetchById(246, {
lastName: 'Updated',
});
await Person.fromJson({ firstName: 'Jennifer' }).$query().update();
await Person.fromJson({ firstName: 'Jennifer' }).$query().patch();
})();
================================================
FILE: tests/ts/query-examples/eager-loading.ts
================================================
import { Person } from '../fixtures/person';
(async () => {
await Person.query().withGraphFetched('pets');
await Person.query().withGraphFetched('[pets, children.[pets, children]]');
await Person.query().withGraphFetched({
pets: true,
children: {
pets: true,
children: true,
},
});
await Person.query().withGraphFetched('[pets, children.^]');
await Person.query().withGraphFetched('[pets, children.^3]');
await Person.query()
.withGraphFetched('[children.[pets, movies], movies]')
.modifyGraph('children.pets', (builder) => {
// Only select pets older than 10 years old for children
// and only return their names.
builder.where('age', '>', 10).select('name');
});
await Person.query()
.withGraphFetched('[pets(selectName, onlyDogs), children(orderByAge).[pets, children]]')
.modifiers({
selectName: (builder) => {
builder.select('name');
},
orderByAge: (builder) => {
builder.orderBy('age');
},
onlyDogs: (builder) => {
builder.where('species', 'dog');
},
});
await Person.query().withGraphFetched(`
children(defaultSelects, orderByAge).[
pets(onlyDogs, orderByName),
movies
]
`);
await Person.query().withGraphFetched(`[
children(orderByAge) as kids .[
pets(filterDogs) as dogs,
pets(filterCats) as cats
movies.[
actors
]
]
]`);
const eager = `[]`;
await Person.query().allowGraph('[pets, children.pets]').withGraphFetched(eager);
await Person.query().withGraphFetched('[pets, children.pets]');
await Person.query().withGraphJoined('[pets, children.pets]');
})();
================================================
FILE: tests/ts/query-examples/graph-inserts.ts
================================================
import { Person } from '../fixtures/person';
(async () => {
await Person.query().insertGraph({
firstName: 'Sylvester',
lastName: 'Stallone',
children: [
{
firstName: 'Sage',
lastName: 'Stallone',
pets: [
{
name: 'Fluffy',
species: 'dog',
},
],
},
],
});
await Person.query().insertGraph([
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
'#id': 'silverLiningsPlaybook',
title: 'Silver Linings Playbook',
duration: 122,
},
],
},
{
firstName: 'Bradley',
lastName: 'Cooper',
movies: [
{
'#ref': 'silverLiningsPlaybook',
},
],
},
]);
await Person.query().insertGraph([
{
'#id': 'jenni',
firstName: 'Jennifer',
lastName: 'Lawrence',
pets: [
{
name: 'I am the dog of #ref{jenni.firstName} whose id is #ref{jenni.id}',
species: 'dog',
},
],
},
]);
await Person.query().insertGraph(
[
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
id: 2636,
},
],
},
],
{
relate: true,
},
);
await Person.query().insertGraph(
[
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
id: 2636,
},
],
},
],
{
relate: ['movies'],
},
);
await Person.query().insertGraph(
[
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
title: 'Silver Linings Playbook',
duration: 122,
actors: [
{
id: 2516,
},
],
},
],
},
],
{
relate: ['movies.actors'],
},
);
await Person.query().insertGraph([
{
firstName: 'Jennifer',
lastName: 'Lawrence',
movies: [
{
'#dbRef': 2636,
},
{
// This will be inserted with an id.
id: 100,
title: 'New movie',
},
],
},
]);
})();
================================================
FILE: tests/ts/query-examples/graph-upserts.ts
================================================
import { UpsertGraphOptions } from '../../../typings/objection';
import { Person } from '../fixtures/person';
import { Animal } from '../fixtures/animal';
(async () => {
await Person.query().upsertGraph({
// This updates the `Jennifer Aniston` person since the id property is present.
id: 1,
firstName: 'Jonnifer',
parent: {
// This also gets updated since the id property is present. If no id was given
// here, Nancy Dow would get deleted, a new Person John Aniston would
// get inserted and related to Jennifer.
id: 2,
firstName: 'John',
lastName: 'Aniston',
},
// Notice that Kat the Cat is not listed in `pets`. It will get deleted.
pets: [
{
// Jennifer just got a new pet. Insert it and relate it to Jennifer. Notice
// that there is no id!
name: 'Wolfgang',
species: 'Dog',
},
{
// It turns out Doggo is a cat. Update it.
id: 1,
species: 'Cat',
},
],
// Notice that Wanderlust is missing from the list. It will get deleted.
// It is also worth mentioning that the Wanderlust's `reviews` or any
// other relations are NOT recursively deleted (unless you have
// defined `ON DELETE CASCADE` or other hooks in the db).
movies: [
{
id: 1,
// Upsert graphs can be arbitrarily deep. This modifies the
// reviews of "Horrible Bosses".
reviews: [
{
// Update a review.
id: 1,
stars: 2,
text: 'Even more Meh',
},
{
// And insert another one.
stars: 5,
title: 'Loved it',
text: 'Best movie ever',
},
{
// And insert a third one.
stars: 4,
title: '4 / 5',
text: 'Would see again',
},
],
},
],
});
let options: UpsertGraphOptions = {
relate: true,
unrelate: true,
};
await Person.query().upsertGraph(
{
// This updates the `Jennifer Aniston` person since the id property is present.
id: 1,
firstName: 'Jonnifer',
// Unrelate the parent. This doesn't delete it.
parent: null,
// Notice that Kat the Cat is not listed in `pets`. It will get unrelated.
pets: [
{
// Jennifer just got a new pet. Insert it and relate it to Jennifer. Notice
// that there is no id!
name: 'Wolfgang',
species: 'Dog',
},
{
// It turns out Doggo is a cat. Update it.
id: 1,
species: 'Cat',
},
],
// Notice that Wanderlust is missing from the list. It will get unrelated.
movies: [
{
id: 1,
// Upsert graphs can be arbitrarily deep. This modifies the
// reviews of "Horrible Bosses".
reviews: [
{
// Update a review.
id: 1,
stars: 2,
text: 'Even more Meh',
},
{
// And insert another one.
stars: 5,
title: 'Loved it',
text: 'Best movie ever',
},
],
},
{
// This is some existing movie that isn't currently related to Jennifer.
// It will get related.
id: 1253,
},
],
},
options,
);
options = {
// Only enable `unrelate` functionality for these two paths.
unrelate: ['pets', 'movies.reviews'],
// Only enable `relate` functionality for 'movies' relation.
relate: ['movies'],
// Disable deleting for movies.
noDelete: ['movies'],
};
await Person.query().upsertGraph(
{
id: 1,
// This gets deleted since `unrelate` list doesn't have 'parent' in it
// and deleting is the default behaviour.
parent: null,
// Notice that Kat the Cat is not listed in `pets`. It will get unrelated.
pets: [
{
// It turns out Doggo is a cat. Update it.
id: 1,
species: 'Cat',
},
],
// Notice that Wanderlust is missing from the list. It will NOT get unrelated
// or deleted since `unrelate` list doesn't contain `movies` and `noDelete`
// list does.
movies: [
{
id: 1,
// Upsert graphs can be arbitrarily deep. This modifies the
// reviews of "Horrible Bosses".
reviews: [
{
// Update a review.
id: 1,
stars: 2,
text: 'Even more Meh',
},
{
// And insert another one.
stars: 5,
title: 'Loved it',
text: 'Best movie ever',
},
],
},
{
// This is some existing movie that isn't currently related to Jennifer.
// It will get related.
id: 1253,
},
],
},
options,
);
// save an animal with only first name of the owner
// owner's type is defined as nullable with `Person | null` - and partial graph should still be accepted
// https://github.com/Vincit/objection.js/pull/2404
await Animal.query().upsertGraph({
species: 'Dog',
name: 'Wolfgang',
owner: { firstName: 'Jennifer' },
});
})();
================================================
FILE: tests/ts/query-examples/relation-queries/delete.ts
================================================
import { Person } from '../../fixtures/person';
(async () => {
const numberOfDeletedRows = await Person.query().delete().where('age', '>', 100);
await Person.query()
.delete()
.whereIn(
'id',
Person.query().select('persons.id').joinRelated('pets').where('pets.name', 'Fluffy'),
);
// This is another way to implement the same query.
await Person.query()
.delete()
.whereExists(Person.relatedQuery('pets').where('pets.name', 'Fluffy'));
const person = (await Person.query().findById(1))!;
// Delete all pets but cats and dogs of a person.
await person.$relatedQuery('pets').delete().whereNotIn('species', ['cat', 'dog']);
// Delete all pets of a person.
await person.$relatedQuery('pets').delete();
})();
================================================
FILE: tests/ts/query-examples/relation-queries/find.ts
================================================
import { Person } from '../../fixtures/person';
(async () => {
const person = await Person.query().findById(1);
const pets = await person!.$relatedQuery('pets').where('species', 'dog').orderBy('name');
console.log(pets[0].name);
const pets2 = await Person.relatedQuery('pets').for([1, 2, 3]);
console.log(pets2[0].species);
const movies = await Person.relatedQuery('movies').for(1);
console.log(movies[0].title);
const mom = await Person.relatedQuery('mom').for(1);
console.log(mom[0].firstName);
})();
================================================
FILE: tests/ts/query-examples/relation-queries/insert.ts
================================================
import { Person } from '../../fixtures/person';
(async () => {
const person = (await Person.query().findById(1))!;
console.log(person.firstName);
const fluffy = await person.$relatedQuery('pets').insert({ name: 'Fluffy' });
console.log(fluffy.species);
const movie = await person.$relatedQuery('movies').insert({ title: 'The room' });
console.log(movie.title);
})();
================================================
FILE: tests/ts/query-examples/relation-queries/relate.ts
================================================
import { Movie } from '../../fixtures/movie';
import { Person } from '../../fixtures/person';
(async () => {
const person = (await Person.query().findById(123))!;
await person.$relatedQuery('movies').relate(50);
await person.$relatedQuery('movies').relate([50, 60, 70]);
await person.$relatedQuery('movies').relate({
foo: 50,
bar: 20,
baz: 10,
});
const someMovie = (await Movie.query().findById(2))!;
await someMovie.$relatedQuery('actors').relate({
id: 50,
someExtra: "I'll be written to the join table",
});
})();
================================================
FILE: tests/ts/query-examples/relation-queries/unrelate.ts
================================================
import { Person } from '../../fixtures/person';
(async () => {
const person = await Person.query().findById(123);
const numUnrelatedRows = await person!.$relatedQuery('movies').unrelate().where('id', 50);
})();
================================================
FILE: tests/ts/query-examples/relation-queries/update.ts
================================================
import { raw, ref } from '../../../../';
import { Person } from '../../fixtures/person';
(async () => {
const numberOfAffectedRows = await Person.query()
.update({ firstName: 'Jennifer', lastName: 'Lawrence' })
.where('id', 134);
await Person.query().update({
firstName: raw("'Jenni' || 'fer'"),
lastName: 'Lawrence',
age: Person.query().avg('age'),
oldLastName: ref('lastName'),
});
await Person.query().update({
lastName: ref('someJsonColumn:mother.lastName').castText(),
'detailsJsonColumn:address.street': 'Elm street',
} as any);
})();
================================================
FILE: tests/ts/transactions/creating.ts
================================================
import { transaction } from '../../../';
import { Person } from '../fixtures/person';
(async () => {
try {
const returnValue = await transaction(Person.knex(), async (trx) => {
// Here you can use the transaction.
// Whatever you return from the transaction callback gets returned
// from the `transaction` function.
return 'the return value of the transaction';
});
// Here the transaction has been committed.
} catch (err) {
// Here the transaction has been rolled back.
}
})();
(async () => {
let trx;
try {
trx = await transaction.start(Person.knex());
// Here you can use the transaction.
// If you created the transaction using `transaction.start`, you need
// commit or rollback the transaction manually.
await trx.commit();
} catch (err) {
trx ? await trx.rollback() : null;
}
})();
================================================
FILE: tests/ts/transactions/using.ts
================================================
import { transaction, Transaction } from '../../../';
import { Animal } from '../fixtures/animal';
import { Person } from '../fixtures/person';
import { Knex } from 'knex';
(async () => {
const knex = Person.knex();
try {
const scrappy = await transaction(knex, async (trx) => {
const jennifer = await Person.query(trx).insert({
firstName: 'Jennifer',
lastName: 'Lawrence',
});
const scrappy = await jennifer.$relatedQuery('pets', trx).insert({ name: 'Scrappy' });
return scrappy;
});
} catch (err) {
throw err;
}
async function insertPersonAndPet(
personAttrs: Partial,
petAttrs: Partial,
trxOrKnex?: Transaction | Knex,
) {
const person = await Person.query(trxOrKnex).insert(personAttrs);
return person.$relatedQuery('pets', trxOrKnex).insert(petAttrs);
}
const personAttrs = {};
const petAttrs = {};
// All following four ways to call insertPersonAndPet work:
// 1.
const trx = await transaction.start(Person.knex());
await insertPersonAndPet(personAttrs, petAttrs, trx);
await trx.commit();
// 2.
await transaction(Person.knex(), async (trx) => {
await insertPersonAndPet(personAttrs, petAttrs, trx);
});
// 3.
await insertPersonAndPet(personAttrs, petAttrs, Person.knex());
// 4.
await insertPersonAndPet(personAttrs, petAttrs);
try {
const scrappy = await transaction(Person, Animal, async (Person, Animal) => {
// Person and Animal inside this function are bound to a newly
// created transaction. The Person and Animal outside this function
// are not! Even if you do `require('./models/Person')` inside this
// function and start a query using the required `Person` it will
// NOT take part in the transaction. Only the actual objects passed
// to this function are bound to the transaction.
await Person.query().insert({ firstName: 'Jennifer', lastName: 'Lawrence' });
return Animal.query().insert({ name: 'Scrappy' });
});
} catch (err) {
console.log('Something went wrong. Neither Jennifer nor Scrappy were inserted');
}
try {
const scrappy = await transaction(Person, async (Person) => {
const jennifer = await Person.query().insert({ firstName: 'Jennifer', lastName: 'Lawrence' });
// This creates a query using the `Animal` model class but we
// don't need to give `Animal` as one of the arguments to the
// transaction function because `jennifer` is an instance of
// the `Person` that is bound to a transaction.
return jennifer.$relatedQuery('pets').insert({ name: 'Scrappy' });
});
} catch (err) {
console.log('Something went wrong. Neither Jennifer nor Scrappy were inserted');
}
await transaction(Person, async (BoundPerson) => {
// This will be executed inside the transaction.
const jennifer = await BoundPerson.query().insert({
firstName: 'Jennifer',
lastName: 'Lawrence',
});
// OH NO! This query is executed outside the transaction
// since the `Animal` class is not bound to the transaction.
await Animal.query().insert({ name: 'Scrappy' });
// OH NO! This query is executed outside the transaction
// since the `Person` class is not bound to the transaction.
// BoundPerson !== Person.
await Person.query().insert({ firstName: 'Bradley' });
});
await transaction(Person, async (Person, trx) => {
// `trx` is the knex transaction object.
// It can be passed to `transacting`, `query` etc.
// methods, or used as a knex query builder.
if (!trx) {
throw new Error();
}
const jennifer = await trx('persons').insert({ firstName: 'Jennifer', lastName: 'Lawrence' });
const scrappy = await Animal.query(trx).insert({ name: 'Scrappy' });
const fluffy = await Animal.query().transacting(trx).insert({ name: 'Fluffy' });
return {
jennifer,
scrappy,
fluffy,
};
});
})();
================================================
FILE: tests/ts/validation.ts
================================================
import { ValidationError } from '../../typings/objection';
import { Person } from './fixtures/person';
(async () => {
const person = Person.fromJson({ firstName: 'jennifer', lastName: 'Lawrence' });
person.$validate();
person.$validate(person);
person.$validate({ firstName: 'jennifer' }, { patch: true });
await Person.query().insert({ firstName: 'jennifer', lastName: 'Lawrence' });
await Person.query().update({ firstName: 'jennifer', lastName: 'Lawrence' }).where('id', 10);
// Patch operation ignores the `required` property of the schema
// and only validates the given properties. This allows a subset
// of model's properties to be updated.
await Person.query().patch({ age: 24 }).where('age', '<', 24);
await Person.query().insertGraph({
firstName: 'Jennifer',
pets: [
{
name: 'Fluffy',
},
],
});
await Person.query().upsertGraph({
id: 1,
pets: [
{
name: 'Fluffy II',
},
],
});
try {
await Person.query().insert({ firstName: 'jennifer' });
} catch (err) {
console.log(err instanceof ValidationError); // --> true
}
// gh-1582
try {
throw new ValidationError({
statusCode: 409,
type: 'InvalidOneTimeCode',
message: 'Wrong code',
data: {
supplied: '1234',
},
modelClass: Person,
});
} catch (e) {
if (e instanceof ValidationError) {
if (e.type === 'InvalidOneTimeCode') {
console.log('one time code was invalid');
}
}
}
})();
================================================
FILE: tests/unit/model/AjvValidator.js
================================================
const addFormats = require('ajv-formats');
const { AjvValidator, Model } = require('../../../');
const expect = require('expect.js');
function modelClass(tableName, schema) {
return class TestModel extends Model {
static get tableName() {
return tableName;
}
static get jsonSchema() {
return schema;
}
};
}
describe('AjvValidator', () => {
describe('patch validator', () => {
const schema = {
definitions: {
TestRef1: {
type: 'object',
properties: {
aRequiredProp1: { type: 'string' },
},
required: ['aRequiredProp1'],
},
TestRef2: {
type: 'object',
properties: {
aRequiredProp2: { type: 'string' },
},
required: ['aRequiredProp2'],
},
},
anyOf: [{ $ref: '#/definitions/TestRef1' }, { $ref: '#/definitions/TestRef2' }],
};
const schema2 = {
type: 'object',
discriminator: { propertyName: 'foo' },
required: ['bar', 'foo'],
properties: {
bar: {
type: 'string',
},
},
oneOf: [
{
properties: {
foo: { const: 'x' },
a: { type: 'string' },
},
required: ['a'],
},
{
properties: {
foo: { enum: ['y', 'z'] },
b: { type: 'string' },
},
required: ['b'],
},
],
};
const schema3 = {
type: 'object',
properties: {
date: {
type: 'string',
format: 'date-time',
},
},
};
const schema4 = {
type: 'object',
properties: {
address: {
type: 'object',
required: ['city'],
properties: {
city: {
type: 'string',
},
zip: {
type: 'string',
},
street: {
type: 'string',
},
},
},
},
};
it('should remove required fields from definitions', () => {
const validator = new AjvValidator({});
const validators = validator.getValidator(modelClass('test', schema), schema, true);
const definitions = Object.entries(validators.schema.definitions);
expect(definitions.length).to.be(2);
definitions.forEach((d) => expect(d.required).to.be(undefined));
});
it('should not remove required fields if there is a discriminator', () => {
const validator = new AjvValidator({
options: {
discriminator: true,
},
});
const validators = validator.getValidator(modelClass('test', schema2), schema2, true);
expect(validators.schema.required).to.eql(['foo']);
});
it('should add ajv formats by default', () => {
expect(() => {
const validator = new AjvValidator({});
validator.getValidator(modelClass('test', schema3), schema3, true);
}).to.not.throwException();
});
it('should remove required fields in inner properties', () => {
const validator = new AjvValidator({});
const validators = validator.getValidator(modelClass('test', schema4), schema4, true);
expect(validators.schema.properties.address.properties).to.not.be(undefined);
expect(validators.schema.properties.address.required).to.be(undefined);
});
it('should not throw errors when adding formats in onCreateAjv hook', () => {
expect(() => {
new AjvValidator({
onCreateAjv: (ajv) => {
addFormats(ajv);
},
});
}).to.not.throwException();
});
it('should handle empty definitions', () => {
const emptyDefinitionsSchema = {
type: 'object',
required: ['a'],
definitions: {},
additionalProperties: false,
properties: {
a: { type: 'string' },
},
};
const validator = new AjvValidator({});
validator.getValidator(
modelClass('test', emptyDefinitionsSchema),
emptyDefinitionsSchema,
true,
);
});
});
});
================================================
FILE: tests/unit/model/Model.js
================================================
const _ = require('lodash');
const expect = require('expect.js');
const { Model, QueryBuilder, ValidationError, raw, fn } = require('../../../');
describe('Model', () => {
describe('fromJson', () => {
let Model1;
beforeEach(() => {
Model1 = modelClass('Model1');
});
it('should copy attributes to the created object', () => {
let json = { a: 1, b: 2, c: { d: 'str1' }, e: [3, 4, { f: 'str2' }] };
let model = Model1.fromJson(json);
expect(model.a).to.equal(1);
expect(model.b).to.equal(2);
expect(model.c.d).to.equal('str1');
expect(model.e[0]).to.equal(3);
expect(model.e[1]).to.equal(4);
expect(model.e[2].f).to.equal('str2');
});
it('should skip properties starting with $', () => {
let model = Model1.fromJson({ a: 1, $b: 2 });
expect(model.a).to.equal(1);
expect(model).to.not.have.property('$b');
});
it('should skip functions', () => {
let model = Model1.fromJson({ a: 1, b: () => {} });
expect(model.a).to.equal(1);
expect(model).to.not.have.property('b');
});
it('should call $parseJson', () => {
let calls = 0;
let json = { a: 1 };
let options = { b: 2 };
Model1.prototype.$parseJson = function (jsn, opt) {
++calls;
expect(jsn).to.eql(json);
expect(opt).to.eql(options);
return { c: 3 };
};
let model = Model1.fromJson(json, options);
expect(model).to.not.have.property('a');
expect(model.c).to.equal(3);
expect(calls).to.equal(1);
});
it('should validate if jsonSchema is defined', () => {
Model1.jsonSchema = {
type: 'object',
required: ['a'],
additionalProperties: false,
properties: {
a: { type: 'string' },
b: { type: 'number' },
c: {
type: 'object',
properties: {
d: { type: 'string' },
e: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: {
f: { type: 'number' },
},
},
},
},
},
},
};
expect(() => {
Model1.fromJson({ a: 'str', b: 1 });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 'str' });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 'a', c: { d: 'test' } });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 'a', c: { d: 'test', e: [{ f: 1 }] } });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 1, b: '1' });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('a');
expect(exp.data).to.have.property('b');
});
expect(() => {
Model1.fromJson({ b: 1 });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('a');
});
expect(() => {
Model1.fromJson({ a: 'a', additional: 1 });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('additional');
});
expect(() => {
Model1.fromJson({ a: 'a', c: { d: 10 } });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('c.d');
});
expect(() => {
Model1.fromJson({ a: 'a', c: { d: 'test', e: [{ f: 'not a number' }] } });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('c.e.0.f');
});
expect(() => {
Model1.fromJson({ a: 'a', c: { d: 'test', e: [{ additional: true }] } });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('c.e.0.additional');
});
});
it('should call $validate if jsonSchema is defined', () => {
let calls = 0;
let json = { a: 'str', b: 2 };
let options = { some: 'option' };
Model1.jsonSchema = {
required: ['a'],
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
};
Model1.prototype.$validate = function (jsn, opt) {
Model.prototype.$validate.call(this, jsn, opt);
++calls;
expect(opt).to.eql(options);
expect(jsn).to.eql(json);
};
expect(() => {
Model1.fromJson(json, options);
}).to.not.throwException((err) => {
console.log(err.stack);
});
expect(calls).to.equal(1);
});
it('should only call jsonSchema once if jsonSchema is a getter', () => {
let calls = 0;
Object.defineProperty(Model1, 'jsonSchema', {
get: () => {
++calls;
return {
required: ['a'],
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
};
},
});
for (let i = 0; i < 10; ++i) {
Model1.fromJson({ a: 'str', b: 2 });
}
let model = Model1.fromJson({ a: 'str', b: 2 });
model.$validate();
model.$validate();
model.$toJson();
model.$toDatabaseJson();
expect(calls).to.equal(1);
});
it('should call $beforeValidate if jsonSchema is defined', () => {
let calls = 0;
let json = { a: 1, b: 2 };
let options = { some: 'option' };
Model1.jsonSchema = {
required: ['a'],
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
};
Model1.prototype.$beforeValidate = function (schema, jsn, opt) {
++calls;
expect(opt).to.eql(options);
expect(jsn).to.eql(json);
expect(schema).to.eql(Model1.jsonSchema);
schema.properties.a.type = 'number';
return schema;
};
expect(() => {
Model1.fromJson(json, options);
}).to.not.throwException();
expect(calls).to.equal(1);
});
it('should call $afterValidate if jsonSchema is defined', () => {
let calls = 0;
let json = { a: 'str', b: 2 };
let options = { some: 'option' };
Model1.jsonSchema = {
required: ['a'],
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
};
Model1.prototype.$afterValidate = function (jsn, opt) {
++calls;
expect(opt).to.eql(options);
expect(jsn).to.eql(json);
};
expect(() => {
Model1.fromJson(json, options);
}).to.not.throwException();
expect(calls).to.equal(1);
});
it('should skip requirement validation if options.patch == true', () => {
Model1.jsonSchema = {
required: ['a'],
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
};
expect(() => {
Model1.fromJson({ a: 'str', b: 1 }, { patch: true });
}).to.not.throwException();
// b is not required.
expect(() => {
Model1.fromJson({ a: 'str' }, { patch: true });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 1, b: '1' }, { patch: true });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('a');
expect(exp.data).to.have.property('b');
});
expect(() => {
Model1.fromJson({ b: 1 }, { patch: true });
}).to.not.throwException();
});
it('should skip requirement validation if options.patch == true (oneOf)', () => {
Model1.jsonSchema = {
oneOf: [
{
required: ['a'],
},
{
required: ['b'],
},
],
properties: {
a: { type: 'string' },
b: { type: 'number' },
c: { type: 'string' },
},
};
expect(() => {
Model1.fromJson({ c: 'str' });
}).to.throwException();
expect(() => {
Model1.fromJson({ a: 'str' });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ b: 1 });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ c: 'str' }, { patch: true });
}).to.not.throwException((err) => console.log(err));
});
it('should skip requirement validation if options.patch == true (anyOf)', () => {
Model1.jsonSchema = {
anyOf: [
{
required: ['a'],
},
{
required: ['b'],
},
],
properties: {
a: { type: 'string' },
b: { type: 'number' },
c: { type: 'string' },
},
};
expect(() => {
Model1.fromJson({ c: 'str' });
}).to.throwException();
expect(() => {
Model1.fromJson({ a: 'str' });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ b: 1 });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ c: 'str' }, { patch: true });
}).to.not.throwException((err) => console.log(err));
});
it('should skip requirement validation if options.patch == true (if/then)', () => {
Model1.jsonSchema = {
properties: {
a: { type: 'string' },
b: { type: 'number' },
c: { type: 'string' },
},
if: {
properties: {
a: {
enum: ['foo'],
},
},
},
then: {
required: ['b'],
},
else: {
required: ['c'],
},
};
expect(() => {
Model1.fromJson({ a: 'foo' });
}).to.throwException();
expect(() => {
Model1.fromJson({ a: 'bar' });
}).to.throwException();
expect(() => {
Model1.fromJson({ a: 'foo', b: 1 });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 'bar', c: 'baz' });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 'foo' }, { patch: true });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 'bar' }, { patch: true });
}).to.not.throwException();
});
it('should skip validation if options.skipValidation == true', () => {
Model1.jsonSchema = {
required: ['a'],
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
};
expect(() => {
Model1.fromJson({ a: 'str', b: 1 }, { skipValidation: true });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 'str' }, { skipValidation: true });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ a: 1, b: '1' }, { skipValidation: true });
}).to.not.throwException();
expect(() => {
Model1.fromJson({ b: 1 }, { skipValidation: true });
}).to.not.throwException();
});
it('should merge default values from jsonSchema', () => {
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' });
expect(model.a).to.equal('str');
expect(model.b).to.equal(666);
expect(model.c).to.eql(obj);
expect(model.c).to.not.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).to.not.equal(obj);
});
// regression introduced in 0.6
// https://github.com/Vincit/objection.js/issues/205
it('should not throw TypeError when jsonSchema.properties == undefined', () => {
Model1.jsonSchema = {
required: ['a'],
};
let model = Model1.fromJson({ a: 100 });
expect(model.a).to.equal(100);
});
it('should validate but not pass if jsonSchema.required exists and jsonSchema.properties == undefined', () => {
Model1.jsonSchema = {
required: ['a'],
};
expect(() => {
Model1.fromJson({ b: 200 });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
});
});
it('should not merge default values from jsonSchema if options.patch == true', () => {
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({ b: 10 }, { patch: true });
expect(model).to.not.have.property('a');
expect(model.b).to.equal(10);
expect(model).to.not.have.property('c');
});
it('should throw with error context if validation fails', () => {
Model1.jsonSchema = {
required: ['a'],
properties: {
a: { type: 'number' },
b: { type: 'string', minLength: 4 },
},
};
expect(() => {
Model1.fromJson({ b: 'abc' });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('a');
expect(exp.data['a']).to.be.a(Array);
expect(exp.data['a'].length).to.be.above(0);
expect(exp.data['a'][0]).to.have.property('message');
expect(exp.data['a'][0]).to.have.property('keyword');
expect(exp.data['a'][0]).to.have.property('params');
expect(exp.data['a'][0].keyword).to.equal('required');
expect(exp.data).to.have.property('b');
expect(exp.data['b']).to.be.a(Array);
expect(exp.data['b'].length).to.be.above(0);
expect(exp.data['b'][0]).to.have.property('message');
expect(exp.data['b'][0]).to.have.property('keyword');
expect(exp.data['b'][0]).to.have.property('params');
expect(exp.data['b'][0].keyword).to.equal('minLength');
expect(exp.data['b'][0].params).to.have.property('limit');
expect(exp.data['b'][0].params.limit).to.equal(4);
});
});
it('should throw if anything non-object is given', () => {
function SomeClass() {}
expect(() => {
Model1.fromJson();
}).to.not.throwException();
expect(() => {
Model1.fromJson(null);
}).to.not.throwException();
expect(() => {
Model1.fromJson(undefined);
}).to.not.throwException();
expect(() => {
Model1.fromJson({});
}).to.not.throwException();
expect(() => {
Model1.fromJson(new SomeClass());
}).to.not.throwException();
expect(() => {
Model1.fromJson('hello');
}).to.throwException();
expect(() => {
Model1.fromJson(new String('hello'));
}).to.throwException();
expect(() => {
Model1.fromJson(1);
}).to.throwException();
expect(() => {
Model1.fromJson(new Number(1));
}).to.throwException();
expect(() => {
Model1.fromJson([{ a: 1 }]);
}).to.throwException();
expect(() => {
Model1.fromJson(/.*/);
}).to.throwException();
expect(() => {
Model1.fromJson(new Date());
}).to.throwException();
expect(() => {
Model1.fromJson(() => {});
}).to.throwException();
expect(() => {
Model1.fromJson(new Int16Array(100));
}).to.throwException();
});
it('should be capable to return multiple validation errors per property', () => {
Model1.jsonSchema = {
required: ['a'],
properties: {
a: {
type: 'string',
minLength: 5,
pattern: '^\\d+$',
},
},
};
expect(() => {
Model1.fromJson({ a: 'four' });
}).to.throwException((exp) => {
expect(exp).to.be.a(ValidationError);
expect(exp.data).to.have.property('a');
expect(exp.data['a']).to.be.a(Array);
expect(exp.data['a']).to.have.length(2);
expect(exp.data['a'][0]).to.have.property('message');
expect(exp.data['a'][0]).to.have.property('keyword');
expect(exp.data['a'][0]).to.have.property('params');
expect(exp.data['a'][0].keyword).to.equal('pattern');
expect(exp.data['a'][1]).to.have.property('message');
expect(exp.data['a'][1]).to.have.property('keyword');
expect(exp.data['a'][1]).to.have.property('params');
expect(exp.data['a'][1].keyword).to.equal('minLength');
});
});
it('should parse relations into Model instances and remove them from database representation', () => {
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,
relation1: [
{ id: 11, model1Id: 10 },
{ id: 12, model1Id: 10 },
],
relation2: { id: 13, model1Id: null },
});
expect(model.relation1[0]).to.be.a(Model2);
expect(model.relation1[1]).to.be.a(Model2);
expect(model.relation2).to.be.a(Model1);
let json = model.$toDatabaseJson();
expect(json).to.not.have.property('relation1');
expect(json).to.not.have.property('relation2');
json = model.$toJson();
expect(json).to.have.property('relation1');
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');
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,
relation1: [
{ id: 11, model1Id: 10 },
{ id: 12, model1Id: 10 },
],
relation2: { id: 13, model1Id: null },
},
{ skipParseRelations: true },
);
expect(model.relation1[0]).to.not.be.a(Model2);
expect(model.relation1[1]).to.not.be.a(Model2);
expect(model.relation2).to.not.be.a(Model1);
});
it('should NOT try to parse non-object relations into Model instances', () => {
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,
relation1: [1, 2, '3', null, undefined, 6],
relation2: '5',
},
{ skipParseRelations: true },
);
expect(model.relation1).to.eql([1, 2, '3', null, undefined, 6]);
expect(model.relation2).to.eql('5');
});
it('null relations should be null in the result', () => {
let Model = modelClass('Model');
Model.relationMappings = {
someRelation: {
relation: Model.BelongsToOneRelation,
modelClass: Model,
join: {
from: 'Model.id',
to: 'Model.model1Id',
},
},
};
let model = Model.fromJson({ a: 1, b: 2, someRelation: null });
expect(model.someRelation).to.equal(null);
});
});
describe('ensureModel', () => {
let Model1;
let Model2;
beforeEach(() => {
Model1 = modelClass('Model1');
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',
},
},
};
});
it('should parse nested relations into model instances even if the root is a model', () => {
let model1 = Model1.fromJson({
id: 10,
model1Id: 13,
});
model1.relation1 = [{ value: 1 }, { value: 2 }];
model1.relation2 = { value: 3, relation1: [{ value: 4 }] };
let model2 = Model1.ensureModel(model1);
expect(model2 === model1).to.equal(true);
expect(model2.relation1[0]).to.be.a(Model2);
expect(model2.relation1[1]).to.be.a(Model2);
expect(model2.relation2).to.be.a(Model1);
expect(model2.relation2.relation1[0]).to.be.a(Model2);
});
it('should not mutate if the whole tree already is models', () => {
let model1 = Model1.fromJson({
id: 10,
model1Id: 13,
relation1: [{ value: 1 }, { value: 2 }],
relation2: { value: 3, relation1: [{ value: 4 }] },
});
let model2 = Model1.ensureModel(model1);
expect(model2 === model1).to.equal(true);
expect(model2.relation1 === model2.relation1).to.equal(true);
expect(model2.relation1[0] === model2.relation1[0]).to.equal(true);
expect(model2.relation1[1] === model2.relation1[1]).to.equal(true);
expect(model2.relation2 === model2.relation2).to.equal(true);
expect(model2.relation2.relation1[0] === model2.relation2.relation1[0]).to.equal(true);
});
it('should work with circular references', () => {
let obj1 = { value: 1 };
let obj2 = { value: 2 };
obj1.relation2 = obj2;
obj2.relation2 = obj1;
const model = Model1.ensureModel(obj1);
expect(model).to.be.a(Model1);
expect(model.relation2).to.be.a(Model1);
expect(model.relation2.relation2 === model).to.equal(true);
expect(model.value).to.equal(1);
expect(model.relation2.value).to.equal(2);
});
});
describe('fromDatabaseJson', () => {
let Model1;
beforeEach(() => {
Model1 = createModelClass();
});
it('should copy attributes to the created object', () => {
let json = { a: 1, b: 2, c: { d: 'str1' }, e: [3, 4, { f: 'str2' }] };
let model = Model1.fromDatabaseJson(json);
expect(model.a).to.equal(1);
expect(model.b).to.equal(2);
expect(model.c.d).to.equal('str1');
expect(model.e[0]).to.equal(3);
expect(model.e[1]).to.equal(4);
expect(model.e[2].f).to.equal('str2');
});
it('should call $parseDatabaseJson', () => {
let calls = 0;
let json = { a: 1 };
Model1.prototype.$parseDatabaseJson = (jsn) => {
++calls;
expect(jsn).to.eql(json);
return { c: 3 };
};
let model = Model1.fromDatabaseJson(json);
expect(model).to.not.have.property('a');
expect(model.c).to.equal(3);
expect(calls).to.equal(1);
});
});
describe('$toJson', () => {
let Model1;
beforeEach(() => {
Model1 = createModelClass();
});
it('should return the internal representation by default', () => {
expect(Model1.fromJson({ a: 1, b: 2, c: { d: [1, 3] } }).$toJson()).to.eql({
a: 1,
b: 2,
c: { d: [1, 3] },
});
});
it('should call $formatJson', () => {
let calls = 0;
let json = { a: 1 };
Model1.prototype.$formatJson = (jsn) => {
++calls;
expect(jsn).to.eql(json);
jsn.b = 2;
return jsn;
};
let model = Model1.fromJson(json);
let output = model.$toJson();
expect(output.a).to.equal(1);
expect(output.b).to.equal(2);
expect(calls).to.equal(1);
});
it('should call $toJson for properties of class Model', () => {
let Model2 = createModelClass();
Model2.prototype.$formatJson = (jsn) => {
jsn.d = 3;
return jsn;
};
let model = Model1.fromJson({ a: 1 });
model.b = Model2.fromJson({ c: 2 });
model.e = [Model2.fromJson({ f: 100 })];
expect(model.$toJson()).to.eql({ a: 1, b: { c: 2, d: 3 }, e: [{ f: 100, d: 3 }] });
});
it('should return a deep copy', () => {
let json = { a: 1, b: [{ c: 2 }], d: { e: 'str' } };
let model = Model1.fromJson(json);
let output = model.$toJson();
expect(output).to.eql(json);
expect(output.b).to.not.equal(json.b);
expect(output.b[0]).to.not.equal(json.b[0]);
expect(output.d).to.not.equal(json.d);
});
it('should be called by JSON.stringify', () => {
Model1.prototype.$formatJson = (jsn) => {
jsn.b = 2;
return jsn;
};
let model = Model1.fromJson({ a: 1 });
expect(JSON.stringify(model)).to.equal('{"a":1,"b":2}');
});
it('properties registered using $omitFromJson method should be removed from the json', () => {
let model = Model1.fromJson({ a: 1, b: 2, c: 3 });
model.$omitFromJson(['b', 'c']);
expect(model.$toJson()).to.eql({ a: 1 });
expect(model).to.eql({ a: 1, b: 2, c: 3 });
});
it('properties registered using $omitFromJson method should be removed from the json (multiple calls)', () => {
let model = Model1.fromJson({ a: 1, b: 2, c: 3 });
model.$omitFromJson(['b']);
model.$omitFromJson(['c']);
model.$omitFromDatabaseJson(['a']);
expect(model.$toJson()).to.eql({ a: 1 });
expect(model).to.eql({ a: 1, b: 2, c: 3 });
});
});
describe('$toDatabaseJson', () => {
let Model1;
beforeEach(() => {
Model1 = createModelClass();
});
it('should return then internal representation by default', () => {
expect(Model1.fromJson({ a: 1, b: 2, c: { d: [1, 3] } }).$toDatabaseJson()).to.eql({
a: 1,
b: 2,
c: { d: [1, 3] },
});
});
it('should format JSON attributes of all kinds', () => {
Model1.jsonAttributes = ['a', 'b', 'c', 'd', 'e', 'f'];
expect(
Model1.fromJson({
a: 1,
b: 'one',
c: { d: [1, 3] },
d: [1, 2, 3],
e: null,
f: undefined,
g: 1,
h: 'one',
i: { d: [1, 3] },
j: [1, 2, 3],
k: null,
l: undefined,
}).$toDatabaseJson(),
).to.eql({
a: '1',
b: '"one"',
c: '{"d":[1,3]}',
d: '[1,2,3]',
e: null,
g: 1,
h: 'one',
i: { d: [1, 3] },
j: [1, 2, 3],
k: null,
});
Model1.jsonAttributes = [];
});
it('should call $formatDatabaseJson', () => {
let calls = 0;
let json = { a: 1 };
Model1.prototype.$formatDatabaseJson = (jsn) => {
++calls;
expect(jsn).to.eql(json);
jsn.b = 2;
return jsn;
};
let model = Model1.fromJson(json);
let output = model.$toDatabaseJson();
expect(output.a).to.equal(1);
expect(output.b).to.equal(2);
expect(calls).to.equal(1);
});
it('should return a deep copy', () => {
let json = { a: 1, b: [{ c: 2 }], d: { e: 'str' } };
let model = Model1.fromJson(json);
let output = model.$toDatabaseJson();
expect(output).to.eql(json);
expect(output.b).to.not.equal(json.b);
expect(output.b[0]).to.not.equal(json.b[0]);
expect(output.d).to.not.equal(json.d);
});
it('properties registered using $omitFromDatabaseJson method should be removed from the json', () => {
let model = Model1.fromJson({ a: 1, b: 2, c: 3 });
model.$omitFromDatabaseJson(['b', 'c']);
expect(model.$toDatabaseJson()).to.eql({ a: 1 });
expect(model).to.eql({ a: 1, b: 2, c: 3 });
});
it('properties registered using $omitFromDatabaseJson method should be removed from the json (multiple calls)', () => {
let model = Model1.fromJson({ a: 1, b: 2, c: 3 });
model.$omitFromDatabaseJson(['b']);
model.$omitFromDatabaseJson(['c']);
model.$omitFromJson(['a']);
expect(model.$toDatabaseJson()).to.eql({ a: 1 });
expect(model).to.eql({ a: 1, b: 2, c: 3 });
});
});
describe('$clone', () => {
let Model1;
beforeEach(() => {
Model1 = createModelClass();
});
it('should clone', () => {
let Model2 = createModelClass();
Model2.prototype.$formatJson = (jsn) => {
jsn.d = 3;
return jsn;
};
let model = Model1.fromJson({ a: 1, g: { h: 100 }, r: [{ h: 50 }] });
model.b = Model2.fromJson({ c: 2 });
model.e = [Model2.fromJson({ f: 100 })];
let clone = model.$clone();
expect(clone).to.eql(model);
expect(clone.$toJson()).to.eql(model.$toJson());
expect(clone.$toJson()).to.eql({
a: 1,
g: { h: 100 },
r: [{ h: 50 }],
b: { c: 2, d: 3 },
e: [{ f: 100, d: 3 }],
});
expect(clone.g).to.not.equal(model.g);
expect(clone.r[0]).to.not.equal(model.r[0]);
expect(clone.b).to.not.equal(model.b);
expect(clone.e[0]).to.not.equal(model.e[0]);
});
it('should shallow clone', () => {
let Model = modelClass('Model');
Model.relationMappings = {
someRelation: {
relation: Model.BelongsToOneRelation,
modelClass: Model,
join: {
from: 'Model.id',
to: 'Model.model1Id',
},
},
};
let model = Model.fromJson({ a: 1, b: 2, someRelation: { a: 3, b: 4 } });
expect(model.$clone()).to.eql({ a: 1, b: 2, someRelation: { a: 3, b: 4 } });
expect(model.$clone({ shallow: true })).to.eql({ a: 1, b: 2 });
});
});
describe('propertyNameToColumnName', () => {
let Model1;
beforeEach(() => {
Model1 = createModelClass({
$formatDatabaseJson: (json) => {
return _.mapKeys(json, (value, key) => {
return _.snakeCase(key);
});
},
});
});
it('should convert a property name to column name', () => {
expect(Model1.propertyNameToColumnName('someProperty')).to.equal('some_property');
});
});
describe('columnNameToPropertyName', () => {
let Model1;
beforeEach(() => {
Model1 = createModelClass({
$parseDatabaseJson: (json) => {
return _.mapKeys(json, (value, key) => {
return _.camelCase(key);
});
},
});
});
it('should convert a column name to property name', () => {
expect(Model1.columnNameToPropertyName('some_property')).to.equal('someProperty');
});
});
describe('virtualAttributes', () => {
it('should include getters', () => {
class Model1 extends Model {
get foo() {
return this.a + this.b;
}
get bar() {
return this.a + this.b;
}
static get virtualAttributes() {
return ['foo'];
}
}
expect(
Model1.fromJson({
a: 100,
b: 10,
rel1: Model1.fromJson({ a: 101, b: 11 }),
rel2: [Model1.fromJson({ a: 102, b: 12 }), Model1.fromJson({ a: 103, b: 13 })],
}).toJSON(),
).to.eql({
a: 100,
b: 10,
foo: 110,
rel1: {
a: 101,
b: 11,
foo: 112,
},
rel2: [
{ a: 102, b: 12, foo: 114 },
{ a: 103, b: 13, foo: 116 },
],
});
});
it('should ignore virtuals when virtuals: false option is passed to toJSON', () => {
class Model1 extends Model {
get foo() {
return this.a + this.b;
}
get bar() {
return this.a + this.b;
}
static get virtualAttributes() {
return ['foo'];
}
}
expect(
Model1.fromJson({
a: 100,
b: 10,
rel1: Model1.fromJson({ a: 101, b: 11 }),
rel2: [Model1.fromJson({ a: 102, b: 12 }), Model1.fromJson({ a: 103, b: 13 })],
}).toJSON({ virtuals: false }),
).to.eql({
a: 100,
b: 10,
rel1: {
a: 101,
b: 11,
},
rel2: [
{ a: 102, b: 12 },
{ a: 103, b: 13 },
],
});
});
it('should ignore virtuals when virtuals: false option is passed to $toJson', () => {
class Model1 extends Model {
get foo() {
return this.a + this.b;
}
get bar() {
return this.a + this.b;
}
static get virtualAttributes() {
return ['foo'];
}
}
expect(
Model1.fromJson({
a: 100,
b: 10,
rel1: Model1.fromJson({ a: 101, b: 11 }),
rel2: [Model1.fromJson({ a: 102, b: 12 }), Model1.fromJson({ a: 103, b: 13 })],
}).$toJson({ virtuals: false }),
).to.eql({
a: 100,
b: 10,
rel1: {
a: 101,
b: 11,
},
rel2: [
{ a: 102, b: 12 },
{ a: 103, b: 13 },
],
});
});
it('should pick a set of virtuals if array is passed to in `virtuals` option', () => {
class Model1 extends Model {
get foo() {
return this.a + this.b;
}
get bar() {
return this.a * this.b;
}
static get virtualAttributes() {
return ['foo'];
}
}
expect(
Model1.fromJson({
a: 100,
b: 10,
rel1: Model1.fromJson({ a: 101, b: 11 }),
rel2: [Model1.fromJson({ a: 102, b: 12 }), Model1.fromJson({ a: 103, b: 13 })],
}).$toJson({ virtuals: ['foo', 'bar'] }),
).to.eql({
a: 100,
b: 10,
foo: 110,
bar: 1000,
rel1: {
a: 101,
b: 11,
foo: 112,
bar: 1111,
},
rel2: [
{ a: 102, b: 12, foo: 114, bar: 1224 },
{ a: 103, b: 13, foo: 116, bar: 1339 },
],
});
});
it('should include virtualAttributes for related models', () => {
class Model1 extends modelClass('Model1') {
static get virtualAttributes() {
return ['foo'];
}
get foo() {
return 'foo';
}
}
class Model2 extends modelClass('Model2') {
static get virtualAttributes() {
return ['bar'];
}
static get relationMappings() {
return {
model1: {
relation: Model.BelongsToOneRelation,
modelClass: Model1,
join: {
from: 'Model2.model1Id',
to: 'Model1.id',
},
},
};
}
get bar() {
return 'bar';
}
}
let model2 = Model2.fromJson({
a: 'a',
model1: {
b: 'b',
c: 'c',
},
});
expect(model2.toJSON()).to.eql({
a: 'a',
bar: 'bar',
model1: {
b: 'b',
c: 'c',
foo: 'foo',
},
});
});
it('should default to virtuals = true even when an options object with no `virtuals` property is passed', () => {
class Model1 extends modelClass('Model1') {
static get virtualAttributes() {
return ['foo'];
}
get foo() {
return 'foo';
}
}
class Model2 extends modelClass('Model2') {
static get virtualAttributes() {
return ['bar'];
}
static get relationMappings() {
return {
model1: {
relation: Model.BelongsToOneRelation,
modelClass: Model1,
join: {
from: 'Model2.model1Id',
to: 'Model1.id',
},
},
};
}
get bar() {
return 'bar';
}
}
let model2 = Model2.fromJson({
a: 'a',
model1: {
b: 'b',
c: 'c',
},
});
expect(model2.toJSON({})).to.eql({
a: 'a',
bar: 'bar',
model1: {
b: 'b',
c: 'c',
foo: 'foo',
},
});
});
it('should include methods', () => {
class Model1 extends Model {
foo() {
return this.a + this.b;
}
bar() {
return this.a + this.b;
}
static get virtualAttributes() {
return ['foo'];
}
}
expect(Model1.fromJson({ a: 100, b: 10 }).toJSON()).to.eql({
a: 100,
b: 10,
foo: 110,
});
});
it('should not try to set readonly properties', () => {
class Model1 extends Model {
get foo() {
return this.a + this.b;
}
// Should ignore all getter-only properties. Not only virtual.
get notEvenVirtual() {
return 'imNotVirtual';
}
get bar() {
return this.c;
}
set bar(c) {
this.c = c;
}
baz() {
return 2 * this.a;
}
static get virtualAttributes() {
return ['foo', 'bar', 'baz'];
}
}
const model = Model1.fromJson({
a: 10,
b: 100,
bar: 1000,
foo: 200,
baz: 300,
notEvenVirtual: 2000,
});
expect(model.toJSON()).to.eql({
a: 10,
b: 100,
c: 1000,
foo: 110,
bar: 1000,
baz: 20,
});
expect(model.$toDatabaseJson()).to.eql({
a: 10,
b: 100,
c: 1000,
});
});
it('should not try to set readonly properties from super classes', () => {
class BaseModel extends Model {
static get virtualAttributes() {
return ['foo'];
}
get foo() {
return this.a + this.b;
}
}
class Model1 extends BaseModel {}
expect(Model1.fromJson({ a: 100, b: 10, foo: 666 }).toJSON()).to.eql({
a: 100,
b: 10,
foo: 110,
});
expect(Model1.fromJson({ a: 100, b: 10, foo: 666 }).$toDatabaseJson()).to.eql({
a: 100,
b: 10,
});
});
});
describe('cloneObjectAttributes', () => {
it('should clone object attributes by default when calling $toJson or $toDatabaseJson', () => {
class Person extends Model {}
const obj = {
foo: {
bar: 1,
},
};
const person = Person.fromDatabaseJson({
objectField: obj,
});
expect(person.objectField).to.equal(obj);
let json = person.$toDatabaseJson();
expect(person.objectField).to.equal(obj);
expect(json.objectField).to.eql(obj);
expect(json.objectField).to.not.equal(obj);
json = person.$toJson();
expect(person.objectField).to.equal(obj);
expect(json.objectField).to.eql(obj);
expect(json.objectField).to.not.equal(obj);
json = person.toJSON();
expect(person.objectField).to.equal(obj);
expect(json.objectField).to.eql(obj);
expect(json.objectField).to.not.equal(obj);
});
it('should NOT clone object attributes when calling $toJson or $toDatabaseJson if Model.cloneObjectAttributes = false', () => {
class Person extends Model {
static get cloneObjectAttributes() {
return false;
}
}
const obj = {
foo: {
bar: 1,
},
};
const person = Person.fromDatabaseJson({
objectField: obj,
});
expect(person.objectField).to.equal(obj);
let json = person.$toDatabaseJson();
expect(person.objectField).to.equal(obj);
expect(json.objectField).to.equal(obj);
json = person.$toJson();
expect(person.objectField).to.equal(obj);
expect(json.objectField).to.equal(obj);
json = person.toJSON();
expect(person.objectField).to.equal(obj);
expect(json.objectField).to.equal(obj);
});
});
it('relationMappings can be a function', () => {
let Model1 = modelClass('Model1');
let Model2 = modelClass('Model2');
Model1.relationMappings = () => {
return {
relation1: {
relation: Model.HasManyRelation,
modelClass: Model2,
join: {
from: 'Model1.id',
to: 'Model2.model1Id',
},
},
};
};
expect(Model1.getRelation('relation1').relatedModelClass).to.equal(Model2);
});
it('if pickJsonSchemaProperties = true and jsonSchema is given, should remove all but schema properties from database representation', () => {
let Model = modelClass('Model');
Model.pickJsonSchemaProperties = true;
Model.jsonSchema = {
type: 'object',
properties: {
prop1: { type: 'number' },
prop2: { type: 'string' },
},
};
let model = Model.fromJson({
prop1: 10,
prop2: '10',
prop3: 'should be removed',
prop4: { also: 'this' },
});
let json = model.$toDatabaseJson();
expect(json.prop1).to.equal(10);
expect(json.prop2).to.equal('10');
expect(json.prop3).to.equal(undefined);
expect(json.prop4).to.equal(undefined);
expect(model.prop1).to.equal(10);
expect(model.prop2).to.equal('10');
expect(model.prop3).to.equal('should be removed');
expect(model.prop4).to.eql({ also: 'this' });
json = model.$toJson();
expect(json.prop1).to.equal(10);
expect(json.prop2).to.equal('10');
expect(json.prop3).to.equal('should be removed');
expect(json.prop4).to.eql({ also: 'this' });
});
it('if pickJsonSchemaProperties = true and jsonSchema is given, should omit relations even if defined in jsonSchema', () => {
let Model = modelClass('Model');
Model.pickJsonSchemaProperties = true;
Model.relationMappings = {
someRelation: {
relation: Model.BelongsToOneRelation,
modelClass: Model,
join: {
from: 'Model.id',
to: 'Model.model1Id',
},
},
};
Model.jsonSchema = {
type: 'object',
properties: {
someRelation: { type: 'object' },
},
};
let model = Model.fromJson({
someRelation: {
value: 'should be removed',
},
});
let json = model.$toDatabaseJson();
expect(json.someRelation).to.equal(undefined);
expect(model.someRelation).to.eql({ value: 'should be removed' });
json = model.$toJson();
expect(json.someRelation).to.eql({ value: 'should be removed' });
});
it('if pickJsonSchemaProperties = false, should select all properties even if jsonSchema is defined', () => {
// pickJsonSchemaProperties = false is the default.
let Model = modelClass('Model');
Model.jsonSchema = {
type: 'object',
properties: {
prop1: { type: 'number' },
prop2: { type: 'string' },
},
};
let model = Model.fromJson({
prop1: 10,
prop2: '10',
prop3: 'should not be removed',
prop4: { also: 'this' },
});
let json = model.$toDatabaseJson();
expect(json.prop1).to.equal(10);
expect(json.prop2).to.equal('10');
expect(json.prop3).to.equal('should not be removed');
expect(json.prop4).to.eql({ also: 'this' });
expect(model.prop1).to.equal(10);
expect(model.prop2).to.equal('10');
expect(model.prop3).to.equal('should not be removed');
expect(model.prop4).to.eql({ also: 'this' });
json = model.$toJson();
expect(json.prop1).to.equal(10);
expect(json.prop2).to.equal('10');
expect(json.prop3).to.equal('should not be removed');
expect(json.prop4).to.eql({ also: 'this' });
});
it('should convert objects to json based on jsonSchema type', () => {
let Model = modelClass('Model');
Model.jsonSchema = {
type: 'object',
properties: {
prop1: { type: 'string' },
prop2: {
type: 'object',
properties: {
subProp1: { type: 'number' },
},
},
prop3: {
type: 'array',
items: {
type: 'object',
properties: {
subProp2: { type: 'boolean' },
},
},
},
prop4: {
anyOf: [
{
type: 'array',
},
{
type: 'string',
},
],
},
prop5: {
oneOf: [
{
type: 'object',
},
{
type: 'string',
},
],
},
},
};
let inputJson = {
prop1: 'text',
prop2: {
subProp1: 1000,
},
prop3: [{ subProp2: true }, { subProp2: false }],
prop4: [1, 2, 3],
prop5: {
subProp3: 'str',
},
};
let model = Model.fromJson(inputJson);
expect(model).to.eql(inputJson);
let dbJson = model.$toDatabaseJson();
expect(dbJson.prop1).to.equal('text');
expect(dbJson.prop2).to.equal('{"subProp1":1000}');
expect(dbJson.prop3).to.equal('[{"subProp2":true},{"subProp2":false}]');
expect(dbJson.prop4).to.equal('[1,2,3]');
expect(dbJson.prop5).to.equal('{"subProp3":"str"}');
let model2 = Model.fromDatabaseJson(dbJson);
expect(model2).to.eql(inputJson);
});
it('should convert objects to json based on jsonAttributes array', () => {
class TestModel extends Model {
static get tableName() {
return 'TestModel';
}
static get jsonSchema() {
return {
type: 'object',
properties: {
prop1: { type: 'string' },
prop2: {
type: 'object',
properties: {
subProp1: { type: 'number' },
},
},
// This will not be converted because it is not listed in `jsonAttributes`.
prop3: {
type: 'array',
items: {
type: 'object',
properties: {
subProp2: { type: 'boolean' },
},
},
},
},
};
}
static get jsonAttributes() {
return ['prop2'];
}
}
let inputJson = {
prop1: 'text',
prop2: {
subProp1: 1000,
},
prop3: [{ subProp2: true }, { subProp2: false }],
};
let model = TestModel.fromJson(inputJson);
expect(model).to.eql(inputJson);
let dbJson = model.$toDatabaseJson();
expect(dbJson.prop1).to.equal('text');
expect(dbJson.prop2).to.equal('{"subProp1":1000}');
expect(dbJson.prop3).to.eql(inputJson.prop3);
let model2 = TestModel.fromDatabaseJson(dbJson);
expect(model2).to.eql(inputJson);
});
it('$setJson should do nothing if null is given', () => {
let Model = modelClass('Model');
let model = Model.fromJson({ a: 1, b: 2 });
model.$setJson(null);
expect(model).to.eql({ a: 1, b: 2 });
});
it('$setRelated should set related model instances', () => {
let Model1 = modelClass('Model1');
let Model2 = modelClass('Model2');
Model1.relationMappings = {
hasMany: {
relation: Model.HasManyRelation,
modelClass: Model2,
join: {
from: 'Model1.id',
to: 'Model2.model1Id',
},
},
belongsToOne: {
relation: Model.BelongsToOneRelation,
modelClass: Model1,
join: {
from: 'Model1.id',
to: 'Model1.model1Id',
},
},
manyToMany: {
relation: Model.ManyToManyRelation,
modelClass: Model1,
join: {
from: 'Model1.id',
through: {
from: 'Model1_Model1.id1',
to: 'Model1_Model1.id2',
},
to: 'Model1.id',
},
},
};
const model1 = Model1.fromJson({});
const setResult = model1.$setRelated('hasMany', Model2.fromJson({ id: 1 }));
expect(model1.hasMany).to.eql([{ id: 1 }]);
expect(setResult === model1).to.equal(true);
model1.$setRelated('hasMany', [Model2.fromJson({ id: 2 })]);
expect(model1.hasMany).to.eql([{ id: 2 }]);
model1.$setRelated('belongsToOne', Model1.fromJson({ id: 1 }));
expect(model1.belongsToOne).to.eql({ id: 1 });
model1.$setRelated('belongsToOne', [Model1.fromJson({ id: 2 })]);
expect(model1.belongsToOne).to.eql({ id: 2 });
model1.$setRelated('manyToMany', Model1.fromJson({ id: 1 }));
expect(model1.manyToMany).to.eql([{ id: 1 }]);
model1.$setRelated('manyToMany', [Model1.fromJson({ id: 2 })]);
expect(model1.manyToMany).to.eql([{ id: 2 }]);
});
it('appendRelated should append related model instances', () => {
let Model1 = modelClass('Model1');
let Model2 = modelClass('Model2');
Model1.relationMappings = {
hasMany: {
relation: Model.HasManyRelation,
modelClass: Model2,
join: {
from: 'Model1.id',
to: 'Model2.model1Id',
},
},
belongsToOne: {
relation: Model.BelongsToOneRelation,
modelClass: Model1,
join: {
from: 'Model1.id',
to: 'Model1.model1Id',
},
},
manyToMany: {
relation: Model.ManyToManyRelation,
modelClass: Model1,
join: {
from: 'Model1.id',
through: {
from: 'Model1_Model1.id1',
to: 'Model1_Model1.id2',
},
to: 'Model1.id',
},
},
};
const model1 = Model1.fromJson({});
const appendResult = model1.$appendRelated('hasMany', Model2.fromJson({ id: 1 }));
expect(model1.hasMany).to.eql([{ id: 1 }]);
expect(appendResult === model1).to.equal(true);
model1.$appendRelated('hasMany', [Model2.fromJson({ id: 2 })]);
expect(model1.hasMany).to.eql([{ id: 1 }, { id: 2 }]);
model1.$appendRelated('belongsToOne', Model1.fromJson({ id: 1 }));
expect(model1.belongsToOne).to.eql({ id: 1 });
model1.$appendRelated('belongsToOne', [Model1.fromJson({ id: 2 })]);
expect(model1.belongsToOne).to.eql({ id: 2 });
model1.$appendRelated('manyToMany', Model1.fromJson({ id: 1 }));
expect(model1.manyToMany).to.eql([{ id: 1 }]);
model1.$appendRelated('manyToMany', [Model1.fromJson({ id: 2 })]);
expect(model1.manyToMany).to.eql([{ id: 1 }, { id: 2 }]);
});
it('$toJson should return result without relations if {shallow: true} is given as argument', () => {
let Model = modelClass('Model');
Model.relationMappings = {
someRelation: {
relation: Model.BelongsToOneRelation,
modelClass: Model,
join: {
from: 'Model.id',
to: 'Model.model1Id',
},
},
};
let model = Model.fromJson({ a: 1, b: 2, someRelation: { a: 3, b: 4 } });
expect(model.$toJson()).to.eql({ a: 1, b: 2, someRelation: { a: 3, b: 4 } });
expect(model.$toJson({ shallow: true })).to.eql({ a: 1, b: 2 });
});
it('toJSON should return result without relations if {shallow: true} is given as argument', () => {
let Model = modelClass('Model');
Model.relationMappings = {
someRelation: {
relation: Model.BelongsToOneRelation,
modelClass: Model,
join: {
from: 'Model.id',
to: 'Model.model1Id',
},
},
};
let model = Model.fromJson({ a: 1, b: 2, someRelation: { a: 3, b: 4 } });
expect(model.toJSON()).to.eql({ a: 1, b: 2, someRelation: { a: 3, b: 4 } });
expect(model.toJSON({ shallow: true })).to.eql({ a: 1, b: 2 });
});
it('Model.raw should return objection.raw', () => {
expect(modelClass('Model').raw).to.equal(raw);
});
it('ensureModel should return null for null input', () => {
let Model = modelClass('Model');
expect(Model.ensureModel(null)).to.equal(null);
});
it('ensureModelArray should return [] for null input', () => {
let Model = modelClass('Model');
expect(Model.ensureModelArray(null)).to.eql([]);
});
it('fetchGraph should return a QueryBuilder', () => {
let Model = modelClass('Model1');
expect(Model.fetchGraph([], '[]')).to.be.a(QueryBuilder);
});
it('$fetchGraph should return a QueryBuilder', () => {
let Model = modelClass('Model1');
expect(Model.fromJson({}).$fetchGraph('[]')).to.be.a(QueryBuilder);
});
it('loadRelated should throw if an invalid expression is given', () => {
let Model = modelClass('Model1');
expect(() => {
Model.loadRelated([], 'notAValidExpression.');
}).to.throwException();
});
it('loadRelated should throw if an invalid expression is given', () => {
let Model = modelClass('Model1');
expect(() => {
Model.loadRelated([], 'notAValidExpression.');
}).to.throwException();
});
it('should use Model.QueryBuilder to create `query()` and `$query()`', () => {
class MyQueryBuilder1 extends QueryBuilder {}
class MyQueryBuilder2 extends QueryBuilder {}
const Model1 = modelClass('Model1');
const Model2 = modelClass('Model2');
Model1.relationMappings = {
someRelation: {
relation: Model.HasManyRelation,
modelClass: Model2,
join: {
from: 'Model1.id',
to: 'Model2.someId',
},
},
};
Model1.QueryBuilder = MyQueryBuilder1;
Model2.QueryBuilder = MyQueryBuilder2;
expect(Model1.query()).to.be.a(MyQueryBuilder1);
expect(Model1.fromJson({}).$query()).to.be.a(MyQueryBuilder1);
expect(Model1.fromJson({}).$relatedQuery('someRelation')).to.be.a(MyQueryBuilder2);
});
it('$modelClass should return this.constructor', () => {
let Model1 = modelClass('Model1');
let model = Model1.fromJson({ id: 1 });
expect(model.$modelClass === model.constructor).to.equal(true);
});
describe('traverse() and $traverse()', () => {
let Model1;
let Model2;
let model;
beforeEach(() => {
Model1 = modelClass('Model1');
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',
},
},
};
});
beforeEach(() => {
model = Model1.fromJson({
id: 1,
model1Id: 2,
relation1: [
{ id: 4, model1Id: 1 },
{ id: 5, model1Id: 1 },
],
relation2: {
id: 2,
model1Id: 3,
relation1: [
{ id: 6, model1Id: 2 },
{ id: 7, model1Id: 2 },
],
relation2: {
id: 3,
model1Id: null,
relation1: [
{ id: 8, model1Id: 3 },
{ id: 9, model1Id: 3 },
{ id: 10, model1Id: 3 },
{ id: 11, model1Id: 3 },
{ id: 12, model1Id: 3 },
{ id: 13, model1Id: 3 },
{ id: 14, model1Id: 3 },
{ id: 15, model1Id: 3 },
{ id: 16, model1Id: 3 },
{ id: 17, model1Id: 3 },
{ id: 18, model1Id: 3 },
{ id: 19, model1Id: 3 },
{ id: 20, model1Id: 3 },
{ id: 21, model1Id: 3 },
{ id: 22, model1Id: 3 },
{ id: 23, model1Id: 3 },
{ id: 24, model1Id: 3 },
{ id: 25, model1Id: 3 },
],
},
},
});
});
it('traverse(modelArray, traverser) should traverse through the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
Model1.traverse([model], (model) => {
if (model instanceof Model1) {
model1Ids.push(model.id);
} else if (model instanceof Model2) {
model2Ids.push(model.id);
}
});
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
it('traverse([], traverser) should not throw', () => {
expect(() => {
Model1.traverse([], function () {});
}).to.not.throwException();
});
it('traverse(undefined, traverser) should not throw', () => {
expect(() => {
Model1.traverse(undefined, function () {});
}).to.not.throwException();
});
it('traverse callback should be passed the model, its parent (if any) and the relation it is in (if any)', () => {
Model1.traverse([model], (model, parent, relationName) => {
if (model instanceof Model1) {
if (model.id === 1) {
expect(parent).to.equal(null);
expect(relationName).to.equal(null);
} else if (model.id === 2) {
expect(parent.id).to.equal(1);
expect(relationName).to.equal('relation2');
} else if (model.id === 3) {
expect(parent.id).to.equal(2);
expect(relationName).to.equal('relation2');
} else {
throw new Error('should never get here');
}
} else if (model instanceof Model2) {
if (model.id >= 4 && model.id <= 5) {
expect(parent).to.be.a(Model1);
expect(parent.id).to.equal(1);
expect(relationName).to.equal('relation1');
} else if (model.id >= 6 && model.id <= 7) {
expect(parent).to.be.a(Model1);
expect(parent.id).to.equal(2);
expect(relationName).to.equal('relation1');
} else if (model.id >= 8 && model.id <= 25) {
expect(parent).to.be.a(Model1);
expect(parent.id).to.equal(3);
expect(relationName).to.equal('relation1');
} else {
throw new Error('should never get here');
}
}
});
});
it('traverse(singleModel, traverser) should traverse through the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
Model1.traverse(model, (model) => {
if (model instanceof Model1) {
model1Ids.push(model.id);
} else if (model instanceof Model2) {
model2Ids.push(model.id);
}
});
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
it('traverse(null, singleModel, traverser) should traverse through the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
Model1.traverse(null, model, (model) => {
if (model instanceof Model1) {
model1Ids.push(model.id);
} else if (model instanceof Model2) {
model2Ids.push(model.id);
}
});
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
it('traverse(ModelClass, model, traverser) should traverse through all ModelClass instances in the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
Model1.traverse(Model2, model, (model) => {
model2Ids.push(model.id);
}).traverse(Model1, model, (model) => {
model1Ids.push(model.id);
});
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
it('$traverse(traverser) should traverse through the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
model.$traverse((model) => {
if (model instanceof Model1) {
model1Ids.push(model.id);
} else if (model instanceof Model2) {
model2Ids.push(model.id);
}
});
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
it('$traverse(ModelClass, traverser) should traverse through the ModelClass instances in the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
model
.$traverse(Model1, (model) => {
model1Ids.push(model.id);
})
.$traverse(Model2, (model) => {
model2Ids.push(model.id);
});
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
});
describe('traverseAsync() and $traverseAsync()', () => {
let Model1;
let Model2;
let model;
beforeEach(() => {
Model1 = modelClass('Model1');
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',
},
},
};
});
beforeEach(() => {
model = Model1.fromJson({
id: 1,
model1Id: 2,
relation1: [
{ id: 4, model1Id: 1 },
{ id: 5, model1Id: 1 },
],
relation2: {
id: 2,
model1Id: 3,
relation1: [
{ id: 6, model1Id: 2 },
{ id: 7, model1Id: 2 },
],
relation2: {
id: 3,
model1Id: null,
relation1: [
{ id: 8, model1Id: 3 },
{ id: 9, model1Id: 3 },
{ id: 10, model1Id: 3 },
{ id: 11, model1Id: 3 },
{ id: 12, model1Id: 3 },
{ id: 13, model1Id: 3 },
{ id: 14, model1Id: 3 },
{ id: 15, model1Id: 3 },
{ id: 16, model1Id: 3 },
{ id: 17, model1Id: 3 },
{ id: 18, model1Id: 3 },
{ id: 19, model1Id: 3 },
{ id: 20, model1Id: 3 },
{ id: 21, model1Id: 3 },
{ id: 22, model1Id: 3 },
{ id: 23, model1Id: 3 },
{ id: 24, model1Id: 3 },
{ id: 25, model1Id: 3 },
],
},
},
});
});
it('traverseAsync(modelArray, traverser) should traverse through the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
return Model1.traverseAsync([model], (model) => {
return new Promise((resolve) => {
setTimeout(() => {
if (model instanceof Model1) {
model1Ids.push(model.id);
} else if (model instanceof Model2) {
model2Ids.push(model.id);
}
resolve();
}, 5);
});
}).then(() => {
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
});
it('traverseAsync(singleModel, traverser) should traverse through the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
return Model1.traverseAsync(model, (model) => {
return new Promise((resolve) => {
setTimeout(() => {
if (model instanceof Model1) {
model1Ids.push(model.id);
} else if (model instanceof Model2) {
model2Ids.push(model.id);
}
resolve();
}, 5);
});
}).then(() => {
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
});
it('traverseAsync callback should be passed the model, its parent (if any) and the relation it is in (if any)', () => {
return Model1.traverseAsync([model], (model, parent, relationName) => {
if (model instanceof Model1) {
if (model.id === 1) {
expect(parent).to.equal(null);
expect(relationName).to.equal(null);
} else if (model.id === 2) {
expect(parent.id).to.equal(1);
expect(relationName).to.equal('relation2');
} else if (model.id === 3) {
expect(parent.id).to.equal(2);
expect(relationName).to.equal('relation2');
} else {
throw new Error('should never get here');
}
} else if (model instanceof Model2) {
if (model.id >= 4 && model.id <= 5) {
expect(parent).to.be.a(Model1);
expect(parent.id).to.equal(1);
expect(relationName).to.equal('relation1');
} else if (model.id >= 6 && model.id <= 7) {
expect(parent).to.be.a(Model1);
expect(parent.id).to.equal(2);
expect(relationName).to.equal('relation1');
} else if (model.id >= 8 && model.id <= 25) {
expect(parent).to.be.a(Model1);
expect(parent.id).to.equal(3);
expect(relationName).to.equal('relation1');
} else {
throw new Error('should never get here');
}
}
});
});
it('traverseAsync(ModelClass, model, traverser) should traverse through all ModelClass instances in the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
return Model1.traverseAsync(Model2, model, (model) => {
model2Ids.push(model.id);
})
.then(() => {
return Model1.traverseAsync(Model1, model, (model) => {
model1Ids.push(model.id);
});
})
.then(() => {
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
});
it('$traverseAsync(traverser) should traverse through the relation tree', () => {
let model1Ids = [];
let model2Ids = [];
return model
.$traverseAsync((model) => {
return new Promise((resolve) => {
setTimeout(() => {
if (model instanceof Model1) {
model1Ids.push(model.id);
} else if (model instanceof Model2) {
model2Ids.push(model.id);
}
resolve();
}, 5);
});
})
.then(() => {
expect(_.sortBy(model1Ids)).to.eql([1, 2, 3]);
expect(_.sortBy(model2Ids)).to.eql(_.range(4, 26));
});
});
});
it('$validate should run hooks and strip relations', () => {
let Model1 = modelClass('Model1');
Model1.prototype.$parseJson = function (json, opt) {
json = Model.prototype.$parseJson.apply(this, arguments);
json.foo = parseInt(json.foo);
return json;
};
Model1.prototype.$formatJson = function (json, opt) {
json = Model.prototype.$formatJson.apply(this, arguments);
json.foo = json.foo.toString();
return json;
};
Model1.jsonSchema = {
type: 'object',
properties: {
foo: { type: 'integer' },
},
};
Model1.relationMappings = {
someRelation: {
relation: Model.BelongsToOneRelation,
modelClass: Model1,
join: {
from: 'Model1.id',
to: 'Model1.someId',
},
},
};
let model = Model1.fromJson({ foo: '10' });
model.someRelation = Model1.fromJson({ foo: '20' });
expect(model.foo).to.equal(10);
model.$validate();
expect(model.foo).to.equal(10);
expect(model.$toJson().foo).to.equal('10');
});
it('Model.fn should return objection.fn', () => {
expect(modelClass('Model1').fn).to.equal(fn);
});
it('make sure JSON.stringify works with toJSON (#869)', () => {
class Person extends Model {
static get idColumn() {
return 'key';
}
}
const p1 = Person.fromJson({ key: 1 });
const p2 = Person.fromJson({ key: 2 });
JSON.stringify([p1, p2]);
});
function modelClass(tableName) {
return class TestModel extends Model {
static get tableName() {
return tableName;
}
};
}
function createModelClass(proto, staticStuff) {
class Model1 extends Model {}
_.merge(Model1.prototype, proto);
_.merge(Model1, staticStuff);
return Model1;
}
});
================================================
FILE: tests/unit/queryBuilder/JoinBuilder.js
================================================
const expect = require('expect.js');
const objection = require('../../../');
const Model = objection.Model;
const { JoinBuilder } = require('../../../lib/queryBuilder/JoinBuilder');
const JoinClause = require('knex/lib/query/joinclause');
describe('JoinBuilder', () => {
it('should have knex.JoinClause methods', () => {
class TestModel extends Model {
static get tableName() {
return 'Model';
}
}
let ignore = [];
let knexJoinClause = new JoinClause();
let builder = JoinBuilder.forClass(TestModel);
for (let name in knexJoinClause) {
let func = knexJoinClause[name];
console.log('checking', name, typeof func);
if (typeof func === 'function' && ignore.indexOf(name) === -1) {
if (typeof builder[name] !== 'function') {
expect().to.fail("knex method '" + name + "' is missing from JoinBuilder");
}
}
}
});
});
================================================
FILE: tests/unit/queryBuilder/QueryBuilder.js
================================================
const _ = require('lodash'),
Knex = require('knex'),
expect = require('expect.js'),
chai = require('chai'),
Bluebird = require('bluebird'),
objection = require('../../../'),
knexUtils = require('../../../lib/utils/knexUtils'),
knexMocker = require('../../../testUtils/mockKnex'),
ref = objection.ref,
Model = objection.Model,
QueryBuilder = objection.QueryBuilder,
QueryBuilderBase = objection.QueryBuilderBase;
describe('QueryBuilder', () => {
let mockKnexQueryResults = [];
let mockKnexQueryResultIndex = 0;
let executedQueries = [];
let mockKnex = null;
let TestModel = null;
before(() => {
let knex = Knex({ client: 'pg' });
mockKnex = knexMocker(knex, function (mock, oldImpl, args) {
executedQueries.push(this.toString());
let result = mockKnexQueryResults[mockKnexQueryResultIndex++] || [];
let promise = Promise.resolve(result);
return promise.then.apply(promise, args);
});
});
beforeEach(() => {
mockKnexQueryResults = [];
mockKnexQueryResultIndex = 0;
executedQueries = [];
TestModel = class TestModel extends Model {
static get tableName() {
return 'Model';
}
};
TestModel.knex(mockKnex);
});
it("should throw if model doesn't have a `tableName`", (done) => {
class TestModel extends Model {
// no tableName
}
TestModel.query(mockKnex)
.then(() => done(new Error('should not get here')))
.catch((err) => {
expect(err.message).to.equal('Model TestModel must have a static property tableName');
done();
})
.catch(done);
});
it('should have knex methods', () => {
let ignore = [
'setMaxListeners',
'getMaxListeners',
'emit',
'addListener',
'on',
'prependListener',
'once',
'prependOnceListener',
'removeListener',
'removeAllListeners',
'listeners',
'listenerCount',
'eventNames',
'rawListeners',
'pluck', // not supported anymore in objection v3+
'queryBuilder', // this method is added to the knex mock, but should not available on objection's QueryBuilder
'raw', // this method is added to the knex mock, but should not available on objection's QueryBuilder
];
let builder = QueryBuilder.forClass(TestModel);
for (let name in mockKnex) {
let func = mockKnex[name];
if (typeof func === 'function' && name.charAt(0) !== '_' && ignore.indexOf(name) === -1) {
if (typeof builder[name] !== 'function') {
expect().to.fail("knex method '" + name + "' is missing from QueryBuilder");
}
}
}
});
it('modelClass() should return the model class', () => {
expect(QueryBuilder.forClass(TestModel).modelClass() === TestModel).to.equal(true);
});
it('modify() should execute the given function and pass the builder to it', () => {
let builder = QueryBuilder.forClass(TestModel);
let called = false;
builder.modify(function (b) {
called = true;
expect(b === builder).to.equal(true);
expect(this === builder).to.equal(true);
});
expect(called).to.equal(true);
});
it('should be able to pass arguments to modify', () => {
let builder = QueryBuilder.forClass(TestModel);
let called1 = false;
let called2 = false;
// Should accept a single function.
builder.modify(
(query, arg1, arg2) => {
called1 = true;
expect(query === builder).to.equal(true);
expect(arg1).to.equal('foo');
expect(arg2).to.equal(1);
},
'foo',
1,
);
expect(called1).to.equal(true);
called1 = false;
called2 = false;
// Should accept an array of functions.
builder.modify(
[
(query, arg1, arg2) => {
called1 = true;
expect(query === builder).to.equal(true);
expect(arg1).to.equal('foo');
expect(arg2).to.equal(1);
},
(query, arg1, arg2) => {
called2 = true;
expect(query === builder).to.equal(true);
expect(arg1).to.equal('foo');
expect(arg2).to.equal(1);
},
],
'foo',
1,
);
expect(called1).to.equal(true);
expect(called2).to.equal(true);
});
it('should be able to pass arguments to modify when using named modifiers', () => {
let builder = QueryBuilder.forClass(TestModel);
let called1 = false;
let called2 = false;
TestModel.modifiers = {
modifier1: (query, arg1, arg2) => {
called1 = true;
expect(query === builder).to.equal(true);
expect(arg1).to.equal('foo');
expect(arg2).to.equal(1);
},
modifier2: (query, arg1, arg2) => {
called2 = true;
expect(query === builder).to.equal(true);
expect(arg1).to.equal('foo');
expect(arg2).to.equal(1);
},
};
// Should accept a single modifier.
builder.modify('modifier1', 'foo', 1);
expect(called1).to.equal(true);
called1 = false;
called2 = false;
// Should accept an array of modifiers.
builder.modify(['modifier1', 'modifier2'], 'foo', 1);
expect(called1).to.equal(true);
expect(called2).to.equal(true);
});
it('should throw if an unknown modifier is specified', () => {
const builder = QueryBuilder.forClass(TestModel);
TestModel.modifiers = {};
expect(() => {
builder.modify('unknown');
}).to.throwException((err) => {
expect(err.message).to.equal(
'Unable to determine modify function from provided value: "unknown".',
);
});
});
it('modify() should do nothing when receiving `undefined`', () => {
let builder = QueryBuilder.forClass(TestModel);
let res;
expect(() => {
res = builder.modify(undefined);
}).to.not.throwException();
expect(res === builder).to.equal(true);
});
it('modify accept a list of strings and call the corresponding modifiers', () => {
const builder = QueryBuilder.forClass(TestModel);
let aCalled = false;
let bCalled = false;
TestModel.modifiers = {
a(qb) {
aCalled = qb === builder;
},
b(qb) {
bCalled = qb === builder;
},
c: 'a',
d: ['c', 'b'],
};
aCalled = false;
bCalled = false;
builder.modify('a');
expect(aCalled).to.equal(true);
expect(bCalled).to.equal(false);
aCalled = false;
bCalled = false;
builder.modify('b');
expect(aCalled).to.equal(false);
expect(bCalled).to.equal(true);
aCalled = false;
bCalled = false;
builder.modify(['a', 'b']);
expect(aCalled).to.equal(true);
expect(bCalled).to.equal(true);
aCalled = false;
bCalled = false;
builder.modify([['a', [[['b']]]]]);
expect(aCalled).to.equal(true);
expect(bCalled).to.equal(true);
aCalled = false;
bCalled = false;
builder.modify('d');
expect(aCalled).to.equal(true);
expect(bCalled).to.equal(true);
});
it('modify calls the modifierNotFound() hook for unknown modifiers', () => {
const builder = QueryBuilder.forClass(TestModel);
let caughtModifiers = [];
TestModel.modifierNotFound = (qb, modifier) => {
if (qb === builder) {
caughtModifiers.push(modifier);
}
};
TestModel.modifiers = {
c: 'a',
d: ['c', 'b'],
};
caughtModifiers = [];
builder.modify('a');
expect(caughtModifiers).to.eql(['a']);
caughtModifiers = [];
builder.modify('b');
expect(caughtModifiers).to.eql(['b']);
caughtModifiers = [];
builder.modify('c');
expect(caughtModifiers).to.eql(['a']);
caughtModifiers = [];
builder.modify('d');
expect(caughtModifiers).to.eql(['a', 'b']);
});
it('should still throw if modifierNotFound() delegate to the definition in the super class', () => {
const builder = QueryBuilder.forClass(TestModel);
TestModel.modifierNotFound = function (builder, modifier) {
Model.modifierNotFound(builder, modifier);
};
expect(() => {
builder.modify('unknown');
}).to.throwException((err) => {
expect(err.message).to.equal(
'Unable to determine modify function from provided value: "unknown".',
);
});
});
it('should not throw if modifierNotFound() handles an unknown modifier', () => {
const builder = QueryBuilder.forClass(TestModel);
let caughtModifier = null;
TestModel.modifierNotFound = (builder, modifier) => {
caughtModifier = modifier;
};
expect(() => {
builder.modify('unknown');
}).to.not.throwException();
expect(caughtModifier).to.equal('unknown');
});
it('should call the callback passed to .then after execution', (done) => {
mockKnexQueryResults = [[{ a: 1 }, { a: 2 }]];
// Make sure the callback is called by not returning a promise from the test.
// Instead call the `done` function so that the test times out if the callback
// is not called.
QueryBuilder.forClass(TestModel)
.then((result) => {
expect(result).to.eql(mockKnexQueryResults[0]);
done();
})
.catch(done);
});
it('should return a promise from .then method', () => {
let promise = QueryBuilder.forClass(TestModel).then(_.identity);
expect(promise).to.be.a(Promise);
return promise;
});
it('should return a promise from .execute method', () => {
let promise = QueryBuilder.forClass(TestModel).execute();
expect(promise).to.be.a(Promise);
return promise;
});
it('should return a promise from .catch method', () => {
let promise = QueryBuilder.forClass(TestModel).catch(_.noop);
expect(promise).to.be.a(Promise);
return promise;
});
it('should select all from the model table if no query methods are called', () => {
let queryBuilder = QueryBuilder.forClass(TestModel);
return queryBuilder.then(() => {
expect(executedQueries).to.eql(['select "Model".* from "Model"']);
});
});
it('should have knex query builder methods', () => {
// Doesn't test all the methods. Just enough to make sure the method calls are correctly
// passed to the knex query builder.
return QueryBuilder.forClass(TestModel)
.select('name', 'id', 'age')
.join('AnotherTable', 'AnotherTable.modelId', 'Model.id')
.where('id', 10)
.where('height', '>', 180)
.where({ name: 'test' })
.orWhere(function (builder) {
// The builder passed to these functions should be a QueryBuilderBase instead of
// knex query builder.
expect(this).to.equal(builder);
expect(this).to.be.a(QueryBuilderBase);
this.where('age', '<', 10).andWhere('eyeColor', 'blue');
})
.then(() => {
expect(executedQueries).to.eql([
[
'select "name", "id", "age" from "Model"',
'inner join "AnotherTable" on "AnotherTable"."modelId" = "Model"."id"',
'where "id" = 10',
'and "height" > 180',
'and "name" = \'test\'',
'or ("age" < 10 and "eyeColor" = \'blue\')',
].join(' '),
]);
});
});
it('should return a QueryBuilder from .timeout method', () => {
const builder = QueryBuilder.forClass(TestModel).timeout(3000);
expect(builder).to.be.a(QueryBuilder);
return builder;
});
describe('where(..., ref(...))', () => {
it('should create a where clause using column references instead of values (1)', () => {
return QueryBuilder.forClass(TestModel)
.where('SomeTable.someColumn', ref('SomeOtherTable.someOtherColumn'))
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where "SomeTable"."someColumn" = "SomeOtherTable"."someOtherColumn"',
]);
});
});
it('should create a where clause using column references instead of values (2)', () => {
return QueryBuilder.forClass(TestModel)
.where('SomeTable.someColumn', '>', ref('SomeOtherTable.someOtherColumn'))
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where "SomeTable"."someColumn" > "SomeOtherTable"."someOtherColumn"',
]);
});
});
it('should fail with invalid operator', () => {
expect(() => {
QueryBuilder.forClass(TestModel)
.where('SomeTable.someColumn', 'lol', ref('SomeOtherTable.someOtherColumn'))
.toKnexQuery()
.toString();
}).to.throwException((err) => {
expect(err.message).to.equal('The operator "lol" is not permitted');
});
});
it('orWhere(..., ref(...)) should create a where clause using column references instead of values', () => {
return QueryBuilder.forClass(TestModel)
.where('id', 10)
.orWhere('SomeTable.someColumn', ref('SomeOtherTable.someOtherColumn'))
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where "id" = 10 or "SomeTable"."someColumn" = "SomeOtherTable"."someOtherColumn"',
]);
});
});
});
describe('whereComposite', () => {
it('should create multiple where queries', () => {
return QueryBuilder.forClass(TestModel)
.whereComposite(['A.a', 'B.b'], '>', [1, 2])
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where ("A"."a" > 1 and "B"."b" > 2)',
]);
});
});
it('should fail with invalid operator', () => {
expect(() => {
QueryBuilder.forClass(TestModel)
.whereComposite('SomeTable.someColumn', 'lol', 'SomeOtherTable.someOtherColumn')
.toKnexQuery()
.toString();
}).to.throwException((err) => {
expect(err.message).to.equal('The operator "lol" is not permitted');
});
});
it('operator should default to `=`', () => {
return QueryBuilder.forClass(TestModel)
.whereComposite(['A.a', 'B.b'], [1, 2])
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where ("A"."a" = 1 and "B"."b" = 2)',
]);
});
});
it('should work like a normal `where` when one column is given (1)', () => {
return QueryBuilder.forClass(TestModel)
.whereComposite(['A.a'], 1)
.then(() => {
expect(executedQueries).to.eql(['select "Model".* from "Model" where "A"."a" = 1']);
});
});
it('should work like a normal `where` when one column is given (2)', () => {
return QueryBuilder.forClass(TestModel)
.whereComposite('A.a', 1)
.then(() => {
expect(executedQueries).to.eql(['select "Model".* from "Model" where "A"."a" = 1']);
});
});
});
describe('whereInComposite', () => {
it('should create a where-in query for composite id and a single choice', () => {
return QueryBuilder.forClass(TestModel)
.whereInComposite(['A.a', 'B.b'], [1, 2])
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where ("A"."a", "B"."b") in ((1, 2))',
]);
});
});
it('should create a where-in query for composite id and array of choices', () => {
return QueryBuilder.forClass(TestModel)
.whereInComposite(
['A.a', 'B.b'],
[
[1, 2],
[3, 4],
],
)
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where ("A"."a", "B"."b") in ((1, 2), (3, 4))',
]);
});
});
it('should work just like a normal where-in query if one column is given (1)', () => {
return QueryBuilder.forClass(TestModel)
.whereInComposite(['A.a'], [[1], [3]])
.then(() => {
expect(executedQueries).to.eql(['select "Model".* from "Model" where "A"."a" in (1, 3)']);
});
});
it('should work just like a normal where-in query if one column is given (2)', () => {
return QueryBuilder.forClass(TestModel)
.whereInComposite('A.a', [[1], [3]])
.then(() => {
expect(executedQueries).to.eql(['select "Model".* from "Model" where "A"."a" in (1, 3)']);
});
});
it('should work just like a normal where-in query if one column is given (3)', () => {
return QueryBuilder.forClass(TestModel)
.whereInComposite('A.a', [1, 3])
.then(() => {
expect(executedQueries).to.eql(['select "Model".* from "Model" where "A"."a" in (1, 3)']);
});
});
it('should work just like a normal where-in query if one column is given (4)', () => {
return QueryBuilder.forClass(TestModel)
.whereInComposite('A.a', TestModel.query().select('a'))
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where "A"."a" in (select "a" from "Model")',
]);
});
});
it('should work just like a normal where-in query if one column is given (5)', () => {
return QueryBuilder.forClass(TestModel)
.whereInComposite('A.a', 1)
.then(() => {
expect(executedQueries).to.eql(['select "Model".* from "Model" where "A"."a" in (1)']);
});
});
it('should create a where-in query for composite id and a subquery', () => {
return QueryBuilder.forClass(TestModel)
.whereInComposite(['A.a', 'B.b'], TestModel.query().select('a', 'b'))
.then(() => {
expect(executedQueries).to.eql([
'select "Model".* from "Model" where ("A"."a","B"."b") in (select "a", "b" from "Model")',
]);
});
});
});
it('should convert array query result into Model instances', () => {
mockKnexQueryResults = [[{ a: 1 }, { a: 2 }]];
return QueryBuilder.forClass(TestModel).then((result) => {
expect(result).to.have.length(2);
expect(result[0]).to.be.a(TestModel);
expect(result[1]).to.be.a(TestModel);
expect(result).to.eql(mockKnexQueryResults[0]);
});
});
it('should convert an object query result into a Model instance', () => {
mockKnexQueryResults = [{ a: 1 }];
return QueryBuilder.forClass(TestModel).then((result) => {
expect(result).to.be.a(TestModel);
expect(result.a).to.equal(1);
});
});
it('should pass the query builder as `this` and parameter for the hooks', (done) => {
let text = '';
QueryBuilder.forClass(TestModel)
.runBefore(function (result, builder) {
expect(builder.constructor.name).to.equal('QueryBuilder');
expect(this).to.equal(builder);
text += 'a';
})
.onBuild(function (builder) {
expect(builder.constructor.name).to.equal('QueryBuilder');
expect(this).to.equal(builder);
text += 'b';
})
.onBuildKnex(function (knexBuilder, builder) {
expect(builder.constructor.name).to.equal('QueryBuilder');
expect(knexUtils.isKnexQueryBuilder(knexBuilder)).to.equal(true);
expect(this).to.equal(knexBuilder);
text += 'c';
})
.runAfter(function (data, builder) {
expect(builder.constructor.name).to.equal('QueryBuilder');
expect(this).to.equal(builder);
text += 'd';
})
.runAfter(function (data, builder) {
expect(builder.constructor.name).to.equal('QueryBuilder');
expect(this).to.equal(builder);
text += 'e';
})
.runAfter(() => {
throw new Error('abort');
})
.onError(function (err, builder) {
expect(builder.constructor.name).to.equal('QueryBuilder');
expect(this).to.equal(builder);
expect(err.message).to.equal('abort');
text += 'f';
})
.then(() => {
expect(text).to.equal('abcdef');
done();
})
.catch((err) => {
done(err);
});
});
it('throwing at any phase should call the onError hook', (done) => {
let called = false;
QueryBuilder.forClass(TestModel)
.runBefore(function (result, builder) {
throw new Error();
})
.onError(function (err, builder) {
called = true;
})
.then(() => {
expect(called).to.equal(true);
done();
})
.catch((err) => {
done(err);
});
});
it('any return value from onError should be the result of the query', (done) => {
QueryBuilder.forClass(TestModel)
.runBefore(function (result, builder) {
throw new Error();
})
.onError(function (err, builder) {
return 'my custom error';
})
.then((result) => {
expect(result).to.equal('my custom error');
done();
})
.catch((err) => {
done(err);
});
});
it('should call run* methods in the correct order', (done) => {
mockKnexQueryResults = [0];
// Again call `done` instead of returning a promise just to make sure the final
// `.then` callback is called. (I'm paranoid).
QueryBuilder.forClass(TestModel)
.runBefore(() => {
expect(mockKnexQueryResults[0]).to.equal(0);
return ++mockKnexQueryResults[0];
})
.runBefore(() => {
expect(mockKnexQueryResults[0]).to.equal(1);
return Bluebird.delay(1).then(() => ++mockKnexQueryResults[0]);
})
.runBefore(() => {
expect(mockKnexQueryResults[0]).to.equal(2);
++mockKnexQueryResults[0];
})
.runAfter((res) => {
expect(res).to.equal(3);
return Bluebird.delay(1).then(() => {
return ++res;
});
})
.runAfter((res) => {
expect(res).to.equal(4);
return ++res;
})
.then((res) => {
expect(res).to.equal(5);
done();
})
.catch(done);
});
it('should not execute query if an error is thrown from runBefore', (done) => {
QueryBuilder.forClass(TestModel)
.runBefore(() => {
throw new Error('some error');
})
.onBuild(() => {
done(new Error('should not get here'));
})
.runAfter(() => {
done(new Error('should not get here'));
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal('some error');
expect(executedQueries).to.have.length(0);
done();
});
});
it('should reject promise if an error is throw from from runAfter', (done) => {
QueryBuilder.forClass(TestModel)
.runAfter(() => {
throw new Error('some error');
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal('some error');
done();
});
});
it('should call custom find implementation defined by findOperationFactory', () => {
return QueryBuilder.forClass(TestModel)
.findOperationFactory(function (builder) {
expect(builder).to.equal(this);
return createFindOperation(builder, { a: 1 });
})
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('select "Model".* from "Model" where "a" = 1');
});
});
it('should not call custom find implementation defined by findOperationFactory if insert is called', () => {
return QueryBuilder.forClass(TestModel)
.findOperationFactory((builder) => {
return createFindOperation(builder, { a: 1 });
})
.insert({ a: 1 })
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('insert into "Model" ("a") values (1) returning "id"');
});
});
it('should not call custom find implementation defined by findOperationFactory if update is called', () => {
return QueryBuilder.forClass(TestModel)
.findOperationFactory((builder) => {
return createFindOperation(builder, { a: 1 });
})
.update({ a: 1 })
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('update "Model" set "a" = 1');
});
});
it('should not call custom find implementation defined by findOperationFactory if delete is called', () => {
return QueryBuilder.forClass(TestModel)
.findOperationFactory((builder) => {
return createFindOperation(builder, { a: 1 });
})
.delete()
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('delete from "Model"');
});
});
it('should call custom insert implementation defined by insertOperationFactory', () => {
return QueryBuilder.forClass(TestModel)
.insertOperationFactory((builder) => {
return createInsertOperation(builder, { b: 2 });
})
.insert({ a: 1 })
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('insert into "Model" ("a", "b") values (1, 2)');
});
});
it('should call custom update implementation defined by updateOperationFactory', () => {
return QueryBuilder.forClass(TestModel)
.updateOperationFactory((builder) => {
return createUpdateOperation(builder, { b: 2 });
})
.update({ a: 1 })
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('update "Model" set "a" = 1, "b" = 2');
});
});
it('should call custom patch implementation defined by patchOperationFactory', () => {
return QueryBuilder.forClass(TestModel)
.patchOperationFactory((builder) => {
return createUpdateOperation(builder, { b: 2 });
})
.patch({ a: 1 })
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('update "Model" set "a" = 1, "b" = 2');
});
});
it('should call custom delete implementation defined by deleteOperationFactory', () => {
return QueryBuilder.forClass(TestModel)
.deleteOperationFactory((builder) => {
return createDeleteOperation(builder, { id: 100 });
})
.delete()
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('delete from "Model" where "id" = 100');
});
});
it('should call custom relate implementation defined by relateOperationFactory', () => {
return QueryBuilder.forClass(TestModel)
.relateOperationFactory((builder) => {
return createInsertOperation(builder, { b: 2 });
})
.relate({ a: 1 })
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('insert into "Model" ("a", "b") values (1, 2)');
});
});
it('should call custom unrelate implementation defined by unrelateOperationFactory', () => {
return QueryBuilder.forClass(TestModel)
.unrelateOperationFactory((builder) => {
return createDeleteOperation(builder, { id: 100 });
})
.unrelate()
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal('delete from "Model" where "id" = 100');
});
});
it('should be able to execute same query multiple times', () => {
let query = QueryBuilder.forClass(TestModel)
.updateOperationFactory((builder) => {
return createUpdateOperation(builder, { b: 2 });
})
.where('test', '<', 100)
.update({ a: 1 });
return query
.then(() => {
expect(executedQueries).to.have.length(1);
expect(query.toKnexQuery().toString()).to.equal(executedQueries[0]);
expect(executedQueries[0]).to.equal(
'update "Model" set "a" = 1, "b" = 2 where "test" < 100',
);
executedQueries = [];
return query;
})
.then(() => {
expect(executedQueries).to.have.length(1);
expect(query.toKnexQuery().toString()).to.equal(executedQueries[0]);
expect(executedQueries[0]).to.equal(
'update "Model" set "a" = 1, "b" = 2 where "test" < 100',
);
executedQueries = [];
return query;
})
.then(() => {
expect(executedQueries).to.have.length(1);
expect(query.toKnexQuery().toString()).to.equal(executedQueries[0]);
expect(executedQueries[0]).to.equal(
'update "Model" set "a" = 1, "b" = 2 where "test" < 100',
);
});
});
it('resultSize should create and execute a query that returns the size of the query', (done) => {
mockKnexQueryResults = [[{ count: '123' }]];
QueryBuilder.forClass(TestModel)
.where('test', 100)
.orderBy('order')
.limit(10)
.offset(100)
.resultSize()
.then((res) => {
expect(executedQueries).to.have.length(1);
expect(res).to.equal(123);
expect(executedQueries[0]).to.equal(
'select count(*) as "count" from (select "Model".* from "Model" where "test" = 100) as "temp"',
);
done();
})
.catch(done);
});
it('should consider withSchema when looking for column info', (done) => {
class TestModelRelated extends Model {
static get tableName() {
return 'Related';
}
}
class TestModel extends Model {
static get tableName() {
return 'Model';
}
static get relationMappings() {
return {
relatedModel: {
relation: Model.BelongsToOneRelation,
modelClass: TestModelRelated,
join: {
from: 'Model.id',
to: 'Related.id',
},
},
};
}
}
TestModel.knex(mockKnex);
TestModelRelated.knex(mockKnex);
mockKnexQueryResults = [[{ count: '123' }]];
QueryBuilder.forClass(TestModel)
.withSchema('someSchema')
.withGraphJoined('relatedModel')
.then(() => {
expect(executedQueries).to.eql([
"select * from information_schema.columns where table_name = 'Model' and table_catalog = current_database() and table_schema = 'someSchema'",
"select * from information_schema.columns where table_name = 'Related' and table_catalog = current_database() and table_schema = 'someSchema'",
'select "Model"."0" as "0" from "someSchema"."Model" left join "someSchema"."Related" as "relatedModel" on "relatedModel"."id" = "Model"."id"',
]);
done();
})
.catch(done);
});
it('range should return a range and the total count', (done) => {
mockKnexQueryResults = [[{ a: '1' }], [{ count: '123' }]];
QueryBuilder.forClass(TestModel)
.where('test', 100)
.orderBy('order')
.range(100, 200)
.then((res) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries).to.eql([
'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 }]);
done();
})
.catch(done);
});
it('page should return a page and the total count', (done) => {
mockKnexQueryResults = [[{ a: '1' }], [{ count: '123' }]];
QueryBuilder.forClass(TestModel)
.where('test', 100)
.orderBy('order')
.page(10, 100)
.then((res) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries).to.eql([
'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 }]);
done();
})
.catch(done);
});
it('isFind, isInsert, isUpdate, isPatch, isDelete, isRelate, isUnrelate should return true only for the right operations', () => {
TestModel.relationMappings = {
someRel: {
relation: Model.HasManyRelation,
modelClass: TestModel,
join: {
from: 'Model.id',
to: 'Model.someRelId',
},
},
};
const queries = {
find: TestModel.query(),
insert: TestModel.query().insert(),
update: TestModel.query().update(),
patch: TestModel.query().patch(),
delete: TestModel.query().delete(),
relate: TestModel.relatedQuery('someRel').relate(1),
unrelate: TestModel.relatedQuery('someRel').unrelate(),
};
// Check all types of operations, call all available checks for reach of them,
// (e.g. isFind(), isUpdate(), etc) and see if they return the expected result.
const getMethodName = (name) => `is${_.capitalize(name === 'patch' ? 'update' : name)}`;
for (const name in queries) {
const query = queries[name];
for (const other in queries) {
const method = getMethodName(other);
chai
.expect(query[method](), `queries.${name}.${method}()`)
.to.equal(method === getMethodName(name));
chai
.expect(query.hasWheres(), `queries.${name}.hasWheres()`)
.to.equal(name.includes('relate'));
chai.expect(query.hasSelects(), `queries.${name}.hasSelects()`).to.equal(false);
}
}
});
it('hasWheres() should return true for all variants of where queries', () => {
TestModel.relationMappings = {
belongsToOneRelation: {
relation: Model.BelongsToOneRelation,
modelClass: TestModel,
join: {
from: 'Model.someId',
to: 'Model.id',
},
},
hasManyRelation: {
relation: Model.HasManyRelation,
modelClass: TestModel,
join: {
from: 'Model.id',
to: 'Model.someId',
},
},
manyToManyRelation: {
relation: Model.ManyToManyRelation,
modelClass: TestModel,
join: {
from: 'Model.id',
through: {
from: 'JoinTable.id1',
to: 'JoinTable.id2',
},
to: 'Model.id',
},
},
};
expect(TestModel.query().hasWheres()).to.equal(false);
expect(TestModel.query().insert({}).hasWheres()).to.equal(false);
expect(TestModel.query().update({}).hasWheres()).to.equal(false);
expect(TestModel.query().patch({}).hasWheres()).to.equal(false);
expect(TestModel.query().delete().hasWheres()).to.equal(false);
const wheres = [
'findOne',
'findById',
'where',
'andWhere',
'orWhere',
'whereNot',
'orWhereNot',
'whereRaw',
'andWhereRaw',
'orWhereRaw',
'whereWrapped',
'whereExists',
'orWhereExists',
'whereNotExists',
'orWhereNotExists',
'whereIn',
'orWhereIn',
'whereNotIn',
'orWhereNotIn',
'whereNull',
'orWhereNull',
'whereNotNull',
'orWhereNotNull',
'whereBetween',
'andWhereBetween',
'whereNotBetween',
'andWhereNotBetween',
'orWhereBetween',
'orWhereNotBetween',
'whereColumn',
'andWhereColumn',
'orWhereColumn',
'whereNotColumn',
'andWhereNotColumn',
'orWhereNotColumn',
];
for (let i = 0; i < wheres.length; i++) {
const name = wheres[i];
const query = TestModel.query()[name](1, '=', 1);
chai.expect(query.hasWheres(), `TestModel.query().${name}().hasWheres()`).to.equal(true);
}
const model = TestModel.fromJson({ id: 1, someId: 1 });
let query = model.$query();
chai.expect(query.hasWheres()).to.equal(true);
query = model.$query().withGraphJoined('manyToManyRelation');
chai.expect(query.hasWheres()).to.equal(true);
query = model.$relatedQuery('belongsToOneRelation');
chai.expect(query.hasWheres()).to.equal(true);
query = model.$relatedQuery('hasManyRelation');
chai.expect(query.hasWheres()).to.equal(true);
query = model.$relatedQuery('manyToManyRelation');
chai.expect(query.hasWheres()).to.equal(true);
});
it('hasSelects() should return true for all variants of select queries', () => {
const selects = [
'select',
'columns',
'column',
'distinct',
'count',
'countDistinct',
'min',
'max',
'sum',
'sumDistinct',
'avg',
'avgDistinct',
];
for (let i = 0; i < selects.length; i++) {
const name = selects[i];
const query = TestModel.query()[name]('arg');
chai
.expect(query.hasSelects(), `TestModel.query().${name}('arg').hasSelects()`)
.to.equal(true);
}
});
it('hasWithGraph() should return true for queries with eager statements', () => {
TestModel.relationMappings = {
someRel: {
relation: Model.HasManyRelation,
modelClass: TestModel,
join: {
from: 'Model.id',
to: 'Model.someRelId',
},
},
};
const query = TestModel.query();
expect(query.hasWithGraph(), false);
query.withGraphFetched('someRel');
expect(query.hasWithGraph(), true);
query.clearWithGraph();
expect(query.hasWithGraph(), false);
});
it('has() should match defined query operations', () => {
// A bunch of random operations to test against.
const operations = [
'range',
'orderBy',
'limit',
'where',
'andWhere',
'whereRaw',
'havingWrapped',
'rightOuterJoin',
'crossJoin',
'offset',
'union',
'count',
'avg',
'with',
];
const test = (query, name, expected) => {
const regexp = new RegExp(`^${name}$`);
chai
.expect(query.has(name), `TestModel.query().${name}('arg').has('${name}')`)
.to.equal(expected);
chai
.expect(query.has(regexp), `TestModel.query().${name}('arg').has(${regexp})`)
.to.equal(expected);
};
operations.forEach((operation) => {
const query = TestModel.query()[operation]('arg');
operations.forEach((testOperation) => {
test(query, testOperation, testOperation === operation);
});
});
});
it('clear() should remove matching query operations', () => {
// A bunch of random operations to test against.
const operations = ['where', 'limit', 'offset', 'count'];
operations.forEach((operation) => {
const query = TestModel.query();
operations.forEach((operation) => query[operation]('arg'));
chai.expect(query.has(operation), `query().has('${operation}')`).to.equal(true);
chai
.expect(
query.clear(operation).has(operation),
`query().clear('${operation}').has('${operation}')`,
)
.to.equal(false);
operations.forEach((testOperation) => {
chai
.expect(query.has(testOperation), `query().has('${testOperation}')`)
.to.equal(testOperation !== operation);
});
});
});
it('update() should call $beforeUpdate on the model', (done) => {
TestModel.prototype.$beforeUpdate = function () {
this.c = 'beforeUpdate';
};
TestModel.prototype.$afterFind = function () {
throw new Error('$afterFind should not be called');
};
let model = TestModel.fromJson({ a: 10, b: 'test' });
QueryBuilder.forClass(TestModel)
.update(model)
.then(() => {
expect(model.c).to.equal('beforeUpdate');
expect(executedQueries[0]).to.equal(
'update "Model" set "a" = 10, "b" = \'test\', "c" = \'beforeUpdate\'',
);
done();
})
.catch(done);
});
it('update() should call $beforeUpdate on the model (async)', (done) => {
TestModel.prototype.$beforeUpdate = function () {
let self = this;
return Bluebird.delay(5).then(() => {
self.c = 'beforeUpdate';
});
};
TestModel.prototype.$afterFind = function () {
throw new Error('$afterFind should not be called');
};
let model = TestModel.fromJson({ a: 10, b: 'test' });
QueryBuilder.forClass(TestModel)
.update(model)
.then(() => {
expect(model.c).to.equal('beforeUpdate');
expect(executedQueries[0]).to.equal(
'update "Model" set "a" = 10, "b" = \'test\', "c" = \'beforeUpdate\'',
);
done();
})
.catch(done);
});
it('patch() should call $beforeUpdate on the model', (done) => {
TestModel.prototype.$beforeUpdate = function () {
this.c = 'beforeUpdate';
};
TestModel.prototype.$afterFind = function () {
throw new Error('$afterFind should not be called');
};
let model = TestModel.fromJson({ a: 10, b: 'test' });
QueryBuilder.forClass(TestModel)
.patch(model)
.then(() => {
expect(model.c).to.equal('beforeUpdate');
expect(executedQueries[0]).to.equal(
'update "Model" set "a" = 10, "b" = \'test\', "c" = \'beforeUpdate\'',
);
done();
})
.catch(done);
});
it('patch() should call $beforeUpdate on the model (async)', (done) => {
TestModel.prototype.$beforeUpdate = function () {
let self = this;
return Bluebird.delay(5).then(() => {
self.c = 'beforeUpdate';
});
};
TestModel.prototype.$afterFind = function () {
throw new Error('$afterFind should not be called');
};
let model = TestModel.fromJson({ a: 10, b: 'test' });
QueryBuilder.forClass(TestModel)
.patch(model)
.then(() => {
expect(model.c).to.equal('beforeUpdate');
expect(executedQueries[0]).to.equal(
'update "Model" set "a" = 10, "b" = \'test\', "c" = \'beforeUpdate\'',
);
done();
})
.catch(done);
});
it('insert() should call $beforeInsert on the model', (done) => {
TestModel.prototype.$beforeInsert = function () {
this.c = 'beforeInsert';
};
TestModel.prototype.$afterFind = function () {
throw new Error('$afterFind should not be called');
};
QueryBuilder.forClass(TestModel)
.insert(TestModel.fromJson({ a: 10, b: 'test' }))
.then((model) => {
expect(model.c).to.equal('beforeInsert');
expect(executedQueries[0]).to.equal(
'insert into "Model" ("a", "b", "c") values (10, \'test\', \'beforeInsert\') returning "id"',
);
done();
})
.catch(done);
});
it('insert() should call $beforeInsert on the model (async)', (done) => {
TestModel.prototype.$beforeInsert = function () {
let self = this;
return Bluebird.delay(5).then(() => {
self.c = 'beforeInsert';
});
};
TestModel.prototype.$afterFind = function () {
throw new Error('$afterFind should not be called');
};
QueryBuilder.forClass(TestModel)
.insert({ a: 10, b: 'test' })
.then((model) => {
expect(model.c).to.equal('beforeInsert');
expect(executedQueries[0]).to.equal(
'insert into "Model" ("a", "b", "c") values (10, \'test\', \'beforeInsert\') returning "id"',
);
done();
})
.catch(done);
});
it('should call $afterFind on the model if no write operation is specified', (done) => {
mockKnexQueryResults = [
[
{
a: 1,
},
{
a: 2,
},
],
];
TestModel.prototype.$afterFind = function (context) {
this.b = this.a * 2 + context.x;
};
QueryBuilder.forClass(TestModel)
.context({ x: 10 })
.then((models) => {
expect(models[0]).to.be.a(TestModel);
expect(models[1]).to.be.a(TestModel);
expect(models).to.eql([
{
a: 1,
b: 12,
},
{
a: 2,
b: 14,
},
]);
done();
})
.catch(done);
});
it('should call $afterFind on the model if no write operation is specified (async)', (done) => {
mockKnexQueryResults = [
[
{
a: 1,
},
{
a: 2,
},
],
];
TestModel.prototype.$afterFind = function (context) {
let self = this;
return Bluebird.delay(10).then(() => {
self.b = self.a * 2 + context.x;
});
};
QueryBuilder.forClass(TestModel)
.context({ x: 10 })
.then((models) => {
expect(models[0]).to.be.a(TestModel);
expect(models[1]).to.be.a(TestModel);
expect(models).to.eql([
{
a: 1,
b: 12,
},
{
a: 2,
b: 14,
},
]);
done();
})
.catch(done);
});
it('should call $afterFind before any `runAfter` hooks', (done) => {
mockKnexQueryResults = [
[
{
a: 1,
},
{
a: 2,
},
],
];
TestModel.prototype.$afterFind = function (context) {
let self = this;
return Bluebird.delay(10).then(() => {
self.b = self.a * 2 + context.x;
});
};
QueryBuilder.forClass(TestModel)
.context({ x: 10 })
.runAfter((result, builder) => {
builder.context().x = 666;
return result;
})
.then((models) => {
expect(models[0]).to.be.a(TestModel);
expect(models[1]).to.be.a(TestModel);
expect(models).to.eql([
{
a: 1,
b: 12,
},
{
a: 2,
b: 14,
},
]);
done();
})
.catch(done);
});
it('should not be able to call setQueryExecutor twice', () => {
expect(() => {
QueryBuilder.forClass(TestModel)
.setQueryExecutor(function () {})
.setQueryExecutor(function () {});
}).to.throwException();
});
it('clearWithGraph() should clear everything related to eager', () => {
let builder = QueryBuilder.forClass(TestModel)
.withGraphFetched('a(f).b', {
f: _.noop,
})
.modifyGraph('a', _.noop);
expect(builder.findOperation('eager')).to.not.equal(null);
builder.clearWithGraph();
expect(builder.findOperation('eager')).to.equal(null);
});
it('clearReject() should clear remove explicit rejection', () => {
let builder = QueryBuilder.forClass(TestModel).reject('error');
expect(builder._explicitRejectValue).to.equal('error');
builder.clearReject();
expect(builder._explicitRejectValue).to.equal(null);
});
it('joinRelated should add join clause to correct place', (done) => {
class M1 extends Model {
static get tableName() {
return 'M1';
}
}
class M2 extends Model {
static get tableName() {
return 'M2';
}
static get relationMappings() {
return {
m1: {
relation: Model.HasManyRelation,
modelClass: M1,
join: {
from: 'M2.id',
to: 'M1.m2Id',
},
},
};
}
}
M1.knex(mockKnex);
M2.knex(mockKnex);
M2.query()
.joinRelated('m1', { alias: 'm' })
.join('M1', 'M1.id', 'M2.m1Id')
.then(() => {
expect(executedQueries[0]).to.equal(
'select "M2".* from "M2" inner join "M1" as "m" on "m"."m2Id" = "M2"."id" inner join "M1" on "M1"."id" = "M2"."m1Id"',
);
done();
})
.catch(done);
});
it('undefined values as query builder method arguments should raise an exception', () => {
expect(() => {
QueryBuilder.forClass(TestModel).where('id', undefined).toKnexQuery();
}).to.throwException((err) => {
expect(err.message).to.equal(
"undefined passed as argument #1 for 'where' operation. Call skipUndefined() method to ignore the undefined values.",
);
});
expect(() => {
QueryBuilder.forClass(TestModel).orWhere('id', '<', undefined).toKnexQuery();
}).to.throwException((err) => {
expect(err.message).to.equal(
"undefined passed as argument #2 for 'orWhere' operation. Call skipUndefined() method to ignore the undefined values.",
);
});
expect(() => {
QueryBuilder.forClass(TestModel).orWhere('id', undefined, 10).toKnexQuery();
}).to.throwException();
expect(() => {
QueryBuilder.forClass(TestModel).delete().whereIn('id', undefined).toKnexQuery();
}).to.throwException();
expect(() => {
QueryBuilder.forClass(TestModel).delete().whereIn('id', [1, undefined, 3]).toKnexQuery();
}).to.throwException((err) => {
expect(err.message).to.equal(
"undefined passed as an item in argument #1 for 'whereIn' operation. Call skipUndefined() method to ignore the undefined values.",
);
});
});
it('undefined values as query builder method arguments should be ignored if `skipUndefined` is called', () => {
expect(() => {
QueryBuilder.forClass(TestModel).skipUndefined().where('id', undefined).toKnexQuery();
}).to.not.throwException();
expect(() => {
QueryBuilder.forClass(TestModel).skipUndefined().orWhere('id', '<', undefined).toKnexQuery();
}).to.not.throwException();
expect(() => {
QueryBuilder.forClass(TestModel).skipUndefined().orWhere('id', undefined, 10).toKnexQuery();
}).to.not.throwException();
expect(() => {
QueryBuilder.forClass(TestModel).skipUndefined().deleteById(undefined).toKnexQuery();
}).to.not.throwException();
expect(() => {
QueryBuilder.forClass(TestModel)
.skipUndefined()
.delete()
.whereIn('id', undefined)
.toKnexQuery();
}).to.not.throwException();
expect(() => {
QueryBuilder.forClass(TestModel)
.skipUndefined()
.delete()
.whereIn('id', [1, undefined, 3])
.toKnexQuery();
}).to.not.throwException();
});
it('all query builder methods should work if model is not bound to a knex, when the query is', () => {
class UnboundModel extends Model {
static get tableName() {
return 'Bar';
}
}
expect(UnboundModel.query(mockKnex).increment('foo', 10).toKnexQuery().toString()).to.equal(
'update "Bar" set "foo" = "foo" + 10',
);
expect(UnboundModel.query(mockKnex).decrement('foo', 5).toKnexQuery().toString()).to.equal(
'update "Bar" set "foo" = "foo" - 5',
);
});
it('first should not add limit(1) by default', () => {
return TestModel.query()
.first()
.then((model) => {
expect(executedQueries[0]).to.equal('select "Model".* from "Model"');
});
});
it('first should add limit(1) if Model.useLimitInFirst = true', () => {
TestModel.useLimitInFirst = true;
return TestModel.query()
.first()
.then((model) => {
expect(executedQueries[0]).to.equal('select "Model".* from "Model" limit 1');
});
});
it('tableNameFor should return the table name', () => {
const query = TestModel.query();
expect(query.tableNameFor(TestModel)).to.equal('Model');
});
it('tableNameFor should return the table name given in from', () => {
const query = TestModel.query().from('Lol');
expect(query.tableNameFor(TestModel)).to.equal('Lol');
});
it('tableRefFor should return the table name by default', () => {
const query = TestModel.query();
expect(query.tableRefFor(TestModel)).to.equal('Model');
});
it('tableRefFor should return the alias', () => {
const query = TestModel.query().alias('Lyl');
expect(query.tableRefFor(TestModel)).to.equal('Lyl');
});
it('should use Model.QueryBuilder in builder methods', () => {
class CustomQueryBuilder extends TestModel.QueryBuilder {}
TestModel.QueryBuilder = CustomQueryBuilder;
const checks = [];
return TestModel.query()
.select('*', (builder) => {
checks.push(builder instanceof CustomQueryBuilder);
})
.where((builder) => {
checks.push(builder instanceof CustomQueryBuilder);
builder.where((builder) => {
checks.push(builder instanceof CustomQueryBuilder);
});
})
.modify((builder) => {
checks.push(builder instanceof CustomQueryBuilder);
})
.then(() => {
expect(checks).to.have.length(4);
expect(checks.every((it) => it)).to.equal(true);
});
});
it('hasSelectionAs', () => {
expect(TestModel.query().hasSelectionAs('foo', 'foo')).to.equal(true);
expect(TestModel.query().hasSelectionAs('foo', 'bar')).to.equal(false);
expect(TestModel.query().select('foo as bar').hasSelectionAs('foo', 'bar')).to.equal(true);
expect(TestModel.query().select('foo').hasSelectionAs('foo', 'bar')).to.equal(false);
expect(TestModel.query().select('*').hasSelectionAs('foo', 'foo')).to.equal(true);
expect(TestModel.query().select('*').hasSelectionAs('foo', 'bar')).to.equal(false);
expect(TestModel.query().select('foo.*').hasSelectionAs('foo.anything', 'anything')).to.equal(
true,
);
expect(
TestModel.query().select('foo.*').hasSelectionAs('foo.anything', 'somethingElse'),
).to.equal(false);
expect(TestModel.query().select('foo.*').hasSelectionAs('bar.anything', 'anything')).to.equal(
false,
);
});
it('hasSelection', () => {
expect(TestModel.query().hasSelection('foo')).to.equal(true);
expect(TestModel.query().hasSelection(ref('foo'))).to.equal(true);
expect(TestModel.query().hasSelection('Model.foo')).to.equal(true);
expect(TestModel.query().hasSelection(ref('Model.foo'))).to.equal(true);
expect(TestModel.query().hasSelection('DifferentTable.foo')).to.equal(false);
expect(TestModel.query().hasSelection(ref('DifferentTable.foo'))).to.equal(false);
expect(TestModel.query().select('*').hasSelection('DifferentTable.anything')).to.equal(true);
expect(TestModel.query().select('foo.*').hasSelection('bar.anything')).to.equal(false);
expect(TestModel.query().select('foo.*').hasSelection('foo.anything')).to.equal(true);
expect(
TestModel.query().select(ref('*')).hasSelection(ref('DifferentTable.anything')),
).to.equal(true);
expect(TestModel.query().select('foo').hasSelection('foo')).to.equal(true);
expect(TestModel.query().select(ref('foo')).hasSelection(ref('foo'))).to.equal(true);
expect(TestModel.query().select('foo').hasSelection('Model.foo')).to.equal(true);
expect(TestModel.query().select(ref('foo')).hasSelection(ref('Model.foo'))).to.equal(true);
expect(TestModel.query().select('foo').hasSelection('DifferentTable.foo')).to.equal(false);
expect(TestModel.query().select(ref('foo')).hasSelection(ref('DifferentTable.foo'))).to.equal(
false,
);
expect(TestModel.query().select('foo').hasSelection('bar')).to.equal(false);
expect(TestModel.query().select(ref('foo')).hasSelection(ref('bar'))).to.equal(false);
expect(TestModel.query().select('Model.foo').hasSelection('foo')).to.equal(true);
expect(TestModel.query().select(ref('Model.foo')).hasSelection(ref('foo'))).to.equal(true);
expect(TestModel.query().select('Model.foo').hasSelection('Model.foo')).to.equal(true);
expect(TestModel.query().select(ref('Model.foo')).hasSelection(ref('Model.foo'))).to.equal(
true,
);
expect(TestModel.query().select('Model.foo').hasSelection('NotTestModel.foo')).to.equal(false);
expect(
TestModel.query().select(ref('Model.foo')).hasSelection(ref('NotTestModel.foo')),
).to.equal(false);
expect(TestModel.query().select('Model.foo').hasSelection('bar')).to.equal(false);
expect(TestModel.query().select(ref('Model.foo')).hasSelection(ref('bar'))).to.equal(false);
expect(TestModel.query().alias('t').select('foo').hasSelection('t.foo')).to.equal(true);
expect(TestModel.query().alias('t').select('t.foo').hasSelection('foo')).to.equal(true);
expect(TestModel.query().alias('t').select('t.foo').hasSelection('t.foo')).to.equal(true);
expect(TestModel.query().alias('t').select('foo').hasSelection('Model.foo')).to.equal(false);
});
it('parseRelationExpression', () => {
expect(QueryBuilder.parseRelationExpression('[foo, bar.baz]')).to.eql({
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: ['foo', 'bar'],
foo: {
$name: 'foo',
$relation: 'foo',
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: [],
},
bar: {
$name: 'bar',
$relation: 'bar',
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: ['baz'],
baz: {
$name: 'baz',
$relation: 'baz',
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: [],
},
},
});
});
describe('eager, allowGraph, and allowGraph', () => {
beforeEach(() => {
const rel = {
relation: TestModel.BelongsToOneRelation,
modelClass: TestModel,
join: {
from: 'Model.foo',
to: 'Model.id',
},
};
TestModel.relationMappings = {
a: rel,
b: rel,
c: rel,
d: rel,
e: rel,
};
});
it("allowGraph('a').withGraphFetched('a(f1)') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('a')
.withGraphFetched('a(f1)', { f1: _.noop })
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch((err) => {
done(new Error('should not get here'));
});
});
it("withGraphFetched('a(f1)').allowGraph('a') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.withGraphFetched('a(f1)', { f1: _.noop })
.allowGraph('a')
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch((err) => {
done(new Error('should not get here'));
});
});
it("allowGraph('[a, b.c.[d, e]]').withGraphFetched('a') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a, b.c.[d, e]]')
.withGraphFetched('a')
.then(() => {
done();
});
});
it("allowGraph('[a, b.c.[d, e]]').withGraphFetched('b.c') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a, b.c.[d, e]]')
.withGraphFetched('b.c')
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch(() => {
done(new Error('should not get here'));
});
});
it("allowGraph('[a, b.c.[d, e]]').withGraphFetched('b.c.e') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a, b.c.[d, e]]')
.withGraphFetched('b.c.e')
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch(() => {
done(new Error('should not get here'));
});
});
it("allowGraph('a').withGraphFetched('a(f1)') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('a')
.withGraphFetched('a(f1)', { f1: _.noop })
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch((err) => {
done(new Error('should not get here'));
});
});
it("allowGraph('[a, b.c.[a, e]]').allowGraph('b.c.[b, d]').withGraphFetched('a') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a, b.c.[a, e]]')
.allowGraph('b.c.[b, d]')
.withGraphFetched('a')
.then(() => {
done();
});
});
it("allowGraph('[a.[a, b], b.c.[a, e]]').allowGraph('[a.[c, d], b.c.[b, d]]').withGraphFetched('a.b') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a.[a, b], b.c.[a, e]]')
.allowGraph('[a.[c, d], b.c.[b, d]]')
.withGraphFetched('a.b')
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch(() => {
done(new Error('should not get here'));
});
});
it("allowGraph('[a.[a, b], b.[a, c]]').allowGraph('[a.[c, d], b.c.[b, d]]').withGraphFetched('a.c') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a.[a, b], b.[a, c]]')
.allowGraph('[a.[c, d], b.c.[b, d]]')
.withGraphFetched('a.c')
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch(() => {
done(new Error('should not get here'));
});
});
it("allowGraph('[a.[a, b], b.[a, c]]').allowGraph('[a.[c, d], b.c.[b, d]]').withGraphFetched('b.a') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a.[a, b], b.[a, c]]')
.allowGraph('[a.[c, d], b.c.[b, d]]')
.withGraphFetched('b.a')
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch(() => {
done(new Error('should not get here'));
});
});
it("allowGraph('[a.[a, b], b.[a, c]]').allowGraph('[a.[c, d], b.c.[b, d]]').withGraphFetched('b.c') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a.[a, b], b.[a, c]]')
.allowGraph('[a.[c, d], b.c.[b, d]]')
.withGraphFetched('b.c')
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch(() => {
done(new Error('should not get here'));
});
});
it("allowGraph('[a.[a, b], b.[a, c]]').allowGraph('[a.[c, d], b.c.[b, d]]').withGraphFetched('b.c.b') should be ok", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a.[a, b], b.[a, c]]')
.allowGraph('[a.[c, d], b.c.[b, d]]')
.withGraphFetched('b.c.b')
.then(() => {
expect(executedQueries).to.have.length(1);
done();
})
.catch(() => {
done(new Error('should not get here'));
});
});
it("allowGraph('[a, b.c.[d, e]]').withGraphFetched('a.b') should fail", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a, b.c.[d, e]]')
.withGraphFetched('a.b')
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
expect(executedQueries).to.have.length(0);
done();
});
});
it("allowGraph('[a, b.c.[d, e]]').allowGraph('a.[c, d]').withGraphFetched('a.b') should fail", (done) => {
QueryBuilder.forClass(TestModel)
.allowGraph('[a, b.c.[d, e]]')
.allowGraph('a.[c, d]')
.withGraphFetched('a.b')
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
expect(executedQueries).to.have.length(0);
done();
});
});
it("eager('a.b').allowGraph('[a, b.c.[d, e]]') should fail", (done) => {
QueryBuilder.forClass(TestModel)
.withGraphFetched('a.b')
.allowGraph('[a, b.c.[d, e]]')
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
expect(executedQueries).to.have.length(0);
done();
});
});
it("eager('a.b').allowGraph('[a, b.c.[d, e]]').allowGraph('a.[c, d]') should fail", (done) => {
QueryBuilder.forClass(TestModel)
.withGraphFetched('a.b')
.allowGraph('[a, b.c.[d, e]]')
.allowGraph('a.[c, d]')
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
expect(executedQueries).to.have.length(0);
done();
});
});
it("eager('b.c.d.e').allowGraph('[a, b.c.[d, e]]') should fail", (done) => {
QueryBuilder.forClass(TestModel)
.withGraphFetched('b.c.d.e')
.allowGraph('[a, b.c.[d, e]]')
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
expect(executedQueries).to.have.length(0);
done();
});
});
it("eager('b.c.d.e').allowGraph('[a, b.c.[d, e]]').allowGraph('b.c.a') should fail", (done) => {
QueryBuilder.forClass(TestModel)
.withGraphFetched('b.c.d.e')
.allowGraph('[a, b.c.[d, e]]')
.allowGraph('b.c.a')
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
expect(executedQueries).to.have.length(0);
done();
});
});
it('graphExpressionObject() should return the eager expression as an object', () => {
const builder = QueryBuilder.forClass(TestModel).withGraphFetched('[a, b.c(foo)]');
expect(builder.graphExpressionObject()).to.eql({
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: ['a', 'b'],
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: [],
},
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
$childNames: ['c'],
c: {
$name: 'c',
$relation: 'c',
$modify: ['foo'],
$recursive: false,
$allRecursive: false,
$childNames: [],
},
},
});
});
it("modifiers() should return the eager expression's modifiers as an object", () => {
const foo = (builder) => builder.where('foo');
const builder = QueryBuilder.forClass(TestModel).withGraphFetched('[a, b.c(foo)]').modifiers({
foo,
});
expect(builder.modifiers()).to.eql({
foo,
});
});
it('should use correct query builders', (done) => {
class M1QueryBuilder extends QueryBuilder {}
class M2QueryBuilder extends QueryBuilder {}
class M3QueryBuilder extends QueryBuilder {}
class M1 extends Model {
static get tableName() {
return 'M1';
}
static get relationMappings() {
return {
m2: {
relation: Model.HasManyRelation,
modelClass: M2,
join: {
from: 'M1.id',
to: 'M2.m1Id',
},
},
};
}
static get QueryBuilder() {
return M1QueryBuilder;
}
}
class M2 extends Model {
static get tableName() {
return 'M2';
}
static get relationMappings() {
return {
m3: {
relation: Model.BelongsToOneRelation,
modelClass: M3,
join: {
from: 'M2.m3Id',
to: 'M3.id',
},
},
};
}
static get QueryBuilder() {
return M2QueryBuilder;
}
}
class M3 extends Model {
static get tableName() {
return 'M3';
}
static get QueryBuilder() {
return M3QueryBuilder;
}
}
M1.knex(mockKnex);
M2.knex(mockKnex);
M3.knex(mockKnex);
mockKnexQueryResults = [
[{ id: 1, m1Id: 2, m3Id: 3 }],
[{ id: 1, m1Id: 2, m3Id: 3 }],
[{ id: 1, m1Id: 2, m3Id: 3 }],
];
let filter1Check = false;
let filter2Check = false;
QueryBuilder.forClass(M1)
.withGraphFetched('m2.m3')
.modifyGraph('m2', (builder) => {
filter1Check = builder instanceof M2QueryBuilder;
})
.modifyGraph('m2.m3', (builder) => {
filter2Check = builder instanceof M3QueryBuilder;
})
.then(() => {
expect(executedQueries).to.eql([
'select "M1".* from "M1"',
'select "M2".* from "M2" where "M2"."m1Id" in (1)',
'select "M3".* from "M3" where "M3"."id" in (3)',
]);
expect(filter1Check).to.equal(true);
expect(filter2Check).to.equal(true);
done();
})
.catch(done);
});
it('$afterFind should be called after relations have been fetched', (done) => {
class M1 extends Model {
static get tableName() {
return 'M1';
}
$afterFind() {
this.ids = _.map(this.someRel, 'id');
}
static get relationMappings() {
return {
someRel: {
relation: Model.HasManyRelation,
modelClass: M1,
join: {
from: 'M1.id',
to: 'M1.m1Id',
},
},
};
}
}
M1.knex(mockKnex);
mockKnexQueryResults = [
[{ id: 1 }, { id: 2 }],
[
{ id: 3, m1Id: 1 },
{ id: 4, m1Id: 1 },
{ id: 5, m1Id: 2 },
{ id: 6, m1Id: 2 },
],
[
{ id: 7, m1Id: 3 },
{ id: 8, m1Id: 3 },
{ id: 9, m1Id: 4 },
{ id: 10, m1Id: 4 },
{ id: 11, m1Id: 5 },
{ id: 12, m1Id: 5 },
{ id: 13, m1Id: 6 },
{ id: 14, m1Id: 6 },
],
];
QueryBuilder.forClass(M1)
.withGraphFetched('someRel.someRel')
.then((x) => {
expect(executedQueries).to.eql([
'select "M1".* from "M1"',
'select "M1".* from "M1" where "M1"."m1Id" in (1, 2)',
'select "M1".* from "M1" where "M1"."m1Id" in (3, 4, 5, 6)',
]);
expect(x).to.eql([
{
id: 1,
ids: [3, 4],
someRel: [
{
id: 3,
m1Id: 1,
ids: [7, 8],
someRel: [
{ id: 7, m1Id: 3, ids: [] },
{ id: 8, m1Id: 3, ids: [] },
],
},
{
id: 4,
m1Id: 1,
ids: [9, 10],
someRel: [
{ id: 9, m1Id: 4, ids: [] },
{ id: 10, m1Id: 4, ids: [] },
],
},
],
},
{
id: 2,
ids: [5, 6],
someRel: [
{
id: 5,
m1Id: 2,
ids: [11, 12],
someRel: [
{ id: 11, m1Id: 5, ids: [] },
{ id: 12, m1Id: 5, ids: [] },
],
},
{
id: 6,
m1Id: 2,
ids: [13, 14],
someRel: [
{ id: 13, m1Id: 6, ids: [] },
{ id: 14, m1Id: 6, ids: [] },
],
},
],
},
]);
done();
})
.catch(done);
});
});
describe('context', () => {
it('context() should merge context', () => {
const builder = TestModel.query();
builder.context({ a: 1 });
expect(builder.context()).to.eql({
a: 1,
});
builder.context({ b: 2 });
expect(builder.context()).to.eql({
a: 1,
b: 2,
});
expect(builder.context().transaction === mockKnex).to.equal(true);
});
it('clearContext() should clear the context', () => {
const builder = TestModel.query();
builder.context({ a: 1 });
expect(builder.context()).to.eql({
a: 1,
});
const builder2 = builder.clearContext();
expect(builder === builder2).to.equal(true);
expect(builder.context()).to.eql({});
});
it('`context` should merge context', () => {
const builder = TestModel.query();
const origContext = { a: 1 };
builder.context(origContext);
builder.context({ b: 2 });
expect(builder.context()).to.eql({
a: 1,
b: 2,
});
expect(origContext).to.eql({
a: 1,
});
expect(builder.context().transaction === mockKnex).to.equal(true);
});
it('`context` can be called without `context` having been called', () => {
const builder = TestModel.query();
const origContext = { a: 1 };
builder.context(origContext);
builder.context({ b: 2 });
expect(builder.context()).to.eql({
a: 1,
b: 2,
});
expect(origContext).to.eql({
a: 1,
});
expect(builder.context().transaction === mockKnex).to.equal(true);
});
it('cloning a query builder should clone the context also', () => {
const builder = TestModel.query();
const origContext = { a: 1 };
builder.context(origContext);
const builder2 = builder.clone();
builder2.context({ b: 2 });
expect(builder.context()).to.eql({
a: 1,
});
expect(builder2.context()).to.eql({
a: 1,
b: 2,
});
expect(origContext).to.eql({
a: 1,
});
expect(builder.context().transaction === mockKnex).to.equal(true);
expect(builder2.context().transaction === mockKnex).to.equal(true);
});
it('calling `childQueryOf` should copy a reference of the context', () => {
const builder = TestModel.query();
const origContext = { a: 1 };
builder.context(origContext);
const builder2 = TestModel.query().childQueryOf(builder);
builder2.context({ b: 2 });
expect(builder.context()).to.eql({
a: 1,
b: 2,
});
expect(builder2.context()).to.eql({
a: 1,
b: 2,
});
expect(origContext).to.eql({
a: 1,
});
expect(builder.context().transaction === mockKnex).to.equal(true);
expect(builder2.context().transaction === mockKnex).to.equal(true);
});
it('calling `childQueryOf(builder, { fork: true })` should copy the context', () => {
const builder = TestModel.query();
const origContext = { a: 1 };
builder.context(origContext);
const builder2 = TestModel.query().childQueryOf(builder, { fork: true });
builder2.context({ b: 2 });
expect(builder.context()).to.eql({
a: 1,
});
expect(builder2.context()).to.eql({
a: 1,
b: 2,
});
expect(origContext).to.eql({
a: 1,
});
expect(builder.context().transaction === mockKnex).to.equal(true);
expect(builder2.context().transaction === mockKnex).to.equal(true);
});
it('values saved to context in hooks should be available later', () => {
let foo = null;
TestModel = class extends TestModel {
$beforeUpdate(opt, ctx) {
ctx.foo = 100;
}
$afterUpdate(opt, ctx) {
foo = ctx.foo;
}
};
return TestModel.query()
.patch({ a: 1 })
.then(() => {
expect(foo).to.equal(100);
});
});
});
describe('toFindQuery', () => {
class Person extends Model {
static get tableName() {
return 'person';
}
static get relationMappings() {
return {
pets: {
relation: this.HasManyRelation,
modelClass: Pet,
join: {
from: 'person.id',
to: 'pet.owner_id',
},
},
movies: {
relation: this.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'person.id',
through: {
from: 'person_movie.person_id',
to: 'person_movie.movie_id',
},
to: 'movie.id',
},
},
};
}
}
class Pet extends Model {
static get tableName() {
return 'pet';
}
static get relationMappings() {
return {
owner: {
relation: this.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'pet.owner_id',
to: 'person.id',
},
},
};
}
}
class Movie extends Model {
static get tableName() {
return 'movie';
}
}
it('query().update()', () => {
testToFindQuery(
Person.query(mockKnex).update({ foo: 'bar' }).where('name', 'like', '%foo'),
'select "person".* from "person" where "name" like ?',
);
});
it('query().relatedQuery("hasMany").update()', () => {
testToFindQuery(
Person.fromJson({ id: 1 })
.$relatedQuery('pets', mockKnex)
.update({ foo: 'bar' })
.where('name', 'like', '%foo'),
'select "pet".* from "pet" where "pet"."owner_id" in (?) and "name" like ?',
);
});
it('query().relatedQuery("belongsToOne").update()', () => {
testToFindQuery(
Pet.fromJson({ owner_id: 1 })
.$relatedQuery('owner', mockKnex)
.patch({ foo: 'bar' })
.where('name', 'like', '%foo'),
'select "person".* from "person" where "person"."id" in (?) and "name" like ?',
);
});
function testToFindQuery(query, sql) {
expect(query.toFindQuery().toKnexQuery().toSQL().sql).to.equal(sql);
}
});
});
const operationBuilder = QueryBuilder.forClass(Model);
function createFindOperation(builder, whereObj) {
const operation = operationBuilder._findOperationFactory(builder);
const origClone = operation.clone;
function clone() {
const operation = origClone.call(this);
operation.onBefore2 = operation.onAfter2 = () => {};
operation.onBuildKnex = (knexBuilder) => {
knexBuilder.where(whereObj);
};
operation.clone = clone;
return operation;
}
operation.clone = clone;
return operation.clone();
}
function createInsertOperation(builder, mergeWithModel) {
const operation = operationBuilder._insertOperationFactory(builder);
const origClone = operation.clone;
function clone() {
const operation = origClone.call(this);
operation.onBefore2 = operation.onBefore3 = operation.onAfter2 = () => {};
operation.onAdd = function (_, args) {
this.models = [args[0]];
return true;
};
operation.onBuildKnex = function (knexBuilder) {
let json = _.merge(this.models[0], mergeWithModel);
knexBuilder.insert(json);
};
operation.clone = clone;
return operation;
}
operation.clone = clone;
return operation.clone();
}
function createUpdateOperation(builder, mergeWithModel) {
const operation = operationBuilder._updateOperationFactory(builder);
const origClone = operation.clone;
function clone() {
const operation = origClone.call(this);
operation.onBefore2 = operation.onBefore3 = operation.onAfter2 = () => {};
operation.onAdd = function (_, args) {
this.model = args[0];
return true;
};
operation.onBuildKnex = function (knexBuilder) {
let json = _.merge(this.model, mergeWithModel);
knexBuilder.update(json);
};
operation.clone = clone;
return operation;
}
operation.clone = clone;
return operation.clone();
}
function createDeleteOperation(builder, whereObj) {
const operation = operationBuilder._deleteOperationFactory(builder);
const origClone = operation.clone;
function clone() {
const operation = origClone.call(this);
operation.onBefore2 = operation.onAfter2 = () => {};
operation.onBuildKnex = (knexBuilder) => {
knexBuilder.delete().where(whereObj);
};
operation.clone = clone;
return operation;
}
operation.clone = clone;
return operation.clone();
}
================================================
FILE: tests/unit/queryBuilder/ReferenceBuilder.js
================================================
const expect = require('expect.js');
const { ref, Model } = require('../../../');
const { ReferenceBuilder } = require('../../../lib/queryBuilder/ReferenceBuilder');
function toRawArgs(ref) {
return ref._createRawArgs(Model.query());
}
describe('ReferenceBuilder', () => {
it('fail if reference cannot be parsed', () => {
expect(() => {
ref();
}).to.throwException();
expect(() => {
ref('');
}).to.throwException();
});
it('should create ReferenceBuilder', () => {
let reference = ref('Awwww.ItWorks');
expect(reference instanceof ReferenceBuilder).to.be.ok();
expect(toRawArgs(reference)).to.eql(['??', ['Awwww.ItWorks']]);
});
it('table method should replace table', () => {
let reference = ref('Table.Column').table('Foo');
expect(toRawArgs(reference)).to.eql(['??', ['Foo.Column']]);
});
it('should allow plain knex reference + casting', () => {
let reference = ref('Table.Column').castBigInt();
expect(toRawArgs(reference)).to.eql(['CAST(?? AS bigint)', ['Table.Column']]);
});
it('should allow field expression + casting', () => {
let reference = ref('Table.Column:jsonAttr').castBool();
expect(toRawArgs(reference)).to.eql(["CAST(??#>>'{jsonAttr}' AS boolean)", ['Table.Column']]);
});
it('should allow field expression + no casting', () => {
let reference = ref('Table.Column:jsonAttr');
expect(toRawArgs(reference)).to.eql(["??#>'{jsonAttr}'", ['Table.Column']]);
});
it('should support few different casts', () => {
expect(toRawArgs(ref('Table.Column:jsonAttr').castText())).to.eql([
"CAST(??#>>'{jsonAttr}' AS text)",
['Table.Column'],
]);
expect(toRawArgs(ref('Table.Column:jsonAttr').castInt())).to.eql([
"CAST(??#>>'{jsonAttr}' AS integer)",
['Table.Column'],
]);
expect(toRawArgs(ref('Table.Column:jsonAttr').castBigInt())).to.eql([
"CAST(??#>>'{jsonAttr}' AS bigint)",
['Table.Column'],
]);
expect(toRawArgs(ref('Table.Column:jsonAttr').castFloat())).to.eql([
"CAST(??#>>'{jsonAttr}' AS float)",
['Table.Column'],
]);
expect(toRawArgs(ref('Table.Column:jsonAttr').castDecimal())).to.eql([
"CAST(??#>>'{jsonAttr}' AS decimal)",
['Table.Column'],
]);
expect(toRawArgs(ref('Table.Column:jsonAttr').castReal())).to.eql([
"CAST(??#>>'{jsonAttr}' AS real)",
['Table.Column'],
]);
expect(toRawArgs(ref('Table.Column:jsonAttr').castBool())).to.eql([
"CAST(??#>>'{jsonAttr}' AS boolean)",
['Table.Column'],
]);
expect(toRawArgs(ref('Table.Column').castJson())).to.eql(['to_jsonb(??)', ['Table.Column']]);
});
});
================================================
FILE: tests/unit/queryBuilder/RelationExpression.js
================================================
const chai = require('chai');
const expect = require('expect.js');
const { RelationExpression } = require('../../../');
describe('RelationExpression', () => {
describe('parse', () => {
it('empty expression', () => {
testParse('', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
});
testParse(
{},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
},
);
});
it('non-string', () => {
let expectedResult = {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
};
testParse(null, expectedResult);
testParse(false, expectedResult);
testParse(true, expectedResult);
testParse(1, expectedResult);
testParse({}, expectedResult);
testParse([], expectedResult);
});
describe('single relation', () => {
it('single relation', () => {
testParse('a', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
testParse(
{
a: {},
},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
);
});
it('list with one value', () => {
testParse('[a]', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
});
it('weird characters', () => {
testParse('_-%§$?+1Aa!€^', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
'_-%§$?+1Aa!€^': {
$name: '_-%§$?+1Aa!€^',
$relation: '_-%§$?+1Aa!€^',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
});
});
describe('nested relations', () => {
it('one level', () => {
testParse('a.b', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
});
});
it('two levels', () => {
testParse('a.b.c', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
});
testParse(
{
a: {
b: {
c: {},
},
},
},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
},
);
});
});
it('multiple relations', () => {
testParse('[a, b, c]', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
testParse(
{
a: true,
b: {},
c: true,
},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
);
});
it('multiple nested relations', () => {
testParse('[a.b, c.d.e, f]', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
d: {
$name: 'd',
$relation: 'd',
$modify: [],
$recursive: false,
$allRecursive: false,
e: {
$name: 'e',
$relation: 'e',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
f: {
$name: 'f',
$relation: 'f',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
testParse(
{
a: {
b: true,
},
c: {
d: {
e: {},
},
},
f: {},
},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
d: {
$name: 'd',
$relation: 'd',
$modify: [],
$recursive: false,
$allRecursive: false,
e: {
$name: 'e',
$relation: 'e',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
f: {
$name: 'f',
$relation: 'f',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
);
});
it('deep nesting and nested lists', () => {
testParse('[a.[b, c.[d, e.f]], g]', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
d: {
$name: 'd',
$relation: 'd',
$modify: [],
$recursive: false,
$allRecursive: false,
},
e: {
$name: 'e',
$relation: 'e',
$modify: [],
$recursive: false,
$allRecursive: false,
f: {
$name: 'f',
$relation: 'f',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
},
g: {
$name: 'g',
$relation: 'g',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
});
it('arguments', () => {
testParse('[a(arg1,arg2,arg3), b(arg4) . [c(), d(arg5 arg6), e]]', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: ['arg1', 'arg2', 'arg3'],
$recursive: false,
$allRecursive: false,
},
b: {
$name: 'b',
$relation: 'b',
$modify: ['arg4'],
$recursive: false,
$allRecursive: false,
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
d: {
$name: 'd',
$relation: 'd',
$modify: ['arg5', 'arg6'],
$recursive: false,
$allRecursive: false,
},
e: {
$name: 'e',
$relation: 'e',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
});
testParse(
{
a: {
$modify: ['arg1', 'arg2', 'arg3'],
},
b: {
$modify: ['arg4'],
c: true,
d: {
$modify: ['arg5', 'arg6'],
},
e: {},
},
},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: ['arg1', 'arg2', 'arg3'],
$recursive: false,
$allRecursive: false,
},
b: {
$name: 'b',
$relation: 'b',
$modify: ['arg4'],
$recursive: false,
$allRecursive: false,
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
d: {
$name: 'd',
$relation: 'd',
$modify: ['arg5', 'arg6'],
$recursive: false,
$allRecursive: false,
},
e: {
$name: 'e',
$relation: 'e',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
);
testParse('a(f1).b(^f2, ^f3)', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: ['f1'],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'b',
$modify: ['^f2', '^f3'],
$recursive: false,
$allRecursive: false,
},
},
});
});
it('alias', () => {
testParse('a as b', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
testParse(
{
b: {
$relation: 'a',
},
},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
);
testParse('aasb', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
aasb: {
$name: 'aasb',
$relation: 'aasb',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
testParse('[ as , b]', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
as: {
$name: 'as',
$relation: 'as',
$modify: [],
$recursive: false,
$allRecursive: false,
},
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
});
testParse(
`a as aa.[
b as bb,
c as cc
]`,
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
aa: {
$name: 'aa',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
bb: {
$name: 'bb',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
cc: {
$name: 'cc',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
);
testParse(
`a(f1, f2) as aa . [
c(f3, f4) as cc,
b as bb .[
e,
f as ff
]
]`,
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
aa: {
$name: 'aa',
$relation: 'a',
$modify: ['f1', 'f2'],
$recursive: false,
$allRecursive: false,
cc: {
$name: 'cc',
$relation: 'c',
$modify: ['f3', 'f4'],
$recursive: false,
$allRecursive: false,
},
bb: {
$name: 'bb',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
e: {
$name: 'e',
$relation: 'e',
$modify: [],
$recursive: false,
$allRecursive: false,
},
ff: {
$name: 'ff',
$relation: 'f',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
},
);
});
it('should ignore whitespace', () => {
testParse(
'\n\r\t [ a (\narg1\n arg2,arg3), \n \n b\n(arg4) . [c(), \td (arg5 arg6), e] \r] ',
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: ['arg1', 'arg2', 'arg3'],
$recursive: false,
$allRecursive: false,
},
b: {
$name: 'b',
$relation: 'b',
$modify: ['arg4'],
$recursive: false,
$allRecursive: false,
c: {
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
d: {
$name: 'd',
$relation: 'd',
$modify: ['arg5', 'arg6'],
$recursive: false,
$allRecursive: false,
},
e: {
$name: 'e',
$relation: 'e',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
},
);
});
it('should throw with invalid input', () => {
testParseFail('.');
testParseFail('..');
testParseFail('a.');
testParseFail('.a');
testParseFail('[');
testParseFail(']');
testParseFail('[[]]');
testParseFail('[a');
testParseFail('a]');
testParseFail('[a.]');
testParseFail('a.[b]]');
testParseFail('a.[.]');
testParseFail('a.[.b]');
testParseFail('[a,,b]');
// Alias tests
testParseFail('a asb');
testParseFail('aas b');
testParseFail('a asd b');
// TODO: enable for v2.0.
// testParseFail('[a.b, a.c]');
// testParseFail('a.[b.c, b.d]');
});
it('should accept single function or modifier name in $modifiers', () => {
const modifier = () => {};
testParse(
{
a: {
$modify: modifier,
},
},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: [modifier],
$recursive: false,
$allRecursive: false,
},
},
);
testParse(
{
a: {
$modify: 'someModifier',
},
},
{
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'a',
$modify: ['someModifier'],
$recursive: false,
$allRecursive: false,
},
},
);
});
});
it('clone', () => {
testClone('[aaa as a . bbb as b.^, c(f)]', {
$name: null,
$relation: null,
$modify: [],
$recursive: false,
$allRecursive: false,
a: {
$name: 'a',
$relation: 'aaa',
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'bbb',
$modify: [],
$recursive: true,
$allRecursive: false,
},
},
c: {
$name: 'c',
$relation: 'c',
$modify: ['f'],
$recursive: false,
$allRecursive: false,
},
});
});
describe('#expressionsAtPath', () => {
it('a from a', () => {
testPath('a', 'a', [
{
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
]);
});
it('a from a.a', () => {
testPath('a.b', 'a', [
{
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
b: {
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
},
]);
});
it('a.b from a', () => {
testPath('a', 'a.b', []);
});
it('a.b from a.b', () => {
testPath('a.b', 'a.b', [
{
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
]);
});
it('a.b from a.[b, c]', () => {
testPath('a.[b, c]', 'a.b', [
{
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
]);
});
it('a.[b, c] from a.[b, c]', () => {
testPath('a.[b, c]', 'a.[b, c]', [
{
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
{
$name: 'c',
$relation: 'c',
$modify: [],
$recursive: false,
$allRecursive: false,
},
]);
});
it('a.[b, d] from a.[b, c]', () => {
testPath('a.[b, c]', 'a.[b, d]', [
{
$name: 'b',
$relation: 'b',
$modify: [],
$recursive: false,
$allRecursive: false,
},
]);
});
it('[a, b.c.d.[e, f]] from [a, b.[g, c.[d.[e, f], i], h]]', () => {
testPath('[a, b.[g, c.[d.[e, f], i], h]]', '[a, b.c.d.[e, f]]', [
{
$name: 'a',
$relation: 'a',
$modify: [],
$recursive: false,
$allRecursive: false,
},
{
$name: 'e',
$relation: 'e',
$modify: [],
$recursive: false,
$allRecursive: false,
},
{
$name: 'f',
$relation: 'f',
$modify: [],
$recursive: false,
$allRecursive: false,
},
]);
});
it('b.c.d.[e, f] from [a, b.[g, c.[d.[e(a1), f(a2)], i], h]]', () => {
testPath('[a, b.[g, c.[d.[e(a1), f(a2)], i], h]]', 'b.c.d.[e, f]', [
{
$name: 'e',
$relation: 'e',
$modify: ['a1'],
$recursive: false,
$allRecursive: false,
},
{
$name: 'f',
$relation: 'f',
$modify: ['a2'],
$recursive: false,
$allRecursive: false,
},
]);
});
it('b.c.d from [a, b.[g, c.[d.[e(a1), f(a2)], i], h]]', () => {
testPath('[a, b.[g, c.[d.[e(a1), f(a2)], i], h]]', 'b.c.d', [
{
$name: 'd',
$relation: 'd',
$modify: [],
$recursive: false,
$allRecursive: false,
e: {
$name: 'e',
$relation: 'e',
$modify: ['a1'],
$recursive: false,
$allRecursive: false,
},
f: {
$name: 'f',
$relation: 'f',
$modify: ['a2'],
$recursive: false,
$allRecursive: false,
},
},
]);
});
});
describe('#merge', () => {
testMerge('a', 'b', '[a, b]');
testMerge('a.b', 'b', '[a.b, b]');
testMerge('a', 'b.c', '[a, b.c]');
testMerge('[a, b]', '[b, c]', '[a, b, c]');
testMerge('a.b', 'a.c', 'a.[b, c]');
testMerge('[a.b, d]', 'a.c', '[a.[b, c], d]');
testMerge('a.[c, d.e, g]', 'a.[c.l, d.[e.m, n], f]', 'a.[c.l, d.[e.m, n], g, f]');
testMerge('a.^4', 'a.^3', 'a.^4');
testMerge('a.^', 'a.^6', 'a.^');
testMerge('a.^6', 'a.^', 'a.^');
testMerge('a.a', 'a.^', 'a.^');
testMerge('a(f)', 'a(g)', 'a(f, g)');
testMerge('a.b(f)', 'a.b(g)', 'a.b(f, g)');
});
describe('#toString', () => {
testToString('a');
testToString('a.b');
testToString('a as b.b as c');
testToString('a as b.[b as c, d as e]');
testToString('a.[b, c]');
testToString('a.[b, c.d]');
testToString('[a, b]');
testToString('[a.[b, c], d.e.f.[g, h.i]]');
testToString('a.*');
testToString('a.^');
testToString('a.^3');
testToString('[a.*, b.c.^]');
});
describe('#isSubExpression', () => {
testSubExpression('*', 'a');
testSubExpression('*', '[a, b]');
testSubExpression('*', 'a.b');
testSubExpression('*', 'a.b.[c, d]');
testSubExpression('*', '[a, b.c, c.d.[e, f.g.[h, i]]]');
testSubExpression('*', '*');
testSubExpression('*', 'a.*');
testSubExpression('*', 'a.^');
testSubExpression('a.*', 'a');
testSubExpression('a.*', 'a.b');
testSubExpression('a.*', 'a.*');
testSubExpression('a.*', 'a.^');
testSubExpression('a.*', 'a.b.c');
testSubExpression('a.*', 'a.[b, c]');
testSubExpression('a.*', 'a.[b, c.d]');
testSubExpression('a.[b.*, c]', 'a.b.c.d');
testSubExpression('a.[b.*, c]', 'a.[b.c.d, c]');
testSubExpression('a.[b.*, c]', 'a.[b.[c, d], c]');
testNotSubExpression('a.*', 'b');
testNotSubExpression('a.*', 'c');
testNotSubExpression('a.*', '[a, b]');
testNotSubExpression('a.*', '*');
testNotSubExpression('a.[b.*, c]', 'a.[b.c.d, c.d]');
testSubExpression('a.b.*', 'a.b.*');
testNotSubExpression('a.b.*', '*');
testNotSubExpression('a.b.*', 'a.*');
testNotSubExpression('a.b.*', 'a.[b.*, c]');
testSubExpression('a', 'a');
testSubExpression('a.b', 'a.b');
testSubExpression('a.b.[c, d]', 'a.b.[c, d]');
testSubExpression('[a.b.[c, d], e]', '[a.b.[c, d], e]');
testSubExpression('a.b', 'a');
testNotSubExpression('a', 'a.b');
testSubExpression('a.b.c', 'a');
testSubExpression('a.b.c', 'a.b');
testNotSubExpression('a', 'a.b.c');
testNotSubExpression('a.b', 'a.b.c');
testSubExpression('a.[b, c]', 'a');
testSubExpression('a.[b, c]', 'a.b');
testSubExpression('a.[b, c]', 'a.c');
testNotSubExpression('a.[b, c]', 'a.c.d');
testNotSubExpression('a.[b, c]', 'b');
testNotSubExpression('a.[b, c]', 'c');
testSubExpression('[a.b.[c, d.e], b]', 'a');
testSubExpression('[a.b.[c, d.e], b]', 'b');
testSubExpression('[a.b.[c, d.e], b]', 'a.b');
testSubExpression('[a.b.[c, d.e], b]', 'a.b.c');
testSubExpression('[a.b.[c, d.e], b]', 'a.b.d');
testSubExpression('[a.b.[c, d.e], b]', 'a.b.d.e');
testSubExpression('[a.b.[c, d.e], b]', '[a.b.[c, d], b]');
testSubExpression('[a.b.[c, d.e], b]', '[a.b.[c, d.[e]], b]');
testNotSubExpression('[a.b.[c, d.e], b]', 'c');
testNotSubExpression('[a.b.[c, d.e], b]', 'b.c');
testNotSubExpression('[a.b.[c, d.e], b]', '[a, b, c]');
testNotSubExpression('[a.b.[c, d.e], b]', 'a.b.e');
testNotSubExpression('[a.b.[c, d.e], b]', '[a.b.e, b]');
testNotSubExpression('[a.b.[c, d.e], b]', '[a.b.c, c]');
testNotSubExpression('[a.b.[c, d.e], b]', 'a.b.[c, e]');
testNotSubExpression('[a.b.[c, d.e], b]', 'a.b.[c, d, e]');
testNotSubExpression('[a.b.[c, d.e], b]', 'a.b.[c, d.[e, f]]');
testSubExpression('a.^', 'a.^');
testSubExpression('a.^', 'a.^100');
testSubExpression('a.^3', 'a.^3');
testSubExpression('a.^3', 'a.^2');
testSubExpression('a.^3', 'a.^1');
testSubExpression('a.^3', 'a.a.a');
testSubExpression('a.^3', 'a.a');
testSubExpression('a.^3', 'a');
testSubExpression('a.^', 'a.a');
testSubExpression('a.^', 'a.a.^');
testSubExpression('a.^', 'a.a.a');
testSubExpression('a.^', 'a.a.a.^');
testSubExpression('[a.^, b.[c.^, d]]', 'a');
testSubExpression('[a.^, b.[c.^, d]]', 'b.c');
testSubExpression('[a.^, b.[c.^, d]]', 'b.c.^');
testSubExpression('[a.^, b.[c.^, d]]', '[a, b]');
testSubExpression('[a.^, b.[c.^, d]]', '[a.^, b]');
testSubExpression('[a.^, b.[c.^, d]]', '[a.a, b]');
testSubExpression('[a.^, b.[c.^, d]]', '[a.a.^, b.c]');
testSubExpression('[a.^, b.[c.^, d]]', '[a.a.^, b.c.^]');
testSubExpression('[a.^, b.[c.^, d]]', '[a.a.^, b.c.c]');
testSubExpression('[a.^, b.[c.^, d]]', '[a.a.^, b.[c.c.c, d]]');
testNotSubExpression('a.^', 'b');
testNotSubExpression('a.^', 'a.b');
testNotSubExpression('a.^', 'a.a.b');
testNotSubExpression('a.^', 'a.a.b.^');
testNotSubExpression('[a.^, b.[c.^, d]]', 'a.b');
testNotSubExpression('[a.^, b.[c.^, d]]', '[c, b]');
testNotSubExpression('[a.^, b.[c.^, d]]', '[c, b]');
testNotSubExpression('[a.^, b.[c.^, d]]', 'b.c.d');
testNotSubExpression('a.^', 'a.[b, ^, c]');
testNotSubExpression('a.^3', 'a.^');
testNotSubExpression('a.^3', 'a.^4');
testNotSubExpression('a.^3', 'a.a.a.a');
testSubExpression('[a as aa.[c as cc . d as dd], b as bb]', 'a as aa');
testSubExpression('[a as aa.[c as cc . d as dd], b as bb]', '[a as aa, b as bb]');
testSubExpression('[a as aa.[c as cc . d as dd], b as bb]', 'a as aa . c as cc');
testSubExpression('[a as aa.[c as cc . d as dd], b as bb]', 'a as aa . c as cc . d as dd');
});
describe('#forEachChildExrpression', () => {
it('should traverse first level children', () => {
const expr = RelationExpression.create('[a, b.c, d]');
const items = [];
const fakeModel = {
getRelationNames() {
return ['a', 'b', 'd'];
},
getRelationUnsafe(name) {
return name + name;
},
};
expr.forEachChildExpression(fakeModel, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
});
expect(items).to.eql([
{ exprName: 'a', relation: 'aa' },
{ exprName: 'b', relation: 'bb' },
{ exprName: 'd', relation: 'dd' },
]);
});
it('should work with recursive expressions', () => {
const expr = RelationExpression.create('a.^');
const items = [];
const fakeModel = {
getRelationNames() {
return ['a'];
},
getRelationUnsafe(name) {
return name + name;
},
};
expr.forEachChildExpression(fakeModel, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
expr.forEachChildExpression(fakeModel, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
expr.forEachChildExpression(fakeModel, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
});
});
});
expect(items).to.eql([
{ exprName: 'a', relation: 'aa' },
{ exprName: 'a', relation: 'aa' },
{ exprName: 'a', relation: 'aa' },
]);
});
it('should work with limited recursive expressions', () => {
const expr = RelationExpression.create('a.^2');
const items = [];
const fakeModel = {
getRelationNames() {
return ['a'];
},
getRelationUnsafe(name) {
return name + name;
},
};
expr.forEachChildExpression(fakeModel, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
expr.forEachChildExpression(fakeModel, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
expr.forEachChildExpression(fakeModel, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
});
});
});
expect(items).to.eql([
{ exprName: 'a', relation: 'aa' },
{ exprName: 'a', relation: 'aa' },
]);
});
it('should work with all recursive expressions', () => {
const expr = RelationExpression.create('a.*');
const items = [];
const fakeModel1 = {
getRelationNames() {
return ['a'];
},
getRelationUnsafe(name) {
return name + name;
},
};
const fakeModel2 = {
getRelationNames() {
return ['b', 'c', 'd'];
},
getRelationUnsafe(name) {
return name + name;
},
};
expr.forEachChildExpression(fakeModel1, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
expr.forEachChildExpression(fakeModel2, (expr, relation) => {
items.push({ exprName: expr.node.$name, relation });
});
});
expect(items).to.eql([
{ exprName: 'a', relation: 'aa' },
{ exprName: 'b', relation: 'bb' },
{ exprName: 'c', relation: 'cc' },
{ exprName: 'd', relation: 'dd' },
]);
});
});
function testParse(str, parsed) {
chai.expect(RelationExpression.create(str).node).to.containSubset(parsed);
}
function testClone(expr, cloned) {
chai.expect(RelationExpression.create(expr).clone().node).to.containSubset(cloned);
}
function testMerge(str1, str2, parsed) {
it(str1 + ' + ' + str2 + ' --> ' + parsed, () => {
expect(RelationExpression.create(str1).merge(str2).toString()).to.equal(parsed);
expect(
RelationExpression.create(str1).merge(RelationExpression.create(str2)).toString(),
).to.equal(parsed);
});
}
function testPath(str, path, expected) {
chai
.expect(
RelationExpression.create(str)
.expressionsAtPath(path)
.map((it) => it.node),
)
.to.containSubset(expected);
}
function testToString(str) {
it(str, () => {
expect(RelationExpression.create(str).toString()).to.equal(str);
});
}
function testToJSON(str, expectedJson) {
it(str, () => {
const json = RelationExpression.create(str).toJSON();
expect(json).to.eql(expectedJson);
expect(RelationExpression.create(json).toString()).to.eql(str);
});
}
function testParseFail(str) {
expect(() => {
RelationExpression.create(str);
}).to.throwException();
}
function testSubExpression(str, subStr) {
it('"' + subStr + '" is a sub expression of "' + str + '"', () => {
expect(RelationExpression.create(str).isSubExpression(subStr)).to.equal(true);
});
}
function testNotSubExpression(str, subStr) {
it('"' + subStr + '" is not a sub expression of "' + str + '"', () => {
expect(RelationExpression.create(str).isSubExpression(subStr)).to.equal(false);
});
}
});
================================================
FILE: tests/unit/queryBuilder/ValueBuilder.js
================================================
const expect = require('expect.js');
const { val, Model } = require('../../../');
const { ValueBuilder } = require('../../../lib/queryBuilder/ValueBuilder');
function toRawArgs(ref) {
return ref._createRawArgs(Model.query());
}
describe('ValueBuilder', () => {
it('should create ValueBuilder', () => {
let builder = val('Awwww.ItWorks');
expect(builder instanceof ValueBuilder).to.be.ok();
expect(toRawArgs(builder)).to.eql(['?', ['Awwww.ItWorks']]);
});
it('should allow casting', () => {
let builder = val('100').castBigInt();
expect(toRawArgs(builder)).to.eql(['CAST(? AS bigint)', ['100']]);
});
it('should stringify when casting to json', () => {
let builder = val({ value: 100 }).castJson();
expect(toRawArgs(builder)).to.eql(['CAST(? AS jsonb)', ['{"value":100}']]);
});
it('should expand arrays', () => {
let builder = val([1, 2, 3]).asArray();
expect(toRawArgs(builder)).to.eql(['ARRAY[?, ?, ?]', [1, 2, 3]]);
});
it('should support aliasing', () => {
let builder = val('Hello').as('greeting');
expect(toRawArgs(builder)).to.eql(['? as ??', ['Hello', 'greeting']]);
});
it('should support simultaneous casting and aliasing', () => {
let builder = val('1').castBigInt().as('total');
expect(toRawArgs(builder)).to.eql(['CAST(? AS bigint) as ??', ['1', 'total']]);
});
});
================================================
FILE: tests/unit/queryBuilder/jsonFieldExpressionParser.js
================================================
const _ = require('lodash'),
expect = require('expect.js'),
parser = require('../../../lib/queryBuilder/parsers/jsonFieldExpressionParser.js');
describe('jsonFieldExpressionParser', () => {
// basic index and field references
testParsing('col:[1]', ['col', 1]);
testParsing("col:['1']", ['col', '1']);
testParsing('col:[a]', ['col', 'a']);
testParsing("col:['a']", ['col', 'a']);
testParsing('col:a', ['col', 'a']);
// less basic random babbling of test cases
testParsing('123', ['123']);
testParsing('123:abc', ['123', 'abc']);
testParsing('123:[1].abc', ['123', 1, 'abc']);
testParsing('123:[1.2].abc', ['123', '1.2', 'abc']);
testParsing('col:[1.2][1].abc', ['col', '1.2', 1, 'abc']);
testParsing('col:["1.2"][1].abc', ['col', '1.2', 1, 'abc']);
testParsing("col:['1']", ['col', '1']);
// with different quotes
testParsing("col:['[1.2]'][1].abc", ['col', '[1.2]', 1, 'abc']);
testParsing("col:['1']", ['col', '1']);
// array reference having only quotes
testParsing("col:[']", ['col', "'"]);
testParsing("col:['']", ['col', "''"]);
testParsing("col:[''']", ['col', "'''"]);
testParsing('col:["]', ['col', '"']);
testParsing('col:[""]', ['col', '""']);
testParsing('col:["""]', ['col', '"""']);
// array reference having quotes and brackets
testParsing('col:field["nofa\'].il"]', ['col', 'field', "nofa'].il"]);
testParsing("col:field['nofa\"].il']", ['col', 'field', 'nofa"].il']);
// quotes in dotreference part
testParsing("col:I'mCool", ['col', "I'mCool"]);
testParsing('col:PleaseMindThe"Quote"', ['col', 'PleaseMindThe"Quote"']);
// spaces in dotreference part
testParsing('col:I work too [100]', ['col', 'I work too ', 100]);
// new column reference style
testParsing('MyCupOfTeaTable.cupOfTea:I work too [100]', [
'MyCupOfTeaTable.cupOfTea',
'I work too ',
100,
]);
// no column given
testFail(':[]');
testFail(':[nocolumn]');
testFail(':["nocolumn"]');
testFail(":['nocolumn']");
testFail(':nocolumn');
// invalid dotreference
testFail('col:[1].');
// trying to use index operator after dot
testFail('col:wat.[1]');
testFail("col:wat.['1']");
testFail('col:wat.["1"]');
testFail('col:wat[1].[1]');
testFail("col:wat[1].['1']");
testFail('col:wat[1].["1"]');
// opening square bracket in dot ref
testFail('col:a[1');
testFail("col:a['1'");
testFail('col:a["1"');
testFail('col:[1].a[1');
testFail("col:[1].a['1'");
testFail('col:[1].a["1"');
// closing square bracket in dot ref
testFail('col:a]1');
testFail("col:a]'1'");
testFail('col:a]"1"');
testFail('col:[1].a]1');
testFail("col:[1].a]'1'");
testFail('col:[1].a]"1"');
// invalid array references
testFail('col:wat[]');
testFail('col:wat[');
testFail('col:wat.a[');
testFail('col:wat]');
testFail('col:wat.a]');
testFail('col:wat[fa[il]');
testFail('col:wat[fa]il]');
testFail('col:wat.field[fa[il]');
testFail('col:wat.field[fa]il]');
// these should fail because inside quotes there is same type of
// quote => parser tries use stringWithoutSquareBrackets token
// for parsing => bracket in key fails parsing
testFail('col:field["fa"]il"]');
testFail("col:field['fa']il']");
describe("field expression parser's general options", () => {
it('should fail if wrong start rule in parser options', () => {
expect(() => {
parser.parse('col', { startRule: 'undefined is not a function' });
}).to.throwException();
});
it('should be able to give start rule as parameter', () => {
let result = parser.parse('col', { startRule: 'start' });
expect(result.columnName).to.be('col');
});
});
});
function testParsing(expr, expected) {
it(expr, () => {
let result = parser.parse(expr);
let resultArray = [result.columnName].concat(_.map(result.access, 'ref'));
expect(JSON.stringify(resultArray)).to.eql(JSON.stringify(expected));
});
}
function testFail(expr) {
it(expr + ' should fail', () => {
expect(() => {
parser.parse(expr);
}).to.throwException();
});
}
================================================
FILE: tests/unit/relations/BelongsToOneRelation.js
================================================
const _ = require('lodash'),
Knex = require('knex'),
expect = require('expect.js'),
Promise = require('bluebird'),
objection = require('../../../'),
knexMocker = require('../../../testUtils/mockKnex'),
RelationOwner = require('../../../lib/relations/RelationOwner').RelationOwner,
Model = objection.Model,
QueryBuilder = objection.QueryBuilder,
BelongsToOneRelation = objection.BelongsToOneRelation;
describe('BelongsToOneRelation', () => {
let mockKnexQueryResults = [];
let executedQueries = [];
let mockKnex = null;
let OwnerModel = null;
let RelatedModel = null;
let relation;
let compositeKeyRelation;
before(() => {
let knex = Knex({ client: 'pg' });
mockKnex = knexMocker(knex, function (mock, oldImpl, args) {
executedQueries.push(this.toString());
let result = mockKnexQueryResults.shift() || [];
let promise = Promise.resolve(result);
return promise.then.apply(promise, args);
});
});
beforeEach(() => {
mockKnexQueryResults = [];
executedQueries = [];
OwnerModel = class OwnerModel extends Model {
static get tableName() {
return 'OwnerModel';
}
};
RelatedModel = class RelatedModel extends Model {
static get tableName() {
return 'RelatedModel';
}
static get modifiers() {
return {
modifier: (builder) => builder.where('filteredProperty', true),
};
}
};
OwnerModel.knex(mockKnex);
RelatedModel.knex(mockKnex);
});
beforeEach(() => {
relation = new BelongsToOneRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: BelongsToOneRelation,
join: {
from: 'OwnerModel.relatedId',
to: 'RelatedModel.rid',
},
});
compositeKeyRelation = new BelongsToOneRelation('nameOfOurRelation', OwnerModel);
compositeKeyRelation.setMapping({
modelClass: RelatedModel,
relation: BelongsToOneRelation,
join: {
from: ['OwnerModel.relatedAId', 'OwnerModel.relatedBId'],
to: ['RelatedModel.aid', 'RelatedModel.bid'],
},
});
});
describe('find', () => {
it('should generate a find query', () => {
let owner = OwnerModel.fromJson({ id: 666, relatedId: 1 });
let expectedResult = [{ id: 1, a: 10, rid: 1 }];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel).findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.eql(expectedResult[0]);
expect(owner.nameOfOurRelation).to.eql(expectedResult[0]);
expect(result).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."rid" in (1)',
);
});
});
it('should generate a find query (composite key)', () => {
let expectedResult = [
{ id: 1, aid: 11, bid: 22 },
{ id: 2, aid: 11, bid: 33 },
];
mockKnexQueryResults = [expectedResult];
let owners = [
OwnerModel.fromJson({ id: 666, relatedAId: 11, relatedBId: 22 }),
OwnerModel.fromJson({ id: 667, relatedAId: 11, relatedBId: 33 }),
];
let builder = QueryBuilder.forClass(RelatedModel).findOperationFactory((builder) => {
return compositeKeyRelation.find(builder, RelationOwner.create(owners));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owners[0].nameOfOurRelation).to.equal(result[0]);
expect(owners[1].nameOfOurRelation).to.equal(result[1]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where ("RelatedModel"."aid", "RelatedModel"."bid") in ((11, 22), (11, 33))',
);
});
});
it('should find for multiple owners', () => {
let expectedResult = [
{ id: 1, a: 10, rid: 2 },
{ id: 2, a: 10, rid: 3 },
];
mockKnexQueryResults = [expectedResult];
let owners = [
OwnerModel.fromJson({ id: 666, relatedId: 2 }),
OwnerModel.fromJson({ id: 667, relatedId: 3 }),
];
let builder = QueryBuilder.forClass(RelatedModel).findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owners));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owners[0].nameOfOurRelation).to.equal(result[0]);
expect(owners[1].nameOfOurRelation).to.equal(result[1]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."rid" in (2, 3)',
);
});
});
it('explicit selects should override the RelatedModel.*', () => {
let expectedResult = [{ id: 1, a: 10, rid: 2 }];
mockKnexQueryResults = [expectedResult];
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
let builder = QueryBuilder.forClass(RelatedModel)
.findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
})
.select('name');
return builder.then((result) => {
expect(result).to.eql(expectedResult[0]);
expect(owner.nameOfOurRelation).to.eql(expectedResult[0]);
expect(result).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel"."rid", "name" from "RelatedModel" where "RelatedModel"."rid" in (2)',
);
});
});
it('should apply the modifier (object)', () => {
createModifiedRelation({ filterCol: 100 });
let expectedResult = [{ id: 1, a: 10, rid: 1 }];
mockKnexQueryResults = [expectedResult];
let owner = OwnerModel.fromJson({ id: 666, relatedId: 1 });
let builder = QueryBuilder.forClass(RelatedModel).findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.eql(expectedResult[0]);
expect(owner.nameOfOurRelation).to.eql(expectedResult[0]);
expect(result).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."rid" in (1) and "filterCol" = 100',
);
});
});
it('should apply the modifier (function)', () => {
createModifiedRelation((query) => {
query.where('name', 'Jennifer');
});
let owner = OwnerModel.fromJson({ id: 666, relatedId: 1 });
let expectedResult = [{ id: 1, a: 10, rid: 1 }];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel).findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.eql(expectedResult[0]);
expect(owner.nameOfOurRelation).to.eql(expectedResult[0]);
expect(result).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."rid" in (1) and "name" = \'Jennifer\'',
);
});
});
it('should support modifiers', () => {
createModifiedRelation('modifier');
let owner = OwnerModel.fromJson({ id: 666, relatedId: 1 });
let expectedResult = [{ id: 1, a: 10, rid: 1 }];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel).findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.eql(expectedResult[0]);
expect(owner.nameOfOurRelation).to.eql(expectedResult[0]);
expect(result).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."rid" in (1) and "filteredProperty" = true',
);
});
});
});
describe('insert', () => {
it('should generate an insert query', () => {
mockKnexQueryResults = [[1]];
let owner = OwnerModel.fromJson({ id: 666 });
let related = [RelatedModel.fromJson({ a: 'str1', rid: 2 })];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str1\', 2) returning "id"',
);
expect(executedQueries[1]).to.equal(
'update "OwnerModel" set "relatedId" = 2 where "OwnerModel"."id" in (666)',
);
expect(owner.nameOfOurRelation).to.equal(result[0]);
expect(owner.relatedId).to.equal(2);
expect(result).to.eql([{ a: 'str1', id: 1, rid: 2 }]);
expect(result[0]).to.be.a(RelatedModel);
});
});
it('should generate an insert query (composite key)', () => {
mockKnexQueryResults = [[{ aid: 11, bid: 22 }]];
let owner = OwnerModel.fromJson({ id: 666 });
let related = [RelatedModel.fromJson({ a: 'str1', aid: 11, bid: 22 })];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return compositeKeyRelation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "aid", "bid") values (\'str1\', 11, 22) returning "id"',
);
expect(executedQueries[1]).to.equal(
'update "OwnerModel" set "relatedAId" = 11, "relatedBId" = 22 where "OwnerModel"."id" in (666)',
);
expect(owner.relatedAId).to.equal(11);
expect(owner.relatedBId).to.equal(22);
expect(owner.nameOfOurRelation).to.equal(result[0]);
expect(result).to.eql([{ a: 'str1', aid: 11, bid: 22 }]);
expect(result[0]).to.be.a(RelatedModel);
});
});
it('should accept json object array', () => {
mockKnexQueryResults = [[5]];
let owner = OwnerModel.fromJson({ id: 666 });
let related = [{ a: 'str1', rid: 2 }];
return QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related)
.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str1\', 2) returning "id"',
);
expect(executedQueries[1]).to.equal(
'update "OwnerModel" set "relatedId" = 2 where "OwnerModel"."id" in (666)',
);
expect(owner.nameOfOurRelation).to.equal(result[0]);
expect(owner.relatedId).to.equal(2);
expect(result).to.eql([{ a: 'str1', id: 5, rid: 2 }]);
expect(result[0]).to.be.a(RelatedModel);
});
});
it('should accept single model', () => {
mockKnexQueryResults = [[1]];
let owner = OwnerModel.fromJson({ id: 666 });
let related = RelatedModel.fromJson({ a: 'str1', rid: 2 });
return QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related)
.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str1\', 2) returning "id"',
);
expect(executedQueries[1]).to.equal(
'update "OwnerModel" set "relatedId" = 2 where "OwnerModel"."id" in (666)',
);
expect(owner.nameOfOurRelation).to.equal(result);
expect(owner.relatedId).to.equal(2);
expect(result).to.eql({ a: 'str1', id: 1, rid: 2 });
expect(result).to.be.a(RelatedModel);
});
});
it('should accept single json object', () => {
mockKnexQueryResults = [[1]];
let owner = OwnerModel.fromJson({ id: 666 });
let related = { a: 'str1', rid: 2 };
return QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related)
.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str1\', 2) returning "id"',
);
expect(executedQueries[1]).to.equal(
'update "OwnerModel" set "relatedId" = 2 where "OwnerModel"."id" in (666)',
);
expect(owner.nameOfOurRelation).to.equal(result);
expect(owner.relatedId).to.equal(2);
expect(result).to.eql({ a: 'str1', id: 1, rid: 2 });
expect(result).to.be.a(RelatedModel);
});
});
it('should fail if trying to insert multiple', (done) => {
mockKnexQueryResults = [[1]];
let owner = OwnerModel.fromJson({ id: 666 });
let related = [
{ a: 'str1', rid: 2 },
{ a: 'str1', rid: 2 },
];
QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related)
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
});
describe('update', () => {
it('should generate an update query', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
let update = RelatedModel.fromJson({ a: 'str1' });
let builder = QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update);
return builder.then((numUpdates) => {
expect(numUpdates).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."rid" in (2)',
);
});
});
it('should generate an update query (composite key)', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ id: 666, relatedAId: 11, relatedBId: 22 });
let update = RelatedModel.fromJson({ a: 'str1', aid: 11, bid: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return compositeKeyRelation.update(builder, RelationOwner.create(owner));
})
.update(update);
return builder.then((numUpdates) => {
expect(numUpdates).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\', "aid" = 11, "bid" = 22 where ("RelatedModel"."aid", "RelatedModel"."bid") in ((11, 22))',
);
});
});
it('should accept json object', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
let update = { a: 'str1' };
return QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update)
.then((numUpdates) => {
expect(numUpdates).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."rid" in (2)',
);
});
});
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 'foo' });
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
let update = RelatedModel.fromJson({ a: 'str1' });
return QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update)
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."rid" in (2) and "someColumn" = \'foo\'',
);
});
});
});
describe('patch', () => {
it('should generate an patch query', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
let patch = RelatedModel.fromJson({ a: 'str1' });
let builder = QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(patch);
return builder.then((numUpdates) => {
expect(numUpdates).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."rid" in (2)',
);
});
});
it('should accept json object', () => {
mockKnexQueryResults = [42];
RelatedModel.jsonSchema = {
type: 'object',
required: ['b'],
properties: {
a: { type: 'string' },
b: { type: 'string' },
},
};
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
let patch = { a: 'str1' };
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(patch)
.then((numUpdates) => {
expect(numUpdates).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."rid" in (2)',
);
});
});
it('should work with increment', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ id: 666, relatedId: 1 });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.increment('test', 1)
.then((numUpdates) => {
expect(numUpdates).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "test" = "test" + 1 where "RelatedModel"."rid" in (1)',
);
});
});
it('should work with decrement', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.decrement('test', 10)
.then((numUpdates) => {
expect(numUpdates).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "test" = "test" - 10 where "RelatedModel"."rid" in (2)',
);
});
});
it('should apply the modifier', () => {
mockKnexQueryResults = [42];
createModifiedRelation({ someColumn: 'foo' });
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
let update = RelatedModel.fromJson({ a: 'str1' });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(update)
.then((numUpdates) => {
expect(numUpdates).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."rid" in (2) and "someColumn" = \'foo\'',
);
});
});
});
describe('delete', () => {
it('should generate a delete query', () => {
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
let builder = QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return relation.delete(builder, RelationOwner.create(owner));
})
.delete();
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.eql(
'delete from "RelatedModel" where "RelatedModel"."rid" in (2)',
);
});
});
it('should generate a delete query (composite key)', () => {
let owner = OwnerModel.fromJson({ id: 666, relatedAId: 11, relatedBId: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return compositeKeyRelation.delete(builder, RelationOwner.create(owner));
})
.delete();
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.eql(
'delete from "RelatedModel" where ("RelatedModel"."aid", "RelatedModel"."bid") in ((11, 22))',
);
});
});
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ id: 666, relatedId: 2 });
return QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return relation.delete(builder, RelationOwner.create(owner));
})
.delete()
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.eql(
'delete from "RelatedModel" where "RelatedModel"."rid" in (2) and "someColumn" = 100',
);
});
});
});
describe('relate', () => {
it('should generate a relate query', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate(10);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "OwnerModel" set "relatedId" = 10 where "OwnerModel"."id" in (666)',
);
});
});
it('should generate a relate query (array value)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate([10]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "OwnerModel" set "relatedId" = 10 where "OwnerModel"."id" in (666)',
);
});
});
it('should generate a relate query (object value)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate({ rid: 10 });
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "OwnerModel" set "relatedId" = 10 where "OwnerModel"."id" in (666)',
);
});
});
it('should generate a relate query (array of objects values)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate([{ rid: 10 }]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "OwnerModel" set "relatedId" = 10 where "OwnerModel"."id" in (666)',
);
});
});
it('should generate a relate query (composite key)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return compositeKeyRelation.relate(builder, RelationOwner.create(owner));
})
.relate([10, 20]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "OwnerModel" set "relatedAId" = 10, "relatedBId" = 20 where "OwnerModel"."id" in (666)',
);
});
});
it('should generate a relate query (composite key with object value)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return compositeKeyRelation.relate(builder, RelationOwner.create(owner));
})
.relate({ aid: 10, bid: 20 });
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "OwnerModel" set "relatedAId" = 10, "relatedBId" = 20 where "OwnerModel"."id" in (666)',
);
});
});
it('should accept one id', () => {
mockKnexQueryResults = [{ a: 1, b: 2 }];
let owner = OwnerModel.fromJson({ id: 666 });
return QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate(11)
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({ a: 1, b: 2 });
expect(executedQueries[0]).to.eql(
'update "OwnerModel" set "relatedId" = 11 where "OwnerModel"."id" in (666)',
);
});
});
it('should fail if trying to relate multiple', (done) => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate([11, 12])
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
it("should fail if object value doesn't contain the needed id", (done) => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate({ wrongId: 10 })
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
it("should fail if object value doesn't contain the needed id (composite key)", (done) => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ id: 666 });
QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return compositeKeyRelation.relate(builder, RelationOwner.create(owner));
})
.relate({ aid: 10, wrongId: 20 })
.then(() => {
done(new Error('should not get here'));
})
.catch(() => {
done();
});
});
});
describe('unrelate', () => {
it('should throw if a `through` object is given', () => {
expect(() => {
relation = new BelongsToOneRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: BelongsToOneRelation,
join: {
from: 'OwnerModel.relatedId',
through: {},
to: 'RelatedModel.rid',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.nameOfOurRelation: Property join.through is not supported for this relation type.',
);
});
});
});
function createModifiedRelation(modifier) {
relation = new BelongsToOneRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: BelongsToOneRelation,
modify: modifier,
join: {
from: 'OwnerModel.relatedId',
to: 'RelatedModel.rid',
},
});
}
});
================================================
FILE: tests/unit/relations/HasManyRelation.js
================================================
const _ = require('lodash'),
Knex = require('knex'),
expect = require('expect.js'),
Promise = require('bluebird'),
objection = require('../../../'),
knexMocker = require('../../../testUtils/mockKnex'),
RelationOwner = require('../../../lib/relations/RelationOwner').RelationOwner,
Model = objection.Model,
QueryBuilder = objection.QueryBuilder,
HasManyRelation = objection.HasManyRelation;
describe('HasManyRelation', () => {
let mockKnexQueryResults = [];
let executedQueries = [];
let mockKnex = null;
let OwnerModel = null;
let RelatedModel = null;
let relation;
let compositeKeyRelation;
before(() => {
let knex = Knex({ client: 'pg' });
mockKnex = knexMocker(knex, function (mock, oldImpl, args) {
executedQueries.push(this.toString());
let result = mockKnexQueryResults.shift() || [];
let promise = Promise.resolve(result);
return promise.then.apply(promise, args);
});
});
beforeEach(() => {
mockKnexQueryResults = [];
executedQueries = [];
OwnerModel = class OwnerModel extends Model {
static get tableName() {
return 'OwnerModel';
}
};
RelatedModel = class RelatedModel extends Model {
static get tableName() {
return 'RelatedModel';
}
static get modifiers() {
return {
modifier: (builder) => builder.where('filteredProperty', true),
};
}
};
OwnerModel.knex(mockKnex);
RelatedModel.knex(mockKnex);
});
beforeEach(() => {
relation = new HasManyRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: HasManyRelation,
join: {
from: 'OwnerModel.oid',
to: 'RelatedModel.ownerId',
},
});
compositeKeyRelation = new HasManyRelation('nameOfOurRelation', OwnerModel);
compositeKeyRelation.setMapping({
modelClass: RelatedModel,
relation: HasManyRelation,
join: {
from: ['OwnerModel.aid', 'OwnerModel.bid'],
to: ['RelatedModel.ownerAId', 'RelatedModel.ownerBId'],
},
});
});
describe('find', () => {
it('should generate a find query', () => {
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, ownerId: 666 },
{ a: 2, ownerId: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."ownerId" in (666) and "name" = \'Teppo\' or "age" > 60',
);
});
});
it('should generate a find query (composite key)', () => {
let owners = [
OwnerModel.fromJson({ aid: 11, bid: 22 }),
OwnerModel.fromJson({ aid: 11, bid: 33 }),
];
let expectedResult = [
{ a: 1, ownerAId: 11, ownerBId: 22 },
{ a: 2, ownerAId: 11, ownerBId: 22 },
{ a: 3, ownerAId: 11, ownerBId: 33 },
{ a: 4, ownerAId: 11, ownerBId: 33 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory((builder) => {
return compositeKeyRelation.find(builder, RelationOwner.create(owners));
});
return builder.then((result) => {
expect(result).to.have.length(4);
expect(result).to.eql(expectedResult);
expect(owners[0].nameOfOurRelation).to.eql([
{ a: 1, ownerAId: 11, ownerBId: 22 },
{ a: 2, ownerAId: 11, ownerBId: 22 },
]);
expect(owners[1].nameOfOurRelation).to.eql([
{ a: 3, ownerAId: 11, ownerBId: 33 },
{ a: 4, ownerAId: 11, ownerBId: 33 },
]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(result[2]).to.be.a(RelatedModel);
expect(result[3]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where ("RelatedModel"."ownerAId", "RelatedModel"."ownerBId") in ((11, 22), (11, 33)) and "name" = \'Teppo\' or "age" > 60',
);
});
});
it('should find for multiple owners', () => {
let owners = [OwnerModel.fromJson({ oid: 666 }), OwnerModel.fromJson({ oid: 667 })];
let expectedResult = [
{ a: 1, ownerId: 666 },
{ a: 2, ownerId: 666 },
{ a: 3, ownerId: 667 },
{ a: 4, ownerId: 667 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owners));
});
return builder.then((result) => {
expect(result).to.have.length(4);
expect(result).to.eql(expectedResult);
expect(owners[0].nameOfOurRelation).to.eql([
{ a: 1, ownerId: 666 },
{ a: 2, ownerId: 666 },
]);
expect(owners[1].nameOfOurRelation).to.eql([
{ a: 3, ownerId: 667 },
{ a: 4, ownerId: 667 },
]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(result[2]).to.be.a(RelatedModel);
expect(result[3]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."ownerId" in (666, 667) and "name" = \'Teppo\' or "age" > 60',
);
});
});
it('explicit selects should override the RelatedModel.*', () => {
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, ownerId: 666 },
{ a: 2, ownerId: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.select('name')
.findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel"."ownerId", "name" from "RelatedModel" where "RelatedModel"."ownerId" in (666) and "name" = \'Teppo\' or "age" > 60',
);
});
});
it('should apply a modifier object', () => {
createModifiedRelation({ someColumn: 'foo' });
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, ownerId: 666 },
{ a: 2, ownerId: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."ownerId" in (666) and "someColumn" = \'foo\' and "name" = \'Teppo\' or "age" > 60',
);
});
});
it('should apply a modifier function', () => {
createModifiedRelation((builder) => builder.where('someColumn', 'foo'));
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, ownerId: 666 },
{ a: 2, ownerId: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."ownerId" in (666) and "someColumn" = \'foo\' and "name" = \'Teppo\' or "age" > 60',
);
});
});
it('should support model modifiers', () => {
createModifiedRelation('modifier');
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, ownerId: 666 },
{ a: 2, ownerId: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
'select "RelatedModel".* from "RelatedModel" where "RelatedModel"."ownerId" in (666) and "filteredProperty" = true and "name" = \'Teppo\' or "age" > 60',
);
});
});
});
describe('insert', () => {
it('should generate an insert query', () => {
mockKnexQueryResults = [[1, 2]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = [RelatedModel.fromJson({ a: 'str1' }), RelatedModel.fromJson({ a: 'str2' })];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "ownerId") values (\'str1\', 666), (\'str2\', 666) returning "id"',
);
expect(owner.nameOfOurRelation).to.eql(result);
expect(result).to.eql([
{ a: 'str1', id: 1, ownerId: 666 },
{ a: 'str2', id: 2, ownerId: 666 },
]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
});
});
it('should generate an insert query (composite key)', () => {
mockKnexQueryResults = [[1, 2]];
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let related = [RelatedModel.fromJson({ a: 'str1' }), RelatedModel.fromJson({ a: 'str2' })];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return compositeKeyRelation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "ownerAId", "ownerBId") values (\'str1\', 11, 22), (\'str2\', 11, 22) returning "id"',
);
expect(owner.nameOfOurRelation).to.eql(result);
expect(result).to.eql([
{ a: 'str1', id: 1, ownerAId: 11, ownerBId: 22 },
{ a: 'str2', id: 2, ownerAId: 11, ownerBId: 22 },
]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
});
});
it('should accept json object array', () => {
mockKnexQueryResults = [[1, 2]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = [{ a: 'str1' }, { a: 'str2' }];
return QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related)
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "ownerId") values (\'str1\', 666), (\'str2\', 666) returning "id"',
);
expect(result).to.eql([
{ a: 'str1', id: 1, ownerId: 666 },
{ a: 'str2', id: 2, ownerId: 666 },
]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
});
});
it('should accept single model', () => {
mockKnexQueryResults = [[1]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = RelatedModel.fromJson({ a: 'str1' });
return QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related)
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "ownerId") values (\'str1\', 666) returning "id"',
);
expect(result).to.eql({ a: 'str1', id: 1, ownerId: 666 });
expect(result).to.be.a(RelatedModel);
});
});
it('should accept single json object', () => {
mockKnexQueryResults = [[1]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = { a: 'str1' };
return QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related)
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "ownerId") values (\'str1\', 666) returning "id"',
);
expect(result).to.eql({ a: 'str1', id: 1, ownerId: 666 });
expect(result).to.be.a(RelatedModel);
});
});
});
describe('update', () => {
it('should generate an update query', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ oid: 666 });
let update = RelatedModel.fromJson({ a: 'str1' });
let builder = QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."ownerId" in (666) and "gender" = \'male\' and "thingy" is not null',
);
});
});
it('should generate an update query (composite key)', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let update = RelatedModel.fromJson({ a: 'str1' });
let builder = QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return compositeKeyRelation.update(builder, RelationOwner.create(owner));
})
.update(update)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where ("RelatedModel"."ownerAId", "RelatedModel"."ownerBId") in ((11, 22)) and "gender" = \'male\' and "thingy" is not null',
);
});
});
it('should accept json object', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ oid: 666 });
let update = { a: 'str1' };
return QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."ownerId" in (666) and "gender" = \'male\' and "thingy" is not null',
);
});
});
it('should apply the modifier', () => {
mockKnexQueryResults = [42];
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
let update = RelatedModel.fromJson({ a: 'str1' });
return QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."ownerId" in (666) and "someColumn" = 100 and "gender" = \'male\' and "thingy" is not null',
);
});
});
});
describe('patch', () => {
it('should generate a patch query', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ oid: 666 });
let patch = RelatedModel.fromJson({ a: 'str1' });
let builder = QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(patch)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."ownerId" in (666) and "gender" = \'male\' and "thingy" is not null',
);
});
});
it('should accept json object', () => {
mockKnexQueryResults = [42];
RelatedModel.jsonSchema = {
type: 'object',
required: ['b'],
properties: {
a: { type: 'string' },
b: { type: 'string' },
},
};
let owner = OwnerModel.fromJson({ oid: 666 });
let patch = { a: 'str1' };
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(patch)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."ownerId" in (666) and "gender" = \'male\' and "thingy" is not null',
);
});
});
it('should work with increment', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.increment('test', 1)
.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "test" = "test" + 1 where "RelatedModel"."ownerId" in (666)',
);
});
});
it('should work with decrement', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.decrement('test', 10)
.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "test" = "test" - 10 where "RelatedModel"."ownerId" in (666)',
);
});
});
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
let patch = RelatedModel.fromJson({ a: 'str1' });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(patch)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "a" = \'str1\' where "RelatedModel"."ownerId" in (666) and "someColumn" = 100 and "gender" = \'male\' and "thingy" is not null',
);
});
});
});
describe('delete', () => {
it('should generate a delete query', () => {
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return relation.delete(builder, RelationOwner.create(owner));
})
.delete()
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'delete from "RelatedModel" where "RelatedModel"."ownerId" in (666) and "gender" = \'male\' and "thingy" is not null',
);
});
});
it('should generate a delete query (composite key)', () => {
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return compositeKeyRelation.delete(builder, RelationOwner.create(owner));
})
.delete()
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'delete from "RelatedModel" where ("RelatedModel"."ownerAId", "RelatedModel"."ownerBId") in ((11, 22)) and "gender" = \'male\' and "thingy" is not null',
);
});
});
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return relation.delete(builder, RelationOwner.create(owner));
})
.delete()
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.eql(
'delete from "RelatedModel" where "RelatedModel"."ownerId" in (666) and "someColumn" = 100 and "gender" = \'male\' and "thingy" is not null',
);
});
});
});
describe('relate', () => {
it('should generate a relate query', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate(10);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerId" = 666 where "RelatedModel"."id" in (10)',
);
});
});
it('should generate a relate query (multiple ids)', () => {
mockKnexQueryResults = [[5, 6, 7]];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate([10, 20, 30]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql([5, 6, 7]);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerId" = 666 where "RelatedModel"."id" in (10, 20, 30)',
);
});
});
it('should generate a relate query (object value)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate({ id: 10 });
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerId" = 666 where "RelatedModel"."id" in (10)',
);
});
});
it('should generate a relate query (array of object values)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate([{ id: 10 }, { id: 20 }]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerId" = 666 where "RelatedModel"."id" in (10, 20)',
);
});
});
it('should generate a relate query (composite key)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return compositeKeyRelation.relate(builder, RelationOwner.create(owner));
})
.relate([1, 2, 3]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerAId" = 11, "ownerBId" = 22 where "RelatedModel"."id" in (1, 2, 3)',
);
});
});
it('should accept one id', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate(11)
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerId" = 666 where "RelatedModel"."id" in (11)',
);
});
});
});
describe('unrelate', () => {
it('should generate a unrelate query', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.unrelateOperationFactory((builder) => {
return relation.unrelate(builder, RelationOwner.create(owner));
})
.unrelate()
.whereIn('code', [55, 66, 77]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerId" = NULL where "code" in (55, 66, 77) and "RelatedModel"."ownerId" in (666)',
);
});
});
it('should generate a unrelate query (composite key)', () => {
mockKnexQueryResults = [123];
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.unrelateOperationFactory((builder) => {
return compositeKeyRelation.unrelate(builder, RelationOwner.create(owner));
})
.unrelate()
.whereIn('code', [55, 66, 77]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerAId" = NULL, "ownerBId" = NULL where "code" in (55, 66, 77) and ("RelatedModel"."ownerAId", "RelatedModel"."ownerBId") in ((11, 22))',
);
});
});
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.unrelateOperationFactory((builder) => {
return relation.unrelate(builder, RelationOwner.create(owner));
})
.unrelate()
.whereIn('code', [55, 66, 77])
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.eql(
'update "RelatedModel" set "ownerId" = NULL where "code" in (55, 66, 77) and "RelatedModel"."ownerId" in (666) and "someColumn" = 100',
);
});
});
it('should throw is a `through` object is given', () => {
expect(() => {
relation = new HasManyRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: HasManyRelation,
join: {
from: 'OwnerModel.oid',
through: {},
to: 'RelatedModel.ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.nameOfOurRelation: Property join.through is not supported for this relation type.',
);
});
});
});
function createModifiedRelation(filter) {
relation = new HasManyRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: HasManyRelation,
// Use filter here on purpose instead of `modify`. Other relation tests test the `modify`
// here we test the `filter` alias.
filter: filter,
join: {
from: 'OwnerModel.oid',
to: 'RelatedModel.ownerId',
},
});
}
});
================================================
FILE: tests/unit/relations/ManyToManyRelation.js
================================================
const _ = require('lodash');
const Knex = require('knex');
const expect = require('expect.js');
const Promise = require('bluebird');
const objection = require('../../../');
const { isFunction } = require('../../../lib/utils/objectUtils');
const knexMocker = require('../../../testUtils/mockKnex');
const RelationOwner = require('../../../lib/relations/RelationOwner').RelationOwner;
const Model = objection.Model;
const QueryBuilder = objection.QueryBuilder;
const ManyToManyRelation = objection.ManyToManyRelation;
describe('ManyToManyRelation', () => {
let mockKnexQueryResults = [];
let executedQueries = [];
let mockKnex = null;
let OwnerModel = null;
let RelatedModel = null;
let JoinModel = null;
let relation;
let compositeKeyRelation;
beforeEach(() => {
let knex = Knex({ client: 'pg' });
mockKnex = knexMocker(knex, function (mock, oldImpl, args) {
executedQueries.push(this.toString());
let result = mockKnexQueryResults.shift() || [];
let promise = Promise.resolve(result);
return promise.then.apply(promise, args);
});
});
beforeEach(() => {
mockKnexQueryResults = [];
executedQueries = [];
OwnerModel = class OwnerModel extends Model {
static get tableName() {
return 'OwnerModel';
}
};
RelatedModel = class RelatedModel extends Model {
static get tableName() {
return 'RelatedModel';
}
static get modifiers() {
return {
modifier: (builder) => builder.where('filteredProperty', true),
};
}
};
JoinModel = class JoinModel extends Model {
static get tableName() {
return 'JoinModel';
}
};
OwnerModel.knex(mockKnex);
RelatedModel.knex(mockKnex);
JoinModel.knex(mockKnex);
});
beforeEach(() => {
relation = new ManyToManyRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: ManyToManyRelation,
join: {
from: 'OwnerModel.oid',
through: {
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
extra: ['extra1', 'extra2'],
},
to: 'RelatedModel.rid',
},
});
compositeKeyRelation = new ManyToManyRelation('nameOfOurRelation', OwnerModel);
compositeKeyRelation.setMapping({
modelClass: RelatedModel,
relation: ManyToManyRelation,
join: {
from: ['OwnerModel.aid', 'OwnerModel.bid'],
through: {
from: ['JoinModel.ownerAId', 'JoinModel.ownerBId'],
to: ['JoinModel.relatedCId', 'JoinModel.relatedDId'],
},
to: ['RelatedModel.cid', 'RelatedModel.did'],
},
});
});
it('should accept a join table in join.through object', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
through: {
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
},
to: 'RelatedModel.ownerId',
},
});
expect(relation.joinTable).to.equal('JoinModel');
expect(relation.joinTableOwnerProp.cols).to.eql(['ownerId']);
expect(relation.joinTableRelatedProp.cols).to.eql(['relatedId']);
});
it('should accept a join model in join.through object', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
through: {
modelClass: JoinModel,
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
},
to: 'RelatedModel.ownerId',
},
});
expect(relation.joinTable).to.equal('JoinModel');
expect(relation.joinTableOwnerProp.cols).to.eql(['ownerId']);
expect(relation.joinTableRelatedProp.props).to.eql(['relatedId']);
expect(isSubclassOf(relation.joinModelClass, JoinModel)).to.equal(true);
});
it('should accept an absolute file path to a join model in join.through object', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
through: {
modelClass: __dirname + '/files/JoinModel',
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
},
to: 'RelatedModel.ownerId',
},
});
expect(relation.joinTable).to.equal('JoinModel');
expect(relation.joinTableOwnerProp.cols).to.eql(['ownerId']);
expect(relation.joinTableRelatedProp.cols).to.eql(['relatedId']);
expect(isSubclassOf(relation.joinModelClass, require('./files/JoinModel'))).to.equal(true);
});
it('should accept a composite keys in join.through object (1)', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: ['OwnerModel.name', 'OwnerModel.dateOfBirth'],
through: {
from: ['JoinModel.ownerName', 'JoinModel.ownerDateOfBirth'],
to: 'JoinModel.relatedId',
},
to: 'RelatedModel.ownerId',
},
});
expect(relation.joinTable).to.equal('JoinModel');
expect(relation.joinTableOwnerProp.cols).to.eql(['ownerName', 'ownerDateOfBirth']);
expect(relation.joinTableRelatedProp.cols).to.eql(['relatedId']);
});
it('should accept a composite keys in join.through object (2)', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: ['OwnerModel.name', 'OwnerModel.dateOfBirth'],
through: {
from: ['JoinModel.ownerName', 'JoinModel.ownerDateOfBirth'],
to: ['JoinModel.relatedA', 'JoinModel.relatedB'],
},
to: ['RelatedModel.A', 'RelatedModel.B'],
},
});
expect(relation.joinTable).to.equal('JoinModel');
expect(relation.joinTableOwnerProp.cols).to.eql(['ownerName', 'ownerDateOfBirth']);
expect(relation.joinTableRelatedProp.cols).to.eql(['relatedA', 'relatedB']);
});
it('should accept an array in through.extra', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
through: {
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
extra: ['extra1', 'extra2'],
},
to: 'RelatedModel.ownerId',
},
});
expect(relation.joinTableExtras[0].joinTableCol).to.equal('extra1');
expect(relation.joinTableExtras[1].joinTableCol).to.equal('extra2');
});
it('should accept a string in through.extra', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
through: {
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
extra: 'extra1',
},
to: 'RelatedModel.ownerId',
},
});
expect(relation.joinTableExtras[0].joinTableCol).to.equal('extra1');
});
it('should accept an object in through.extra', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
through: {
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
extra: {
extra1: 'extraColumn',
},
},
to: 'RelatedModel.ownerId',
},
});
expect(relation.joinTableExtras[0].joinTableCol).to.equal('extraColumn');
expect(relation.joinTableExtras[0].joinTableProp).to.equal('extraColumn');
expect(relation.joinTableExtras[0].aliasCol).to.equal('extra1');
});
it('should fail if join.through.modelClass is not a subclass of Model', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
through: {
modelClass: function () {},
from: 'JoinModel.relatedId',
to: 'JoinModel.ownerId',
},
to: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: join.through.modelClass: 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.',
);
});
});
it('should fail if join.through.modelClass is an invalid path', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
through: {
modelClass: '/not/a/path/to/a/model',
from: 'JoinModel.relatedId',
to: 'JoinModel.ownerId',
},
to: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
expect(err.message).to.contain(
"OwnerModel.relationMappings.testRelation: Cannot find module '/not/a/path/to/a/model'",
);
});
});
it('should fail if join.through.to is missing', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
through: {
from: 'JoinModel.relatedId',
},
to: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
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"}',
);
});
});
it('should fail if join.through.from is missing', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
through: {
to: 'JoinModel.ownerId',
},
to: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
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"}',
);
});
});
it('join.through.from should have format joinTable.columnName', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
through: {
from: 'relatedId',
to: 'JoinModel.ownerId',
},
to: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
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"].',
);
});
});
it('join.through.to should have format JoinModel.columnName', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
through: {
from: 'JoinModel.relatedId',
to: 'ownerId',
},
to: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
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"].',
);
});
});
it('join.through `to` and `from` should point to the same table', () => {
let relation = new ManyToManyRelation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
through: {
from: 'JoinModel.relatedId',
to: 'OtherTable.ownerId',
},
to: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
expect(err.message).to.contain(
'OwnerModel.relationMappings.testRelation: join.through `from` and `to` must point to the same join table.',
);
});
});
it('should accept a modifier in join.through', () => {
// modifier defined in join.through.modify
let modifier = (builder) => builder.where('someColumn', 'foo');
createJoinThroughModifiedRelation(modifier);
expect(relation.joinTableModify).to.be.a(Function);
// test also join.through.filter
let relationFilter = new ManyToManyRelation('testRelation', OwnerModel);
relationFilter.setMapping({
relation: ManyToManyRelation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
through: {
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
filter: modifier,
},
to: 'RelatedModel.ownerId',
},
});
expect(relationFilter.joinTableModify).to.be.a(Function);
});
describe('find', () => {
it('should generate a find query', () => {
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, objectiontmpjoin0: 666 },
{ a: 2, objectiontmpjoin0: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory((builder) => {
return relation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
[
'select "RelatedModel".*, "JoinModel"."extra1" as "extra1", "JoinModel"."extra2" as "extra2", "JoinModel"."ownerId" as "objectiontmpjoin0"',
'from "RelatedModel"',
'inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId"',
'where "JoinModel"."ownerId" in (666)',
'and "name" = \'Teppo\'',
'or "age" > 60',
].join(' '),
);
});
});
it('should generate a find query (composite key)', () => {
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let expectedResult = [
{ a: 1, objectiontmpjoin0: 11, objectiontmpjoin1: 22 },
{ a: 2, objectiontmpjoin0: 11, objectiontmpjoin1: 22 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory((builder) => {
return compositeKeyRelation.find(builder, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
[
'select "RelatedModel".*, "JoinModel"."ownerAId" as "objectiontmpjoin0", "JoinModel"."ownerBId" as "objectiontmpjoin1"',
'from "RelatedModel"',
'inner join "JoinModel" on "RelatedModel"."cid" = "JoinModel"."relatedCId" and "RelatedModel"."did" = "JoinModel"."relatedDId"',
'where ("JoinModel"."ownerAId", "JoinModel"."ownerBId") in ((11, 22))',
'and "name" = \'Teppo\'',
'or "age" > 60',
].join(' '),
);
});
});
it('should find for multiple owners', () => {
let owners = [OwnerModel.fromJson({ oid: 666 }), OwnerModel.fromJson({ oid: 667 })];
let expectedResult = [
{ a: 1, objectiontmpjoin0: 666 },
{ a: 2, objectiontmpjoin0: 666 },
{ a: 3, objectiontmpjoin0: 667 },
{ a: 4, objectiontmpjoin0: 667 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory(function () {
return relation.find(this, RelationOwner.create(owners));
});
return builder.then((result) => {
expect(result).to.have.length(4);
expect(result).to.eql(expectedResult);
expect(owners[0].nameOfOurRelation).to.eql([{ a: 1 }, { a: 2 }]);
expect(owners[1].nameOfOurRelation).to.eql([{ a: 3 }, { a: 4 }]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(result[2]).to.be.a(RelatedModel);
expect(result[3]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
[
'select "RelatedModel".*, "JoinModel"."extra1" as "extra1", "JoinModel"."extra2" as "extra2", "JoinModel"."ownerId" as "objectiontmpjoin0"',
'from "RelatedModel"',
'inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId"',
'where "JoinModel"."ownerId" in (666, 667)',
'and "name" = \'Teppo\'',
'or "age" > 60',
].join(' '),
);
});
});
it('should find for multiple owners (composite key)', () => {
let owners = [
OwnerModel.fromJson({ aid: 11, bid: 22 }),
OwnerModel.fromJson({ aid: 11, bid: 33 }),
];
let expectedResult = [
{ a: 1, objectiontmpjoin0: 11, objectiontmpjoin1: 22 },
{ a: 2, objectiontmpjoin0: 11, objectiontmpjoin1: 22 },
{ a: 3, objectiontmpjoin0: 11, objectiontmpjoin1: 33 },
{ a: 4, objectiontmpjoin0: 11, objectiontmpjoin1: 33 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory(function () {
return compositeKeyRelation.find(this, RelationOwner.create(owners));
});
return builder.then((result) => {
expect(result).to.have.length(4);
expect(result).to.eql(expectedResult);
expect(owners[0].nameOfOurRelation).to.eql([{ a: 1 }, { a: 2 }]);
expect(owners[1].nameOfOurRelation).to.eql([{ a: 3 }, { a: 4 }]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(result[2]).to.be.a(RelatedModel);
expect(result[3]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
[
'select "RelatedModel".*, "JoinModel"."ownerAId" as "objectiontmpjoin0", "JoinModel"."ownerBId" as "objectiontmpjoin1"',
'from "RelatedModel"',
'inner join "JoinModel" on "RelatedModel"."cid" = "JoinModel"."relatedCId" and "RelatedModel"."did" = "JoinModel"."relatedDId"',
'where ("JoinModel"."ownerAId", "JoinModel"."ownerBId") in ((11, 22), (11, 33))',
'and "name" = \'Teppo\'',
'or "age" > 60',
].join(' '),
);
});
});
it('explicit selects should override the RelatedModel.*', () => {
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, objectiontmpjoin0: 666 },
{ a: 2, objectiontmpjoin0: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.select('name')
.findOperationFactory(function () {
return relation.find(this, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
[
'select "JoinModel"."ownerId" as "objectiontmpjoin0", "RelatedModel"."rid", "name"',
'from "RelatedModel"',
'inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId"',
'where "JoinModel"."ownerId" in (666)',
'and "name" = \'Teppo\'',
'or "age" > 60',
].join(' '),
);
});
});
// TODO expectedResult array is changed in-place and the items in it are replaced with model instances. SHOULD FIX THAT!
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, objectiontmpjoin0: 666 },
{ a: 2, objectiontmpjoin0: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory(function () {
return relation.find(this, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
[
'select "RelatedModel".*, "JoinModel"."ownerId" as "objectiontmpjoin0"',
'from "RelatedModel"',
'inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId"',
'where "JoinModel"."ownerId" in (666)',
'and "someColumn" = 100',
'and "name" = \'Teppo\'',
'or "age" > 60',
].join(' '),
);
});
});
// TODO expectedResult array is changed in-place and the items in it are replaced with model instances. SHOULD FIX THAT!
it('should support modifiers', () => {
createModifiedRelation('modifier');
let owner = OwnerModel.fromJson({ oid: 666 });
let expectedResult = [
{ a: 1, objectiontmpjoin0: 666 },
{ a: 2, objectiontmpjoin0: 666 },
];
mockKnexQueryResults = [expectedResult];
let builder = QueryBuilder.forClass(RelatedModel)
.where('name', 'Teppo')
.orWhere('age', '>', 60)
.findOperationFactory(function () {
return relation.find(this, RelationOwner.create(owner));
});
return builder.then((result) => {
expect(result).to.have.length(2);
expect(result).to.eql(expectedResult);
expect(owner.nameOfOurRelation).to.eql(expectedResult);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
[
'select "RelatedModel".*, "JoinModel"."ownerId" as "objectiontmpjoin0"',
'from "RelatedModel"',
'inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId"',
'where "JoinModel"."ownerId" in (666)',
'and "filteredProperty" = true',
'and "name" = \'Teppo\'',
'or "age" > 60',
].join(' '),
);
});
});
it('should support modifiers in join.through', () => {
createJoinThroughModifiedRelation((builder) => builder.where('someColumn', 'foo'));
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel).findOperationFactory(function () {
return relation.find(this, RelationOwner.create(owner));
});
return builder.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(
[
'select "RelatedModel".*, "JoinModel"."ownerId" as "objectiontmpjoin0"',
'from "RelatedModel"',
'inner join (select "JoinModel".* from "JoinModel" where "someColumn" = \'foo\') as "JoinModel"',
'on "RelatedModel"."rid" = "JoinModel"."relatedId"',
'where "JoinModel"."ownerId" in (666)',
].join(' '),
);
});
});
});
describe('insert', () => {
it('should generate an insert query', () => {
mockKnexQueryResults = [[1, 2]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = [
RelatedModel.fromJson({ a: 'str1', rid: 3 }),
RelatedModel.fromJson({ a: 'str2', rid: 4 }),
];
owner.nameOfOurRelation = [RelatedModel.fromJson({ a: 'str0', id: 3 })];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str1\', 3), (\'str2\', 4) returning "id"',
);
expect(executedQueries[1]).to.equal(
'insert into "JoinModel" ("ownerId", "relatedId") values (666, 3), (666, 4) returning "relatedId"',
);
expect(_.sortBy(owner.nameOfOurRelation, 'id')).to.eql([
{ a: 'str1', id: 1, rid: 3 },
{ a: 'str2', id: 2, rid: 4 },
{ a: 'str0', id: 3 },
]);
expect(result).to.eql([
{ a: 'str1', id: 1, rid: 3 },
{ a: 'str2', id: 2, rid: 4 },
]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
});
});
it('should generate an insert query (composite key)', () => {
mockKnexQueryResults = [
[
{ id: 1, cid: 33, did: 44 },
{ id: 2, cid: 33, did: 55 },
],
];
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let related = [
RelatedModel.fromJson({ a: 'str1', cid: 33, did: 44 }),
RelatedModel.fromJson({ a: 'str2', cid: 33, did: 55 }),
];
owner.nameOfOurRelation = [RelatedModel.fromJson({ a: 'str0', id: 3 })];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return compositeKeyRelation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "cid", "did") values (\'str1\', 33, 44), (\'str2\', 33, 55) returning "id"',
);
expect(executedQueries[1]).to.equal(
'insert into "JoinModel" ("ownerAId", "ownerBId", "relatedCId", "relatedDId") values (11, 22, 33, 44), (11, 22, 33, 55) returning "relatedCId", "relatedDId"',
);
expect(_.sortBy(owner.nameOfOurRelation, 'id')).to.eql([
{ a: 'str1', id: 1, cid: 33, did: 44 },
{ a: 'str2', id: 2, cid: 33, did: 55 },
{ a: 'str0', id: 3 },
]);
expect(result).to.eql([
{ a: 'str1', id: 1, cid: 33, did: 44 },
{ a: 'str2', id: 2, cid: 33, did: 55 },
]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
});
});
it('should accept json object array', () => {
mockKnexQueryResults = [[1, 2]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = [
{ a: 'str1', rid: 3 },
{ a: 'str2', rid: 4 },
];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str1\', 3), (\'str2\', 4) returning "id"',
);
expect(executedQueries[1]).to.equal(
'insert into "JoinModel" ("ownerId", "relatedId") values (666, 3), (666, 4) returning "relatedId"',
);
expect(owner.nameOfOurRelation).to.eql([
{ a: 'str1', id: 1, rid: 3 },
{ a: 'str2', id: 2, rid: 4 },
]);
expect(result).to.eql([
{ a: 'str1', id: 1, rid: 3 },
{ a: 'str2', id: 2, rid: 4 },
]);
expect(result[0]).to.be.a(RelatedModel);
expect(result[1]).to.be.a(RelatedModel);
});
});
it('should accept single model', () => {
mockKnexQueryResults = [[1]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = RelatedModel.fromJson({ a: 'str1', rid: 2 });
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str1\', 2) returning "id"',
);
expect(executedQueries[1]).to.equal(
'insert into "JoinModel" ("ownerId", "relatedId") values (666, 2) returning "relatedId"',
);
expect(result).to.eql({ a: 'str1', id: 1, rid: 2 });
expect(result).to.be.a(RelatedModel);
});
});
it('should accept single json object', () => {
mockKnexQueryResults = [[1]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = { a: 'str1', rid: 2 };
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str1\', 2) returning "id"',
);
expect(executedQueries[1]).to.equal(
'insert into "JoinModel" ("ownerId", "relatedId") values (666, 2) returning "relatedId"',
);
expect(result).to.eql({ a: 'str1', id: 1, rid: 2 });
expect(result).to.be.a(RelatedModel);
});
});
it('should insert extra properties to join table', () => {
mockKnexQueryResults = [[1, 2]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = RelatedModel.fromJson({
a: 'str2',
rid: 4,
extra1: 'extraVal1',
extra2: 'extraVal2',
});
owner.nameOfOurRelation = [RelatedModel.fromJson({ a: 'str0', id: 3 })];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str2\', 4) returning "id"',
);
expect(executedQueries[1]).to.equal(
'insert into "JoinModel" ("extra1", "extra2", "ownerId", "relatedId") values (\'extraVal1\', \'extraVal2\', 666, 4) returning "relatedId"',
);
expect(_.sortBy(_.invokeMap(owner.nameOfOurRelation, 'toJSON'), 'id')).to.eql([
{ a: 'str2', id: 1, rid: 4, extra1: 'extraVal1', extra2: 'extraVal2' },
{ a: 'str0', id: 3 },
]);
expect(result).to.be.a(RelatedModel);
expect(result.toJSON()).to.eql({
a: 'str2',
id: 1,
rid: 4,
extra1: 'extraVal1',
extra2: 'extraVal2',
});
});
});
it('should insert extra properties to join table (not all)', () => {
mockKnexQueryResults = [[1, 2]];
let owner = OwnerModel.fromJson({ oid: 666 });
let related = RelatedModel.fromJson({ a: 'str2', rid: 4, extra2: 'extraVal2' });
owner.nameOfOurRelation = [RelatedModel.fromJson({ a: 'str0', id: 3 })];
let builder = QueryBuilder.forClass(RelatedModel)
.insertOperationFactory((builder) => {
return relation.insert(builder, RelationOwner.create(owner));
})
.insert(related);
let toString = builder.toKnexQuery().toString();
let toSql = builder.toKnexQuery().toString();
return builder.then((result) => {
expect(executedQueries).to.have.length(2);
expect(executedQueries[0]).to.equal(toString);
expect(executedQueries[0]).to.equal(toSql);
expect(executedQueries[0]).to.equal(
'insert into "RelatedModel" ("a", "rid") values (\'str2\', 4) returning "id"',
);
expect(executedQueries[1]).to.equal(
'insert into "JoinModel" ("extra2", "ownerId", "relatedId") values (\'extraVal2\', 666, 4) returning "relatedId"',
);
expect(_.sortBy(_.invokeMap(owner.nameOfOurRelation, 'toJSON'), 'id')).to.eql([
{ a: 'str2', id: 1, rid: 4, extra2: 'extraVal2' },
{ a: 'str0', id: 3 },
]);
expect(result).to.be.a(RelatedModel);
expect(result.toJSON()).to.eql({ a: 'str2', id: 1, rid: 4, extra2: 'extraVal2' });
});
});
});
describe('update', () => {
it('should generate an update query', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ oid: 666 });
let update = RelatedModel.fromJson({ a: 'str1' });
let builder = QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "a" = 'str1' where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "gender" = 'male' and "thingy" is not null)`,
);
});
});
it('should generate an update query (composite key)', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let update = RelatedModel.fromJson({ a: 'str1' });
let builder = QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return compositeKeyRelation.update(builder, RelationOwner.create(owner));
})
.update(update)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "a" = 'str1' where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."cid" = "JoinModel"."relatedCId" and "RelatedModel"."did" = "JoinModel"."relatedDId" where ("JoinModel"."ownerAId", "JoinModel"."ownerBId") in ((11, 22)) and "gender" = 'male' and "thingy" is not null)`,
);
});
});
it('should accept json object', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ oid: 666 });
let update = { a: 'str1' };
return QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "a" = 'str1' where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "gender" = 'male' and "thingy" is not null)`,
);
});
});
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
let update = RelatedModel.fromJson({ a: 'str1' });
return QueryBuilder.forClass(RelatedModel)
.updateOperationFactory((builder) => {
return relation.update(builder, RelationOwner.create(owner));
})
.update(update)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "a" = 'str1' where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "someColumn" = 100 and "gender" = 'male' and "thingy" is not null)`,
);
});
});
});
describe('patch', () => {
it('should generate a patch query', () => {
mockKnexQueryResults = [42];
let owner = OwnerModel.fromJson({ oid: 666 });
let patch = RelatedModel.fromJson({ a: 'str1' });
let builder = QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(patch)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "a" = 'str1' where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "gender" = 'male' and "thingy" is not null)`,
);
});
});
it('should accept json object', () => {
mockKnexQueryResults = [42];
RelatedModel.jsonSchema = {
type: 'object',
required: ['b'],
properties: {
id: { type: 'number' },
a: { type: 'string' },
b: { type: 'string' },
},
};
let owner = OwnerModel.fromJson({ oid: 666 });
let patch = { a: 'str1' };
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(patch)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then((numUpdated) => {
expect(numUpdated).to.equal(42);
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "a" = 'str1' where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "gender" = 'male' and "thingy" is not null)`,
);
});
});
it('should work with increment', () => {
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.increment('test', 1)
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "test" = "test" + 1 where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666))`,
);
});
});
it('should work with decrement', () => {
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.decrement('test', 10)
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "test" = "test" - 10 where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666))`,
);
});
});
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
let patch = RelatedModel.fromJson({ a: 'str1' });
return QueryBuilder.forClass(RelatedModel)
.patchOperationFactory((builder) => {
return relation.patch(builder, RelationOwner.create(owner));
})
.patch(patch)
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then(() => {
expect(executedQueries).to.have.length(1);
expect(executedQueries[0]).to.eql(
`update "RelatedModel" set "a" = 'str1' where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "someColumn" = 100 and "gender" = 'male' and "thingy" is not null)`,
);
});
});
});
describe('delete', () => {
it('should generate a delete query', () => {
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return relation.delete(builder, RelationOwner.create(owner));
})
.delete()
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
`delete from "RelatedModel" where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "gender" = 'male' and "thingy" is not null)`,
);
});
});
it('should generate a delete query (composite key)', () => {
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return compositeKeyRelation.delete(builder, RelationOwner.create(owner));
})
.delete()
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored');
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
`delete from "RelatedModel" where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."cid" = "JoinModel"."relatedCId" and "RelatedModel"."did" = "JoinModel"."relatedDId" where ("JoinModel"."ownerAId", "JoinModel"."ownerBId") in ((11, 22)) and "gender" = 'male' and "thingy" is not null)`,
);
});
});
it('should apply the modifier', () => {
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.deleteOperationFactory((builder) => {
return relation.delete(builder, RelationOwner.create(owner));
})
.delete()
.where('gender', 'male')
.whereNotNull('thingy')
.select('shouldBeIgnored')
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.eql(
`delete from "RelatedModel" where "RelatedModel"."id" in (select "RelatedModel"."id" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "someColumn" = 100 and "gender" = 'male' and "thingy" is not null)`,
);
});
});
});
describe('relate', () => {
it('should generate a relate query', () => {
mockKnexQueryResults = [[5]];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate(10);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
[
'insert into "JoinModel" ("ownerId", "relatedId") values (666, 10) returning "relatedId"',
].join(' '),
);
});
});
it('should generate a relate query (array value)', () => {
mockKnexQueryResults = [[5, 6, 7]];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate([10, 20, 30]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(3);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
[
'insert into "JoinModel" ("ownerId", "relatedId") values (666, 10), (666, 20), (666, 30) returning "relatedId"',
].join(' '),
);
});
});
it('should generate a relate query (object value)', () => {
mockKnexQueryResults = [[5, 6, 7]];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate([{ rid: 10 }, { rid: 20 }, { rid: 30 }]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(3);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
[
'insert into "JoinModel" ("ownerId", "relatedId") values (666, 10), (666, 20), (666, 30) returning "relatedId"',
].join(' '),
);
});
});
it('should generate a relate query (composite key)', () => {
mockKnexQueryResults = [
[
{ relatedCId: 33, relatedDId: 44 },
{ relatedCId: 33, relatedDId: 55 },
{ relatedCId: 66, relatedDId: 77 },
],
];
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return compositeKeyRelation.relate(builder, RelationOwner.create(owner));
})
.relate([
[33, 44],
[33, 55],
[66, 77],
]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(3);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
[
'insert into "JoinModel" ("ownerAId", "ownerBId", "relatedCId", "relatedDId") values (11, 22, 33, 44), (11, 22, 33, 55), (11, 22, 66, 77) returning "relatedCId", "relatedDId"',
].join(' '),
);
});
});
it('should generate a relate query (composite key with object value)', () => {
mockKnexQueryResults = [
[
{ relatedCId: 33, relatedDId: 44 },
{ relatedCId: 33, relatedDId: 55 },
{ relatedCId: 66, relatedDId: 77 },
],
];
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return compositeKeyRelation.relate(builder, RelationOwner.create(owner));
})
.relate([
{ cid: 33, did: 44 },
{ cid: 33, did: 55 },
{ cid: 66, did: 77 },
]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.equal(3);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
[
'insert into "JoinModel" ("ownerAId", "ownerBId", "relatedCId", "relatedDId") values (11, 22, 33, 44), (11, 22, 33, 55), (11, 22, 66, 77) returning "relatedCId", "relatedDId"',
].join(' '),
);
});
});
it('should accept one id', () => {
mockKnexQueryResults = [[5]];
let owner = OwnerModel.fromJson({ oid: 666 });
return QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate(11)
.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(1);
expect(executedQueries[0]).to.eql(
'insert into "JoinModel" ("ownerId", "relatedId") values (666, 11) returning "relatedId"',
);
});
});
it('should also insert extra properties', () => {
mockKnexQueryResults = [[5]];
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.relateOperationFactory((builder) => {
return relation.relate(builder, RelationOwner.create(owner));
})
.relate({ rid: 10, extra2: 'foo', shouldNotBeInQuery: 'bar' });
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(1);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
[
'insert into "JoinModel" ("extra2", "ownerId", "relatedId") values (\'foo\', 666, 10) returning "relatedId"',
].join(' '),
);
});
});
});
describe('unrelate', () => {
it('should generate a unrelate query', () => {
mockKnexQueryResults = [123];
createModifiedRelation({ someColumn: 100 });
let owner = OwnerModel.fromJson({ oid: 666 });
let builder = QueryBuilder.forClass(RelatedModel)
.unrelateOperationFactory((builder) => {
return relation.unrelate(builder, RelationOwner.create(owner));
})
.unrelate()
.whereIn('code', [55, 66, 77]);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql(123);
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
`delete from "JoinModel" where "JoinModel"."relatedId" in (select "RelatedModel"."rid" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."rid" = "JoinModel"."relatedId" where "JoinModel"."ownerId" in (666) and "someColumn" = 100 and "code" in (55, 66, 77)) and "JoinModel"."ownerId" in (666)`,
);
});
});
it('should generate a unrelate query (composite key)', () => {
let owner = OwnerModel.fromJson({ aid: 11, bid: 22 });
let builder = QueryBuilder.forClass(RelatedModel)
.unrelateOperationFactory((builder) => {
return compositeKeyRelation.unrelate(builder, RelationOwner.create(owner));
})
.unrelate()
.whereIn('code', [55, 66, 77])
.where('someColumn', 100);
return builder.then((result) => {
expect(executedQueries).to.have.length(1);
expect(result).to.eql({});
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.equal(builder.toKnexQuery().toString());
expect(executedQueries[0]).to.eql(
`delete from "JoinModel" where ("JoinModel"."relatedCId","JoinModel"."relatedDId") in (select "RelatedModel"."cid", "RelatedModel"."did" from "RelatedModel" inner join "JoinModel" on "RelatedModel"."cid" = "JoinModel"."relatedCId" and "RelatedModel"."did" = "JoinModel"."relatedDId" where ("JoinModel"."ownerAId", "JoinModel"."ownerBId") in ((11, 22)) and "code" in (55, 66, 77) and "someColumn" = 100) and ("JoinModel"."ownerAId", "JoinModel"."ownerBId") in ((11, 22))`,
);
});
});
});
function createModifiedRelation(modify) {
relation = new ManyToManyRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: ManyToManyRelation,
modify: modify,
join: {
from: 'OwnerModel.oid',
through: {
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
},
to: 'RelatedModel.rid',
},
});
}
function createJoinThroughModifiedRelation(modify) {
relation = new ManyToManyRelation('nameOfOurRelation', OwnerModel);
relation.setMapping({
modelClass: RelatedModel,
relation: ManyToManyRelation,
join: {
from: 'OwnerModel.oid',
through: {
from: 'JoinModel.ownerId',
to: 'JoinModel.relatedId',
modify: modify,
},
to: 'RelatedModel.rid',
},
});
}
});
function isSubclassOf(Constructor, SuperConstructor) {
if (!isFunction(SuperConstructor)) {
return false;
}
while (isFunction(Constructor)) {
if (Constructor === SuperConstructor) {
return true;
}
Constructor = Object.getPrototypeOf(Constructor);
}
return false;
}
================================================
FILE: tests/unit/relations/Relation.js
================================================
const _ = require('lodash');
const Knex = require('knex');
const expect = require('expect.js');
const objection = require('../../../');
const Relation = objection.Relation;
describe('Relation', () => {
let OwnerModel = null;
let RelatedModel = null;
let RelatedModelNamedExport = null;
beforeEach(() => {
delete require.cache[__dirname + '/files/OwnerModel.js'];
delete require.cache[__dirname + '/files/RelatedModel.js'];
OwnerModel = require(__dirname + '/files/OwnerModel');
RelatedModel = require(__dirname + '/files/RelatedModel');
RelatedModelNamedExport = require(__dirname + '/files/RelatedModelNamedExport').RelatedModel;
});
it('should accept a Model subclass as modelClass', () => {
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
to: 'RelatedModel.ownerId',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['id']);
expect(relation.ownerProp.props).to.eql(['id']);
expect(relation.relatedProp.cols).to.eql(['ownerId']);
expect(relation.relatedProp.props).to.eql(['ownerId']);
});
it('should accept a function returning Model subclass as modelClass', () => {
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: () => RelatedModel,
join: {
from: 'OwnerModel.id',
to: 'RelatedModel.ownerId',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['id']);
expect(relation.ownerProp.props).to.eql(['id']);
expect(relation.relatedProp.cols).to.eql(['ownerId']);
expect(relation.relatedProp.props).to.eql(['ownerId']);
});
it('should accept a path to a Model subclass as modelClass', () => {
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: __dirname + '/files/RelatedModel',
join: {
from: 'OwnerModel.id',
to: 'RelatedModel.ownerId',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['id']);
expect(relation.ownerProp.props).to.eql(['id']);
expect(relation.relatedProp.cols).to.eql(['ownerId']);
expect(relation.relatedProp.props).to.eql(['ownerId']);
});
it('should accept a relative path to a Model subclass as modelClass (resolved using Model.modelPaths)', () => {
OwnerModel.modelPaths = [__dirname + '/files/'];
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: 'RelatedModel',
join: {
from: 'OwnerModel.id',
to: 'RelatedModel.ownerId',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['id']);
expect(relation.ownerProp.props).to.eql(['id']);
expect(relation.relatedProp.cols).to.eql(['ownerId']);
expect(relation.relatedProp.props).to.eql(['ownerId']);
});
it('multiple items in `Model.modelPaths` should work', () => {
OwnerModel.modelPaths = [__dirname, __dirname + '/files/'];
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: 'RelatedModel',
join: {
from: 'OwnerModel.id',
to: 'RelatedModel.ownerId',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['id']);
expect(relation.ownerProp.props).to.eql(['id']);
expect(relation.relatedProp.cols).to.eql(['ownerId']);
expect(relation.relatedProp.props).to.eql(['ownerId']);
});
it('should accept a module with named exports', () => {
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: __dirname + '/files/RelatedModelNamedExport',
join: {
from: 'OwnerModel.id',
to: 'RelatedModel.ownerId',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModelNamedExport);
expect(relation.ownerProp.cols).to.eql(['id']);
expect(relation.ownerProp.props).to.eql(['id']);
expect(relation.relatedProp.cols).to.eql(['ownerId']);
expect(relation.relatedProp.props).to.eql(['ownerId']);
});
it('should accept a composite key as an array of columns', () => {
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: ['OwnerModel.name', 'OwnerModel.dateOfBirth'],
to: ['RelatedModel.ownerName', 'RelatedModel.ownerDateOfBirth'],
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['name', 'dateOfBirth']);
expect(relation.ownerProp.props).to.eql(['name', 'dateOfBirth']);
expect(relation.relatedProp.cols).to.eql(['ownerName', 'ownerDateOfBirth']);
expect(relation.relatedProp.props).to.eql(['ownerName', 'ownerDateOfBirth']);
});
it('should fail if relation property and the relation itself have the same name', () => {
let relation = new Relation('foo', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.foo',
to: 'RelatedModel.ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
"OwnerModel.relationMappings.foo: join: relation name and join property 'foo' 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.",
);
});
});
it('should pass through erros thrown from jsonSchema getter', () => {
Object.defineProperties(OwnerModel, {
jsonSchema: {
enumerable: true,
get() {
throw new Error('whoops, invalid json shchema getter');
},
},
});
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
to: 'RelatedModel.ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal('whoops, invalid json shchema getter');
});
});
it('should fail if modelClass is not a subclass of Model', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: function SomeConstructor() {},
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: modelClass: 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.',
);
});
});
it('should fail if modelClass resolves to a module that exports multiple model classes', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: __dirname + '/files/InvalidModelManyNamedModels',
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.match(
/OwnerModel\.relationMappings\.testRelation: modelClass: path .*\/tests\/unit\/relations\/files\/InvalidModelManyNamedModels exports multiple models\. Don't know which one to choose\./,
);
});
});
it('should fail if modelClass is missing', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: null,
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: modelClass is not defined',
);
});
});
it('should fail if modelClass is an invalid file path', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: 'blaa',
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: modelClass: could not resolve blaa using modelPaths',
);
});
});
it('should fail if modelClass is a file path that points to a non-model', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: __dirname + '/files/InvalidModel',
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(
/^OwnerModel\.relationMappings\.testRelation: modelClass: (.+)\/InvalidModel is an invalid file path to a model class$/.test(
err.message,
),
).to.equal(true);
});
});
it('should fail if relation is not defined', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: relation is not defined',
);
});
});
it('should fail if relation is not a Relation subclass', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: function () {},
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: relation is not a subclass of Relation',
);
});
});
it('should fail if OwnerModelClass is not a subclass of Model', () => {
let relation = new Relation('testRelation', {});
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal("Relation: Relation's owner is not a subclass of Model");
});
});
it('join.to should have format ModelName.columnName', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
to: 'ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: join.to must have format TableName.columnName. For example "SomeTable.id" or in case of composite key ["SomeTable.a", "SomeTable.b"].',
);
});
});
it('join.to should point to either of the related model classes', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'SomeOtherModel.id',
to: 'RelatedModel.ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
"OwnerModel.relationMappings.testRelation: 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 'SomeOtherModel' is not correct",
);
});
});
it('join.from should have format ModelName.columnName', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'id',
to: 'RelatedModel.ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: join.from must have format TableName.columnName. For example "SomeTable.id" or in case of composite key ["SomeTable.a", "SomeTable.b"].',
);
});
});
it('join.from should point to either of the related model classes', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
to: 'SomeOtherModel.ownerId',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
"OwnerModel.relationMappings.testRelation: 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 'SomeOtherModel' is not correct",
);
});
});
it('should fail if join object is missing', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: join must be an object that maps the columns of the related models together. For example: {from: "SomeTable.id", to: "SomeOtherTable.someModelId"}',
);
});
});
it('should fail if join.from is missing', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
to: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: join must be an object that maps the columns of the related models together. For example: {from: "SomeTable.id", to: "SomeOtherTable.someModelId"}',
);
});
});
it('should fail if join.to is missing', () => {
let relation = new Relation('testRelation', OwnerModel);
expect(() => {
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'OwnerModel.id',
},
});
}).to.throwException((err) => {
expect(err.message).to.equal(
'OwnerModel.relationMappings.testRelation: join must be an object that maps the columns of the related models together. For example: {from: "SomeTable.id", to: "SomeOtherTable.someModelId"}',
);
});
});
it('the values of `join.to` and `join.from` can be swapped', () => {
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
to: 'OwnerModel.id',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['id']);
expect(relation.ownerProp.props).to.eql(['id']);
expect(relation.relatedProp.cols).to.eql(['ownerId']);
expect(relation.relatedProp.props).to.eql(['ownerId']);
});
it('relatedCol and ownerCol should be in database format', () => {
let relation = new Relation('testRelation', OwnerModel);
Object.defineProperty(OwnerModel, 'tableName', {
get() {
return 'owner_model';
},
});
OwnerModel.prototype.$parseDatabaseJson = (json) => {
return _.mapKeys(json, (value, key) => {
return _.camelCase(key);
});
};
Object.defineProperty(RelatedModel, 'tableName', {
get() {
return 'related-model';
},
});
RelatedModel.prototype.$parseDatabaseJson = (json) => {
return _.mapKeys(json, (value, key) => {
return _.camelCase(key);
});
};
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'owner_model.id_col',
to: 'related-model.owner-id',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['id_col']);
expect(relation.ownerProp.props).to.eql(['idCol']);
expect(relation.relatedProp.cols).to.eql(['owner-id']);
expect(relation.relatedProp.props).to.eql(['ownerId']);
});
it('should allow relations on tables under a schema', () => {
let relation = new Relation('testRelation', OwnerModel);
Object.defineProperty(OwnerModel, 'tableName', {
get() {
return 'schema1.owner_model';
},
});
Object.defineProperty(RelatedModel, 'tableName', {
get() {
return 'schema2.related_model';
},
});
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'schema1.owner_model.id',
to: 'schema2.related_model.owner_id',
},
});
expect(relation.ownerModelClass).to.equal(OwnerModel);
expect(relation.relatedModelClass).to.equal(RelatedModel);
expect(relation.ownerProp.cols).to.eql(['id']);
expect(relation.ownerProp.props).to.eql(['id']);
expect(relation.relatedProp.cols).to.eql(['owner_id']);
expect(relation.relatedProp.props).to.eql(['owner_id']);
});
it('joinModelClass should return null for relations without join models', () => {
let relation = new Relation('testRelation', OwnerModel);
relation.setMapping({
relation: Relation,
modelClass: RelatedModel,
join: {
from: 'RelatedModel.ownerId',
to: 'OwnerModel.id',
},
});
const knex = Knex({ client: 'pg' });
expect(relation.joinModelClass).to.equal(null);
});
});
================================================
FILE: tests/unit/relations/files/InvalidModel.js
================================================
module.exports = class InvalidModel {};
================================================
FILE: tests/unit/relations/files/InvalidModelManyNamedModels.js
================================================
const { Model } = require('../../../../');
class RelatedModel1 extends Model {
static get tableName() {
return this.name;
}
}
class RelatedModel2 extends Model {
static get tableName() {
return this.name;
}
}
module.exports = {
someCrap: 42,
RelatedModel1,
moreUselessShit: {},
RelatedModel2,
};
================================================
FILE: tests/unit/relations/files/JoinModel.js
================================================
const { Model } = require('../../../../');
class JoinModel extends Model {
static get tableName() {
return this.name;
}
}
module.exports = JoinModel;
================================================
FILE: tests/unit/relations/files/ModelWithARandomError.js
================================================
const { Model } = require('../../../../');
throw new Error('some random error');
class TestModel extends Model {
static get tableName() {
return 'test';
}
}
================================================
FILE: tests/unit/relations/files/OwnerModel.js
================================================
const { Model } = require('../../../../');
class OwnerModel extends Model {
static get tableName() {
return this.name;
}
}
module.exports = OwnerModel;
================================================
FILE: tests/unit/relations/files/RelatedModel.js
================================================
const { Model } = require('../../../../');
class RelatedModel extends Model {
static get tableName() {
return this.name;
}
}
module.exports = RelatedModel;
================================================
FILE: tests/unit/relations/files/RelatedModelNamedExport.js
================================================
const { Model } = require('../../../../');
class RelatedModel extends Model {
static get tableName() {
return this.name;
}
}
module.exports = {
someCrap: 42,
RelatedModel,
moreUselessShit: {},
};
================================================
FILE: tests/unit/utils/resolveModel.js
================================================
const { resolveModel } = require('../../../lib/utils/resolveModel');
const expect = require('expect.js');
const path = require('path');
describe('resolveModule', function () {
it("should throw a correct error when resolving a module which has a some error in it's body", () => {
// see GH issue #962
expect(() => {
resolveModel(path.resolve(__dirname, '../relations/files/ModelWithARandomError.js'));
}).throwError(/some random error/);
});
});
================================================
FILE: tests/unit/utils.js
================================================
const expect = require('expect.js');
const Promise = require('bluebird');
const classUtils = require('../../lib/utils/classUtils');
const {
snakeCase,
camelCase,
snakeCaseKeys,
camelCaseKeys,
} = require('../../lib/utils/identifierMapping');
const { range } = require('lodash');
const { compose, mixin } = require('../../lib/utils/mixin');
const { map } = require('../../lib/utils/promiseUtils');
const { jsonEquals, uniqBy } = require('../../lib/utils/objectUtils');
describe('utils', () => {
describe('mixin', () => {
it('should mixin rest of the arguments to the first argument', () => {
class X {}
const m1 = (C) =>
class extends C {
f() {
return 1;
}
};
const m2 = (C) =>
class extends C {
f() {
return super.f() + 1;
}
};
const Y = mixin(X, m1, m2);
const y = new Y();
expect(y.f()).to.equal(2);
if (process.version >= 'v8.0.0') {
expect(Y.name).to.equal('X');
}
const Z = mixin(X, [m1, m2]);
const z = new Z();
expect(z.f()).to.equal(2);
if (process.version >= 'v8.0.0') {
expect(Z.name).to.equal('X');
}
});
});
describe('compose', () => {
it('should compose multiple functions', () => {
class X {}
const m1 = (C) =>
class extends C {
f() {
return 1;
}
};
const m2 = (C) =>
class extends C {
f() {
return super.f() + 1;
}
};
const m3 = compose(m1, m2);
const m4 = compose([m1, m2]);
const Y = m3(X);
const y = new Y();
expect(y.f()).to.equal(2);
if (process.version >= 'v8.0.0') {
expect(Y.name).to.equal('X');
}
const Z = m4(X);
const z = new Z();
expect(z.f()).to.equal(2);
if (process.version >= 'v8.0.0') {
expect(Z.name).to.equal('X');
}
});
});
describe('snakeCase module', () => {
describe('snakeCase and camelCase functions', () => {
test('*', '*');
test('foo', 'foo');
test('fooBar', 'foo_bar');
test('foo1Bar2', 'foo1_bar2');
test('fooBAR', 'foo_bar', 'fooBar');
test('fooBaR', 'foo_ba_r');
test('föö', 'föö');
test('fööBär', 'föö_bär');
test('föö1Bär2', 'föö1_bär2');
test('fööBÄR', 'föö_bär', 'fööBär');
test('fööBäR', 'föö_bä_r');
test('foo1bar2', 'foo1bar2');
test('Foo', 'foo', 'foo');
test('FooBar', 'foo_bar', 'fooBar');
test('märkäLänttiÄäliö', 'märkä_läntti_ääliö');
test('fooBar:spamBaz:troloLolo', 'foo_bar:spam_baz:trolo_lolo');
test('fooBar.spamBaz.troloLolo', 'foo_bar.spam_baz.trolo_lolo');
testUnderscoreBeforeNumbers('*', '*');
testUnderscoreBeforeNumbers('foo', 'foo');
testUnderscoreBeforeNumbers('fooBar', 'foo_bar');
testUnderscoreBeforeNumbers('foo1Bar2', 'foo_1_bar_2');
testUnderscoreBeforeNumbers('fooBAR', 'foo_bar', 'fooBar');
testUnderscoreBeforeNumbers('fooBaR', 'foo_ba_r');
testUnderscoreBeforeNumbers('föö', 'föö');
testUnderscoreBeforeNumbers('fööBär', 'föö_bär');
testUnderscoreBeforeNumbers('föö1Bär2', 'föö_1_bär_2');
testUnderscoreBeforeNumbers('föö12Bär21', 'föö_12_bär_21');
testUnderscoreBeforeNumbers('föö09Bär90', 'föö_09_bär_90');
testUnderscoreBeforeNumbers('fööBÄR', 'föö_bär', 'fööBär');
testUnderscoreBeforeNumbers('fööBäR', 'föö_bä_r');
testUnderscoreBeforeNumbers('foo1bar2', 'foo_1bar_2');
testUnderscoreBeforeNumbers('Foo', 'foo', 'foo');
testUnderscoreBeforeNumbers('FooBar', 'foo_bar', 'fooBar');
testUnderscoreBeforeNumbers('märkäLänttiÄäliö', 'märkä_läntti_ääliö');
testUnderscoreBeforeNumbers('fooBar:spamBaz:troloLolo', 'foo_bar:spam_baz:trolo_lolo');
testUnderscoreBeforeNumbers('fooBar.spamBaz.troloLolo', 'foo_bar.spam_baz.trolo_lolo');
testUnderscoreBetweenUppercaseLetters('*', '*');
testUnderscoreBetweenUppercaseLetters('foo', 'foo');
testUnderscoreBetweenUppercaseLetters('fooBar', 'foo_bar');
testUnderscoreBetweenUppercaseLetters('foo1Bar2', 'foo1_bar2');
testUnderscoreBetweenUppercaseLetters('fooBAR', 'foo_b_a_r');
testUnderscoreBetweenUppercaseLetters('fooBaR', 'foo_ba_r');
testUnderscoreBetweenUppercaseLetters('föö', 'föö');
testUnderscoreBetweenUppercaseLetters('fööBär', 'föö_bär');
testUnderscoreBetweenUppercaseLetters('föö1Bär2', 'föö1_bär2');
testUnderscoreBetweenUppercaseLetters('föö09Bär90', 'föö09_bär90');
testUnderscoreBetweenUppercaseLetters('fööBÄR', 'föö_b_ä_r');
testUnderscoreBetweenUppercaseLetters('fööBäR', 'föö_bä_r');
testUnderscoreBetweenUppercaseLetters('foo1bar2', 'foo1bar2');
testUnderscoreBetweenUppercaseLetters('Foo', 'foo', 'foo');
testUnderscoreBetweenUppercaseLetters('FooBar', 'foo_bar', 'fooBar');
testUnderscoreBetweenUppercaseLetters('märkäLänttiÄäliö', 'märkä_läntti_ääliö');
testUnderscoreBetweenUppercaseLetters(
'fooBar:spamBaz:troloLolo',
'foo_bar:spam_baz:trolo_lolo',
);
testUnderscoreBetweenUppercaseLetters(
'fooBar.spamBaz.troloLolo',
'foo_bar.spam_baz.trolo_lolo',
);
function test(camel, snake, backToCamel) {
backToCamel = backToCamel || camel;
it(`${camel} --> ${snake} --> ${backToCamel}`, () => {
expect(snakeCase(camel)).to.equal(snake);
expect(snakeCaseKeys({ [camel]: 'foo' })).to.eql({ [snake]: 'foo' });
expect(camelCase(snakeCase(camel))).to.equal(backToCamel);
expect(camelCaseKeys(snakeCaseKeys({ [camel]: 'foo' }))).to.eql({ [backToCamel]: 'foo' });
});
}
function testUnderscoreBeforeNumbers(camel, snake, backToCamel) {
backToCamel = backToCamel || camel;
const opt = { underscoreBeforeDigits: true };
it(`${camel} --> ${snake} --> ${backToCamel}`, () => {
expect(snakeCase(camel, opt)).to.equal(snake);
expect(camelCase(snakeCase(camel, opt), opt)).to.equal(backToCamel);
});
}
function testUnderscoreBetweenUppercaseLetters(camel, snake, backToCamel) {
backToCamel = backToCamel || camel;
const opt = { underscoreBetweenUppercaseLetters: true };
it(`${camel} --> ${snake} --> ${backToCamel}`, () => {
expect(snakeCase(camel, opt)).to.equal(snake);
expect(camelCase(snakeCase(camel, opt), opt)).to.equal(backToCamel);
});
}
});
});
describe('promiseUtils', () => {
describe('map', () => {
it('should work like Promise.all if concurrency is not given', () => {
const numItems = 20;
let running = 0;
let maxRunning = 0;
let startOrder = [];
return map(range(numItems), (item, index) => {
startOrder.push(item);
running++;
maxRunning = Math.max(maxRunning, running);
return Promise.delay(Math.round(Math.random() * 10))
.then(() => 2 * item)
.then((result) => {
--running;
return result;
});
}).then((result) => {
expect(maxRunning).to.equal(numItems);
expect(result).to.eql(range(numItems).map((it) => it * 2));
expect(startOrder).to.eql(range(numItems));
});
});
it('should not start new operations after an error has been thrown', (done) => {
const numItems = 20;
let errorThrown = false;
let callbackCalledAfterError = false;
map(range(numItems), (item, index) => {
if (errorThrown) {
callbackCalledAfterError = true;
}
return Promise.delay(Math.round(Math.random() * 10)).then(() => {
if (index === 10) {
errorThrown = true;
throw new Error('fail');
} else {
return item;
}
});
})
.then(() => {
done(new Error('should not get here'));
})
.catch((err) => {
expect(err.message).to.equal('fail');
expect(callbackCalledAfterError).to.equal(false);
done();
})
.catch(done);
});
it('should only run opt.concurrency operations at a time', () => {
const concurrency = 4;
const numItems = 20;
let running = 0;
let startOrder = [];
return map(
range(numItems),
(item, index) => {
startOrder.push(item);
running++;
expect(running).to.be.lessThan(concurrency + 1);
return Promise.delay(Math.round(Math.random() * 10))
.then(() => 2 * item)
.then((result) => {
--running;
return result;
});
},
{ concurrency },
).then((result) => {
expect(result).to.eql(range(numItems).map((it) => it * 2));
expect(startOrder).to.eql(range(numItems));
});
});
it('should work with synchronous callbacks', () => {
const concurrency = 4;
const numItems = 20;
let startOrder = [];
return map(
range(numItems),
(item, index) => {
startOrder.push(item);
return 2 * item;
},
{ concurrency },
).then((result) => {
expect(result).to.eql(range(numItems).map((it) => it * 2));
expect(startOrder).to.eql(range(numItems));
});
});
});
});
describe('jsonEquals', () => {
it('should work with primitives', () => {
expect(jsonEquals(1, 1)).to.equal(true);
expect(jsonEquals('foo', 'foo')).to.equal(true);
expect(jsonEquals(false, false)).to.equal(true);
expect(jsonEquals(true, true)).to.equal(true);
const date = new Date();
expect(jsonEquals(date, date)).to.equal(true);
expect(jsonEquals(date, new Date(date))).to.equal(true);
expect(jsonEquals(new Date(date), date)).to.equal(true);
expect(jsonEquals(1, 2)).to.equal(false);
expect(jsonEquals('foo', 'bar')).to.equal(false);
expect(jsonEquals(true, false)).to.equal(false);
expect(jsonEquals(0, false)).to.equal(false);
expect(jsonEquals(false, 0)).to.equal(false);
expect(jsonEquals('1', 1)).to.equal(false);
expect(jsonEquals(1, '1')).to.equal(false);
expect(jsonEquals(true, false)).to.equal(false);
expect(jsonEquals('true', true)).to.equal(false);
expect(jsonEquals(true, 'true')).to.equal(false);
expect(jsonEquals(new Date(), new Date(Date.now() + 1))).to.equal(false);
});
it('should work with arrays', () => {
expect(jsonEquals([], [])).to.equal(true);
expect(jsonEquals([1], [1])).to.equal(true);
expect(jsonEquals([1, 2], [1, 2])).to.equal(true);
expect(jsonEquals(['foo', 'bar'], ['foo', 'bar'])).to.equal(true);
expect(jsonEquals(['1', 2], [1, '2'])).to.equal(false);
expect(jsonEquals([1], 1)).to.equal(false);
expect(jsonEquals(2, [2])).to.equal(false);
expect(jsonEquals([0], [])).to.equal(false);
expect(jsonEquals([], [0])).to.equal(false);
expect(jsonEquals([1], [2])).to.equal(false);
expect(jsonEquals([1, 2], [2, 1])).to.equal(false);
expect(jsonEquals([1, 2], [1, 2, 3])).to.equal(false);
expect(jsonEquals([1, 2, 3], [1, 2])).to.equal(false);
expect(jsonEquals(['2', 2], [1, '2'])).to.equal(false);
});
it('should work with objects', () => {
expect(jsonEquals({}, {})).to.equal(true);
expect(jsonEquals({ a: 1, b: 2 }, { b: 2, a: 1 })).to.equal(true);
expect(jsonEquals({ a: 1, b: 2 }, { b: 2, a: 2 })).to.equal(false);
expect(jsonEquals({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).to.equal(false);
expect(jsonEquals({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).to.equal(false);
});
it('should work with nested stuff', () => {
expect(
jsonEquals(
{
a: [1, { b: 'foo' }, false],
},
{
a: [1, { b: 'foo' }, false],
},
),
).to.equal(true);
expect(
jsonEquals(
{
a: [1, { b: 'foo' }, false],
},
{
a: [1, { b: 'bar' }, false],
},
),
).to.equal(false);
expect(
jsonEquals(
{
a: [1, { b: 'foo' }, false],
},
{
a: [1, { b: 'foo' }, true],
},
),
).to.equal(false);
expect(
jsonEquals(
[
{
a: [1, { b: 'foo' }, false],
},
1,
],
[
{
a: [1, { b: 'foo' }, false],
},
1,
],
),
).to.equal(true);
expect(
jsonEquals(
[
{
a: [1, { b: 'foo' }, false],
},
1,
],
[
{
a: ['1', { b: 'foo' }, false],
},
1,
],
),
).to.equal(false);
});
});
describe('uniqBy', () => {
const items = [
[Buffer.from('00000000000000000000000000007AAD', 'hex')],
[Buffer.from('00000000000000000000000000007AAE', 'hex')],
[Buffer.from('00000000000000000000000000007AAC', 'hex')],
];
it('should work with Buffer items', () => {
const itemsForTest = items.map(([value]) => value);
expect(uniqBy(itemsForTest)).to.eql(itemsForTest);
});
it('should work with Buffer[] items', () => {
expect(uniqBy(items)).to.eql(items);
});
it('should work with Buffer[] items with custom keyGetter function', () => {
expect(
uniqBy(items, (item) =>
item.map((x) => (Buffer.isBuffer(x) ? x.toString('hex') : x)).join(','),
),
).to.eql(items);
});
});
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"allowJs": false,
"experimentalDecorators": true,
"module": "commonjs",
"moduleResolution": "node",
"noEmit": true,
"noImplicitReturns": true,
"noImplicitUseStrict": false,
"noImplicitAny": true,
"strict": true,
"target": "es6"
},
"include": ["tests/ts/**/*.ts"]
}
================================================
FILE: typings/objection/index.d.ts
================================================
///
// 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
import Ajv, { Options as AjvOptions } from 'ajv';
import * as dbErrors from 'db-errors';
import { Knex } from 'knex';
// Export the entire Objection namespace.
export = Objection;
declare namespace Objection {
const raw: RawFunction;
const val: ValueFunction;
const ref: ReferenceFunction;
const fn: FunctionFunction;
const compose: ComposeFunction;
const mixin: MixinFunction;
const snakeCaseMappers: SnakeCaseMappersFactory;
const knexSnakeCaseMappers: KnexSnakeCaseMappersFactory;
const transaction: transaction;
const initialize: initialize;
const DBError: typeof dbErrors.DBError;
const DataError: typeof dbErrors.DataError;
const CheckViolationError: typeof dbErrors.CheckViolationError;
const UniqueViolationError: typeof dbErrors.UniqueViolationError;
const ConstraintViolationError: typeof dbErrors.ConstraintViolationError;
const ForeignKeyViolationError: typeof dbErrors.ForeignKeyViolationError;
const NotNullViolationError: typeof dbErrors.NotNullViolationError;
export interface RawBuilder extends Aliasable {}
export interface RawFunction extends RawInterface {}
export interface RawInterface {
(sql: string, ...bindings: any[]): R;
}
export interface ValueBuilder extends Castable {}
export interface ValueFunction {
(
value: PrimitiveValue | PrimitiveValue[] | PrimitiveValueObject | PrimitiveValueObject[],
): ValueBuilder;
}
export interface ReferenceBuilder extends Castable {
from(tableReference: string): this;
}
export interface ReferenceFunction {
(expression: string): ReferenceBuilder;
}
export interface FunctionBuilder extends Castable {}
export interface SqlFunctionShortcut {
(...args: any[]): FunctionBuilder;
}
export interface FunctionFunction {
(functionName: string, ...arguments: any[]): FunctionBuilder;
now(precision: number): FunctionBuilder;
now(): FunctionBuilder;
coalesce: SqlFunctionShortcut;
concat: SqlFunctionShortcut;
sum: SqlFunctionShortcut;
avg: SqlFunctionShortcut;
min: SqlFunctionShortcut;
max: SqlFunctionShortcut;
count: SqlFunctionShortcut;
upper: SqlFunctionShortcut;
lower: SqlFunctionShortcut;
}
export interface ComposeFunction {
(...plugins: Plugin[]): Plugin;
(plugins: Plugin[]): Plugin;
}
export interface Plugin {
(modelClass: M): M;
}
export interface MixinFunction {
(modelClass: MC, ...plugins: Plugin[]): MC;
(modelClass: MC, plugins: Plugin[]): MC;
}
interface Aliasable {
as(alias: string): this;
}
interface Castable extends Aliasable {
castText(): this;
castInt(): this;
castBigInt(): this;
castFloat(): this;
castDecimal(): this;
castReal(): this;
castBool(): this;
castJson(): this;
castArray(): this;
asArray(): this;
castType(sqlType: string): this;
castTo(sqlType: string): this;
}
type Raw = RawBuilder | Knex.Raw;
type Operator = string;
type ColumnRef = string | Raw | ReferenceBuilder;
type TableRef = ColumnRef | AnyQueryBuilder | CallbackVoid