diff --git a/doc/guide/query-examples.md b/doc/guide/query-examples.md index 1e2becd87..0589d948a 100644 --- a/doc/guide/query-examples.md +++ b/doc/guide/query-examples.md @@ -1026,7 +1026,7 @@ The query above will insert a pet named `I am the dog of Jennifer whose id is 52 A `#ref` to a new model (a model without an id), or a `#ref{}` to the id of a new model, needs the id that the database gives the model when the graph inserts it. If the graph doesn't write that model, e.g. because the `noInsert` option skips it, or because it sits below a model that `upsertGraph` ignores because of the `noRelate` option or removes because of `#delete` or `#unrelate`, such a reference throws a `ValidationError` of type `InvalidGraph` before anything is written. A plain `#ref` to a model with an id relates the existing row, and a `#ref{}` to any other property reads the model as the graph gives it, so neither needs the model to be written. Below a model that `noInsert` skips, only the branches that references of written models point into are written, without the skipped model. -`upsertGraph` writes the models below rows that it relates into a `HasManyRelation` or `HasOneRelation` from another owner after the rest of the graph (see [graph upserts](#graph-upserts)), so the rest of the graph can't `#ref` new models below them, nor read their ids with `#ref{}`. The references of these models are checked once the rest of the graph is written, before they are written themselves. +`upsertGraph` loads and writes the models below rows that it relates into a `HasManyRelation` or `HasOneRelation` from another owner together with the rest of the graph, with their relations read where the rows end up, so references can point to new models below them like to any other model. 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. diff --git a/doc/release-notes/changelog.md b/doc/release-notes/changelog.md index 1b8a980b8..002753e2f 100644 --- a/doc/release-notes/changelog.md +++ b/doc/release-notes/changelog.md @@ -9,11 +9,10 @@ - The knex peer dependency is `^3.3.0`. See [migration](/release-notes/migration.md#dropped-support-for-knex-3-3) [#95](https://github.com/ditojs/objection/issues/95) - The typings require TypeScript 5.8 or newer, and CI checks them with TypeScript 5.8 and 7. See [migration](/release-notes/migration.md#dropped-support-for-typescript-5-8) - MySQL 8.0.17 or newer is required. MySQL 5.7 is no longer supported. See [migration](/release-notes/migration.md#dropped-support-for-mysql-8) [#292](https://github.com/ditojs/objection/issues/292) -- Many-to-many `patch()` / `update()` with join table extras update the related rows in one query instead of in batches of 500 values. On SQL Server they need SQL Server 2016 or newer, with a database compatibility level of 130 or higher, for `openjson`. Related rows with binary ids, which bind one value per row, fail above about 2100 rows on SQL Server and 999 on SQLite before 3.32, and related rows with composite ids on PostgreSQL above about 32,000 rows with two-column ids (65535 values). See [migration](/release-notes/migration.md#many-to-many-patches-with-join-table-extras-run-in-one-query) [#287](https://github.com/ditojs/objection/issues/287) +- The typings are ES module declarations instead of `export =`. See [migration](/release-notes/migration.md#typescript-the-typings-are-es-module-declarations) - The database error classes are objection's own, from a vendored copy of db-errors, and `instanceof` checks against classes from the `db-errors` package no longer match. See [migration](/release-notes/migration.md#database-errors-are-objection-s-own-classes) - Model files given as paths are loaded with `require()`, so ES module model files need to export the model class as default export or as their only model class, and can't use top-level await. See [migration](/release-notes/migration.md#model-files-given-as-paths) - The deprecated `skipUndefined()` is removed. `undefined` values passed to `where()` and the other SQL builder methods, and `undefined` ids, throw. See [migration](/release-notes/migration.md#skipundefined-is-removed) [#53](https://github.com/ditojs/objection/issues/53) -- The typings are ES module declarations instead of `export =`. See [migration](/release-notes/migration.md#typescript-the-typings-are-es-module-declarations) - The `result` of the static `after*` hooks is what the query returns: a model with `first()`, `findById()`, `insert()` of a single model, `patchAndFetchById()` and queries for to-one relations, `undefined` if nothing is found, a number for updates and deletes, and arrays otherwise, instead of always an array. The hooks run as the last step of the query, after `first()` and the fetches of `insertAndFetch()` and `patchAndFetch()`, whose `afterInsert` and `afterUpdate` get the fetched model. In `before*` hooks, `result` is `undefined`. `insertAndFetch()` and `patchAndFetch()` throw if the `afterFind` of their fetch returns something else than the fetched models. See [migration](/release-notes/migration.md#static-hooks-get-the-result-in-the-shape-that-the-query-returns) [#66](https://github.com/ditojs/objection/issues/66) [#210](https://github.com/ditojs/objection/issues/210) - `runAfter()` callbacks, also the ones of `context()`, `throwIfNotFound()` and `traverse()`, run after the static `after*` hooks and get the result in the shape that the query returns, also when they're added before `first()`, and before `page()` and `range()` wrap it in `{ results, total }`. See [migration](/release-notes/migration.md#static-hooks-get-the-result-in-the-shape-that-the-query-returns) [#66](https://github.com/ditojs/objection/issues/66) - The outer queries of `upsertGraph()` and `upsertGraphAndFetch()` no longer call the static `beforeFind` and `afterFind` hooks of the root model class with the input models, nor `$afterFind` on the input models that they return, unless they read the written rows back with `returning()`. The queries that run for the graph call the hooks. See [migration](/release-notes/migration.md#static-hooks-get-the-result-in-the-shape-that-the-query-returns) [#66](https://github.com/ditojs/objection/issues/66) @@ -21,66 +20,62 @@ - `withGraphFetched()` assigns what a relation query returns to the owners, after the static `afterFind` hook of the related model class and the `runAfter()` callbacks of the relation query, so the related models that they return are assigned, filtered or reordered. Returning other objects than these models throws an error, where 3.x assigned the related models before both and ignored their return values. The same applies to `$relatedQuery()` with `relatedFindQueryMutates`. See [migration](/release-notes/migration.md#withgraphfetched-assigns-what-the-relation-query-returns) [#1963](https://github.com/Vincit/objection.js/issues/1963) - `withGraphJoined()` calls the static `afterFind` hooks of the joined related model classes, bottom-up after the query, with the related models of all owners, the `relation` and the owners as `items`, and assigns what they return like `withGraphFetched()` does with the same models, filtered or reordered. `asFindQuery()` and `cancelQuery()` throw in these hooks. See [migration](/release-notes/migration.md#withgraphfetched-assigns-what-the-relation-query-returns) [#1963](https://github.com/Vincit/objection.js/issues/1963) - `StaticHookArguments` declares `result: R` with `R = undefined`, and the static hooks of `Model` declare the shape of `result` that they get. See [migration](/release-notes/migration.md#static-hooks-get-the-result-in-the-shape-that-the-query-returns) [#66](https://github.com/ditojs/objection/issues/66) -- `idColumn = undefined` means that the model has no primary key, like `idColumn = null`, instead of a column named `undefined` [#129](https://github.com/ditojs/objection/issues/129) - `limit()`, `offset()` and `first()` of relations loaded with `withGraphFetched()` apply per parent by default: the `limitPerParent` graph option defaults to `true`. The relation query becomes a subquery numbered with `row_number()`, so its order can only refer to the related table's selected columns and their aliases. Set `limitPerParent: false` to limit the whole relation query for all parents as in 3.x. See [migration](/release-notes/migration.md#limit-offset-and-first-apply-per-parent-in-graph-modifiers) [#127](https://github.com/ditojs/objection/issues/127) [#429](https://github.com/Vincit/objection.js/issues/429) -- `insertGraph()`, `upsertGraph()` and their `*AndFetch()` variants throw an error when combined with `onConflict()`, `ignore()` or `merge()`, instead of ignoring the clause with a warning. See [migration](/release-notes/migration.md#onconflict-with-insertgraph-and-upsertgraph-throws) [#2156](https://github.com/Vincit/objection.js/issues/2156) -- The deprecated `allowMarkers` option is removed from the `InsertGraphOptions` typings. It has no effect since 3.8.0, where `insertGraph()` rejects `#unrelate` and `#delete` markers. See [migration](/release-notes/migration.md#typescript-insertgraph-has-no-allowmarkers-option) -- `snakeCaseMappers()` only converts the column part of field expressions like `patch({ 'jsonColumn:someKey': 1 })` and keeps their JSON keys as written: the `preserveJsonKeys` option defaults to `true`. Set `preserveJsonKeys: false` to convert the JSON keys to snake case too, as in 3.x. `knexSnakeCaseMappers()` never converted them. Keys with a `:` that the model's `jsonSchema` declares in `properties` are columns and converted as a whole, so a column like `ns:foo_bar` round-trips as `ns:fooBar`. See [migration](/release-notes/migration.md#snakecasemappers-keeps-the-json-keys-of-field-expressions) [#1089](https://github.com/Vincit/objection.js/issues/1089) -- `insert()` and `update()` reject field expression keys like `'meta:color'` with a `ValidationError` of type `ModelValidation`, as they write whole values. 3.x `update()` writes them with `jsonb_set` and validates them like a patch, and 3.x `insert()` passes them to the database as column names. Use `patch()` to write parts of json columns. Keys with a `:` that the schema declares in `properties`, e.g. `'ns:name'`, are normal properties and stay allowed, as do keys that don't parse as field expressions, e.g. `'ns:'`, which `patch()` and `merge()` also write as columns instead of failing to parse them. Only patch validation validates field expression keys against the json column's schema: `fromJson()` without `patch` validates them as top-level properties, as before 3.8. See [migration](/release-notes/migration.md#insert-and-update-reject-field-expression-keys) [#160](https://github.com/ditojs/objection/issues/160) +- `findById()` and `findByIds()` add their id condition in its own parentheses, like `$query()` and the `*ById()` writes, so `orWhere()` can't widen it: `findById(1).orWhere(b)` is `id = 1 and (b)`, where 3.x built `id = 1 or b`, and `where(a).orWhere(b).findById(1)` is `(a or b) and id = 1`. See [migration](/release-notes/migration.md#findbyid-and-findbyids-keep-their-id-condition-in-its-own-group) [#271](https://github.com/ditojs/objection/issues/271) +- Aggregates like `count()` and `sum()` select their aliases, not the columns they're passed, but still replace the select all of the query. `withGraphFetched()` and `withGraphJoined()` select the join columns that queries with aggregates like `count('* as n')` or `count('ownerId')` need, where 3.x left them out and returned empty relations. `withGraphFetched()` and relation queries use an aggregate, raw SQL or a subquery aliased as a join column, like `max('ownerId as ownerId')`, instead of selecting the column again. Grouped queries need to group by these columns too, or the database rejects them. In `withGraphJoined()`, the root query selects only its id columns next to aggregates instead of all its columns. See [migration](/release-notes/migration.md#aggregates-don-t-select-the-columns-they-re-passed) [#111](https://github.com/ditojs/objection/issues/111) +- Writes with `returning()` resolve in the result kind of their query: `patch()`, `update()` and `delete()` through `$relatedQuery()` of a to-one relation resolve to the written model, or `undefined`, like the find query, instead of an array, also as `result` of the static `afterUpdate()` and `afterDelete()` hooks. The typings give writes with `returning()`, before or after the write, one model after `$query()`, `findById()`, `first()`, the `*ById()` methods and to-one `$relatedQuery()`, which they typed as arrays, and an array otherwise. Custom query builders that override `patchById()`, `updateById()` or `deleteById()` declare `NumberQueryBuilder>`. See [migration](/release-notes/migration.md#writes-of-to-one-relation-queries-with-returning-resolve-to-one-model) [#53](https://github.com/ditojs/objection/issues/53) +- `update()`, `patch()` and `delete()` throw an error for `offset()` when the query is executed, instead of warning that knex leaves it out, and on MySQL, `delete()` throws for `orderBy()` with `limit()`, as knex leaves out the order. A many-to-many `patch()` / `update()` with join table extras on MySQL and SQL Server honours the offset and doesn't throw. See [migration](/release-notes/migration.md#offset-in-updates-and-deletes-throws) [#286](https://github.com/ditojs/objection/issues/286) [#235](https://github.com/ditojs/objection/issues/235) +- Many-to-many `patch()` / `update()` with join table extras update the related rows in one query instead of in batches of 500 values. On SQL Server they need SQL Server 2016 or newer, with a database compatibility level of 130 or higher, for `openjson`. Related rows with binary ids, which bind one value per row, fail above about 2100 rows on SQL Server and 999 on SQLite before 3.32, and related rows with composite ids on PostgreSQL above about 32,000 rows with two-column ids (65535 values). See [migration](/release-notes/migration.md#many-to-many-patches-with-join-table-extras-run-in-one-query) [#287](https://github.com/ditojs/objection/issues/287) - BelongsToOne `unrelate()` applies the relation's `modify` / `filter` also without a where clause, and only unrelates owners whose related row matches it, like the other relations do, instead of unrelating all owners. With a filter, it clears the foreign keys of the owner models after the query, and only of those it unrelated [#234](https://github.com/ditojs/objection/issues/234) - Relation expressions that name a relation twice, like `'[movies.reviews, movies.actors]'`, throw an error instead of using only the last one with a warning: in `withGraphFetched()`, `withGraphJoined()`, `allowGraph()`, `joinRelated()` and other relation expressions, and in the graph options of `insertGraph()` and `upsertGraph()`. See [migration](/release-notes/migration.md#relation-expressions-that-name-a-relation-twice-throw) [#131](https://github.com/ditojs/objection/issues/131) -- `insertGraph()`, `upsertGraph()` and their `*AndFetch()` variants throw an error for unknown graph options like `noInset` instead of ignoring them with a warning, and `insertGraph()` also for the options of `upsertGraph()` that have no effect on an insert, like `noDelete` or `allowMarkers`. See [migration](/release-notes/migration.md#unknown-graph-options-throw) [#84](https://github.com/ditojs/objection/issues/84) -- The type parameter of `RelateQueryBuilder` is the number query builder, e.g. `RelateQueryBuilder>` in custom query builders that override `relate()`. See [migration](/release-notes/migration.md#typescript-relatequerybuilder-takes-the-number-query-builder) -- Custom query builders declare one `RebindType` member instead of the five `ArrayQueryBuilderType`, `SingleQueryBuilderType`, `MaybeSingleQueryBuilderType`, `NumberQueryBuilderType` and `PageQueryBuilderType` members, which are removed: `declare RebindType: MyQueryBuilder`, with the extra types `X` as third type parameter. The helper types `SingleQueryBuilder` etc. take a query builder, and `WithGraphModelQueryBuilder` and `ReturningMethod` are removed. See [migration](/release-notes/migration.md#typescript-custom-query-builders-declare-rebindtype) [#125](https://github.com/ditojs/objection/issues/125) [#319](https://github.com/Vincit/objection.js/issues/319) -- The internal helper types of the relation expression parser, the `withGraph*()` narrowing and the property names, like `ParseRelationString`, `WithGraph` and `RelationPropertyNames`, are no longer exported, and the unused `ItemType` is removed. See [migration](/release-notes/migration.md#typescript-internal-helper-types-are-no-longer-exported) -- `static jsonSchema` is typed `ReadonlyJSONSchema`, a JSON schema with readonly arrays, so that it takes schemas declared `as const`. A model's schema read through the static type can't be passed where a `JSONSchema` is expected, unless the model declares `static jsonSchema: JSONSchema`. See [migration](/release-notes/migration.md#typescript-model-jsonschema-is-a-readonly-schema) [#16](https://github.com/ditojs/objection/issues/16) -- `insert()`, `patch()`, `update()`, their `*AndFetch*()` and `*ById()` variants, `merge()` and `relate()` reject relation properties, which they ignore at runtime: they take a `PartialModelRow`, a model instance or its properties without relations, instead of `PartialModelObject`. Values whose type includes the relation properties are rejected too, e.g. `Partial`, `ModelObject` or `PartialModelObject` of models with relations. `insertGraph()` and `upsertGraph()` take relations. See [migration](/release-notes/migration.md#typescript-insert-patch-and-update-reject-relation-data) [#53](https://github.com/ditojs/objection/issues/53) -- `distinctOn()` is typed to take column names, `raw()` and `ref()`, like `groupBy()`, instead of any selection, as it doesn't select its columns. -- `ModelObject`, `PartialModelObject` and the other property types leave out properties starting with `$`, which objection treats as internal, like `toJSON()` does. See [migration](/release-notes/migration.md#typescript-property-types-leave-out-properties) [#16](https://github.com/ditojs/objection/issues/16) -- `$relatedQuery()` of a to-one relation, e.g. `person.$relatedQuery('parent')`, resolves to `Model | undefined`, as it does at runtime when there is no related row. See [migration](/release-notes/migration.md#typescript-relatedquery-of-to-one-relations-may-resolve-to-undefined) [#2355](https://github.com/Vincit/objection.js/issues/2355) -- Writes with `returning()` resolve in the result kind of their query: `patch()`, `update()` and `delete()` through `$relatedQuery()` of a to-one relation resolve to the written model, or `undefined`, like the find query, instead of an array, also as `result` of the static `afterUpdate()` and `afterDelete()` hooks. The typings give writes with `returning()`, before or after the write, one model after `$query()`, `findById()`, `first()`, the `*ById()` methods and to-one `$relatedQuery()`, which they typed as arrays, and an array otherwise. Custom query builders that override `patchById()`, `updateById()` or `deleteById()` declare `NumberQueryBuilder>`. See [migration](/release-notes/migration.md#writes-of-to-one-relation-queries-with-returning-resolve-to-one-model) [#53](https://github.com/ditojs/objection/issues/53) -- `toJSON()` and `$toJson()` return `ModelJson`: the properties without methods, and the related models as JSON too, instead of `ModelObject` with model instances. See [migration](/release-notes/migration.md#typescript-tojson-returns-modeljson) [#2380](https://github.com/Vincit/objection.js/issues/2380) -- The rows of `relate().returning()` are typed `Pojo[]` instead of `any[]`, as they are join rows, related rows or owner rows depending on the relation, not models of the query. Cast them to the row type you expect. See [migration](/release-notes/migration.md#typescript-relate-returning-rows-are-plain-objects) -- `insertGraph()` throws a `ValidationError` of type `InvalidGraph` for new models below related models, instead of ignoring them with a warning. Models with ids below related models are still ignored. See [migration](/release-notes/migration.md#insertgraph-throws-for-new-models-below-related-models) [#217](https://github.com/ditojs/objection/issues/217) -- `upsertGraph()` removing the row of a `BelongsToOneRelation` with `pet: null` or `#delete` always sets the owner's foreign key to `null` before it deletes the row, like `unrelate()`, also when the owner's `jsonSchema` doesn't allow `null` for it. 3.9 deleted the row first in that case and left the owner's reference to the database. Owners whose schema only allows a number now fail with a `ValidationError` and `NOT NULL` columns with a database error, where `ON DELETE CASCADE` deleted the owner and `ON DELETE SET NULL` cleared the reference. See [migration](/release-notes/migration.md#upsertgraph-sets-the-foreign-key-to-null-before-deleting-a-belongstoonerelation-row) [#212](https://github.com/ditojs/objection/issues/212) -- `upsertGraph()` keeps the current model of a `HasOneRelation` or `HasOneThroughRelation` when the graph holds a model marked with `#unrelate` or `#delete` that isn't the current one, or a replacement that `noInsert` or `noRelate` skips, like for `BelongsToOneRelation` since 3.9. 3.x deleted or unrelated the current model. To-many relations are unchanged: their array is the full list. See [migration](/release-notes/migration.md#upsertgraph-keeps-the-current-to-one-model-for-ignored-models) [#261](https://github.com/ditojs/objection/issues/261) -- `upsertGraph()` throws a `NotFoundError` when a relate or update finds no row to write, e.g. a row moved below a new row while `ON DELETE CASCADE` deletes it with its old parent, or a row deleted by a concurrent query, instead of reporting success and returning the row. Writes that a `beforeUpdate` hook resolves, e.g. with `cancelQuery()`, and patches that leave no column to write still succeed [#214](https://github.com/ditojs/objection/issues/214) -- Aggregates like `count()` and `sum()` select their aliases, not the columns they're passed, but still replace the select all of the query. `withGraphFetched()` and `withGraphJoined()` select the join columns that queries with aggregates like `count('* as n')` or `count('ownerId')` need, where 3.x left them out and returned empty relations. `withGraphFetched()` and relation queries use an aggregate, raw SQL or a subquery aliased as a join column, like `max('ownerId as ownerId')`, instead of selecting the column again. Grouped queries need to group by these columns too, or the database rejects them. In `withGraphJoined()`, the root query selects only its id columns next to aggregates instead of all its columns. See [migration](/release-notes/migration.md#aggregates-don-t-select-the-columns-they-re-passed) [#111](https://github.com/ditojs/objection/issues/111) - Relation expressions with anything after `*` or `^` in a relation path, like `'parent.^.pets'`, `'*.pets'` or `'children.*.pets'`, or with `*` and `^` in a list, like `'[children, *]'`, throw an error instead of silently ignoring the rest: in `withGraphFetched()`, `withGraphJoined()`, `allowGraph()`, `joinRelated()` and other relation expressions, and in the graph options of `insertGraph()` and `upsertGraph()`. See [migration](/release-notes/migration.md#misplaced-and-in-relation-expressions-throw) [#221](https://github.com/ditojs/objection/issues/221) -- `update()`, `patch()` and `delete()` throw an error for `offset()` when the query is executed, instead of warning that knex leaves it out, and on MySQL, `delete()` throws for `orderBy()` with `limit()`, as knex leaves out the order. A many-to-many `patch()` / `update()` with join table extras on MySQL and SQL Server honours the offset and doesn't throw. See [migration](/release-notes/migration.md#offset-in-updates-and-deletes-throws) [#286](https://github.com/ditojs/objection/issues/286) [#235](https://github.com/ditojs/objection/issues/235) -- JSON field expressions (`ref('column:path')`, `'column:path'` keys in `patch()`), `ref('column').castJson()` and the `whereJson*()` methods of objection throw an error on SQL Server and Oracle instead of warning, as they generate SQL that these databases don't understand, and so do `whereJsonSupersetOf()` and `whereJsonSubsetOf()` on SQLite. Other databases than PostgreSQL, MySQL and SQLite still get a warning. See [migration](/release-notes/migration.md#json-field-expressions-throw-on-sql-server-and-oracle) [#113](https://github.com/ditojs/objection/issues/113) -- On MySQL, the casts of `ref()` and `val()` use MySQL's types: `castFloat()` is `DOUBLE` instead of `FLOAT` and `castReal()` `FLOAT` instead of `DOUBLE`, like `float` and `real` in PostgreSQL, and `castDecimal()` is `DECIMAL(65,30)` instead of `DECIMAL`, which rounds to integers. `castText()`, `castInt()`, `castBigInt()` and `castBool()`, which MySQL rejected, are `CHAR` and `SIGNED`. On MySQL and SQLite, `val()` with an object or array and `val().castJson()` are json values, `CAST(? AS JSON)` on MySQL and `json(?)` on SQLite, instead of text. See [migration](/release-notes/migration.md#casts-json-values-and-json-methods-on-mysql-and-sqlite) [#276](https://github.com/Vincit/objection.js/issues/276) -- `$setRelated()` and `$appendRelated()` keep the foreign keys in sync with the relation. For a `BelongsToOneRelation` the object's foreign key is set to the related model's key, e.g. `post.$setRelated('author', user)` sets `post.authorId`, or to `null` when the relation is cleared or the related model has no key. For `HasOneRelation` and `HasManyRelation` the related models' foreign keys are set to the object's key, or to `null` when the object has no key, and those of the models that are removed or replaced are set to `null` if they still point to the object. Conflicting foreign keys are overwritten. Many-to-many and has-one-through relations are unchanged, and `fromJson()`, `$setJson()` and queries don't sync keys. See [migration](/release-notes/migration.md#setrelated-and-appendrelated-sync-foreign-keys) [#2001](https://github.com/Vincit/objection.js/issues/2001) - `$fetchGraph()`, `fetchGraph()` and `$relatedQuery()` throw an error when owner models loaded from the database are missing a join property, e.g. an id left out of a partial `select()`, instead of warning and finding no related models for them. Owner models built in memory, e.g. with `fromJson()`, find no related models for a missing join property without a warning. This includes the foreign key of a `BelongsToOneRelation`, which 3.x didn't report unless it was part of the id. Models returned by `insert()` aren't loaded from the database, also with `returning()`. `$clone()` keeps whether a model was loaded from the database. See [migration](/release-notes/migration.md#missing-and-array-join-properties-throw) [#1832](https://github.com/Vincit/objection.js/issues/1832) - `withGraphFetched()` and `$fetchGraph()` throw an error when related models come back without their join property, e.g. snake_case relation mappings with `knexSnakeCaseMappers()`, instead of warning and dropping them. See [migration](/release-notes/migration.md#missing-and-array-join-properties-throw) [#2258](https://github.com/Vincit/objection.js/issues/2258) - Fetching a relation for owner models whose join property holds an array, e.g. a JSON array of keys, throws an error instead of warning and finding no related models. `$relatedQuery()` keeps working for them. See [migration](/release-notes/migration.md#missing-and-array-join-properties-throw) [#114](https://github.com/ditojs/objection/issues/114) -- `upsertGraph()` relates the rows that move to existing owners of a `HasManyRelation` or `HasOneRelation` before it deletes the removed rows, so rows moved out of a deleted parent, up a level or to a sibling are kept, where `ON DELETE CASCADE` deleted them and foreign keys without `ON DELETE` action failed. The update hooks of these relates run before the delete hooks. Rows that wait for the keys or references of models that the graph inserts are related after the inserts, as in 3.x. A relate into a `HasOneRelation` still waits for the removal of the current model, or for the relate that moves it to another owner, also a new one or one whose slot is freed first. A row related from another owner that takes the unique key of a row that the graph removes in its new place, e.g. with `unique(ownerId, name)`, fails with a `UniqueViolationError`, where 3.x removed the other row first, and so does a new row that takes the unique key of a row that moves to a new owner, where 3.x cleared the moved row's key first. See [migration](/release-notes/migration.md#upsertgraph-relates-moved-rows-before-it-deletes) [#214](https://github.com/ditojs/objection/issues/214) -- `findById()` and `findByIds()` add their id condition in its own parentheses, like `$query()` and the `*ById()` writes, so `orWhere()` can't widen it: `findById(1).orWhere(b)` is `id = 1 and (b)`, where 3.x built `id = 1 or b`, and `where(a).orWhere(b).findById(1)` is `(a or b) and id = 1`. See [migration](/release-notes/migration.md#findbyid-and-findbyids-keep-their-id-condition-in-its-own-group) [#271](https://github.com/ditojs/objection/issues/271) +- `$setRelated()` and `$appendRelated()` keep the foreign keys in sync with the relation. For a `BelongsToOneRelation` the object's foreign key is set to the related model's key, e.g. `post.$setRelated('author', user)` sets `post.authorId`, or to `null` when the relation is cleared or the related model has no key. For `HasOneRelation` and `HasManyRelation` the related models' foreign keys are set to the object's key, or to `null` when the object has no key, and those of the models that are removed or replaced are set to `null` if they still point to the object. Conflicting foreign keys are overwritten. Many-to-many and has-one-through relations are unchanged, and `fromJson()`, `$setJson()` and queries don't sync keys. See [migration](/release-notes/migration.md#setrelated-and-appendrelated-sync-foreign-keys) [#2001](https://github.com/Vincit/objection.js/issues/2001) +- `idColumn = undefined` means that the model has no primary key, like `idColumn = null`, instead of a column named `undefined` [#129](https://github.com/ditojs/objection/issues/129) - On PostgreSQL drivers (`pg`, `pgnative`, `cockroachdb`), JSON attributes keep the values that the driver returns, so a `json` / `jsonb` column holding a string that looks like JSON, e.g. `'123'` or `'{"a":1}'`, is no longer parsed a second time, and JSON in `text` columns is no longer parsed. Set `jsonColumnFormat = 'text'` on models that keep JSON in `text` columns on PostgreSQL. Other drivers parse JSON attributes as in 3.x. See [migration](/release-notes/migration.md#json-in-postgresql-text-columns-is-no-longer-parsed) [#205](https://github.com/ditojs/objection/issues/205) -- `upsertGraph()` upserts the models below related models in the same graph, instead of in nested `upsertGraph()` calls after all other writes: their rows are fetched together, their writes run in the same steps as the rest of the graph, and `#ref` can point between them. A related model with relations whose row doesn't exist throws a `NotFoundError` before anything is written, where 3.x wrote the rest of the graph first. The relations of rows that the graph moves to another owner are read where the rows end up, all errors are thrown before anything is written, and a moved row is written once, so its update hooks run once, in the order of the other writes of their kind, where 3.x upserted the models below moved rows in a later pass and wrote a moved row twice if they changed its keys. See [migration](/release-notes/migration.md#upsertgraph-upserts-below-related-models-in-the-same-graph) -- `upsertGraph()` doesn't patch existing models that the graph gives no changes, e.g. a model with only its id and an `#id`, or a related model whose BelongsToOne key stays the same, so their update hooks don't run, where 3.x patched them and ran the hooks -- `upsertGraph()` patches the join rows of existing many-to-many models only with the join table extras that change, and with none under `noUpdate`, where 3.x wrote all extras that the graph gives with every patch of the related model. With `noUpdate`, a model whose changes `noUpdate` drops isn't patched and runs no update hooks, where 3.9.0 ran its `$beforeUpdate` hook once without writing -- `insertGraph()` and `upsertGraph()` throw a `ValidationError` of type `InvalidGraph` for graphs that give one row two different values for the same column, or the slot of a `HasOneRelation` two rows, e.g. a row under two owners through the same foreign key, or under an owner while its own `BelongsToOneRelation` names another model, or a model related twice through a many-to-many relation with different join table extras. This holds for any two places of a row, also for the values that places read through the rows they relate, e.g. two rows that list each other as siblings at different owners. 3.x wrote them in an undefined order that could delete rows the graph names. A model related twice with the same extras gets one join row. See [migration](/release-notes/migration.md#insertgraph-and-upsertgraph-reject-rows-placed-in-conflicting-ways) +- `snakeCaseMappers()` only converts the column part of field expressions like `patch({ 'jsonColumn:someKey': 1 })` and keeps their JSON keys as written: the `preserveJsonKeys` option defaults to `true`. Set `preserveJsonKeys: false` to convert the JSON keys to snake case too, as in 3.x. `knexSnakeCaseMappers()` never converted them. Keys with a `:` that the model's `jsonSchema` declares in `properties` are columns and converted as a whole, so a column like `ns:foo_bar` round-trips as `ns:fooBar`. See [migration](/release-notes/migration.md#snakecasemappers-keeps-the-json-keys-of-field-expressions) [#1089](https://github.com/Vincit/objection.js/issues/1089) +- `insert()` and `update()` reject field expression keys like `'meta:color'` with a `ValidationError` of type `ModelValidation`, as they write whole values. 3.x `update()` writes them with `jsonb_set` and validates them like a patch, and 3.x `insert()` passes them to the database as column names. Use `patch()` to write parts of json columns. Keys with a `:` that the schema declares in `properties`, e.g. `'ns:name'`, are normal properties and stay allowed, as do keys that don't parse as field expressions, e.g. `'ns:'`, which `patch()` and `merge()` also write as columns instead of failing to parse them. Only patch validation validates field expression keys against the json column's schema: `fromJson()` without `patch` validates them as top-level properties, as before 3.8. See [migration](/release-notes/migration.md#insert-and-update-reject-field-expression-keys) [#160](https://github.com/ditojs/objection/issues/160) +- JSON field expressions (`ref('column:path')`, `'column:path'` keys in `patch()`), `ref('column').castJson()` and the `whereJson*()` methods of objection throw an error on SQL Server and Oracle instead of warning, as they generate SQL that these databases don't understand, and so do `whereJsonSupersetOf()` and `whereJsonSubsetOf()` on SQLite. Other databases than PostgreSQL, MySQL and SQLite still get a warning. See [migration](/release-notes/migration.md#json-field-expressions-throw-on-sql-server-and-oracle) [#113](https://github.com/ditojs/objection/issues/113) +- On MySQL, the casts of `ref()` and `val()` use MySQL's types: `castFloat()` is `DOUBLE` instead of `FLOAT` and `castReal()` `FLOAT` instead of `DOUBLE`, like `float` and `real` in PostgreSQL, and `castDecimal()` is `DECIMAL(65,30)` instead of `DECIMAL`, which rounds to integers. `castText()`, `castInt()`, `castBigInt()` and `castBool()`, which MySQL rejected, are `CHAR` and `SIGNED`. On MySQL and SQLite, `val()` with an object or array and `val().castJson()` are json values, `CAST(? AS JSON)` on MySQL and `json(?)` on SQLite, instead of text. See [migration](/release-notes/migration.md#casts-json-values-and-json-methods-on-mysql-and-sqlite) [#276](https://github.com/Vincit/objection.js/issues/276) +- `insertGraph()`, `upsertGraph()` and their `*AndFetch()` variants throw an error when combined with `onConflict()`, `ignore()` or `merge()`, instead of ignoring the clause with a warning. See [migration](/release-notes/migration.md#onconflict-with-insertgraph-and-upsertgraph-throws) [#2156](https://github.com/Vincit/objection.js/issues/2156) +- `insertGraph()`, `upsertGraph()` and their `*AndFetch()` variants throw an error for unknown graph options like `noInset` instead of ignoring them with a warning, and `insertGraph()` also for the options of `upsertGraph()` that have no effect on an insert, like `noDelete` or `allowMarkers`. See [migration](/release-notes/migration.md#unknown-graph-options-throw) [#84](https://github.com/ditojs/objection/issues/84) +- `insertGraph()` throws a `ValidationError` of type `InvalidGraph` for new models below related models, instead of ignoring them with a warning. Models with ids below related models are still ignored. See [migration](/release-notes/migration.md#insertgraph-throws-for-new-models-below-related-models) [#217](https://github.com/ditojs/objection/issues/217) +- `insertGraph()` and `upsertGraph()` throw a `ValidationError` of type `InvalidGraph` for graphs that give one row two different values for the same column, or the slot of a `HasOneRelation` two rows. Examples are a row under two owners through the same foreign key, a row under an owner while its own `BelongsToOneRelation` names another model, and a model related twice through a many-to-many relation with different join table extras. This holds for any two places of a row, also for the values that places read through the rows they relate, e.g. two rows that list each other as siblings at different owners. It also holds for a place that clears a key that another place places rows by, e.g. a row that its owner's list leaves out while its own `siblings` are placed at that owner. 3.x wrote these graphs in an undefined order that could delete rows the graph names, and wrote the rows placed by a cleared key with `null`. A model related twice with the same extras gets one join row. See [migration](/release-notes/migration.md#insertgraph-and-upsertgraph-reject-rows-placed-in-conflicting-ways) +- `upsertGraph()` upserts the models below related models in the same graph, instead of in nested `upsertGraph()` calls after all other writes: their rows are fetched together, their writes run in the same steps as the rest of the graph, and `#ref` can point between them. A related model with relations whose row doesn't exist throws a `NotFoundError` before anything is written, where 3.x wrote the rest of the graph first. The relations of rows that the graph moves to another owner are read where the rows end up, whether a row moves through a relate into a new owner, its own `BelongsToOneRelation` or a key that another place writes. The siblings that a moved row lists anywhere in the graph apply at its new owner: its old siblings stay where they are, and `siblings: []` unrelates all other rows at the new owner. Rows keep their related rows when a key changes with them instead, e.g. through a `#ref{}`. All errors are thrown before anything is written. A moved row is written once, so its update hooks run once, in the order of the other writes of their kind. 3.x upserted the models below moved rows in a later pass, and wrote a moved row twice if they changed its keys. See [migration](/release-notes/migration.md#upsertgraph-upserts-below-related-models-in-the-same-graph) [#427](https://github.com/ditojs/objection/pull/427) +- `upsertGraph()` relates the rows that move to existing owners of a `HasManyRelation` or `HasOneRelation` before it deletes the removed rows. Rows moved out of a deleted parent, up a level or to a sibling are kept, where `ON DELETE CASCADE` deleted them and foreign keys without `ON DELETE` action failed. The update hooks of these relates run before the delete hooks. Rows that wait for the keys or references of models that the graph inserts are related after the inserts, as in 3.x. A relate into a `HasOneRelation` still waits for the removal of the current model, or for the relate that moves it to another owner, also a new one or one whose slot is freed first. A row related from another owner that takes the unique key of a row that the graph removes in its new place, e.g. with `unique(ownerId, name)`, fails with a `UniqueViolationError`, where 3.x removed the other row first. So does a new row that takes the unique key of a row that moves to a new owner, where 3.x cleared the moved row's key first. Swapping the rows of two `HasOneRelation` slots with a unique foreign key, e.g. two people's `pet`, fails with a `UniqueViolationError` too, where 3.x with `unrelate` unrelated both rows first. See [migration](/release-notes/migration.md#upsertgraph-relates-moved-rows-before-it-deletes) [#214](https://github.com/ditojs/objection/issues/214) - `upsertGraph()` checks that the models it relates exist, also through `#dbRef`, and throws a `NotFoundError` "model (id=N) does not exist" before anything is written if one doesn't and the graph doesn't insert it elsewhere, for all relation types. 3.9.0 only checked related models with relations of their own: it patched no row for a missing `HasManyRelation` or `HasOneRelation` model, and wrote the foreign key or join row of a missing `BelongsToOneRelation` or `ManyToManyRelation` model. The graph fetches the ids of the related models without relations in one query per model class. See [migration](/release-notes/migration.md#upsertgraph-checks-that-related-models-exist) [#372](https://github.com/ditojs/objection/issues/372) -- `insertGraph()` and `upsertGraph()` start the data paths of models in array input with their index in the array: the keys of `ValidationError`s, the `dataPath` of `NotFoundError`s and the `dataPath` option that validators get, e.g. `[1].pets[0].name` instead of `pets[0].name`, which 3.x gave for every model of the array. Graphs of a single object keep their paths. See [migration](/release-notes/migration.md#insertgraph-and-upsertgraph-errors-name-the-index-of-array-input) +- `upsertGraph()` throws a `NotFoundError` when a relate or update finds no row to write, e.g. a row moved below a new row while `ON DELETE CASCADE` deletes it with its old parent, or a row deleted by a concurrent query, instead of reporting success and returning the row. Writes that a `beforeUpdate` hook resolves, e.g. with `cancelQuery()`, and patches that leave no column to write still succeed [#214](https://github.com/ditojs/objection/issues/214) +- `upsertGraph()` removing the row of a `BelongsToOneRelation` with `pet: null` or `#delete` always sets the owner's foreign key to `null` before it deletes the row, like `unrelate()`, also when the owner's `jsonSchema` doesn't allow `null` for it. 3.9 deleted the row first in that case and left the owner's reference to the database. Owners whose schema only allows a number now fail with a `ValidationError` and `NOT NULL` columns with a database error, where `ON DELETE CASCADE` deleted the owner and `ON DELETE SET NULL` cleared the reference. See [migration](/release-notes/migration.md#upsertgraph-sets-the-foreign-key-to-null-before-deleting-a-belongstoonerelation-row) [#212](https://github.com/ditojs/objection/issues/212) +- `upsertGraph()` keeps the current model of a `HasOneRelation` or `HasOneThroughRelation` when the graph holds a model marked with `#unrelate` or `#delete` that isn't the current one, or a replacement that `noInsert` or `noRelate` skips, like for `BelongsToOneRelation` since 3.9. 3.x deleted or unrelated the current model. To-many relations are unchanged: their array is the full list. See [migration](/release-notes/migration.md#upsertgraph-keeps-the-current-to-one-model-for-ignored-models) [#261](https://github.com/ditojs/objection/issues/261) - `insertGraph()` and `upsertGraph()` throw a `ValidationError` of type `InvalidGraph` before anything is written for a `#ref` to a new model, or a `#ref{}` to the id of a new model, that the graph doesn't write, e.g. below a model that `noRelate` ignores or that is marked with `#delete`, or skipped by `noInsert`. Other `#ref{}` read the models as the graph gives them, also in branches that the graph doesn't write. In 3.x, a `#ref` into a branch that `noRelate`, `#delete` or `#unrelate` leave out failed with an error, sometimes after writing, and a `#ref{}` into such a branch wrote the `#ref{}` text itself, also for ids. A plain `#ref` to a model that `noInsert` skipped silently wrote `null`, and a `#ref{}` to its id wrote `null`, or failed after writing in a key that the relation sets too. See [migration](/release-notes/migration.md#ref-to-new-models-that-the-graph-doesn-t-write-throws) [#53](https://github.com/ditojs/objection/issues/53) -- `insertGraph()` and `upsertGraph()` write in fewer queries: rows are deleted with one query per relation, with all owners as `items` in the static hooks, instead of one query per owner. See [migration](/release-notes/migration.md#insertgraph-and-upsertgraph-write-in-fewer-queries) -- Custom relation classes that extend `Relation` directly declare where their keys are stored with a `keySide` getter (`'owner'`, `'related'` or `'join'`) to be used in `insertGraph()` and `upsertGraph()`. Relations that extend the built-in relation classes inherit it -- `insertGraph()` and `upsertGraph()` never return the `#id`, `#ref`, `#dbRef`, `#unrelate` and `#delete` markers, where 3.x returned them on models that the graph ignored or removed, and graph options with values of the wrong type throw also when no model of the graph reads them - `insertGraph()` and `upsertGraph()` keep only the branches below a model that `noInsert` skips that a `#ref` or `#ref{}` of a written model points into, directly or through other kept branches, and write no join rows for the skipped model. In 3.9.0, one reference kept the whole branch, so the referenced model's siblings were inserted without foreign keys, and its many-to-many relations got join rows without the skipped model's key, which failed on NOT NULL join columns. An existing row that the graph only places in such a pruned branch is now removed where its owner elsewhere leaves it out, like below a skipped model that nothing references, where 3.9.0 kept it whenever a reference kept the branch. See [migration](/release-notes/migration.md#insertgraph-and-upsertgraph-keep-only-the-referenced-branches-below-noinsert-models) [#369](https://github.com/ditojs/objection/issues/369) -- `upsertGraph()` gives a row that the graph places in several places the same keys at all of them, in any order of the places: the siblings, children and join rows of each place follow the key that one place writes, or the owner that the row moves to, instead of the key the row had. [#427](https://github.com/ditojs/objection/pull/427) -- `upsertGraph()` ignores a key that the graph gives a row but doesn't write, e.g. under `noUpdate` or at a `BelongsToOneRelation` `#ref` place: the rows related through it get the row's real key, where 3.x gave them the unwritten one. [#427](https://github.com/ditojs/objection/pull/427) -- `upsertGraph()` treats a row that lists itself in a relation on its own key, e.g. in its own `siblings`, as giving that relation no key, so the listing no longer conflicts with the key that the row's other places give: where 3.x threw a `ValidationError` of type `InvalidGraph`, such a graph is written, or throws the `NotFoundError` that asks for the `relate` option when a row it moves isn't related. A row that such a relation leaves out of its own list, e.g. a pet whose `siblings` name only the other pets, is removed from it, as in 3.x, also where the graph relates or moves the row, where 4.0.0-rc.2 could write its old key back after removing it. [#427](https://github.com/ditojs/objection/pull/427) -- `upsertGraph()` patches a key that the graph gives a row, where it changes the key or the row isn't loaded, before it writes the rows that it relates by that key, at the same place or another one, so a foreign key with `ON UPDATE CASCADE` on that column holds, where 3.x wrote these rows with the old key and relied on the cascade. For these graphs, the patch and its `$beforeUpdate` run before the inserts and `$beforeInsert` of these rows, and before the relates and patches of the rows that read the key. A patch that also waits for a key or `#ref{}` value of these rows throws a `ValidationError` of type `InvalidGraph` before anything is written. [#428](https://github.com/ditojs/objection/issues/428) -- The siblings and other relations that a row lists anywhere in the graph apply where `upsertGraph()` moves it, through a relate into a new owner, its own `BelongsToOneRelation` or a key that another place writes: its old siblings stay where they are, and `siblings: []` unrelates all other rows at the new owner. Rows keep their related rows when a key changes with them instead, e.g. through a `#ref{}`. [#427](https://github.com/ditojs/objection/pull/427) -- `upsertGraph()` throws a `ValidationError` of type `InvalidGraph` before anything is written when one place clears a key that another place places rows by, e.g. a row that its owner's list leaves out while its own `siblings` are placed at that owner, where 3.x and 4.0.0-rc.2 wrote these rows with `null`. See [migration](/release-notes/migration.md#insertgraph-and-upsertgraph-reject-rows-placed-in-conflicting-ways) -- `upsertGraph()` can't swap the rows of two `HasOneRelation` slots with a unique foreign key, e.g. two people's `pet`, which fails with a `UniqueViolationError`, also where one of the owners moves, which 4.0.0-rc.2 wrote in two passes - `insertGraph()` and `upsertGraph()` throw `the object graph contains cyclic references` before anything is written for cycles of `#ref{}` values, where 3.x wrote the unresolved `#ref{…}` text. [#427](https://github.com/ditojs/objection/pull/427) +- `insertGraph()` and `upsertGraph()` start the data paths of models in array input with their index in the array: the keys of `ValidationError`s, the `dataPath` of `NotFoundError`s and the `dataPath` option that validators get, e.g. `[1].pets[0].name` instead of `pets[0].name`, which 3.x gave for every model of the array. Graphs of a single object keep their paths. See [migration](/release-notes/migration.md#insertgraph-and-upsertgraph-errors-name-the-index-of-array-input) +- `insertGraph()` and `upsertGraph()` never return the `#id`, `#ref`, `#dbRef`, `#unrelate` and `#delete` markers, where 3.x returned them on models that the graph ignored or removed, and graph options with values of the wrong type throw also when no model of the graph reads them +- `upsertGraph()` doesn't patch existing models that the graph gives no changes, e.g. a model with only its id and an `#id`, or a related model whose BelongsToOne key stays the same, so their update hooks don't run, where 3.x patched them and ran the hooks. The same holds for a model whose changes `noUpdate` drops, where 3.9.0 ran its `$beforeUpdate` hook once without writing. The join rows of existing many-to-many models are patched only with the join table extras that change, and with none under `noUpdate`, where 3.x wrote all extras that the graph gives with every patch of the related model +- `upsertGraph()` gives a row that the graph places in several places the same keys at all of them, in any order of the places: the siblings, children and join rows of each place follow the key that one place writes, or the owner that the row moves to, instead of the key the row had. A key that the graph gives a row but doesn't write, e.g. under `noUpdate` or at a `BelongsToOneRelation` `#ref` place, is ignored: the rows related through it get the row's real key, where 3.x gave them the unwritten one. [#427](https://github.com/ditojs/objection/pull/427) +- `upsertGraph()` treats a row that lists itself in a relation on its own key, e.g. in its own `siblings`, as giving that relation no key, so the listing no longer conflicts with the key that the row's other places give: where 3.x threw a `ValidationError` of type `InvalidGraph`, such a graph is written, or throws the `NotFoundError` that asks for the `relate` option when a row it moves isn't related. A row that such a relation leaves out of its own list, e.g. a pet whose `siblings` name only the other pets, is removed from it, as in 3.x, also where the graph relates or moves the row. [#427](https://github.com/ditojs/objection/pull/427) +- `upsertGraph()` patches a key that the graph gives a row, where it changes the key or the row isn't loaded, before it writes the rows that it relates by that key, at the same place or another one, so a foreign key with `ON UPDATE CASCADE` on that column holds, where 3.x wrote these rows with the old key and relied on the cascade. For these graphs, the patch and its `$beforeUpdate` run before the inserts and `$beforeInsert` of these rows, and before the relates and patches of the rows that read the key. A patch that also waits for a key or `#ref{}` value of these rows throws a `ValidationError` of type `InvalidGraph` before anything is written. [#428](https://github.com/ditojs/objection/issues/428) - `upsertGraph()` writes the keys that it changes under `noUpdate` with `patch()` instead of `update()`, so `$beforeUpdate()` and `$afterUpdate()` get `patch: true` in their options for them, as for the patches of other models. They were already validated as patches. [#415](https://github.com/ditojs/objection/issues/415) +- `insertGraph()` and `upsertGraph()` write in fewer queries: rows are deleted with one query per relation instead of one per owner, with all owners as `items` in the static hooks. On SQLite, rows are inserted in batches too, where 3.x inserted one row per query there, so the static `beforeInsert` and `afterInsert` hooks run once per batch with several models. See [migration](/release-notes/migration.md#insertgraph-and-upsertgraph-write-in-fewer-queries) +- Custom relation classes that extend `Relation` directly declare where their keys are stored with a `keySide` getter (`'owner'`, `'related'` or `'join'`) to be used in `insertGraph()` and `upsertGraph()`. Relations that extend the built-in relation classes inherit it. See [migration](/release-notes/migration.md#custom-relations-that-extend-relation-declare-keyside) +- The deprecated `allowMarkers` option is removed from the `InsertGraphOptions` typings. It has no effect since 3.8.0, where `insertGraph()` rejects `#unrelate` and `#delete` markers. See [migration](/release-notes/migration.md#typescript-insertgraph-has-no-allowmarkers-option) +- The type parameter of `RelateQueryBuilder` is the number query builder, e.g. `RelateQueryBuilder>` in custom query builders that override `relate()`. See [migration](/release-notes/migration.md#typescript-relatequerybuilder-takes-the-number-query-builder) +- Custom query builders declare one `RebindType` member instead of the five `ArrayQueryBuilderType`, `SingleQueryBuilderType`, `MaybeSingleQueryBuilderType`, `NumberQueryBuilderType` and `PageQueryBuilderType` members, which are removed: `declare RebindType: MyQueryBuilder`, with the extra types `X` as third type parameter. The helper types `SingleQueryBuilder` etc. take a query builder, and `WithGraphModelQueryBuilder` and `ReturningMethod` are removed. See [migration](/release-notes/migration.md#typescript-custom-query-builders-declare-rebindtype) [#125](https://github.com/ditojs/objection/issues/125) [#319](https://github.com/Vincit/objection.js/issues/319) +- The internal helper types of the relation expression parser, the `withGraph*()` narrowing and the property names, like `ParseRelationString`, `WithGraph` and `RelationPropertyNames`, are no longer exported, and the unused `ItemType` is removed. See [migration](/release-notes/migration.md#typescript-internal-helper-types-are-no-longer-exported) +- `static jsonSchema` is typed `ReadonlyJSONSchema`, a JSON schema with readonly arrays, so that it takes schemas declared `as const`. A model's schema read through the static type can't be passed where a `JSONSchema` is expected, unless the model declares `static jsonSchema: JSONSchema`. See [migration](/release-notes/migration.md#typescript-model-jsonschema-is-a-readonly-schema) [#16](https://github.com/ditojs/objection/issues/16) +- `insert()`, `patch()`, `update()`, their `*AndFetch*()` and `*ById()` variants, `merge()` and `relate()` reject relation properties, which they ignore at runtime: they take a `PartialModelRow`, a model instance or its properties without relations, instead of `PartialModelObject`. Values whose type includes the relation properties are rejected too, e.g. `Partial`, `ModelObject` or `PartialModelObject` of models with relations. `insertGraph()` and `upsertGraph()` take relations. See [migration](/release-notes/migration.md#typescript-insert-patch-and-update-reject-relation-data) [#53](https://github.com/ditojs/objection/issues/53) +- `ModelObject`, `PartialModelObject` and the other property types leave out properties starting with `$`, which objection treats as internal, like `toJSON()` does. See [migration](/release-notes/migration.md#typescript-property-types-leave-out-properties) [#16](https://github.com/ditojs/objection/issues/16) +- `$relatedQuery()` of a to-one relation, e.g. `person.$relatedQuery('parent')`, resolves to `Model | undefined`, as it does at runtime when there is no related row. See [migration](/release-notes/migration.md#typescript-relatedquery-of-to-one-relations-may-resolve-to-undefined) [#2355](https://github.com/Vincit/objection.js/issues/2355) +- `toJSON()` and `$toJson()` return `ModelJson`: the properties without methods, and the related models as JSON too, instead of `ModelObject` with model instances. See [migration](/release-notes/migration.md#typescript-tojson-returns-modeljson) [#2380](https://github.com/Vincit/objection.js/issues/2380) +- The rows of `relate().returning()` are typed `Pojo[]` instead of `any[]`, as they are join rows, related rows or owner rows depending on the relation, not models of the query. Cast them to the row type you expect. See [migration](/release-notes/migration.md#typescript-relate-returning-rows-are-plain-objects) +- `distinctOn()` is typed to take column names, `raw()` and `ref()`, like `groupBy()`, instead of any selection, as it doesn't select its columns. ### Features -- `insert()` of several models works on SQLite (3.35 or newer) through `returning`, and on all databases when all models have their ids or the model has no `idColumn`, e.g. join rows. Rows that don't fit into one query, or that leave out columns that other rows have on SQLite, are inserted in several queries, which are only atomic in a transaction. `insertGraph()` and `upsertGraph()` insert in batches there too, where 3.x inserted one row per query on SQLite, so the static `beforeInsert` and `afterInsert` hooks run once per batch with several models +- `insert()` of several models works on SQLite (3.35 or newer) through `returning`, and on all databases when all models have their ids or the model has no `idColumn`, e.g. join rows. Rows that don't fit into one query, or that leave out columns that other rows have on SQLite, are inserted in several queries, which are only atomic in a transaction. - `BelongsToManyRelation`, a relation where the owner holds an array of the related models' keys, e.g. `posts.tagIds = [1, 4, 7]` for `post.tags`, in a JSON column on all databases or a native array column on PostgreSQL. It supports finding the related models with `$relatedQuery()`, `relatedQuery()`, `withGraphFetched()`, `withGraphJoined()` and `joinRelated()`. `insert()`, `delete()`, `relate()` and `unrelate()` through the relation rewrite the arrays, `insertGraph()` and `upsertGraph()` write them, and `$setRelated()` and `$appendRelated()` keep them in sync. The reads of the keys that these writes run call no find hooks. See [relations](/guide/relations.md) [#2667](https://github.com/Vincit/objection.js/issues/2667) - `Model.jsonColumnFormat` says how the database returns JSON columns: `'auto'` (default) leaves them as they are on PostgreSQL drivers and parses JSON text on others, `'text'` parses them strictly and throws for invalid JSON, `'parsed'` never parses them [#205](https://github.com/ditojs/objection/issues/205) - JSON field expressions work on MySQL and SQLite: in `ref()`, e.g. `ref('column:a.b').castInt()` in `select()`, `where()`, `orderBy()`, joins and relation mappings, as keys in `patch()` and `merge()`, e.g. `patch({ 'column:a.b': 1 })`, and in the `whereJson*()` methods, except for `whereJsonSupersetOf()` and `whereJsonSubsetOf()` on SQLite. objection writes the SQL of each database, `JSON_EXTRACT()` and `JSON_SET()` on MySQL and `json_extract()` and `json_set()` on SQLite, and casts to their types. `ref().castJson()` works on both too. See [JSON queries](/recipes/json-queries.md) [#276](https://github.com/Vincit/objection.js/issues/276) @@ -90,42 +85,41 @@ ### Fixes -- The error of `upsertGraph()` queries with other query builder calls names `$query()`, which adds such a call too [#1168](https://github.com/Vincit/objection.js/issues/1168) -- `upsertGraphAndFetch()` loads the relations of a `withGraphFetched()` on its query with their modifiers, named modifiers, aliases and options into the fetched models, like `insertGraphAndFetch()`, instead of ignoring the modifiers and aliases [#839](https://github.com/Vincit/objection.js/issues/839) - On SQLite, `insert()` stores arrays in columns that aren't `jsonAttributes` as JSON, like objects, instead of `'[object Object]'` -- `insertGraph()` and `upsertGraph()` resolve a `#ref{}` in a foreign key that the relation sets too, instead of failing with `replace is not a function` [#1378](https://github.com/Vincit/objection.js/issues/1378) -- `insertGraph()` and `upsertGraph()` insert several models without properties on PostgreSQL, instead of failing with "The query is empty" [#1694](https://github.com/Vincit/objection.js/issues/1694) -- `insertGraph()` and `upsertGraph()` fetch the keys that the database generates for inserted rows, like a uuid that isn't the id, when relations join through them, instead of writing join rows and foreign keys without them [#853](https://github.com/Vincit/objection.js/issues/853) -- `upsertGraph()` gives new models the keys of their existing parents that the graph leaves out, e.g. a `code` column that the relation joins through, instead of inserting them with NULL -- `insertGraph()` and `upsertGraph()` stay under the number of values that a query can bind on SQL Server, PostgreSQL and SQLite when they insert, patch and remove many rows, and fetch many roots or related models -- A many-to-many `patch()` / `update()` with join table extras and `limit()` on MySQL and SQL Server can order by columns of the join table [#161](https://github.com/ditojs/objection/issues/161) -- `asFindQuery()` in the static update hooks of a many-to-many `patch()` / `update()` with join table extras finds more related rows than SQL Server binds values in one statement, instead of failing after the rows were patched, as it did since 3.8.0. These patches filter the related rows by their ids in one statement instead of in batches of 500 values, bound as one JSON value on SQL Server and SQLite and as one array on PostgreSQL. See the breaking change above for binary and composite ids [#287](https://github.com/ditojs/objection/issues/287) -- A many-to-many `patch()` / `update()` with join table extras and `limit()` on MySQL and SQL Server writes the extras to the join rows of the related rows that the limit picks when the relation joins through a related column other than the id, instead of to the join rows that refer to other rows by these ids, as it did since 3.8.0 [#360](https://github.com/ditojs/objection/issues/360) +- `patch()` and `merge()` with field expression keys like `'meta:color'` keep writing them when the query is built more than once, e.g. with `toKnexQuery()` before running it, or through a `clone()`, instead of resolving to 0 without a write. Building the query no longer changes the data of `patch()` and `merge()`. The patched model keeps these keys until the write has run, so `$afterUpdate()` sees them, and then loses them, also with `snakeCaseMappers()`: the models that `$query().patch()`, `patchAndFetch*()` and `upsertGraph()` return don't have them [#53](https://github.com/ditojs/objection/issues/53) +- JSON paths are written into `NULL` JSON columns, by `patch()` and `merge()` with field expression keys and by `relate()` through a relation whose key is in a JSON path. They start from an empty object, or an empty array for a path that starts with an index, where they left the column `NULL` and wrote nothing. A `null` written this way gives `{ "key": null }`, e.g. the unrelate of a `BelongsToOneRelation` with its key in a JSON path. On PostgreSQL and MySQL, a nested path still doesn't create its missing parent levels, so the column becomes `{}` without the value, as for columns that hold `{}` +- A many-to-many `patch()` / `update()` with join table extras and `limit()` on MySQL and SQL Server can order by columns of the join table, and writes the extras to the join rows of the related rows that the limit picks when the relation joins through a related column other than the id, instead of to the join rows that refer to other rows by these ids, as it did since 3.8.0 [#161](https://github.com/ditojs/objection/issues/161) [#360](https://github.com/ditojs/objection/issues/360) +- `asFindQuery()` in the static update hooks of a many-to-many `patch()` / `update()` with join table extras finds more related rows than SQL Server binds values in one statement, instead of failing after the rows were patched, as it did since 3.8.0. The ids of the related rows are bound as one JSON value on SQL Server and SQLite and as one array on PostgreSQL, see the breaking change above for binary and composite ids [#287](https://github.com/ditojs/objection/issues/287) - `cancelQuery()` with a single row in the `beforeInsert` hook of an `insert()` merges it into the inserted model, like an array of rows, instead of ignoring it [#66](https://github.com/ditojs/objection/issues/66) -- `insertGraph()` and `upsertGraph()` reject only after the queries they already started have finished, so a transaction is rolled back after them. Before, rows of these queries could be committed after the rollback on SQLite. [#335](https://github.com/ditojs/objection/pull/335) -- `upsertGraph()` sets the foreign key of a model that a `#ref` relates into a `HasManyRelation` or `HasOneRelation`. Before, the row was left without it, and the id of the owner could be cleared [#311](https://github.com/ditojs/objection/issues/311) -- `upsertGraph()` no longer exposes the `objectiontmpjoin*` aliases of the owner join columns on the many-to-many related models of the graphs that it loads, e.g. in the `items` of the static hooks of its deletes, as 3.9.0 did [#370](https://github.com/ditojs/objection/issues/370) -- `patch()` and `merge()` with field expression keys like `'meta:color'` keep writing them when the query is built more than once, e.g. with `toKnexQuery()` before running it, or through a `clone()`, instead of resolving to 0 without a write. Building the query no longer changes the data of `patch()` and `merge()`: the patched model keeps these keys until the write has run, so `$afterUpdate()` sees them, and then loses them, also with `snakeCaseMappers()`, so the models that `$query().patch()`, `patchAndFetch*()` and `upsertGraph()` return don't have them [#53](https://github.com/ditojs/objection/issues/53) +- `insertGraph()` and `upsertGraph()` reject only after the queries they already started have finished, so a transaction is rolled back after them. In 3.x, rows of these queries could be committed after the rollback on SQLite. [#335](https://github.com/ditojs/objection/pull/335) +- `upsertGraph()` writes the foreign keys that it sets or clears in a JSON path of a column, e.g. `join: { from: 'person.id', to: 'pet.data:sitterId' }`, as path updates that keep the other values of the column, also with `noUpdate` and `update`, where it wrote the whole column with only the keys in it and lost the rest. A column that the graph gives other values than the keys, or whose key sits below its top level, e.g. `json:stuff.favoritePetId`, is still written whole. The hooks of such a write see the keys as field expression properties like `'pet.data:sitterId'`, and a change of the hooks to the column itself isn't written. [#415](https://github.com/ditojs/objection/issues/415) +- `insertGraph()` and `upsertGraph()` fetch the keys that the database generates for inserted rows, like a uuid that isn't the id, when relations join through them, instead of writing join rows and foreign keys without them [#853](https://github.com/Vincit/objection.js/issues/853) +- `insertGraph()` and `upsertGraph()` resolve a `#ref{}` in a foreign key that the relation sets too, instead of failing with `replace is not a function` [#1378](https://github.com/Vincit/objection.js/issues/1378) +- `upsertGraph()` sets the foreign key of a model that a `#ref` relates into a `HasManyRelation` or `HasOneRelation`, where 3.x left the row without it and could clear the id of the owner [#311](https://github.com/ditojs/objection/issues/311) - `upsertGraph()` with `relate` and `noUpdate` writes the foreign keys of models that it relates through a `HasManyRelation` or `HasOneRelation`, like it does for `BelongsToOneRelation` and `ManyToManyRelation`, instead of leaving them unrelated without an error, as it did since 1.5.0 [#53](https://github.com/ditojs/objection/issues/53) - `upsertGraph()` resolves a `#ref{}` to a value of an existing model, e.g. a foreign key that an inserted related model gives it, once the graph has filled it in, instead of reading its old value [#356](https://github.com/ditojs/objection/issues/356) +- `upsertGraph()` gives new models the keys of their existing parents that the graph leaves out, e.g. a `code` column that the relation joins through, instead of inserting them with NULL - `upsertGraph()` with the `unrelate` option moves a has-many row that the graph relates to another owner through the same relation without unrelating it first, which failed on a NOT NULL foreign key when the new owner is inserted [#214](https://github.com/ditojs/objection/issues/214) - `upsertGraph()` keeps a row that the graph keeps elsewhere when it replaces the row as the `BelongsToOneRelation` model of another one, e.g. a root whose new boss is the model it was the boss of: `{ id: 2, boss: { id: 1, boss: { id: 3 } } }` with 2 as the boss of 1, where 3.x deleted the root - `upsertGraph()` keeps the join table extras of an existing join row when a `#ref` relates the many-to-many model again, where 3.x set them to `null` - `upsertGraph()` inserts a new row into a `HasOneRelation` after the current row moves out of it to a new owner, and after the rows that move along with it from slot to slot, instead of failing with a `UniqueViolationError` on a unique foreign key. A current row that moves out below a row that moves to another owner is related to its new slot before a row goes into its old one [#368](https://github.com/ditojs/objection/issues/368) [#398](https://github.com/ditojs/objection/issues/398) -- The models that `upsertGraph()` returns write all their props in later patches, e.g. with `$query().patch()`, instead of leaving out the props that the upsert found unchanged, as they did since 3.9.0 [#371](https://github.com/ditojs/objection/issues/371) -- `upsertGraph()` patches rows whose foreign keys point to each other one after the other, e.g. a row and the model that it relates both as its `BelongsToOneRelation` and its `HasOneRelation` model, instead of at the same time, which could fail with `ER_LOCK_DEADLOCK` on MySQL outside of transactions, as it could in 3.9.0 [#385](https://github.com/ditojs/objection/issues/385) +- `upsertGraph()` keeps a row that a HasMany or HasOne relation leaves out when another place of the row sets the same key column: a relation on that column, e.g. pet 4 in `{ id: 2, pets: [{ id: 3 }, { id: 4, siblings: [] }] }` with `siblings` joined on `ownerId`, or the row's own `BelongsToOneRelation`, e.g. person 2 in `{ id: 2, parent: { id: 1, children: [] } }`, where 3.x unrelated it +- `upsertGraph()` keeps a new row that one place inserts into a relation when a row that the graph moves leaves it out of a relation on the same key column, e.g. the new pet in `{ name: 'new', pets: [{ id: 10, siblings: [{ id: 10 }] }, { name: 'Bo' }] }` with `siblings` joined on `ownerId`, where 3.x deleted it with the relations of the moved row +- `upsertGraph()` writes the relations of a row that the graph gives in several places the same way whatever their order, e.g. it removes person 3 from `children` in `[{ id: 1, children: [{ id: 2 }] }, { id: 1, name: 'a' }]`, as it does with the places swapped or with the first place alone, where 3.x kept the person depending on the order - `upsertGraph()` writes a model once when a `#ref` places it where the graph places the model itself, e.g. `pets: [{ '#id': 'p', name: 'new' }, { '#ref': 'p' }]`, where 3.x patched it again after writing it, which ran its update hooks - `upsertGraph()` writes the props of a model marked with `#unrelate` through a `#ref` to it in another place, e.g. `{ id: 1, boss: { name: 'new', movies: [{ id: 3, '#id': 'm', title: 't', '#unrelate': true }] }, movies: [{ '#ref': 'm' }] }` writes the title, where 3.9.0 dropped the props, also those of the `#ref` - `insertGraph()` and `upsertGraph()` write only the join table extras that a many-to-many `#ref` gives itself to its join row, and never write the extras of the join row of the model it refers to into other rows, e.g. `[{ name: 'a', movies: [{ '#ref': 'm' }] }, { name: 'b', movies: [{ '#id': 'm', title: 't', role: 'lead' }] }]` gives the join row of `a` no role, where 3.x copied `role: 'lead'` to it, and a `#ref` in a HasMany relation no longer fails with a missing `role` column +- The models that `upsertGraph()` returns write all their props in later patches, e.g. with `$query().patch()`, instead of leaving out the props that the upsert found unchanged, as they did since 3.9.0 [#371](https://github.com/ditojs/objection/issues/371) +- `upsertGraph()` patches rows whose foreign keys point to each other one after the other, e.g. a row and the model that it relates both as its `BelongsToOneRelation` and its `HasOneRelation` model, instead of at the same time, which could fail with `ER_LOCK_DEADLOCK` on MySQL outside of transactions, as it could in 3.9.0 [#385](https://github.com/ditojs/objection/issues/385) +- `upsertGraph()` no longer exposes the `objectiontmpjoin*` aliases of the owner join columns on the many-to-many related models of the graphs that it loads, e.g. in the `items` of the static hooks of its deletes, as 3.9.0 did [#370](https://github.com/ditojs/objection/issues/370) - `upsertGraph()` on SQLite throws for `BigInt` ids, which the SQLite driver binds as `NULL`, where 3.x wrote `NULL` keys or patched nothing - `upsertGraph()` throws a `ValidationError` of type `InvalidGraph` before anything is written for a graph that changes a key that is part of a row's id, e.g. with `idColumn = ['ownerId', 'name']`, by relating the row to another owner, or by pointing its own `BelongsToOneRelation` to another row or clearing it with `null`, `#unrelate` or `#delete`. 3.9.0 wrote nothing and returned the model as changed, or threw a `NotFoundError` for its relations -- `upsertGraph()` keeps a row that a HasMany or HasOne relation leaves out when another place of the row sets the same key column: a relation on that column, e.g. pet 4 in `{ id: 2, pets: [{ id: 3 }, { id: 4, siblings: [] }] }` with `siblings` joined on `ownerId`, or the row's own `BelongsToOneRelation`, e.g. person 2 in `{ id: 2, parent: { id: 1, children: [] } }`, where 3.x unrelated it -- `upsertGraph()` keeps a new row that one place inserts into a relation when a row that the graph moves leaves it out of a relation on the same key column, e.g. the new pet in `{ name: 'new', pets: [{ id: 10, siblings: [{ id: 10 }] }, { name: 'Bo' }] }` with `siblings` joined on `ownerId`, where 3.x deleted it with the relations of the moved row -- `upsertGraph()` writes the relations of a row that the graph gives in several places the same way whatever their order, e.g. it removes person 3 from `children` in `[{ id: 1, children: [{ id: 2 }] }, { id: 1, name: 'a' }]`, as it does with the places swapped or with the first place alone, where 3.x kept the person depending on the order -- `patch()` and `merge()` with field expression keys like `'meta:color'`, and `relate()` through a relation whose key is in a JSON path, write into a `NULL` JSON column, starting from an empty object, or an empty array for a path that starts with an index, where they left it `NULL` and wrote nothing. A `null` written this way gives `{ "key": null }`, e.g. the unrelate of a `BelongsToOneRelation` with its key in a JSON path. On PostgreSQL and MySQL, a nested path still doesn't create its missing parent levels, so the column becomes `{}` without the value, as for columns that hold `{}` -- `upsertGraph()` writes the foreign keys that it sets or clears in a JSON path of a column, e.g. `join: { from: 'person.id', to: 'pet.data:sitterId' }`, as path updates that keep the other values of the column, also with `noUpdate` and `update`, where it wrote the whole column with only the keys in it and lost the rest. A column that the graph gives other values than the keys, or whose key sits below its top level, e.g. `json:stuff.favoritePetId`, is still written whole. The hooks of such a write see the keys as field expression properties like `'pet.data:sitterId'`, and a change of the hooks to the column itself isn't written. [#415](https://github.com/ditojs/objection/issues/415) -- `insertGraph()` and `upsertGraph()` throw a `ValidationError` of type `InvalidGraph` for a `#ref` at the root of a graph, which has no relation to place its model in, where they failed with a `TypeError` -- `upsertGraph()` relates a `BelongsToOneRelation` model that only gives the relation's key, e.g. `pet: { code: 'c1' }` with `relate`, by writing the key to its owner without looking up its row, as with `noUpdate`, and throws a `ValidationError` of type `InvalidGraph` before anything is written when such a model gives other props that it would write, relations, or an `#id` that others refer to. Before, a model like this failed with an error about a missing id unless `noUpdate` was given, also in 3.x, and with `noUpdate`, its relations, e.g. `{ code: 'c1', parent: { id: 2 } }`, were silently left out +- `insertGraph()` and `upsertGraph()` throw a `ValidationError` of type `InvalidGraph` for a `#ref` at the root of a graph, which has no relation to place its model in, where 3.x failed with a `TypeError` +- `upsertGraph()` relates a `BelongsToOneRelation` model that only gives the relation's key, e.g. `pet: { code: 'c1' }` with `relate`, by writing the key to its owner without looking up its row, as with `noUpdate`, and throws a `ValidationError` of type `InvalidGraph` before anything is written when such a model gives other props that it would write, relations, or an `#id` that others refer to. 3.x failed for such a model unless `noUpdate` was given, and with `noUpdate` inserted it as a new row when it gave relations, e.g. `{ code: 'c1', parent: { id: 2 } }` +- `insertGraph()` and `upsertGraph()` insert several models without properties on PostgreSQL, instead of failing with "The query is empty" [#1694](https://github.com/Vincit/objection.js/issues/1694) +- `insertGraph()` and `upsertGraph()` stay under the number of values that a query can bind on SQL Server, PostgreSQL and SQLite when they insert, patch and remove many rows, and fetch many roots or related models +- `upsertGraphAndFetch()` loads the relations of a `withGraphFetched()` on its query with their modifiers, named modifiers, aliases and options into the fetched models, like `insertGraphAndFetch()`, instead of ignoring the modifiers and aliases [#839](https://github.com/Vincit/objection.js/issues/839) +- The error of `upsertGraph()` queries with other query builder calls names `$query()`, which adds such a call too [#1168](https://github.com/Vincit/objection.js/issues/1168) ### Types diff --git a/doc/release-notes/migration.md b/doc/release-notes/migration.md index b1fa8e1a2..9d53db3ca 100644 --- a/doc/release-notes/migration.md +++ b/doc/release-notes/migration.md @@ -7,6 +7,7 @@ This section lists the breaking changes in objection 4.0. - [Dropped support for knex < 3.3](#dropped-support-for-knex-3-3) - [Dropped support for TypeScript < 5.8](#dropped-support-for-typescript-5-8) - [Dropped support for MySQL < 8](#dropped-support-for-mysql-8) +- [Many-to-many patches with join table extras run in one query](#many-to-many-patches-with-join-table-extras-run-in-one-query) - [Database errors are objection's own classes](#database-errors-are-objection-s-own-classes) - [Model files given as paths](#model-files-given-as-paths) - [skipUndefined() is removed](#skipundefined-is-removed) @@ -23,6 +24,7 @@ This section lists the breaking changes in objection 4.0. - [upsertGraph() relates moved rows before it deletes](#upsertgraph-relates-moved-rows-before-it-deletes) - [upsertGraph() upserts below related models in the same graph](#upsertgraph-upserts-below-related-models-in-the-same-graph) - [insertGraph() and upsertGraph() write in fewer queries](#insertgraph-and-upsertgraph-write-in-fewer-queries) +- [Custom relations that extend Relation declare keySide](#custom-relations-that-extend-relation-declare-keyside) - [insertGraph() and upsertGraph() reject rows placed in conflicting ways](#insertgraph-and-upsertgraph-reject-rows-placed-in-conflicting-ways) - [upsertGraph() checks that related models exist](#upsertgraph-checks-that-related-models-exist) - [insertGraph() and upsertGraph() errors name the index of array input](#insertgraph-and-upsertgraph-errors-name-the-index-of-array-input) @@ -199,6 +201,8 @@ A row related from another owner can no longer take the unique key of a row that A new row can't take the unique key of a row that moves to a new owner either: the move waits for the insert of the new owner, so the row still holds its key when the new row is inserted. 3.x cleared the moved row's foreign key first, which fails for `NOT NULL` keys. Insert the new owner with a separate call first, or make the unique constraint deferred and run `upsertGraph()` in a transaction. +Swapping the rows of two `HasOneRelation` slots with a unique foreign key, e.g. `[{ id: 1, pet: { id: 2 } }, { id: 2, pet: { id: 1 } }]`, fails with a `UniqueViolationError` too, as each row is related while the other one still holds the key. 3.x with `unrelate` unrelated both rows first. Unrelate one of the rows with a separate query in the same transaction first. + ## upsertGraph() upserts below related models in the same graph 3.x upserted the models below a related model with a nested `upsertGraph()` call for each owner and relation, after all other writes of the graph. 4.0 fetches the rows of the related models with their relations together with the rest of the graph, and writes them in the same steps, so their deletes, inserts and patches run with the others of their kind instead of last. The options keep matching the full relation paths. @@ -249,7 +253,7 @@ await Person.query().insertGraph([{ firstName: 'Jennifer' }, { pets: [{ name: 1 ## `#ref` to new models that the graph doesn't write throws -A `#ref` to a new model, or a `#ref{}` to the id of a new model, that the graph doesn't write throws a `ValidationError` of type `InvalidGraph` before anything is written, e.g. for a model that `noInsert` skips, or below a model that `noRelate` ignores or that is marked with `#delete` or `#unrelate`. For references of models below rows that `upsertGraph()` moves into a `HasManyRelation` or `HasOneRelation`, it's thrown once the rest of the graph is written, before these models are. Point the reference to a model that the graph writes, or give the referenced model its id. Other `#ref{}` read the models as the graph gives them, also in branches that the graph doesn't write. In 3.x, a `#ref` into a branch that `noRelate`, `#delete` or `#unrelate` leave out failed with an error, sometimes after writing, and a `#ref{}` into such a branch wrote the `#ref{}` text itself, also for ids. A plain `#ref` to a model that `noInsert` skipped silently wrote `null`, and a `#ref{}` to its id wrote `null`, or failed after writing in a key that the relation sets too. See [the guide](/guide/query-examples.md#graph-inserts) for the rule. +A `#ref` to a new model, or a `#ref{}` to the id of a new model, that the graph doesn't write throws a `ValidationError` of type `InvalidGraph` before anything is written, e.g. for a model that `noInsert` skips, or below a model that `noRelate` ignores or that is marked with `#delete` or `#unrelate`. Point the reference to a model that the graph writes, or give the referenced model its id. Other `#ref{}` read the models as the graph gives them, also in branches that the graph doesn't write. In 3.x, a `#ref` into a branch that `noRelate`, `#delete` or `#unrelate` leave out failed with an error, sometimes after writing, and a `#ref{}` into such a branch wrote the `#ref{}` text itself, also for ids. A plain `#ref` to a model that `noInsert` skipped silently wrote `null`, and a `#ref{}` to its id wrote `null`, or failed after writing in a key that the relation sets too. See [the guide](/guide/query-examples.md#graph-inserts) for the rule. ## insertGraph() and upsertGraph() keep only the referenced branches below noInsert models @@ -281,6 +285,20 @@ To keep such a row, place it where the graph writes it, or use `noDelete` for it Rows that the graph deletes are deleted with one query per relation instead of one per owner, so the static `beforeDelete` and `afterDelete` hooks run once with all owners as `items`. Join rows and unrelated rows are still removed per owner. +On SQLite, rows that the graph inserts are inserted in batches, where 3.x inserted one row per query, so the static `beforeInsert` and `afterInsert` hooks get several models at once. + +## Custom relations that extend Relation declare keySide + +`insertGraph()` and `upsertGraph()` read where the keys of a relation are stored from its `keySide` getter: `'owner'` for keys in the owner rows, like `BelongsToOneRelation`, `'related'` for keys in the related rows, like `HasManyRelation`, and `'join'` for keys in the rows of a join table, like `ManyToManyRelation`. Relation classes that extend the built-in relations inherit it. Classes that extend `Relation` directly need to declare it, or graph writes through them throw `not implemented`: + +```js +class MyRelation extends Relation { + get keySide() { + return 'related'; + } +} +``` + ## typescript: The typings are ES module declarations The typings no longer use `export =`. They declare the named exports of the ES module they describe. Apart from the default import, ES module and bundler projects need no changes. @@ -312,7 +330,7 @@ class Person extends Model { The static `after*` hooks run as the last step of the query, after `first()`, `page()`'s models and the fetches of the `*AndFetch()` methods, but before the `runAfter()` callbacks, including the ones in `context()`, `throwIfNotFound()` and `traverse()`. These callbacks now get the result in the shape that the query returns and after the hooks, e.g. the model with `first()` also if `runAfter()` is called before `first()`, and they run before `page()` and `range()` wrap the result in `{ results, total }`. -The outer queries of `upsertGraph()` and `upsertGraphAndFetch()` no longer call the static `beforeFind` and `afterFind` hooks of the root model class with the input models. The queries that run for the models of the graph and their relations call the hooks as before. Likewise, `fetchGraph()` / `$fetchGraph()` and the relations that `skipFetched` skips no longer call the static `beforeFind` hook of the models they already have, as they didn't call `afterFind`: the find hooks are only called for queries that run. +The outer queries of `upsertGraph()` and `upsertGraphAndFetch()` no longer call the static `beforeFind` and `afterFind` hooks of the root model class with the input models, nor `$afterFind` on the input models that they return. The queries that run for the models of the graph and their relations call the hooks as before. Likewise, `fetchGraph()` / `$fetchGraph()` and the relations that `skipFetched` skips no longer call the static `beforeFind` hook of the models they already have, as they didn't call `afterFind`: the find hooks are only called for queries that run. TypeScript: `StaticHookArguments` declares `result: R`, with `R = undefined` by default, and the static hooks of `Model` declare the shapes that they get, e.g. `StaticHookArguments` for `afterFind`. Overrides that destructure `result` from `StaticHookArguments` need the type of the result as second type argument, e.g. `StaticHookArguments`.