json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -0,0 +1,3827 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Migrating to v2"
|
|
3
|
+
chapter: 33
|
|
4
|
+
chapter_label: "33"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 33. Migrating to v2
|
|
8
|
+
|
|
9
|
+
Version 2 changes setup, resource calls and transaction ownership. Upgrade each
|
|
10
|
+
application's source and dependency versions together. Old option spellings are
|
|
11
|
+
rejected; there is no compatibility mode. This guide describes the required port;
|
|
12
|
+
it does not imply that any consuming application or seed generator is migrated.
|
|
13
|
+
Before v2 is published, install the intended tarball or pin an exact reviewed Git
|
|
14
|
+
commit in place of the registry package. Verify the installed artifact; changing
|
|
15
|
+
the version declaration alone does not migrate an application.
|
|
16
|
+
|
|
17
|
+
## Replace the API host
|
|
18
|
+
|
|
19
|
+
Remove the direct `hooked-api` dependency from the application:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm uninstall hooked-api
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Import the constructor from this package:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { JsonRestApi, RestApiPlugin, RestApiKnexPlugin } from 'json-rest-api'
|
|
29
|
+
|
|
30
|
+
const api = new JsonRestApi({ name: 'app', logger: applicationLogger })
|
|
31
|
+
await api.use(RestApiPlugin, { format: 'jsonapi' })
|
|
32
|
+
await api.use(RestApiKnexPlugin, { knex })
|
|
33
|
+
await api.addResource('books', bookOptions)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`api.resources.books.get(...)`, the other resource methods and `api.transaction(...)`
|
|
37
|
+
keep their documented call shapes. Update the setup and extension code:
|
|
38
|
+
|
|
39
|
+
| Previous API | Replacement |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `Api` imported from `hooked-api` | `JsonRestApi` imported from `json-rest-api` |
|
|
42
|
+
| `api.scopes` | `api.resources` |
|
|
43
|
+
| `api.addScope(...)` | `api.addResource(...)` |
|
|
44
|
+
| Injected `addScopeMethod` | Injected `addResourceMethod` |
|
|
45
|
+
| Setup hook `scope:added` | `resource:added` |
|
|
46
|
+
| Resource option `scopeMethods` | Resource option `methods` |
|
|
47
|
+
| `customize({ scopeMethods })` | `customize({ methods })` |
|
|
48
|
+
| Dependency alternative `'a\|b'` | `dependencies: [['a', 'b']]` |
|
|
49
|
+
| Constructor `log` / `logging` options | Injected `logger` object |
|
|
50
|
+
| `HookedApiError` base class | `RestApiError` from `json-rest-api` |
|
|
51
|
+
|
|
52
|
+
Logging is silent by default. Supply the logger's level methods directly; configure
|
|
53
|
+
formatting and filtering in that logger. Plugin installation preserves the original
|
|
54
|
+
error, so stop matching hooked-api's wrapping message. There is no compatibility mode.
|
|
55
|
+
Generic scope aliases, `customize({ apiMethods })` and method-registration lifecycle
|
|
56
|
+
hooks have been removed. Install API methods through a plugin's `addApiMethod` instead.
|
|
57
|
+
|
|
58
|
+
Hooks support named `beforeFunction` or `afterFunction` placement, with one placement
|
|
59
|
+
per handler. Numeric ordering and per-plugin placement are unsupported. Returning
|
|
60
|
+
`false` stops that hook list; throw an appropriate error to reject a request.
|
|
61
|
+
|
|
62
|
+
Resources are ordinary objects. Call shared methods with their resource receiver;
|
|
63
|
+
use `api.resources.books.get.bind(api.resources.books)` if you need a detached callback.
|
|
64
|
+
Resource vars and helpers inherit live API defaults. Local writes override them;
|
|
65
|
+
deleting an override reveals the default again. `Object.keys(resource.vars)` lists
|
|
66
|
+
local values only, rather than combining inherited defaults.
|
|
67
|
+
|
|
68
|
+
Await plugin and resource setup sequentially. Discard an instance after a setup failure;
|
|
69
|
+
plugin installation is not transactional. Finish setup before serving requests.
|
|
70
|
+
|
|
71
|
+
TypeScript applications can keep an explicit `JsonRestApi` resource map using the
|
|
72
|
+
existing resource, relationship and bulk method interfaces. Ordinary literal
|
|
73
|
+
schemas also infer attribute types on the handle returned by `addResource()`;
|
|
74
|
+
see [resource and hook typing](../API.md#typescript-resources-and-hooks).
|
|
75
|
+
|
|
76
|
+
## Remove the S3 demo adapter
|
|
77
|
+
|
|
78
|
+
`S3Storage` and `S3StorageOptions` have been removed, including the direct
|
|
79
|
+
`plugins/storage/s3-storage.js` import. The old adapter generated mock URLs;
|
|
80
|
+
it did not upload to S3. There is no replacement export or compatibility mode.
|
|
81
|
+
|
|
82
|
+
Use the application's existing storage integration through the `FileStorage`
|
|
83
|
+
`upload`/`delete` contract, or use `LocalStorage` for local files. Remote-provider
|
|
84
|
+
credentials, clients and storage policy belong to the application. See
|
|
85
|
+
[file handling](26-file-uploads.md) for the adapter contract.
|
|
86
|
+
|
|
87
|
+
## Start with the common changes
|
|
88
|
+
|
|
89
|
+
For an ordinary repository/app migration, work through these first:
|
|
90
|
+
|
|
91
|
+
1. [Replace the API host](#replace-the-api-host), then [update Node](#runtime-requirement) and [rename the two response options](#rename-the-two-response-options).
|
|
92
|
+
2. [Replace `inputRecord` with `data` or `document`](#replace-inputrecord-with-data-or-document) and
|
|
93
|
+
[select input and output independently](#port-calls-that-mixed-jsonapi-input-with-plain-output).
|
|
94
|
+
3. [Update configuration and consumers together](#update-configuration-and-consumers-together),
|
|
95
|
+
then check [result shapes](#result-shapes) and [request context](#arguments-and-request-context).
|
|
96
|
+
4. Review [relationship writes](#review-relationship-writes) and
|
|
97
|
+
[transaction/error handling](#transactions-and-errors) where your app uses them.
|
|
98
|
+
5. [Verify the port](#verify-the-port) against the intended package set.
|
|
99
|
+
|
|
100
|
+
The remaining sections are feature-specific references. In particular, check
|
|
101
|
+
[storage naming](#storage-naming-configuration), [schema migration helpers](#schema-migration-helpers)
|
|
102
|
+
and [temporal storage](#temporal-values-and-storage-serializers) before changing
|
|
103
|
+
stored data. Connector/plugin sections apply only to the integrations your app uses.
|
|
104
|
+
|
|
105
|
+
| If your app customizes… | Review |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| Stored values or table names | [Structured attributes](#object-and-array-attributes), [temporal values](#temporal-values-and-storage-serializers), [schema helpers](#schema-migration-helpers), [storage naming](#storage-naming-configuration) |
|
|
108
|
+
| Queries and output | [Fieldsets](#sparse-fieldsets-include-relationships), [sort fields](#sort-fields-and-defaults), [pagination](#pagination-modes-and-counts), [includes](#full-includes-and-projected-fields) |
|
|
109
|
+
| Hooks and plugins | [Read failures](#setter-getter-and-computed-field-failures), [schema enrichment](#schema-enrichment-and-canonical-registration), [write responses](#write-hooks-and-the-returned-response), [query builders](#native-query-builders-and-explicit-custom-filter-translation) |
|
|
110
|
+
| Transports and files | [CORS](#cors-and-http-response-hooks), [uploads](#multipart-uploads), [Socket.IO](#socketio-startup-and-shutdown), [bulk writes](#bulk-writes) |
|
|
111
|
+
| Logging | [Logger enhancement](#enhancing-an-existing-logger), [error causes](#error-causes-in-diagnostic-output), [field redaction](#field-aware-write-diagnostics) |
|
|
112
|
+
| Conditional writes | [Stored revisions](#opt-in-stored-revision-migration), [HTTP conditions](#opt-in-http-representation-conditions) |
|
|
113
|
+
|
|
114
|
+
## Runtime requirement
|
|
115
|
+
|
|
116
|
+
Use Node 24 or newer and reinstall dependencies after upgrading Node so native
|
|
117
|
+
database modules match its ABI. This library's development and CI checks run on
|
|
118
|
+
Node 24 only; `.nvmrc` pins 24.6.0. Update application runtime configuration when
|
|
119
|
+
performing its migration.
|
|
120
|
+
|
|
121
|
+
## Declare searchable relationship fields explicitly
|
|
122
|
+
|
|
123
|
+
The library no longer guesses that a resource is a pivot table from its number
|
|
124
|
+
of belongs-to fields. That heuristic used to enable search automatically, even
|
|
125
|
+
overriding `search: false`.
|
|
126
|
+
|
|
127
|
+
If an application filters a pivot resource by a relationship, declare the filter
|
|
128
|
+
on the field or in `searchSchema`, just as for any other resource:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
schema: {
|
|
132
|
+
groupId: { type: 'id', belongsTo: 'groups', as: 'group', search: true },
|
|
133
|
+
itemId: { type: 'id', belongsTo: 'items', as: 'item' }
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Here `filters: { group: groupId }` is supported. Filtering by `item` is rejected
|
|
138
|
+
unless separately declared. Omitting `search` or setting it to `false` does not
|
|
139
|
+
generate a filter; an explicit `searchSchema` entry or schema enrichment hook
|
|
140
|
+
can still declare one. Relationship add, replace, remove, related-resource reads
|
|
141
|
+
and includes work without enabling public search on pivot fields.
|
|
142
|
+
|
|
143
|
+
No database migration is needed. Review application calls that directly filter
|
|
144
|
+
pivot resources and add only the search declarations those calls require.
|
|
145
|
+
|
|
146
|
+
## What stays familiar
|
|
147
|
+
|
|
148
|
+
Keep resource declarations and calls through `api.resources.books`. The familiar
|
|
149
|
+
`get`, `query`, `post`, `put`, `patch`, and `delete` operations remain. JSON:API
|
|
150
|
+
documents and plain records both remain useful formats. This migration does not
|
|
151
|
+
require replacing ordinary repository code with a new ORM.
|
|
152
|
+
|
|
153
|
+
## Rename the two response options
|
|
154
|
+
|
|
155
|
+
| Old option | Replacement |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `simplified: true` | `format: 'plain'` |
|
|
158
|
+
| `simplified: false` | `format: 'jsonapi'` |
|
|
159
|
+
| `returnFullRecord: 'full'` or `true` | `returning: 'full'` |
|
|
160
|
+
| `returnFullRecord: 'minimal'` | `returning: 'minimal'` |
|
|
161
|
+
| `returnFullRecord: 'no'` or `false` | `returning: 'none'` |
|
|
162
|
+
|
|
163
|
+
`format` chooses output representation. `returning` chooses how much a write returns.
|
|
164
|
+
Boolean aliases and the old option names are removed, not deprecated aliases.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// Before
|
|
168
|
+
const book = await api.resources.books.get({
|
|
169
|
+
id: '42',
|
|
170
|
+
simplified: true
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// After
|
|
174
|
+
const book = await api.resources.books.get({
|
|
175
|
+
id: '42',
|
|
176
|
+
format: 'plain'
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
For a JSON:API response, use `format: 'jsonapi'`. Keep the existing `queryParams`
|
|
181
|
+
container for filters, sort, sparse fields, includes, and pagination unless a
|
|
182
|
+
separately documented query-contract change is made.
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
const document = await api.resources.books.query({
|
|
186
|
+
format: 'jsonapi',
|
|
187
|
+
queryParams: { fields: { books: 'title' }, page: { size: 20 } }
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Replace inputRecord with data or document
|
|
192
|
+
|
|
193
|
+
Resource POST/PUT/PATCH accept exactly one input key: `data` for plain resource
|
|
194
|
+
values, or `document` for a JSON:API document. The old `inputRecord` parameter is
|
|
195
|
+
removed. Both keys together are rejected, including a key set to `undefined`.
|
|
196
|
+
Fields inside `data` are resource values; options outside it control the operation.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
// Before: plain resource values were inside inputRecord
|
|
200
|
+
await api.resources.books.post({
|
|
201
|
+
inputRecord: { title: 'Dune' },
|
|
202
|
+
format: 'plain',
|
|
203
|
+
returning: 'full'
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// After: plain input remains convenient and has a clear boundary
|
|
207
|
+
await api.resources.books.post({
|
|
208
|
+
data: { title: 'Dune' },
|
|
209
|
+
format: 'plain',
|
|
210
|
+
returning: 'full'
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Fields named `format`, `returning`, `queryParams`, `document`, or `data` belong
|
|
215
|
+
inside `data`; they are ordinary data there. Plain input is never autodetected as
|
|
216
|
+
a JSON:API document. `data.id` is the record identity: on PATCH/PUT it must
|
|
217
|
+
match the target `id`, and cannot redirect the operation to another record. Keep the target ID outside the
|
|
218
|
+
patch data:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
await api.resources.books.patch({
|
|
222
|
+
id: '42',
|
|
223
|
+
data: { title: 'Updated title' },
|
|
224
|
+
format: 'plain',
|
|
225
|
+
returning: 'none'
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
When using JSON:API input, rename the parameter to `document` and retain its
|
|
230
|
+
document structure. `format` independently selects the response:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
await api.resources.books.patch({
|
|
234
|
+
id: '42',
|
|
235
|
+
document: {
|
|
236
|
+
data: {
|
|
237
|
+
type: 'books',
|
|
238
|
+
id: '42',
|
|
239
|
+
attributes: { title: 'Updated title' }
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
format: 'jsonapi',
|
|
243
|
+
returning: 'full'
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Port calls that mixed JSON:API input with plain output
|
|
248
|
+
|
|
249
|
+
Older versions detected JSON:API documents inside `inputRecord`; the intermediate
|
|
250
|
+
v2 contract instead coupled input to `format`. Both are replaced by explicit
|
|
251
|
+
input keys. `format` now selects only output, so a JSON:API `document` can return
|
|
252
|
+
plain output and plain `data` can return a JSON:API document.
|
|
253
|
+
|
|
254
|
+
Ordinary repositories can remove JSON:API builders entirely:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
// Before: JSON:API input was autodetected under the plain default.
|
|
258
|
+
const profile = await profiles.patch({
|
|
259
|
+
inputRecord: createJsonApiInputRecord('userProfiles', { displayName }, { id: userId }),
|
|
260
|
+
transaction: trx
|
|
261
|
+
}, requestContext)
|
|
262
|
+
|
|
263
|
+
// After: record data, target ID, and transaction are explicit.
|
|
264
|
+
const profile = await profiles.patch({
|
|
265
|
+
id: userId,
|
|
266
|
+
data: { displayName },
|
|
267
|
+
format: 'plain',
|
|
268
|
+
transaction: trx
|
|
269
|
+
}, requestContext)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
For a belongs-to relationship, plain input uses the relationship name and ID,
|
|
273
|
+
for example `data: { name: 'Team', owner: userId }`. Keep existing domain
|
|
274
|
+
normalization. A repository intentionally consuming JSON:API documents can keep
|
|
275
|
+
its application-owned document builder instead. The `createJsonApiInputRecord`
|
|
276
|
+
name below represents that application's function; the old JSKIT export with
|
|
277
|
+
that name has been removed.
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
const profile = await profiles.patch({
|
|
281
|
+
id: userId,
|
|
282
|
+
document: createJsonApiInputRecord('userProfiles', { displayName }, { id: userId }),
|
|
283
|
+
format: 'plain',
|
|
284
|
+
transaction: trx
|
|
285
|
+
}, requestContext)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
There is one resource method implementation, not a separate `api.jsonapi` API.
|
|
289
|
+
HTTP connectors submit their request body through `document` and request JSON:API
|
|
290
|
+
output. Validation, permissions, hooks and transaction behavior are shared.
|
|
291
|
+
Full-response preparation still happens before commit.
|
|
292
|
+
|
|
293
|
+
Write documents accept optional object-valued top-level `meta`, `links` and
|
|
294
|
+
`jsonapi` members. They remain available to hooks on `context.inputRecord` and
|
|
295
|
+
are not stored attributes. `errors` and `included` are rejected: accepting
|
|
296
|
+
documents does not enable compound writes or JSON:API extensions.
|
|
297
|
+
|
|
298
|
+
The hook field `context.inputRecord` remains the normalized operation document
|
|
299
|
+
for both input kinds. Do not mechanically rename that field when migrating
|
|
300
|
+
public call parameters. See the [hook context contract](13-hooks-and-lifecycle.md).
|
|
301
|
+
|
|
302
|
+
## Update configuration and consumers together
|
|
303
|
+
|
|
304
|
+
Use the same scalar option names for plugin defaults, resource overrides, and
|
|
305
|
+
individual calls. Precedence is **call → resource → plugin → built-in default**.
|
|
306
|
+
The built-in defaults are `format: 'plain'` and `returning: 'full'`.
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
// Before
|
|
310
|
+
await api.use(RestApiPlugin, {
|
|
311
|
+
simplifiedApi: true,
|
|
312
|
+
simplifiedTransport: false,
|
|
313
|
+
returnRecordApi: { post: 'full', put: 'full', patch: 'full' }
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// After
|
|
317
|
+
await api.use(RestApiPlugin, { format: 'plain', returning: 'full' })
|
|
318
|
+
|
|
319
|
+
// A resource can override these defaults.
|
|
320
|
+
await api.addResource('books', {
|
|
321
|
+
schema: bookSchema,
|
|
322
|
+
format: 'jsonapi',
|
|
323
|
+
returning: 'minimal'
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Per-method return maps are removed. Choose one resource default and set
|
|
328
|
+
`returning` on calls needing a different result. For example, port a former
|
|
329
|
+
`{ post: 'full', put: 'none', patch: 'none' }` policy by keeping the full default
|
|
330
|
+
and passing `returning: 'none'` on PUT/PATCH. Invalid strings, booleans, maps,
|
|
331
|
+
and the removed option names raise `REST_API_VALIDATION`; they never silently
|
|
332
|
+
select a fallback.
|
|
333
|
+
|
|
334
|
+
The library's Express/Fastify resource routes explicitly use `format: 'jsonapi'`
|
|
335
|
+
and `returning: 'full'`, regardless of programmatic defaults. Remove
|
|
336
|
+
`simplifiedTransport`, `returnRecordTransport`, and simulated `isTransport`
|
|
337
|
+
parameters. Standard resource POST responses contain a document with status
|
|
338
|
+
201; updates return their document with status 200. DELETE and relationship
|
|
339
|
+
mutations return no content. An application owning its HTTP handlers should
|
|
340
|
+
select its response contract explicitly.
|
|
341
|
+
|
|
342
|
+
For jskit-ai, start with `packages/json-rest-api-core/src/server/jsonRestApiHost.js`.
|
|
343
|
+
Then update the shared JSON:API CRUD repository and the plain-record repositories
|
|
344
|
+
in `users-core` and `workspaces-core`. Update generated-code templates before
|
|
345
|
+
regenerating app code. Each app with custom calls or hooks needs its own review.
|
|
346
|
+
|
|
347
|
+
For applications using the library's Fastify connector, register resources
|
|
348
|
+
before starting the server. Connector routes and JSON parsers now occupy their
|
|
349
|
+
own Fastify scope, leaving host parsers intact. The route validator uses the
|
|
350
|
+
existing request contract without Ajv stripping fields or adding coercion rules.
|
|
351
|
+
Both connectors return 422 for invalid resource documents, 400 for malformed
|
|
352
|
+
JSON, and 413 for an oversized body. Bodyless resource DELETE works with either
|
|
353
|
+
accepted JSON content type.
|
|
354
|
+
|
|
355
|
+
Check HTTP clients and tests for these boundary changes:
|
|
356
|
+
|
|
357
|
+
- Send `Content-Type: application/vnd.api+json` without `charset`. Ordinary
|
|
358
|
+
`application/json` requests remain accepted. Unsupported types and JSON:API
|
|
359
|
+
extensions receive 415; unknown profiles are ignored.
|
|
360
|
+
- Allow `application/vnd.api+json` in `Accept`, or use an appropriate wildcard.
|
|
361
|
+
Incompatible headers receive 406 before writing. Responses use the exact
|
|
362
|
+
JSON:API media type and preserve `Vary: Accept` alongside other vary values.
|
|
363
|
+
- Remove retired options from query strings as well as resource calls. They
|
|
364
|
+
now receive 422. Fractional page sizes/numbers also receive 422 instead of
|
|
365
|
+
being truncated; opaque cursor strings retain their spelling.
|
|
366
|
+
- Correct unknown resource types in sparse fieldsets. They now receive the
|
|
367
|
+
typed `REST_API_FIELDSET_INVALID` error (HTTP 400), including prototype-like
|
|
368
|
+
names, rather than being silently ignored.
|
|
369
|
+
|
|
370
|
+
The [Fastify guide](23-fastify.md) documents body limits, prefixed 404s and
|
|
371
|
+
the host's `onBadUrl` setting for malformed URLs. Fastify's default malformed-URL
|
|
372
|
+
response occurs before connector hooks and retains its native 400 shape unless
|
|
373
|
+
the host configures that setting. Express maps malformed route parameters to
|
|
374
|
+
JSON:API errors. These transport changes require no stored-data migration.
|
|
375
|
+
|
|
376
|
+
## Result shapes
|
|
377
|
+
|
|
378
|
+
| Operation | `format: 'plain'` | `format: 'jsonapi'` |
|
|
379
|
+
| --- | --- | --- |
|
|
380
|
+
| `get` | Plain record | `{ data: resource, ... }` |
|
|
381
|
+
| `query` | `{ data: [record, ...], meta?, links? }` | JSON:API collection document |
|
|
382
|
+
| Resource write, `returning: 'full'` | Plain record | Full resource document |
|
|
383
|
+
| Resource write, `returning: 'minimal'` | `{ type, id }` | `{ data: { type, id } }` |
|
|
384
|
+
| Resource write, `returning: 'none'` | `undefined` | `undefined` |
|
|
385
|
+
| `getRelated`, to one | Plain record or `null` | Document with resource or `data: null` |
|
|
386
|
+
| `getRelated`, to many | Same envelope as `query` | JSON:API collection document |
|
|
387
|
+
|
|
388
|
+
An empty collection has `data: []`. Plain collections retain their envelope so
|
|
389
|
+
pagination links and metadata remain available. Plain includes become nested
|
|
390
|
+
records; JSON:API includes stay in the document's `included` array with
|
|
391
|
+
relationship identifiers in `data.relationships`. Missing primary resources
|
|
392
|
+
raise the existing typed `not_found` error.
|
|
393
|
+
|
|
394
|
+
To-one related reads (`belongsTo`, polymorphic belongs-to, and `hasOne`) now
|
|
395
|
+
run the target resource's GET lifecycle whether or not fields or includes are
|
|
396
|
+
requested. Previously, a read without those options could bypass target
|
|
397
|
+
`checkPermissions`, `checkDataPermissions`, `checkDataPermissionsGet`, and
|
|
398
|
+
GET finish hooks by returning an included record directly. Check permission
|
|
399
|
+
hooks that previously only ran when callers supplied a fieldset. Target GET
|
|
400
|
+
errors now propagate consistently; SQL-hidden or absent linkage still returns
|
|
401
|
+
`null`. Parent GET permission checks retain the parent's full attributes.
|
|
402
|
+
The caller's transaction is used for both reads and remains caller-owned.
|
|
403
|
+
|
|
404
|
+
`getRelationship` also retains parent attributes for its internal GET permission
|
|
405
|
+
checks, instead of imposing an ID-only parent fieldset. Its response still
|
|
406
|
+
contains only linkage and links. A parent data-permission hook that denies a
|
|
407
|
+
normal GET can now deny the linkage endpoint using the same attributes.
|
|
408
|
+
|
|
409
|
+
`getRelated` and `getRelationship` set the current resource's `scopeName` before
|
|
410
|
+
permission hooks run. Reusing a context from another resource cannot leave those
|
|
411
|
+
hooks checking its stale resource name.
|
|
412
|
+
|
|
413
|
+
Included resources and relationship identifiers now require the target resource's
|
|
414
|
+
`query` permission, including nested includes and default linkage without
|
|
415
|
+
`include`. The library applies this check at the shared query-filtering boundary
|
|
416
|
+
before loading related rows. A permission hook receives `method: 'query'`, the
|
|
417
|
+
target scope/schema, no parent `id` or parent filters, and the caller's auth and
|
|
418
|
+
transaction. Explicit permission errors reject the read; row policies continue
|
|
419
|
+
to filter individual rows or null hidden to-one linkage. GET-only data hooks
|
|
420
|
+
remain part of the single-resource GET lifecycle, not collection/include policy.
|
|
421
|
+
|
|
422
|
+
Review applications that grant a primary read while denying query permission on
|
|
423
|
+
its related resources. Their includes/linkage previously bypassed that denial.
|
|
424
|
+
To-one `getRelated` also resolves authorized linkage before running target GET.
|
|
425
|
+
A full write response that fails this permission check rolls back its owned
|
|
426
|
+
write; `returning: 'minimal'` or `'none'` does not fetch that full response.
|
|
427
|
+
|
|
428
|
+
Regular-storage many-to-many includes and linkage now enforce the declared pivot
|
|
429
|
+
resource's query permission and row filters, before include limits. This applies
|
|
430
|
+
to standard and windowed includes with mapped pivot keys. AnyAPI continues to
|
|
431
|
+
store these associations as tenant-scoped canonical links rather than rows in a
|
|
432
|
+
declared pivot resource; do not expect a pivot-row policy to govern those links.
|
|
433
|
+
|
|
434
|
+
Canonical many-to-many linkage now applies target query permission and row filters
|
|
435
|
+
in SQL before loading link rows. Its `relationship-identifiers` filter query is
|
|
436
|
+
built once per relationship read, including an empty relationship, rather than
|
|
437
|
+
once per 100 already-loaded target IDs. The returned builder may be used inside
|
|
438
|
+
several paged SQL statements. Keep these hooks as query transformations; do not
|
|
439
|
+
depend on their call count for per-record work. Parent GETs may still filter
|
|
440
|
+
other declared relationships independently. Target auth, transaction and scoped
|
|
441
|
+
metadata remain available, and parent filters are not applied to the target.
|
|
442
|
+
Public resource arguments and response shapes need no change for this fix.
|
|
443
|
+
|
|
444
|
+
Plain responses now expand all available included levels. Previously the
|
|
445
|
+
converter discarded the remaining included document after the first level and
|
|
446
|
+
looked up the wrong nested schema property, losing nested belongs-to identifiers.
|
|
447
|
+
For example, `include: ['authors.publisher.country']` now produces
|
|
448
|
+
`book.authors[0].publisher.country`. Cycles end with `{ id }` (and `_type` for
|
|
449
|
+
polymorphic references), so the result remains JSON-serializable. Repeated sibling
|
|
450
|
+
references expand independently; relationships missing from `included` keep their
|
|
451
|
+
identifier instead of being dropped. This changes response shape only; stored
|
|
452
|
+
records and relationships do not require a data migration.
|
|
453
|
+
|
|
454
|
+
Plain conversion also shares one type/ID lookup across the included document.
|
|
455
|
+
This removes repeated searches for large responses without changing call
|
|
456
|
+
arguments or the expansion rules above. It does not impose a new response limit;
|
|
457
|
+
large or repeatedly expanded graphs still need memory for their complete output.
|
|
458
|
+
|
|
459
|
+
For a hasMany related-resource read, the parent relationship no longer needs
|
|
460
|
+
`search: true` on the child foreign key or polymorphic discriminator fields.
|
|
461
|
+
Parent membership is applied internally alongside your permitted child filters,
|
|
462
|
+
before pagination and counts. A caller filter on a searchable parent relationship
|
|
463
|
+
is intersected with membership instead of being overwritten. Non-searchable
|
|
464
|
+
fields remain unavailable as public filters. Follow the returned pagination
|
|
465
|
+
links: they now retain the related-resource route, requested fields, filters,
|
|
466
|
+
and pagination parameters.
|
|
467
|
+
|
|
468
|
+
Many-to-many related reads now use the target resource's collection query too.
|
|
469
|
+
Pass target filters, fields, includes, sort and pagination exactly as you would
|
|
470
|
+
to `api.resources.books.query(...)`; the library intersects that query with
|
|
471
|
+
the parent's membership. Results and counts describe distinct target records,
|
|
472
|
+
including when storage contains duplicate links. Default limits and sort now
|
|
473
|
+
come from the target resource. Follow returned links to traverse the collection.
|
|
474
|
+
|
|
475
|
+
Target query permissions and query-filter hooks run on these reads. Regular
|
|
476
|
+
storage also checks the declared pivot resource's query permission and filters
|
|
477
|
+
its membership subquery. AnyAPI stores membership in tenant-scoped canonical
|
|
478
|
+
links. The former per-member GET calls and formatted pivot queries are removed;
|
|
479
|
+
move any custom behavior that relied on their GET/finish hooks to the target
|
|
480
|
+
query hooks or the appropriate row policy. No scoped consumer source currently
|
|
481
|
+
uses `getRelated` or those read hooks.
|
|
482
|
+
|
|
483
|
+
Resource GET/query and related reads now reject undeclared include paths with
|
|
484
|
+
`REST_API_INCLUDE_INVALID` (HTTP 400, `source.parameter: 'include'`), including
|
|
485
|
+
empty collections and absent to-one targets. Use declared relationship names,
|
|
486
|
+
not attribute names, and remove obsolete paths instead of relying on them being
|
|
487
|
+
ignored. Finite cycles remain valid; a polymorphic nested path may apply to a
|
|
488
|
+
subset of its declared target types. This follows the
|
|
489
|
+
[JSON:API include contract](https://jsonapi.org/format/#fetching-includes).
|
|
490
|
+
|
|
491
|
+
Two filter corrections can change results: `!= null` selects non-null values,
|
|
492
|
+
and `oneOf` with `splitBy` and `matchAll: false` matches any split term as
|
|
493
|
+
documented. A `between` filter now requires an array of exactly two non-null
|
|
494
|
+
bounds. Use an equality filter for one value; omit an optional range filter
|
|
495
|
+
when no bounds are supplied. Empty or oversized arrays no longer silently
|
|
496
|
+
remove the range condition. `enablePaginationCounts: false` at plugin level
|
|
497
|
+
now disables offset count queries. These changes require no stored-data migration.
|
|
498
|
+
Offset totals also count distinct primary resources when a search joins multiple
|
|
499
|
+
matching children, so duplicate join rows no longer inflate page counts.
|
|
500
|
+
|
|
501
|
+
To-one relationship identifiers now obey the target resource's row policy and
|
|
502
|
+
autofilters even when you do not request an include. This applies to belongsTo
|
|
503
|
+
and polymorphic linkage on primary and included resources, and to full write
|
|
504
|
+
responses. A hidden target produces JSON:API `data: null`; plain output omits
|
|
505
|
+
that relationship field, following the existing null-belongsTo convention.
|
|
506
|
+
Stored foreign keys remain unchanged. Do not use a previously exposed hidden
|
|
507
|
+
target ID as an authorization shortcut. A borrowed transaction also governs
|
|
508
|
+
these visibility reads. Lookups are grouped by target resource type rather
|
|
509
|
+
than performed separately for each record.
|
|
510
|
+
|
|
511
|
+
AnyAPI now omits belongs-to foreign keys and polymorphic type/ID backing fields
|
|
512
|
+
from response attributes, matching regular storage. This also applies to sparse
|
|
513
|
+
responses, included resources and plain output. Read the declared relationship:
|
|
514
|
+
for example, use `data.relationships.subject.data` in JSON:API or `record.subject`
|
|
515
|
+
in plain output instead of `attributes.subject_id` and `attributes.subject_type`.
|
|
516
|
+
Hidden targets retain the null/omitted behavior above. Storage columns and write
|
|
517
|
+
linkage are unchanged; ordinary ID attributes that do not back a relationship
|
|
518
|
+
remain attributes.
|
|
519
|
+
|
|
520
|
+
Cross-resource and polymorphic search paths now check each joined resource's
|
|
521
|
+
`query` permission and apply its row policy/autofilters with
|
|
522
|
+
`queryPurpose: 'search-join'`. Move mandatory predicates out of branches that only
|
|
523
|
+
handle `collection`. Related-only matches against hidden attributes disappear
|
|
524
|
+
from results and counts; a primary attribute can still match an OR search.
|
|
525
|
+
Explicit target permission failures reject the query, even if no primary rows
|
|
526
|
+
match. No per-target GET hooks run. Combined direct/nested polymorphic filters
|
|
527
|
+
now work in either argument order, including mapped tables and columns.
|
|
528
|
+
|
|
529
|
+
PostgreSQL text searches (`like`, `contains`, `startsWith`, `endsWith`) now work
|
|
530
|
+
on numeric relationship IDs by comparing their text representation. A pattern
|
|
531
|
+
such as `"01"` does not become the ID `1`, and `"1e0"` does not become `"1"`.
|
|
532
|
+
Existing string validation still applies, including trimming. Equality/range
|
|
533
|
+
comparisons retain the field's type; text matching retains the database's case
|
|
534
|
+
behavior. No call-site or stored-data migration is required for this correction.
|
|
535
|
+
|
|
536
|
+
The same visibility rule now applies to filters on relationship backing keys:
|
|
537
|
+
for example, `filters: { author_id: id }` and the declared `author` alias only
|
|
538
|
+
match a visible author. Polymorphic ID/type filters check the declared target
|
|
539
|
+
types together; identical IDs in different types remain distinct references.
|
|
540
|
+
Hidden and missing references both behave as null, including for `!=`, lists,
|
|
541
|
+
ranges and `oneOf` searches. Independent primary-attribute OR matches remain
|
|
542
|
+
eligible. These checks use the caller's transaction and run before counts and
|
|
543
|
+
pagination. They do not change stored relationships.
|
|
544
|
+
|
|
545
|
+
Sorting by a relationship key or its declared alias now uses the same visibility
|
|
546
|
+
rule. This includes polymorphic ID/type fields and resource `defaultSort`.
|
|
547
|
+
Hidden and absent references sort together as null, with later sort fields and
|
|
548
|
+
the primary ID breaking ties. Cursor boundaries and generated links use those
|
|
549
|
+
same values: a hidden `author_id` appears as `author_id:~null` in a cursor,
|
|
550
|
+
never as the hidden author's ID. Visible linkage and includes still work.
|
|
551
|
+
The target's `query` permission and `search-join` policy apply to these lookups,
|
|
552
|
+
including offset pagination and queries without explicit pagination.
|
|
553
|
+
Start a new pagination traversal after upgrading, and follow returned links.
|
|
554
|
+
|
|
555
|
+
Null and array filters now survive generated pagination links. For manual HTTP
|
|
556
|
+
requests, the explicit typed form is:
|
|
557
|
+
|
|
558
|
+
```text
|
|
559
|
+
filter[author][json]=null
|
|
560
|
+
filter[authorIds][json]=["1","2"]
|
|
561
|
+
filter[authorIds][json]=[]
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
URL-encode these parameters normally. `filter[name]=null` still searches for the
|
|
565
|
+
literal string `null`; only the `[json]` suffix requests JSON parsing. Malformed
|
|
566
|
+
JSON returns HTTP 400. Generated links use this form for nulls, arrays and object
|
|
567
|
+
filter values, preserving embedded commas and empty lists. Programmatic calls
|
|
568
|
+
continue to pass JavaScript values in `queryParams.filters`. Use the returned
|
|
569
|
+
links directly instead of reconstructing or dropping these typed parameters.
|
|
570
|
+
|
|
571
|
+
Collection includes now honor the same limits and ordering on both backends.
|
|
572
|
+
AnyAPI previously ignored these settings. `strategy: 'window'` limits visible
|
|
573
|
+
children per parent, including reverse polymorphic collections; `standard` (the
|
|
574
|
+
default) limits the global target set. Under a global many-to-many limit, a shared
|
|
575
|
+
target counts once and remains linked to each of its selected parents. Duplicate
|
|
576
|
+
links do not consume slots, and selecting query projections no longer switches
|
|
577
|
+
away from per-parent limits. `orderBy` uses mapped target fields, relationship
|
|
578
|
+
aliases or SQL query projections, with nulls last and target IDs breaking ties.
|
|
579
|
+
|
|
580
|
+
Review callers that assumed an include returned every related record: the
|
|
581
|
+
included relationship linkage now describes its selected subset on AnyAPI too.
|
|
582
|
+
Use the related or relationship endpoint for full visible membership and its
|
|
583
|
+
pagination. An omitted limit uses the target query default; `limit: 0` selects no
|
|
584
|
+
children and `limit: null` explicitly disables the include limit. Window includes
|
|
585
|
+
require database window-function support. The resource method signatures are
|
|
586
|
+
unchanged by this fix.
|
|
587
|
+
|
|
588
|
+
The unused deep helpers `buildWindowedIncludeSubquery`, `buildOrderByClause` and
|
|
589
|
+
`applyStandardIncludeConfig` were removed from
|
|
590
|
+
`plugins/core/lib/querying/knex-window-queries.js`. Configure relationships through
|
|
591
|
+
their `include` settings instead of importing storage query builders. No forwarding
|
|
592
|
+
exports or compatibility implementation remain. Check application extensions
|
|
593
|
+
for imports of the removed helpers.
|
|
594
|
+
|
|
595
|
+
Other unused deep exports have also been removed: `createRequiredIndexes` and
|
|
596
|
+
`analyzeRequiredIndexes` from `knex-cross-table-search.js`, `isNonDatabaseField`
|
|
597
|
+
from `knex-field-helpers.js`, `translateSelectFieldsForAdapter` from
|
|
598
|
+
`storage-adapter.js`, and `getLogicalFieldName` from `storage-mapping.js`.
|
|
599
|
+
Remove imports of these internal utilities. Use resource configuration and the
|
|
600
|
+
existing storage adapter when writing custom queries; create indexes through
|
|
601
|
+
reviewed schema changes. The automatic cross-table index advice was unused by
|
|
602
|
+
query execution and did not reliably identify the required indexes.
|
|
603
|
+
|
|
604
|
+
The standalone AnyAPI `api.helpers.dataQueryCount` helper has been removed.
|
|
605
|
+
Use `resource.query({ queryParams: { page: { number: 1, size: 1 } } })` and read
|
|
606
|
+
`result.meta.pagination.total` when a pagination total is needed. This uses the
|
|
607
|
+
same filters and access rules as the resource's returned records.
|
|
608
|
+
|
|
609
|
+
AnyAPI also allocates string slots for non-primary scalar fields declared
|
|
610
|
+
`type: 'id'`, including polymorphic backing IDs. Such fields previously lacked
|
|
611
|
+
storage. Writes and filters bind the normalized ID as text, avoiding SQLite's
|
|
612
|
+
numeric-to-TEXT conversion of `1` to `"1.0"`. Newly supported ID fields are
|
|
613
|
+
allocated after the existing ordinary fields so those fields retain their slots
|
|
614
|
+
when an unchanged declaration is registered again. If you use an explicit
|
|
615
|
+
`canonicalFieldsMap`, assign each such field a free string slot. Back up and
|
|
616
|
+
inspect existing descriptors before migration; values never stored by the older
|
|
617
|
+
implementation cannot be recovered automatically. This fix does not provide
|
|
618
|
+
general schema-reordering or schema-evolution migration.
|
|
619
|
+
|
|
620
|
+
`returning` governs POST/PUT/PATCH resource writes. DELETE and relationship
|
|
621
|
+
mutations return `undefined`. Linkage methods retain one explicit representation:
|
|
622
|
+
`getRelationship` returns `{ data, links }`, with resource identifiers, an array,
|
|
623
|
+
or null; relationship writes take `relationshipData` in JSON:API identifier
|
|
624
|
+
form. Their purpose is to manage linkage, so plain formatting does not flatten
|
|
625
|
+
that linkage document.
|
|
626
|
+
|
|
627
|
+
## Arguments and request context
|
|
628
|
+
|
|
629
|
+
Keep controls in the first argument and request context in the second. This
|
|
630
|
+
convention already matches jskit-ai and avoids moving every context field into
|
|
631
|
+
every method's parameter parser. A transaction belongs in the first argument:
|
|
632
|
+
|
|
633
|
+
```js
|
|
634
|
+
await api.resources.books.patch({
|
|
635
|
+
id: bookId,
|
|
636
|
+
data: { title: 'Updated' },
|
|
637
|
+
format: 'plain',
|
|
638
|
+
returning: 'full',
|
|
639
|
+
transaction: trx,
|
|
640
|
+
queryParams: { include: ['author'] }
|
|
641
|
+
}, requestContext)
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Forward both arguments when wrapping resource calls. HTTP relationship routes
|
|
645
|
+
now forward the connector context too, and linkage reads forward a caller's
|
|
646
|
+
transaction to their nested resource read. No stored-data migration is required
|
|
647
|
+
by these option and argument changes.
|
|
648
|
+
|
|
649
|
+
## Review relationship writes
|
|
650
|
+
|
|
651
|
+
Reverse relationships (`hasMany`, `hasOne`, and polymorphic `via`) now update the
|
|
652
|
+
existing child records through their resource PATCH operation. Previous hasMany
|
|
653
|
+
endpoint calls could either fail on raw foreign-key validation or report success
|
|
654
|
+
without changing membership. Check for application workarounds that manually
|
|
655
|
+
patched children after one of those calls.
|
|
656
|
+
|
|
657
|
+
For a to-many relationship, POST adds members, PATCH replaces the complete
|
|
658
|
+
membership, and DELETE removes only the specified members. Repeated identifiers
|
|
659
|
+
and repeated successful add/remove calls do not create extra membership or
|
|
660
|
+
delete child records. These semantics follow the
|
|
661
|
+
[JSON:API relationship update contract](https://jsonapi.org/format/#crud-updating-to-many-relationships).
|
|
662
|
+
|
|
663
|
+
```js
|
|
664
|
+
const relationship = { id: publisherId, relationshipName: 'books' }
|
|
665
|
+
const book = { type: 'books', id: bookId }
|
|
666
|
+
|
|
667
|
+
await api.resources.publishers.postRelationship({
|
|
668
|
+
...relationship, relationshipData: [book]
|
|
669
|
+
}, requestContext)
|
|
670
|
+
|
|
671
|
+
// Keep exactly this book; unlink other members.
|
|
672
|
+
await api.resources.publishers.patchRelationship({
|
|
673
|
+
...relationship, relationshipData: [book]
|
|
674
|
+
}, requestContext)
|
|
675
|
+
|
|
676
|
+
await api.resources.publishers.deleteRelationship({
|
|
677
|
+
...relationship, relationshipData: [book]
|
|
678
|
+
}, requestContext)
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
Use `relationshipData: []` to clear a to-many relationship with PATCH. A to-one
|
|
682
|
+
PATCH takes one `{ type, id }` identifier or `null`. Unlinking requires the child
|
|
683
|
+
foreign key to allow null explicitly (`nullable: true`); an optional field is
|
|
684
|
+
not automatically nullable. Target types and cardinality are validated on the
|
|
685
|
+
relationship routes as well as normal resource writes.
|
|
686
|
+
|
|
687
|
+
Normal resource POST/PATCH/PUT payloads can also supply reverse relationships.
|
|
688
|
+
PATCH changes only supplied relationships. For PUT, omitting the JSON:API
|
|
689
|
+
`relationships` object preserves reverse and many-to-many membership; supplying
|
|
690
|
+
that object clears any omitted reverse or many-to-many relationships. Stored
|
|
691
|
+
attributes and belongs-to foreign keys still follow PUT's complete replacement
|
|
692
|
+
validation. Include every relationship you intend to retain when supplying a
|
|
693
|
+
PUT relationships object.
|
|
694
|
+
|
|
695
|
+
Child PATCH hooks and permissions now run for actual reverse-membership changes,
|
|
696
|
+
with the caller context and transaction. A replacement that cannot unlink an
|
|
697
|
+
inaccessible existing child fails rather than leaving a partial relationship.
|
|
698
|
+
A transaction owned by the operation rolls back earlier changes on failure.
|
|
699
|
+
The library leaves completion of a caller-supplied transaction to its owner,
|
|
700
|
+
which must now be `api.transaction` for library writes. A rejected participating
|
|
701
|
+
write aborts that unit even when caught; a native database error may already have
|
|
702
|
+
aborted its SQL transaction. Concurrent replacements now serialize on the parent, and
|
|
703
|
+
many-to-many additions lock related records too, including additions through
|
|
704
|
+
opposite endpoints. This prevents merged replacements, duplicate pivot rows,
|
|
705
|
+
and multiple children left behind by competing hasOne replacements. Belongs-to
|
|
706
|
+
and polymorphic writes also lock and recheck their targets after validation;
|
|
707
|
+
a target deleted in that interval causes the write to fail.
|
|
708
|
+
|
|
709
|
+
Reverse additions and replacements now use the same target-identity locking
|
|
710
|
+
helper. Equivalent ID spellings under the database's equality rules identify
|
|
711
|
+
one child: retaining it does not invoke PATCH again, and adding it through
|
|
712
|
+
multiple equivalent spellings writes it once. A removal still ignores missing
|
|
713
|
+
or unrelated children. No method arguments change.
|
|
714
|
+
|
|
715
|
+
Reverse membership reads now use batches of at most 100 requested IDs. A
|
|
716
|
+
replacement walks removed children in database ID order, reading at most 101
|
|
717
|
+
identifiers at a time and unlinking them before any additions. Keep child hooks
|
|
718
|
+
independent of a database's former incidental row order. Hooks and permissions
|
|
719
|
+
still run for each changed child, and the enclosing transaction owns rollback;
|
|
720
|
+
this does not turn child writes into a bulk SQL update.
|
|
721
|
+
|
|
722
|
+
Canonical many-to-many attachment also pages existing physical links: at most
|
|
723
|
+
100 requested target IDs and 101 returned edge rows per query. Pre-existing
|
|
724
|
+
duplicate edges and their payloads remain intact, and inverse metadata is still
|
|
725
|
+
repaired on the first retained match. A duplicate-heavy batch can issue more
|
|
726
|
+
SQL statements while keeping read memory bounded. Arguments, resource hooks,
|
|
727
|
+
target/edge locks and transaction ownership are unchanged; no data migration is
|
|
728
|
+
required for this paging change.
|
|
729
|
+
|
|
730
|
+
Relationship mutations can issue an UPDATE that preserves the parent's ID even
|
|
731
|
+
when no attribute changed. That row version change makes stale PostgreSQL
|
|
732
|
+
repeatable-read transactions fail instead of applying an old membership snapshot.
|
|
733
|
+
Database UPDATE triggers on the parent may therefore run for relationship-only
|
|
734
|
+
writes. Resource hooks still follow their existing operation lifecycle; these
|
|
735
|
+
internal locks do not invoke a second resource PATCH hook.
|
|
736
|
+
If you use per-table database grants, account for parent UPDATEs and target
|
|
737
|
+
`SELECT ... FOR UPDATE` statements when configuring the application's database
|
|
738
|
+
role.
|
|
739
|
+
|
|
740
|
+
Treat native deadlock, lock-timeout and serialization errors as failed transaction
|
|
741
|
+
attempts. Let `api.transaction` roll back, then retry the complete callback in a
|
|
742
|
+
new unit only when its side effects can safely repeat. SQLite permits one active
|
|
743
|
+
writer and can reject a stale read snapshot's write upgrade. The library does
|
|
744
|
+
not automatically retry or change the caller's isolation level. The core managed
|
|
745
|
+
helper owns completion; migrate application transaction wrappers accordingly.
|
|
746
|
+
|
|
747
|
+
Existing duplicate pivot/link rows are not automatically removed. Inspect any
|
|
748
|
+
previously concurrent relationship workloads and reconcile duplicates while
|
|
749
|
+
preserving their pivot metadata. For a regular pivot, group by its two mapped
|
|
750
|
+
foreign-key columns; for canonical `any_links`, group by `tenant_id`,
|
|
751
|
+
`relationship`, `left_resource`, `left_id`, `right_resource` and `right_id`.
|
|
752
|
+
Groups with more than one row need reconciliation. This fix requires no column
|
|
753
|
+
migration and does not replace database constraints used by direct SQL writers.
|
|
754
|
+
|
|
755
|
+
Canonical many-to-many removal and replacement now delete the selected links in
|
|
756
|
+
scoped SQL batches instead of reading the complete old membership and deleting
|
|
757
|
+
each link individually. Both stored directions are recognized, including links
|
|
758
|
+
created before an inverse was declared. Retained links keep their row IDs and
|
|
759
|
+
payloads; attachment repairs missing inverse metadata in place. Replacement
|
|
760
|
+
with `relationshipData: []` also clears matching links whose inverse metadata
|
|
761
|
+
is missing. Tenant, relationship and target resource type all constrain the
|
|
762
|
+
write, so another resource that reuses a target ID is left intact. No call-site
|
|
763
|
+
or schema migration is needed for this batching change. If you instrument SQL,
|
|
764
|
+
expect fewer DELETE statements and additional target locks for retained links
|
|
765
|
+
during replacement; transaction ownership and the resource hook lifecycle stay
|
|
766
|
+
the same.
|
|
767
|
+
|
|
768
|
+
## Transactions and errors
|
|
769
|
+
|
|
770
|
+
A failed call can represent a committed write. When commit acknowledgement is
|
|
771
|
+
lost, `unknown` does not tell you whether the database stored the change. Keep
|
|
772
|
+
an application operation identity and reconcile persisted state before replaying
|
|
773
|
+
the write or deleting tracked uploads. No commit/rollback side-effect hooks run
|
|
774
|
+
for an unknown outcome. See the [commit uncertainty contract](20-transaction-outcomes.md#when-commit-acknowledgement-is-lost)
|
|
775
|
+
for the evidence used and the limits of failure-injection coverage.
|
|
776
|
+
|
|
777
|
+
Migrate transaction wrappers to `api.transaction(callback, context?)`. Its callback
|
|
778
|
+
receives a real Knex transaction for resource calls and raw SQL; the library owns
|
|
779
|
+
completion and deferred hooks. Pass that handle through related operations,
|
|
780
|
+
using a separate context for each enlisted operation. Remove overlapping
|
|
781
|
+
transaction ownership from application wrappers.
|
|
782
|
+
|
|
783
|
+
Unmanaged raw transactions and child savepoints cannot own library writes.
|
|
784
|
+
Raw Knex transactions still support direct SQL, low-level storage helpers and
|
|
785
|
+
read-only API operations. See the [managed transaction contract](19-managed-transactions.md).
|
|
786
|
+
|
|
787
|
+
```js
|
|
788
|
+
// Before: a Knex callback owned library writes.
|
|
789
|
+
await knex.transaction(transaction => changeBooks(transaction))
|
|
790
|
+
|
|
791
|
+
// After: the library owns writes, raw SQL and deferred completion hooks.
|
|
792
|
+
await api.transaction(async transaction => {
|
|
793
|
+
const book = await api.resources.books.patch({
|
|
794
|
+
id: '42', data: { title: 'Updated title' }, format: 'plain', transaction
|
|
795
|
+
})
|
|
796
|
+
await transaction('audit_entries').insert({ book_id: book.id })
|
|
797
|
+
return book
|
|
798
|
+
})
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Await every operation and let the callback return normally to commit. Throw to
|
|
802
|
+
roll back. A caught participating write or observed SQL failure still aborts
|
|
803
|
+
the unit. Do not call `commit()` or `rollback()` yourself inside the callback.
|
|
804
|
+
Pass the received handle through helper functions to compose work; calling
|
|
805
|
+
`api.transaction` again starts a separate top-level unit.
|
|
806
|
+
|
|
807
|
+
Completion ownership is private to the library. Remove any hook code that sets
|
|
808
|
+
`context.shouldCommit` to take over or suppress completion; that diagnostic flag
|
|
809
|
+
does not control it. Each operation's completion hooks receive its own context.
|
|
810
|
+
Every accepted resource/relationship operation runs its completion chain once,
|
|
811
|
+
including nested PATCH calls made by relationship replacement and reverse-child
|
|
812
|
+
updates. Update observers that assumed one completion hook meant one database
|
|
813
|
+
commit. Transaction and bulk owners do not add a synthetic resource hook chain.
|
|
814
|
+
On rollback, failed operations retain their original error, while successful
|
|
815
|
+
operations receive the owner's failure. Inspect secondary completion errors on
|
|
816
|
+
the outer helper's context.
|
|
817
|
+
|
|
818
|
+
Reusing a context after a successful write no longer carries the previous
|
|
819
|
+
commit flag into a later operation. A rejected write rolls back its own new
|
|
820
|
+
transaction, while an operation borrowing a caller's transaction leaves its
|
|
821
|
+
completion to the caller. This also fixes connection exhaustion after denied
|
|
822
|
+
entries in non-atomic bulk calls that reuse authenticated context.
|
|
823
|
+
PUT validation also uses the current payload and record rather than attributes
|
|
824
|
+
retained from a previous call. Omitting populated attributes still fails a
|
|
825
|
+
replacement; creating a new ID does not inherit the old record's requirements.
|
|
826
|
+
|
|
827
|
+
Write failures now use the root export `RestApiWriteError`. Read failures retain
|
|
828
|
+
their existing contract. The wrapper's `cause` is the original thrown value and
|
|
829
|
+
its read-only `transactionOutcome` is `none`, `pending`, `committed`, `rolledBack`
|
|
830
|
+
or `unknown`. Existing `code`, `subtype` and other ordinary diagnostic fields
|
|
831
|
+
remain readable directly. Code that compares original error identity or checks
|
|
832
|
+
the original class with `instanceof` must inspect the cause instead.
|
|
833
|
+
|
|
834
|
+
```js
|
|
835
|
+
import { RestApiWriteError } from 'json-rest-api'
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
await api.resources.books.post({ document, format: 'jsonapi' })
|
|
839
|
+
} catch (error) {
|
|
840
|
+
if (!(error instanceof RestApiWriteError)) throw error
|
|
841
|
+
console.error(error.code, error.transactionOutcome, error.cause)
|
|
842
|
+
throw error
|
|
843
|
+
}
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
A nested write can be another `RestApiWriteError` in the cause chain. For example,
|
|
847
|
+
an atomic batch preserves its child's `pending` snapshot while reporting the
|
|
848
|
+
outer transaction as `rolledBack`. HTTP JSON:API errors add
|
|
849
|
+
`meta.transactionOutcome`; non-atomic bulk errors add
|
|
850
|
+
`error.transactionOutcome` per entry. Status codes alone do not establish whether
|
|
851
|
+
a write committed. A `committed` or `unknown` failure must not trigger blind
|
|
852
|
+
replay, and `pending` leaves the decision to the transaction owner. Even confirmed
|
|
853
|
+
database rollback says nothing about external effects already performed.
|
|
854
|
+
|
|
855
|
+
For example, a POST using a generated ID can commit a new book, then reject
|
|
856
|
+
because an `afterCommit` notification fails. Repeating that POST creates another
|
|
857
|
+
book. The first error reports `committed`; it is a failed call with a stored
|
|
858
|
+
write. A lost commit acknowledgement instead reports `unknown`, which also
|
|
859
|
+
cannot authorize a blind retry. Handle the failed side effect or reconcile the
|
|
860
|
+
write using application identifiers rather than replaying it automatically.
|
|
861
|
+
|
|
862
|
+
Errors before transaction setup—including malformed bodies, oversized requests
|
|
863
|
+
and explicit request rejection—report `none`. A response-hook failure after a
|
|
864
|
+
successful write reports `committed`; adding that failure does not erase an
|
|
865
|
+
earlier write's confirmed outcome. Read-only HTTP errors have no write-outcome
|
|
866
|
+
metadata.
|
|
867
|
+
|
|
868
|
+
Owned transaction completion now also checks Knex's `executionPromise`. Knex can
|
|
869
|
+
resolve `commit()` or `rollback()` while reporting a failed control query through
|
|
870
|
+
that promise. Such failures now reject the write or become rollback diagnostics;
|
|
871
|
+
they no longer run the corresponding completion hooks or publish committed
|
|
872
|
+
registry metadata. Existing payloads and stored data need no migration. A rejected
|
|
873
|
+
commit still does not prove rollback, including when the driver reports itself
|
|
874
|
+
completed. The [outcome contract](20-transaction-outcomes.md) explains
|
|
875
|
+
the evidence and remaining verification boundaries.
|
|
876
|
+
|
|
877
|
+
PostgreSQL can also acknowledge COMMIT by reporting ROLLBACK after an earlier
|
|
878
|
+
SQL error was caught. Those writes now reject instead of reporting success.
|
|
879
|
+
The library recognizes the confirmed rollback, runs existing resource rollback
|
|
880
|
+
cleanup, and avoids commit hooks or cache publication. It does not issue a
|
|
881
|
+
second rollback. This correction requires no payload or stored-data migration;
|
|
882
|
+
an application hook that catches a database error can now expose a failure
|
|
883
|
+
that was previously mistaken for a successful write.
|
|
884
|
+
|
|
885
|
+
Custom transaction factories must return top-level transactions. Returning a
|
|
886
|
+
savepoint previously allowed `afterCommit` or registry cache publication before
|
|
887
|
+
the parent transaction committed. Owned completion now rejects that case and
|
|
888
|
+
rolls back the savepoint; it leaves unrelated parent work and parent completion
|
|
889
|
+
with the caller. The failure reports rolledBack only when savepoint rollback
|
|
890
|
+
is confirmed. Managed acquisition now rejects that factory result before
|
|
891
|
+
resource hooks or uploads run. Explicit raw savepoints passed in `transaction`
|
|
892
|
+
also reject before library writes; their raw SQL and completion remain the
|
|
893
|
+
caller's responsibility. No schema or stored-data migration is needed.
|
|
894
|
+
|
|
895
|
+
Resource and relationship write failures now retain the original error when
|
|
896
|
+
rollback, an `afterRollback` hook, or error logging also fails. The supplied
|
|
897
|
+
operation context exposes the original `error` and `cleanupErrors`, an ordered array of
|
|
898
|
+
`{ phase, error }` entries. Phases include `rollback`, `afterRollback` and `logging`.
|
|
899
|
+
File cleanup adds the phases and field information described under multipart
|
|
900
|
+
uploads; diagnostics gathered before rollback are retained.
|
|
901
|
+
Managed completion records rejected hook chains with `operationIndex`,
|
|
902
|
+
`scopeName` and `method`, using `afterCommit` or `afterRollback`. Internal
|
|
903
|
+
diagnostic-collection failures use `finalization` and `finalizerIndex`.
|
|
904
|
+
Later completion work is still attempted after an earlier failure. On confirmed
|
|
905
|
+
commit the first completion failure is primary; on rollback the original write
|
|
906
|
+
or callback failure remains primary. See the
|
|
907
|
+
[managed completion contract](19-managed-transactions.md#completion-work).
|
|
908
|
+
These diagnostic fields are cleared at the start of the next write, including
|
|
909
|
+
when its context is reused. The original error object is not mutated; frozen
|
|
910
|
+
errors retain their identity as the wrapper's cause. A failure rejected before
|
|
911
|
+
transaction setup has outcome `none` and its own original `context.error`.
|
|
912
|
+
|
|
913
|
+
`afterRollback` receives the original failure as `context.error` and runs only
|
|
914
|
+
after rollback is confirmed, including PostgreSQL reporting ROLLBACK for COMMIT.
|
|
915
|
+
If rollback rejects without confirmation, the library retains
|
|
916
|
+
that secondary failure and does not claim rollback succeeded or run rollback
|
|
917
|
+
cleanup hooks. A participating write leaves completion to its managed owner. Asynchronous
|
|
918
|
+
error logging is awaited, and its rejection is recorded without replacing the
|
|
919
|
+
write error. These fields are diagnostics separate from the wrapper's immutable
|
|
920
|
+
transaction outcome. Atomic bulk rollback uses the same diagnostic shape, as described above.
|
|
921
|
+
Remaining cleanup and extension-error boundaries are still under review; the
|
|
922
|
+
rollback-hook and logging guarantees above describe resource and relationship
|
|
923
|
+
methods.
|
|
924
|
+
|
|
925
|
+
The shared failure handler also checks the driver's completion state before
|
|
926
|
+
attempting rollback. A commit wrapper that rejects after actual completion
|
|
927
|
+
cannot trigger rollback hooks that delete committed uploads. Completion alone
|
|
928
|
+
does not prove which outcome occurred; without confirmation the error reports
|
|
929
|
+
`unknown`.
|
|
930
|
+
|
|
931
|
+
Query-projection and include errors retain their original causes, including
|
|
932
|
+
`null`, `undefined` and other non-Error throws. Typed API errors propagate
|
|
933
|
+
unchanged through reads and become the write wrapper's cause when raised during
|
|
934
|
+
a write. Unexpected projection errors have a wrapper with resource/field
|
|
935
|
+
context and `cause`. Include errors retain their context through the cause
|
|
936
|
+
chain. Redundant logging in nested include catches has been removed so it cannot
|
|
937
|
+
replace the failure. GET/query reject an affected response; full write-response
|
|
938
|
+
failures occur before owned commit, and borrowed transactions remain active for
|
|
939
|
+
their owner. Callers should classify typed errors and inspect causes rather
|
|
940
|
+
than match the wording of unexpected-error messages. Field callbacks follow
|
|
941
|
+
the policy below; arbitrary hook-dispatch failures remain under review.
|
|
942
|
+
|
|
943
|
+
Direct canonical registry reads and explicit descriptor refreshes retain unexpected causes with
|
|
944
|
+
`context: { tenant, scopeName, phase: 'descriptor' }`. Typed API errors keep
|
|
945
|
+
their identity. The direct registry lookup returns null for absent persisted
|
|
946
|
+
metadata; a required refresh rejects without replacing the published resource.
|
|
947
|
+
|
|
948
|
+
Canonical resource operations use published descriptors, so a cold or failing
|
|
949
|
+
registry read no longer blocks an otherwise valid CRUD/relationship request.
|
|
950
|
+
Required published metadata that is missing still rejects, as do invalid stored
|
|
951
|
+
relationship targets and data-query failures. Data reads/writes continue using
|
|
952
|
+
the active transaction. Borrowed transactions remain active for their owner to
|
|
953
|
+
complete. CRUD arguments and response shapes are unchanged; direct registry
|
|
954
|
+
changes take effect only through the publication steps above.
|
|
955
|
+
|
|
956
|
+
POST, PUT and PATCH also reject an unexpected failure while refreshing
|
|
957
|
+
`context.minimalRecord` after writing. Previously this failure could be logged
|
|
958
|
+
and ignored, allowing a successful response and completion hooks with stale
|
|
959
|
+
state. The cause chain retains the failure and resource context with phase
|
|
960
|
+
`postWriteRead`; typed errors retain their identity within that chain. This refresh runs for
|
|
961
|
+
`returning: 'full'`, `'minimal'` and `'none'`. It happens before finish hooks and
|
|
962
|
+
owned commit, so a failure rolls back owned changes, including relationship
|
|
963
|
+
updates. Borrowed transactions remain active for the caller to complete.
|
|
964
|
+
|
|
965
|
+
Callers need no new option. Handle the rejected operation and correct the
|
|
966
|
+
metadata/storage failure instead of relying on partial success. Custom
|
|
967
|
+
`dataGetMinimal` helpers must propagate read failures; they cannot rely on the
|
|
968
|
+
library ignoring them. Express and Fastify send an error response for failures
|
|
969
|
+
in request data reads and writes (500 for unexpected failures, the original
|
|
970
|
+
status for typed errors). Configuration-read failures outside a request do not
|
|
971
|
+
invalidate an already published resource. These rules do not change post-commit
|
|
972
|
+
outcome uncertainty.
|
|
973
|
+
|
|
974
|
+
## Verify the port
|
|
975
|
+
|
|
976
|
+
1. Confirm the library resolves to the intended v2 artifact, and each app resolves
|
|
977
|
+
to the matching versions of its shared repositories and generated code.
|
|
978
|
+
2. Run library checks and affected application tests; an old installed
|
|
979
|
+
dependency passing tests does not verify the port.
|
|
980
|
+
3. Exercise a create/read/update/delete flow, scoped authorization, and a complete
|
|
981
|
+
pagination traversal in each relevant app. Check a multi-operation rollback
|
|
982
|
+
wherever transaction ownership changes.
|
|
983
|
+
4. Regenerate affected source and confirm it emits the new calls directly.
|
|
984
|
+
5. Recheck call sites added during the migration, and remove obsolete wrappers
|
|
985
|
+
and imports once their replacements pass.
|
|
986
|
+
|
|
987
|
+
Record the verification commands and artifact versions with each application
|
|
988
|
+
upgrade. Apply these steps to seed generators and generated templates as well
|
|
989
|
+
as checked-in application code.
|
|
990
|
+
|
|
991
|
+
## TypeScript imports
|
|
992
|
+
|
|
993
|
+
The package exposes declarations through `json-rest-api` itself:
|
|
994
|
+
|
|
995
|
+
```ts
|
|
996
|
+
import { RestApiPlugin, RestApiWriteError } from 'json-rest-api'
|
|
997
|
+
import type { RestApiPluginOptions, ResourceCoreMethods, ResourceVersionOptions } from 'json-rest-api'
|
|
998
|
+
|
|
999
|
+
const restOptions = {
|
|
1000
|
+
format: 'plain',
|
|
1001
|
+
returning: 'minimal'
|
|
1002
|
+
} satisfies RestApiPluginOptions
|
|
1003
|
+
|
|
1004
|
+
// The resource schema must also declare revision as a stored string attribute.
|
|
1005
|
+
const versionOptions = { versionField: 'revision' } satisfies ResourceVersionOptions
|
|
1006
|
+
|
|
1007
|
+
interface Book { title: string }
|
|
1008
|
+
async function readTitle(books: ResourceCoreMethods<Book>, id: string) {
|
|
1009
|
+
const book = await books.get({ id })
|
|
1010
|
+
return book.title // string | undefined: sparse fields can omit title
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
Use normal value imports for plugins, storage classes and errors; use type-only
|
|
1015
|
+
imports for option and resource interfaces. No runtime files live under the
|
|
1016
|
+
declaration `types/` directory. TypeScript consumers need Node and Knex types for
|
|
1017
|
+
the current declaration surface. Framework peers are still optional at runtime.
|
|
1018
|
+
|
|
1019
|
+
For ordinary literal schemas, retain the handle returned by `addResource()` to
|
|
1020
|
+
infer attribute names and value types. `ResourceSchema`, `InferInput` and
|
|
1021
|
+
`InferOutput` also support reusable definitions. Existing JavaScript setup and
|
|
1022
|
+
call syntax stay the same. The library's `npm run test:public-types` checks these
|
|
1023
|
+
declarations through a packed consumer.
|
|
1024
|
+
|
|
1025
|
+
An explicit `JsonRestApi` resource map remains the choice for typed central
|
|
1026
|
+
registries, relationships, dynamic enrichment, method replacements and custom
|
|
1027
|
+
API-level defaults. Match its format/returning generics to the actual resource
|
|
1028
|
+
configuration. Plugin installation itself does not infer options or methods;
|
|
1029
|
+
use `satisfies` with the plugin's option interface. Literal resource registration
|
|
1030
|
+
checks built-in field and resource options. Custom plugin configuration uses an
|
|
1031
|
+
explicit intersection with its own interface. See the
|
|
1032
|
+
[typing contract and limits](../API.md#typescript-resources-and-hooks).
|
|
1033
|
+
|
|
1034
|
+
Known hooks now describe phase-specific input and library-owned fields. This
|
|
1035
|
+
does not change mutable context at runtime. If a hook receives an ownership type
|
|
1036
|
+
error, inspect the documented mutation phase rather than casting it away.
|
|
1037
|
+
Use `HookHandler<Event, AppContext>` for known hooks and
|
|
1038
|
+
`RuntimeHook<PluginContext>` for arbitrary plugin events; see the
|
|
1039
|
+
[hook guide](13-hooks-and-lifecycle.md).
|
|
1040
|
+
|
|
1041
|
+
## Object and array attributes
|
|
1042
|
+
|
|
1043
|
+
Declare structured fields using their public JSON type:
|
|
1044
|
+
|
|
1045
|
+
```js
|
|
1046
|
+
schema: {
|
|
1047
|
+
metadata: { type: 'object', nullable: true },
|
|
1048
|
+
tags: { type: 'array', nullable: true }
|
|
1049
|
+
}
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
Built-in storage writes JSON text and reads it back as objects or arrays, in
|
|
1053
|
+
both plain and JSON:API responses. Nested JSON values, empty objects/arrays and
|
|
1054
|
+
SQL null are preserved. Canonical storage now accepts arrays in its existing
|
|
1055
|
+
JSON slots; adding this support does not alter existing tables.
|
|
1056
|
+
|
|
1057
|
+
Remove setters whose only purpose was `JSON.stringify`. A setter for an object
|
|
1058
|
+
or array must return that logical type; it can still prepare or transform its
|
|
1059
|
+
contents. Reads now return structured values, so remove application-level
|
|
1060
|
+
`JSON.parse` calls that expected these fields to be strings.
|
|
1061
|
+
|
|
1062
|
+
```js
|
|
1063
|
+
// Before: storage conversion was done in a setter.
|
|
1064
|
+
metadata: { type: 'object', setter: value => JSON.stringify(value) }
|
|
1065
|
+
|
|
1066
|
+
// After: built-in storage owns JSON conversion.
|
|
1067
|
+
metadata: { type: 'object' }
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
If a custom storage representation is necessary, keep its conversion in
|
|
1071
|
+
`storage.serialize` and decode it with a getter. The getter receives the driver's
|
|
1072
|
+
stored value: native JSON columns may return an object, while text columns
|
|
1073
|
+
return a string. For example:
|
|
1074
|
+
|
|
1075
|
+
```js
|
|
1076
|
+
metadata: {
|
|
1077
|
+
type: 'object',
|
|
1078
|
+
nullable: true,
|
|
1079
|
+
storage: {
|
|
1080
|
+
serialize: value => value == null ? null : JSON.stringify({ wrapped: value })
|
|
1081
|
+
},
|
|
1082
|
+
getter: value => value == null
|
|
1083
|
+
? null
|
|
1084
|
+
: (typeof value === 'string' ? JSON.parse(value) : value).wrapped
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
Getters decode custom representations before the final declared shape check.
|
|
1089
|
+
Built-in structured values are decoded before getters on dependent fields and
|
|
1090
|
+
computed callbacks run. A stored value with the wrong declared shape fails the
|
|
1091
|
+
read. Built-in writes reject wrong-shaped setter results, circular references
|
|
1092
|
+
and BigInt values before SQL, including with `returning: 'none'`. Supply JSON
|
|
1093
|
+
compatible contents; the default serializer uses JavaScript `JSON.stringify`.
|
|
1094
|
+
|
|
1095
|
+
### Querying structured attributes
|
|
1096
|
+
|
|
1097
|
+
Whole-object/array equality and ordering are not generic library operations.
|
|
1098
|
+
Remove `search: true`, generic search aliases/`oneOf` references, explicit
|
|
1099
|
+
`sortableFields`, and default sorts targeting those fields. A structured
|
|
1100
|
+
projection cannot declare `sortable: true`. Local invalid declarations reject
|
|
1101
|
+
schema compilation; relationship paths reject when resolved, allowing forward
|
|
1102
|
+
resource declarations. A request to sort a structured field returns a validation
|
|
1103
|
+
error (HTTP 422) before SQL. Direct adapter comparison conversion also rejects
|
|
1104
|
+
structured fields, including null and empty arrays, before invoking serializers.
|
|
1105
|
+
Array operands for `in` and `between` on scalar stored fields remain supported.
|
|
1106
|
+
|
|
1107
|
+
Expose the JSON key that gives the application a meaningful order as a scalar
|
|
1108
|
+
projection. For example, this selects an integer `metadata.value`:
|
|
1109
|
+
|
|
1110
|
+
```js
|
|
1111
|
+
queryFields: {
|
|
1112
|
+
metadataValue: {
|
|
1113
|
+
type: 'integer',
|
|
1114
|
+
sortable: true,
|
|
1115
|
+
select: ({ knex, column }) => {
|
|
1116
|
+
const field = column('metadata')
|
|
1117
|
+
if (knex.client.config.client === 'pg') {
|
|
1118
|
+
return knex.raw('CAST(CAST(?? AS json) #>> ?::text[] AS integer)', [field, '{value}'])
|
|
1119
|
+
}
|
|
1120
|
+
if (knex.client.config.client === 'mysql2') {
|
|
1121
|
+
return knex.raw('CAST(JSON_UNQUOTE(JSON_EXTRACT(??, ?)) AS SIGNED)', [field, '$.value'])
|
|
1122
|
+
}
|
|
1123
|
+
return knex.raw('CAST(json_extract(??, ?) AS INTEGER)', [field, '$.value'])
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
Then use `queryParams: { sort: ['metadataValue'], page: { size: 20 } }`.
|
|
1130
|
+
Scalar projections support sparse output and forward/backward cursor traversal.
|
|
1131
|
+
Sorting and cursor comparisons operate on the selected SQL value; output getters
|
|
1132
|
+
format that value afterwards. Custom storage serializers must produce values
|
|
1133
|
+
compatible with the physical column, and custom scalar cursors must satisfy the
|
|
1134
|
+
declared scalar type. A getter does not make an incompatible stored cursor valid.
|
|
1135
|
+
|
|
1136
|
+
For application-specific JSON filters, use `searchSchema.applyFilter` with an
|
|
1137
|
+
explicit SQL predicate and bound values. This callback owns the predicate's
|
|
1138
|
+
meaning and value encoding. For example, a null-presence filter for built-in JSON
|
|
1139
|
+
storage can resolve the physical column without hard-coding canonical slots:
|
|
1140
|
+
|
|
1141
|
+
```js
|
|
1142
|
+
searchSchema: {
|
|
1143
|
+
hasMetadata: {
|
|
1144
|
+
type: 'boolean',
|
|
1145
|
+
applyFilter: (query, present) => {
|
|
1146
|
+
const column = api.knex.helpers.getStorageAdapter('books').translateColumn('metadata')
|
|
1147
|
+
return present ? query.whereNotNull(column) : query.whereNull(column)
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
The example belongs to the `books` resource. Use the query hook's adapter and
|
|
1154
|
+
alias when a custom filter introduces joins. Whole-document custom comparisons
|
|
1155
|
+
must account for the chosen database's JSON semantics.
|
|
1156
|
+
|
|
1157
|
+
PostgreSQL JSON selections now use `to_jsonb` so relationship filters can remove
|
|
1158
|
+
duplicate parents with `DISTINCT`. This applies to stored structured fields and
|
|
1159
|
+
structured projections. Public object/array values are preserved, and existing
|
|
1160
|
+
tables are not altered. Custom structured storage should use JSON-compatible SQL
|
|
1161
|
+
representations, such as JSON columns or JSON-encoded text.
|
|
1162
|
+
|
|
1163
|
+
## Temporal values and storage serializers
|
|
1164
|
+
|
|
1165
|
+
Use JSON values in both plain and JSON:API write records:
|
|
1166
|
+
|
|
1167
|
+
| Field type | Input example | Public read value |
|
|
1168
|
+
| --- | --- | --- |
|
|
1169
|
+
| `date` | `'2024-02-29'` | `'2024-02-29'` |
|
|
1170
|
+
| `dateTime` | `'2024-03-01T07:59:59.987+08:00'` | `'2024-02-29T23:59:59.987Z'` |
|
|
1171
|
+
| `time` | `'09:30'` or `'09:30:00.123'` | `'09:30:00'` or `'09:30:00.123'` without a declared precision |
|
|
1172
|
+
| `epochMilliseconds` | `1709251199987` or `'1709251199987'` | `1709251199987` |
|
|
1173
|
+
| `epochSeconds` | `1709251199` or `'1709251199'` | `1709251199` |
|
|
1174
|
+
|
|
1175
|
+
Client date/dateTime fields take strings, not JavaScript Date objects or SQL
|
|
1176
|
+
datetime text. Use `null` to clear a nullable field. Epochs must be integers:
|
|
1177
|
+
zero and negative epochs are valid; the absolute limits are 8640000000000000
|
|
1178
|
+
milliseconds and 8640000000000 seconds. Calendar dates must exist. DateTime
|
|
1179
|
+
offsets must normalize to a UTC year between 0000 and 9999; values outside that
|
|
1180
|
+
range now fail validation before built-in storage, including writes requesting no record.
|
|
1181
|
+
|
|
1182
|
+
`temporalPrecision` limits fractional digits. Built-in dateTime conversion uses
|
|
1183
|
+
JavaScript Date and supports milliseconds. It rejects nonzero submillisecond
|
|
1184
|
+
digits; declaring precision 6 alone does not enable six-digit writes. A value
|
|
1185
|
+
ending in `.123000Z` can be stored as `.123Z`. Database strings can retain finer
|
|
1186
|
+
precision when read. Time fields remain strings and do not require a Date
|
|
1187
|
+
conversion. Getters, computed fields and response hooks may return native Date
|
|
1188
|
+
or BigInt values; the declared temporal field type normalizes the final response
|
|
1189
|
+
and rejects invalid values.
|
|
1190
|
+
|
|
1191
|
+
Keep `storage.serialize(value, details)` functions on stored attributes.
|
|
1192
|
+
Both storage modes honor them for writes and filters.
|
|
1193
|
+
The function receives the logical `fieldName`, actual `columnName`, `definition`,
|
|
1194
|
+
`schemaInfo`, write `context`, and `operation` (`post`, `put`, `patch` or `filter`).
|
|
1195
|
+
Comparisons use `operation: 'filter'` and a null context. Return the same storage
|
|
1196
|
+
representation for equivalent values on writes and comparisons. The callback
|
|
1197
|
+
must be synchronous; returning a Promise now fails before the SQL operation,
|
|
1198
|
+
including when no write response is requested. Use field setters for asynchronous preparation. Custom temporal
|
|
1199
|
+
serializers must preserve the required precision, normalize offsets consistently,
|
|
1200
|
+
and return values supported by the chosen SQL column/driver and read normalizer.
|
|
1201
|
+
|
|
1202
|
+
### Temporal formatting and native databases
|
|
1203
|
+
|
|
1204
|
+
Time reads now always include seconds. With `temporalPrecision: 3`, both
|
|
1205
|
+
`'09:30'` and `'09:30:00.000'` read as `'09:30:00.000'` and compare equally.
|
|
1206
|
+
Without a declared precision, redundant fractional zeros are removed:
|
|
1207
|
+
`'09:30:00.100000'` reads as `'09:30:00.1'`. Update assertions that depended on
|
|
1208
|
+
the submitted spelling. DateTime reads use UTC, retain meaningful microseconds,
|
|
1209
|
+
and keep at least millisecond digits unless a smaller precision is declared;
|
|
1210
|
+
`.100000Z` reads as `.100Z`, while `.123456Z` remains unchanged.
|
|
1211
|
+
|
|
1212
|
+
Library reads request temporal strings from PostgreSQL/MySQL so driver Date
|
|
1213
|
+
conversion cannot discard microseconds or shift calendar dates. These options
|
|
1214
|
+
apply to library queries; the supplied Knex instance's connection configuration
|
|
1215
|
+
and ordinary caller queries are unchanged. PostgreSQL uses its normal ISO date
|
|
1216
|
+
output. Built-in PostgreSQL writes/comparisons bind explicit UTC values and map
|
|
1217
|
+
public year 0000 to PostgreSQL's 1 BC. MySQL native DATE/DATETIME writes reject
|
|
1218
|
+
years below 1000 with `storage_range`, following its documented supported range.
|
|
1219
|
+
Use a custom text column/serializer if the application needs earlier MySQL dates.
|
|
1220
|
+
See [node-postgres temporal parsing](https://node-postgres.com/features/types)
|
|
1221
|
+
and [MySQL date ranges](https://dev.mysql.com/doc/refman/8.0/en/datetime.html).
|
|
1222
|
+
|
|
1223
|
+
New table creation and generated migrations use fractional precision 6 for
|
|
1224
|
+
dateTime/time columns when no precision is declared. An explicit precision
|
|
1225
|
+
still takes precedence. This avoids MySQL's default precision 0 rounding a
|
|
1226
|
+
timestamp into the next second or day. Existing regular tables need an ordinary
|
|
1227
|
+
schema migration to the intended precision; table creation does not alter them.
|
|
1228
|
+
Preserve each column's nullability, defaults and other modifiers in that migration.
|
|
1229
|
+
The regular-table diff now detects temporal precision changes. PostgreSQL time
|
|
1230
|
+
columns created through older Knex helpers may report precision 6 even when the
|
|
1231
|
+
resource declared precision 3: the previous time builder ignored that argument.
|
|
1232
|
+
New table creation and migration code declare `time(n)` explicitly. Inspect live
|
|
1233
|
+
precision before changing existing columns; reducing it can round stored values.
|
|
1234
|
+
|
|
1235
|
+
Native `setValues` columns require an explicit MySQL target when generating a
|
|
1236
|
+
migration, for example `{ dialect: 'mysql2' }`. Runtime create/add/alter helpers
|
|
1237
|
+
reject SET columns on other clients before SQL. A cross-dialect migration diff
|
|
1238
|
+
continues to warn and omit unsupported SET changes; review those warnings rather
|
|
1239
|
+
than treating the generated supported subset as a complete schema migration.
|
|
1240
|
+
|
|
1241
|
+
Schema helpers now reject invalid `temporalPrecision` before executing SQL.
|
|
1242
|
+
Native PostgreSQL/MySQL time/dateTime columns require an integer from 0 through 6.
|
|
1243
|
+
For finer precision, use a text column and an appropriate storage mapping. When
|
|
1244
|
+
intentionally generating a higher-precision SQLite definition, pass an explicit
|
|
1245
|
+
`dialect: 'better-sqlite3'`; an unspecified migration target is rejected for
|
|
1246
|
+
precision above 6. Migration diffs use their explicit or introspected dialect.
|
|
1247
|
+
Runtime create/add/alter helpers use the actual database client, regardless of a
|
|
1248
|
+
supplied migration-dialect option. SQLite column declarations remain distinct
|
|
1249
|
+
from the built-in dateTime converter's millisecond write limit.
|
|
1250
|
+
|
|
1251
|
+
Increasing precision cannot restore digits already discarded by older writes.
|
|
1252
|
+
Native PostgreSQL/MySQL TIME columns reject input with more than six meaningful
|
|
1253
|
+
fractional digits before writing; SQLite and AnyAPI text time slots can retain
|
|
1254
|
+
finer values. [MySQL fractional-second behavior](https://dev.mysql.com/doc/refman/8.0/en/fractional-seconds.html)
|
|
1255
|
+
also explains why directly injecting extra digits into a lower-precision column
|
|
1256
|
+
can round them before the library reads them.
|
|
1257
|
+
|
|
1258
|
+
For custom PostgreSQL serializers, include an offset on timestamp-with-time-zone
|
|
1259
|
+
values, such as `'2024-02-29T23:59:59.123456Z'`. Offset-free timestamp input uses
|
|
1260
|
+
the server session's time zone. A custom serializer must make that choice
|
|
1261
|
+
explicit and use the same representation for comparisons.
|
|
1262
|
+
|
|
1263
|
+
Query projections compare the SQL expression's stored value directly, retaining
|
|
1264
|
+
microsecond precision without a write serializer. Remove `storage` from
|
|
1265
|
+
`queryFields` declarations; it is now rejected during compilation. Keep the
|
|
1266
|
+
serializer on the underlying stored attribute.
|
|
1267
|
+
|
|
1268
|
+
### Custom serializer cursors and projection getters
|
|
1269
|
+
|
|
1270
|
+
Cursors for serialized attributes and SQL projections now retain the database
|
|
1271
|
+
value before getters run. Following a cursor binds that value directly instead
|
|
1272
|
+
of serializing it again. For example, a serializer that stores `"alpha"` as
|
|
1273
|
+
`"stored:alpha"` previously produced a next-page comparison against
|
|
1274
|
+
`"stored:stored:alpha"`. Numeric scaling had the same problem.
|
|
1275
|
+
|
|
1276
|
+
Restart pagination after upgrading and use returned cursors or links unchanged.
|
|
1277
|
+
Do not build a cursor from response attributes: getters can change the value,
|
|
1278
|
+
and database temporal spelling can differ from the JSON representation. Scalar
|
|
1279
|
+
cursor validation still checks the declared type; temporal validation accepts
|
|
1280
|
+
the supported database spelling without changing the bound value. Sort order
|
|
1281
|
+
is the SQL value's order. A getter does not change that order.
|
|
1282
|
+
|
|
1283
|
+
Projection declarations no longer take `storage`, `setter`, or `runSetterAfter`.
|
|
1284
|
+
For example, this precise timestamp projection needs no serializer:
|
|
1285
|
+
|
|
1286
|
+
```javascript
|
|
1287
|
+
queryFields: {
|
|
1288
|
+
preciseTimestamp: {
|
|
1289
|
+
type: 'dateTime',
|
|
1290
|
+
temporalPrecision: 6,
|
|
1291
|
+
sortable: true,
|
|
1292
|
+
select: ({ knex, column }) => knex.raw('??', [column('recordedAt')])
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
Projection `getter` and `runGetterAfter` declarations now execute through the
|
|
1298
|
+
same compiled dependency order as stored-field getters. They were previously
|
|
1299
|
+
ignored. Getters may be async; computed fields receive their completed values.
|
|
1300
|
+
Remove a previously ignored getter if its behavior was not intended. Invalid
|
|
1301
|
+
callbacks/dependencies reject compilation, and getter failures propagate as
|
|
1302
|
+
read errors before computed output runs.
|
|
1303
|
+
|
|
1304
|
+
Resource ID fields and relationship backing ID/type fields cannot declare
|
|
1305
|
+
`storage.serialize`, including declarations added by enrichment or canonical
|
|
1306
|
+
field additions. The previous behavior could change an ordinary foreign key
|
|
1307
|
+
while canonical storage bypassed the write callback and only serialized filters.
|
|
1308
|
+
Remove these serializers; use resource `normalizeId` for ID canonicalization.
|
|
1309
|
+
Already transformed stored IDs require an explicit application data migration;
|
|
1310
|
+
removing a callback does not repair existing data automatically.
|
|
1311
|
+
|
|
1312
|
+
### Existing AnyAPI temporal data
|
|
1313
|
+
|
|
1314
|
+
New AnyAPI resources allocate `date` and `time` fields to string slots. Calendar
|
|
1315
|
+
dates and times of day do not need a timestamp column. `dateTime` fields retain
|
|
1316
|
+
the `date_N` slots, now with fractional precision 6. Existing metadata assigning
|
|
1317
|
+
date/time fields to timestamp slots, or native canonical timestamp columns with
|
|
1318
|
+
lower precision, causes an explicit migration-required error.
|
|
1319
|
+
Resource re-registration checks the persisted mapping before replacing it, so
|
|
1320
|
+
starting the new API cannot silently rebind unmigrated temporal fields to empty
|
|
1321
|
+
slots. Complete the data migration before registering those resources again.
|
|
1322
|
+
|
|
1323
|
+
For disposable development data, recreate the AnyAPI database and run the seeds
|
|
1324
|
+
against the new schema. For data that must be preserved:
|
|
1325
|
+
|
|
1326
|
+
1. Back up the database and stop application writers for the migration.
|
|
1327
|
+
2. Inventory date/time fields from `any_resource_configs.schema_json` and their
|
|
1328
|
+
mappings in `any_field_configs`. Reserve unused `string_N` slots per resource;
|
|
1329
|
+
the existing limit is ten string slots. Update any explicit
|
|
1330
|
+
`canonicalFieldsMap` declarations to those allocations.
|
|
1331
|
+
3. Copy each old field into its reserved string slot on `any_records`, selecting
|
|
1332
|
+
only that metadata row's tenant and resource. Use `YYYY-MM-DD` for dates and
|
|
1333
|
+
the time format above for time-only values. Inspect the raw stored values
|
|
1334
|
+
and the original write time zone: an old API read may already have shifted a
|
|
1335
|
+
date or discarded precision. Keep the old slots until verification is complete.
|
|
1336
|
+
4. Update that field's `slot_type` to `string`, `slot_index` to the reserved
|
|
1337
|
+
index, and `slot_column` to its `string_N` name. Keep logical IDs, tenant/resource
|
|
1338
|
+
identifiers and relationship rows unchanged. Do not recreate metadata for
|
|
1339
|
+
unrelated fields or renumber their slots.
|
|
1340
|
+
5. On PostgreSQL/MySQL, migrate `any_records.date_1` through `date_5` to timestamp/
|
|
1341
|
+
datetime precision 6, preserving the PostgreSQL time-zone type and column
|
|
1342
|
+
modifiers. Even an unused canonical timestamp slot must have precision 6.
|
|
1343
|
+
6. Restart the library to reload descriptors. Verify row counts, date/time
|
|
1344
|
+
values, nulls, relationships, exact filters and complete cursor traversals
|
|
1345
|
+
against the backup before resuming writers.
|
|
1346
|
+
|
|
1347
|
+
The repository includes an executable example at
|
|
1348
|
+
`examples/migrations/anyapi-temporal-v2.js`. Its named
|
|
1349
|
+
`migrateAnyApiTemporalFields(transaction, moves)` function implements steps 3–4
|
|
1350
|
+
in batches of 100 records. It requires an explicit source/target mapping and a
|
|
1351
|
+
conversion callback for each field. It checks the persisted field type and source
|
|
1352
|
+
mapping, rejects occupied targets (including unassigned slots containing data),
|
|
1353
|
+
validates converted strings against the declared temporal precision, and leaves
|
|
1354
|
+
old slots and unrelated metadata intact. Nulls are copied without invoking the
|
|
1355
|
+
callback. Soft-deleted records are included.
|
|
1356
|
+
|
|
1357
|
+
For example, **only if the original application stored dates at UTC midnight
|
|
1358
|
+
and times as UTC-anchored timestamps or bare time strings**, the conversion can be:
|
|
1359
|
+
|
|
1360
|
+
```js
|
|
1361
|
+
import { migrateAnyApiTemporalFields } from 'json-rest-api/examples/migrations/anyapi-temporal-v2.js'
|
|
1362
|
+
import { normalizeDateValue } from 'json-rest-api/plugins/core/lib/querying-writing/database-value-normalizers.js'
|
|
1363
|
+
|
|
1364
|
+
const moves = [
|
|
1365
|
+
{
|
|
1366
|
+
tenant: 'your-tenant', resource: 'events', field: 'day',
|
|
1367
|
+
from: 'date_1', to: 'string_8',
|
|
1368
|
+
convert: value => normalizeDateValue(value, 'date')
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
tenant: 'your-tenant', resource: 'events', field: 'atTime',
|
|
1372
|
+
from: 'date_2', to: 'string_10',
|
|
1373
|
+
convert: value => typeof value === 'string' && /^\d{2}:/.test(value)
|
|
1374
|
+
? value
|
|
1375
|
+
: normalizeDateValue(value, 'dateTime').slice(11, -1)
|
|
1376
|
+
}
|
|
1377
|
+
]
|
|
1378
|
+
const report = await knex.transaction(trx => migrateAnyApiTemporalFields(trx, moves))
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
Replace every name and slot with the inventory from your database. Converters
|
|
1382
|
+
receive the raw value and `{ tenant, resource, field, id }`, may be asynchronous,
|
|
1383
|
+
and must return a valid string for every non-null value. PostgreSQL/MySQL reads
|
|
1384
|
+
retain raw temporal strings so the driver cannot discard microseconds before
|
|
1385
|
+
conversion. Use a different converter for a local calendar/time convention or
|
|
1386
|
+
custom serializer; the example does not infer the original time zone. Update
|
|
1387
|
+
any serializer that still expects the old timestamp representation.
|
|
1388
|
+
|
|
1389
|
+
Let a conversion error escape the transaction callback so Knex rolls back all
|
|
1390
|
+
copied records and mapping changes. The example neither commits nor rolls back
|
|
1391
|
+
the caller's transaction. Rerunning completed moves fails the source-mapping
|
|
1392
|
+
check; inspect metadata instead of treating that error as success.
|
|
1393
|
+
|
|
1394
|
+
For step 5, the following is the tested precision change for **standard,
|
|
1395
|
+
nullable canonical columns with no defaults and Knex's default timezone type**:
|
|
1396
|
+
|
|
1397
|
+
```js
|
|
1398
|
+
// PostgreSQL/MySQL only; run separately from the data transaction.
|
|
1399
|
+
await knex.schema.alterTable('any_records', table => {
|
|
1400
|
+
for (let index = 1; index <= 5; index++) {
|
|
1401
|
+
table.dateTime(`date_${index}`, { precision: 6 }).nullable().alter()
|
|
1402
|
+
}
|
|
1403
|
+
})
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
Inspect column definitions first and adapt this DDL if their type, nullability,
|
|
1407
|
+
default or other modifiers differ. MySQL DDL can commit implicitly; the copy
|
|
1408
|
+
transaction and this precision change are separate operations. Keep writers
|
|
1409
|
+
stopped if either fails. Increasing precision preserves existing values but
|
|
1410
|
+
cannot recover fractional digits lost in earlier writes.
|
|
1411
|
+
|
|
1412
|
+
In the restarted resource declarations, preserve **all** existing allocations in
|
|
1413
|
+
`canonicalFieldsMap`, changing only the migrated fields. A supplied map must
|
|
1414
|
+
include every stored non-ID field. For the test resource, the complete map is:
|
|
1415
|
+
|
|
1416
|
+
```js
|
|
1417
|
+
canonicalFieldsMap: {
|
|
1418
|
+
name: 'string_1', day: 'string_8', atTime: 'string_10',
|
|
1419
|
+
occurredAt: 'date_3', personId: 'rel_1_id'
|
|
1420
|
+
}
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
`tests/anyapi-temporal-migration.test.js` executes this example and the standard
|
|
1424
|
+
precision DDL on SQLite, PostgreSQL and MySQL, with fresh API initialization,
|
|
1425
|
+
overlapping logical IDs in two tenants, relationships, nulls, exact filters,
|
|
1426
|
+
forward/backward cursors, failed conversions and caller rollback. These are
|
|
1427
|
+
isolated synthetic migrations. Verify the conversion against a copy of each
|
|
1428
|
+
application database and update its seed generators before deployment.
|
|
1429
|
+
|
|
1430
|
+
AnyAPI's duplicate private filter pass and its unused deep helper exports
|
|
1431
|
+
`coerceValueForDefinition`, `normalizeFilterValues` and `ensureFilterableField`
|
|
1432
|
+
were removed. Declare filters in the resource/search schema and use ordinary
|
|
1433
|
+
resource queries. Remove direct imports of these private helpers if your
|
|
1434
|
+
application has any.
|
|
1435
|
+
|
|
1436
|
+
## Schema migration helpers
|
|
1437
|
+
|
|
1438
|
+
`generateKnexMigrationDiff()` now preserves undeclared indexes and foreign keys
|
|
1439
|
+
by default, as it already did for columns. A resource declaration need not
|
|
1440
|
+
describe every constraint in an application's database. Check `warnings` for
|
|
1441
|
+
preserved metadata. For an intentional removal, pass the corresponding boolean
|
|
1442
|
+
in `options`: `allowDropColumns`, `allowDropIndexes` or `allowDropForeignKeys`.
|
|
1443
|
+
These options reject non-boolean values. Explicit changes to a named index or
|
|
1444
|
+
foreign key can still produce a replacement, with a warning.
|
|
1445
|
+
|
|
1446
|
+
Column drops reject when their plan retains a dependent index or foreign key.
|
|
1447
|
+
Full snapshots also reject generated columns and SQLite partial/expression
|
|
1448
|
+
indexes, instead of reporting an incomplete comparison of those features.
|
|
1449
|
+
Use an authored migration for database features outside the snapshot model.
|
|
1450
|
+
Review warnings about required-column backfills and narrowing integer ranges.
|
|
1451
|
+
Generation does not execute DDL; keep migration execution/history in the app's
|
|
1452
|
+
Knex workflow and write recovery steps deliberately. See
|
|
1453
|
+
[diff migrations](21-schema-and-migrations.md#diff-migrations).
|
|
1454
|
+
|
|
1455
|
+
Regular `addKnexFields` and `alterKnexFields` now honor resource-level
|
|
1456
|
+
`storage.naming`. Under `naming: 'exact'`, `loginCount` addresses that exact
|
|
1457
|
+
column. Inspect tables previously changed through these helpers for unintended
|
|
1458
|
+
snake-case columns; this fix does not rename or move existing data. Retain
|
|
1459
|
+
explicit `storage.column` mappings in supplied field definitions.
|
|
1460
|
+
|
|
1461
|
+
AnyAPI `addKnexFields({ fields, canonicalFieldsMap?, searchSchema? })` now persists
|
|
1462
|
+
added definitions and refreshes the compiled schema, including defaults,
|
|
1463
|
+
transforms and cached request contracts. A multi-field call uses one metadata
|
|
1464
|
+
transaction. Existing records remain unchanged; backfill old rows separately.
|
|
1465
|
+
Missing `fields`, duplicate fields and invalid maps now throw instead of silently
|
|
1466
|
+
succeeding or leaving unusable metadata. See
|
|
1467
|
+
[AnyAPI field additions](21-schema-and-migrations.md#anyapi-field-additions).
|
|
1468
|
+
|
|
1469
|
+
Explicit maps take effect during registration, before `createKnexTable()`.
|
|
1470
|
+
Repeating that method no longer re-registers the initial schema and discards
|
|
1471
|
+
additions. Keep the expanded declaration and complete map in app source for
|
|
1472
|
+
restart, including functions that JSON metadata cannot reconstruct.
|
|
1473
|
+
|
|
1474
|
+
Deep `AnyapiRegistry` writes must receive a transaction from `api.transaction`
|
|
1475
|
+
when joining an existing unit. `addKnexFields()` uses this owner for its metadata
|
|
1476
|
+
batch internally. Direct registry callers must invalidate the registry descriptor
|
|
1477
|
+
cache after confirmed commit. Transaction-local reads use that
|
|
1478
|
+
transaction and bypass the global cache, preventing rolled-back metadata from
|
|
1479
|
+
leaking through cached descriptors. This does not publish new resource metadata.
|
|
1480
|
+
Resource operations use `resource.vars.schemaInfo.descriptor`, the same published
|
|
1481
|
+
mapping as their storage adapter. CRUD, counts, includes and relationship calls
|
|
1482
|
+
do not query configuration tables or apply direct registry changes implicitly.
|
|
1483
|
+
Register related resources on the API before querying them; persisted registry
|
|
1484
|
+
entries alone do not supply runtime resource declarations.
|
|
1485
|
+
|
|
1486
|
+
Use `addKnexFields()` for supported field additions; it commits metadata before
|
|
1487
|
+
publishing the new compiled schema and adapter. After a direct, committed mapping
|
|
1488
|
+
migration with the same declarations, invalidate the registry cache and call
|
|
1489
|
+
`createKnexTable()` to refresh the descriptor. A failed refresh retains the
|
|
1490
|
+
previous published resource. Structural schema changes also need updated source
|
|
1491
|
+
declarations and compilation; invalidation alone cannot reconstruct callbacks.
|
|
1492
|
+
Perform configuration/mapping changes while application operations are stopped.
|
|
1493
|
+
Arbitrary in-place metadata edits or concurrent reconfiguration are not a
|
|
1494
|
+
supported publication mechanism. No automatic data migration is performed.
|
|
1495
|
+
|
|
1496
|
+
The internal `preloadRelatedDescriptors` helper now accepts
|
|
1497
|
+
`{ descriptor, scopes }` and returns its map synchronously; `scopes` is the
|
|
1498
|
+
resource collection. It no longer accepts registry/transaction arguments.
|
|
1499
|
+
`AnyapiQueryAdapter` no longer stores its unused registry argument. Deep callers
|
|
1500
|
+
must use the published resource metadata when building related query maps.
|
|
1501
|
+
|
|
1502
|
+
Owned registry registration/allocation now preserves the original failure when
|
|
1503
|
+
rollback also rejects; secondary diagnostics go to the registry error logger.
|
|
1504
|
+
Failed owned writes invalidate cached descriptors, including when a commit
|
|
1505
|
+
call rejects after database completion. A completed transaction is not rolled
|
|
1506
|
+
back again, and its next descriptor read reloads persisted metadata. Rejection
|
|
1507
|
+
does not by itself establish that a write was rolled back or can safely be
|
|
1508
|
+
retried. The descriptor cache also isolates tenant/resource pairs containing
|
|
1509
|
+
`::`; this cache-key change does not alter persisted data or method arguments.
|
|
1510
|
+
|
|
1511
|
+
The registry now retains at most 100 recently used descriptors. This is a cache
|
|
1512
|
+
bound, not a resource limit: an evicted descriptor reloads on the next explicit
|
|
1513
|
+
registry lookup, while normal resource operations continue using their published
|
|
1514
|
+
metadata. Direct registry callers must pass `{ transaction }` when borrowing
|
|
1515
|
+
one; do not rely on a warm cache to avoid acquiring another database connection.
|
|
1516
|
+
An explicit `{ bypassCache: true }` read that confirms removal now discards its
|
|
1517
|
+
stale cache entry. It does not remove a running resource or publish a new schema.
|
|
1518
|
+
No data migration or resource declaration change is needed for cache eviction.
|
|
1519
|
+
|
|
1520
|
+
Request-contract reuse now includes the resource name in its key. Deep callers
|
|
1521
|
+
sharing compiled field definitions across resource names receive the correct
|
|
1522
|
+
JSON:API type validation. Temporal normalization still accepts its supported
|
|
1523
|
+
precision declarations; unusual precision simply builds an uncached validator.
|
|
1524
|
+
Neither correction adds a caller option or requires a payload migration.
|
|
1525
|
+
|
|
1526
|
+
Schema inspection now supports PostgreSQL's current schema. Generated migrations
|
|
1527
|
+
preserve BigInt defaults as BigInt literals, and unchanged effective defaults or
|
|
1528
|
+
implicit size metadata no longer cause repeated alterations. Diff plans now
|
|
1529
|
+
include `dropCheckConstraints` for PostgreSQL inline-enum replacements. Generated
|
|
1530
|
+
enum diffs execute on PostgreSQL/MySQL; SQLite diffs report a required table rebuild
|
|
1531
|
+
and retain the existing enum definition. This is distinct from changing only an
|
|
1532
|
+
enum default, which is supported on all three databases. See the
|
|
1533
|
+
[schema and migration guide](21-schema-and-migrations.md) for execution
|
|
1534
|
+
and transaction limits. These helpers do not automatically migrate existing
|
|
1535
|
+
application databases.
|
|
1536
|
+
|
|
1537
|
+
Direct `alterKnexFields` now replaces PostgreSQL inline enum checks correctly,
|
|
1538
|
+
with rollback on failure. SQLite changes that require replacing an enum check
|
|
1539
|
+
throw before the field batch starts; changing only its default still works.
|
|
1540
|
+
For deep-helper callers, PostgreSQL transactions retain ownership through a
|
|
1541
|
+
savepoint. MySQL caller transactions are rejected before their implicit DDL
|
|
1542
|
+
commit, and SQLite borrowed rebuilds require foreign-key enforcement disabled
|
|
1543
|
+
before the transaction starts, with validation/restoration owned by the caller.
|
|
1544
|
+
|
|
1545
|
+
Schema DDL now preserves apostrophes, backslashes and question marks in enum
|
|
1546
|
+
values and static text/JSON/array defaults. Inspect previously generated or
|
|
1547
|
+
applied definitions containing those characters; regenerate faulty migration
|
|
1548
|
+
code and migrate stored data explicitly where necessary. Standalone generators
|
|
1549
|
+
must specify `options.dialect` for values needing dialect-specific SQL escaping.
|
|
1550
|
+
Resource generators infer the dialect from their Knex client.
|
|
1551
|
+
|
|
1552
|
+
MySQL diffs replacing a foreign-key-supporting index now include the dependent
|
|
1553
|
+
key in `dropForeignKeys` and `addForeignKeys`, even if its definition is unchanged.
|
|
1554
|
+
Review the whole plan and pause application writes for these multiple-statement
|
|
1555
|
+
DDL migrations; MySQL does not roll them back as one transaction.
|
|
1556
|
+
|
|
1557
|
+
## Sparse fieldsets include relationships
|
|
1558
|
+
|
|
1559
|
+
Fieldsets now restrict attributes **and relationships** in both formats. Earlier
|
|
1560
|
+
versions rejected valid relationship names yet returned unrequested relationships.
|
|
1561
|
+
Update requests that use both `include` and `fields` to select the relationships
|
|
1562
|
+
their callers read:
|
|
1563
|
+
|
|
1564
|
+
```js
|
|
1565
|
+
// Before: relied on an unrequested author relationship being returned.
|
|
1566
|
+
queryParams: { include: ['author'], fields: { books: 'title' } }
|
|
1567
|
+
|
|
1568
|
+
// After: asks for both the attribute and the relationship.
|
|
1569
|
+
queryParams: {
|
|
1570
|
+
include: ['author'],
|
|
1571
|
+
fields: { books: 'title,author', authors: 'name' }
|
|
1572
|
+
}
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
Use the declared relationship name (`author`), not its storage field (`author_id`).
|
|
1576
|
+
An empty fieldset (`books: ''`) returns no attributes or relationships; omit
|
|
1577
|
+
`books` from `fields` for the normal visible response. IDs remain present.
|
|
1578
|
+
Included resource types obey their own fieldsets, and unrequested computed
|
|
1579
|
+
fields are not evaluated. Full write responses follow the same rules without
|
|
1580
|
+
changing the fields stored by the write.
|
|
1581
|
+
|
|
1582
|
+
JSON:API still permits requested `included` resources when the primary fieldset
|
|
1583
|
+
omits their relationship linkage. Plain output reaches related resources through
|
|
1584
|
+
selected relationship properties, so select each relationship along a nested
|
|
1585
|
+
path. No stored-data migration is required. See the
|
|
1586
|
+
[JSON:API sparse-fieldset rules](https://jsonapi.org/format/#fetching-sparse-fieldsets).
|
|
1587
|
+
|
|
1588
|
+
The internal `findRelationshipDefinition` helper moved from
|
|
1589
|
+
`rest-api-plugin-methods/common.js` to
|
|
1590
|
+
`lib/querying-writing/relationship-contracts.js` under `plugins/core`, so fieldset
|
|
1591
|
+
validation and relationship methods use the same resolver. Update any deep
|
|
1592
|
+
imports; the former module does not forward that export.
|
|
1593
|
+
|
|
1594
|
+
## Generated labels and computed visibility
|
|
1595
|
+
|
|
1596
|
+
`LabelPlugin` now preserves an explicitly declared stored or computed `label`.
|
|
1597
|
+
Automatic labels use public stored values, resolve a global-search alias through
|
|
1598
|
+
its local `actualField`, retain null fallbacks in sparse reads, and fall back to
|
|
1599
|
+
the current resource's logical ID. Hidden, normally hidden and virtual fields
|
|
1600
|
+
are excluded as automatic sources. Applications that need a custom display rule
|
|
1601
|
+
can declare their own computed `label`; see the [label guide](18-labels.md).
|
|
1602
|
+
|
|
1603
|
+
Computed callbacks now receive `{ id, attributes, ... }`, with the record's public
|
|
1604
|
+
logical ID supplied on GET, query and included-resource reads. Existing callbacks
|
|
1605
|
+
can keep their current arguments. Use `id` instead of looking for a custom ID
|
|
1606
|
+
column in `attributes` or using the parent request's ID.
|
|
1607
|
+
|
|
1608
|
+
Declare a computed field's inputs in `dependencies`, especially normally hidden
|
|
1609
|
+
stored fields. Sparse reads no longer fetch every normally hidden field just
|
|
1610
|
+
because one computed field was requested. That fallback could select nonexistent
|
|
1611
|
+
virtual columns and made sparse/full behavior disagree. Declared virtual inputs
|
|
1612
|
+
are not selected as SQL columns.
|
|
1613
|
+
|
|
1614
|
+
Hidden computed fields are no longer returned. Normally hidden computed fields
|
|
1615
|
+
require an explicit sparse-fieldset request. Visibility also applies after
|
|
1616
|
+
virtual input values are added to a full write response. Review callers that
|
|
1617
|
+
accidentally relied on these values being exposed.
|
|
1618
|
+
|
|
1619
|
+
Automatic labels require no data migration. If an older LabelPlugin installation
|
|
1620
|
+
removed an authored stored `label` from the compiled schema, check whether its
|
|
1621
|
+
column/canonical mapping exists and use the normal schema migration helpers to
|
|
1622
|
+
add it if needed. Values that were never persisted cannot be recovered by this
|
|
1623
|
+
code change.
|
|
1624
|
+
|
|
1625
|
+
## Cursor validation and registered schema handlers
|
|
1626
|
+
|
|
1627
|
+
Cursor validation now uses the type handlers and validators captured by the
|
|
1628
|
+
resource's compiled attribute schema. Previously each cursor field created a
|
|
1629
|
+
schema from the current global registry, so changing a registered handler could
|
|
1630
|
+
make cursor validation disagree with the already-compiled resource.
|
|
1631
|
+
|
|
1632
|
+
Register schema extensions before adding resources. Existing compiled resources
|
|
1633
|
+
retain those handler functions; a new resource initialization or supported
|
|
1634
|
+
recompilation captures the current registrations. This does not introduce general
|
|
1635
|
+
live schema editing. Normal cursor spelling, numeric conversion, opaque IDs,
|
|
1636
|
+
null ordering, temporal precision and HTTP validation errors are unchanged.
|
|
1637
|
+
Temporal output conversion keeps its existing separate contracts and precision
|
|
1638
|
+
behavior; cursor validation does not truncate an invalid over-precise token.
|
|
1639
|
+
|
|
1640
|
+
Applications using resource methods need no argument changes. Direct importers
|
|
1641
|
+
of the internal `validateCursorValues` helper must pass the owning schema as its
|
|
1642
|
+
fourth argument:
|
|
1643
|
+
|
|
1644
|
+
```js
|
|
1645
|
+
validateCursorValues(descriptors, values, 'after', scope.vars.schemaInfo.schemaInstance)
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
The helper has no fallback to the global registry. Contract reuse is scoped to
|
|
1649
|
+
that schema instance, and values/errors remain local to each call.
|
|
1650
|
+
|
|
1651
|
+
## Field dependencies and projection metadata
|
|
1652
|
+
|
|
1653
|
+
Getter/setter declarations keep their callback signatures and use
|
|
1654
|
+
`runGetterAfter`/`runSetterAfter`. Computed fields keep `dependencies` and
|
|
1655
|
+
`compute({ attributes, id, ... })`. Dependencies now support transitive chains
|
|
1656
|
+
and fields without their own callback. Missing names, cycles, non-array
|
|
1657
|
+
declarations and supplied non-function getters/setters fail registration.
|
|
1658
|
+
Remove placeholder `null`/`false` callbacks; omit the callback and its ordering
|
|
1659
|
+
property when the field needs no transformation.
|
|
1660
|
+
|
|
1661
|
+
List every local input a callback reads. Sparse reads fetch the entire dependency
|
|
1662
|
+
chain, run getters before computations, and compute prerequisites once. A getter
|
|
1663
|
+
cannot depend on a computed field; move that calculation to a computed field.
|
|
1664
|
+
Setters see available validated input and earlier setter results; they do not
|
|
1665
|
+
load omitted PATCH fields from the database. Read logical IDs from callback
|
|
1666
|
+
`id`. Remove read dependencies on raw relationship backing fields or relationship
|
|
1667
|
+
aliases: these are not attribute values at callback time. The
|
|
1668
|
+
[dependency contract](12-field-transformations.md#dependency-contract)
|
|
1669
|
+
includes a chained example and visibility rules.
|
|
1670
|
+
|
|
1671
|
+
Full write responses now apply virtual getters before computations. Virtual
|
|
1672
|
+
input belongs to the written type/ID and is no longer copied onto included or
|
|
1673
|
+
unrelated records. Update calculations that expected raw virtual input after a
|
|
1674
|
+
getter, and remove any reliance on a parent's input appearing on its includes.
|
|
1675
|
+
|
|
1676
|
+
For custom plugins, replace writes to `scope.vars.queryFields` in `resource:added`
|
|
1677
|
+
with declarations in `context.queryFields` during `schema:enrich`. Read the final
|
|
1678
|
+
map from `scope.vars.schemaInfo.queryFields`. There is no compatibility copy at
|
|
1679
|
+
the former path. Core normalizes the declarations and checks collisions against
|
|
1680
|
+
the final attribute/computed/relationship/ID namespace before publication.
|
|
1681
|
+
Canonical additions rebuild this map together with dependencies.
|
|
1682
|
+
See the [plugin example](29-writing-plugins.md#the-query-field-seam).
|
|
1683
|
+
Ordinary applications using `QueryProjectionsPlugin` and resource `queryFields`
|
|
1684
|
+
options keep that setup unchanged.
|
|
1685
|
+
|
|
1686
|
+
The unused internal `sortFieldsByDependencies` wrapper was removed. Internal
|
|
1687
|
+
code uses the existing `topologicalSort` through `compileFieldDependencies`;
|
|
1688
|
+
applications should declare dependencies in their resource schemas.
|
|
1689
|
+
|
|
1690
|
+
### Response dependency metadata
|
|
1691
|
+
|
|
1692
|
+
JSON:API resource objects no longer expose the internal
|
|
1693
|
+
`__$jsonrestapi_computed_deps$__` member. Update snapshots or application code
|
|
1694
|
+
that consumed it. Computed fields still receive their dependencies during
|
|
1695
|
+
enrichment; cleanup happens at the final response boundary, including full and
|
|
1696
|
+
minimal write responses after finish hooks.
|
|
1697
|
+
|
|
1698
|
+
Use declared `dependencies` and callback arguments for computations. Put
|
|
1699
|
+
application metadata in `meta` or declared attributes. Cleanup removes only the
|
|
1700
|
+
reserved resource-level member; keys inside user attributes and metadata retain
|
|
1701
|
+
their values. Method arguments and stored records need no migration for this fix.
|
|
1702
|
+
|
|
1703
|
+
## Resource IDs and links
|
|
1704
|
+
|
|
1705
|
+
Write documents and relationship identifiers accept strings or finite numbers,
|
|
1706
|
+
then apply the resource's ID normalizer and schema. Plain inputs now preserve
|
|
1707
|
+
those values for the same validation as JSON:API inputs. They no longer silently
|
|
1708
|
+
drop zero, turn false into an unlinked relationship, or stringify objects into
|
|
1709
|
+
IDs. Use `null` to clear a to-one relationship. Convert a BigInt to a string
|
|
1710
|
+
before placing it in a write document or relationship identifier.
|
|
1711
|
+
|
|
1712
|
+
The default normalizer trims strings and accepts numeric path IDs (including
|
|
1713
|
+
BigInt); it no longer accepts booleans, arrays or objects through implicit
|
|
1714
|
+
`toString()`. A configured `normalizeId` remains the explicit customization
|
|
1715
|
+
point. Resource-specific normalizers still override the plugin default.
|
|
1716
|
+
|
|
1717
|
+
PATCH and PUT use the path ID only when the body ID is absent or undefined.
|
|
1718
|
+
An explicit zero must match the path after normalization. Explicit null, empty
|
|
1719
|
+
or invalid body IDs fail validation instead of being replaced by the path ID.
|
|
1720
|
+
For example, this now rejects without changing record `42`:
|
|
1721
|
+
|
|
1722
|
+
```js
|
|
1723
|
+
await api.resources.books.patch({
|
|
1724
|
+
id: '42',
|
|
1725
|
+
format: 'plain',
|
|
1726
|
+
data: { id: 0, title: 'Wrong target' }
|
|
1727
|
+
})
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
The schema still controls valid keys: `type: 'id'` requires positive values;
|
|
1731
|
+
an integer or string key can permit zero. Public IDs remain strings. Leading
|
|
1732
|
+
zeroes, case, punctuation and large numeric strings remain intact for opaque
|
|
1733
|
+
string keys. Use a string to avoid JavaScript numeric precision loss.
|
|
1734
|
+
|
|
1735
|
+
SQL bigint primary and relationship IDs now retain their exact decimal values
|
|
1736
|
+
on library reads and writes. For example, `9223372036854775806` and
|
|
1737
|
+
`9223372036854775807` remain distinct through includes, relationship changes
|
|
1738
|
+
and cursor pages. For explicitly supplied keys this large, use a string ID
|
|
1739
|
+
declaration and pass decimal strings; keep SQL primary and reference column
|
|
1740
|
+
types compatible. Existing text keys retain leading zeroes.
|
|
1741
|
+
|
|
1742
|
+
Storage hooks can now receive an exact string where SQLite or MySQL previously
|
|
1743
|
+
returned a rounded Number. Keep identifiers as strings instead of converting
|
|
1744
|
+
them with `Number()` or `parseInt()`. This correction needs no new API option or
|
|
1745
|
+
data migration. It applies to library queries without changing the caller's
|
|
1746
|
+
Knex connection configuration. A Number already rounded in application code
|
|
1747
|
+
cannot be repaired by the library.
|
|
1748
|
+
|
|
1749
|
+
PostgreSQL seed/import jobs that write explicit IDs into sequence-backed keys
|
|
1750
|
+
must synchronize the sequence before later generated-ID inserts. Explicit IDs
|
|
1751
|
+
do not advance that sequence. Keep synchronization in the seed/import workflow.
|
|
1752
|
+
Both ordinary and custom ID columns are exercised with actual
|
|
1753
|
+
PostgreSQL/MySQL generated inserts and all return modes.
|
|
1754
|
+
|
|
1755
|
+
Resource and relationship URLs, including POST's Location header, now encode
|
|
1756
|
+
the ID as one path segment. Pass the original ID to resource methods; follow
|
|
1757
|
+
returned URLs as supplied, without encoding them again. For example, ID
|
|
1758
|
+
`part/one?x` produces `/books/part%2Fone%3Fx`.
|
|
1759
|
+
|
|
1760
|
+
Table creation and migration generation now map `type: 'integer'` to integer
|
|
1761
|
+
columns; previously it fell through to string columns. Existing tables are not
|
|
1762
|
+
altered automatically. If you used these helpers with integer declarations,
|
|
1763
|
+
inspect the stored values and review the generated migration before applying
|
|
1764
|
+
it. Determine whether existing application tables need conversion before
|
|
1765
|
+
applying the changed schema helpers.
|
|
1766
|
+
|
|
1767
|
+
## Storage naming configuration
|
|
1768
|
+
|
|
1769
|
+
Use a string for `storage.naming`: `snake_case` (also `snake` or `snakeCase`) or
|
|
1770
|
+
`exact` (also `field` or `verbatim`). Omit it for `snake_case`. Coercible objects
|
|
1771
|
+
and inherited property names such as `constructor` are invalid naming modes
|
|
1772
|
+
and now reject at the normalization boundary. Replace an object with its
|
|
1773
|
+
intended string value; applications using the documented strings need no change.
|
|
1774
|
+
|
|
1775
|
+
Canonical row conversion also preserves a field literally named `undefined`
|
|
1776
|
+
when a relationship lacks a reverse attribute mapping. An absent mapping no
|
|
1777
|
+
longer causes that unrelated attribute to be removed.
|
|
1778
|
+
|
|
1779
|
+
### Public field names and physical columns
|
|
1780
|
+
|
|
1781
|
+
Declared fields named `constructor`, `prototype`, `toString`, `valueOf`,
|
|
1782
|
+
`hasOwnProperty`, `isPrototypeOf`, `propertyIsEnumerable` and `toLocaleString`
|
|
1783
|
+
are handled as own fields. Stored values, generated filters, sparse responses,
|
|
1784
|
+
cursor sorting and field callbacks must not resolve to inherited JavaScript
|
|
1785
|
+
properties. This applies to plain records and JSON:API attributes.
|
|
1786
|
+
|
|
1787
|
+
Ordinary Knex storage rejects physical columns literally named `constructor`,
|
|
1788
|
+
`prototype` or `__proto__`. Knex's update compiler drops these names; a write
|
|
1789
|
+
could previously leave an old value unchanged. Validation now rejects these
|
|
1790
|
+
mappings during resource registration, adapter creation and table/migration
|
|
1791
|
+
generation. Unsupported `idProperty` names are also rejected. Use an explicit
|
|
1792
|
+
physical mapping while keeping the public name:
|
|
1793
|
+
|
|
1794
|
+
```js
|
|
1795
|
+
schema: {
|
|
1796
|
+
constructor: {
|
|
1797
|
+
type: 'string',
|
|
1798
|
+
storage: { column: 'constructor_value' }
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
```
|
|
1802
|
+
|
|
1803
|
+
If the old physical column already contains data, first rename it in your normal
|
|
1804
|
+
database migration, then change the declaration. For example:
|
|
1805
|
+
|
|
1806
|
+
```js
|
|
1807
|
+
await knex.schema.alterTable('books', table => {
|
|
1808
|
+
table.renameColumn('constructor', 'constructor_value')
|
|
1809
|
+
})
|
|
1810
|
+
```
|
|
1811
|
+
|
|
1812
|
+
Review dependent indexes, constraints and raw SQL, and verify row counts and
|
|
1813
|
+
values on the application's database before enabling the new declaration.
|
|
1814
|
+
Registration and `createKnexTable()` do not rename existing columns for you.
|
|
1815
|
+
Canonical storage uses allocated slots and does not need this physical-column
|
|
1816
|
+
mapping for public `constructor` or `prototype` fields.
|
|
1817
|
+
|
|
1818
|
+
Minimal reads used by write authorization and PUT completeness checks now
|
|
1819
|
+
translate physical columns to logical fields once. If a physical column name
|
|
1820
|
+
also names another logical field, hooks now receive the correct original
|
|
1821
|
+
attributes. Update any hook that compensated for the previous double mapping.
|
|
1822
|
+
|
|
1823
|
+
### Declaration dictionaries and reserved names
|
|
1824
|
+
|
|
1825
|
+
Use plain objects or null-prototype dictionaries with **own** declarations for
|
|
1826
|
+
schemas, search fields, projections and relationships. Definitions inherited
|
|
1827
|
+
from a custom prototype now reject instead of disappearing during compilation.
|
|
1828
|
+
This includes an object literal such as `{ __proto__: definition }`, which
|
|
1829
|
+
changes an object's prototype rather than declaring an ordinary property.
|
|
1830
|
+
|
|
1831
|
+
`__proto__` is not a supported field, filter, projection, relationship alias or
|
|
1832
|
+
relationship backing-field name. These declarations reject before compilation
|
|
1833
|
+
can lose the key. The same checks apply after enrichment hooks, during field
|
|
1834
|
+
additions, in direct storage/table helpers and at the canonical registry's write
|
|
1835
|
+
and descriptor-loading boundaries. Rename such a declaration. Preserve the
|
|
1836
|
+
existing physical column or canonical slot through an explicit mapping when it
|
|
1837
|
+
contains data; do not re-register over old metadata to erase migration evidence.
|
|
1838
|
+
|
|
1839
|
+
These checks do not recursively strip user data. An object-valued attribute can
|
|
1840
|
+
contain JSON keys named `__proto__`, `constructor` or `prototype`; strings such
|
|
1841
|
+
as `__proto__` also remain valid resource IDs. Their existing validation rules
|
|
1842
|
+
still apply.
|
|
1843
|
+
|
|
1844
|
+
Declare a belongs-to relationship alias as a string, for example
|
|
1845
|
+
`ownerId: { type: 'id', belongsTo: 'users', as: 'owner' }`. Numbers, booleans,
|
|
1846
|
+
arrays and objects in `as` now reject during schema validation, including values
|
|
1847
|
+
introduced by `schema:enrich`. Replace `as: ['owner']` with `as: 'owner'`; do not
|
|
1848
|
+
depend on JavaScript coercing a configuration value into a property name.
|
|
1849
|
+
|
|
1850
|
+
The relationship mapping options `target`, `through` and `via` also require
|
|
1851
|
+
strings when supplied. For example, change `target: ['users']` to
|
|
1852
|
+
`target: 'users'`, and `through: ['memberships']` to `through: 'memberships'`.
|
|
1853
|
+
They check the declaration's type; they do not require a forward-referenced
|
|
1854
|
+
target or pivot resource to be registered earlier.
|
|
1855
|
+
|
|
1856
|
+
Use strings for schema `belongsTo` targets and relationship `foreignKey` and
|
|
1857
|
+
`otherKey` names too. In particular, enrichment hooks cannot bypass the
|
|
1858
|
+
backing-name type checks: change `foreignKey: ['ownerId']` to
|
|
1859
|
+
`foreignKey: 'ownerId'`. A schema field uses `belongsTo: 'users'`, not
|
|
1860
|
+
`belongsTo: ['users']`.
|
|
1861
|
+
|
|
1862
|
+
Polymorphic relationship declarations require `types` to be a nonempty array of
|
|
1863
|
+
nonempty strings, and `typeField` and `idField` must be nonempty strings.
|
|
1864
|
+
For example, use `{ types: ['users'], typeField: 'subjectType', idField:
|
|
1865
|
+
'subjectId' }` as the `belongsToPolymorphic` definition. The authored
|
|
1866
|
+
polymorphic registration check still requires its target resources to be
|
|
1867
|
+
registered; these shape checks do not establish backing-column existence.
|
|
1868
|
+
|
|
1869
|
+
For reverse relationships, `hasOne` requires `target` and `foreignKey`; `hasMany`
|
|
1870
|
+
requires `target` and either `foreignKey` or its reverse-polymorphic `via`;
|
|
1871
|
+
`manyToMany` requires `through`, `foreignKey` and `otherKey`.
|
|
1872
|
+
Many-to-many `target` remains optional where the
|
|
1873
|
+
existing relationship-name default applies.
|
|
1874
|
+
|
|
1875
|
+
Declare these relationships in the resource's `relationships` option.
|
|
1876
|
+
`computedSchema:enrich.schemaRelationships` remains an inspection input, as
|
|
1877
|
+
described in [computed enrichment](#computed-field-enrichment). Defensive
|
|
1878
|
+
validation now catches malformed relationship mappings if a hook mutates that
|
|
1879
|
+
input; this does not make mutation or replacement of the inspection map a
|
|
1880
|
+
supported relationship declaration API. Move such declarations into the
|
|
1881
|
+
resource configuration when migrating.
|
|
1882
|
+
|
|
1883
|
+
Resource **names** must be JavaScript
|
|
1884
|
+
identifiers starting with a letter, `_` or `$`, excluding `__proto__`,
|
|
1885
|
+
`constructor` and `prototype`. Other inherited names such as `toString`,
|
|
1886
|
+
`valueOf` and `hasOwnProperty` now work with omitted or explicit fieldsets.
|
|
1887
|
+
This resource-name restriction does not prohibit `constructor` or `prototype`
|
|
1888
|
+
as attribute or relationship names. Valid relationship aliases also work in
|
|
1889
|
+
include trees and polymorphic include grouping.
|
|
1890
|
+
|
|
1891
|
+
Null relationship representation is unchanged: JSON:API contains `data: null`;
|
|
1892
|
+
plain conversion omits that relationship property. Use `Object.hasOwn(record,
|
|
1893
|
+
name)` when checking for presence of a potentially inherited name.
|
|
1894
|
+
|
|
1895
|
+
This establishes the own-property and reserved-name boundary. Full JSON:API
|
|
1896
|
+
member-name validation and other namespace/capability conflicts remain part of
|
|
1897
|
+
the wider compiled-metadata work; arbitrary punctuation is not guaranteed to
|
|
1898
|
+
work in every query syntax.
|
|
1899
|
+
|
|
1900
|
+
## Sort fields and defaults
|
|
1901
|
+
|
|
1902
|
+
Use a string or an array of strings for resource `defaultSort`:
|
|
1903
|
+
|
|
1904
|
+
```javascript
|
|
1905
|
+
// Replace this inconsistent configuration:
|
|
1906
|
+
defaultSort: { field: 'name', direction: 'desc' }
|
|
1907
|
+
|
|
1908
|
+
// With:
|
|
1909
|
+
defaultSort: ['-name']
|
|
1910
|
+
```
|
|
1911
|
+
|
|
1912
|
+
The object form previously passed registration but failed through the public
|
|
1913
|
+
query method. Invalid non-string entries now fail during schema compilation.
|
|
1914
|
+
`null` and `[]` leave the default as ascending resource ID; explicit query sorting
|
|
1915
|
+
overrides the resource default. Keep programmatic `queryParams.sort` as an array.
|
|
1916
|
+
The resource ID remains the stable tie-breaker unless already included.
|
|
1917
|
+
|
|
1918
|
+
Sorting by `id` always means the resource ID. Sorting by a stored attribute or
|
|
1919
|
+
query projection uses that field, even if `searchSchema` defines a filter with
|
|
1920
|
+
the same name and a different `actualField`. For example:
|
|
1921
|
+
|
|
1922
|
+
```javascript
|
|
1923
|
+
searchSchema: {
|
|
1924
|
+
name: { type: 'string', actualField: 'shortName' },
|
|
1925
|
+
byShortName: { type: 'string', actualField: 'shortName' }
|
|
1926
|
+
},
|
|
1927
|
+
sortableFields: ['id', 'name', 'byShortName']
|
|
1928
|
+
```
|
|
1929
|
+
|
|
1930
|
+
Here `filters: { name: 'Ada' }` still filters `shortName`; `sort: ['name']`
|
|
1931
|
+
orders the stored `name`. Use `sort: ['byShortName']` to order by the distinct
|
|
1932
|
+
alias. An explicit sortable alias must resolve to a scalar stored field;
|
|
1933
|
+
relationship aliases retain their visibility checks. Whole-document JSON and
|
|
1934
|
+
cross-resource scalar ordering are not introduced by this change.
|
|
1935
|
+
|
|
1936
|
+
The same resolution applies to collection include `orderBy`. Cursor comparisons
|
|
1937
|
+
use the stored field's type and selected value, independently of the filter's
|
|
1938
|
+
input type. Sort fields needed internally are selected even when sparse output
|
|
1939
|
+
omits them. Ordinary and canonical storage both follow these rules.
|
|
1940
|
+
Restart existing cursor traversals after upgrading if a filter alias overlapped
|
|
1941
|
+
a sort field; the old cursor may describe a different ordering.
|
|
1942
|
+
|
|
1943
|
+
### Hidden sort fields
|
|
1944
|
+
|
|
1945
|
+
Fields marked `hidden: true` cannot be sort keys. This includes stored fields,
|
|
1946
|
+
their search/relationship aliases, query projections, resource `defaultSort`,
|
|
1947
|
+
explicit `sortableFields`, and collection include `orderBy`. Previously, an
|
|
1948
|
+
explicit hidden stored sort could produce an incomplete cursor; a hidden
|
|
1949
|
+
projection marked sortable could expose its SQL value in cursors and links.
|
|
1950
|
+
|
|
1951
|
+
Remove hidden fields and their aliases from sort configuration and choose a
|
|
1952
|
+
public scalar sort field instead:
|
|
1953
|
+
|
|
1954
|
+
```javascript
|
|
1955
|
+
schema: {
|
|
1956
|
+
secret: { type: 'string', hidden: true },
|
|
1957
|
+
displayOrder: { type: 'integer', normallyHidden: true }
|
|
1958
|
+
},
|
|
1959
|
+
sortableFields: ['id', 'displayOrder'],
|
|
1960
|
+
defaultSort: ['displayOrder']
|
|
1961
|
+
```
|
|
1962
|
+
|
|
1963
|
+
Use `normallyHidden` only for publicly readable values that should be omitted
|
|
1964
|
+
by default. They remain sortable and their values can appear in cursors even
|
|
1965
|
+
when omitted by a sparse fieldset. Keep sensitive values `hidden`.
|
|
1966
|
+
|
|
1967
|
+
Invalid local declarations fail during schema compilation with
|
|
1968
|
+
`REST_API_VALIDATION` and violation rule `hidden_sort`. Include ordering is
|
|
1969
|
+
checked when the target resource is resolved, preserving forward declarations.
|
|
1970
|
+
Request validation excludes hidden sort keys, and query hooks cannot reintroduce
|
|
1971
|
+
them. Hidden fields/projections still work as declared getter/computed
|
|
1972
|
+
dependencies; their raw values remain omitted from resource output.
|
|
1973
|
+
|
|
1974
|
+
## Pagination modes and counts
|
|
1975
|
+
|
|
1976
|
+
SQLite ordering now uses native `NULLS FIRST/LAST`, preserving the public null
|
|
1977
|
+
placement and cursor results while allowing existing ordering indexes to help.
|
|
1978
|
+
The verified engine is SQLite 3.49.2; SQLite engines older than 3.30 are no longer
|
|
1979
|
+
supported by these ordering helpers. PostgreSQL/MySQL ordering is unchanged.
|
|
1980
|
+
No payload or cursor migration is needed. Optional index choices and the existing
|
|
1981
|
+
migration paths belong in reviewed [schema migrations](21-schema-and-migrations.md).
|
|
1982
|
+
|
|
1983
|
+
The existing page options keep their meanings in both storage modes:
|
|
1984
|
+
|
|
1985
|
+
| Query page | Behavior |
|
|
1986
|
+
| --- | --- |
|
|
1987
|
+
| Omitted or `{}` | Apply the resource's capped default limit; omit pagination metadata |
|
|
1988
|
+
| `{ number: 2, size: 25 }` | Offset page 2, using the capped size for the offset and links |
|
|
1989
|
+
| `{ number: 2 }` | Offset page 2 with the resource's default size |
|
|
1990
|
+
| `{ size: 25 }` | First cursor page |
|
|
1991
|
+
| `{ after: cursor }` or `{ before: cursor }` | Adjacent forward/backward cursor page with the resource's default size |
|
|
1992
|
+
|
|
1993
|
+
Add `size` to a cursor boundary to choose a different page size. Page sizes are
|
|
1994
|
+
capped by `queryMaxLimit`. Use one of `number`, `after` or `before`; combining
|
|
1995
|
+
them, supplying non-positive/fractional page numbers/sizes, or supplying an
|
|
1996
|
+
empty cursor rejects before SQL. Follow generated links to retain filters,
|
|
1997
|
+
fieldsets and the effective size.
|
|
1998
|
+
|
|
1999
|
+
`enablePaginationCounts` controls totals for offset pages. With counts disabled,
|
|
2000
|
+
offset metadata contains `page` and `pageSize`; it does not promise totals or
|
|
2001
|
+
`hasMore`. Cursor pages use one extra row for `hasMore` and never issue a count
|
|
2002
|
+
query, regardless of this setting. Ordinary storage previously issued a count
|
|
2003
|
+
on the first cursor page and discarded its result; that extra query and its
|
|
2004
|
+
count-filter hook invocation have been removed. Collection predicates still
|
|
2005
|
+
run for every cursor page. Keep mandatory filters in the collection path.
|
|
2006
|
+
|
|
2007
|
+
Malformed cursor syntax now has the same error message in either storage mode,
|
|
2008
|
+
with code `REST_API_VALIDATION`, a `page.after` or `page.before` field, and rule
|
|
2009
|
+
`invalid_cursor`. Prefer the typed code/details when handling errors instead of
|
|
2010
|
+
matching a former backend-specific message. No call-site migration is needed
|
|
2011
|
+
for applications already using the page options above.
|
|
2012
|
+
|
|
2013
|
+
## CORS and HTTP response hooks
|
|
2014
|
+
|
|
2015
|
+
Install `CorsPlugin` after Express or Fastify. Installation without a connector
|
|
2016
|
+
now fails immediately. Pass options directly to `api.use(CorsPlugin, options)`.
|
|
2017
|
+
Origin predicates may be async: their boolean result is now awaited, including
|
|
2018
|
+
inside origin arrays. A rejected predicate becomes a 500 response without CORS
|
|
2019
|
+
permission. Do not rely on the old behavior that treated a Promise as permission.
|
|
2020
|
+
`origin: false` disables origin permission, and `maxAge: 0` disables preflight
|
|
2021
|
+
caching. These values are no longer replaced by defaults.
|
|
2022
|
+
|
|
2023
|
+
Denied OPTIONS responses now use the JSON:API error envelope (`errors[0]`,
|
|
2024
|
+
status 403) instead of `{ error: 'CORS origin not allowed' }`. Other requests
|
|
2025
|
+
retain their actual resource/status behavior; CORS does not authorize writes.
|
|
2026
|
+
Existing credential/origin defaults remain permissive, so use explicit allowed
|
|
2027
|
+
origins for credentialed applications. See the [CORS guide](24-cors.md).
|
|
2028
|
+
|
|
2029
|
+
`transport:response` now runs for early parser/media/Accept errors and explicit
|
|
2030
|
+
`context.reject(...)` responses. Its request body can therefore be undefined;
|
|
2031
|
+
avoid assuming authentication, parsing or a resource method has completed.
|
|
2032
|
+
The hook chain is attempted once. If it throws, the connector maps the error
|
|
2033
|
+
without rerunning response hooks. A response-stage failure cannot roll back an
|
|
2034
|
+
already committed write.
|
|
2035
|
+
|
|
2036
|
+
Vary headers from the host, custom route, CORS and response hooks are merged
|
|
2037
|
+
case-insensitively, including on rejected requests; compare fields rather than
|
|
2038
|
+
their order. Fastify's native malformed-URL boundary still precedes connector
|
|
2039
|
+
hooks and has no connector CORS headers. Raw responses sent by host middleware
|
|
2040
|
+
or a hook that takes over the response need their own headers. No stored-data
|
|
2041
|
+
migration is required.
|
|
2042
|
+
|
|
2043
|
+
## Multipart uploads
|
|
2044
|
+
|
|
2045
|
+
Unexpected file detector/parser/storage errors now retain their cause and
|
|
2046
|
+
operation context. Typed errors still propagate unchanged. Unexpected storage
|
|
2047
|
+
failures return HTTP 500 rather than being mislabeled as validation errors;
|
|
2048
|
+
declared MIME/size/schema violations remain 422 and typed access denials remain
|
|
2049
|
+
403. Update callers that classified every upload failure as invalid input.
|
|
2050
|
+
|
|
2051
|
+
File cleanup keeps attempting later files after a cleanup or warning-log failure.
|
|
2052
|
+
Inspect `context.cleanupErrors` for `temporaryFileCleanup`, `uploadedFileCleanup`
|
|
2053
|
+
and associated `logging` entries. Failed deletions retain their tracking entries
|
|
2054
|
+
for reconciliation. Tracking belongs to the creating transaction, preventing a
|
|
2055
|
+
reused context from deleting files committed by an earlier transaction. Managed
|
|
2056
|
+
rollback invokes enlisted cleanup hooks after confirmed completion.
|
|
2057
|
+
|
|
2058
|
+
After commit, uploaded objects belong to the application. Replacing or deleting
|
|
2059
|
+
a record does not delete its previously committed files, including when no
|
|
2060
|
+
references remain in that resource. URLs may be shared across records and
|
|
2061
|
+
external consumers. If your application needs garbage collection, establish
|
|
2062
|
+
ownership and absence of references before calling `storage.delete(url)`; do
|
|
2063
|
+
not attach unconditional file deletion to a record's DELETE or replacement.
|
|
2064
|
+
Rollback cleans only newly uploaded objects belonging to the failed transaction.
|
|
2065
|
+
See the [file ownership contract](26-file-uploads.md#ownership-after-commit).
|
|
2066
|
+
|
|
2067
|
+
`LocalStorage` now reserves filenames exclusively, so concurrent original/custom
|
|
2068
|
+
names cannot overwrite another upload or make rollback delete its bytes.
|
|
2069
|
+
Existing files and symlinks are occupied names. Filename inspection now rejects
|
|
2070
|
+
permission and other I/O failures instead of returning an available name.
|
|
2071
|
+
Failed uploads remove only their reserved destination. If removal or closing
|
|
2072
|
+
also fails, inspect the returned `AggregateError`'s `cause` and `errors` for the
|
|
2073
|
+
original and cleanup failures. Its destination may need reconciliation. Buffer
|
|
2074
|
+
and temporary-file inputs retain their existing signatures; see
|
|
2075
|
+
[storage behavior](26-file-uploads.md#storage-adapters).
|
|
2076
|
+
|
|
2077
|
+
Install `FileHandlingPlugin` before `ExpressPlugin` and install the selected
|
|
2078
|
+
optional peer (`busboy` or `formidable`). Missing parsers and invalid factories
|
|
2079
|
+
now fail setup. The Express connector accepts multipart only when a detector is
|
|
2080
|
+
registered and uploads are enabled; Fastify continues to reject multipart.
|
|
2081
|
+
|
|
2082
|
+
The corrected parsers use Busboy 1.x and Formidable 3.x. Both return buffered
|
|
2083
|
+
files with `filename`, `mimetype`, `size` and `data`. If a custom storage adapter
|
|
2084
|
+
used Formidable's old `file.filepath` or `file.cleanup`, use `file.data` instead:
|
|
2085
|
+
the detector now removes its private request directory before returning.
|
|
2086
|
+
Custom detectors' cleanup callbacks are still supported.
|
|
2087
|
+
|
|
2088
|
+
Repeated file fields now produce 400. Upload one file for each distinct declared
|
|
2089
|
+
file field. Repeated text fields become arrays consistently across parsers;
|
|
2090
|
+
bracketed array suffixes group values in arrival order. Unknown text/file fields
|
|
2091
|
+
are validated, including prototype-like names that could previously disappear.
|
|
2092
|
+
Parser limits produce 413, and declared file MIME/size violations produce 422.
|
|
2093
|
+
Review the documented [bounded defaults](26-file-uploads.md#protocol-configuration)
|
|
2094
|
+
and set parser limits explicitly when larger uploads are needed.
|
|
2095
|
+
|
|
2096
|
+
Multipart POST/PUT/PATCH use the route's resource type; PUT takes its ID from
|
|
2097
|
+
the URL. Required file fields follow the normal schema rules, so PATCH can omit
|
|
2098
|
+
an existing file. Numeric `maxSize` now works, including zero for empty files.
|
|
2099
|
+
Plain programmatic calls with custom detectors update the canonical input record
|
|
2100
|
+
correctly. Detector errors propagate instead of falling through to another
|
|
2101
|
+
parser or silently continuing the write.
|
|
2102
|
+
|
|
2103
|
+
Owned and managed rollback cleanup, multiple uploads, replacement and deletion
|
|
2104
|
+
are verified with real local storage. Previously committed files remain
|
|
2105
|
+
application-owned: replacing a URL or deleting its resource does not delete
|
|
2106
|
+
the old object, which may have other references. Confirmed rollback cleans new
|
|
2107
|
+
uploads; confirmed commit releases their tracking. Review
|
|
2108
|
+
[file ownership after commit](26-file-uploads.md#ownership-after-commit)
|
|
2109
|
+
when migrating application cleanup code. No stored-data migration is required
|
|
2110
|
+
by this parser correction.
|
|
2111
|
+
|
|
2112
|
+
## Socket.IO startup and shutdown
|
|
2113
|
+
|
|
2114
|
+
Keep the existing `await api.startSocketServer(server, options)` call, and await
|
|
2115
|
+
it before accepting subscription traffic. Calling it while another start is
|
|
2116
|
+
pending or a Socket.IO server is active now rejects; remove duplicate bootstrap
|
|
2117
|
+
calls instead of relying on replacement of a live server.
|
|
2118
|
+
|
|
2119
|
+
Redis connection failure no longer leaves Socket.IO attached or published in
|
|
2120
|
+
`api.io`. Partial connections are closed, the HTTP server and authentication
|
|
2121
|
+
configuration remain intact, and the application can retry. The supplied Redis
|
|
2122
|
+
reconnection policy still controls retries. Set
|
|
2123
|
+
`redis.socket.reconnectStrategy: false` if your bootstrap should reject on the
|
|
2124
|
+
first connection failure.
|
|
2125
|
+
|
|
2126
|
+
Closing Socket.IO also closes the HTTP server supplied to it. The plugin now
|
|
2127
|
+
owns shutdown of its two Redis clients when that HTTP server closes and clears
|
|
2128
|
+
`api.io` and its server/client references. Remove any subsequent manual `quit()`
|
|
2129
|
+
calls on `api.vars.socketIORedisClients`. Close your application database
|
|
2130
|
+
separately. No new shutdown method, compatibility wrapper or data migration is
|
|
2131
|
+
required. The [Socket.IO guide](28-socketio.md) lists the verified paths and
|
|
2132
|
+
the remaining network/failover limits.
|
|
2133
|
+
|
|
2134
|
+
## Socket.IO subscriptions and notifications
|
|
2135
|
+
|
|
2136
|
+
Subscription messages now accept only `resource`, `filters` and an optional
|
|
2137
|
+
nonempty string `subscriptionId`. Remove `include` and `fields` from subscribe
|
|
2138
|
+
and restore messages; those options never changed the notification data. Keep
|
|
2139
|
+
them in the subsequent resource GET/query instead. They are also absent from
|
|
2140
|
+
the acknowledgement's `data`. Other unknown options receive `UNSUPPORTED_OPTION`.
|
|
2141
|
+
|
|
2142
|
+
Null, scalar and array filters now receive `INVALID_FILTERS`. A duplicate active
|
|
2143
|
+
subscription ID receives `SUBSCRIPTION_EXISTS`; unsubscribe before replacing it.
|
|
2144
|
+
Concurrent admissions respect the per-socket limit. A subscription hook receives
|
|
2145
|
+
`{ context }`, with `context.subscription` and `context.auth`; it may modify
|
|
2146
|
+
filters but not identity. Its final filters are validated.
|
|
2147
|
+
|
|
2148
|
+
Custom SQL search fields use their existing `applyFilter(query, value)` callback
|
|
2149
|
+
for subscription matching too. Remove Socket.IO-only `filterRecord` callbacks;
|
|
2150
|
+
the earlier draft's requirement for a parallel JavaScript predicate is withdrawn.
|
|
2151
|
+
Notification eligibility now runs ordinary resource queries before/after writes
|
|
2152
|
+
in the same transaction, with subscriber identity and an internal changed-ID
|
|
2153
|
+
constraint. Query hooks must support these calls.
|
|
2154
|
+
|
|
2155
|
+
If row policies or autofilters need context beyond `auth`, supply it from trusted
|
|
2156
|
+
server data in the existing hook:
|
|
2157
|
+
|
|
2158
|
+
```js
|
|
2159
|
+
subscriptionFilters: {
|
|
2160
|
+
functionName: 'subscriber-context',
|
|
2161
|
+
handler: ({ context: { subscription, auth } }) => {
|
|
2162
|
+
subscription.context = {
|
|
2163
|
+
scopeValues: { workspaceId: auth.workspaceId },
|
|
2164
|
+
visibility: { groups: auth.groups }
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
```
|
|
2169
|
+
|
|
2170
|
+
The property defaults to `{}`. Keep it JSON-serializable when using Redis.
|
|
2171
|
+
Client messages cannot set it. Populate whatever context your own policies
|
|
2172
|
+
require; the property names above are application examples. Resource query
|
|
2173
|
+
permissions receive this context on admission and notification as well.
|
|
2174
|
+
|
|
2175
|
+
Relationship POST/DELETE now run `beforeDataCall` and their respective
|
|
2176
|
+
`beforeDataCallPostRelationship` / `beforeDataCallDeleteRelationship` hooks
|
|
2177
|
+
before mutation. Hooks shared across methods must handle these operations.
|
|
2178
|
+
|
|
2179
|
+
Update consumers of `change.type`:
|
|
2180
|
+
|
|
2181
|
+
| Previous value | New value |
|
|
2182
|
+
| --- | --- |
|
|
2183
|
+
| `resource.postd` | `resource.created` |
|
|
2184
|
+
| `resource.putd` | `resource.created` for PUT-create, otherwise `resource.updated` |
|
|
2185
|
+
| `resource.patchd` | `resource.updated` |
|
|
2186
|
+
| `resource.deleted` | Unchanged |
|
|
2187
|
+
|
|
2188
|
+
The `action` field retains resource method names (`post`, `put`, `patch`,
|
|
2189
|
+
`delete`). IDs, including `deletedRecord.id`, are strings. Relationship changes
|
|
2190
|
+
produce parent updates with action `patch`, and changed reverse children also
|
|
2191
|
+
produce their resource updates. Each matching subscription gets its own event;
|
|
2192
|
+
use `subscriptionId` when one connection holds multiple views. Both entering and
|
|
2193
|
+
leaving a filtered or authorized result trigger invalidation when the corresponding
|
|
2194
|
+
query succeeds. Deletion uses the before-write query.
|
|
2195
|
+
|
|
2196
|
+
Query permissions are checked again before notification. `socket.data.subscriptions`
|
|
2197
|
+
is now a serializable array for adapter transport; applications that inspected
|
|
2198
|
+
the old internal Map must update that code. No stored-data migration is needed.
|
|
2199
|
+
The [Socket.IO guide](28-socketio.md) separates verified local behavior from
|
|
2200
|
+
remaining Redis failover/network uncertainty. Atomic bulk enlists each child's
|
|
2201
|
+
completion hooks with its owner. A failed deferred notice now leaves later
|
|
2202
|
+
queued notices eligible for delivery. The draining operation's context records
|
|
2203
|
+
`socketioBroadcast` cleanup diagnostics with a zero-based `broadcastIndex`;
|
|
2204
|
+
the call retains its committed outcome and first failure. Failed notices are
|
|
2205
|
+
not automatically replayed. See the Socket.IO guide for partial-delivery limits.
|
|
2206
|
+
Managed callbacks defer repeated changes to the same resource until completion.
|
|
2207
|
+
In-memory notices do not guarantee exactly-once delivery or recovery after
|
|
2208
|
+
process failure. Verify subscription and notification handling in each application
|
|
2209
|
+
that uses this plugin.
|
|
2210
|
+
|
|
2211
|
+
## Bulk writes
|
|
2212
|
+
|
|
2213
|
+
Remove `batchSize` and `enableOptimizations` from plugin configuration, and pass
|
|
2214
|
+
the remaining options directly:
|
|
2215
|
+
|
|
2216
|
+
```javascript
|
|
2217
|
+
await api.use(BulkOperationsPlugin, {
|
|
2218
|
+
maxBulkOperations: 100,
|
|
2219
|
+
defaultAtomic: true
|
|
2220
|
+
})
|
|
2221
|
+
```
|
|
2222
|
+
|
|
2223
|
+
`maxBulkOperations` must be a positive safe integer and `defaultAtomic` a boolean.
|
|
2224
|
+
Unknown keys now fail installation, including a nested `'bulk-operations'`
|
|
2225
|
+
wrapper. The removed settings did not batch SQL: `batchSize` only divided a
|
|
2226
|
+
sequential loop, and the optimization hook never ran. Removing the loop setting
|
|
2227
|
+
also removes its invalid-value hang/skip paths. Bulk operations still execute
|
|
2228
|
+
the normal resource methods, including validation, permission checks and hooks.
|
|
2229
|
+
Atomic requests use one transaction; non-atomic requests complete each record's
|
|
2230
|
+
transaction separately. No stored-data migration is needed.
|
|
2231
|
+
|
|
2232
|
+
Bulk POST replaces `inputRecords` with either `data: plainRecords` or
|
|
2233
|
+
`document: { data: jsonApiResources }`. Bulk PATCH entries use either
|
|
2234
|
+
`{ id, data: plainValues }` or `{ id, document: { data: jsonApiResource } }`.
|
|
2235
|
+
Do not put a JSON:API resource directly inside an operation's `data` field.
|
|
2236
|
+
Each entry explicitly selects its input; neither bulk method infers it from
|
|
2237
|
+
`format`. The documented bulk HTTP bodies keep their wire shapes: POST uses
|
|
2238
|
+
`{ data: [resourceObject, ...] }` and PATCH uses `{ operations: [...] }`.
|
|
2239
|
+
The connector translates them into these method arguments. Remove raw-array
|
|
2240
|
+
bulk POST shorthand and arrays of individually wrapped documents; send one
|
|
2241
|
+
document containing the resource array instead.
|
|
2242
|
+
|
|
2243
|
+
Bulk POST/PATCH honor output-only `format` and `returning`, including resource
|
|
2244
|
+
defaults. Bulk results retain their `meta`/`errors` envelope:
|
|
2245
|
+
`returning: 'full'` supplies complete records in `data`, `minimal` supplies
|
|
2246
|
+
identifiers, and `none` omits `data`. Bulk PATCH no longer performs an extra GET
|
|
2247
|
+
to override a minimal/none return choice.
|
|
2248
|
+
|
|
2249
|
+
```js
|
|
2250
|
+
await api.resources.books.bulkPatch({
|
|
2251
|
+
operations: [{ id: '42', data: { title: 'Updated title' } }],
|
|
2252
|
+
format: 'plain',
|
|
2253
|
+
returning: 'none',
|
|
2254
|
+
atomic: true
|
|
2255
|
+
}, requestContext)
|
|
2256
|
+
// { meta: { total: 1, succeeded: 1, failed: 0, atomic: true }, ... }
|
|
2257
|
+
```
|
|
2258
|
+
|
|
2259
|
+
`atomic` must be a boolean in direct calls. HTTP accepts `atomic=true` or
|
|
2260
|
+
`atomic=false`; misspellings now fail with 422 before writing. Removed response
|
|
2261
|
+
options also fail clearly. Numeric zero is a valid bulk PATCH ID when the
|
|
2262
|
+
resource's declared ID schema accepts zero.
|
|
2263
|
+
|
|
2264
|
+
Install the connector and bulk plugin before declaring resources. Bulk routes
|
|
2265
|
+
now register through the active scope hook and retain authenticated request
|
|
2266
|
+
context. They explicitly select full JSON:API records, regardless of
|
|
2267
|
+
programmatic defaults. POST responds with 201; PATCH and DELETE respond with
|
|
2268
|
+
200 and the batch summary. The optional plugin reserves the `bulk` path segment
|
|
2269
|
+
for these methods; do not use it as an opaque resource ID with this plugin.
|
|
2270
|
+
These endpoints are the library's bulk protocol, not the JSON:API Atomic
|
|
2271
|
+
Operations extension. An atomic denial rolls back the batch; a non-atomic batch
|
|
2272
|
+
retains permitted operations and reports denied entries by index.
|
|
2273
|
+
|
|
2274
|
+
An atomic batch now keeps its original child, validation or commit error if
|
|
2275
|
+
rollback also fails. The outer batch attempts rollback once for an unfinished
|
|
2276
|
+
transaction it owns. Its supplied context exposes the original `error` and any secondary
|
|
2277
|
+
rollback failure in `cleanupErrors`, using `{ phase: 'rollback', error }` entries.
|
|
2278
|
+
Both fields are cleared when the context starts another bulk call. A transaction
|
|
2279
|
+
already completed by the driver is not rolled back; a rejected commit still
|
|
2280
|
+
does not establish a safe retry. The error wrapper reports the available outcome.
|
|
2281
|
+
This change does
|
|
2282
|
+
not add bulk completion hooks or change non-atomic result envelopes.
|
|
2283
|
+
|
|
2284
|
+
Child cleanup warnings and failures now survive on the supplied batch context.
|
|
2285
|
+
`cleanupErrors` entries add a zero-based `bulkIndex` to their existing phase,
|
|
2286
|
+
original error and file information. This includes warnings from successful
|
|
2287
|
+
entries, rollback/cleanup-hook failures from rejected entries, and diagnostics
|
|
2288
|
+
collected before an atomic batch rethrows. Managed completion-chain failures
|
|
2289
|
+
also identify `operationIndex`, `scopeName` and `method`. Secondary errors remain on the
|
|
2290
|
+
context rather than being added to the batch response.
|
|
2291
|
+
|
|
2292
|
+
Remaining child `fileHandlingUploads` entries also reach the batch context with
|
|
2293
|
+
`bulkIndex`, preserving storage and transaction references. Context reuse clears
|
|
2294
|
+
old error diagnostics, retains unresolved upload tracking and starts each new
|
|
2295
|
+
child with separate tracking. Do not use retained tracking as a deletion or retry
|
|
2296
|
+
instruction: files can belong to committed data when an earlier `afterCommit`
|
|
2297
|
+
hook prevents the tracking-release hook from running, or to pending outer work.
|
|
2298
|
+
|
|
2299
|
+
For non-atomic results, `meta.failed` counts rejected calls. A post-commit hook
|
|
2300
|
+
failure can therefore appear as an indexed error even though the write is
|
|
2301
|
+
committed. Inspect the underlying failure and transaction evidence before
|
|
2302
|
+
retrying. Each error entry now has `error.transactionOutcome`; remaining managed
|
|
2303
|
+
integration is tracked separately.
|
|
2304
|
+
No payload or stored-data migration is required for this diagnostic correction.
|
|
2305
|
+
|
|
2306
|
+
Stored file handles also now read as strings when the ordinary SQL driver returns
|
|
2307
|
+
their UTF-8 bytes as a Buffer. The shared normalizer decodes declared `file`
|
|
2308
|
+
attributes, including byte views, without changing blob fields or database
|
|
2309
|
+
columns. Invalid encoding fails with its original cause and field context.
|
|
2310
|
+
Custom getters retain their raw database input. No schema migration or consumer
|
|
2311
|
+
Buffer-to-string wrapper is needed; use the resulting URL string directly.
|
|
2312
|
+
|
|
2313
|
+
Bulk POST/PATCH/DELETE now honor `transaction` in the first argument; previously
|
|
2314
|
+
they ignored it and could commit separately from the caller's transaction.
|
|
2315
|
+
Select `atomic: true` when supplying it, including when the plugin's
|
|
2316
|
+
`defaultAtomic` is false. Each child and its relationship changes use the same
|
|
2317
|
+
transaction. The in-progress managed contract requires `api.transaction` to
|
|
2318
|
+
own the unit. Earlier entries and the failing entry's pending writes roll back
|
|
2319
|
+
together, including when the callback catches a participating write failure.
|
|
2320
|
+
Catching the error does not restore those rows or create a savepoint.
|
|
2321
|
+
|
|
2322
|
+
```javascript
|
|
2323
|
+
await api.transaction(async transaction => {
|
|
2324
|
+
return api.resources.books.bulkPatch({
|
|
2325
|
+
operations: [{ id: '42', data: { title: 'Updated title' } }],
|
|
2326
|
+
format: 'plain',
|
|
2327
|
+
returning: 'minimal',
|
|
2328
|
+
atomic: true,
|
|
2329
|
+
transaction
|
|
2330
|
+
})
|
|
2331
|
+
})
|
|
2332
|
+
```
|
|
2333
|
+
|
|
2334
|
+
`atomic: false` with a supplied transaction now fails with
|
|
2335
|
+
`REST_API_VALIDATION` on `transaction`, before any child or SQL runs. Independent
|
|
2336
|
+
per-entry completion requires calls without an outer transaction. A supplied
|
|
2337
|
+
completed transaction also rejects instead of being silently replaced. Calls
|
|
2338
|
+
without `transaction` keep their atomic/non-atomic ownership. Completion hooks,
|
|
2339
|
+
file cleanup and notifications use the managed owner. No stored-data migration
|
|
2340
|
+
is needed for this transaction-ownership change. Check any application wrapper
|
|
2341
|
+
that supplies raw transactions to bulk methods.
|
|
2342
|
+
|
|
2343
|
+
## Application-supplied primary IDs in table helpers
|
|
2344
|
+
|
|
2345
|
+
Ordinary table creation, generated migrations and migration diffs now recognize
|
|
2346
|
+
the declared primary ID by its mapped column, including `integer` and `string`
|
|
2347
|
+
definitions. Previously only a field with type `id` was recognized; a declared
|
|
2348
|
+
integer/string primary key could produce a duplicate generated ID column.
|
|
2349
|
+
|
|
2350
|
+
For a caller-supplied opaque key, declare the primary key explicitly:
|
|
2351
|
+
|
|
2352
|
+
```js
|
|
2353
|
+
await api.addResource('records', {
|
|
2354
|
+
idProperty: 'record_key',
|
|
2355
|
+
schema: {
|
|
2356
|
+
id: {
|
|
2357
|
+
type: 'string', primary: true, required: true,
|
|
2358
|
+
storage: { column: 'record_key' }
|
|
2359
|
+
},
|
|
2360
|
+
name: { type: 'string' }
|
|
2361
|
+
}
|
|
2362
|
+
})
|
|
2363
|
+
```
|
|
2364
|
+
|
|
2365
|
+
The same pattern works with `type: 'integer'`. `primary: true` keeps the
|
|
2366
|
+
authored column instead of adding an implicit auto-incrementing ID. Supply IDs
|
|
2367
|
+
when writing to a table without database allocation or a default.
|
|
2368
|
+
An opaque ID without that primary declaration cannot use implicit numeric
|
|
2369
|
+
allocation: the helper now rejects that configuration before producing DDL.
|
|
2370
|
+
For lower-level table definitions, `autoIncrement: false` remains available.
|
|
2371
|
+
|
|
2372
|
+
Table snapshots now accept these non-null, single-column integer/string primary
|
|
2373
|
+
keys without requiring auto-increment or a database default. Existing tables are
|
|
2374
|
+
not altered by this change. Regenerate migrations that failed with duplicate ID
|
|
2375
|
+
columns and review the resulting DDL; an unchanged valid table should produce
|
|
2376
|
+
an empty migration diff. Missing, nullable, floating-point and composite resource
|
|
2377
|
+
IDs remain rejected by snapshot validation.
|
|
2378
|
+
|
|
2379
|
+
## Collection filtering hooks
|
|
2380
|
+
|
|
2381
|
+
Collection reads now honor a query builder assigned to
|
|
2382
|
+
`context.knexQuery.query` by a `knexQueryFiltering` hook. Previously, both storage
|
|
2383
|
+
modes could ignore that replacement and omit restrictions added to it; ordinary
|
|
2384
|
+
pagination totals could disagree with the returned page, and canonical totals
|
|
2385
|
+
could omit the restriction too. Related-resource collections use the corrected
|
|
2386
|
+
path as well.
|
|
2387
|
+
|
|
2388
|
+
When a hook needs a separate builder, clone the supplied query and apply further
|
|
2389
|
+
conditions to that clone. Keep its tenant/resource constraints, relationship
|
|
2390
|
+
membership and existing visibility predicates. The selected builder governs the
|
|
2391
|
+
page. Ordinary storage also runs filtering hooks for its separate count query;
|
|
2392
|
+
canonical storage clones the filtered collection query for that count. Apply the
|
|
2393
|
+
complete predicate on every filtering call, including count calls. Surrounding
|
|
2394
|
+
`context.knexQuery` metadata is restored after filtering succeeds or fails.
|
|
2395
|
+
Row-policy functions retain their existing
|
|
2396
|
+
`true`/`false` return contract. This correction adds no public option and requires
|
|
2397
|
+
no change to hooks that already mutate or replace the supplied builder correctly.
|
|
2398
|
+
|
|
2399
|
+
## Full includes and projected fields
|
|
2400
|
+
|
|
2401
|
+
Ordinary-storage belongsTo, hasOne and polymorphic belongsTo includes now select
|
|
2402
|
+
visible SQL projections when no target fieldset is supplied. Previously these
|
|
2403
|
+
paths could omit the projections and pass missing dependencies to computed
|
|
2404
|
+
fields. Full and sparse includes now use the same field-selection rules as
|
|
2405
|
+
collection includes and canonical storage.
|
|
2406
|
+
|
|
2407
|
+
No call changes are required. Remove any fieldset used solely to work around
|
|
2408
|
+
missing projections; keep intentional sparse fieldsets to control response
|
|
2409
|
+
size. For example, `books.get({ id: '1', queryParams: { include: ['author'] } })`
|
|
2410
|
+
now returns the author's visible projections without also specifying
|
|
2411
|
+
`fields.authors`. A projection used only as a computed-field dependency remains
|
|
2412
|
+
absent from a sparse response unless requested. Projection failures reject the
|
|
2413
|
+
operation; full write-response failures happen before an owned commit.
|
|
2414
|
+
|
|
2415
|
+
### Cyclic includes and resource identity
|
|
2416
|
+
|
|
2417
|
+
A resource appears once across a JSON:API document's `data` and `included`.
|
|
2418
|
+
For example, `items.get({ id: '1', format: 'jsonapi', queryParams: { include: ['groups.items'] } })`
|
|
2419
|
+
keeps the item in `data` and its group in `included`. The group's relationship
|
|
2420
|
+
can point back to `items/1` without adding another copy of that item. Relationships
|
|
2421
|
+
discovered through the cycle are retained on the primary resource, and computed
|
|
2422
|
+
fields run once per represented resource. Explicit collection include limits
|
|
2423
|
+
remain in effect when another path revisits the same resource.
|
|
2424
|
+
|
|
2425
|
+
If your response resolver searches only `included`, index both primary `data`
|
|
2426
|
+
and `included` by type and ID instead. Update snapshots that expected a primary
|
|
2427
|
+
resource to be repeated in `included`. Relationship identifiers and resource URLs
|
|
2428
|
+
keep their existing shape; this requires no stored-data migration.
|
|
2429
|
+
|
|
2430
|
+
Plain responses resolve references against both primary and included resources.
|
|
2431
|
+
Peers in a primary collection can therefore expand fully. Actual cycles end in
|
|
2432
|
+
an `{ id }` reference, with `_type` retained for polymorphic relationships;
|
|
2433
|
+
independent branches still receive separate objects. Keep custom computed
|
|
2434
|
+
callbacks free of assumptions that a cyclic path will invoke them twice.
|
|
2435
|
+
|
|
2436
|
+
### Empty includes and refresh links
|
|
2437
|
+
|
|
2438
|
+
JSON:API resource reads and full write responses now retain `included: []` when
|
|
2439
|
+
an explicit include returns no resources. This also applies to empty collections
|
|
2440
|
+
and related endpoints with `data: null`. An explicitly empty include is supported:
|
|
2441
|
+
|
|
2442
|
+
```js
|
|
2443
|
+
await api.resources.groups.get({
|
|
2444
|
+
id: '1',
|
|
2445
|
+
format: 'jsonapi',
|
|
2446
|
+
queryParams: { include: [] }
|
|
2447
|
+
})
|
|
2448
|
+
// { data: { ... }, included: [], links: { self: '.../groups/1?include=' } }
|
|
2449
|
+
```
|
|
2450
|
+
|
|
2451
|
+
Omitting `include` keeps it absent from normalized query parameters and omits
|
|
2452
|
+
`included` when there are no included resources. HTTP `?include=` parses as an
|
|
2453
|
+
explicit empty array and survives serialization into refresh/pagination links.
|
|
2454
|
+
Hooks and direct parser callers that iterate includes should use
|
|
2455
|
+
`context.queryParams?.include ?? []`; do not assume an omitted include has been
|
|
2456
|
+
populated with an empty array. Update response snapshots for explicitly requested
|
|
2457
|
+
includes that used to omit `included`.
|
|
2458
|
+
|
|
2459
|
+
Top-level `links.self` on single-resource and related responses now preserves
|
|
2460
|
+
includes and fieldsets, as collection links do. Follow that link to refresh the
|
|
2461
|
+
same representation. Resource-level `data.links.self` remains the resource URL.
|
|
2462
|
+
Update assertions that assumed those two links were always identical.
|
|
2463
|
+
|
|
2464
|
+
Plain results retain their existing shapes. Programmatic `returning: 'minimal'`
|
|
2465
|
+
and `'none'` retain their selected return contracts; HTTP resource writes select
|
|
2466
|
+
full JSON:API responses. No method arguments or stored records need migration.
|
|
2467
|
+
|
|
2468
|
+
### Relationship visibility hooks
|
|
2469
|
+
|
|
2470
|
+
Row policies and `knexQueryFiltering` hooks with
|
|
2471
|
+
`queryPurpose: 'relationship-identifiers'` now run once per batch of at most
|
|
2472
|
+
100 unique target IDs for each resource type. Remove assumptions that this is
|
|
2473
|
+
one callback per request, and remove flags that skip a visibility predicate
|
|
2474
|
+
after the first query. Add the full predicate to each supplied builder; cloning
|
|
2475
|
+
that builder remains supported. Keep ordering, pagination and include limits
|
|
2476
|
+
in the operation/resource settings, and retain the selected ID and mandatory
|
|
2477
|
+
scope constraints when customizing a visibility query.
|
|
2478
|
+
|
|
2479
|
+
Identifier-visibility selections no longer put every ID into one `WHERE IN`. These reads
|
|
2480
|
+
select distinct IDs, preserve the input linkage order and duplicate occurrences,
|
|
2481
|
+
and retain the caller's transaction. A later batch failure rejects the whole
|
|
2482
|
+
read. See the [row-policy contract](17-row-policies.md#query-purposes-and-lifecycle-coverage)
|
|
2483
|
+
for the context supplied to these hooks.
|
|
2484
|
+
|
|
2485
|
+
### Nested belongs-to include batches
|
|
2486
|
+
|
|
2487
|
+
Regular and polymorphic belongs-to includes now load at most 100 unique target
|
|
2488
|
+
IDs per resource type in each query. Large nested includes no longer place all
|
|
2489
|
+
of those IDs in one SQL statement. Target permission/filter hooks with
|
|
2490
|
+
`queryPurpose: 'include'` and SQL projection `select` callbacks run for every
|
|
2491
|
+
batch. Apply the complete predicate or projection each time; remove assumptions
|
|
2492
|
+
that they run once per include path. This does not change computed-field
|
|
2493
|
+
callbacks into query callbacks.
|
|
2494
|
+
|
|
2495
|
+
All target batches are gathered before traversing nested collections. Existing
|
|
2496
|
+
global and per-parent collection limits still apply across that complete parent
|
|
2497
|
+
set. A later batch failure rejects the operation; a failed full write response
|
|
2498
|
+
rolls back an owned transaction, while a borrowed transaction remains the
|
|
2499
|
+
caller's responsibility. Canonical storage also batches parent-link prefetches.
|
|
2500
|
+
This change adds no method options and does not impose a total response-memory
|
|
2501
|
+
bound or batch every collection include query.
|
|
2502
|
+
|
|
2503
|
+
### Large collection identifier queries
|
|
2504
|
+
|
|
2505
|
+
Collection includes and relationship-metadata reads now deduplicate their ID
|
|
2506
|
+
lists. Above 100 distinct IDs, PostgreSQL receives one array parameter and
|
|
2507
|
+
SQLite receives one JSON array parameter through JSON1. Sorting, global limits,
|
|
2508
|
+
per-parent limits and policy predicates still run in the original SQL query.
|
|
2509
|
+
SQL projection and filtering callbacks retain their collection-query frequency.
|
|
2510
|
+
The separately batched belongs-to loaders retain the behavior described above.
|
|
2511
|
+
|
|
2512
|
+
Code that inspects raw Knex statements must allow array predicates and ID
|
|
2513
|
+
subqueries; use the documented query-purpose/context fields when applying
|
|
2514
|
+
policies. Small lists keep ordinary `IN` predicates. MySQL retains its ordinary
|
|
2515
|
+
`IN` query through Knex's text protocol, preserving its existing numeric and
|
|
2516
|
+
collation comparisons. These changes add no public options. They avoid database
|
|
2517
|
+
parameter-count overflow, but complete ID lists and responses still consume
|
|
2518
|
+
memory proportional to their size.
|
|
2519
|
+
|
|
2520
|
+
### Explicit includes and linkage reads
|
|
2521
|
+
|
|
2522
|
+
An explicitly included relationship is now loaded directly by its include
|
|
2523
|
+
loader. The library skips the preliminary identifier read that the loader used
|
|
2524
|
+
to replace afterward. For standard many-to-many includes, authorized target
|
|
2525
|
+
records are selected and limited in SQL before the library reads their parent
|
|
2526
|
+
mappings. Window includes keep their per-parent limit behavior. Result ordering,
|
|
2527
|
+
duplicate-edge handling, sparse fields and transaction ownership remain the same.
|
|
2528
|
+
|
|
2529
|
+
Hooks observing `relationship-identifiers` queries will see fewer calls for
|
|
2530
|
+
explicitly included relationships. Target and ordinary pivot include permission
|
|
2531
|
+
checks still run. Mapping reads now occur after the target query, on the same
|
|
2532
|
+
transaction; a mapping-read failure still fails the operation. Complete unlimited
|
|
2533
|
+
includes can require additional mapping pages. No API option or data migration
|
|
2534
|
+
is needed.
|
|
2535
|
+
|
|
2536
|
+
### Sparse responses and relationship hooks
|
|
2537
|
+
|
|
2538
|
+
Sparse fieldsets now avoid database reads for omitted relationship linkage in
|
|
2539
|
+
both storage modes. For example, `queryParams: { fields: { books: 'title' } }`
|
|
2540
|
+
does not load the book's collection identifiers or check visibility of to-one
|
|
2541
|
+
linkage that will be removed from the response. Selected linkage retains its
|
|
2542
|
+
visibility checks. Requests without a fieldset for that resource keep their
|
|
2543
|
+
existing linkage behavior.
|
|
2544
|
+
|
|
2545
|
+
An explicit `include` still loads its resources even when the corresponding
|
|
2546
|
+
relationship field is omitted. Nested includes, target permissions, computed
|
|
2547
|
+
attribute dependencies and transaction ownership retain their existing behavior.
|
|
2548
|
+
No new option is needed.
|
|
2549
|
+
|
|
2550
|
+
If a hook reads collection linkage from `context.record`, request that
|
|
2551
|
+
relationship in the resource's fieldset or load the data explicitly for the
|
|
2552
|
+
hook's purpose. Do not use the number of `relationship-identifiers` filtering
|
|
2553
|
+
hooks as a request notification: omitted linkage no longer triggers those reads.
|
|
2554
|
+
Authorization for requested primary/included resources still runs; authorization
|
|
2555
|
+
must not depend on an incidental query for an omitted relationship.
|
|
2556
|
+
|
|
2557
|
+
### Canonical parent-link reads
|
|
2558
|
+
|
|
2559
|
+
Canonical many-to-many linkage now reads at most 101 physical link rows per
|
|
2560
|
+
query, retaining its existing 100-parent batch size. It still returns complete
|
|
2561
|
+
visible linkage. Raw linkage is assembled in physical link-ID order for each
|
|
2562
|
+
parent; use declared include ordering or related-resource query sorting when
|
|
2563
|
+
application order matters. Configured include limits and ordering still apply
|
|
2564
|
+
to the complete requested parent set.
|
|
2565
|
+
|
|
2566
|
+
The prefetch now also enforces the relationship's declared target resource type,
|
|
2567
|
+
matching the existing related-resource query. A same-key physical link to a
|
|
2568
|
+
different resource no longer appears in this relationship. No method option or
|
|
2569
|
+
data migration is needed. Later page failures reject the operation and preserve
|
|
2570
|
+
the existing owned/borrowed transaction behavior. Raw SQL observers should expect
|
|
2571
|
+
additional reads for large link collections.
|
|
2572
|
+
|
|
2573
|
+
### Ordinary pivot membership writes
|
|
2574
|
+
|
|
2575
|
+
Many-to-many additions and replacements now read existing pivot rows for at
|
|
2576
|
+
most 100 requested target IDs at a time, paging duplicate physical edges in
|
|
2577
|
+
bounded results. New inserts still use batches of 100 rows. Replacement deletes
|
|
2578
|
+
unwanted rows using one predicate for the complete desired set, including large
|
|
2579
|
+
SQLite/PostgreSQL ID lists. Retained matching pivot rows keep their IDs and
|
|
2580
|
+
metadata. Empty replacement removes every link for that owner.
|
|
2581
|
+
|
|
2582
|
+
Equivalent requested target IDs, as resolved by the database's target ID column,
|
|
2583
|
+
are written once using the first submitted spelling. For example, `BETA` and
|
|
2584
|
+
`beta` referring to the same target no longer create two new pivot rows in one
|
|
2585
|
+
request. Existing duplicate rows are not merged automatically. Keep target ID
|
|
2586
|
+
and pivot reference column types/collations consistent; this does not migrate
|
|
2587
|
+
existing application schemas.
|
|
2588
|
+
|
|
2589
|
+
Method arguments, permission hooks and transaction ownership are unchanged.
|
|
2590
|
+
Code that observes raw SQL must allow paged pivot reads and a single exclusion
|
|
2591
|
+
DELETE for replacement. The complete input and keep-list still require memory
|
|
2592
|
+
proportional to their size; the batching does not set a new relationship limit.
|
|
2593
|
+
|
|
2594
|
+
## Relationship target validation
|
|
2595
|
+
|
|
2596
|
+
POST, PUT and PATCH payloads, including relationship endpoints, now use one
|
|
2597
|
+
target-validation contract. Readable target rows are selected in batches of at
|
|
2598
|
+
most 100 distinct identifiers; `checkPermissions` then runs with `method: 'get'`
|
|
2599
|
+
for each distinct target within each relationship. Its `originalContext`
|
|
2600
|
+
contains that target's `id`, `scopeName`, `schemaInfo` and `minimalRecord`, plus
|
|
2601
|
+
the caller's auth and transaction. Repeated identifiers do not repeat permission
|
|
2602
|
+
checks within one relationship. A target referenced through two relationships
|
|
2603
|
+
is checked for each relationship.
|
|
2604
|
+
|
|
2605
|
+
Many-to-many writes no longer run a full target GET after this check. Move any
|
|
2606
|
+
link-authorization logic from `checkDataPermissionsGet`, `beforeDataGet`, getters,
|
|
2607
|
+
computed fields, enrichment or `finishGet` into the existing permission hook:
|
|
2608
|
+
|
|
2609
|
+
```js
|
|
2610
|
+
checkPermissions: {
|
|
2611
|
+
functionName: 'authorize-readable-target',
|
|
2612
|
+
handler: ({ context }) => {
|
|
2613
|
+
if (context.method !== 'get') return
|
|
2614
|
+
const request = context.originalContext
|
|
2615
|
+
assertCanRead(request.auth, request.minimalRecord)
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
```
|
|
2619
|
+
|
|
2620
|
+
The minimal record contains stored attributes and to-one linkage; it has no
|
|
2621
|
+
response getters/computed fields or includes. Response hooks still run for real
|
|
2622
|
+
GETs and requested full write responses. Use write/transaction hooks for side
|
|
2623
|
+
effects of linking. Remove assumptions about one target GET per supplied ID.
|
|
2624
|
+
|
|
2625
|
+
Row policies and `knexQueryFiltering` run with
|
|
2626
|
+
`queryPurpose: 'relationship-validation'`, empty client filters and no individual
|
|
2627
|
+
`context.id`. Add the complete row predicate to every supplied builder. Retain
|
|
2628
|
+
its selected ID and mandatory scope constraints; the filtered ID subquery
|
|
2629
|
+
selects the full minimal rows without multiplying them through policy joins.
|
|
2630
|
+
An alternate-spelling lookup can run an additional scoped query to retain the
|
|
2631
|
+
database's ID collation semantics. Per-target decisions belong in the permission
|
|
2632
|
+
hook, where the individual ID and record are available.
|
|
2633
|
+
|
|
2634
|
+
Target locks and storage existence checks remain. A failure rejects before
|
|
2635
|
+
link writes; an owned operation rolls back, while a borrowed transaction stays
|
|
2636
|
+
with its caller. Internal pivot/link storage helpers now assume the resource
|
|
2637
|
+
operation has authorized its payload; applications should use resource methods.
|
|
2638
|
+
No option retains the old target GET lifecycle. Port application hooks that
|
|
2639
|
+
relied on those implicit reads.
|
|
2640
|
+
|
|
2641
|
+
## Setter, getter and computed-field failures
|
|
2642
|
+
|
|
2643
|
+
Field callbacks now propagate failures in both formats and storage modes,
|
|
2644
|
+
including primary resources, included resources and nested includes. There is
|
|
2645
|
+
no additional policy option. The changes are:
|
|
2646
|
+
|
|
2647
|
+
| Callback failure | Previous behavior | Current behavior |
|
|
2648
|
+
| --- | --- | --- |
|
|
2649
|
+
| Unexpected setter error | Converted to validation error (422), losing its cause | Operation rejects with the original cause; HTTP 500 |
|
|
2650
|
+
| Unexpected getter error | Logged; response kept the current field value | Operation rejects; HTTP 500 |
|
|
2651
|
+
| Unexpected computed-field error | Logged; response substituted null | Operation rejects; HTTP 500 |
|
|
2652
|
+
| Typed API error | Propagated at field callbacks | Unchanged on reads; retained as the write wrapper's cause, with code/details still readable |
|
|
2653
|
+
| Relationship target permission failure | Could be replaced with not-found (404) | Original classification/cause retained by target validation; a missing target still produces 404 |
|
|
2654
|
+
|
|
2655
|
+
In the cause chain, unexpected callback errors have `cause` and
|
|
2656
|
+
`context: { scopeName, fieldName, phase }`, where `phase` is `setter`, `getter`
|
|
2657
|
+
or `computed`. Include processing can add outer wrappers; follow the cause
|
|
2658
|
+
chain to the field error. Synchronous throws and asynchronous rejections follow
|
|
2659
|
+
the same rule, including non-Error values such as null or undefined.
|
|
2660
|
+
|
|
2661
|
+
If a setter intentionally rejects user input, change its generic throw to the
|
|
2662
|
+
existing validation error class. For example, inside a resource schema:
|
|
2663
|
+
|
|
2664
|
+
```js
|
|
2665
|
+
import { RestApiValidationError } from 'json-rest-api'
|
|
2666
|
+
|
|
2667
|
+
const quantityField = {
|
|
2668
|
+
type: 'number',
|
|
2669
|
+
setter: value => {
|
|
2670
|
+
if (!Number.isInteger(value)) {
|
|
2671
|
+
// Previously: throw new Error('Quantity must be a whole number')
|
|
2672
|
+
throw new RestApiValidationError('Quantity must be a whole number', {
|
|
2673
|
+
fields: ['data.attributes.quantity'],
|
|
2674
|
+
violations: [{
|
|
2675
|
+
field: 'data.attributes.quantity',
|
|
2676
|
+
rule: 'integer',
|
|
2677
|
+
message: 'Quantity must be a whole number'
|
|
2678
|
+
}]
|
|
2679
|
+
})
|
|
2680
|
+
}
|
|
2681
|
+
return value
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
```
|
|
2685
|
+
|
|
2686
|
+
This keeps intentional input rejection at 422. A typed forbidden resource
|
|
2687
|
+
error remains 403. Unexpected programming or service failures should propagate.
|
|
2688
|
+
When a value is intentionally unavailable, return that domain value explicitly:
|
|
2689
|
+
|
|
2690
|
+
```js
|
|
2691
|
+
const helpfulnessField = {
|
|
2692
|
+
type: 'number',
|
|
2693
|
+
computed: true,
|
|
2694
|
+
dependencies: ['helpful_votes', 'total_votes'],
|
|
2695
|
+
compute: ({ attributes }) => attributes.total_votes === 0
|
|
2696
|
+
? null
|
|
2697
|
+
: 100 * attributes.helpful_votes / attributes.total_votes
|
|
2698
|
+
}
|
|
2699
|
+
```
|
|
2700
|
+
|
|
2701
|
+
A callback's own intentional fallback still works. Review existing generic
|
|
2702
|
+
validation throws and code that depended on the library substituting null or
|
|
2703
|
+
returning an untransformed value. Handle rejected reads and writes instead of
|
|
2704
|
+
treating an incomplete response as success.
|
|
2705
|
+
|
|
2706
|
+
GET/query return no successful partial response after an executed callback
|
|
2707
|
+
fails. POST/PUT/PATCH full-response enrichment runs before owned commit, so its
|
|
2708
|
+
failure rolls back owned record and relationship changes. Borrowed transactions
|
|
2709
|
+
remain active for their owner to complete. Minimal/none responses do not run
|
|
2710
|
+
callbacks solely to enrich an unused full response; callbacks needed for other
|
|
2711
|
+
work, such as validating related resources, still run and can fail the write.
|
|
2712
|
+
Sparse fieldsets likewise avoid callbacks that are neither selected nor needed
|
|
2713
|
+
as dependencies. These rules do not undo a write whose commit was already
|
|
2714
|
+
acknowledged; post-commit side effects and cleanup retain their own semantics.
|
|
2715
|
+
|
|
2716
|
+
## Stored relationship metadata
|
|
2717
|
+
|
|
2718
|
+
Canonical reverse relationship linkage now selects only child identity and
|
|
2719
|
+
parent-reference columns. Large target attributes are no longer fetched merely
|
|
2720
|
+
to construct linkage. Returned IDs, filters and transaction ownership are
|
|
2721
|
+
unchanged. Query hooks for `queryPurpose: 'relationship-identifiers'` should
|
|
2722
|
+
preserve these selections when replacing the builder; add predicates against
|
|
2723
|
+
the underlying columns as usual. No payload or data migration is needed.
|
|
2724
|
+
|
|
2725
|
+
Polymorphic linkage read from storage must use a target type declared by its
|
|
2726
|
+
relationship. For example, with
|
|
2727
|
+
`subject: { belongsToPolymorphic: { types: ['groups'], ... } }`, a stored
|
|
2728
|
+
`subjectType: 'items'` is invalid even if an `items` resource exists. Previously
|
|
2729
|
+
ordinary includes could silently omit that target, while canonical storage
|
|
2730
|
+
could include it. Both modes now reject with HTTP 500 and programmatic error
|
|
2731
|
+
context `{ scopeName, relationshipName, phase: 'relationshipData' }`.
|
|
2732
|
+
This also applies to linkage returned without an include. A disallowed target
|
|
2733
|
+
submitted through the write API still produces a validation error (422).
|
|
2734
|
+
|
|
2735
|
+
Repair inconsistent stored type/ID pairs or update the declared relationship
|
|
2736
|
+
when the target type is intentional. Nullable, intentionally empty
|
|
2737
|
+
relationships retain their existing representation. Sparse fieldsets do not
|
|
2738
|
+
guarantee bypassing stored-linkage checks: preliminary reads and relationship
|
|
2739
|
+
selection can still require those columns.
|
|
2740
|
+
|
|
2741
|
+
Required reverse-relationship metadata must also exist: a `via` relationship
|
|
2742
|
+
must refer to a polymorphic relationship, and canonical foreign-key mappings
|
|
2743
|
+
must resolve to a stored field. Missing required metadata now rejects instead
|
|
2744
|
+
of returning empty linkage. Correct the resource declaration or stored
|
|
2745
|
+
canonical metadata, then retry. Schema-read failures during field selection
|
|
2746
|
+
and record conversion retain their original cause and resource context with
|
|
2747
|
+
phase `relationshipMetadata`; they are no longer ignored as if the resource
|
|
2748
|
+
had no relationships.
|
|
2749
|
+
|
|
2750
|
+
Nested polymorphic include paths can still apply to only some declared target
|
|
2751
|
+
types. If `subject` allows `writers` and `books`, and only writers have a
|
|
2752
|
+
`profile`, `include=subject.profile` remains valid; books simply have no profile
|
|
2753
|
+
branch. A path supported by none of the declared target types is rejected by
|
|
2754
|
+
request validation.
|
|
2755
|
+
|
|
2756
|
+
These read checks also apply while preparing write responses. A failure before
|
|
2757
|
+
owned commit rolls back the owned write; a borrowed transaction remains its
|
|
2758
|
+
owner's responsibility. An acknowledged commit cannot be undone by a later
|
|
2759
|
+
response or side-effect error.
|
|
2760
|
+
|
|
2761
|
+
## Schema enrichment and canonical registration
|
|
2762
|
+
|
|
2763
|
+
Use `schema:enrich` to change attribute definitions. Use `searchSchema:enrich`
|
|
2764
|
+
to change filter definitions. Previously the search hook received the attribute
|
|
2765
|
+
map in `context.fields`, so edits could affect writes while failing to change
|
|
2766
|
+
filter validation. It now receives the generated search map, including explicit
|
|
2767
|
+
search aliases. Adding, editing, deleting or replacing `context.fields` affects
|
|
2768
|
+
the search contract. The hook also runs when the initial search map is empty.
|
|
2769
|
+
|
|
2770
|
+
```js
|
|
2771
|
+
await api.customize({
|
|
2772
|
+
hooks: {
|
|
2773
|
+
'schema:enrich': {
|
|
2774
|
+
functionName: 'declare-product-stock',
|
|
2775
|
+
handler: ({ context }) => {
|
|
2776
|
+
if (context.scopeName !== 'products') return
|
|
2777
|
+
context.fields.stock = { type: 'integer', defaultTo: 0 }
|
|
2778
|
+
}
|
|
2779
|
+
},
|
|
2780
|
+
'searchSchema:enrich': {
|
|
2781
|
+
functionName: 'declare-product-filters',
|
|
2782
|
+
handler: ({ context }) => {
|
|
2783
|
+
if (context.scopeName !== 'products') return
|
|
2784
|
+
context.fields.nameContains = {
|
|
2785
|
+
type: 'string', actualField: 'name', filterOperator: 'contains'
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
})
|
|
2791
|
+
// Register resources after installing their schema hooks.
|
|
2792
|
+
```
|
|
2793
|
+
|
|
2794
|
+
Treat `originalFields` as inspection input; edit `fields`. Definitions are
|
|
2795
|
+
shallow copies, so this is not a guarantee of deep immutability. Search index
|
|
2796
|
+
hints are applied to the hook's final result. A search-only alias is not a
|
|
2797
|
+
writable attribute.
|
|
2798
|
+
|
|
2799
|
+
Both storage modes now use the enriched compiled field definitions. Canonical
|
|
2800
|
+
registration previously persisted raw authored definitions and then overwrote
|
|
2801
|
+
some enriched metadata. It now allocates storage from compiled definitions and
|
|
2802
|
+
attaches its descriptor to the same compiled schema. This preserves types,
|
|
2803
|
+
removed/added fields, search overrides and callbacks without a second schema
|
|
2804
|
+
compilation. Canonical metadata no longer invents an attribute declaration for
|
|
2805
|
+
an implicit resource ID; metadata consumers should use `schemaInfo.idProperty`
|
|
2806
|
+
to identify the resource ID configuration.
|
|
2807
|
+
|
|
2808
|
+
Enrichment runs once at resource registration, and once for each canonical
|
|
2809
|
+
`addKnexFields` compilation. Repeated canonical `createKnexTable()` refreshes
|
|
2810
|
+
storage metadata without rerunning enrichment or restoring removed search
|
|
2811
|
+
fields. Deep registry allocation does not update a running resource's compiled
|
|
2812
|
+
schema; use the resource helper and retain its expanded declaration in source.
|
|
2813
|
+
Declare callbacks in source again after restart: persisted JSON cannot restore
|
|
2814
|
+
functions. Ordinary `addKnexFields` retains its DDL-only behavior.
|
|
2815
|
+
|
|
2816
|
+
Fastify now resolves the current cached request contract when validating a
|
|
2817
|
+
payload. A belongs-to alias added through canonical `addKnexFields` can be used
|
|
2818
|
+
in resource POST/PUT/PATCH documents after server startup, as with Express and
|
|
2819
|
+
programmatic calls. The JSON Schema published at route registration remains an
|
|
2820
|
+
initialization snapshot; finalize declarations before generating route-based
|
|
2821
|
+
API documentation.
|
|
2822
|
+
|
|
2823
|
+
### Computed-field enrichment
|
|
2824
|
+
|
|
2825
|
+
Plugins that derive computed fields from schema metadata should use
|
|
2826
|
+
`computedSchema:enrich` instead of adding fields to `vars.schemaInfo.computed`
|
|
2827
|
+
in `resource:added`. The compiler awaits this hook after attribute and search
|
|
2828
|
+
enrichment, including during canonical field additions. Edit or replace
|
|
2829
|
+
`context.fields`; each definition must have `computed: true` and a `type`,
|
|
2830
|
+
and any supplied `compute` callback must be a function. Computed names cannot
|
|
2831
|
+
collide with stored attributes.
|
|
2832
|
+
|
|
2833
|
+
This validation also applies to authored computed fields. `compute: false`,
|
|
2834
|
+
`0`, `null` and `''` previously registered successfully but silently omitted
|
|
2835
|
+
the field from output. They now fail registration with `invalid compute
|
|
2836
|
+
function`. Supply a function, or omit `compute` when an `enrichAttributes`
|
|
2837
|
+
hook intentionally supplies the value.
|
|
2838
|
+
|
|
2839
|
+
```js
|
|
2840
|
+
await api.customize({
|
|
2841
|
+
hooks: {
|
|
2842
|
+
'computedSchema:enrich': {
|
|
2843
|
+
functionName: 'derive-product-summary',
|
|
2844
|
+
handler: ({ context }) => {
|
|
2845
|
+
if (context.scopeName !== 'products') return
|
|
2846
|
+
context.fields.summary = {
|
|
2847
|
+
type: 'string',
|
|
2848
|
+
computed: true,
|
|
2849
|
+
dependencies: ['name'],
|
|
2850
|
+
compute: ({ attributes }) => `Product: ${attributes.name}`
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
})
|
|
2856
|
+
```
|
|
2857
|
+
|
|
2858
|
+
The context also exposes `originalFields` (authored computed declarations),
|
|
2859
|
+
`schemaStructure`, `searchSchemaStructure`, `schemaRelationships`, `idProperty`, `scopeName` and
|
|
2860
|
+
`scopeOptions` for inspection. Change stored fields in `schema:enrich` and
|
|
2861
|
+
search fields in `searchSchema:enrich`; the computed hook's other structures
|
|
2862
|
+
are inspection inputs, not an alternative configuration mutation API.
|
|
2863
|
+
Definitions are shallow copies, not deeply immutable objects.
|
|
2864
|
+
|
|
2865
|
+
LabelPlugin now uses this stage. Automatic labels survive canonical field
|
|
2866
|
+
additions and use the newly compiled search/preference candidates. Explicit
|
|
2867
|
+
stored or computed labels remain authoritative. If computed enrichment or
|
|
2868
|
+
validation fails during canonical `addKnexFields`, the candidate is rejected
|
|
2869
|
+
before field allocation; existing compiled metadata and records remain usable.
|
|
2870
|
+
|
|
2871
|
+
### Existing canonical data
|
|
2872
|
+
|
|
2873
|
+
Registration now rejects removal or remapping of an existing stored field when
|
|
2874
|
+
the resource has records. The error starts with `Canonical storage migration
|
|
2875
|
+
required` and identifies the field. An owned registration rolls back metadata
|
|
2876
|
+
changes; a borrowed registry transaction remains the caller's responsibility.
|
|
2877
|
+
This guard prevents a changed schema order or newly effective enrichment hook
|
|
2878
|
+
from silently reading one field's existing slot as another field.
|
|
2879
|
+
|
|
2880
|
+
Before upgrading an app with existing canonical data:
|
|
2881
|
+
|
|
2882
|
+
1. Inspect its schema hooks and compare their final field types with persisted
|
|
2883
|
+
field definitions and `canonicalFieldMap`.
|
|
2884
|
+
2. Keep every existing stored field in the declaration. For changes that retain
|
|
2885
|
+
the same storage types, preserve existing slot assignments with an explicit
|
|
2886
|
+
complete `canonicalFieldsMap`; allocate unused slots for new fields.
|
|
2887
|
+
3. If a field needs a different slot pool, a new value encoding or removal,
|
|
2888
|
+
migrate and verify its data and metadata explicitly before registering that
|
|
2889
|
+
changed layout. A slot map cannot convert values between storage types.
|
|
2890
|
+
4. Restart with the expanded schema, callbacks and slot map, then verify old
|
|
2891
|
+
records, new writes, filters and relationship linkage.
|
|
2892
|
+
|
|
2893
|
+
Registration is not a data migration. The layout guard does not verify semantic
|
|
2894
|
+
changes to callbacks or values that retain the same physical slot. Empty
|
|
2895
|
+
resources can change layout without this guard rejecting registration.
|
|
2896
|
+
|
|
2897
|
+
Older canonical registrations could also allocate an undeclared numeric field
|
|
2898
|
+
named after a custom `idProperty`. That synthetic field is no longer allocated.
|
|
2899
|
+
If its removal triggers the guard, inspect the old slot and metadata in your
|
|
2900
|
+
data migration. Remove its field configuration/schema entry only after proving
|
|
2901
|
+
the slot has no values to preserve, and retain every real field's slot map.
|
|
2902
|
+
Do not add a fake attribute merely to retain the unused allocation.
|
|
2903
|
+
|
|
2904
|
+
## Write hooks and the returned response
|
|
2905
|
+
|
|
2906
|
+
POST, PUT and PATCH prepare their response before committing an owned
|
|
2907
|
+
transaction. `finish`, then `finishPost`/`finishPut`/`finishPatch`, are the last
|
|
2908
|
+
hooks that can customize `context.responseRecord`. The library then normalizes
|
|
2909
|
+
schema-declared values and reapplies requested sparse fieldsets to full output,
|
|
2910
|
+
including related records. A finish hook cannot expose an excluded field by
|
|
2911
|
+
adding it after the nested GET. Minimal responses retain their resource type
|
|
2912
|
+
and ID; `returning: 'none'` still returns undefined.
|
|
2913
|
+
|
|
2914
|
+
The returned minimal/full response is now a detached snapshot. `afterCommit`
|
|
2915
|
+
is awaited for external side effects, but mutations to its context or response
|
|
2916
|
+
do not rewrite the prepared result. Move response changes from `afterCommit`
|
|
2917
|
+
into the appropriate finish hook. For example, JSON:API response metadata can be
|
|
2918
|
+
added with:
|
|
2919
|
+
|
|
2920
|
+
```js
|
|
2921
|
+
await api.customize({
|
|
2922
|
+
hooks: {
|
|
2923
|
+
finishPost: {
|
|
2924
|
+
functionName: 'add-write-metadata',
|
|
2925
|
+
handler: ({ context }) => {
|
|
2926
|
+
if (context.format !== 'jsonapi' || context.returning !== 'full') return
|
|
2927
|
+
context.responseRecord.meta = { requestId: context.requestId }
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
})
|
|
2932
|
+
```
|
|
2933
|
+
|
|
2934
|
+
Use data in response extensions and metadata, not functions or live application
|
|
2935
|
+
objects. The snapshot uses `structuredClone`; a non-cloneable extension rejects
|
|
2936
|
+
before commit, with owned transactions rolled back and borrowed transactions
|
|
2937
|
+
left to their owner. This does not add validation of arbitrary JSON attributes
|
|
2938
|
+
or metadata. Schema-declared temporal fields still use their existing conversion
|
|
2939
|
+
rules. A failing `afterCommit` hook still rejects after the database has committed;
|
|
2940
|
+
the error reports `committed`.
|
|
2941
|
+
|
|
2942
|
+
POST, PUT and PATCH clear any ID left by a previous operation before processing
|
|
2943
|
+
hooks run. POST's generated ID becomes available as `context.id` at
|
|
2944
|
+
`afterDataCallPost`, and remains available to later hooks and the nested response
|
|
2945
|
+
GET. Before insertion, a caller-supplied ID is available in
|
|
2946
|
+
`context.inputRecord.data.id` after input conversion. PUT and PATCH assign their
|
|
2947
|
+
validated target ID before `beforeSchemaValidate`. Early processing hooks can
|
|
2948
|
+
inspect the current request in `context.params`; they must not use a previous
|
|
2949
|
+
operation's ID.
|
|
2950
|
+
|
|
2951
|
+
## Direct imports of the setter helper
|
|
2952
|
+
|
|
2953
|
+
Resource declarations and setter callback arguments keep their existing shape.
|
|
2954
|
+
If application code directly imported `applyFieldSetters` from the internal
|
|
2955
|
+
resource-method `common.js`, update its call:
|
|
2956
|
+
|
|
2957
|
+
```js
|
|
2958
|
+
// Before: assign the helper's returned attributes.
|
|
2959
|
+
context.inputRecord.data.attributes = await applyFieldSetters(
|
|
2960
|
+
context.inputRecord.data.attributes, context.schemaInfo, context, api, helpers
|
|
2961
|
+
)
|
|
2962
|
+
|
|
2963
|
+
// Now: the helper assigns the transformed attributes to this write context.
|
|
2964
|
+
await applyFieldSetters(context, api, helpers)
|
|
2965
|
+
```
|
|
2966
|
+
|
|
2967
|
+
The helper returns no value. It runs setters at the same point, after validation
|
|
2968
|
+
and before storage, without completing the transaction. Applications using
|
|
2969
|
+
public resource calls need no change for this internal simplification.
|
|
2970
|
+
The [lifecycle guide](13-hooks-and-lifecycle.md)
|
|
2971
|
+
documents the retained hooks and their ordering.
|
|
2972
|
+
|
|
2973
|
+
## Application rollout and experimental features
|
|
2974
|
+
|
|
2975
|
+
Coordinate dependency versions, consumer repositories, generated code and seeds
|
|
2976
|
+
before upgrading applications. Run their actual workflows against the intended
|
|
2977
|
+
package set. Library-side verification does not establish consumer compatibility.
|
|
2978
|
+
|
|
2979
|
+
### JSKIT repositories
|
|
2980
|
+
|
|
2981
|
+
JSKIT's `Document` / `Documents` repository methods keep their existing public
|
|
2982
|
+
names and JSON:API responses. Repository callers continue to pass plain payloads
|
|
2983
|
+
and `{ trx, context }` options. The shared implementation selects `data` for
|
|
2984
|
+
writes, `format: 'jsonapi'` for documents, and `returning: 'full'` for returned
|
|
2985
|
+
write attributes. DELETE explicitly selects `returning: 'none'`.
|
|
2986
|
+
|
|
2987
|
+
Custom repositories calling the library directly need the same changes. Plain
|
|
2988
|
+
collection reads return an envelope with a `data` array; plain single-resource
|
|
2989
|
+
reads and full writes return the record itself. When a foreign-key field has a
|
|
2990
|
+
relationship alias, plain library input uses that alias. For example,
|
|
2991
|
+
`authorId: { belongsTo: 'authors', as: 'author' }` accepts `data: { author: id }`.
|
|
2992
|
+
JSKIT's shared repository maps its domain payload's `authorId` to `author` and
|
|
2993
|
+
rejects payloads that supply both names.
|
|
2994
|
+
|
|
2995
|
+
Copied repositories can reuse the shared CRUD implementation and retain their
|
|
2996
|
+
application-specific methods. Here `lockDocumentById` is the application's
|
|
2997
|
+
existing scoped lock method, kept in the same module:
|
|
2998
|
+
|
|
2999
|
+
```js
|
|
3000
|
+
import { createCrudJsonApiRepository } from '@jskit-ai/crud-core/server/jsonApiRepository'
|
|
3001
|
+
|
|
3002
|
+
const repository = {
|
|
3003
|
+
...createCrudJsonApiRepository({ api, resource, resourceScopeName }),
|
|
3004
|
+
lockDocumentById
|
|
3005
|
+
}
|
|
3006
|
+
```
|
|
3007
|
+
|
|
3008
|
+
Preserve custom scoping, locks and write normalization when removing copied
|
|
3009
|
+
code. Standard document writes return full documents; the shared repository
|
|
3010
|
+
does not forward a `returning` option. A custom minimal-response path still
|
|
3011
|
+
needs an explicit resource call with `data`, `format: 'jsonapi'` and
|
|
3012
|
+
`returning: 'minimal'`.
|
|
3013
|
+
|
|
3014
|
+
Use the repository's transaction owner for operations involving JSON REST:
|
|
3015
|
+
|
|
3016
|
+
```js
|
|
3017
|
+
// Before: a raw Knex owner surrounded JSON REST repository writes.
|
|
3018
|
+
await knex.transaction(async trx => {
|
|
3019
|
+
await repository.createDocument(payload, { trx, context })
|
|
3020
|
+
})
|
|
3021
|
+
|
|
3022
|
+
// After: the repository delegates ownership to api.transaction.
|
|
3023
|
+
await repository.withTransaction(async trx => {
|
|
3024
|
+
const document = await repository.createDocument(payload, { trx, context })
|
|
3025
|
+
await trx('audit_entries').insert({ record_id: document.data.id })
|
|
3026
|
+
})
|
|
3027
|
+
```
|
|
3028
|
+
|
|
3029
|
+
Pass `trx` explicitly to every participating repository call. The transaction
|
|
3030
|
+
remains callable for raw SQL; return normally to commit and throw to roll back.
|
|
3031
|
+
Keep independent raw Knex transaction helpers for integrations that perform
|
|
3032
|
+
only SQL. They do not need to become JSON REST consumers.
|
|
3033
|
+
|
|
3034
|
+
Put an allocation retry around the complete transaction. For example, the
|
|
3035
|
+
application-owned `allocateCode(trx)` below computes a candidate using only SQL;
|
|
3036
|
+
`isCodeAllocationConflict` identifies its exact unique constraint from the
|
|
3037
|
+
underlying driver error. Other uniqueness failures must escape the loop.
|
|
3038
|
+
|
|
3039
|
+
```js
|
|
3040
|
+
import { findDuplicateEntryError } from '@jskit-ai/database-runtime/shared'
|
|
3041
|
+
|
|
3042
|
+
async function createWithAllocatedCode(payload, context) {
|
|
3043
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
3044
|
+
try {
|
|
3045
|
+
return await repository.withTransaction(async trx => {
|
|
3046
|
+
const code = await allocateCode(trx)
|
|
3047
|
+
return repository.createDocument({ ...payload, code }, { trx, context })
|
|
3048
|
+
})
|
|
3049
|
+
} catch (error) {
|
|
3050
|
+
const duplicate = findDuplicateEntryError(error)
|
|
3051
|
+
if (attempt === 3 || error?.transactionOutcome !== 'rolledBack' ||
|
|
3052
|
+
!duplicate || !isCodeAllocationConflict(duplicate)) {
|
|
3053
|
+
throw error
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
```
|
|
3059
|
+
|
|
3060
|
+
Each attempt gets a new transaction and recomputes the candidate. Do not catch
|
|
3061
|
+
and retry `standard()` inside a failed CRUD lifecycle transaction, or restart a
|
|
3062
|
+
transaction supplied by another owner. Keep external effects outside this
|
|
3063
|
+
retrying unit unless the application can safely repeat or reconcile them.
|
|
3064
|
+
|
|
3065
|
+
Application-owned uploads need the same outcome check before compensation.
|
|
3066
|
+
This example creates one new object used only by this operation. `uploads.save`
|
|
3067
|
+
and `uploads.remove` are the application's storage integration; the caller
|
|
3068
|
+
supplies its own mutable `diagnostics = { cleanupErrors: [] }`:
|
|
3069
|
+
|
|
3070
|
+
```js
|
|
3071
|
+
async function createWithUpload(payload, file, context, diagnostics) {
|
|
3072
|
+
const uploaded = await uploads.save(file)
|
|
3073
|
+
diagnostics.uploaded = uploaded
|
|
3074
|
+
try {
|
|
3075
|
+
return await repository.withTransaction(trx =>
|
|
3076
|
+
repository.createDocument(
|
|
3077
|
+
{ ...payload, fileUrl: uploaded.url },
|
|
3078
|
+
{ trx, context }
|
|
3079
|
+
)
|
|
3080
|
+
)
|
|
3081
|
+
} catch (error) {
|
|
3082
|
+
if (error?.transactionOutcome === 'rolledBack') {
|
|
3083
|
+
try {
|
|
3084
|
+
await uploads.remove(uploaded.key)
|
|
3085
|
+
} catch (cleanupError) {
|
|
3086
|
+
diagnostics.cleanupErrors.push({ key: uploaded.key, error: cleanupError })
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
throw error
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
```
|
|
3093
|
+
|
|
3094
|
+
The original write error and its outcome still reach the caller; secondary
|
|
3095
|
+
cleanup failures and the uploaded object's identity remain in the application's
|
|
3096
|
+
diagnostics. Other outcomes retain the object for application reconciliation.
|
|
3097
|
+
In particular, deleting it after a committed or unknown failure could break a
|
|
3098
|
+
stored record. Library-managed file
|
|
3099
|
+
uploads already register rollback cleanup; do not add this second cleanup path
|
|
3100
|
+
for those files. See [file ownership](26-file-uploads.md#ownership-after-commit).
|
|
3101
|
+
|
|
3102
|
+
JSKIT copies the outer context for each library operation; hooks inside that
|
|
3103
|
+
operation share the resulting object. Its `visibilityContext` and `scopeValues`
|
|
3104
|
+
maps are copied, while other nested application objects retain their references.
|
|
3105
|
+
Use those maps for user/workspace scoping. A nested operation needs its own
|
|
3106
|
+
working context and explicit transaction participation. Remove hooks that repair
|
|
3107
|
+
dates or query results already normalized by v2; keep application-specific
|
|
3108
|
+
serializers and policies.
|
|
3109
|
+
|
|
3110
|
+
Custom HTTP adapters must translate the library's typed error codes and resource
|
|
3111
|
+
subtypes into status codes, in addition to preserving `transactionOutcome`.
|
|
3112
|
+
Library errors need not carry an HTTP `statusCode`. Validation is 422, missing
|
|
3113
|
+
resources 404, forbidden resources 403, and conflicts 409. Client retry policies
|
|
3114
|
+
must inspect the outcome: `pending`, `committed` and `unknown` are not permission
|
|
3115
|
+
to replay a write, even if another copied error field resembles a retryable CSRF
|
|
3116
|
+
failure. An application-defined retry callback owns its own policy.
|
|
3117
|
+
|
|
3118
|
+
The JSKIT migration uses Node 24 for verification and requires Node 24 or newer
|
|
3119
|
+
for packages that load v2. Upgrade the framework packages and lockfile together,
|
|
3120
|
+
rebuild the catalog and distributed agent docs from authored source patterns, and check
|
|
3121
|
+
custom app repositories and hooks separately. Passing JSKIT tests does not port
|
|
3122
|
+
Vibe64, its seeds or other applications. Those remain separate migration steps.
|
|
3123
|
+
|
|
3124
|
+
The local dispatcher preserves null/undefined throws; see [transaction outcomes](20-transaction-outcomes.md).
|
|
3125
|
+
[Positioning](31-positioning.md) now coordinates writes through database locks;
|
|
3126
|
+
its chapter describes tested concurrency and remaining storage requirements.
|
|
3127
|
+
|
|
3128
|
+
### Positioning coordination and retired placeholders
|
|
3129
|
+
|
|
3130
|
+
Existing resource calls using `beforeId`, `null` for append and `'FIRST'` continue
|
|
3131
|
+
to work. PostgreSQL and MySQL writers serialize through a transaction-owned
|
|
3132
|
+
coordinator row, including empty groups and moves between groups. SQLite can
|
|
3133
|
+
report busy conflicts; stale transaction snapshots can reject. Applications must
|
|
3134
|
+
retry a failed move in a fresh operation/transaction when appropriate.
|
|
3135
|
+
|
|
3136
|
+
Installation creates `json_rest_api_positioning_locks` if absent. Provision the
|
|
3137
|
+
table before deployment when the runtime account lacks DDL permission, and keep
|
|
3138
|
+
all cooperating writers on the same table. The [positioning guide](31-positioning.md)
|
|
3139
|
+
contains its schema and collation requirements.
|
|
3140
|
+
|
|
3141
|
+
- Changing a group without `beforeId` now places the record at the configured
|
|
3142
|
+
beginning/end of its destination group instead of retaining a potentially
|
|
3143
|
+
duplicate key. Same-group updates preserve their position.
|
|
3144
|
+
- PUT creates receive positions, and PUT replacements preserve or recalculate
|
|
3145
|
+
their managed position. Other persisted fields still follow complete PUT
|
|
3146
|
+
replacement rules.
|
|
3147
|
+
- Missing/out-of-group targets append; a self-targeted move stays in place.
|
|
3148
|
+
Explicit targets now use normal GET permissions and visibility.
|
|
3149
|
+
- Default query ordering uses the supported query hook. Bytewise collation on
|
|
3150
|
+
position columns is required for ordinary sorting and cursor pagination;
|
|
3151
|
+
the allocator's binary comparisons do not change those query semantics.
|
|
3152
|
+
- Remove `rebalanceThreshold`; it never caused automatic rebalancing and now
|
|
3153
|
+
rejects. The no-op `api.positioning.reorder()` method has been removed. Use
|
|
3154
|
+
ordinary PATCH calls or atomic bulk PATCH for actual changes.
|
|
3155
|
+
- Existing keys must be valid fractional keys. Key exhaustion rejects before
|
|
3156
|
+
exceeding declared storage length; migrate/rebalance the data deliberately.
|
|
3157
|
+
- Deep imports of `calculatePosition()` now take `(previousPosition, nextPosition)`
|
|
3158
|
+
string bounds. The former array/ID arguments are no longer supported.
|
|
3159
|
+
|
|
3160
|
+
## Native query builders and explicit custom-filter translation
|
|
3161
|
+
|
|
3162
|
+
Both storage modes now give query hooks a native Knex builder. Canonical storage
|
|
3163
|
+
no longer intercepts Knex methods to guess logical field/table names. This removes
|
|
3164
|
+
the old query proxy, its alias/descriptor traversal and its partial emulation of
|
|
3165
|
+
Knex methods. Keep the supplied scoped builder (or clone it) and use explicit
|
|
3166
|
+
column/value translation. Native Knex joins, callbacks, aliases, aggregates, raw
|
|
3167
|
+
bindings and cloning retain Knex's own behavior.
|
|
3168
|
+
|
|
3169
|
+
Custom search filters now receive a third argument:
|
|
3170
|
+
|
|
3171
|
+
```js
|
|
3172
|
+
customRank: {
|
|
3173
|
+
type: 'number',
|
|
3174
|
+
applyFilter(query, input, { column, value }) {
|
|
3175
|
+
query.where(column('rank'), '>=', value('rank', input))
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
```
|
|
3179
|
+
|
|
3180
|
+
Replace `query.where('rank', '>=', input)` with this form when `rank` is a logical
|
|
3181
|
+
resource field. `column(field)` returns a qualified physical column, including
|
|
3182
|
+
canonical slots or an explicitly mapped ordinary column. `value(field, input)`
|
|
3183
|
+
applies that field's storage conversion/custom serializer. The validated public
|
|
3184
|
+
filter input remains the second argument; it is not automatically reinterpreted
|
|
3185
|
+
as another field's stored value. The details object also supplies `context` and
|
|
3186
|
+
`scopeName`. The callback's `this` remains the same builder as its first argument.
|
|
3187
|
+
|
|
3188
|
+
For a declared joined filter such as `actualField: 'groups.code'`, use
|
|
3189
|
+
`column('groups.code')` and `value('groups.code', input)`. These helpers resolve
|
|
3190
|
+
the declared path's prepared alias; they do not create arbitrary additional
|
|
3191
|
+
joins. Existing related-query permission checks still run. Custom SQL continues
|
|
3192
|
+
to own any extra relationships or visibility semantics it introduces. Callbacks
|
|
3193
|
+
run synchronously inside the current predicate group and must mutate that
|
|
3194
|
+
builder; returned builders/promises are not awaited or selected.
|
|
3195
|
+
|
|
3196
|
+
For `knexQueryFiltering` hooks, the existing
|
|
3197
|
+
`createStorageAdapterUtilities({ context })` helper supplies
|
|
3198
|
+
`translateColumn(scopeName, field)` and `translateFilterValue(scopeName, field,
|
|
3199
|
+
input)`. Keep using `context.knexQuery.tableName` as the active SQL alias and
|
|
3200
|
+
`context.knexQuery.storageAdapter` for resource storage operations. The legacy
|
|
3201
|
+
`context.knexQuery.adapter` now refers to that same storage adapter in both
|
|
3202
|
+
modes; it is no longer a query-method proxy. Removed private proxy state includes
|
|
3203
|
+
`__adapter__`, its related-descriptor/alias maps, and
|
|
3204
|
+
`context.resourceToTableName`. Importing the removed internal
|
|
3205
|
+
`anyapi-query-adapter.js` module is no longer supported.
|
|
3206
|
+
|
|
3207
|
+
A filtering hook can translate a local field without importing another helper:
|
|
3208
|
+
|
|
3209
|
+
```js
|
|
3210
|
+
handler({ context }) {
|
|
3211
|
+
const { query, tableName, storageAdapter } = context.knexQuery
|
|
3212
|
+
const rankColumn = `${tableName}.${storageAdapter.translateColumn('rank')}`
|
|
3213
|
+
query.where(rankColumn, '>=', storageAdapter.translateFilterValue('rank', 10))
|
|
3214
|
+
}
|
|
3215
|
+
```
|
|
3216
|
+
|
|
3217
|
+
Apply this predicate to the supplied query, including count invocations. Keep
|
|
3218
|
+
using declared cross-resource search paths where possible; writing a native join
|
|
3219
|
+
does not automatically add the target resource's permission or visibility checks.
|
|
3220
|
+
|
|
3221
|
+
The storage adapter's `buildBaseQuery({ transaction, tableAlias })` constructs a
|
|
3222
|
+
native builder with the optional alias and retains canonical tenant/resource
|
|
3223
|
+
predicates qualified by that alias. It borrows the database handle and does not
|
|
3224
|
+
complete transactions. Translate every logical identifier used in custom SQL;
|
|
3225
|
+
raw SQL text and binding strings are not rewritten. Consumer repositories have
|
|
3226
|
+
not been migrated by this library-only change.
|
|
3227
|
+
|
|
3228
|
+
## Storage helpers consume prepared write attributes
|
|
3229
|
+
|
|
3230
|
+
Ordinary PUT/PATCH no longer re-extract belongs-to values from the request's
|
|
3231
|
+
relationship object. Core relationship processing and setters already prepare
|
|
3232
|
+
`context.inputRecord.data.attributes` before calling storage, as they do for
|
|
3233
|
+
POST and canonical writes. Resource API calls need no migration for this change.
|
|
3234
|
+
Custom storage implementations should consume those prepared attributes before
|
|
3235
|
+
applying physical storage mapping, preserving setter results.
|
|
3236
|
+
|
|
3237
|
+
A custom `dataPost` helper must return a nonempty string, a finite number, or a
|
|
3238
|
+
bigint ID. The lifecycle converts bigint IDs to strings without losing precision.
|
|
3239
|
+
Invalid helper results reject before `afterDataCall` hooks and roll back the
|
|
3240
|
+
write, including with `returning: 'none'` or `'minimal'`. Resource `normalizeId`
|
|
3241
|
+
is not invoked again on the helper's result.
|
|
3242
|
+
|
|
3243
|
+
The unexported internal `plugins/core/lib/writing/knex-json-api-transformers-writing.js`
|
|
3244
|
+
module and its `processBelongsToRelationships` helper have been removed. If you
|
|
3245
|
+
imported that file directly, remove the second relationship conversion and use
|
|
3246
|
+
the prepared attributes supplied to your storage callback. Public relationship
|
|
3247
|
+
validation and authorization remain in the resource lifecycle.
|
|
3248
|
+
|
|
3249
|
+
## Pass explicit field maps to foreign-key inspection
|
|
3250
|
+
|
|
3251
|
+
The internal `getForeignKeyFields(fields, relationships)` helper accepts a field
|
|
3252
|
+
map. If an extension passes a schema instance, change the call to
|
|
3253
|
+
`getForeignKeyFields(schema.structure, relationships)`. Library callers already
|
|
3254
|
+
pass `schemaInfo.schemaStructure` or an enrichment field map.
|
|
3255
|
+
|
|
3256
|
+
The helper no longer guesses whether a property named `structure` is a wrapper.
|
|
3257
|
+
That guess misread a legitimate attribute named `structure`, hid relationship
|
|
3258
|
+
backing fields and could bypass the prohibition on identity serializers. Public
|
|
3259
|
+
resource declarations can continue to use `structure` as an attribute name.
|
|
3260
|
+
|
|
3261
|
+
## Table helper schemas use an explicit structure wrapper
|
|
3262
|
+
|
|
3263
|
+
Direct calls to `createKnexTable`, `addKnexFields`, `alterKnexFields`,
|
|
3264
|
+
`generateKnexMigration` and `generateKnexMigrationDiff` now require a schema
|
|
3265
|
+
object whose `structure` property contains field definitions. A
|
|
3266
|
+
`json-rest-schema` schema instance already supplies that property and remains
|
|
3267
|
+
accepted. Field definitions must be objects.
|
|
3268
|
+
|
|
3269
|
+
Change bare field maps at the schema argument:
|
|
3270
|
+
|
|
3271
|
+
```js
|
|
3272
|
+
// Before
|
|
3273
|
+
await alterKnexFields(knex, 'books', { title: { type: 'string' } })
|
|
3274
|
+
|
|
3275
|
+
// After
|
|
3276
|
+
await alterKnexFields(knex, 'books', {
|
|
3277
|
+
structure: { title: { type: 'string' } }
|
|
3278
|
+
})
|
|
3279
|
+
```
|
|
3280
|
+
|
|
3281
|
+
For helpers that accept table metadata, keep `storage`, `indexes`, `foreignKeys`
|
|
3282
|
+
and `checkConstraints` beside `structure`. Fields with those names belong inside
|
|
3283
|
+
`structure`, like any other field. Add/alter helpers still reject top-level table
|
|
3284
|
+
constraints they cannot apply; use a migration diff for those changes.
|
|
3285
|
+
|
|
3286
|
+
Public resource calls such as `books.addKnexFields({ fields })`,
|
|
3287
|
+
`books.alterKnexFields({ fields })` and `books.createKnexTable()` keep their current
|
|
3288
|
+
arguments. The plugin supplies the explicit helper schema internally.
|
|
3289
|
+
|
|
3290
|
+
Bare maps are rejected before SQL or migration output is produced. This removes
|
|
3291
|
+
the ambiguity that could interpret a field named `structure` as a wrapper and
|
|
3292
|
+
generate columns from its definition's `type` and `nullable` properties.
|
|
3293
|
+
|
|
3294
|
+
## Read helpers use compiled field maps
|
|
3295
|
+
|
|
3296
|
+
Internal response, include and field-selection helpers now read
|
|
3297
|
+
`scope.vars.schemaInfo.schemaStructure` directly. Extensions supplying synthetic
|
|
3298
|
+
resource metadata must provide that field map alongside `schemaRelationships`;
|
|
3299
|
+
`schemaInstance` is the validation object, not an alternate field-map argument.
|
|
3300
|
+
Normal registered resources already provide both compiled values.
|
|
3301
|
+
|
|
3302
|
+
The internal `getSchemaStructure` export from `knex-constants.js` is removed.
|
|
3303
|
+
Use the resource's compiled `schemaStructure`, or explicitly access `.structure`
|
|
3304
|
+
on a schema instance you own. No wrapper guessing or compatibility adapter is
|
|
3305
|
+
needed.
|
|
3306
|
+
|
|
3307
|
+
|
|
3308
|
+
## Schema compilation snapshots declarations
|
|
3309
|
+
|
|
3310
|
+
Resource schema compilation now copies declaration data on entry and again when
|
|
3311
|
+
publishing its result. Changing an original field's nested `storage`, validation,
|
|
3312
|
+
relationship or dependency declaration after registration no longer changes the
|
|
3313
|
+
currently compiled schema. Enrichment hooks may mutate or replace their supplied
|
|
3314
|
+
fields during compilation; retaining that object and editing it after the hook
|
|
3315
|
+
pipeline finishes no longer changes published metadata.
|
|
3316
|
+
|
|
3317
|
+
Declare changes before registration, or use the supported schema operation such
|
|
3318
|
+
as canonical `addKnexFields()` while application operations are stopped. Do not
|
|
3319
|
+
use mutations of `resource.vars.schemaInfo` as a configuration API. Each explicit
|
|
3320
|
+
compilation uses its supplied input; this does not add automatic invalidation,
|
|
3321
|
+
concurrent reconfiguration, or a general resource-customization API.
|
|
3322
|
+
|
|
3323
|
+
Callbacks retain their identity and may still access application state through
|
|
3324
|
+
closures. Plain objects, arrays, dates, regular expressions and nested schema
|
|
3325
|
+
instances are copied; nested schemas retain their registered type/validator
|
|
3326
|
+
handlers and operation definitions. Other class instances remain application-owned
|
|
3327
|
+
references. The library does not freeze caller-owned objects or attempt to clone
|
|
3328
|
+
external services. This boundary applies to compiled declarations; it does not
|
|
3329
|
+
turn all runtime options, hook variables or application state into immutable data.
|
|
3330
|
+
|
|
3331
|
+
|
|
3332
|
+
Canonical registration also keeps an owned copy of declarations for later field
|
|
3333
|
+
additions. Changing the original registration object, or a previous
|
|
3334
|
+
`addKnexFields({ fields })` input, does not silently change existing fields during
|
|
3335
|
+
a subsequent addition. Pass new declarations to the supported operation instead.
|
|
3336
|
+
|
|
3337
|
+
Initial `sortableFields` and `defaultSort` arrays are copied into resource
|
|
3338
|
+
variables. Editing the original arrays no longer changes query behavior. An
|
|
3339
|
+
explicit assignment such as `resource.vars.defaultSort = ['-name']` still affects
|
|
3340
|
+
subsequent requests; this is a runtime variable override, not a schema change.
|
|
3341
|
+
It does not recompile fields or change their query eligibility.
|
|
3342
|
+
|
|
3343
|
+
|
|
3344
|
+
## Plugin registries require explicit entries
|
|
3345
|
+
|
|
3346
|
+
Named row policies, autofilter resolvers and autofilter presets must be registered
|
|
3347
|
+
as own entries of their configuration maps. Inherited names such as `constructor`
|
|
3348
|
+
or `toString` no longer count as registered entries; resource registration rejects
|
|
3349
|
+
them as unknown. Autofilters also require an actual declared schema field rather
|
|
3350
|
+
than accepting inherited object properties as fields.
|
|
3351
|
+
|
|
3352
|
+
Explicitly registered policy, resolver and preset names remain valid, including
|
|
3353
|
+
prototype-like names. Use computed properties or `Object.fromEntries()` if a map
|
|
3354
|
+
key is `__proto__`, so it is an own entry rather than JavaScript object-literal
|
|
3355
|
+
prototype syntax. Explicit resolver names are also retained in plugin
|
|
3356
|
+
introspection. Policy and resolver callbacks still run per request; their return
|
|
3357
|
+
values and authorization results are not cached by this change.
|
|
3358
|
+
|
|
3359
|
+
|
|
3360
|
+
## When configuration changes take effect
|
|
3361
|
+
|
|
3362
|
+
| Configuration | Effective boundary | How to change it |
|
|
3363
|
+
| --- | --- | --- |
|
|
3364
|
+
| Named policy/resolver maps and autofilter presets | Plugin installation captures entries and preset declarations; resource registration selects them | Supply the intended registry before installing the plugin. Callback closures may still read current application state. |
|
|
3365
|
+
| Fields, validation, search, relationships, projections and computed declarations | Awaited resource registration compiles and publishes an owned snapshot after enrichment | Declare them before registration. Finish registering forward-referenced resources before serving operations. |
|
|
3366
|
+
| Enrichment hook fields | Mutable during the corresponding compilation stage | Edit `context.fields`; `context.originalFields` remains a separate view of that stage's original nested declaration data. Install enrichment hooks before registering resources. |
|
|
3367
|
+
| Runtime hook/helper/variable customization | A subsequent method invocation observes the current customization | Use `api.customize()` for runtime hooks/helpers/global variables. Resource variable overrides remain explicit; they do not recompile schema facts. |
|
|
3368
|
+
| Response defaults, query default/max limits, include depth and allowed sort fields | Read for each operation; method parameters override the defaults where that method supports an override | Change runtime variables deliberately. Include-depth and allowed-sort changes replace the retained request contracts; format and page-default changes do not require a new schema or adapter. |
|
|
3369
|
+
| Canonical field additions | Successful metadata commit followed by publication of the new compiled resource | Use `addKnexFields()` while application operations are stopped. Existing source declarations and previous addition inputs are owned copies. Failed additions retain the prior published resource. |
|
|
3370
|
+
| Descriptor-only mapping refresh | Explicit refresh after the committed mapping migration | Follow the earlier canonical migration instructions; registry eviction alone does not republish an active resource. |
|
|
3371
|
+
|
|
3372
|
+
`api.customize()` is not a schema replacement API. Arbitrary writes to
|
|
3373
|
+
`scopeOptions`, compiled `schemaInfo`, descriptors or canonical maps are not
|
|
3374
|
+
supported configuration operations. Structural changes outside the supported
|
|
3375
|
+
addition path need updated source declarations, any required data/schema migration,
|
|
3376
|
+
and resource initialization. Concurrent structural reconfiguration is unsupported.
|
|
3377
|
+
There is no automatic fingerprinting of mutable metadata or cache of caller
|
|
3378
|
+
permissions, resolver values or query results.
|
|
3379
|
+
|
|
3380
|
+
|
|
3381
|
+
## Compiled output definition indexes
|
|
3382
|
+
|
|
3383
|
+
Response normalization now reads `resource.vars.schemaInfo.outputFields` and
|
|
3384
|
+
`outputRelationships`. These are compiled lookup indexes over the existing field,
|
|
3385
|
+
computed, projection and relationship definitions. They are refreshed with the
|
|
3386
|
+
compiled resource, including after supported canonical field additions.
|
|
3387
|
+
|
|
3388
|
+
Ordinary resource calls need no changes. Code that calls the deep
|
|
3389
|
+
`normalizeRecordAttributes()` helper with handcrafted scope metadata must supply
|
|
3390
|
+
these indexes; the helper no longer rebuilds them from `schemaStructure`,
|
|
3391
|
+
`computed`, `queryFields` and `schemaRelationships` for every record. Prefer passing
|
|
3392
|
+
real `api.resources`. For a single attributes object with an explicit definition
|
|
3393
|
+
map, `normalizeAttributes(attributes, definitions)` remains available unchanged.
|
|
3394
|
+
Direct mutation of compiled indexes is not a configuration API.
|
|
3395
|
+
|
|
3396
|
+
## Compiled foreign-key membership
|
|
3397
|
+
|
|
3398
|
+
Code constructing synthetic `schemaInfo` for deep query/conversion helpers must
|
|
3399
|
+
provide `foreignKeyFields`, a `Set` of logical backing-field names for ordinary
|
|
3400
|
+
and polymorphic relationships. Obtain this metadata from the registered resource
|
|
3401
|
+
where possible. Compilation publishes the set with the resource; do not mutate
|
|
3402
|
+
it or add resource IDs to it. Make a local copy when extending identity fields.
|
|
3403
|
+
Supported canonical field additions publish a replacement set automatically.
|
|
3404
|
+
The public resource call syntax is unchanged by this optimization.
|
|
3405
|
+
|
|
3406
|
+
Conversion no longer reads relationship declarations to reconstruct polymorphic
|
|
3407
|
+
backing-field membership. Supply both type and ID backing fields in the compiled
|
|
3408
|
+
set when constructing synthetic metadata. The conversion error boundary wraps
|
|
3409
|
+
failures while reading that set, retaining typed errors and original causes.
|
|
3410
|
+
|
|
3411
|
+
## Relationship aliases must be unique
|
|
3412
|
+
|
|
3413
|
+
Each relationship name must have exactly one declaration. Two `belongsTo` fields
|
|
3414
|
+
cannot share an `as` name, and a field's `as` cannot also appear in the resource's
|
|
3415
|
+
`relationships` map. Previously these configurations were accepted but different
|
|
3416
|
+
operations could choose different definitions or cardinalities.
|
|
3417
|
+
|
|
3418
|
+
Give distinct relationships distinct names, for example `authorId` with
|
|
3419
|
+
`as: 'author'` and `reviewerId` with `as: 'reviewer'`. Declare a belongs-to
|
|
3420
|
+
relationship on its backing field; remove a duplicate declaration from the
|
|
3421
|
+
`relationships` map. Update payload and include names if you rename an alias.
|
|
3422
|
+
The compiler checks enriched declarations too. A rejected canonical field
|
|
3423
|
+
addition retains the previously published resource and its stored data.
|
|
3424
|
+
|
|
3425
|
+
## Derived attributes cannot declare relationships
|
|
3426
|
+
|
|
3427
|
+
Computed fields and SQL query projections cannot declare `belongsTo` or
|
|
3428
|
+
`belongsToPolymorphic`. These fields produce attribute values and do not provide
|
|
3429
|
+
the stored backing fields required by relationship operations. Previously the
|
|
3430
|
+
compiler accepted these options and could produce inconsistent relationship
|
|
3431
|
+
lookup and output normalization. The same rejection applies after enrichment.
|
|
3432
|
+
|
|
3433
|
+
Remove relationship options from derived attributes. Declare an ordinary
|
|
3434
|
+
relationship on its stored foreign-key field using `belongsTo` and `as`; declare
|
|
3435
|
+
a polymorphic relationship in the resource's `relationships` map, identifying its
|
|
3436
|
+
stored type and ID fields. Give a derived display value its own attribute name.
|
|
3437
|
+
A rejected canonical addition keeps the previously published schema intact.
|
|
3438
|
+
|
|
3439
|
+
## Compiled relationship lookup and polymorphic declaration location
|
|
3440
|
+
|
|
3441
|
+
`findRelationshipDefinition()` now reads the registered resource's
|
|
3442
|
+
`schemaInfo.outputRelationships` index. Deep-helper callers constructing synthetic
|
|
3443
|
+
metadata must supply that index, including both ordinary belongs-to aliases and
|
|
3444
|
+
declared relationships. Entries reference the existing definitions. Prefer the
|
|
3445
|
+
compiled metadata on `api.resources` and do not mutate its lookup index. The
|
|
3446
|
+
helper returns null for absent names, including inherited object names.
|
|
3447
|
+
|
|
3448
|
+
Declare `belongsToPolymorphic` in the resource's `relationships` map, with its
|
|
3449
|
+
`typeField` and `idField` pointing to stored schema fields. Placing it inside an
|
|
3450
|
+
individual stored field is now rejected, including after schema enrichment and
|
|
3451
|
+
when adding canonical fields. Previously that placement was accepted but lookup
|
|
3452
|
+
could not find the relationship and its backing fields were not recognized.
|
|
3453
|
+
Move the relationship definition into the map; keep its backing fields in
|
|
3454
|
+
`schema`. No stored data conversion is needed merely to move the declaration.
|
|
3455
|
+
|
|
3456
|
+
## Request contracts reuse compiled relationship metadata
|
|
3457
|
+
|
|
3458
|
+
Deep callers of `getRequestContracts()` constructing synthetic `schemaInfo` must
|
|
3459
|
+
provide `foreignKeyFields` and `outputRelationships` along with their stored and
|
|
3460
|
+
search schema metadata. Normal registered resources already provide these facts.
|
|
3461
|
+
The foreign-key set excludes relationship backing fields from writable attribute
|
|
3462
|
+
contracts; the relationship index supplies linkage cardinalities and target types.
|
|
3463
|
+
There is no fallback that reconstructs these indexes from raw declarations.
|
|
3464
|
+
|
|
3465
|
+
For an ordinary belongs-to relationship, its `belongsTo` value determines the
|
|
3466
|
+
allowed resource type, consistent with relationship operations. An additional
|
|
3467
|
+
`target` property does not override it. Use `target` for has-one/has-many and
|
|
3468
|
+
many-to-many declarations. Existing public call syntax and the one-retained-variant
|
|
3469
|
+
request-contract cache are unchanged by this refactor.
|
|
3470
|
+
|
|
3471
|
+
## File upload rules follow compiled resource configuration
|
|
3472
|
+
|
|
3473
|
+
File handling now reads the compiled stored fields, including MIME/size rules
|
|
3474
|
+
produced by `schema:enrich`. Mutating the original declaration after registration
|
|
3475
|
+
no longer changes those rules. Installing the file plugin after resource
|
|
3476
|
+
registration also discovers existing compiled file fields. Supported canonical
|
|
3477
|
+
field additions refresh the file-field list when the compiled resource changes;
|
|
3478
|
+
a rejected addition keeps the previous rules, including an empty file-field list.
|
|
3479
|
+
|
|
3480
|
+
File storage backends are external handles. A file field's `storage` object with
|
|
3481
|
+
an `upload()` method retains its identity and method receiver during declaration
|
|
3482
|
+
snapshotting, so backend state and intentional method replacement continue to
|
|
3483
|
+
work. MIME and size declarations are still owned snapshots. Ordinary column
|
|
3484
|
+
mapping objects without `upload()` remain copied declarations. Directly mutating
|
|
3485
|
+
published compiled metadata is unsupported.
|
|
3486
|
+
|
|
3487
|
+
## New compiler hook: `schema:compiled`
|
|
3488
|
+
|
|
3489
|
+
Plugins can derive resource metadata in `schema:compiled`. It receives
|
|
3490
|
+
`context.scopeName`, `context.scopeOptions` and `context.schemaInfo` after core
|
|
3491
|
+
field, relationship, storage, query and dependency compilation. Read the compiled
|
|
3492
|
+
facts and attach plugin-owned metadata under a distinct key on `schemaInfo`.
|
|
3493
|
+
Change stored, search, computed or projected declarations in their existing
|
|
3494
|
+
enrichment hooks; core compiled facts are read-only at this stage.
|
|
3495
|
+
|
|
3496
|
+
The candidate is published only after all handlers succeed, using an owned
|
|
3497
|
+
snapshot. Retained candidate objects cannot mutate the published metadata.
|
|
3498
|
+
Throwing rejects registration or a canonical field-addition candidate before it
|
|
3499
|
+
replaces the resource. This hook runs on compilation, not on ordinary requests
|
|
3500
|
+
or a descriptor-only refresh.
|
|
3501
|
+
|
|
3502
|
+
Autofilter uses this hook to validate and derive its configuration on every
|
|
3503
|
+
compilation. Resolver metadata and write stamping follow enriched relationship
|
|
3504
|
+
aliases after canonical additions; removing a required autofilter field rejects
|
|
3505
|
+
the candidate. `api.autofilter.getScopeConfig(name)` is unchanged. Deep readers of
|
|
3506
|
+
`resource.vars.autofilter` must move to `resource.vars.schemaInfo.autofilter` or
|
|
3507
|
+
prefer the public inspection helper. The root registry summary remains available
|
|
3508
|
+
through `api.autofilter.getConfig()`. No forwarding metadata copy is kept.
|
|
3509
|
+
|
|
3510
|
+
## Include configuration is validated before publication
|
|
3511
|
+
|
|
3512
|
+
Include configuration is now checked from the compiled relationship definitions,
|
|
3513
|
+
including enrichment, before a resource is published or a canonical field
|
|
3514
|
+
addition commits. An explicit resource `queryMaxLimit` takes precedence over the
|
|
3515
|
+
inherited value for this check. Previously the registration stages could reject
|
|
3516
|
+
valid higher overrides or miss enriched limits above a lower resource maximum.
|
|
3517
|
+
|
|
3518
|
+
`include.limit` accepts a non-negative integer, `null` or `false`; omission keeps
|
|
3519
|
+
the existing default. Zero is valid, and `null`/`false` retain their existing
|
|
3520
|
+
unbounded-include meaning. Negative, fractional, NaN and nonnumeric values now
|
|
3521
|
+
fail compilation. `include.orderBy` retains its array form and validates entry
|
|
3522
|
+
syntax during compilation. Target-field resolution and database window-function
|
|
3523
|
+
support remain query-time checks so forward resource references still work.
|
|
3524
|
+
Query-time target caps and selection behavior are unchanged.
|
|
3525
|
+
|
|
3526
|
+
The deep `validateIncludeConfigurations` helper now has a named export and takes
|
|
3527
|
+
the `schema:compiled` candidate context (`scopeName`, `scopeOptions`, `schemaInfo`),
|
|
3528
|
+
not the former scope-added context. Normal plugin users need no call-site change.
|
|
3529
|
+
|
|
3530
|
+
## Enhancing an existing logger
|
|
3531
|
+
|
|
3532
|
+
The internal `enhanceLogger(logger, options)` utility now safely replaces methods
|
|
3533
|
+
on the supplied logger. Its wrappers capture each original writer and receiver
|
|
3534
|
+
at enhancement time; previously they looked up the replaced method again and
|
|
3535
|
+
could recurse until the stack overflowed. The utility still returns the same
|
|
3536
|
+
logger, preserves writer return values and propagates writer failures. Existing
|
|
3537
|
+
`createEnhancedLogger` wrappers likewise retain the writers captured at creation.
|
|
3538
|
+
Install the desired base writers before creating an enhanced logger.
|
|
3539
|
+
|
|
3540
|
+
## Error causes in diagnostic output
|
|
3541
|
+
|
|
3542
|
+
Operation failure reports now include these stable fields:
|
|
3543
|
+
|
|
3544
|
+
| Field | Meaning |
|
|
3545
|
+
| --- | --- |
|
|
3546
|
+
| `method` | Resource method, HTTP verb, or named setup/socket operation; null when unavailable |
|
|
3547
|
+
| `scopeName` | Resolved resource name; null before resource resolution or for server-wide operations |
|
|
3548
|
+
| `phase` | Reporting boundary, such as `writeFailure`, `httpError`, `include`, `admission`, or `redisShutdown` |
|
|
3549
|
+
| `backend` | Knex client name, `socketio`, or `redis`; null when unavailable |
|
|
3550
|
+
| `transactionOutcome` | Existing transaction state at reporting time: `none`, `pending`, `committed`, `rolledBack`, or `unknown` |
|
|
3551
|
+
|
|
3552
|
+
`phase` describes where an error is reported. It does not identify every hook
|
|
3553
|
+
that ran before the failure. Existing nested getter, setter, include and
|
|
3554
|
+
relationship-metadata errors retain their more specific context and causes.
|
|
3555
|
+
Cleanup warnings may report `pending` even when the operation later commits.
|
|
3556
|
+
Socket notification permission checks run outside a transaction and report
|
|
3557
|
+
`none`; subscription-matching reads inside a write report its borrowed transaction
|
|
3558
|
+
state. Setup/debug traces are informational and need not contain this envelope.
|
|
3559
|
+
|
|
3560
|
+
Formatted diagnostics now include native `Error.cause` and `AggregateError.errors`
|
|
3561
|
+
properties, which JavaScript normally makes non-enumerable. Nested errors retain
|
|
3562
|
+
their names, messages and selected stacks, including errors inside diagnostic
|
|
3563
|
+
`details`. Primitive causes are retained. Existing depth and cycle limits apply
|
|
3564
|
+
to these nested values, and the original errors are not modified. Consumers that
|
|
3565
|
+
parse diagnostic objects should allow these additional fields. This does not
|
|
3566
|
+
change HTTP error responses or the errors thrown by resource methods.
|
|
3567
|
+
|
|
3568
|
+
## Shared filter traces omit payloads
|
|
3569
|
+
|
|
3570
|
+
The shared basic-filter trace messages no longer print filter values, entire
|
|
3571
|
+
filter objects or field definitions. They retain scope, table, field and operator
|
|
3572
|
+
information. Filtering behavior and SQL bindings are unchanged. This is a
|
|
3573
|
+
specific trace-output change. See [field-aware diagnostics](#field-aware-write-diagnostics)
|
|
3574
|
+
for structured redaction and its limits.
|
|
3575
|
+
|
|
3576
|
+
|
|
3577
|
+
Ordinary storage operation messages also omit query parameters, POST bodies and
|
|
3578
|
+
PATCH attributes. Upload-completion messages omit stored URLs, and Socket.IO
|
|
3579
|
+
subscription messages omit filters. These changes affect diagnostics only;
|
|
3580
|
+
records, upload results and subscription acknowledgements retain their values.
|
|
3581
|
+
|
|
3582
|
+
|
|
3583
|
+
Include-loader diagnostics now report ID counts rather than identifier arrays.
|
|
3584
|
+
They also omit complete relationship/include definitions and selection lists
|
|
3585
|
+
from the affected load messages. Query construction and returned includes are
|
|
3586
|
+
unchanged.
|
|
3587
|
+
|
|
3588
|
+
|
|
3589
|
+
## Connector error logs use route templates
|
|
3590
|
+
|
|
3591
|
+
Express and Fastify request-error diagnostics now use the registered route
|
|
3592
|
+
pattern, such as `/countries/:id`, as `path`. They no longer include a raw `url`
|
|
3593
|
+
field or put query strings and concrete resource IDs in that path metadata.
|
|
3594
|
+
Express may omit `path` for errors that occur before a route is matched.
|
|
3595
|
+
HTTP routing and error responses are unchanged. Error-object details are a
|
|
3596
|
+
separate diagnostic payload and may still carry resource identifiers.
|
|
3597
|
+
|
|
3598
|
+
Both connectors await error diagnostics. A failing writer leaves the original
|
|
3599
|
+
HTTP response intact and adds the logging failure to `context.cleanupErrors`
|
|
3600
|
+
with `phase: 'logging'` and `during: 'httpError'`. Express route-registration
|
|
3601
|
+
diagnostics also preserve the original setup rejection if logging fails.
|
|
3602
|
+
|
|
3603
|
+
HTTP diagnostic loggers omit structured `body` and `headers` containers,
|
|
3604
|
+
including on errors raised before a route supplies resource metadata. Known JSON
|
|
3605
|
+
parser failures (`type: 'entity.parse.failed'`) produce a generic diagnostic
|
|
3606
|
+
preview with name, message, type and status. Their raw message, stack, body and
|
|
3607
|
+
cause are omitted because parser messages can quote the request body. Express
|
|
3608
|
+
uses this parser identifier already; the Fastify JSON parser supplies the same
|
|
3609
|
+
identifier. The original error still reaches hooks and HTTP error mapping.
|
|
3610
|
+
|
|
3611
|
+
A read hook that throws `null` or `undefined` produces a generic JSON:API 500
|
|
3612
|
+
response. Conditional PUT and positioning target reads recognize typed
|
|
3613
|
+
not-found errors; other hook failures propagate through the transaction's
|
|
3614
|
+
normal failure handling.
|
|
3615
|
+
|
|
3616
|
+
## Formatted errors are bounded previews
|
|
3617
|
+
|
|
3618
|
+
`formatError` and `formatErrorString` now produce bounded diagnostic previews:
|
|
3619
|
+
50 entries per container, 256 serialized-value visits, 2,048 characters per
|
|
3620
|
+
string, and a shared 8,192-character budget for copied text. Existing depth and
|
|
3621
|
+
cycle handling still applies. `[Truncated]` markers identify omitted content;
|
|
3622
|
+
these limits are not a promise of an exact byte length after JSON escaping or
|
|
3623
|
+
pretty printing. The original error and its attached values are unchanged.
|
|
3624
|
+
|
|
3625
|
+
Non-object `toJSON()` results appear under `toJSONResult` rather than being
|
|
3626
|
+
spread into character-index properties. Big integers and symbols in diagnostic
|
|
3627
|
+
values become text, functions become markers, and object-valued error names or
|
|
3628
|
+
messages become a non-string-text marker. These are diagnostic representations,
|
|
3629
|
+
not changes to the errors thrown by the API or the HTTP error contract.
|
|
3630
|
+
|
|
3631
|
+
One-line summaries share the bounded formatter and are capped at 2,048
|
|
3632
|
+
characters, including any truncation marker.
|
|
3633
|
+
Short summary spelling is unchanged. Summaries inspect only message, code,
|
|
3634
|
+
violations and fields; they do not invoke custom `toJSON()` methods.
|
|
3635
|
+
|
|
3636
|
+
## Enhanced logger events share a preview budget
|
|
3637
|
+
|
|
3638
|
+
Standard enhanced log methods now bound the complete argument list, including
|
|
3639
|
+
message strings and additional diagnostic data. They format at most 50 supplied
|
|
3640
|
+
arguments and use the existing shared depth/traversal/text limits for the emitted
|
|
3641
|
+
event. Truncation is explicit; source arguments are not mutated. The internal
|
|
3642
|
+
`formatDiagnosticValue` helper exposes that same bounded value conversion.
|
|
3643
|
+
|
|
3644
|
+
Validation errors now produce one structured event containing their details.
|
|
3645
|
+
The former extra `Validation error details:` event has been removed. Logger
|
|
3646
|
+
receiver binding, return values and exceptions still come from the original
|
|
3647
|
+
writer. These bounds do not apply to direct calls that bypass the enhanced
|
|
3648
|
+
logger, and bounding is not sensitive-field redaction.
|
|
3649
|
+
|
|
3650
|
+
The storage, positioning and file plugins use this formatter for their own
|
|
3651
|
+
diagnostics too. Include messages report counts instead of complete path
|
|
3652
|
+
collections. These changes preserve logger binding and query/file behavior;
|
|
3653
|
+
they do not add a resource-field policy to every logger.
|
|
3654
|
+
|
|
3655
|
+
## Field-aware write diagnostics
|
|
3656
|
+
|
|
3657
|
+
The resource write-error logger now derives redacted field names from compiled
|
|
3658
|
+
`outputFields`: both `hidden: true` and `normallyHidden: true` are redacted when
|
|
3659
|
+
that metadata is available. Explicitly requesting a normally-hidden field in a
|
|
3660
|
+
response does not make its diagnostic value visible. Caller input and thrown
|
|
3661
|
+
errors are unchanged.
|
|
3662
|
+
|
|
3663
|
+
The internal enhanced logger and formatter accept `redactFields` for structured
|
|
3664
|
+
payload keys. Matching values become `[Redacted]` before they are read. Validation
|
|
3665
|
+
objects whose `field` is a listed name or `data.attributes.<name>` retain field
|
|
3666
|
+
and rule metadata with a redacted message; their other payload properties are
|
|
3667
|
+
omitted. Summaries use the same policy. Enhanced loggers snapshot the configured
|
|
3668
|
+
field-name list at creation.
|
|
3669
|
+
|
|
3670
|
+
When field redaction is configured or `includeStack: false` is selected,
|
|
3671
|
+
`formatError` inspects ordinary error properties without invoking `toJSON()`.
|
|
3672
|
+
Custom conversion could otherwise read an excluded stack or copy a hidden value
|
|
3673
|
+
under another key before filtering. Unrestricted diagnostics still use custom
|
|
3674
|
+
`toJSON()` results. Nested error-envelope accessors are inspected once through the
|
|
3675
|
+
bounded value formatter; their returned errors use ordinary property inspection.
|
|
3676
|
+
|
|
3677
|
+
This is structured-field redaction, not a scanner for arbitrary secret text in
|
|
3678
|
+
messages. Early failures without compiled metadata cannot infer which application
|
|
3679
|
+
attributes are sensitive. Custom search validators should return general
|
|
3680
|
+
validation messages: their text can become the query error's top-level message,
|
|
3681
|
+
where field-key redaction cannot recognize a value embedded in a sentence.
|
|
3682
|
+
|
|
3683
|
+
The resource-field policy applies to write reports, matched HTTP resource routes,
|
|
3684
|
+
file cleanup, subscription admission/removal, subscription matching and
|
|
3685
|
+
notification permission reports. It uses the resolved resource's compiled fields;
|
|
3686
|
+
it does not guess a resource from a URL, an authentication error or a client-supplied
|
|
3687
|
+
subscription name. Authentication and server-wide reports omit raw handshake,
|
|
3688
|
+
credential and subscription-filter objects. Application properties deliberately
|
|
3689
|
+
attached to an authentication error still appear as bounded diagnostics: a field
|
|
3690
|
+
named `accessKey` there does not inherit an unrelated resource's field policy.
|
|
3691
|
+
|
|
3692
|
+
Buffers and other binary values are represented by type and byte length,
|
|
3693
|
+
including uploaded-file metadata and nested errors. Identifiers, filenames,
|
|
3694
|
+
paths, arbitrary application/driver error text and values copied under unrelated
|
|
3695
|
+
keys are not automatically secret-free. Use general error messages, omit secrets
|
|
3696
|
+
from custom error properties, and apply any broader retention/redaction policy in
|
|
3697
|
+
the application-owned logger. Direct application logger calls do not pass through
|
|
3698
|
+
the library's formatter. The built-in runtime no longer emits upstream method
|
|
3699
|
+
arguments or plugin-option dumps.
|
|
3700
|
+
|
|
3701
|
+
Socket connection/disconnection diagnostics and Redis setup/shutdown diagnostics
|
|
3702
|
+
contain throwing and rejecting writers. They cannot prevent handler installation,
|
|
3703
|
+
subscription acknowledgement or later Redis cleanup attempts. A Redis setup
|
|
3704
|
+
failure retains cleanup failures as secondary `AggregateError` members. The Redis
|
|
3705
|
+
adapter is configured before attaching Socket.IO to the caller's HTTP server, so
|
|
3706
|
+
an adapter setup failure leaves that server's listeners intact. If destruction
|
|
3707
|
+
itself fails, startup reports the failure without waiting indefinitely for the
|
|
3708
|
+
connection attempt that could not be cancelled.
|
|
3709
|
+
|
|
3710
|
+
|
|
3711
|
+
## Opt-in stored revision migration
|
|
3712
|
+
|
|
3713
|
+
Resources that select `versionField` need an explicit stored string field and a
|
|
3714
|
+
backfill of existing rows before conditional writers are enabled. Follow the
|
|
3715
|
+
[version-field migration guide](22-version-field-migrations.md) for
|
|
3716
|
+
ordinary and canonical storage, a tested backfill example, retry/rollback and
|
|
3717
|
+
readiness checks. Clients submit `expectedVersion` outside record attributes;
|
|
3718
|
+
bulk PATCH/DELETE use aligned `expectedVersions` arrays. These are revision
|
|
3719
|
+
conditions. HTTP representation conditions use the separate contract below.
|
|
3720
|
+
|
|
3721
|
+
## Opt-in HTTP representation conditions
|
|
3722
|
+
|
|
3723
|
+
The executable client example is `examples/conditional-http-client.js` in the
|
|
3724
|
+
repository. Import `readForConditionalUpdate`, pass the complete resource URL
|
|
3725
|
+
(including its query), edit the returned `document`, then call `save` with a
|
|
3726
|
+
JSON:API update document. Supply application headers and `credentials: 'include'`
|
|
3727
|
+
when your cross-origin session requires them. Each returned object retains the
|
|
3728
|
+
original read's ETag: after saving, read again before another edit. The example
|
|
3729
|
+
returns the write's `Response` so the caller can handle 412 and other errors.
|
|
3730
|
+
Run `node --test tests/http-validator-client-example.test.js` on Node 24 to
|
|
3731
|
+
exercise it against real Express and Fastify HTTP servers.
|
|
3732
|
+
|
|
3733
|
+
HTTP representation conditions require no version column. They compare the
|
|
3734
|
+
selected JSON response, including requested includes,
|
|
3735
|
+
fieldsets, computed values and response-hook changes.
|
|
3736
|
+
|
|
3737
|
+
Enable it when installing either connector, before registering resources:
|
|
3738
|
+
|
|
3739
|
+
```js
|
|
3740
|
+
await api.use(ExpressPlugin, { mountPath: '/api', httpValidators: true })
|
|
3741
|
+
// Or use FastifyPlugin with the same httpValidators option.
|
|
3742
|
+
```
|
|
3743
|
+
|
|
3744
|
+
The default is `false`. Successful GET/HEAD responses with a body receive a
|
|
3745
|
+
strong ETag. Preserve the complete quoted header value and send it unchanged in
|
|
3746
|
+
`If-Match` when updating or deleting the same resource. Use the same query string
|
|
3747
|
+
and representation-selecting request headers for the read and write:
|
|
3748
|
+
|
|
3749
|
+
```js
|
|
3750
|
+
const url = '/api/articles/42?include=author'
|
|
3751
|
+
const selectedHeaders = { Accept: 'application/vnd.api+json' }
|
|
3752
|
+
const current = await fetch(url, { headers: selectedHeaders })
|
|
3753
|
+
if (!current.ok) throw new Error(`Read failed: ${current.status}`)
|
|
3754
|
+
const etag = current.headers.get('ETag')
|
|
3755
|
+
if (!etag) throw new Error('The selected response has no ETag')
|
|
3756
|
+
|
|
3757
|
+
const updated = await fetch(url, {
|
|
3758
|
+
method: 'PATCH',
|
|
3759
|
+
headers: {
|
|
3760
|
+
...selectedHeaders,
|
|
3761
|
+
'Content-Type': 'application/vnd.api+json',
|
|
3762
|
+
'If-Match': etag
|
|
3763
|
+
},
|
|
3764
|
+
body: JSON.stringify({
|
|
3765
|
+
data: { type: 'articles', id: '42', attributes: { title: 'Revised title' } }
|
|
3766
|
+
})
|
|
3767
|
+
})
|
|
3768
|
+
if (updated.status === 412) {
|
|
3769
|
+
// Read again and resolve the competing change before submitting another edit.
|
|
3770
|
+
} else if (!updated.ok) {
|
|
3771
|
+
throw new Error(`Update failed: ${updated.status}`)
|
|
3772
|
+
}
|
|
3773
|
+
```
|
|
3774
|
+
|
|
3775
|
+
A matching strong tag permits registered resource PUT/PATCH/DELETE. A stale tag
|
|
3776
|
+
returns 412 with code `REST_API_PRECONDITION_FAILED`. Weak tags cannot match;
|
|
3777
|
+
malformed headers return 422. `If-Match: *` checks visible existence only and
|
|
3778
|
+
does not detect intervening edits. Omitting the header leaves writes
|
|
3779
|
+
unconditional. Conditional PUT cannot create a missing resource. Collection
|
|
3780
|
+
POST, relationship mutations and bulk routes currently reject `If-Match` with
|
|
3781
|
+
422; use their documented direct revision conditions where supported. Normal
|
|
3782
|
+
validation and authorization errors take precedence over comparison. A hidden
|
|
3783
|
+
PUT target rejected by existing resource checks retains 404; a missing target
|
|
3784
|
+
that otherwise permits PUT-create returns 412. These HTTP PUT outcomes are not
|
|
3785
|
+
an identical hidden/missing response contract. This
|
|
3786
|
+
option does not implement `If-None-Match` caching. Stored `expectedVersion`
|
|
3787
|
+
conflicts remain a distinct 409 error.
|
|
3788
|
+
|
|
3789
|
+
For cross-origin browser clients, install `CorsPlugin` after the connector and
|
|
3790
|
+
allow `If-Match` requests and expose `ETag` responses. These arrays replace the
|
|
3791
|
+
defaults, so retain other headers your application needs:
|
|
3792
|
+
|
|
3793
|
+
```js
|
|
3794
|
+
await api.use(CorsPlugin, {
|
|
3795
|
+
origin: 'https://app.example.com',
|
|
3796
|
+
credentials: true,
|
|
3797
|
+
methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
3798
|
+
allowedHeaders: [
|
|
3799
|
+
'Content-Type', 'Authorization', 'X-Requested-With',
|
|
3800
|
+
'X-HTTP-Method-Override', 'Accept', 'Origin', 'If-Match'
|
|
3801
|
+
],
|
|
3802
|
+
exposedHeaders: ['X-Total-Count', 'X-Page-Count', 'Link', 'Location', 'ETag']
|
|
3803
|
+
})
|
|
3804
|
+
```
|
|
3805
|
+
|
|
3806
|
+
Enabling this option makes the connector serialize successful GET/HEAD bodies
|
|
3807
|
+
once with `JSON.stringify` after response hooks and send those exact bytes.
|
|
3808
|
+
Express JSON spacing, replacers and escaping settings, and Fastify custom JSON
|
|
3809
|
+
serializers, do not process those pre-serialized bodies. Move required body
|
|
3810
|
+
transformations into the resource output or transport response hook before
|
|
3811
|
+
enabling it. Middleware that subsequently changes bytes or content encoding
|
|
3812
|
+
must also maintain the corresponding validator. Successful PUT responses emit
|
|
3813
|
+
no validator in this mode; fetch the selected representation again for its tag.
|
|
3814
|
+
|
|
3815
|
+
A strong conditional write renders a speculative GET inside its managed
|
|
3816
|
+
serializable transaction before changing the resource. Response hooks see
|
|
3817
|
+
`context.precondition === true` and normalized GET request metadata during that
|
|
3818
|
+
render, then run separately for the actual write response. Keep rendering
|
|
3819
|
+
deterministic and free of external side effects. Computed database reads must
|
|
3820
|
+
borrow `context.transaction` and preserve the caller's authorization context.
|
|
3821
|
+
Normalized request headers are copied; native request/reply objects and
|
|
3822
|
+
application callbacks are still owned by the actual request. Do not use native
|
|
3823
|
+
response methods while rendering the speculative view. Transaction conflicts
|
|
3824
|
+
currently retain native error handling (HTTP 500), with no automatic retry.
|
|
3825
|
+
|
|
3826
|
+
See the [API reference](../API.md) for conditional request options and
|
|
3827
|
+
[version-field migrations](22-version-field-migrations.md) for database preparation.
|