json-rest-api 1.0.29 → 2.1.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.
Files changed (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,3838 @@
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
+ `ensureAnyApiSchema` now adds `schema_version` and `definition_json` to
1470
+ `any_resource_configs` and a unique slot index to `any_field_configs`. It refuses
1471
+ to add the index while two fields of one resource share a slot, naming the slot;
1472
+ reassign one of them first. Deployments using `ensureSchema: false` must run
1473
+ `ensureAnyApiSchema` in a migration. Registration no longer rejects a declaration
1474
+ that omits a populated field or reorders fields: stored slots stay where they
1475
+ are, and omitted populated fields keep their data without being published.
1476
+ Fields added before this release that the declaration does not repeat are kept
1477
+ and published from their stored definitions.
1478
+
1479
+ Explicit maps take effect during registration, before `createKnexTable()`.
1480
+ Repeating that method no longer re-registers the initial schema and discards
1481
+ additions. Keep the expanded declaration and complete map in app source for
1482
+ restart, including functions that JSON metadata cannot reconstruct.
1483
+
1484
+ Deep `AnyapiRegistry` writes must receive a transaction from `api.transaction`
1485
+ when joining an existing unit. `addKnexFields()` uses this owner for its metadata
1486
+ batch internally. Direct registry callers must invalidate the registry descriptor
1487
+ cache after confirmed commit. Transaction-local reads use that
1488
+ transaction and bypass the global cache, preventing rolled-back metadata from
1489
+ leaking through cached descriptors. This does not publish new resource metadata.
1490
+ Resource operations use `resource.vars.schemaInfo.descriptor`, the same published
1491
+ mapping as their storage adapter. CRUD, counts, includes and relationship calls
1492
+ do not query configuration tables or apply direct registry changes implicitly.
1493
+ Register related resources on the API before querying them; persisted registry
1494
+ entries alone do not supply runtime resource declarations.
1495
+
1496
+ Use `addKnexFields()` for supported field additions; it commits metadata before
1497
+ publishing the new compiled schema and adapter. After a direct, committed mapping
1498
+ migration with the same declarations, invalidate the registry cache and call
1499
+ `createKnexTable()` to refresh the descriptor. A failed refresh retains the
1500
+ previous published resource. Structural schema changes also need updated source
1501
+ declarations and compilation; invalidation alone cannot reconstruct callbacks.
1502
+ Perform configuration/mapping changes while application operations are stopped.
1503
+ Arbitrary in-place metadata edits or concurrent reconfiguration are not a
1504
+ supported publication mechanism. No automatic data migration is performed.
1505
+
1506
+ The internal `preloadRelatedDescriptors` helper now accepts
1507
+ `{ descriptor, scopes }` and returns its map synchronously; `scopes` is the
1508
+ resource collection. It no longer accepts registry/transaction arguments.
1509
+ `AnyapiQueryAdapter` no longer stores its unused registry argument. Deep callers
1510
+ must use the published resource metadata when building related query maps.
1511
+
1512
+ Owned registry registration/allocation now preserves the original failure when
1513
+ rollback also rejects; secondary diagnostics go to the registry error logger.
1514
+ Failed owned writes invalidate cached descriptors, including when a commit
1515
+ call rejects after database completion. A completed transaction is not rolled
1516
+ back again, and its next descriptor read reloads persisted metadata. Rejection
1517
+ does not by itself establish that a write was rolled back or can safely be
1518
+ retried. The descriptor cache also isolates tenant/resource pairs containing
1519
+ `::`; this cache-key change does not alter persisted data or method arguments.
1520
+
1521
+ The registry now retains at most 100 recently used descriptors. This is a cache
1522
+ bound, not a resource limit: an evicted descriptor reloads on the next explicit
1523
+ registry lookup, while normal resource operations continue using their published
1524
+ metadata. Direct registry callers must pass `{ transaction }` when borrowing
1525
+ one; do not rely on a warm cache to avoid acquiring another database connection.
1526
+ An explicit `{ bypassCache: true }` read that confirms removal now discards its
1527
+ stale cache entry. It does not remove a running resource or publish a new schema.
1528
+ No data migration or resource declaration change is needed for cache eviction.
1529
+
1530
+ Request-contract reuse now includes the resource name in its key. Deep callers
1531
+ sharing compiled field definitions across resource names receive the correct
1532
+ JSON:API type validation. Temporal normalization still accepts its supported
1533
+ precision declarations; unusual precision simply builds an uncached validator.
1534
+ Neither correction adds a caller option or requires a payload migration.
1535
+
1536
+ Schema inspection now supports PostgreSQL's current schema. Generated migrations
1537
+ preserve BigInt defaults as BigInt literals, and unchanged effective defaults or
1538
+ implicit size metadata no longer cause repeated alterations. Diff plans now
1539
+ include `dropCheckConstraints` for PostgreSQL inline-enum replacements. Generated
1540
+ enum diffs execute on PostgreSQL/MySQL; SQLite diffs report a required table rebuild
1541
+ and retain the existing enum definition. This is distinct from changing only an
1542
+ enum default, which is supported on all three databases. See the
1543
+ [schema and migration guide](21-schema-and-migrations.md) for execution
1544
+ and transaction limits. These helpers do not automatically migrate existing
1545
+ application databases.
1546
+
1547
+ Direct `alterKnexFields` now replaces PostgreSQL inline enum checks correctly,
1548
+ with rollback on failure. SQLite changes that require replacing an enum check
1549
+ throw before the field batch starts; changing only its default still works.
1550
+ For deep-helper callers, PostgreSQL transactions retain ownership through a
1551
+ savepoint. MySQL caller transactions are rejected before their implicit DDL
1552
+ commit, and SQLite borrowed rebuilds require foreign-key enforcement disabled
1553
+ before the transaction starts, with validation/restoration owned by the caller.
1554
+
1555
+ Schema DDL now preserves apostrophes, backslashes and question marks in enum
1556
+ values and static text/JSON/array defaults. Inspect previously generated or
1557
+ applied definitions containing those characters; regenerate faulty migration
1558
+ code and migrate stored data explicitly where necessary. Standalone generators
1559
+ must specify `options.dialect` for values needing dialect-specific SQL escaping.
1560
+ Resource generators infer the dialect from their Knex client.
1561
+
1562
+ MySQL diffs replacing a foreign-key-supporting index now include the dependent
1563
+ key in `dropForeignKeys` and `addForeignKeys`, even if its definition is unchanged.
1564
+ Review the whole plan and pause application writes for these multiple-statement
1565
+ DDL migrations; MySQL does not roll them back as one transaction.
1566
+
1567
+ ## Sparse fieldsets include relationships
1568
+
1569
+ Fieldsets now restrict attributes **and relationships** in both formats. Earlier
1570
+ versions rejected valid relationship names yet returned unrequested relationships.
1571
+ Update requests that use both `include` and `fields` to select the relationships
1572
+ their callers read:
1573
+
1574
+ ```js
1575
+ // Before: relied on an unrequested author relationship being returned.
1576
+ queryParams: { include: ['author'], fields: { books: 'title' } }
1577
+
1578
+ // After: asks for both the attribute and the relationship.
1579
+ queryParams: {
1580
+ include: ['author'],
1581
+ fields: { books: 'title,author', authors: 'name' }
1582
+ }
1583
+ ```
1584
+
1585
+ Use the declared relationship name (`author`), not its storage field (`author_id`).
1586
+ An empty fieldset (`books: ''`) returns no attributes or relationships; omit
1587
+ `books` from `fields` for the normal visible response. IDs remain present.
1588
+ Included resource types obey their own fieldsets, and unrequested computed
1589
+ fields are not evaluated. Full write responses follow the same rules without
1590
+ changing the fields stored by the write.
1591
+
1592
+ JSON:API still permits requested `included` resources when the primary fieldset
1593
+ omits their relationship linkage. Plain output reaches related resources through
1594
+ selected relationship properties, so select each relationship along a nested
1595
+ path. No stored-data migration is required. See the
1596
+ [JSON:API sparse-fieldset rules](https://jsonapi.org/format/#fetching-sparse-fieldsets).
1597
+
1598
+ The internal `findRelationshipDefinition` helper moved from
1599
+ `rest-api-plugin-methods/common.js` to
1600
+ `lib/querying-writing/relationship-contracts.js` under `plugins/core`, so fieldset
1601
+ validation and relationship methods use the same resolver. Update any deep
1602
+ imports; the former module does not forward that export.
1603
+
1604
+ ## Generated labels and computed visibility
1605
+
1606
+ `LabelPlugin` now preserves an explicitly declared stored or computed `label`.
1607
+ Automatic labels use public stored values, resolve a global-search alias through
1608
+ its local `actualField`, retain null fallbacks in sparse reads, and fall back to
1609
+ the current resource's logical ID. Hidden, normally hidden and virtual fields
1610
+ are excluded as automatic sources. Applications that need a custom display rule
1611
+ can declare their own computed `label`; see the [label guide](18-labels.md).
1612
+
1613
+ Computed callbacks now receive `{ id, attributes, ... }`, with the record's public
1614
+ logical ID supplied on GET, query and included-resource reads. Existing callbacks
1615
+ can keep their current arguments. Use `id` instead of looking for a custom ID
1616
+ column in `attributes` or using the parent request's ID.
1617
+
1618
+ Declare a computed field's inputs in `dependencies`, especially normally hidden
1619
+ stored fields. Sparse reads no longer fetch every normally hidden field just
1620
+ because one computed field was requested. That fallback could select nonexistent
1621
+ virtual columns and made sparse/full behavior disagree. Declared virtual inputs
1622
+ are not selected as SQL columns.
1623
+
1624
+ Hidden computed fields are no longer returned. Normally hidden computed fields
1625
+ require an explicit sparse-fieldset request. Visibility also applies after
1626
+ virtual input values are added to a full write response. Review callers that
1627
+ accidentally relied on these values being exposed.
1628
+
1629
+ Automatic labels require no data migration. If an older LabelPlugin installation
1630
+ removed an authored stored `label` from the compiled schema, check whether its
1631
+ column/canonical mapping exists and use the normal schema migration helpers to
1632
+ add it if needed. Values that were never persisted cannot be recovered by this
1633
+ code change.
1634
+
1635
+ ## Cursor validation and registered schema handlers
1636
+
1637
+ Cursor validation now uses the type handlers and validators captured by the
1638
+ resource's compiled attribute schema. Previously each cursor field created a
1639
+ schema from the current global registry, so changing a registered handler could
1640
+ make cursor validation disagree with the already-compiled resource.
1641
+
1642
+ Register schema extensions before adding resources. Existing compiled resources
1643
+ retain those handler functions; a new resource initialization or supported
1644
+ recompilation captures the current registrations. This does not introduce general
1645
+ live schema editing. Normal cursor spelling, numeric conversion, opaque IDs,
1646
+ null ordering, temporal precision and HTTP validation errors are unchanged.
1647
+ Temporal output conversion keeps its existing separate contracts and precision
1648
+ behavior; cursor validation does not truncate an invalid over-precise token.
1649
+
1650
+ Applications using resource methods need no argument changes. Direct importers
1651
+ of the internal `validateCursorValues` helper must pass the owning schema as its
1652
+ fourth argument:
1653
+
1654
+ ```js
1655
+ validateCursorValues(descriptors, values, 'after', scope.vars.schemaInfo.schemaInstance)
1656
+ ```
1657
+
1658
+ The helper has no fallback to the global registry. Contract reuse is scoped to
1659
+ that schema instance, and values/errors remain local to each call.
1660
+
1661
+ ## Field dependencies and projection metadata
1662
+
1663
+ Getter/setter declarations keep their callback signatures and use
1664
+ `runGetterAfter`/`runSetterAfter`. Computed fields keep `dependencies` and
1665
+ `compute({ attributes, id, ... })`. Dependencies now support transitive chains
1666
+ and fields without their own callback. Missing names, cycles, non-array
1667
+ declarations and supplied non-function getters/setters fail registration.
1668
+ Remove placeholder `null`/`false` callbacks; omit the callback and its ordering
1669
+ property when the field needs no transformation.
1670
+
1671
+ List every local input a callback reads. Sparse reads fetch the entire dependency
1672
+ chain, run getters before computations, and compute prerequisites once. A getter
1673
+ cannot depend on a computed field; move that calculation to a computed field.
1674
+ Setters see available validated input and earlier setter results; they do not
1675
+ load omitted PATCH fields from the database. Read logical IDs from callback
1676
+ `id`. Remove read dependencies on raw relationship backing fields or relationship
1677
+ aliases: these are not attribute values at callback time. The
1678
+ [dependency contract](12-field-transformations.md#dependency-contract)
1679
+ includes a chained example and visibility rules.
1680
+
1681
+ Full write responses now apply virtual getters before computations. Virtual
1682
+ input belongs to the written type/ID and is no longer copied onto included or
1683
+ unrelated records. Update calculations that expected raw virtual input after a
1684
+ getter, and remove any reliance on a parent's input appearing on its includes.
1685
+
1686
+ For custom plugins, replace writes to `scope.vars.queryFields` in `resource:added`
1687
+ with declarations in `context.queryFields` during `schema:enrich`. Read the final
1688
+ map from `scope.vars.schemaInfo.queryFields`. There is no compatibility copy at
1689
+ the former path. Core normalizes the declarations and checks collisions against
1690
+ the final attribute/computed/relationship/ID namespace before publication.
1691
+ Canonical additions rebuild this map together with dependencies.
1692
+ See the [plugin example](29-writing-plugins.md#the-query-field-seam).
1693
+ Ordinary applications using `QueryProjectionsPlugin` and resource `queryFields`
1694
+ options keep that setup unchanged.
1695
+
1696
+ The unused internal `sortFieldsByDependencies` wrapper was removed. Internal
1697
+ code uses the existing `topologicalSort` through `compileFieldDependencies`;
1698
+ applications should declare dependencies in their resource schemas.
1699
+
1700
+ ### Response dependency metadata
1701
+
1702
+ JSON:API resource objects no longer expose the internal
1703
+ `__$jsonrestapi_computed_deps$__` member. Update snapshots or application code
1704
+ that consumed it. Computed fields still receive their dependencies during
1705
+ enrichment; cleanup happens at the final response boundary, including full and
1706
+ minimal write responses after finish hooks.
1707
+
1708
+ Use declared `dependencies` and callback arguments for computations. Put
1709
+ application metadata in `meta` or declared attributes. Cleanup removes only the
1710
+ reserved resource-level member; keys inside user attributes and metadata retain
1711
+ their values. Method arguments and stored records need no migration for this fix.
1712
+
1713
+ ## Resource IDs and links
1714
+
1715
+ Write documents and relationship identifiers accept strings or finite numbers,
1716
+ then apply the resource's ID normalizer and schema. Plain inputs now preserve
1717
+ those values for the same validation as JSON:API inputs. They no longer silently
1718
+ drop zero, turn false into an unlinked relationship, or stringify objects into
1719
+ IDs. Use `null` to clear a to-one relationship. Convert a BigInt to a string
1720
+ before placing it in a write document or relationship identifier.
1721
+
1722
+ The default normalizer trims strings and accepts numeric path IDs (including
1723
+ BigInt); it no longer accepts booleans, arrays or objects through implicit
1724
+ `toString()`. A configured `normalizeId` remains the explicit customization
1725
+ point. Resource-specific normalizers still override the plugin default.
1726
+
1727
+ PATCH and PUT use the path ID only when the body ID is absent or undefined.
1728
+ An explicit zero must match the path after normalization. Explicit null, empty
1729
+ or invalid body IDs fail validation instead of being replaced by the path ID.
1730
+ For example, this now rejects without changing record `42`:
1731
+
1732
+ ```js
1733
+ await api.resources.books.patch({
1734
+ id: '42',
1735
+ format: 'plain',
1736
+ data: { id: 0, title: 'Wrong target' }
1737
+ })
1738
+ ```
1739
+
1740
+ The schema still controls valid keys: `type: 'id'` requires positive values;
1741
+ an integer or string key can permit zero. Public IDs remain strings. Leading
1742
+ zeroes, case, punctuation and large numeric strings remain intact for opaque
1743
+ string keys. Use a string to avoid JavaScript numeric precision loss.
1744
+
1745
+ SQL bigint primary and relationship IDs now retain their exact decimal values
1746
+ on library reads and writes. For example, `9223372036854775806` and
1747
+ `9223372036854775807` remain distinct through includes, relationship changes
1748
+ and cursor pages. For explicitly supplied keys this large, use a string ID
1749
+ declaration and pass decimal strings; keep SQL primary and reference column
1750
+ types compatible. Existing text keys retain leading zeroes.
1751
+
1752
+ Storage hooks can now receive an exact string where SQLite or MySQL previously
1753
+ returned a rounded Number. Keep identifiers as strings instead of converting
1754
+ them with `Number()` or `parseInt()`. This correction needs no new API option or
1755
+ data migration. It applies to library queries without changing the caller's
1756
+ Knex connection configuration. A Number already rounded in application code
1757
+ cannot be repaired by the library.
1758
+
1759
+ PostgreSQL seed/import jobs that write explicit IDs into sequence-backed keys
1760
+ must synchronize the sequence before later generated-ID inserts. Explicit IDs
1761
+ do not advance that sequence. Keep synchronization in the seed/import workflow.
1762
+ Both ordinary and custom ID columns are exercised with actual
1763
+ PostgreSQL/MySQL generated inserts and all return modes.
1764
+
1765
+ Resource and relationship URLs, including POST's Location header, now encode
1766
+ the ID as one path segment. Pass the original ID to resource methods; follow
1767
+ returned URLs as supplied, without encoding them again. For example, ID
1768
+ `part/one?x` produces `/books/part%2Fone%3Fx`.
1769
+
1770
+ Table creation and migration generation now map `type: 'integer'` to integer
1771
+ columns; previously it fell through to string columns. Existing tables are not
1772
+ altered automatically. If you used these helpers with integer declarations,
1773
+ inspect the stored values and review the generated migration before applying
1774
+ it. Determine whether existing application tables need conversion before
1775
+ applying the changed schema helpers.
1776
+
1777
+ ## Storage naming configuration
1778
+
1779
+ Use a string for `storage.naming`: `snake_case` (also `snake` or `snakeCase`) or
1780
+ `exact` (also `field` or `verbatim`). Omit it for `snake_case`. Coercible objects
1781
+ and inherited property names such as `constructor` are invalid naming modes
1782
+ and now reject at the normalization boundary. Replace an object with its
1783
+ intended string value; applications using the documented strings need no change.
1784
+
1785
+ Canonical row conversion also preserves a field literally named `undefined`
1786
+ when a relationship lacks a reverse attribute mapping. An absent mapping no
1787
+ longer causes that unrelated attribute to be removed.
1788
+
1789
+ ### Public field names and physical columns
1790
+
1791
+ Declared fields named `constructor`, `prototype`, `toString`, `valueOf`,
1792
+ `hasOwnProperty`, `isPrototypeOf`, `propertyIsEnumerable` and `toLocaleString`
1793
+ are handled as own fields. Stored values, generated filters, sparse responses,
1794
+ cursor sorting and field callbacks must not resolve to inherited JavaScript
1795
+ properties. This applies to plain records and JSON:API attributes.
1796
+
1797
+ Ordinary Knex storage rejects physical columns literally named `constructor`,
1798
+ `prototype` or `__proto__`. Knex's update compiler drops these names; a write
1799
+ could previously leave an old value unchanged. Validation now rejects these
1800
+ mappings during resource registration, adapter creation and table/migration
1801
+ generation. Unsupported `idProperty` names are also rejected. Use an explicit
1802
+ physical mapping while keeping the public name:
1803
+
1804
+ ```js
1805
+ schema: {
1806
+ constructor: {
1807
+ type: 'string',
1808
+ storage: { column: 'constructor_value' }
1809
+ }
1810
+ }
1811
+ ```
1812
+
1813
+ If the old physical column already contains data, first rename it in your normal
1814
+ database migration, then change the declaration. For example:
1815
+
1816
+ ```js
1817
+ await knex.schema.alterTable('books', table => {
1818
+ table.renameColumn('constructor', 'constructor_value')
1819
+ })
1820
+ ```
1821
+
1822
+ Review dependent indexes, constraints and raw SQL, and verify row counts and
1823
+ values on the application's database before enabling the new declaration.
1824
+ Registration and `createKnexTable()` do not rename existing columns for you.
1825
+ Canonical storage uses allocated slots and does not need this physical-column
1826
+ mapping for public `constructor` or `prototype` fields.
1827
+
1828
+ Minimal reads used by write authorization and PUT completeness checks now
1829
+ translate physical columns to logical fields once. If a physical column name
1830
+ also names another logical field, hooks now receive the correct original
1831
+ attributes. Update any hook that compensated for the previous double mapping.
1832
+
1833
+ ### Declaration dictionaries and reserved names
1834
+
1835
+ Use plain objects or null-prototype dictionaries with **own** declarations for
1836
+ schemas, search fields, projections and relationships. Definitions inherited
1837
+ from a custom prototype now reject instead of disappearing during compilation.
1838
+ This includes an object literal such as `{ __proto__: definition }`, which
1839
+ changes an object's prototype rather than declaring an ordinary property.
1840
+
1841
+ `__proto__` is not a supported field, filter, projection, relationship alias or
1842
+ relationship backing-field name. These declarations reject before compilation
1843
+ can lose the key. The same checks apply after enrichment hooks, during field
1844
+ additions, in direct storage/table helpers and at the canonical registry's write
1845
+ and descriptor-loading boundaries. Rename such a declaration. Preserve the
1846
+ existing physical column or canonical slot through an explicit mapping when it
1847
+ contains data; do not re-register over old metadata to erase migration evidence.
1848
+
1849
+ These checks do not recursively strip user data. An object-valued attribute can
1850
+ contain JSON keys named `__proto__`, `constructor` or `prototype`; strings such
1851
+ as `__proto__` also remain valid resource IDs. Their existing validation rules
1852
+ still apply.
1853
+
1854
+ Declare a belongs-to relationship alias as a string, for example
1855
+ `ownerId: { type: 'id', belongsTo: 'users', as: 'owner' }`. Numbers, booleans,
1856
+ arrays and objects in `as` now reject during schema validation, including values
1857
+ introduced by `schema:enrich`. Replace `as: ['owner']` with `as: 'owner'`; do not
1858
+ depend on JavaScript coercing a configuration value into a property name.
1859
+
1860
+ The relationship mapping options `target`, `through` and `via` also require
1861
+ strings when supplied. For example, change `target: ['users']` to
1862
+ `target: 'users'`, and `through: ['memberships']` to `through: 'memberships'`.
1863
+ They check the declaration's type; they do not require a forward-referenced
1864
+ target or pivot resource to be registered earlier.
1865
+
1866
+ Use strings for schema `belongsTo` targets and relationship `foreignKey` and
1867
+ `otherKey` names too. In particular, enrichment hooks cannot bypass the
1868
+ backing-name type checks: change `foreignKey: ['ownerId']` to
1869
+ `foreignKey: 'ownerId'`. A schema field uses `belongsTo: 'users'`, not
1870
+ `belongsTo: ['users']`.
1871
+
1872
+ Polymorphic relationship declarations require `types` to be a nonempty array of
1873
+ nonempty strings, and `typeField` and `idField` must be nonempty strings.
1874
+ For example, use `{ types: ['users'], typeField: 'subjectType', idField:
1875
+ 'subjectId' }` as the `belongsToPolymorphic` definition. The authored
1876
+ polymorphic registration check still requires its target resources to be
1877
+ registered; these shape checks do not establish backing-column existence.
1878
+
1879
+ For reverse relationships, `hasOne` requires `target` and `foreignKey`; `hasMany`
1880
+ requires `target` and either `foreignKey` or its reverse-polymorphic `via`;
1881
+ `manyToMany` requires `through`, `foreignKey` and `otherKey`.
1882
+ Many-to-many `target` remains optional where the
1883
+ existing relationship-name default applies.
1884
+
1885
+ Declare these relationships in the resource's `relationships` option.
1886
+ `computedSchema:enrich.schemaRelationships` remains an inspection input, as
1887
+ described in [computed enrichment](#computed-field-enrichment). Defensive
1888
+ validation now catches malformed relationship mappings if a hook mutates that
1889
+ input; this does not make mutation or replacement of the inspection map a
1890
+ supported relationship declaration API. Move such declarations into the
1891
+ resource configuration when migrating.
1892
+
1893
+ Resource **names** must be JavaScript
1894
+ identifiers starting with a letter, `_` or `$`, excluding `__proto__`,
1895
+ `constructor` and `prototype`. Other inherited names such as `toString`,
1896
+ `valueOf` and `hasOwnProperty` now work with omitted or explicit fieldsets.
1897
+ This resource-name restriction does not prohibit `constructor` or `prototype`
1898
+ as attribute or relationship names. Valid relationship aliases also work in
1899
+ include trees and polymorphic include grouping.
1900
+
1901
+ Null relationship representation is unchanged: JSON:API contains `data: null`;
1902
+ plain conversion omits that relationship property. Use `Object.hasOwn(record,
1903
+ name)` when checking for presence of a potentially inherited name.
1904
+
1905
+ This establishes the own-property and reserved-name boundary. Full JSON:API
1906
+ member-name validation and other namespace/capability conflicts remain part of
1907
+ the wider compiled-metadata work; arbitrary punctuation is not guaranteed to
1908
+ work in every query syntax.
1909
+
1910
+ ## Sort fields and defaults
1911
+
1912
+ Use a string or an array of strings for resource `defaultSort`:
1913
+
1914
+ ```javascript
1915
+ // Replace this inconsistent configuration:
1916
+ defaultSort: { field: 'name', direction: 'desc' }
1917
+
1918
+ // With:
1919
+ defaultSort: ['-name']
1920
+ ```
1921
+
1922
+ The object form previously passed registration but failed through the public
1923
+ query method. Invalid non-string entries now fail during schema compilation.
1924
+ `null` and `[]` leave the default as ascending resource ID; explicit query sorting
1925
+ overrides the resource default. Keep programmatic `queryParams.sort` as an array.
1926
+ The resource ID remains the stable tie-breaker unless already included.
1927
+
1928
+ Sorting by `id` always means the resource ID. Sorting by a stored attribute or
1929
+ query projection uses that field, even if `searchSchema` defines a filter with
1930
+ the same name and a different `actualField`. For example:
1931
+
1932
+ ```javascript
1933
+ searchSchema: {
1934
+ name: { type: 'string', actualField: 'shortName' },
1935
+ byShortName: { type: 'string', actualField: 'shortName' }
1936
+ },
1937
+ sortableFields: ['id', 'name', 'byShortName']
1938
+ ```
1939
+
1940
+ Here `filters: { name: 'Ada' }` still filters `shortName`; `sort: ['name']`
1941
+ orders the stored `name`. Use `sort: ['byShortName']` to order by the distinct
1942
+ alias. An explicit sortable alias must resolve to a scalar stored field;
1943
+ relationship aliases retain their visibility checks. Whole-document JSON and
1944
+ cross-resource scalar ordering are not introduced by this change.
1945
+
1946
+ The same resolution applies to collection include `orderBy`. Cursor comparisons
1947
+ use the stored field's type and selected value, independently of the filter's
1948
+ input type. Sort fields needed internally are selected even when sparse output
1949
+ omits them. Ordinary and canonical storage both follow these rules.
1950
+ Restart existing cursor traversals after upgrading if a filter alias overlapped
1951
+ a sort field; the old cursor may describe a different ordering.
1952
+
1953
+ ### Hidden sort fields
1954
+
1955
+ Fields marked `hidden: true` cannot be sort keys. This includes stored fields,
1956
+ their search/relationship aliases, query projections, resource `defaultSort`,
1957
+ explicit `sortableFields`, and collection include `orderBy`. Previously, an
1958
+ explicit hidden stored sort could produce an incomplete cursor; a hidden
1959
+ projection marked sortable could expose its SQL value in cursors and links.
1960
+
1961
+ Remove hidden fields and their aliases from sort configuration and choose a
1962
+ public scalar sort field instead:
1963
+
1964
+ ```javascript
1965
+ schema: {
1966
+ secret: { type: 'string', hidden: true },
1967
+ displayOrder: { type: 'integer', normallyHidden: true }
1968
+ },
1969
+ sortableFields: ['id', 'displayOrder'],
1970
+ defaultSort: ['displayOrder']
1971
+ ```
1972
+
1973
+ Use `normallyHidden` only for publicly readable values that should be omitted
1974
+ by default. They remain sortable and their values can appear in cursors even
1975
+ when omitted by a sparse fieldset. Keep sensitive values `hidden`.
1976
+
1977
+ Invalid local declarations fail during schema compilation with
1978
+ `REST_API_VALIDATION` and violation rule `hidden_sort`. Include ordering is
1979
+ checked when the target resource is resolved, preserving forward declarations.
1980
+ Request validation excludes hidden sort keys, and query hooks cannot reintroduce
1981
+ them. Hidden fields/projections still work as declared getter/computed
1982
+ dependencies; their raw values remain omitted from resource output.
1983
+
1984
+ ## Pagination modes and counts
1985
+
1986
+ SQLite ordering now uses native `NULLS FIRST/LAST`, preserving the public null
1987
+ placement and cursor results while allowing existing ordering indexes to help.
1988
+ The verified engine is SQLite 3.49.2; SQLite engines older than 3.30 are no longer
1989
+ supported by these ordering helpers. PostgreSQL/MySQL ordering is unchanged.
1990
+ No payload or cursor migration is needed. Optional index choices and the existing
1991
+ migration paths belong in reviewed [schema migrations](21-schema-and-migrations.md).
1992
+
1993
+ The existing page options keep their meanings in both storage modes:
1994
+
1995
+ | Query page | Behavior |
1996
+ | --- | --- |
1997
+ | Omitted or `{}` | Apply the resource's capped default limit; omit pagination metadata |
1998
+ | `{ number: 2, size: 25 }` | Offset page 2, using the capped size for the offset and links |
1999
+ | `{ number: 2 }` | Offset page 2 with the resource's default size |
2000
+ | `{ size: 25 }` | First cursor page |
2001
+ | `{ after: cursor }` or `{ before: cursor }` | Adjacent forward/backward cursor page with the resource's default size |
2002
+
2003
+ Add `size` to a cursor boundary to choose a different page size. Page sizes are
2004
+ capped by `queryMaxLimit`. Use one of `number`, `after` or `before`; combining
2005
+ them, supplying non-positive/fractional page numbers/sizes, or supplying an
2006
+ empty cursor rejects before SQL. Follow generated links to retain filters,
2007
+ fieldsets and the effective size.
2008
+
2009
+ `enablePaginationCounts` controls totals for offset pages. With counts disabled,
2010
+ offset metadata contains `page` and `pageSize`; it does not promise totals or
2011
+ `hasMore`. Cursor pages use one extra row for `hasMore` and never issue a count
2012
+ query, regardless of this setting. Ordinary storage previously issued a count
2013
+ on the first cursor page and discarded its result; that extra query and its
2014
+ count-filter hook invocation have been removed. Collection predicates still
2015
+ run for every cursor page. Keep mandatory filters in the collection path.
2016
+
2017
+ Malformed cursor syntax now has the same error message in either storage mode,
2018
+ with code `REST_API_VALIDATION`, a `page.after` or `page.before` field, and rule
2019
+ `invalid_cursor`. Prefer the typed code/details when handling errors instead of
2020
+ matching a former backend-specific message. No call-site migration is needed
2021
+ for applications already using the page options above.
2022
+
2023
+ ## CORS and HTTP response hooks
2024
+
2025
+ Install `CorsPlugin` after Express or Fastify. Installation without a connector
2026
+ now fails immediately. Pass options directly to `api.use(CorsPlugin, options)`.
2027
+ Origin predicates may be async: their boolean result is now awaited, including
2028
+ inside origin arrays. A rejected predicate becomes a 500 response without CORS
2029
+ permission. Do not rely on the old behavior that treated a Promise as permission.
2030
+ `origin: false` disables origin permission, and `maxAge: 0` disables preflight
2031
+ caching. These values are no longer replaced by defaults.
2032
+
2033
+ Denied OPTIONS responses now use the JSON:API error envelope (`errors[0]`,
2034
+ status 403) instead of `{ error: 'CORS origin not allowed' }`. Other requests
2035
+ retain their actual resource/status behavior; CORS does not authorize writes.
2036
+ Existing credential/origin defaults remain permissive, so use explicit allowed
2037
+ origins for credentialed applications. See the [CORS guide](24-cors.md).
2038
+
2039
+ `transport:response` now runs for early parser/media/Accept errors and explicit
2040
+ `context.reject(...)` responses. Its request body can therefore be undefined;
2041
+ avoid assuming authentication, parsing or a resource method has completed.
2042
+ The hook chain is attempted once. If it throws, the connector maps the error
2043
+ without rerunning response hooks. A response-stage failure cannot roll back an
2044
+ already committed write.
2045
+
2046
+ Vary headers from the host, custom route, CORS and response hooks are merged
2047
+ case-insensitively, including on rejected requests; compare fields rather than
2048
+ their order. Fastify's native malformed-URL boundary still precedes connector
2049
+ hooks and has no connector CORS headers. Raw responses sent by host middleware
2050
+ or a hook that takes over the response need their own headers. No stored-data
2051
+ migration is required.
2052
+
2053
+ ## Multipart uploads
2054
+
2055
+ Unexpected file detector/parser/storage errors now retain their cause and
2056
+ operation context. Typed errors still propagate unchanged. Unexpected storage
2057
+ failures return HTTP 500 rather than being mislabeled as validation errors;
2058
+ declared MIME/size/schema violations remain 422 and typed access denials remain
2059
+ 403. Update callers that classified every upload failure as invalid input.
2060
+
2061
+ File cleanup keeps attempting later files after a cleanup or warning-log failure.
2062
+ Inspect `context.cleanupErrors` for `temporaryFileCleanup`, `uploadedFileCleanup`
2063
+ and associated `logging` entries. Failed deletions retain their tracking entries
2064
+ for reconciliation. Tracking belongs to the creating transaction, preventing a
2065
+ reused context from deleting files committed by an earlier transaction. Managed
2066
+ rollback invokes enlisted cleanup hooks after confirmed completion.
2067
+
2068
+ After commit, uploaded objects belong to the application. Replacing or deleting
2069
+ a record does not delete its previously committed files, including when no
2070
+ references remain in that resource. URLs may be shared across records and
2071
+ external consumers. If your application needs garbage collection, establish
2072
+ ownership and absence of references before calling `storage.delete(url)`; do
2073
+ not attach unconditional file deletion to a record's DELETE or replacement.
2074
+ Rollback cleans only newly uploaded objects belonging to the failed transaction.
2075
+ See the [file ownership contract](26-file-uploads.md#ownership-after-commit).
2076
+
2077
+ `LocalStorage` now reserves filenames exclusively, so concurrent original/custom
2078
+ names cannot overwrite another upload or make rollback delete its bytes.
2079
+ Existing files and symlinks are occupied names. Filename inspection now rejects
2080
+ permission and other I/O failures instead of returning an available name.
2081
+ Failed uploads remove only their reserved destination. If removal or closing
2082
+ also fails, inspect the returned `AggregateError`'s `cause` and `errors` for the
2083
+ original and cleanup failures. Its destination may need reconciliation. Buffer
2084
+ and temporary-file inputs retain their existing signatures; see
2085
+ [storage behavior](26-file-uploads.md#storage-adapters).
2086
+
2087
+ Install `FileHandlingPlugin` before `ExpressPlugin` and install the selected
2088
+ optional peer (`busboy` or `formidable`). Missing parsers and invalid factories
2089
+ now fail setup. The Express connector accepts multipart only when a detector is
2090
+ registered and uploads are enabled; Fastify continues to reject multipart.
2091
+
2092
+ The corrected parsers use Busboy 1.x and Formidable 3.x. Both return buffered
2093
+ files with `filename`, `mimetype`, `size` and `data`. If a custom storage adapter
2094
+ used Formidable's old `file.filepath` or `file.cleanup`, use `file.data` instead:
2095
+ the detector now removes its private request directory before returning.
2096
+ Custom detectors' cleanup callbacks are still supported.
2097
+
2098
+ Repeated file fields now produce 400. Upload one file for each distinct declared
2099
+ file field. Repeated text fields become arrays consistently across parsers;
2100
+ bracketed array suffixes group values in arrival order. Unknown text/file fields
2101
+ are validated, including prototype-like names that could previously disappear.
2102
+ Parser limits produce 413, and declared file MIME/size violations produce 422.
2103
+ Review the documented [bounded defaults](26-file-uploads.md#protocol-configuration)
2104
+ and set parser limits explicitly when larger uploads are needed.
2105
+
2106
+ Multipart POST/PUT/PATCH use the route's resource type; PUT takes its ID from
2107
+ the URL. Required file fields follow the normal schema rules, so PATCH can omit
2108
+ an existing file. Numeric `maxSize` now works, including zero for empty files.
2109
+ Plain programmatic calls with custom detectors update the canonical input record
2110
+ correctly. Detector errors propagate instead of falling through to another
2111
+ parser or silently continuing the write.
2112
+
2113
+ Owned and managed rollback cleanup, multiple uploads, replacement and deletion
2114
+ are verified with real local storage. Previously committed files remain
2115
+ application-owned: replacing a URL or deleting its resource does not delete
2116
+ the old object, which may have other references. Confirmed rollback cleans new
2117
+ uploads; confirmed commit releases their tracking. Review
2118
+ [file ownership after commit](26-file-uploads.md#ownership-after-commit)
2119
+ when migrating application cleanup code. No stored-data migration is required
2120
+ by this parser correction.
2121
+
2122
+ ## Socket.IO startup and shutdown
2123
+
2124
+ Keep the existing `await api.startSocketServer(server, options)` call, and await
2125
+ it before accepting subscription traffic. Calling it while another start is
2126
+ pending or a Socket.IO server is active now rejects; remove duplicate bootstrap
2127
+ calls instead of relying on replacement of a live server.
2128
+
2129
+ Redis connection failure no longer leaves Socket.IO attached or published in
2130
+ `api.io`. Partial connections are closed, the HTTP server and authentication
2131
+ configuration remain intact, and the application can retry. The supplied Redis
2132
+ reconnection policy still controls retries. Set
2133
+ `redis.socket.reconnectStrategy: false` if your bootstrap should reject on the
2134
+ first connection failure.
2135
+
2136
+ Closing Socket.IO also closes the HTTP server supplied to it. The plugin now
2137
+ owns shutdown of its two Redis clients when that HTTP server closes and clears
2138
+ `api.io` and its server/client references. Remove any subsequent manual `quit()`
2139
+ calls on `api.vars.socketIORedisClients`. Close your application database
2140
+ separately. No new shutdown method, compatibility wrapper or data migration is
2141
+ required. The [Socket.IO guide](28-socketio.md) lists the verified paths and
2142
+ the remaining network/failover limits.
2143
+
2144
+ ## Socket.IO subscriptions and notifications
2145
+
2146
+ Subscription messages now accept only `resource`, `filters` and an optional
2147
+ nonempty string `subscriptionId`. Remove `include` and `fields` from subscribe
2148
+ and restore messages; those options never changed the notification data. Keep
2149
+ them in the subsequent resource GET/query instead. They are also absent from
2150
+ the acknowledgement's `data`. Other unknown options receive `UNSUPPORTED_OPTION`.
2151
+
2152
+ Null, scalar and array filters now receive `INVALID_FILTERS`. A duplicate active
2153
+ subscription ID receives `SUBSCRIPTION_EXISTS`; unsubscribe before replacing it.
2154
+ Concurrent admissions respect the per-socket limit. A subscription hook receives
2155
+ `{ context }`, with `context.subscription` and `context.auth`; it may modify
2156
+ filters but not identity. Its final filters are validated.
2157
+
2158
+ Custom SQL search fields use their existing `applyFilter(query, value)` callback
2159
+ for subscription matching too. Remove Socket.IO-only `filterRecord` callbacks;
2160
+ the earlier draft's requirement for a parallel JavaScript predicate is withdrawn.
2161
+ Notification eligibility now runs ordinary resource queries before/after writes
2162
+ in the same transaction, with subscriber identity and an internal changed-ID
2163
+ constraint. Query hooks must support these calls.
2164
+
2165
+ If row policies or autofilters need context beyond `auth`, supply it from trusted
2166
+ server data in the existing hook:
2167
+
2168
+ ```js
2169
+ subscriptionFilters: {
2170
+ functionName: 'subscriber-context',
2171
+ handler: ({ context: { subscription, auth } }) => {
2172
+ subscription.context = {
2173
+ scopeValues: { workspaceId: auth.workspaceId },
2174
+ visibility: { groups: auth.groups }
2175
+ }
2176
+ }
2177
+ }
2178
+ ```
2179
+
2180
+ The property defaults to `{}`. Keep it JSON-serializable when using Redis.
2181
+ Client messages cannot set it. Populate whatever context your own policies
2182
+ require; the property names above are application examples. Resource query
2183
+ permissions receive this context on admission and notification as well.
2184
+
2185
+ Relationship POST/DELETE now run `beforeDataCall` and their respective
2186
+ `beforeDataCallPostRelationship` / `beforeDataCallDeleteRelationship` hooks
2187
+ before mutation. Hooks shared across methods must handle these operations.
2188
+
2189
+ Update consumers of `change.type`:
2190
+
2191
+ | Previous value | New value |
2192
+ | --- | --- |
2193
+ | `resource.postd` | `resource.created` |
2194
+ | `resource.putd` | `resource.created` for PUT-create, otherwise `resource.updated` |
2195
+ | `resource.patchd` | `resource.updated` |
2196
+ | `resource.deleted` | Unchanged |
2197
+
2198
+ The `action` field retains resource method names (`post`, `put`, `patch`,
2199
+ `delete`). IDs, including `deletedRecord.id`, are strings. Relationship changes
2200
+ produce parent updates with action `patch`, and changed reverse children also
2201
+ produce their resource updates. Each matching subscription gets its own event;
2202
+ use `subscriptionId` when one connection holds multiple views. Both entering and
2203
+ leaving a filtered or authorized result trigger invalidation when the corresponding
2204
+ query succeeds. Deletion uses the before-write query.
2205
+
2206
+ Query permissions are checked again before notification. `socket.data.subscriptions`
2207
+ is now a serializable array for adapter transport; applications that inspected
2208
+ the old internal Map must update that code. No stored-data migration is needed.
2209
+ The [Socket.IO guide](28-socketio.md) separates verified local behavior from
2210
+ remaining Redis failover/network uncertainty. Atomic bulk enlists each child's
2211
+ completion hooks with its owner. A failed deferred notice now leaves later
2212
+ queued notices eligible for delivery. The draining operation's context records
2213
+ `socketioBroadcast` cleanup diagnostics with a zero-based `broadcastIndex`;
2214
+ the call retains its committed outcome and first failure. Failed notices are
2215
+ not automatically replayed. See the Socket.IO guide for partial-delivery limits.
2216
+ Managed callbacks defer repeated changes to the same resource until completion.
2217
+ In-memory notices do not guarantee exactly-once delivery or recovery after
2218
+ process failure. Verify subscription and notification handling in each application
2219
+ that uses this plugin.
2220
+
2221
+ ## Bulk writes
2222
+
2223
+ Remove `batchSize` and `enableOptimizations` from plugin configuration, and pass
2224
+ the remaining options directly:
2225
+
2226
+ ```javascript
2227
+ await api.use(BulkOperationsPlugin, {
2228
+ maxBulkOperations: 100,
2229
+ defaultAtomic: true
2230
+ })
2231
+ ```
2232
+
2233
+ `maxBulkOperations` must be a positive safe integer and `defaultAtomic` a boolean.
2234
+ Unknown keys now fail installation, including a nested `'bulk-operations'`
2235
+ wrapper. The removed settings did not batch SQL: `batchSize` only divided a
2236
+ sequential loop, and the optimization hook never ran. Removing the loop setting
2237
+ also removes its invalid-value hang/skip paths. Bulk operations still execute
2238
+ the normal resource methods, including validation, permission checks and hooks.
2239
+ Atomic requests use one transaction; non-atomic requests complete each record's
2240
+ transaction separately. No stored-data migration is needed.
2241
+
2242
+ Bulk POST replaces `inputRecords` with either `data: plainRecords` or
2243
+ `document: { data: jsonApiResources }`. Bulk PATCH entries use either
2244
+ `{ id, data: plainValues }` or `{ id, document: { data: jsonApiResource } }`.
2245
+ Do not put a JSON:API resource directly inside an operation's `data` field.
2246
+ Each entry explicitly selects its input; neither bulk method infers it from
2247
+ `format`. The documented bulk HTTP bodies keep their wire shapes: POST uses
2248
+ `{ data: [resourceObject, ...] }` and PATCH uses `{ operations: [...] }`.
2249
+ The connector translates them into these method arguments. Remove raw-array
2250
+ bulk POST shorthand and arrays of individually wrapped documents; send one
2251
+ document containing the resource array instead.
2252
+
2253
+ Bulk POST/PATCH honor output-only `format` and `returning`, including resource
2254
+ defaults. Bulk results retain their `meta`/`errors` envelope:
2255
+ `returning: 'full'` supplies complete records in `data`, `minimal` supplies
2256
+ identifiers, and `none` omits `data`. Bulk PATCH no longer performs an extra GET
2257
+ to override a minimal/none return choice.
2258
+
2259
+ ```js
2260
+ await api.resources.books.bulkPatch({
2261
+ operations: [{ id: '42', data: { title: 'Updated title' } }],
2262
+ format: 'plain',
2263
+ returning: 'none',
2264
+ atomic: true
2265
+ }, requestContext)
2266
+ // { meta: { total: 1, succeeded: 1, failed: 0, atomic: true }, ... }
2267
+ ```
2268
+
2269
+ `atomic` must be a boolean in direct calls. HTTP accepts `atomic=true` or
2270
+ `atomic=false`; misspellings now fail with 422 before writing. Removed response
2271
+ options also fail clearly. Numeric zero is a valid bulk PATCH ID when the
2272
+ resource's declared ID schema accepts zero.
2273
+
2274
+ Install the connector and bulk plugin before declaring resources. Bulk routes
2275
+ now register through the active scope hook and retain authenticated request
2276
+ context. They explicitly select full JSON:API records, regardless of
2277
+ programmatic defaults. POST responds with 201; PATCH and DELETE respond with
2278
+ 200 and the batch summary. The optional plugin reserves the `bulk` path segment
2279
+ for these methods; do not use it as an opaque resource ID with this plugin.
2280
+ These endpoints are the library's bulk protocol, not the JSON:API Atomic
2281
+ Operations extension. An atomic denial rolls back the batch; a non-atomic batch
2282
+ retains permitted operations and reports denied entries by index.
2283
+
2284
+ An atomic batch now keeps its original child, validation or commit error if
2285
+ rollback also fails. The outer batch attempts rollback once for an unfinished
2286
+ transaction it owns. Its supplied context exposes the original `error` and any secondary
2287
+ rollback failure in `cleanupErrors`, using `{ phase: 'rollback', error }` entries.
2288
+ Both fields are cleared when the context starts another bulk call. A transaction
2289
+ already completed by the driver is not rolled back; a rejected commit still
2290
+ does not establish a safe retry. The error wrapper reports the available outcome.
2291
+ This change does
2292
+ not add bulk completion hooks or change non-atomic result envelopes.
2293
+
2294
+ Child cleanup warnings and failures now survive on the supplied batch context.
2295
+ `cleanupErrors` entries add a zero-based `bulkIndex` to their existing phase,
2296
+ original error and file information. This includes warnings from successful
2297
+ entries, rollback/cleanup-hook failures from rejected entries, and diagnostics
2298
+ collected before an atomic batch rethrows. Managed completion-chain failures
2299
+ also identify `operationIndex`, `scopeName` and `method`. Secondary errors remain on the
2300
+ context rather than being added to the batch response.
2301
+
2302
+ Remaining child `fileHandlingUploads` entries also reach the batch context with
2303
+ `bulkIndex`, preserving storage and transaction references. Context reuse clears
2304
+ old error diagnostics, retains unresolved upload tracking and starts each new
2305
+ child with separate tracking. Do not use retained tracking as a deletion or retry
2306
+ instruction: files can belong to committed data when an earlier `afterCommit`
2307
+ hook prevents the tracking-release hook from running, or to pending outer work.
2308
+
2309
+ For non-atomic results, `meta.failed` counts rejected calls. A post-commit hook
2310
+ failure can therefore appear as an indexed error even though the write is
2311
+ committed. Inspect the underlying failure and transaction evidence before
2312
+ retrying. Each error entry now has `error.transactionOutcome`; remaining managed
2313
+ integration is tracked separately.
2314
+ No payload or stored-data migration is required for this diagnostic correction.
2315
+
2316
+ Stored file handles also now read as strings when the ordinary SQL driver returns
2317
+ their UTF-8 bytes as a Buffer. The shared normalizer decodes declared `file`
2318
+ attributes, including byte views, without changing blob fields or database
2319
+ columns. Invalid encoding fails with its original cause and field context.
2320
+ Custom getters retain their raw database input. No schema migration or consumer
2321
+ Buffer-to-string wrapper is needed; use the resulting URL string directly.
2322
+
2323
+ Bulk POST/PATCH/DELETE now honor `transaction` in the first argument; previously
2324
+ they ignored it and could commit separately from the caller's transaction.
2325
+ Select `atomic: true` when supplying it, including when the plugin's
2326
+ `defaultAtomic` is false. Each child and its relationship changes use the same
2327
+ transaction. The in-progress managed contract requires `api.transaction` to
2328
+ own the unit. Earlier entries and the failing entry's pending writes roll back
2329
+ together, including when the callback catches a participating write failure.
2330
+ Catching the error does not restore those rows or create a savepoint.
2331
+
2332
+ ```javascript
2333
+ await api.transaction(async transaction => {
2334
+ return api.resources.books.bulkPatch({
2335
+ operations: [{ id: '42', data: { title: 'Updated title' } }],
2336
+ format: 'plain',
2337
+ returning: 'minimal',
2338
+ atomic: true,
2339
+ transaction
2340
+ })
2341
+ })
2342
+ ```
2343
+
2344
+ `atomic: false` with a supplied transaction now fails with
2345
+ `REST_API_VALIDATION` on `transaction`, before any child or SQL runs. Independent
2346
+ per-entry completion requires calls without an outer transaction. A supplied
2347
+ completed transaction also rejects instead of being silently replaced. Calls
2348
+ without `transaction` keep their atomic/non-atomic ownership. Completion hooks,
2349
+ file cleanup and notifications use the managed owner. No stored-data migration
2350
+ is needed for this transaction-ownership change. Check any application wrapper
2351
+ that supplies raw transactions to bulk methods.
2352
+
2353
+ ## Application-supplied primary IDs in table helpers
2354
+
2355
+ Ordinary table creation, generated migrations and migration diffs now recognize
2356
+ the declared primary ID by its mapped column, including `integer` and `string`
2357
+ definitions. Previously only a field with type `id` was recognized; a declared
2358
+ integer/string primary key could produce a duplicate generated ID column.
2359
+
2360
+ For a caller-supplied opaque key, declare the primary key explicitly:
2361
+
2362
+ ```js
2363
+ await api.addResource('records', {
2364
+ idProperty: 'record_key',
2365
+ schema: {
2366
+ id: {
2367
+ type: 'string', primary: true, required: true,
2368
+ storage: { column: 'record_key' }
2369
+ },
2370
+ name: { type: 'string' }
2371
+ }
2372
+ })
2373
+ ```
2374
+
2375
+ The same pattern works with `type: 'integer'`. `primary: true` keeps the
2376
+ authored column instead of adding an implicit auto-incrementing ID. Supply IDs
2377
+ when writing to a table without database allocation or a default.
2378
+ An opaque ID without that primary declaration cannot use implicit numeric
2379
+ allocation: the helper now rejects that configuration before producing DDL.
2380
+ For lower-level table definitions, `autoIncrement: false` remains available.
2381
+
2382
+ Table snapshots now accept these non-null, single-column integer/string primary
2383
+ keys without requiring auto-increment or a database default. Existing tables are
2384
+ not altered by this change. Regenerate migrations that failed with duplicate ID
2385
+ columns and review the resulting DDL; an unchanged valid table should produce
2386
+ an empty migration diff. Missing, nullable, floating-point and composite resource
2387
+ IDs remain rejected by snapshot validation.
2388
+
2389
+ ## Collection filtering hooks
2390
+
2391
+ Collection reads now honor a query builder assigned to
2392
+ `context.knexQuery.query` by a `knexQueryFiltering` hook. Previously, both storage
2393
+ modes could ignore that replacement and omit restrictions added to it; ordinary
2394
+ pagination totals could disagree with the returned page, and canonical totals
2395
+ could omit the restriction too. Related-resource collections use the corrected
2396
+ path as well.
2397
+
2398
+ When a hook needs a separate builder, clone the supplied query and apply further
2399
+ conditions to that clone. Keep its tenant/resource constraints, relationship
2400
+ membership and existing visibility predicates. The selected builder governs the
2401
+ page. Ordinary storage also runs filtering hooks for its separate count query;
2402
+ canonical storage clones the filtered collection query for that count. Apply the
2403
+ complete predicate on every filtering call, including count calls. Surrounding
2404
+ `context.knexQuery` metadata is restored after filtering succeeds or fails.
2405
+ Row-policy functions retain their existing
2406
+ `true`/`false` return contract. This correction adds no public option and requires
2407
+ no change to hooks that already mutate or replace the supplied builder correctly.
2408
+
2409
+ ## Full includes and projected fields
2410
+
2411
+ Ordinary-storage belongsTo, hasOne and polymorphic belongsTo includes now select
2412
+ visible SQL projections when no target fieldset is supplied. Previously these
2413
+ paths could omit the projections and pass missing dependencies to computed
2414
+ fields. Full and sparse includes now use the same field-selection rules as
2415
+ collection includes and canonical storage.
2416
+
2417
+ No call changes are required. Remove any fieldset used solely to work around
2418
+ missing projections; keep intentional sparse fieldsets to control response
2419
+ size. For example, `books.get({ id: '1', queryParams: { include: ['author'] } })`
2420
+ now returns the author's visible projections without also specifying
2421
+ `fields.authors`. A projection used only as a computed-field dependency remains
2422
+ absent from a sparse response unless requested. Projection failures reject the
2423
+ operation; full write-response failures happen before an owned commit.
2424
+
2425
+ ### Cyclic includes and resource identity
2426
+
2427
+ A resource appears once across a JSON:API document's `data` and `included`.
2428
+ For example, `items.get({ id: '1', format: 'jsonapi', queryParams: { include: ['groups.items'] } })`
2429
+ keeps the item in `data` and its group in `included`. The group's relationship
2430
+ can point back to `items/1` without adding another copy of that item. Relationships
2431
+ discovered through the cycle are retained on the primary resource, and computed
2432
+ fields run once per represented resource. Explicit collection include limits
2433
+ remain in effect when another path revisits the same resource.
2434
+
2435
+ If your response resolver searches only `included`, index both primary `data`
2436
+ and `included` by type and ID instead. Update snapshots that expected a primary
2437
+ resource to be repeated in `included`. Relationship identifiers and resource URLs
2438
+ keep their existing shape; this requires no stored-data migration.
2439
+
2440
+ Plain responses resolve references against both primary and included resources.
2441
+ Peers in a primary collection can therefore expand fully. Actual cycles end in
2442
+ an `{ id }` reference, with `_type` retained for polymorphic relationships;
2443
+ independent branches still receive separate objects. Keep custom computed
2444
+ callbacks free of assumptions that a cyclic path will invoke them twice.
2445
+
2446
+ ### Empty includes and refresh links
2447
+
2448
+ JSON:API resource reads and full write responses now retain `included: []` when
2449
+ an explicit include returns no resources. This also applies to empty collections
2450
+ and related endpoints with `data: null`. An explicitly empty include is supported:
2451
+
2452
+ ```js
2453
+ await api.resources.groups.get({
2454
+ id: '1',
2455
+ format: 'jsonapi',
2456
+ queryParams: { include: [] }
2457
+ })
2458
+ // { data: { ... }, included: [], links: { self: '.../groups/1?include=' } }
2459
+ ```
2460
+
2461
+ Omitting `include` keeps it absent from normalized query parameters and omits
2462
+ `included` when there are no included resources. HTTP `?include=` parses as an
2463
+ explicit empty array and survives serialization into refresh/pagination links.
2464
+ Hooks and direct parser callers that iterate includes should use
2465
+ `context.queryParams?.include ?? []`; do not assume an omitted include has been
2466
+ populated with an empty array. Update response snapshots for explicitly requested
2467
+ includes that used to omit `included`.
2468
+
2469
+ Top-level `links.self` on single-resource and related responses now preserves
2470
+ includes and fieldsets, as collection links do. Follow that link to refresh the
2471
+ same representation. Resource-level `data.links.self` remains the resource URL.
2472
+ Update assertions that assumed those two links were always identical.
2473
+
2474
+ Plain results retain their existing shapes. Programmatic `returning: 'minimal'`
2475
+ and `'none'` retain their selected return contracts; HTTP resource writes select
2476
+ full JSON:API responses. No method arguments or stored records need migration.
2477
+
2478
+ ### Relationship visibility hooks
2479
+
2480
+ Row policies and `knexQueryFiltering` hooks with
2481
+ `queryPurpose: 'relationship-identifiers'` now run once per batch of at most
2482
+ 100 unique target IDs for each resource type. Remove assumptions that this is
2483
+ one callback per request, and remove flags that skip a visibility predicate
2484
+ after the first query. Add the full predicate to each supplied builder; cloning
2485
+ that builder remains supported. Keep ordering, pagination and include limits
2486
+ in the operation/resource settings, and retain the selected ID and mandatory
2487
+ scope constraints when customizing a visibility query.
2488
+
2489
+ Identifier-visibility selections no longer put every ID into one `WHERE IN`. These reads
2490
+ select distinct IDs, preserve the input linkage order and duplicate occurrences,
2491
+ and retain the caller's transaction. A later batch failure rejects the whole
2492
+ read. See the [row-policy contract](17-row-policies.md#query-purposes-and-lifecycle-coverage)
2493
+ for the context supplied to these hooks.
2494
+
2495
+ ### Nested belongs-to include batches
2496
+
2497
+ Regular and polymorphic belongs-to includes now load at most 100 unique target
2498
+ IDs per resource type in each query. Large nested includes no longer place all
2499
+ of those IDs in one SQL statement. Target permission/filter hooks with
2500
+ `queryPurpose: 'include'` and SQL projection `select` callbacks run for every
2501
+ batch. Apply the complete predicate or projection each time; remove assumptions
2502
+ that they run once per include path. This does not change computed-field
2503
+ callbacks into query callbacks.
2504
+
2505
+ All target batches are gathered before traversing nested collections. Existing
2506
+ global and per-parent collection limits still apply across that complete parent
2507
+ set. A later batch failure rejects the operation; a failed full write response
2508
+ rolls back an owned transaction, while a borrowed transaction remains the
2509
+ caller's responsibility. Canonical storage also batches parent-link prefetches.
2510
+ This change adds no method options and does not impose a total response-memory
2511
+ bound or batch every collection include query.
2512
+
2513
+ ### Large collection identifier queries
2514
+
2515
+ Collection includes and relationship-metadata reads now deduplicate their ID
2516
+ lists. Above 100 distinct IDs, PostgreSQL receives one array parameter and
2517
+ SQLite receives one JSON array parameter through JSON1. Sorting, global limits,
2518
+ per-parent limits and policy predicates still run in the original SQL query.
2519
+ SQL projection and filtering callbacks retain their collection-query frequency.
2520
+ The separately batched belongs-to loaders retain the behavior described above.
2521
+
2522
+ Code that inspects raw Knex statements must allow array predicates and ID
2523
+ subqueries; use the documented query-purpose/context fields when applying
2524
+ policies. Small lists keep ordinary `IN` predicates. MySQL retains its ordinary
2525
+ `IN` query through Knex's text protocol, preserving its existing numeric and
2526
+ collation comparisons. These changes add no public options. They avoid database
2527
+ parameter-count overflow, but complete ID lists and responses still consume
2528
+ memory proportional to their size.
2529
+
2530
+ ### Explicit includes and linkage reads
2531
+
2532
+ An explicitly included relationship is now loaded directly by its include
2533
+ loader. The library skips the preliminary identifier read that the loader used
2534
+ to replace afterward. For standard many-to-many includes, authorized target
2535
+ records are selected and limited in SQL before the library reads their parent
2536
+ mappings. Window includes keep their per-parent limit behavior. Result ordering,
2537
+ duplicate-edge handling, sparse fields and transaction ownership remain the same.
2538
+
2539
+ Hooks observing `relationship-identifiers` queries will see fewer calls for
2540
+ explicitly included relationships. Target and ordinary pivot include permission
2541
+ checks still run. Mapping reads now occur after the target query, on the same
2542
+ transaction; a mapping-read failure still fails the operation. Complete unlimited
2543
+ includes can require additional mapping pages. No API option or data migration
2544
+ is needed.
2545
+
2546
+ ### Sparse responses and relationship hooks
2547
+
2548
+ Sparse fieldsets now avoid database reads for omitted relationship linkage in
2549
+ both storage modes. For example, `queryParams: { fields: { books: 'title' } }`
2550
+ does not load the book's collection identifiers or check visibility of to-one
2551
+ linkage that will be removed from the response. Selected linkage retains its
2552
+ visibility checks. Requests without a fieldset for that resource keep their
2553
+ existing linkage behavior.
2554
+
2555
+ An explicit `include` still loads its resources even when the corresponding
2556
+ relationship field is omitted. Nested includes, target permissions, computed
2557
+ attribute dependencies and transaction ownership retain their existing behavior.
2558
+ No new option is needed.
2559
+
2560
+ If a hook reads collection linkage from `context.record`, request that
2561
+ relationship in the resource's fieldset or load the data explicitly for the
2562
+ hook's purpose. Do not use the number of `relationship-identifiers` filtering
2563
+ hooks as a request notification: omitted linkage no longer triggers those reads.
2564
+ Authorization for requested primary/included resources still runs; authorization
2565
+ must not depend on an incidental query for an omitted relationship.
2566
+
2567
+ ### Canonical parent-link reads
2568
+
2569
+ Canonical many-to-many linkage now reads at most 101 physical link rows per
2570
+ query, retaining its existing 100-parent batch size. It still returns complete
2571
+ visible linkage. Raw linkage is assembled in physical link-ID order for each
2572
+ parent; use declared include ordering or related-resource query sorting when
2573
+ application order matters. Configured include limits and ordering still apply
2574
+ to the complete requested parent set.
2575
+
2576
+ The prefetch now also enforces the relationship's declared target resource type,
2577
+ matching the existing related-resource query. A same-key physical link to a
2578
+ different resource no longer appears in this relationship. No method option or
2579
+ data migration is needed. Later page failures reject the operation and preserve
2580
+ the existing owned/borrowed transaction behavior. Raw SQL observers should expect
2581
+ additional reads for large link collections.
2582
+
2583
+ ### Ordinary pivot membership writes
2584
+
2585
+ Many-to-many additions and replacements now read existing pivot rows for at
2586
+ most 100 requested target IDs at a time, paging duplicate physical edges in
2587
+ bounded results. New inserts still use batches of 100 rows. Replacement deletes
2588
+ unwanted rows using one predicate for the complete desired set, including large
2589
+ SQLite/PostgreSQL ID lists. Retained matching pivot rows keep their IDs and
2590
+ metadata. Empty replacement removes every link for that owner.
2591
+
2592
+ Equivalent requested target IDs, as resolved by the database's target ID column,
2593
+ are written once using the first submitted spelling. For example, `BETA` and
2594
+ `beta` referring to the same target no longer create two new pivot rows in one
2595
+ request. Existing duplicate rows are not merged automatically. Keep target ID
2596
+ and pivot reference column types/collations consistent; this does not migrate
2597
+ existing application schemas.
2598
+
2599
+ Method arguments, permission hooks and transaction ownership are unchanged.
2600
+ Code that observes raw SQL must allow paged pivot reads and a single exclusion
2601
+ DELETE for replacement. The complete input and keep-list still require memory
2602
+ proportional to their size; the batching does not set a new relationship limit.
2603
+
2604
+ ## Relationship target validation
2605
+
2606
+ POST, PUT and PATCH payloads, including relationship endpoints, now use one
2607
+ target-validation contract. Readable target rows are selected in batches of at
2608
+ most 100 distinct identifiers; `checkPermissions` then runs with `method: 'get'`
2609
+ for each distinct target within each relationship. Its `originalContext`
2610
+ contains that target's `id`, `scopeName`, `schemaInfo` and `minimalRecord`, plus
2611
+ the caller's auth and transaction. Repeated identifiers do not repeat permission
2612
+ checks within one relationship. A target referenced through two relationships
2613
+ is checked for each relationship.
2614
+
2615
+ Many-to-many writes no longer run a full target GET after this check. Move any
2616
+ link-authorization logic from `checkDataPermissionsGet`, `beforeDataGet`, getters,
2617
+ computed fields, enrichment or `finishGet` into the existing permission hook:
2618
+
2619
+ ```js
2620
+ checkPermissions: {
2621
+ functionName: 'authorize-readable-target',
2622
+ handler: ({ context }) => {
2623
+ if (context.method !== 'get') return
2624
+ const request = context.originalContext
2625
+ assertCanRead(request.auth, request.minimalRecord)
2626
+ }
2627
+ }
2628
+ ```
2629
+
2630
+ The minimal record contains stored attributes and to-one linkage; it has no
2631
+ response getters/computed fields or includes. Response hooks still run for real
2632
+ GETs and requested full write responses. Use write/transaction hooks for side
2633
+ effects of linking. Remove assumptions about one target GET per supplied ID.
2634
+
2635
+ Row policies and `knexQueryFiltering` run with
2636
+ `queryPurpose: 'relationship-validation'`, empty client filters and no individual
2637
+ `context.id`. Add the complete row predicate to every supplied builder. Retain
2638
+ its selected ID and mandatory scope constraints; the filtered ID subquery
2639
+ selects the full minimal rows without multiplying them through policy joins.
2640
+ An alternate-spelling lookup can run an additional scoped query to retain the
2641
+ database's ID collation semantics. Per-target decisions belong in the permission
2642
+ hook, where the individual ID and record are available.
2643
+
2644
+ Target locks and storage existence checks remain. A failure rejects before
2645
+ link writes; an owned operation rolls back, while a borrowed transaction stays
2646
+ with its caller. Internal pivot/link storage helpers now assume the resource
2647
+ operation has authorized its payload; applications should use resource methods.
2648
+ No option retains the old target GET lifecycle. Port application hooks that
2649
+ relied on those implicit reads.
2650
+
2651
+ ## Setter, getter and computed-field failures
2652
+
2653
+ Field callbacks now propagate failures in both formats and storage modes,
2654
+ including primary resources, included resources and nested includes. There is
2655
+ no additional policy option. The changes are:
2656
+
2657
+ | Callback failure | Previous behavior | Current behavior |
2658
+ | --- | --- | --- |
2659
+ | Unexpected setter error | Converted to validation error (422), losing its cause | Operation rejects with the original cause; HTTP 500 |
2660
+ | Unexpected getter error | Logged; response kept the current field value | Operation rejects; HTTP 500 |
2661
+ | Unexpected computed-field error | Logged; response substituted null | Operation rejects; HTTP 500 |
2662
+ | Typed API error | Propagated at field callbacks | Unchanged on reads; retained as the write wrapper's cause, with code/details still readable |
2663
+ | Relationship target permission failure | Could be replaced with not-found (404) | Original classification/cause retained by target validation; a missing target still produces 404 |
2664
+
2665
+ In the cause chain, unexpected callback errors have `cause` and
2666
+ `context: { scopeName, fieldName, phase }`, where `phase` is `setter`, `getter`
2667
+ or `computed`. Include processing can add outer wrappers; follow the cause
2668
+ chain to the field error. Synchronous throws and asynchronous rejections follow
2669
+ the same rule, including non-Error values such as null or undefined.
2670
+
2671
+ If a setter intentionally rejects user input, change its generic throw to the
2672
+ existing validation error class. For example, inside a resource schema:
2673
+
2674
+ ```js
2675
+ import { RestApiValidationError } from 'json-rest-api'
2676
+
2677
+ const quantityField = {
2678
+ type: 'number',
2679
+ setter: value => {
2680
+ if (!Number.isInteger(value)) {
2681
+ // Previously: throw new Error('Quantity must be a whole number')
2682
+ throw new RestApiValidationError('Quantity must be a whole number', {
2683
+ fields: ['data.attributes.quantity'],
2684
+ violations: [{
2685
+ field: 'data.attributes.quantity',
2686
+ rule: 'integer',
2687
+ message: 'Quantity must be a whole number'
2688
+ }]
2689
+ })
2690
+ }
2691
+ return value
2692
+ }
2693
+ }
2694
+ ```
2695
+
2696
+ This keeps intentional input rejection at 422. A typed forbidden resource
2697
+ error remains 403. Unexpected programming or service failures should propagate.
2698
+ When a value is intentionally unavailable, return that domain value explicitly:
2699
+
2700
+ ```js
2701
+ const helpfulnessField = {
2702
+ type: 'number',
2703
+ computed: true,
2704
+ dependencies: ['helpful_votes', 'total_votes'],
2705
+ compute: ({ attributes }) => attributes.total_votes === 0
2706
+ ? null
2707
+ : 100 * attributes.helpful_votes / attributes.total_votes
2708
+ }
2709
+ ```
2710
+
2711
+ A callback's own intentional fallback still works. Review existing generic
2712
+ validation throws and code that depended on the library substituting null or
2713
+ returning an untransformed value. Handle rejected reads and writes instead of
2714
+ treating an incomplete response as success.
2715
+
2716
+ GET/query return no successful partial response after an executed callback
2717
+ fails. POST/PUT/PATCH full-response enrichment runs before owned commit, so its
2718
+ failure rolls back owned record and relationship changes. Borrowed transactions
2719
+ remain active for their owner to complete. Minimal/none responses do not run
2720
+ callbacks solely to enrich an unused full response; callbacks needed for other
2721
+ work, such as validating related resources, still run and can fail the write.
2722
+ Sparse fieldsets likewise avoid callbacks that are neither selected nor needed
2723
+ as dependencies. These rules do not undo a write whose commit was already
2724
+ acknowledged; post-commit side effects and cleanup retain their own semantics.
2725
+
2726
+ ## Stored relationship metadata
2727
+
2728
+ Canonical reverse relationship linkage now selects only child identity and
2729
+ parent-reference columns. Large target attributes are no longer fetched merely
2730
+ to construct linkage. Returned IDs, filters and transaction ownership are
2731
+ unchanged. Query hooks for `queryPurpose: 'relationship-identifiers'` should
2732
+ preserve these selections when replacing the builder; add predicates against
2733
+ the underlying columns as usual. No payload or data migration is needed.
2734
+
2735
+ Polymorphic linkage read from storage must use a target type declared by its
2736
+ relationship. For example, with
2737
+ `subject: { belongsToPolymorphic: { types: ['groups'], ... } }`, a stored
2738
+ `subjectType: 'items'` is invalid even if an `items` resource exists. Previously
2739
+ ordinary includes could silently omit that target, while canonical storage
2740
+ could include it. Both modes now reject with HTTP 500 and programmatic error
2741
+ context `{ scopeName, relationshipName, phase: 'relationshipData' }`.
2742
+ This also applies to linkage returned without an include. A disallowed target
2743
+ submitted through the write API still produces a validation error (422).
2744
+
2745
+ Repair inconsistent stored type/ID pairs or update the declared relationship
2746
+ when the target type is intentional. Nullable, intentionally empty
2747
+ relationships retain their existing representation. Sparse fieldsets do not
2748
+ guarantee bypassing stored-linkage checks: preliminary reads and relationship
2749
+ selection can still require those columns.
2750
+
2751
+ Required reverse-relationship metadata must also exist: a `via` relationship
2752
+ must refer to a polymorphic relationship, and canonical foreign-key mappings
2753
+ must resolve to a stored field. Missing required metadata now rejects instead
2754
+ of returning empty linkage. Correct the resource declaration or stored
2755
+ canonical metadata, then retry. Schema-read failures during field selection
2756
+ and record conversion retain their original cause and resource context with
2757
+ phase `relationshipMetadata`; they are no longer ignored as if the resource
2758
+ had no relationships.
2759
+
2760
+ Nested polymorphic include paths can still apply to only some declared target
2761
+ types. If `subject` allows `writers` and `books`, and only writers have a
2762
+ `profile`, `include=subject.profile` remains valid; books simply have no profile
2763
+ branch. A path supported by none of the declared target types is rejected by
2764
+ request validation.
2765
+
2766
+ These read checks also apply while preparing write responses. A failure before
2767
+ owned commit rolls back the owned write; a borrowed transaction remains its
2768
+ owner's responsibility. An acknowledged commit cannot be undone by a later
2769
+ response or side-effect error.
2770
+
2771
+ ## Schema enrichment and canonical registration
2772
+
2773
+ Use `schema:enrich` to change attribute definitions. Use `searchSchema:enrich`
2774
+ to change filter definitions. Previously the search hook received the attribute
2775
+ map in `context.fields`, so edits could affect writes while failing to change
2776
+ filter validation. It now receives the generated search map, including explicit
2777
+ search aliases. Adding, editing, deleting or replacing `context.fields` affects
2778
+ the search contract. The hook also runs when the initial search map is empty.
2779
+
2780
+ ```js
2781
+ await api.customize({
2782
+ hooks: {
2783
+ 'schema:enrich': {
2784
+ functionName: 'declare-product-stock',
2785
+ handler: ({ context }) => {
2786
+ if (context.scopeName !== 'products') return
2787
+ context.fields.stock = { type: 'integer', defaultTo: 0 }
2788
+ }
2789
+ },
2790
+ 'searchSchema:enrich': {
2791
+ functionName: 'declare-product-filters',
2792
+ handler: ({ context }) => {
2793
+ if (context.scopeName !== 'products') return
2794
+ context.fields.nameContains = {
2795
+ type: 'string', actualField: 'name', filterOperator: 'contains'
2796
+ }
2797
+ }
2798
+ }
2799
+ }
2800
+ })
2801
+ // Register resources after installing their schema hooks.
2802
+ ```
2803
+
2804
+ Treat `originalFields` as inspection input; edit `fields`. Definitions are
2805
+ shallow copies, so this is not a guarantee of deep immutability. Search index
2806
+ hints are applied to the hook's final result. A search-only alias is not a
2807
+ writable attribute.
2808
+
2809
+ Both storage modes now use the enriched compiled field definitions. Canonical
2810
+ registration previously persisted raw authored definitions and then overwrote
2811
+ some enriched metadata. It now allocates storage from compiled definitions and
2812
+ attaches its descriptor to the same compiled schema. This preserves types,
2813
+ removed/added fields, search overrides and callbacks without a second schema
2814
+ compilation. Canonical metadata no longer invents an attribute declaration for
2815
+ an implicit resource ID; metadata consumers should use `schemaInfo.idProperty`
2816
+ to identify the resource ID configuration.
2817
+
2818
+ Enrichment runs once at resource registration, once for each canonical
2819
+ `addKnexFields` compilation, and once for a reload that changes the field set.
2820
+ Repeated canonical `createKnexTable()` refreshes storage metadata without
2821
+ rerunning enrichment or restoring removed search fields. Deep registry allocation
2822
+ does not update a running resource's compiled schema; use the resource helper or
2823
+ `reloadSchema()`. Additions survive restarts without being declared again, but
2824
+ persisted JSON cannot restore functions: declare fields with callbacks in source,
2825
+ or servers hide them. Ordinary `addKnexFields` retains its DDL-only behavior.
2826
+
2827
+ Fastify now resolves the current cached request contract when validating a
2828
+ payload. A belongs-to alias added through canonical `addKnexFields` can be used
2829
+ in resource POST/PUT/PATCH documents after server startup, as with Express and
2830
+ programmatic calls. The JSON Schema published at route registration remains an
2831
+ initialization snapshot; finalize declarations before generating route-based
2832
+ API documentation.
2833
+
2834
+ ### Computed-field enrichment
2835
+
2836
+ Plugins that derive computed fields from schema metadata should use
2837
+ `computedSchema:enrich` instead of adding fields to `vars.schemaInfo.computed`
2838
+ in `resource:added`. The compiler awaits this hook after attribute and search
2839
+ enrichment, including during canonical field additions. Edit or replace
2840
+ `context.fields`; each definition must have `computed: true` and a `type`,
2841
+ and any supplied `compute` callback must be a function. Computed names cannot
2842
+ collide with stored attributes.
2843
+
2844
+ This validation also applies to authored computed fields. `compute: false`,
2845
+ `0`, `null` and `''` previously registered successfully but silently omitted
2846
+ the field from output. They now fail registration with `invalid compute
2847
+ function`. Supply a function, or omit `compute` when an `enrichAttributes`
2848
+ hook intentionally supplies the value.
2849
+
2850
+ ```js
2851
+ await api.customize({
2852
+ hooks: {
2853
+ 'computedSchema:enrich': {
2854
+ functionName: 'derive-product-summary',
2855
+ handler: ({ context }) => {
2856
+ if (context.scopeName !== 'products') return
2857
+ context.fields.summary = {
2858
+ type: 'string',
2859
+ computed: true,
2860
+ dependencies: ['name'],
2861
+ compute: ({ attributes }) => `Product: ${attributes.name}`
2862
+ }
2863
+ }
2864
+ }
2865
+ }
2866
+ })
2867
+ ```
2868
+
2869
+ The context also exposes `originalFields` (authored computed declarations),
2870
+ `schemaStructure`, `searchSchemaStructure`, `schemaRelationships`, `idProperty`, `scopeName` and
2871
+ `scopeOptions` for inspection. Change stored fields in `schema:enrich` and
2872
+ search fields in `searchSchema:enrich`; the computed hook's other structures
2873
+ are inspection inputs, not an alternative configuration mutation API.
2874
+ Definitions are shallow copies, not deeply immutable objects.
2875
+
2876
+ LabelPlugin now uses this stage. Automatic labels survive canonical field
2877
+ additions and use the newly compiled search/preference candidates. Explicit
2878
+ stored or computed labels remain authoritative. If computed enrichment or
2879
+ validation fails during canonical `addKnexFields`, the candidate is rejected
2880
+ before field allocation; existing compiled metadata and records remain usable.
2881
+
2882
+ ### Existing canonical data
2883
+
2884
+ Registration now rejects removal or remapping of an existing stored field when
2885
+ the resource has records. The error starts with `Canonical storage migration
2886
+ required` and identifies the field. An owned registration rolls back metadata
2887
+ changes; a borrowed registry transaction remains the caller's responsibility.
2888
+ This guard prevents a changed schema order or newly effective enrichment hook
2889
+ from silently reading one field's existing slot as another field.
2890
+
2891
+ Before upgrading an app with existing canonical data:
2892
+
2893
+ 1. Inspect its schema hooks and compare their final field types with persisted
2894
+ field definitions and `canonicalFieldMap`.
2895
+ 2. Keep every existing stored field in the declaration. For changes that retain
2896
+ the same storage types, preserve existing slot assignments with an explicit
2897
+ complete `canonicalFieldsMap`; allocate unused slots for new fields.
2898
+ 3. If a field needs a different slot pool, a new value encoding or removal,
2899
+ migrate and verify its data and metadata explicitly before registering that
2900
+ changed layout. A slot map cannot convert values between storage types.
2901
+ 4. Restart with the expanded schema, callbacks and slot map, then verify old
2902
+ records, new writes, filters and relationship linkage.
2903
+
2904
+ Registration is not a data migration. The layout guard does not verify semantic
2905
+ changes to callbacks or values that retain the same physical slot. Empty
2906
+ resources can change layout without this guard rejecting registration.
2907
+
2908
+ Older canonical registrations could also allocate an undeclared numeric field
2909
+ named after a custom `idProperty`. That synthetic field is no longer allocated.
2910
+ If its removal triggers the guard, inspect the old slot and metadata in your
2911
+ data migration. Remove its field configuration/schema entry only after proving
2912
+ the slot has no values to preserve, and retain every real field's slot map.
2913
+ Do not add a fake attribute merely to retain the unused allocation.
2914
+
2915
+ ## Write hooks and the returned response
2916
+
2917
+ POST, PUT and PATCH prepare their response before committing an owned
2918
+ transaction. `finish`, then `finishPost`/`finishPut`/`finishPatch`, are the last
2919
+ hooks that can customize `context.responseRecord`. The library then normalizes
2920
+ schema-declared values and reapplies requested sparse fieldsets to full output,
2921
+ including related records. A finish hook cannot expose an excluded field by
2922
+ adding it after the nested GET. Minimal responses retain their resource type
2923
+ and ID; `returning: 'none'` still returns undefined.
2924
+
2925
+ The returned minimal/full response is now a detached snapshot. `afterCommit`
2926
+ is awaited for external side effects, but mutations to its context or response
2927
+ do not rewrite the prepared result. Move response changes from `afterCommit`
2928
+ into the appropriate finish hook. For example, JSON:API response metadata can be
2929
+ added with:
2930
+
2931
+ ```js
2932
+ await api.customize({
2933
+ hooks: {
2934
+ finishPost: {
2935
+ functionName: 'add-write-metadata',
2936
+ handler: ({ context }) => {
2937
+ if (context.format !== 'jsonapi' || context.returning !== 'full') return
2938
+ context.responseRecord.meta = { requestId: context.requestId }
2939
+ }
2940
+ }
2941
+ }
2942
+ })
2943
+ ```
2944
+
2945
+ Use data in response extensions and metadata, not functions or live application
2946
+ objects. The snapshot uses `structuredClone`; a non-cloneable extension rejects
2947
+ before commit, with owned transactions rolled back and borrowed transactions
2948
+ left to their owner. This does not add validation of arbitrary JSON attributes
2949
+ or metadata. Schema-declared temporal fields still use their existing conversion
2950
+ rules. A failing `afterCommit` hook still rejects after the database has committed;
2951
+ the error reports `committed`.
2952
+
2953
+ POST, PUT and PATCH clear any ID left by a previous operation before processing
2954
+ hooks run. POST's generated ID becomes available as `context.id` at
2955
+ `afterDataCallPost`, and remains available to later hooks and the nested response
2956
+ GET. Before insertion, a caller-supplied ID is available in
2957
+ `context.inputRecord.data.id` after input conversion. PUT and PATCH assign their
2958
+ validated target ID before `beforeSchemaValidate`. Early processing hooks can
2959
+ inspect the current request in `context.params`; they must not use a previous
2960
+ operation's ID.
2961
+
2962
+ ## Direct imports of the setter helper
2963
+
2964
+ Resource declarations and setter callback arguments keep their existing shape.
2965
+ If application code directly imported `applyFieldSetters` from the internal
2966
+ resource-method `common.js`, update its call:
2967
+
2968
+ ```js
2969
+ // Before: assign the helper's returned attributes.
2970
+ context.inputRecord.data.attributes = await applyFieldSetters(
2971
+ context.inputRecord.data.attributes, context.schemaInfo, context, api, helpers
2972
+ )
2973
+
2974
+ // Now: the helper assigns the transformed attributes to this write context.
2975
+ await applyFieldSetters(context, api, helpers)
2976
+ ```
2977
+
2978
+ The helper returns no value. It runs setters at the same point, after validation
2979
+ and before storage, without completing the transaction. Applications using
2980
+ public resource calls need no change for this internal simplification.
2981
+ The [lifecycle guide](13-hooks-and-lifecycle.md)
2982
+ documents the retained hooks and their ordering.
2983
+
2984
+ ## Application rollout and experimental features
2985
+
2986
+ Coordinate dependency versions, consumer repositories, generated code and seeds
2987
+ before upgrading applications. Run their actual workflows against the intended
2988
+ package set. Library-side verification does not establish consumer compatibility.
2989
+
2990
+ ### JSKIT repositories
2991
+
2992
+ JSKIT's `Document` / `Documents` repository methods keep their existing public
2993
+ names and JSON:API responses. Repository callers continue to pass plain payloads
2994
+ and `{ trx, context }` options. The shared implementation selects `data` for
2995
+ writes, `format: 'jsonapi'` for documents, and `returning: 'full'` for returned
2996
+ write attributes. DELETE explicitly selects `returning: 'none'`.
2997
+
2998
+ Custom repositories calling the library directly need the same changes. Plain
2999
+ collection reads return an envelope with a `data` array; plain single-resource
3000
+ reads and full writes return the record itself. When a foreign-key field has a
3001
+ relationship alias, plain library input uses that alias. For example,
3002
+ `authorId: { belongsTo: 'authors', as: 'author' }` accepts `data: { author: id }`.
3003
+ JSKIT's shared repository maps its domain payload's `authorId` to `author` and
3004
+ rejects payloads that supply both names.
3005
+
3006
+ Copied repositories can reuse the shared CRUD implementation and retain their
3007
+ application-specific methods. Here `lockDocumentById` is the application's
3008
+ existing scoped lock method, kept in the same module:
3009
+
3010
+ ```js
3011
+ import { createCrudJsonApiRepository } from '@jskit-ai/crud-core/server/jsonApiRepository'
3012
+
3013
+ const repository = {
3014
+ ...createCrudJsonApiRepository({ api, resource, resourceScopeName }),
3015
+ lockDocumentById
3016
+ }
3017
+ ```
3018
+
3019
+ Preserve custom scoping, locks and write normalization when removing copied
3020
+ code. Standard document writes return full documents; the shared repository
3021
+ does not forward a `returning` option. A custom minimal-response path still
3022
+ needs an explicit resource call with `data`, `format: 'jsonapi'` and
3023
+ `returning: 'minimal'`.
3024
+
3025
+ Use the repository's transaction owner for operations involving JSON REST:
3026
+
3027
+ ```js
3028
+ // Before: a raw Knex owner surrounded JSON REST repository writes.
3029
+ await knex.transaction(async trx => {
3030
+ await repository.createDocument(payload, { trx, context })
3031
+ })
3032
+
3033
+ // After: the repository delegates ownership to api.transaction.
3034
+ await repository.withTransaction(async trx => {
3035
+ const document = await repository.createDocument(payload, { trx, context })
3036
+ await trx('audit_entries').insert({ record_id: document.data.id })
3037
+ })
3038
+ ```
3039
+
3040
+ Pass `trx` explicitly to every participating repository call. The transaction
3041
+ remains callable for raw SQL; return normally to commit and throw to roll back.
3042
+ Keep independent raw Knex transaction helpers for integrations that perform
3043
+ only SQL. They do not need to become JSON REST consumers.
3044
+
3045
+ Put an allocation retry around the complete transaction. For example, the
3046
+ application-owned `allocateCode(trx)` below computes a candidate using only SQL;
3047
+ `isCodeAllocationConflict` identifies its exact unique constraint from the
3048
+ underlying driver error. Other uniqueness failures must escape the loop.
3049
+
3050
+ ```js
3051
+ import { findDuplicateEntryError } from '@jskit-ai/database-runtime/shared'
3052
+
3053
+ async function createWithAllocatedCode(payload, context) {
3054
+ for (let attempt = 1; attempt <= 3; attempt++) {
3055
+ try {
3056
+ return await repository.withTransaction(async trx => {
3057
+ const code = await allocateCode(trx)
3058
+ return repository.createDocument({ ...payload, code }, { trx, context })
3059
+ })
3060
+ } catch (error) {
3061
+ const duplicate = findDuplicateEntryError(error)
3062
+ if (attempt === 3 || error?.transactionOutcome !== 'rolledBack' ||
3063
+ !duplicate || !isCodeAllocationConflict(duplicate)) {
3064
+ throw error
3065
+ }
3066
+ }
3067
+ }
3068
+ }
3069
+ ```
3070
+
3071
+ Each attempt gets a new transaction and recomputes the candidate. Do not catch
3072
+ and retry `standard()` inside a failed CRUD lifecycle transaction, or restart a
3073
+ transaction supplied by another owner. Keep external effects outside this
3074
+ retrying unit unless the application can safely repeat or reconcile them.
3075
+
3076
+ Application-owned uploads need the same outcome check before compensation.
3077
+ This example creates one new object used only by this operation. `uploads.save`
3078
+ and `uploads.remove` are the application's storage integration; the caller
3079
+ supplies its own mutable `diagnostics = { cleanupErrors: [] }`:
3080
+
3081
+ ```js
3082
+ async function createWithUpload(payload, file, context, diagnostics) {
3083
+ const uploaded = await uploads.save(file)
3084
+ diagnostics.uploaded = uploaded
3085
+ try {
3086
+ return await repository.withTransaction(trx =>
3087
+ repository.createDocument(
3088
+ { ...payload, fileUrl: uploaded.url },
3089
+ { trx, context }
3090
+ )
3091
+ )
3092
+ } catch (error) {
3093
+ if (error?.transactionOutcome === 'rolledBack') {
3094
+ try {
3095
+ await uploads.remove(uploaded.key)
3096
+ } catch (cleanupError) {
3097
+ diagnostics.cleanupErrors.push({ key: uploaded.key, error: cleanupError })
3098
+ }
3099
+ }
3100
+ throw error
3101
+ }
3102
+ }
3103
+ ```
3104
+
3105
+ The original write error and its outcome still reach the caller; secondary
3106
+ cleanup failures and the uploaded object's identity remain in the application's
3107
+ diagnostics. Other outcomes retain the object for application reconciliation.
3108
+ In particular, deleting it after a committed or unknown failure could break a
3109
+ stored record. Library-managed file
3110
+ uploads already register rollback cleanup; do not add this second cleanup path
3111
+ for those files. See [file ownership](26-file-uploads.md#ownership-after-commit).
3112
+
3113
+ JSKIT copies the outer context for each library operation; hooks inside that
3114
+ operation share the resulting object. Its `visibilityContext` and `scopeValues`
3115
+ maps are copied, while other nested application objects retain their references.
3116
+ Use those maps for user/workspace scoping. A nested operation needs its own
3117
+ working context and explicit transaction participation. Remove hooks that repair
3118
+ dates or query results already normalized by v2; keep application-specific
3119
+ serializers and policies.
3120
+
3121
+ Custom HTTP adapters must translate the library's typed error codes and resource
3122
+ subtypes into status codes, in addition to preserving `transactionOutcome`.
3123
+ Library errors need not carry an HTTP `statusCode`. Validation is 422, missing
3124
+ resources 404, forbidden resources 403, and conflicts 409. Client retry policies
3125
+ must inspect the outcome: `pending`, `committed` and `unknown` are not permission
3126
+ to replay a write, even if another copied error field resembles a retryable CSRF
3127
+ failure. An application-defined retry callback owns its own policy.
3128
+
3129
+ The JSKIT migration uses Node 24 for verification and requires Node 24 or newer
3130
+ for packages that load v2. Upgrade the framework packages and lockfile together,
3131
+ rebuild the catalog and distributed agent docs from authored source patterns, and check
3132
+ custom app repositories and hooks separately. Passing JSKIT tests does not port
3133
+ Vibe64, its seeds or other applications. Those remain separate migration steps.
3134
+
3135
+ The local dispatcher preserves null/undefined throws; see [transaction outcomes](20-transaction-outcomes.md).
3136
+ [Positioning](31-positioning.md) now coordinates writes through database locks;
3137
+ its chapter describes tested concurrency and remaining storage requirements.
3138
+
3139
+ ### Positioning coordination and retired placeholders
3140
+
3141
+ Existing resource calls using `beforeId`, `null` for append and `'FIRST'` continue
3142
+ to work. PostgreSQL and MySQL writers serialize through a transaction-owned
3143
+ coordinator row, including empty groups and moves between groups. SQLite can
3144
+ report busy conflicts; stale transaction snapshots can reject. Applications must
3145
+ retry a failed move in a fresh operation/transaction when appropriate.
3146
+
3147
+ Installation creates `json_rest_api_positioning_locks` if absent. Provision the
3148
+ table before deployment when the runtime account lacks DDL permission, and keep
3149
+ all cooperating writers on the same table. The [positioning guide](31-positioning.md)
3150
+ contains its schema and collation requirements.
3151
+
3152
+ - Changing a group without `beforeId` now places the record at the configured
3153
+ beginning/end of its destination group instead of retaining a potentially
3154
+ duplicate key. Same-group updates preserve their position.
3155
+ - PUT creates receive positions, and PUT replacements preserve or recalculate
3156
+ their managed position. Other persisted fields still follow complete PUT
3157
+ replacement rules.
3158
+ - Missing/out-of-group targets append; a self-targeted move stays in place.
3159
+ Explicit targets now use normal GET permissions and visibility.
3160
+ - Default query ordering uses the supported query hook. Bytewise collation on
3161
+ position columns is required for ordinary sorting and cursor pagination;
3162
+ the allocator's binary comparisons do not change those query semantics.
3163
+ - Remove `rebalanceThreshold`; it never caused automatic rebalancing and now
3164
+ rejects. The no-op `api.positioning.reorder()` method has been removed. Use
3165
+ ordinary PATCH calls or atomic bulk PATCH for actual changes.
3166
+ - Existing keys must be valid fractional keys. Key exhaustion rejects before
3167
+ exceeding declared storage length; migrate/rebalance the data deliberately.
3168
+ - Deep imports of `calculatePosition()` now take `(previousPosition, nextPosition)`
3169
+ string bounds. The former array/ID arguments are no longer supported.
3170
+
3171
+ ## Native query builders and explicit custom-filter translation
3172
+
3173
+ Both storage modes now give query hooks a native Knex builder. Canonical storage
3174
+ no longer intercepts Knex methods to guess logical field/table names. This removes
3175
+ the old query proxy, its alias/descriptor traversal and its partial emulation of
3176
+ Knex methods. Keep the supplied scoped builder (or clone it) and use explicit
3177
+ column/value translation. Native Knex joins, callbacks, aliases, aggregates, raw
3178
+ bindings and cloning retain Knex's own behavior.
3179
+
3180
+ Custom search filters now receive a third argument:
3181
+
3182
+ ```js
3183
+ customRank: {
3184
+ type: 'number',
3185
+ applyFilter(query, input, { column, value }) {
3186
+ query.where(column('rank'), '>=', value('rank', input))
3187
+ }
3188
+ }
3189
+ ```
3190
+
3191
+ Replace `query.where('rank', '>=', input)` with this form when `rank` is a logical
3192
+ resource field. `column(field)` returns a qualified physical column, including
3193
+ canonical slots or an explicitly mapped ordinary column. `value(field, input)`
3194
+ applies that field's storage conversion/custom serializer. The validated public
3195
+ filter input remains the second argument; it is not automatically reinterpreted
3196
+ as another field's stored value. The details object also supplies `context` and
3197
+ `scopeName`. The callback's `this` remains the same builder as its first argument.
3198
+
3199
+ For a declared joined filter such as `actualField: 'groups.code'`, use
3200
+ `column('groups.code')` and `value('groups.code', input)`. These helpers resolve
3201
+ the declared path's prepared alias; they do not create arbitrary additional
3202
+ joins. Existing related-query permission checks still run. Custom SQL continues
3203
+ to own any extra relationships or visibility semantics it introduces. Callbacks
3204
+ run synchronously inside the current predicate group and must mutate that
3205
+ builder; returned builders/promises are not awaited or selected.
3206
+
3207
+ For `knexQueryFiltering` hooks, the existing
3208
+ `createStorageAdapterUtilities({ context })` helper supplies
3209
+ `translateColumn(scopeName, field)` and `translateFilterValue(scopeName, field,
3210
+ input)`. Keep using `context.knexQuery.tableName` as the active SQL alias and
3211
+ `context.knexQuery.storageAdapter` for resource storage operations. The legacy
3212
+ `context.knexQuery.adapter` now refers to that same storage adapter in both
3213
+ modes; it is no longer a query-method proxy. Removed private proxy state includes
3214
+ `__adapter__`, its related-descriptor/alias maps, and
3215
+ `context.resourceToTableName`. Importing the removed internal
3216
+ `anyapi-query-adapter.js` module is no longer supported.
3217
+
3218
+ A filtering hook can translate a local field without importing another helper:
3219
+
3220
+ ```js
3221
+ handler({ context }) {
3222
+ const { query, tableName, storageAdapter } = context.knexQuery
3223
+ const rankColumn = `${tableName}.${storageAdapter.translateColumn('rank')}`
3224
+ query.where(rankColumn, '>=', storageAdapter.translateFilterValue('rank', 10))
3225
+ }
3226
+ ```
3227
+
3228
+ Apply this predicate to the supplied query, including count invocations. Keep
3229
+ using declared cross-resource search paths where possible; writing a native join
3230
+ does not automatically add the target resource's permission or visibility checks.
3231
+
3232
+ The storage adapter's `buildBaseQuery({ transaction, tableAlias })` constructs a
3233
+ native builder with the optional alias and retains canonical tenant/resource
3234
+ predicates qualified by that alias. It borrows the database handle and does not
3235
+ complete transactions. Translate every logical identifier used in custom SQL;
3236
+ raw SQL text and binding strings are not rewritten. Consumer repositories have
3237
+ not been migrated by this library-only change.
3238
+
3239
+ ## Storage helpers consume prepared write attributes
3240
+
3241
+ Ordinary PUT/PATCH no longer re-extract belongs-to values from the request's
3242
+ relationship object. Core relationship processing and setters already prepare
3243
+ `context.inputRecord.data.attributes` before calling storage, as they do for
3244
+ POST and canonical writes. Resource API calls need no migration for this change.
3245
+ Custom storage implementations should consume those prepared attributes before
3246
+ applying physical storage mapping, preserving setter results.
3247
+
3248
+ A custom `dataPost` helper must return a nonempty string, a finite number, or a
3249
+ bigint ID. The lifecycle converts bigint IDs to strings without losing precision.
3250
+ Invalid helper results reject before `afterDataCall` hooks and roll back the
3251
+ write, including with `returning: 'none'` or `'minimal'`. Resource `normalizeId`
3252
+ is not invoked again on the helper's result.
3253
+
3254
+ The unexported internal `plugins/core/lib/writing/knex-json-api-transformers-writing.js`
3255
+ module and its `processBelongsToRelationships` helper have been removed. If you
3256
+ imported that file directly, remove the second relationship conversion and use
3257
+ the prepared attributes supplied to your storage callback. Public relationship
3258
+ validation and authorization remain in the resource lifecycle.
3259
+
3260
+ ## Pass explicit field maps to foreign-key inspection
3261
+
3262
+ The internal `getForeignKeyFields(fields, relationships)` helper accepts a field
3263
+ map. If an extension passes a schema instance, change the call to
3264
+ `getForeignKeyFields(schema.structure, relationships)`. Library callers already
3265
+ pass `schemaInfo.schemaStructure` or an enrichment field map.
3266
+
3267
+ The helper no longer guesses whether a property named `structure` is a wrapper.
3268
+ That guess misread a legitimate attribute named `structure`, hid relationship
3269
+ backing fields and could bypass the prohibition on identity serializers. Public
3270
+ resource declarations can continue to use `structure` as an attribute name.
3271
+
3272
+ ## Table helper schemas use an explicit structure wrapper
3273
+
3274
+ Direct calls to `createKnexTable`, `addKnexFields`, `alterKnexFields`,
3275
+ `generateKnexMigration` and `generateKnexMigrationDiff` now require a schema
3276
+ object whose `structure` property contains field definitions. A
3277
+ `json-rest-schema` schema instance already supplies that property and remains
3278
+ accepted. Field definitions must be objects.
3279
+
3280
+ Change bare field maps at the schema argument:
3281
+
3282
+ ```js
3283
+ // Before
3284
+ await alterKnexFields(knex, 'books', { title: { type: 'string' } })
3285
+
3286
+ // After
3287
+ await alterKnexFields(knex, 'books', {
3288
+ structure: { title: { type: 'string' } }
3289
+ })
3290
+ ```
3291
+
3292
+ For helpers that accept table metadata, keep `storage`, `indexes`, `foreignKeys`
3293
+ and `checkConstraints` beside `structure`. Fields with those names belong inside
3294
+ `structure`, like any other field. Add/alter helpers still reject top-level table
3295
+ constraints they cannot apply; use a migration diff for those changes.
3296
+
3297
+ Public resource calls such as `books.addKnexFields({ fields })`,
3298
+ `books.alterKnexFields({ fields })` and `books.createKnexTable()` keep their current
3299
+ arguments. The plugin supplies the explicit helper schema internally.
3300
+
3301
+ Bare maps are rejected before SQL or migration output is produced. This removes
3302
+ the ambiguity that could interpret a field named `structure` as a wrapper and
3303
+ generate columns from its definition's `type` and `nullable` properties.
3304
+
3305
+ ## Read helpers use compiled field maps
3306
+
3307
+ Internal response, include and field-selection helpers now read
3308
+ `scope.vars.schemaInfo.schemaStructure` directly. Extensions supplying synthetic
3309
+ resource metadata must provide that field map alongside `schemaRelationships`;
3310
+ `schemaInstance` is the validation object, not an alternate field-map argument.
3311
+ Normal registered resources already provide both compiled values.
3312
+
3313
+ The internal `getSchemaStructure` export from `knex-constants.js` is removed.
3314
+ Use the resource's compiled `schemaStructure`, or explicitly access `.structure`
3315
+ on a schema instance you own. No wrapper guessing or compatibility adapter is
3316
+ needed.
3317
+
3318
+
3319
+ ## Schema compilation snapshots declarations
3320
+
3321
+ Resource schema compilation now copies declaration data on entry and again when
3322
+ publishing its result. Changing an original field's nested `storage`, validation,
3323
+ relationship or dependency declaration after registration no longer changes the
3324
+ currently compiled schema. Enrichment hooks may mutate or replace their supplied
3325
+ fields during compilation; retaining that object and editing it after the hook
3326
+ pipeline finishes no longer changes published metadata.
3327
+
3328
+ Declare changes before registration, or use the supported schema operation such
3329
+ as canonical `addKnexFields()` while application operations are stopped. Do not
3330
+ use mutations of `resource.vars.schemaInfo` as a configuration API. Each explicit
3331
+ compilation uses its supplied input; this does not add automatic invalidation,
3332
+ concurrent reconfiguration, or a general resource-customization API.
3333
+
3334
+ Callbacks retain their identity and may still access application state through
3335
+ closures. Plain objects, arrays, dates, regular expressions and nested schema
3336
+ instances are copied; nested schemas retain their registered type/validator
3337
+ handlers and operation definitions. Other class instances remain application-owned
3338
+ references. The library does not freeze caller-owned objects or attempt to clone
3339
+ external services. This boundary applies to compiled declarations; it does not
3340
+ turn all runtime options, hook variables or application state into immutable data.
3341
+
3342
+
3343
+ Canonical registration also keeps an owned copy of declarations for later field
3344
+ additions. Changing the original registration object, or a previous
3345
+ `addKnexFields({ fields })` input, does not silently change existing fields during
3346
+ a subsequent addition. Pass new declarations to the supported operation instead.
3347
+
3348
+ Initial `sortableFields` and `defaultSort` arrays are copied into resource
3349
+ variables. Editing the original arrays no longer changes query behavior. An
3350
+ explicit assignment such as `resource.vars.defaultSort = ['-name']` still affects
3351
+ subsequent requests; this is a runtime variable override, not a schema change.
3352
+ It does not recompile fields or change their query eligibility.
3353
+
3354
+
3355
+ ## Plugin registries require explicit entries
3356
+
3357
+ Named row policies, autofilter resolvers and autofilter presets must be registered
3358
+ as own entries of their configuration maps. Inherited names such as `constructor`
3359
+ or `toString` no longer count as registered entries; resource registration rejects
3360
+ them as unknown. Autofilters also require an actual declared schema field rather
3361
+ than accepting inherited object properties as fields.
3362
+
3363
+ Explicitly registered policy, resolver and preset names remain valid, including
3364
+ prototype-like names. Use computed properties or `Object.fromEntries()` if a map
3365
+ key is `__proto__`, so it is an own entry rather than JavaScript object-literal
3366
+ prototype syntax. Explicit resolver names are also retained in plugin
3367
+ introspection. Policy and resolver callbacks still run per request; their return
3368
+ values and authorization results are not cached by this change.
3369
+
3370
+
3371
+ ## When configuration changes take effect
3372
+
3373
+ | Configuration | Effective boundary | How to change it |
3374
+ | --- | --- | --- |
3375
+ | 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. |
3376
+ | 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. |
3377
+ | 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. |
3378
+ | 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. |
3379
+ | 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. |
3380
+ | 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. |
3381
+ | 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. |
3382
+
3383
+ `api.customize()` is not a schema replacement API. Arbitrary writes to
3384
+ `scopeOptions`, compiled `schemaInfo`, descriptors or canonical maps are not
3385
+ supported configuration operations. Structural changes outside the supported
3386
+ addition path need updated source declarations, any required data/schema migration,
3387
+ and resource initialization. Concurrent structural reconfiguration is unsupported.
3388
+ There is no automatic fingerprinting of mutable metadata or cache of caller
3389
+ permissions, resolver values or query results.
3390
+
3391
+
3392
+ ## Compiled output definition indexes
3393
+
3394
+ Response normalization now reads `resource.vars.schemaInfo.outputFields` and
3395
+ `outputRelationships`. These are compiled lookup indexes over the existing field,
3396
+ computed, projection and relationship definitions. They are refreshed with the
3397
+ compiled resource, including after supported canonical field additions.
3398
+
3399
+ Ordinary resource calls need no changes. Code that calls the deep
3400
+ `normalizeRecordAttributes()` helper with handcrafted scope metadata must supply
3401
+ these indexes; the helper no longer rebuilds them from `schemaStructure`,
3402
+ `computed`, `queryFields` and `schemaRelationships` for every record. Prefer passing
3403
+ real `api.resources`. For a single attributes object with an explicit definition
3404
+ map, `normalizeAttributes(attributes, definitions)` remains available unchanged.
3405
+ Direct mutation of compiled indexes is not a configuration API.
3406
+
3407
+ ## Compiled foreign-key membership
3408
+
3409
+ Code constructing synthetic `schemaInfo` for deep query/conversion helpers must
3410
+ provide `foreignKeyFields`, a `Set` of logical backing-field names for ordinary
3411
+ and polymorphic relationships. Obtain this metadata from the registered resource
3412
+ where possible. Compilation publishes the set with the resource; do not mutate
3413
+ it or add resource IDs to it. Make a local copy when extending identity fields.
3414
+ Supported canonical field additions publish a replacement set automatically.
3415
+ The public resource call syntax is unchanged by this optimization.
3416
+
3417
+ Conversion no longer reads relationship declarations to reconstruct polymorphic
3418
+ backing-field membership. Supply both type and ID backing fields in the compiled
3419
+ set when constructing synthetic metadata. The conversion error boundary wraps
3420
+ failures while reading that set, retaining typed errors and original causes.
3421
+
3422
+ ## Relationship aliases must be unique
3423
+
3424
+ Each relationship name must have exactly one declaration. Two `belongsTo` fields
3425
+ cannot share an `as` name, and a field's `as` cannot also appear in the resource's
3426
+ `relationships` map. Previously these configurations were accepted but different
3427
+ operations could choose different definitions or cardinalities.
3428
+
3429
+ Give distinct relationships distinct names, for example `authorId` with
3430
+ `as: 'author'` and `reviewerId` with `as: 'reviewer'`. Declare a belongs-to
3431
+ relationship on its backing field; remove a duplicate declaration from the
3432
+ `relationships` map. Update payload and include names if you rename an alias.
3433
+ The compiler checks enriched declarations too. A rejected canonical field
3434
+ addition retains the previously published resource and its stored data.
3435
+
3436
+ ## Derived attributes cannot declare relationships
3437
+
3438
+ Computed fields and SQL query projections cannot declare `belongsTo` or
3439
+ `belongsToPolymorphic`. These fields produce attribute values and do not provide
3440
+ the stored backing fields required by relationship operations. Previously the
3441
+ compiler accepted these options and could produce inconsistent relationship
3442
+ lookup and output normalization. The same rejection applies after enrichment.
3443
+
3444
+ Remove relationship options from derived attributes. Declare an ordinary
3445
+ relationship on its stored foreign-key field using `belongsTo` and `as`; declare
3446
+ a polymorphic relationship in the resource's `relationships` map, identifying its
3447
+ stored type and ID fields. Give a derived display value its own attribute name.
3448
+ A rejected canonical addition keeps the previously published schema intact.
3449
+
3450
+ ## Compiled relationship lookup and polymorphic declaration location
3451
+
3452
+ `findRelationshipDefinition()` now reads the registered resource's
3453
+ `schemaInfo.outputRelationships` index. Deep-helper callers constructing synthetic
3454
+ metadata must supply that index, including both ordinary belongs-to aliases and
3455
+ declared relationships. Entries reference the existing definitions. Prefer the
3456
+ compiled metadata on `api.resources` and do not mutate its lookup index. The
3457
+ helper returns null for absent names, including inherited object names.
3458
+
3459
+ Declare `belongsToPolymorphic` in the resource's `relationships` map, with its
3460
+ `typeField` and `idField` pointing to stored schema fields. Placing it inside an
3461
+ individual stored field is now rejected, including after schema enrichment and
3462
+ when adding canonical fields. Previously that placement was accepted but lookup
3463
+ could not find the relationship and its backing fields were not recognized.
3464
+ Move the relationship definition into the map; keep its backing fields in
3465
+ `schema`. No stored data conversion is needed merely to move the declaration.
3466
+
3467
+ ## Request contracts reuse compiled relationship metadata
3468
+
3469
+ Deep callers of `getRequestContracts()` constructing synthetic `schemaInfo` must
3470
+ provide `foreignKeyFields` and `outputRelationships` along with their stored and
3471
+ search schema metadata. Normal registered resources already provide these facts.
3472
+ The foreign-key set excludes relationship backing fields from writable attribute
3473
+ contracts; the relationship index supplies linkage cardinalities and target types.
3474
+ There is no fallback that reconstructs these indexes from raw declarations.
3475
+
3476
+ For an ordinary belongs-to relationship, its `belongsTo` value determines the
3477
+ allowed resource type, consistent with relationship operations. An additional
3478
+ `target` property does not override it. Use `target` for has-one/has-many and
3479
+ many-to-many declarations. Existing public call syntax and the one-retained-variant
3480
+ request-contract cache are unchanged by this refactor.
3481
+
3482
+ ## File upload rules follow compiled resource configuration
3483
+
3484
+ File handling now reads the compiled stored fields, including MIME/size rules
3485
+ produced by `schema:enrich`. Mutating the original declaration after registration
3486
+ no longer changes those rules. Installing the file plugin after resource
3487
+ registration also discovers existing compiled file fields. Supported canonical
3488
+ field additions refresh the file-field list when the compiled resource changes;
3489
+ a rejected addition keeps the previous rules, including an empty file-field list.
3490
+
3491
+ File storage backends are external handles. A file field's `storage` object with
3492
+ an `upload()` method retains its identity and method receiver during declaration
3493
+ snapshotting, so backend state and intentional method replacement continue to
3494
+ work. MIME and size declarations are still owned snapshots. Ordinary column
3495
+ mapping objects without `upload()` remain copied declarations. Directly mutating
3496
+ published compiled metadata is unsupported.
3497
+
3498
+ ## New compiler hook: `schema:compiled`
3499
+
3500
+ Plugins can derive resource metadata in `schema:compiled`. It receives
3501
+ `context.scopeName`, `context.scopeOptions` and `context.schemaInfo` after core
3502
+ field, relationship, storage, query and dependency compilation. Read the compiled
3503
+ facts and attach plugin-owned metadata under a distinct key on `schemaInfo`.
3504
+ Change stored, search, computed or projected declarations in their existing
3505
+ enrichment hooks; core compiled facts are read-only at this stage.
3506
+
3507
+ The candidate is published only after all handlers succeed, using an owned
3508
+ snapshot. Retained candidate objects cannot mutate the published metadata.
3509
+ Throwing rejects registration or a canonical field-addition candidate before it
3510
+ replaces the resource. This hook runs on compilation, not on ordinary requests
3511
+ or a descriptor-only refresh.
3512
+
3513
+ Autofilter uses this hook to validate and derive its configuration on every
3514
+ compilation. Resolver metadata and write stamping follow enriched relationship
3515
+ aliases after canonical additions; removing a required autofilter field rejects
3516
+ the candidate. `api.autofilter.getScopeConfig(name)` is unchanged. Deep readers of
3517
+ `resource.vars.autofilter` must move to `resource.vars.schemaInfo.autofilter` or
3518
+ prefer the public inspection helper. The root registry summary remains available
3519
+ through `api.autofilter.getConfig()`. No forwarding metadata copy is kept.
3520
+
3521
+ ## Include configuration is validated before publication
3522
+
3523
+ Include configuration is now checked from the compiled relationship definitions,
3524
+ including enrichment, before a resource is published or a canonical field
3525
+ addition commits. An explicit resource `queryMaxLimit` takes precedence over the
3526
+ inherited value for this check. Previously the registration stages could reject
3527
+ valid higher overrides or miss enriched limits above a lower resource maximum.
3528
+
3529
+ `include.limit` accepts a non-negative integer, `null` or `false`; omission keeps
3530
+ the existing default. Zero is valid, and `null`/`false` retain their existing
3531
+ unbounded-include meaning. Negative, fractional, NaN and nonnumeric values now
3532
+ fail compilation. `include.orderBy` retains its array form and validates entry
3533
+ syntax during compilation. Target-field resolution and database window-function
3534
+ support remain query-time checks so forward resource references still work.
3535
+ Query-time target caps and selection behavior are unchanged.
3536
+
3537
+ The deep `validateIncludeConfigurations` helper now has a named export and takes
3538
+ the `schema:compiled` candidate context (`scopeName`, `scopeOptions`, `schemaInfo`),
3539
+ not the former scope-added context. Normal plugin users need no call-site change.
3540
+
3541
+ ## Enhancing an existing logger
3542
+
3543
+ The internal `enhanceLogger(logger, options)` utility now safely replaces methods
3544
+ on the supplied logger. Its wrappers capture each original writer and receiver
3545
+ at enhancement time; previously they looked up the replaced method again and
3546
+ could recurse until the stack overflowed. The utility still returns the same
3547
+ logger, preserves writer return values and propagates writer failures. Existing
3548
+ `createEnhancedLogger` wrappers likewise retain the writers captured at creation.
3549
+ Install the desired base writers before creating an enhanced logger.
3550
+
3551
+ ## Error causes in diagnostic output
3552
+
3553
+ Operation failure reports now include these stable fields:
3554
+
3555
+ | Field | Meaning |
3556
+ | --- | --- |
3557
+ | `method` | Resource method, HTTP verb, or named setup/socket operation; null when unavailable |
3558
+ | `scopeName` | Resolved resource name; null before resource resolution or for server-wide operations |
3559
+ | `phase` | Reporting boundary, such as `writeFailure`, `httpError`, `include`, `admission`, or `redisShutdown` |
3560
+ | `backend` | Knex client name, `socketio`, or `redis`; null when unavailable |
3561
+ | `transactionOutcome` | Existing transaction state at reporting time: `none`, `pending`, `committed`, `rolledBack`, or `unknown` |
3562
+
3563
+ `phase` describes where an error is reported. It does not identify every hook
3564
+ that ran before the failure. Existing nested getter, setter, include and
3565
+ relationship-metadata errors retain their more specific context and causes.
3566
+ Cleanup warnings may report `pending` even when the operation later commits.
3567
+ Socket notification permission checks run outside a transaction and report
3568
+ `none`; subscription-matching reads inside a write report its borrowed transaction
3569
+ state. Setup/debug traces are informational and need not contain this envelope.
3570
+
3571
+ Formatted diagnostics now include native `Error.cause` and `AggregateError.errors`
3572
+ properties, which JavaScript normally makes non-enumerable. Nested errors retain
3573
+ their names, messages and selected stacks, including errors inside diagnostic
3574
+ `details`. Primitive causes are retained. Existing depth and cycle limits apply
3575
+ to these nested values, and the original errors are not modified. Consumers that
3576
+ parse diagnostic objects should allow these additional fields. This does not
3577
+ change HTTP error responses or the errors thrown by resource methods.
3578
+
3579
+ ## Shared filter traces omit payloads
3580
+
3581
+ The shared basic-filter trace messages no longer print filter values, entire
3582
+ filter objects or field definitions. They retain scope, table, field and operator
3583
+ information. Filtering behavior and SQL bindings are unchanged. This is a
3584
+ specific trace-output change. See [field-aware diagnostics](#field-aware-write-diagnostics)
3585
+ for structured redaction and its limits.
3586
+
3587
+
3588
+ Ordinary storage operation messages also omit query parameters, POST bodies and
3589
+ PATCH attributes. Upload-completion messages omit stored URLs, and Socket.IO
3590
+ subscription messages omit filters. These changes affect diagnostics only;
3591
+ records, upload results and subscription acknowledgements retain their values.
3592
+
3593
+
3594
+ Include-loader diagnostics now report ID counts rather than identifier arrays.
3595
+ They also omit complete relationship/include definitions and selection lists
3596
+ from the affected load messages. Query construction and returned includes are
3597
+ unchanged.
3598
+
3599
+
3600
+ ## Connector error logs use route templates
3601
+
3602
+ Express and Fastify request-error diagnostics now use the registered route
3603
+ pattern, such as `/countries/:id`, as `path`. They no longer include a raw `url`
3604
+ field or put query strings and concrete resource IDs in that path metadata.
3605
+ Express may omit `path` for errors that occur before a route is matched.
3606
+ HTTP routing and error responses are unchanged. Error-object details are a
3607
+ separate diagnostic payload and may still carry resource identifiers.
3608
+
3609
+ Both connectors await error diagnostics. A failing writer leaves the original
3610
+ HTTP response intact and adds the logging failure to `context.cleanupErrors`
3611
+ with `phase: 'logging'` and `during: 'httpError'`. Express route-registration
3612
+ diagnostics also preserve the original setup rejection if logging fails.
3613
+
3614
+ HTTP diagnostic loggers omit structured `body` and `headers` containers,
3615
+ including on errors raised before a route supplies resource metadata. Known JSON
3616
+ parser failures (`type: 'entity.parse.failed'`) produce a generic diagnostic
3617
+ preview with name, message, type and status. Their raw message, stack, body and
3618
+ cause are omitted because parser messages can quote the request body. Express
3619
+ uses this parser identifier already; the Fastify JSON parser supplies the same
3620
+ identifier. The original error still reaches hooks and HTTP error mapping.
3621
+
3622
+ A read hook that throws `null` or `undefined` produces a generic JSON:API 500
3623
+ response. Conditional PUT and positioning target reads recognize typed
3624
+ not-found errors; other hook failures propagate through the transaction's
3625
+ normal failure handling.
3626
+
3627
+ ## Formatted errors are bounded previews
3628
+
3629
+ `formatError` and `formatErrorString` now produce bounded diagnostic previews:
3630
+ 50 entries per container, 256 serialized-value visits, 2,048 characters per
3631
+ string, and a shared 8,192-character budget for copied text. Existing depth and
3632
+ cycle handling still applies. `[Truncated]` markers identify omitted content;
3633
+ these limits are not a promise of an exact byte length after JSON escaping or
3634
+ pretty printing. The original error and its attached values are unchanged.
3635
+
3636
+ Non-object `toJSON()` results appear under `toJSONResult` rather than being
3637
+ spread into character-index properties. Big integers and symbols in diagnostic
3638
+ values become text, functions become markers, and object-valued error names or
3639
+ messages become a non-string-text marker. These are diagnostic representations,
3640
+ not changes to the errors thrown by the API or the HTTP error contract.
3641
+
3642
+ One-line summaries share the bounded formatter and are capped at 2,048
3643
+ characters, including any truncation marker.
3644
+ Short summary spelling is unchanged. Summaries inspect only message, code,
3645
+ violations and fields; they do not invoke custom `toJSON()` methods.
3646
+
3647
+ ## Enhanced logger events share a preview budget
3648
+
3649
+ Standard enhanced log methods now bound the complete argument list, including
3650
+ message strings and additional diagnostic data. They format at most 50 supplied
3651
+ arguments and use the existing shared depth/traversal/text limits for the emitted
3652
+ event. Truncation is explicit; source arguments are not mutated. The internal
3653
+ `formatDiagnosticValue` helper exposes that same bounded value conversion.
3654
+
3655
+ Validation errors now produce one structured event containing their details.
3656
+ The former extra `Validation error details:` event has been removed. Logger
3657
+ receiver binding, return values and exceptions still come from the original
3658
+ writer. These bounds do not apply to direct calls that bypass the enhanced
3659
+ logger, and bounding is not sensitive-field redaction.
3660
+
3661
+ The storage, positioning and file plugins use this formatter for their own
3662
+ diagnostics too. Include messages report counts instead of complete path
3663
+ collections. These changes preserve logger binding and query/file behavior;
3664
+ they do not add a resource-field policy to every logger.
3665
+
3666
+ ## Field-aware write diagnostics
3667
+
3668
+ The resource write-error logger now derives redacted field names from compiled
3669
+ `outputFields`: both `hidden: true` and `normallyHidden: true` are redacted when
3670
+ that metadata is available. Explicitly requesting a normally-hidden field in a
3671
+ response does not make its diagnostic value visible. Caller input and thrown
3672
+ errors are unchanged.
3673
+
3674
+ The internal enhanced logger and formatter accept `redactFields` for structured
3675
+ payload keys. Matching values become `[Redacted]` before they are read. Validation
3676
+ objects whose `field` is a listed name or `data.attributes.<name>` retain field
3677
+ and rule metadata with a redacted message; their other payload properties are
3678
+ omitted. Summaries use the same policy. Enhanced loggers snapshot the configured
3679
+ field-name list at creation.
3680
+
3681
+ When field redaction is configured or `includeStack: false` is selected,
3682
+ `formatError` inspects ordinary error properties without invoking `toJSON()`.
3683
+ Custom conversion could otherwise read an excluded stack or copy a hidden value
3684
+ under another key before filtering. Unrestricted diagnostics still use custom
3685
+ `toJSON()` results. Nested error-envelope accessors are inspected once through the
3686
+ bounded value formatter; their returned errors use ordinary property inspection.
3687
+
3688
+ This is structured-field redaction, not a scanner for arbitrary secret text in
3689
+ messages. Early failures without compiled metadata cannot infer which application
3690
+ attributes are sensitive. Custom search validators should return general
3691
+ validation messages: their text can become the query error's top-level message,
3692
+ where field-key redaction cannot recognize a value embedded in a sentence.
3693
+
3694
+ The resource-field policy applies to write reports, matched HTTP resource routes,
3695
+ file cleanup, subscription admission/removal, subscription matching and
3696
+ notification permission reports. It uses the resolved resource's compiled fields;
3697
+ it does not guess a resource from a URL, an authentication error or a client-supplied
3698
+ subscription name. Authentication and server-wide reports omit raw handshake,
3699
+ credential and subscription-filter objects. Application properties deliberately
3700
+ attached to an authentication error still appear as bounded diagnostics: a field
3701
+ named `accessKey` there does not inherit an unrelated resource's field policy.
3702
+
3703
+ Buffers and other binary values are represented by type and byte length,
3704
+ including uploaded-file metadata and nested errors. Identifiers, filenames,
3705
+ paths, arbitrary application/driver error text and values copied under unrelated
3706
+ keys are not automatically secret-free. Use general error messages, omit secrets
3707
+ from custom error properties, and apply any broader retention/redaction policy in
3708
+ the application-owned logger. Direct application logger calls do not pass through
3709
+ the library's formatter. The built-in runtime no longer emits upstream method
3710
+ arguments or plugin-option dumps.
3711
+
3712
+ Socket connection/disconnection diagnostics and Redis setup/shutdown diagnostics
3713
+ contain throwing and rejecting writers. They cannot prevent handler installation,
3714
+ subscription acknowledgement or later Redis cleanup attempts. A Redis setup
3715
+ failure retains cleanup failures as secondary `AggregateError` members. The Redis
3716
+ adapter is configured before attaching Socket.IO to the caller's HTTP server, so
3717
+ an adapter setup failure leaves that server's listeners intact. If destruction
3718
+ itself fails, startup reports the failure without waiting indefinitely for the
3719
+ connection attempt that could not be cancelled.
3720
+
3721
+
3722
+ ## Opt-in stored revision migration
3723
+
3724
+ Resources that select `versionField` need an explicit stored string field and a
3725
+ backfill of existing rows before conditional writers are enabled. Follow the
3726
+ [version-field migration guide](22-version-field-migrations.md) for
3727
+ ordinary and canonical storage, a tested backfill example, retry/rollback and
3728
+ readiness checks. Clients submit `expectedVersion` outside record attributes;
3729
+ bulk PATCH/DELETE use aligned `expectedVersions` arrays. These are revision
3730
+ conditions. HTTP representation conditions use the separate contract below.
3731
+
3732
+ ## Opt-in HTTP representation conditions
3733
+
3734
+ The executable client example is `examples/conditional-http-client.js` in the
3735
+ repository. Import `readForConditionalUpdate`, pass the complete resource URL
3736
+ (including its query), edit the returned `document`, then call `save` with a
3737
+ JSON:API update document. Supply application headers and `credentials: 'include'`
3738
+ when your cross-origin session requires them. Each returned object retains the
3739
+ original read's ETag: after saving, read again before another edit. The example
3740
+ returns the write's `Response` so the caller can handle 412 and other errors.
3741
+ Run `node --test tests/http-validator-client-example.test.js` on Node 24 to
3742
+ exercise it against real Express and Fastify HTTP servers.
3743
+
3744
+ HTTP representation conditions require no version column. They compare the
3745
+ selected JSON response, including requested includes,
3746
+ fieldsets, computed values and response-hook changes.
3747
+
3748
+ Enable it when installing either connector, before registering resources:
3749
+
3750
+ ```js
3751
+ await api.use(ExpressPlugin, { mountPath: '/api', httpValidators: true })
3752
+ // Or use FastifyPlugin with the same httpValidators option.
3753
+ ```
3754
+
3755
+ The default is `false`. Successful GET/HEAD responses with a body receive a
3756
+ strong ETag. Preserve the complete quoted header value and send it unchanged in
3757
+ `If-Match` when updating or deleting the same resource. Use the same query string
3758
+ and representation-selecting request headers for the read and write:
3759
+
3760
+ ```js
3761
+ const url = '/api/articles/42?include=author'
3762
+ const selectedHeaders = { Accept: 'application/vnd.api+json' }
3763
+ const current = await fetch(url, { headers: selectedHeaders })
3764
+ if (!current.ok) throw new Error(`Read failed: ${current.status}`)
3765
+ const etag = current.headers.get('ETag')
3766
+ if (!etag) throw new Error('The selected response has no ETag')
3767
+
3768
+ const updated = await fetch(url, {
3769
+ method: 'PATCH',
3770
+ headers: {
3771
+ ...selectedHeaders,
3772
+ 'Content-Type': 'application/vnd.api+json',
3773
+ 'If-Match': etag
3774
+ },
3775
+ body: JSON.stringify({
3776
+ data: { type: 'articles', id: '42', attributes: { title: 'Revised title' } }
3777
+ })
3778
+ })
3779
+ if (updated.status === 412) {
3780
+ // Read again and resolve the competing change before submitting another edit.
3781
+ } else if (!updated.ok) {
3782
+ throw new Error(`Update failed: ${updated.status}`)
3783
+ }
3784
+ ```
3785
+
3786
+ A matching strong tag permits registered resource PUT/PATCH/DELETE. A stale tag
3787
+ returns 412 with code `REST_API_PRECONDITION_FAILED`. Weak tags cannot match;
3788
+ malformed headers return 422. `If-Match: *` checks visible existence only and
3789
+ does not detect intervening edits. Omitting the header leaves writes
3790
+ unconditional. Conditional PUT cannot create a missing resource. Collection
3791
+ POST, relationship mutations and bulk routes currently reject `If-Match` with
3792
+ 422; use their documented direct revision conditions where supported. Normal
3793
+ validation and authorization errors take precedence over comparison. A hidden
3794
+ PUT target rejected by existing resource checks retains 404; a missing target
3795
+ that otherwise permits PUT-create returns 412. These HTTP PUT outcomes are not
3796
+ an identical hidden/missing response contract. This
3797
+ option does not implement `If-None-Match` caching. Stored `expectedVersion`
3798
+ conflicts remain a distinct 409 error.
3799
+
3800
+ For cross-origin browser clients, install `CorsPlugin` after the connector and
3801
+ allow `If-Match` requests and expose `ETag` responses. These arrays replace the
3802
+ defaults, so retain other headers your application needs:
3803
+
3804
+ ```js
3805
+ await api.use(CorsPlugin, {
3806
+ origin: 'https://app.example.com',
3807
+ credentials: true,
3808
+ methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
3809
+ allowedHeaders: [
3810
+ 'Content-Type', 'Authorization', 'X-Requested-With',
3811
+ 'X-HTTP-Method-Override', 'Accept', 'Origin', 'If-Match'
3812
+ ],
3813
+ exposedHeaders: ['X-Total-Count', 'X-Page-Count', 'Link', 'Location', 'ETag']
3814
+ })
3815
+ ```
3816
+
3817
+ Enabling this option makes the connector serialize successful GET/HEAD bodies
3818
+ once with `JSON.stringify` after response hooks and send those exact bytes.
3819
+ Express JSON spacing, replacers and escaping settings, and Fastify custom JSON
3820
+ serializers, do not process those pre-serialized bodies. Move required body
3821
+ transformations into the resource output or transport response hook before
3822
+ enabling it. Middleware that subsequently changes bytes or content encoding
3823
+ must also maintain the corresponding validator. Successful PUT responses emit
3824
+ no validator in this mode; fetch the selected representation again for its tag.
3825
+
3826
+ A strong conditional write renders a speculative GET inside its managed
3827
+ serializable transaction before changing the resource. Response hooks see
3828
+ `context.precondition === true` and normalized GET request metadata during that
3829
+ render, then run separately for the actual write response. Keep rendering
3830
+ deterministic and free of external side effects. Computed database reads must
3831
+ borrow `context.transaction` and preserve the caller's authorization context.
3832
+ Normalized request headers are copied; native request/reply objects and
3833
+ application callbacks are still owned by the actual request. Do not use native
3834
+ response methods while rendering the speculative view. Transaction conflicts
3835
+ currently retain native error handling (HTTP 500), with no automatic retry.
3836
+
3837
+ See the [API reference](../API.md) for conditional request options and
3838
+ [version-field migrations](22-version-field-migrations.md) for database preparation.