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
package/docs/API.md CHANGED
@@ -1,52 +1,236 @@
1
+ ---
2
+ title: "API reference"
3
+ ---
4
+
1
5
  # JSON REST API - Complete API Reference
2
6
 
3
7
  This reference provides comprehensive documentation for all methods, parameters, and features available in the json-rest-api library.
4
8
 
5
- ## Important Notes
9
+ ## Current contract
10
+
11
+ This reference describes the v2 API. See the [migration guide](GUIDE/33-migrating-to-v2.md) and
12
+ [backend limits](GUIDE/30-backend-capabilities.md) before upgrading.
6
13
 
7
- 1. **Context Parameter**: The `context` parameter shown in method signatures is typically managed internally by the framework. When using the API programmatically, you usually don't need to provide it unless you're implementing custom request identity/auth context or other request-specific data.
14
+ Pass method controls in the first `params` object and application information in
15
+ the optional second context object. POST/PUT/PATCH accept exactly one input key:
16
+ `data` for plain resource values, or `document` for a JSON:API document. Both keys
17
+ together are rejected, even if one value is undefined. Direct attribute shorthand
18
+ and the removed `inputRecord` parameter are rejected. A plain field named `data`
19
+ belongs inside the `data` argument and does not select JSON:API input.
8
20
 
9
- 2. **App-Owned Auth Context**: `json-rest-api` does not define authentication semantics. If you use `context.auth` in hooks, treat it as application-owned data supplied by your transport or surrounding stack.
21
+ `format: 'plain' | 'jsonapi'` selects only the output representation.
22
+ Programmatic calls default to `plain`. POST/PUT/PATCH use
23
+ `returning: 'none' | 'minimal' | 'full'`, defaulting to `full`. Per-call options
24
+ override configured resource/plugin defaults. The built-in HTTP connectors
25
+ select JSON:API with full write responses independently of these defaults;
26
+ resource deletion and relationship mutations return no content.
27
+ Boolean representation/return aliases are removed and rejected.
10
28
 
11
- 3. **Simplified Mode Defaults**:
12
- - `simplifiedApi`: `true` (default for programmatic API calls)
13
- - `simplifiedTransport`: `false` (default for HTTP transport)
29
+ The input key and output format are independent:
14
30
 
15
- 4. **All parameters must be passed within a single params object** as the first argument to each method.
31
+ ```javascript
32
+ // Plain input and a plain result: an ordinary repository needs no JSON:API types.
33
+ async function renameArticle (id, title, context) {
34
+ return api.resources.articles.patch({ id, data: { title } }, context)
35
+ }
16
36
 
17
- 5. **Simplified shorthand is payload-only**: when you pass attributes directly (for example, `post({ title: 'Hello' })`), that plain object is treated as the resource payload. If you also need per-call options such as `transaction`, `queryParams`, `simplified`, or `returnFullRecord`, put the record under `inputRecord` and keep those options at the top level of `params`.
37
+ // Plain input with a JSON:API response.
38
+ const wireResponse = await api.resources.articles.patch({
39
+ id: '42', data: { title: 'Revised' }, format: 'jsonapi'
40
+ })
41
+
42
+ // JSON:API input with a plain response.
43
+ const plainResult = await api.resources.articles.patch({
44
+ id: '42',
45
+ document: { data: { type: 'articles', id: '42', attributes: { title: 'Revised' } } },
46
+ format: 'plain'
47
+ })
48
+ ```
49
+
50
+ All combinations use the same permissions, validation, hooks, storage and
51
+ transaction lifecycle. Full-response preparation still runs before a write
52
+ commits. The HTTP connectors pass JSON:API request bodies as `document` and
53
+ select JSON:API output; there is no separate programmatic CRUD implementation.
54
+
55
+ Write documents may contain object-valued top-level `meta`, `links` and `jsonapi`
56
+ members. Hooks can inspect them on the normalized `context.inputRecord`; they
57
+ are not stored attributes. `errors` and `included` are rejected for writes:
58
+ compound writes and JSON:API extensions are not enabled by accepting a document.
59
+
60
+ On a resource configured with `versionField`, PUT/PATCH/DELETE and relationship
61
+ writes accept an `expectedVersion` string; unconditional writes may omit it.
62
+ POST does not accept that condition. See [optimistic concurrency](GUIDE/33-migrating-to-v2.md)
63
+ for version-field configuration, bulk version arrays and the separate opt-in
64
+ HTTP validator contract.
65
+
66
+ Read calls can borrow raw Knex transactions. Library writes must use the handle
67
+ from `api.transaction` when grouping operations; they reject unmanaged raw
68
+ transactions. Context is application-owned but enriched during each call, so use
69
+ a separate context for each enlisted operation. See [transaction support](#transaction-support).
18
70
 
19
71
  ## Table of Contents
20
72
 
21
- 1. [Core API Methods](#core-api-methods)
73
+ 1. [API setup and runtime](#api-setup-and-runtime)
74
+ 2. [Core API Methods](#core-api-methods)
22
75
  - [QUERY - Retrieve Collections](#query---retrieve-collections)
23
76
  - [GET - Retrieve Single Resource](#get---retrieve-single-resource)
24
77
  - [POST - Create Resource](#post---create-resource)
25
78
  - [PUT - Replace Resource](#put---replace-resource)
26
79
  - [PATCH - Update Resource](#patch---update-resource)
27
80
  - [DELETE - Remove Resource](#delete---remove-resource)
28
- 2. [Relationship Methods](#relationship-methods)
81
+ 3. [Relationship Methods](#relationship-methods)
29
82
  - [getRelated - Retrieve Related Resources](#getrelated---retrieve-related-resources)
30
83
  - [getRelationship - Retrieve Relationship Identifiers](#getrelationship---retrieve-relationship-identifiers)
31
84
  - [postRelationship - Add to Relationship](#postrelationship---add-to-relationship)
32
85
  - [patchRelationship - Replace Relationship](#patchrelationship---replace-relationship)
33
86
  - [deleteRelationship - Remove from Relationship](#deleterelationship---remove-from-relationships)
34
- 3. [Hook System](#hook-system)
87
+ 4. [Hook System](#hook-system)
35
88
  - [Complete Hook Execution Order](#complete-hook-execution-order)
36
89
  - [Hook Context Objects](#hook-context-objects)
37
- 4. [Query Features](#query-features)
90
+ 5. [Query Features](#query-features)
38
91
  - [Filtering](#filtering)
39
92
  - [Sorting](#sorting)
40
93
  - [Pagination](#pagination)
41
94
  - [Sparse Fieldsets](#sparse-fieldsets)
42
95
  - [Including Related Resources](#including-related-resources)
43
- 5. [Configuration Options](#configuration-options)
44
- 6. [Schema Configuration](#schema-configuration)
45
- 7. [Error Handling](#error-handling)
46
- 8. [Advanced Features](#advanced-features)
96
+ 6. [Configuration Options](#configuration-options)
97
+ 7. [Schema Configuration](#schema-configuration)
98
+ 8. [Error Handling](#error-handling)
99
+ 9. [Advanced Features](#advanced-features)
47
100
 
48
101
  ---
49
102
 
103
+ ## API setup and runtime
104
+
105
+ Import `JsonRestApi` from `json-rest-api`. Create an instance, await each plugin
106
+ installation, then register resources before accepting requests.
107
+
108
+ ```js
109
+ const api = new JsonRestApi({ name: 'catalog', logger: applicationLogger })
110
+ await api.use(RestApiPlugin)
111
+ await api.use(RestApiKnexPlugin, { knex })
112
+ await api.addResource('books', bookOptions)
113
+ ```
114
+
115
+ The [quickstart](QUICKSTART.md) supplies a complete setup with imports and schemas.
116
+ `name` and `logger` are optional. The logger accepts `trace`, `debug`, `info`,
117
+ `warn`, `error` and `fatal` methods; omitted methods are silent. Configure log
118
+ formatting and filtering in your logger. Old `log` and `logging` options reject.
119
+
120
+ | Member | Contract |
121
+ | --- | --- |
122
+ | `api.use(plugin, options?)` | Installs a named plugin after checking dependencies; resolves to the API |
123
+ | `api.addResource(name, options?)` | Registers a unique resource, runs setup hooks and resolves to that resource |
124
+ | `api.resources[name]` | The registered resource object and its methods |
125
+ | `api.customize({ hooks, methods, vars, helpers })` | Adds global hooks, shared resource methods and API defaults; resolves to the API |
126
+ | `api.runHooks(event, context?)` | Runs global handlers sequentially; resolves to `false` if one stops the list, otherwise `true` |
127
+ | `api.vars`, `api.helpers` | Defaults inherited by resource vars and helpers |
128
+
129
+ Resource options include schemas and plugin configuration, plus local `hooks`,
130
+ `methods`, `vars` and `helpers`. Resource methods receive the runtime argument
131
+ object, not positional data arguments; see [writing plugins](GUIDE/29-writing-plugins.md).
132
+ Global setup hooks run before resource-local operation hooks are registered.
133
+
134
+ Shared resource methods require their resource as the receiver. Keep calls such
135
+ as `api.resources.books.get(params, context)` intact, or bind the resource when
136
+ passing a method as a callback. Local vars/helpers override inherited defaults;
137
+ `Object.keys` lists local entries only. Shared method replacements apply to
138
+ resources without their own local override.
139
+
140
+ Await setup sequentially. Setup failure is not rolled back; discard that instance
141
+ and correct its configuration before starting again. Registration and
142
+ `customize()` do not migrate stored data or rebuild arbitrary schema mutations.
143
+
144
+ ### TypeScript resources and hooks
145
+
146
+ The existing registration call infers ordinary attribute types on its returned
147
+ resource. Install the REST/storage plugins first, as in JavaScript:
148
+
149
+ ```ts
150
+ const books = await api.addResource('books', {
151
+ schema: {
152
+ title: { type: 'string', required: true },
153
+ pages: { type: 'number', nullable: true }
154
+ }
155
+ })
156
+
157
+ await books.patch({ id: '1', data: { pages: 412 } })
158
+ // Type error: pages is numeric, not a string.
159
+ // await books.patch({ id: '1', data: { pages: 'long' } })
160
+
161
+ const book = await books.get({ id: '1' })
162
+ // title: string | undefined; visibility and sparse fieldsets can omit it.
163
+ const title = book.title
164
+ ```
165
+
166
+ `ResourceSchema` describes built-in field declarations. For reusable schemas,
167
+ retain literal options with `as const satisfies ResourceSchema`.
168
+ `InferInput<typeof schema>` and `InferOutput<typeof schema>` can replace manually
169
+ duplicated attribute interfaces. Input properties remain optional: defaults and
170
+ hooks can supply required persisted fields. Computed fields are excluded from
171
+ input, and hidden fields from inferred output. Resource result types keep output
172
+ properties optional for permissions and projections. The public resource ID is
173
+ always `id`; inferred registration also omits a declared physical `idProperty`.
174
+ An ID property known only as a dynamic string needs an explicit resource model.
175
+ Optional stored fields without `required: true` or `nullable: false` also include
176
+ SQL `null` in their output type. Input `null` still requires `nullable: true`.
177
+
178
+ ```ts
179
+ import type { ResourceSchema, InferInput, InferOutput } from 'json-rest-api'
180
+
181
+ const schema = {
182
+ title: { type: 'string', required: true },
183
+ pages: { type: 'number' }
184
+ } as const satisfies ResourceSchema
185
+
186
+ type BookInput = InferInput<typeof schema>
187
+ type BookFields = InferOutput<typeof schema>
188
+ ```
189
+
190
+ Inference describes built-in CRUD with the standard API defaults (`plain`,
191
+ `full`). Literal resource-level `format` and `returning` options are inferred,
192
+ as are per-call overrides. Arbitrary plugin installations, API-level defaults,
193
+ `vars` mutations and replaced methods are not inferred. Keep an explicit resource
194
+ map when using those features, dynamic schema enrichment, relationship fields or
195
+ custom transformations:
196
+
197
+ ```ts
198
+ import { JsonRestApi } from 'json-rest-api'
199
+ import type { ResourceCoreMethods } from 'json-rest-api'
200
+
201
+ interface BookFields { title: string; displayTitle: string }
202
+ interface BookInput { title?: string }
203
+ type Books = ResourceCoreMethods<BookFields, BookInput, 'books', 'jsonapi'>
204
+ const api = new JsonRestApi<{ books: Books }>()
205
+ ```
206
+
207
+ The explicit map remains authoritative for both returned handles and
208
+ `api.resources`. Awaited registration does not grow the static type of an existing
209
+ `api.resources` registry. Without an explicit map, keep the inferred returned
210
+ handle. Resource-local method overrides use the general runtime declarations
211
+ instead of pretending the overridden methods still perform built-in CRUD.
212
+
213
+ Attribute inference omits inline belongs-to backing fields; it does not generate
214
+ relationship result trees. Use the existing `RelationshipMethods` and explicit
215
+ input/output fields for relationships, separately declared polymorphic backing
216
+ fields and managed version-field input restrictions. Callback/serializer output
217
+ is `unknown` unless supplied explicitly. Inferred values describe public
218
+ representations, not every accepted coercion or every database constraint.
219
+ Nested object schemas use existing schema instances, not a new nested-schema DSL.
220
+
221
+ `RuntimeResourceOptions` describes built-in resource settings. Plugin extensions
222
+ can declare an intersection with their own option interface and pass that typed
223
+ configuration, retaining a precise resource map where the plugin changes fields
224
+ or methods. This does not require a runtime wrapper.
225
+
226
+ Known hooks receive phase-specific editor guidance, including library-owned
227
+ read-only fields and permission/enrichment context references. Use
228
+ `RuntimeCustomization<AppContext>` or the optional second `JsonRestApi` type
229
+ argument to describe custom application state. `HookHandler<Event, AppContext>`
230
+ types an individual known hook; `RuntimeHook<PluginContext>` describes a custom
231
+ plugin hook. These declarations do not freeze context or replace runtime
232
+ validation. See the [hook recipes and editor guidance](GUIDE/13-hooks-and-lifecycle.md).
233
+
50
234
  ## Core API Methods
51
235
 
52
236
  ### QUERY - Retrieve Collections
@@ -55,7 +239,7 @@ Retrieves a collection of resources with support for filtering, sorting, paginat
55
239
 
56
240
  #### Method Signature
57
241
  ```javascript
58
- const result = await api.resources.[resourceType].query(params, context)
242
+ const result = await api.resources[resourceType].query(params, context)
59
243
  ```
60
244
 
61
245
  #### Parameters
@@ -71,7 +255,7 @@ All parameters are passed within a single `params` object:
71
255
  sort: Array, // Sort fields
72
256
  page: Object // Pagination parameters
73
257
  },
74
- simplified: Boolean, // Override simplified mode (default: true for API)
258
+ format: String, // 'plain' (default) or 'jsonapi'
75
259
  transaction: Object // Database transaction object
76
260
  }
77
261
  ```
@@ -88,12 +272,12 @@ All parameters are passed within a single `params` object:
88
272
  | `queryParams.page.size` | Number | No | Items per page |
89
273
  | `queryParams.page.after` | String | No | Cursor for forward pagination |
90
274
  | `queryParams.page.before` | String | No | Cursor for backward pagination |
91
- | `simplified` | Boolean | No | Override simplified mode setting (default: true) |
275
+ | `format` | String | No | `'plain'` or `'jsonapi'`; uses the configured default when omitted |
92
276
  | `transaction` | Object | No | Database transaction object |
93
277
 
94
278
  #### Return Value
95
279
 
96
- **JSON:API Mode (simplified: false):**
280
+ **JSON:API Mode (format: 'jsonapi'):**
97
281
  ```javascript
98
282
  {
99
283
  data: [
@@ -121,10 +305,12 @@ All parameters are passed within a single `params` object:
121
305
  }
122
306
  ],
123
307
  meta: {
124
- page: {
308
+ pagination: {
309
+ page: 1,
310
+ pageSize: 10,
311
+ pageCount: 5,
125
312
  total: 50,
126
- size: 10,
127
- number: 1
313
+ hasMore: true
128
314
  }
129
315
  },
130
316
  links: {
@@ -135,7 +321,7 @@ All parameters are passed within a single `params` object:
135
321
  }
136
322
  ```
137
323
 
138
- **Simplified Mode (simplified: true - default):**
324
+ **Plain Mode (format: 'plain' - default):**
139
325
  ```javascript
140
326
  {
141
327
  data: [
@@ -143,7 +329,6 @@ All parameters are passed within a single `params` object:
143
329
  id: '1',
144
330
  title: 'First Article',
145
331
  content: 'Article content...',
146
- author: '10',
147
332
  author: {
148
333
  id: '10',
149
334
  name: 'John Doe'
@@ -151,10 +336,12 @@ All parameters are passed within a single `params` object:
151
336
  }
152
337
  ],
153
338
  meta: {
154
- page: {
339
+ pagination: {
340
+ page: 1,
341
+ pageSize: 10,
342
+ pageCount: 5,
155
343
  total: 50,
156
- size: 10,
157
- number: 1
344
+ hasMore: true
158
345
  }
159
346
  }
160
347
  }
@@ -171,7 +358,7 @@ Accept: application/vnd.api+json
171
358
 
172
359
  **Basic Query:**
173
360
  ```javascript
174
- // Get all articles (simplified mode by default)
361
+ // Get all articles (plain mode by default)
175
362
  const result = await api.resources.articles.query({});
176
363
 
177
364
  // HTTP equivalent
@@ -237,7 +424,7 @@ const result = await api.resources.articles.query({
237
424
  filters: { status: 'published' },
238
425
  include: ['author']
239
426
  },
240
- simplified: false
427
+ format: 'jsonapi'
241
428
  });
242
429
 
243
430
  // Returns full JSON:API structure with type, id, attributes, relationships
@@ -245,18 +432,15 @@ const result = await api.resources.articles.query({
245
432
 
246
433
  **Cursor-based Pagination:**
247
434
  ```javascript
248
- // Get next page using cursor
249
- const result = await api.resources.articles.query({
250
- queryParams: {
251
- page: {
252
- after: 'eyJpZCI6MTAsImNyZWF0ZWRBdCI6IjIwMjQtMDEtMTUifQ==',
253
- size: 10
254
- }
255
- }
435
+ const firstPage = await api.resources.articles.query({
436
+ queryParams: { page: { size: 10 } }
256
437
  });
257
-
258
- // HTTP equivalent
259
- // GET /articles?page[after]=eyJpZCI6MTAsImNyZWF0ZWRBdCI6IjIwMjQtMDEtMTUifQ==&page[size]=10
438
+ const cursor = firstPage.meta.pagination.cursor?.next;
439
+ if (cursor) {
440
+ const nextPage = await api.resources.articles.query({
441
+ queryParams: { page: { after: cursor, size: 10 } }
442
+ });
443
+ }
260
444
  ```
261
445
 
262
446
  ---
@@ -267,7 +451,7 @@ Retrieves a single resource by its ID with optional relationship inclusion.
267
451
 
268
452
  #### Method Signature
269
453
  ```javascript
270
- const result = await api.resources.[resourceType].get(params, context)
454
+ const result = await api.resources[resourceType].get(params, context)
271
455
  ```
272
456
 
273
457
  #### Parameters
@@ -279,7 +463,7 @@ const result = await api.resources.[resourceType].get(params, context)
279
463
  include: Array, // Relationship paths to include
280
464
  fields: Object // Sparse fieldsets
281
465
  },
282
- simplified: Boolean, // Override simplified mode (default: true for API)
466
+ format: String, // 'plain' (default) or 'jsonapi'
283
467
  transaction: Object // Database transaction object
284
468
  }
285
469
  ```
@@ -290,14 +474,14 @@ const result = await api.resources.[resourceType].get(params, context)
290
474
  | `queryParams` | Object | No | Query parameters |
291
475
  | `queryParams.include` | Array | No | Relationship paths to include |
292
476
  | `queryParams.fields` | Object | No | Sparse fieldsets for specific resource types |
293
- | `simplified` | Boolean | No | Override simplified mode setting (default: true) |
477
+ | `format` | String | No | `'plain'` or `'jsonapi'`; uses the configured default when omitted |
294
478
  | `transaction` | Object | No | Database transaction object |
295
479
 
296
480
  `id` is normalized with the effective `normalizeId` function before validation and lookup. If it normalizes to an empty value, the operation fails as `REST_API_RESOURCE` with subtype `not_found`.
297
481
 
298
482
  #### Return Value
299
483
 
300
- **JSON:API Mode (simplified: false):**
484
+ **JSON:API Mode (format: 'jsonapi'):**
301
485
  ```javascript
302
486
  {
303
487
  data: {
@@ -325,7 +509,7 @@ const result = await api.resources.[resourceType].get(params, context)
325
509
  }
326
510
  ```
327
511
 
328
- **Simplified Mode (simplified: true - default):**
512
+ **Plain Mode (format: 'plain' - default):**
329
513
  ```javascript
330
514
  {
331
515
  id: '1',
@@ -350,7 +534,7 @@ Accept: application/vnd.api+json
350
534
 
351
535
  **Basic Get:**
352
536
  ```javascript
353
- // Get article by ID (simplified mode by default)
537
+ // Get article by ID (plain mode by default)
354
538
  const result = await api.resources.articles.get({
355
539
  id: '1'
356
540
  });
@@ -399,7 +583,7 @@ const result = await api.resources.articles.get({
399
583
  queryParams: {
400
584
  include: ['author', 'tags']
401
585
  },
402
- simplified: false
586
+ format: 'jsonapi'
403
587
  });
404
588
 
405
589
  // Returns full JSON:API document structure
@@ -413,54 +597,56 @@ Creates a new resource with attributes and optional relationships.
413
597
 
414
598
  #### Method Signature
415
599
  ```javascript
416
- const result = await api.resources.[resourceType].post(params, context)
600
+ const result = await api.resources[resourceType].post(params, context)
417
601
  ```
418
602
 
419
603
  #### Parameters
420
604
 
421
605
  ```javascript
422
606
  {
423
- inputRecord: Object, // Required: Resource data (JSON:API or simplified)
607
+ data: Object, // Plain resource values; choose data OR document
608
+ document: Object, // JSON:API document; never supply both
424
609
  queryParams: {
425
610
  include: Array, // For response formatting
426
611
  fields: Object // For response formatting
427
612
  },
428
- simplified: Boolean, // Override simplified mode (default: true for API)
613
+ format: String, // 'plain' (default) or 'jsonapi'
429
614
  transaction: Object, // Database transaction object
430
- returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
615
+ returning: String // 'none', 'minimal' or 'full' (default)
431
616
  }
432
617
  ```
433
618
 
434
619
  | Parameter | Type | Required | Description |
435
620
  |-----------|------|----------|-------------|
436
- | `inputRecord` | Object | Yes | Resource data to create |
437
- | `inputRecord.id` | String | No (simplified) | Optional explicit logical resource ID |
438
- | `inputRecord.data` | Object | Yes (JSON:API) | Resource data container |
439
- | `inputRecord.data.id` | String | No (JSON:API) | Optional explicit logical resource ID |
440
- | `inputRecord.data.type` | String | Yes (JSON:API) | Resource type |
441
- | `inputRecord.data.attributes` | Object | Yes (JSON:API) | Resource attributes |
442
- | `inputRecord.data.relationships` | Object | No | Related resources |
621
+ | `data` | Object | One input key required | Resource data to create as plain values; excludes `document` |
622
+ | `document` | Object | One input key required | JSON:API input document; excludes `data` |
623
+ | `data.id` | String | No (plain) | Optional explicit logical resource ID |
624
+ | `document.data` | Object | Yes (JSON:API) | Resource data container |
625
+ | `document.data.id` | String | No (JSON:API) | Optional explicit logical resource ID |
626
+ | `document.data.type` | String | Yes (JSON:API) | Resource type |
627
+ | `document.data.attributes` | Object | No | Resource attributes, subject to schema requirements/defaults |
628
+ | `document.data.relationships` | Object | No | Related resources |
443
629
  | `queryParams` | Object | No | For includes/fields in response |
444
- | `simplified` | Boolean | No | Override simplified mode (default: true) |
630
+ | `format` | String | No | `'plain'` or `'jsonapi'`; uses the configured default when omitted |
445
631
  | `transaction` | Object | No | Database transaction object |
446
- | `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
632
+ | `returning` | String | No | `'none'`, `'minimal'` or `'full'`; booleans are rejected |
447
633
 
448
634
  When an explicit resource id is provided, `normalizeId` runs before persistence and before any follow-up record fetch used for the return payload. If the normalized id is empty, the request fails as `REST_API_VALIDATION` on `data.id`.
449
635
 
450
636
  #### Return Value Behavior
451
637
 
452
- The return value depends on `returnFullRecord` setting and whether it's an API or transport call:
638
+ POST/PUT/PATCH use the selected `returning` mode:
453
639
 
454
- **Default behavior:**
455
- - API calls (programmatic): `returnFullRecord = 'full'` (returns complete resource)
456
- - Transport calls (HTTP): `returnFullRecord = 'no'` (returns 204 No Content)
640
+ | Mode | Plain output | JSON:API output |
641
+ | --- | --- | --- |
642
+ | `none` | `undefined` | `undefined` |
643
+ | `minimal` | `{ type, id }` | `{ data: { type, id } }` |
644
+ | `full` | Resource object | Resource document |
457
645
 
458
- **Options:**
459
- - `'no'`: Returns `undefined` (204 No Content)
460
- - `'minimal'`: Returns resource with ID only
461
- - `'full'`: Returns complete resource with all fields
462
-
463
- Boolean values are accepted for backward compatibility: `true` is normalized to `'full'` and `false` is normalized to `'no'`. New code should use the string values.
646
+ Full responses honor requested fieldsets/includes and visibility; they do not
647
+ promise every declared field. Programmatic calls default to `full`. HTTP
648
+ connectors choose their response behavior explicitly and map no-content writes
649
+ to 204. A POST with a response body uses 201; PUT/PATCH with a body use 200.
464
650
 
465
651
  #### HTTP Equivalent
466
652
 
@@ -487,11 +673,11 @@ Accept: application/vnd.api+json
487
673
 
488
674
  #### Examples
489
675
 
490
- **Basic Create (Simplified Mode):**
676
+ **Basic Create (Plain Mode):**
491
677
  ```javascript
492
- // Create article with simplified input (default mode)
678
+ // Create article with plain input (default mode)
493
679
  const result = await api.resources.articles.post({
494
- inputRecord: {
680
+ data: {
495
681
  title: 'New Article',
496
682
  content: 'Article content...',
497
683
  status: 'draft',
@@ -506,7 +692,7 @@ const result = await api.resources.articles.post({
506
692
  ```javascript
507
693
  // Create article with JSON:API format
508
694
  const result = await api.resources.articles.post({
509
- inputRecord: {
695
+ document: {
510
696
  data: {
511
697
  type: 'articles',
512
698
  attributes: {
@@ -521,7 +707,7 @@ const result = await api.resources.articles.post({
521
707
  }
522
708
  }
523
709
  },
524
- simplified: false
710
+ format: 'jsonapi'
525
711
  });
526
712
  ```
527
713
 
@@ -530,7 +716,7 @@ const result = await api.resources.articles.post({
530
716
  // With normalizeId configured to trim and uppercase ids,
531
717
  // this record is stored as ARTICLE-42
532
718
  const result = await api.resources.articles.post({
533
- inputRecord: {
719
+ document: {
534
720
  data: {
535
721
  type: 'articles',
536
722
  id: ' article-42 ',
@@ -540,15 +726,15 @@ const result = await api.resources.articles.post({
540
726
  }
541
727
  }
542
728
  },
543
- simplified: false
729
+ format: 'jsonapi'
544
730
  });
545
731
  ```
546
732
 
547
733
  **Create with Multiple Relationships:**
548
734
  ```javascript
549
- // Create article with author and tags (simplified)
735
+ // Create article with author and tags (plain)
550
736
  const result = await api.resources.articles.post({
551
- inputRecord: {
737
+ data: {
552
738
  title: 'New Article',
553
739
  content: 'Article content...',
554
740
  author: '10',
@@ -564,28 +750,29 @@ const result = await api.resources.articles.post({
564
750
  ```javascript
565
751
  // Create and return only ID
566
752
  const result = await api.resources.articles.post({
567
- inputRecord: {
753
+ data: {
568
754
  title: 'New Article',
569
755
  content: 'Article content...'
570
756
  },
571
- returnFullRecord: 'minimal'
757
+ returning: 'minimal'
572
758
  });
573
759
 
574
- // Returns (simplified mode):
760
+ // Returns (plain mode):
575
761
  // {
762
+ // type: 'articles',
576
763
  // id: '123'
577
764
  // }
578
765
  ```
579
766
 
580
767
  **Create with No Return:**
581
768
  ```javascript
582
- // Create without returning data (like HTTP transport)
769
+ // Create without returning data to this programmatic caller
583
770
  const result = await api.resources.articles.post({
584
- inputRecord: {
771
+ data: {
585
772
  title: 'New Article',
586
773
  content: 'Article content...'
587
774
  },
588
- returnFullRecord: 'no'
775
+ returning: 'none'
589
776
  });
590
777
 
591
778
  // Returns: undefined
@@ -595,48 +782,55 @@ const result = await api.resources.articles.post({
595
782
 
596
783
  ### PUT - Replace Resource
597
784
 
598
- Completely replaces an existing resource. All attributes must be provided; missing relationships are removed.
785
+ Creates a missing target at the supplied ID or replaces a visible existing
786
+ resource. Replacement applies schema required/default/nullability rules and the
787
+ method's relationship-omission semantics. See [PUT and PATCH](GUIDE/10-put-and-patch.md).
788
+ Target visibility checks still apply.
599
789
 
600
790
  #### Method Signature
601
791
  ```javascript
602
- const result = await api.resources.[resourceType].put(params, context)
792
+ const result = await api.resources[resourceType].put(params, context)
603
793
  ```
604
794
 
605
795
  #### Parameters
606
796
 
607
797
  ```javascript
608
798
  {
609
- inputRecord: Object, // Required: Complete resource data
799
+ id: String|Number|BigInt, // Target ID; may instead be supplied in data.id or document.data.id
800
+ data: Object, // Complete plain resource values; choose data OR document
801
+ document: Object, // Complete JSON:API resource document
610
802
  queryParams: {
611
803
  include: Array, // For response formatting
612
804
  fields: Object // For response formatting
613
805
  },
614
- simplified: Boolean, // Override simplified mode (default: true for API)
806
+ format: String, // 'plain' (default) or 'jsonapi'
615
807
  transaction: Object, // Database transaction object
616
- returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
808
+ returning: String // 'none', 'minimal' or 'full' (default)
617
809
  }
618
810
  ```
619
811
 
620
812
  | Parameter | Type | Required | Description |
621
813
  |-----------|------|----------|-------------|
622
- | `inputRecord` | Object | Yes | Complete resource data |
623
- | `inputRecord.id` | String | Yes (simplified) | Logical resource ID |
624
- | `inputRecord.data.id` | String | Yes (JSON:API) | Logical resource ID |
625
- | `inputRecord.data.type` | String | Yes (JSON:API) | Resource type |
626
- | `inputRecord.data.attributes` | Object | Yes (JSON:API) | All resource attributes |
627
- | `inputRecord.data.relationships` | Object | No | All relationships (missing ones are nulled) |
814
+ | `data` | Object | One input key required | Complete resource data as plain values; excludes `document` |
815
+ | `document` | Object | One input key required | JSON:API input document; excludes `data` |
816
+ | `id` | String\|Number\|BigInt | Conditional | Target ID; required if the input document does not supply one |
817
+ | `data.id` | String\|Number | Conditional (plain) | Target ID when top-level `id` is omitted |
818
+ | `document.data.id` | String\|Number | Conditional (JSON:API) | Target ID when top-level `id` is omitted |
819
+ | `document.data.type` | String | Yes (JSON:API) | Resource type |
820
+ | `document.data.attributes` | Object | Yes (JSON:API) | All resource attributes |
821
+ | `document.data.relationships` | Object | No | All relationships (missing ones are nulled) |
628
822
  | `queryParams` | Object | No | For response formatting |
629
- | `simplified` | Boolean | No | Override simplified mode (default: true) |
823
+ | `format` | String | No | `'plain'` or `'jsonapi'`; uses the configured default when omitted |
630
824
  | `transaction` | Object | No | Database transaction object |
631
- | `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
825
+ | `returning` | String | No | `'none'`, `'minimal'` or `'full'`; booleans are rejected |
632
826
 
633
- `inputRecord.id` and `inputRecord.data.id` always refer to the logical resource id. `idProperty` and storage mapping affect the backing column name, not the API field name, and the resource id is not part of `attributes`.
827
+ `data.id` and `document.data.id` always refer to the logical resource id. `idProperty` and storage mapping affect the backing column name, not the API field name, and the resource id is not part of `attributes`.
634
828
 
635
829
  If both the URL id and body id are present, both are normalized before the equality check. If either id normalizes to an empty value, the operation fails before storage is touched.
636
830
 
637
831
  #### Return Value
638
832
 
639
- Updated resource based on `returnFullRecord` setting (defaults: API='full', transport='no').
833
+ Returns according to `returning`: `none`, `minimal` or `full`. See the POST return-value table.
640
834
 
641
835
  #### HTTP Equivalent
642
836
 
@@ -665,11 +859,11 @@ Accept: application/vnd.api+json
665
859
 
666
860
  #### Examples
667
861
 
668
- **Basic Replace (Simplified):**
862
+ **Basic Replace (Plain):**
669
863
  ```javascript
670
- // Replace entire article (simplified mode)
864
+ // Replace entire article (plain mode)
671
865
  const result = await api.resources.articles.put({
672
- inputRecord: {
866
+ data: {
673
867
  id: '1',
674
868
  title: 'Completely New Title',
675
869
  content: 'Entirely new content',
@@ -684,7 +878,7 @@ const result = await api.resources.articles.put({
684
878
  ```javascript
685
879
  // Replace article with JSON:API format
686
880
  const result = await api.resources.articles.put({
687
- inputRecord: {
881
+ document: {
688
882
  data: {
689
883
  type: 'articles',
690
884
  id: '1',
@@ -703,7 +897,7 @@ const result = await api.resources.articles.put({
703
897
  }
704
898
  }
705
899
  },
706
- simplified: false
900
+ format: 'jsonapi'
707
901
  });
708
902
  ```
709
903
 
@@ -711,7 +905,7 @@ const result = await api.resources.articles.put({
711
905
  ```javascript
712
906
  // Replace and explicitly remove relationships
713
907
  const result = await api.resources.articles.put({
714
- inputRecord: {
908
+ data: {
715
909
  id: '1',
716
910
  title: 'Article Without Author',
717
911
  content: 'Content...',
@@ -730,42 +924,46 @@ Partially updates an existing resource. Only provided attributes and relationshi
730
924
 
731
925
  #### Method Signature
732
926
  ```javascript
733
- const result = await api.resources.[resourceType].patch(params, context)
927
+ const result = await api.resources[resourceType].patch(params, context)
734
928
  ```
735
929
 
736
930
  #### Parameters
737
931
 
738
932
  ```javascript
739
933
  {
740
- inputRecord: Object, // Required: Partial resource data
934
+ id: String|Number|BigInt, // Target ID; may instead be supplied in data.id or document.data.id
935
+ data: Object, // Partial plain resource values; choose data OR document
936
+ document: Object, // Partial JSON:API resource document
741
937
  queryParams: {
742
938
  include: Array, // For response formatting
743
939
  fields: Object // For response formatting
744
940
  },
745
- simplified: Boolean, // Override simplified mode (default: true for API)
941
+ format: String, // 'plain' (default) or 'jsonapi'
746
942
  transaction: Object, // Database transaction object
747
- returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
943
+ returning: String // 'none', 'minimal' or 'full' (default)
748
944
  }
749
945
  ```
750
946
 
751
947
  | Parameter | Type | Required | Description |
752
948
  |-----------|------|----------|-------------|
753
- | `inputRecord` | Object | Yes | Partial resource data |
754
- | `inputRecord.id` | String | Yes (simplified) | Logical resource ID |
755
- | `inputRecord.data.id` | String | Yes (JSON:API) | Logical resource ID |
756
- | `inputRecord.data.type` | String | Yes (JSON:API) | Resource type |
757
- | `inputRecord.data.attributes` | Object | No | Attributes to update |
758
- | `inputRecord.data.relationships` | Object | No | Relationships to update |
949
+ | `data` | Object | One input key required | Partial resource data as plain values; excludes `document` |
950
+ | `document` | Object | One input key required | JSON:API input document; excludes `data` |
951
+ | `id` | String\|Number\|BigInt | Conditional | Target ID; required if the input document does not supply one |
952
+ | `data.id` | String\|Number | Conditional (plain) | Target ID when top-level `id` is omitted |
953
+ | `document.data.id` | String\|Number | Conditional (JSON:API) | Target ID when top-level `id` is omitted |
954
+ | `document.data.type` | String | Yes (JSON:API) | Resource type |
955
+ | `document.data.attributes` | Object | No | Attributes to update |
956
+ | `document.data.relationships` | Object | No | Relationships to update |
759
957
  | `queryParams` | Object | No | For response formatting |
760
- | `simplified` | Boolean | No | Override simplified mode (default: true) |
958
+ | `format` | String | No | `'plain'` or `'jsonapi'`; uses the configured default when omitted |
761
959
  | `transaction` | Object | No | Database transaction object |
762
- | `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
960
+ | `returning` | String | No | `'none'`, `'minimal'` or `'full'`; booleans are rejected |
763
961
 
764
962
  If both the URL id and body id are present, both are normalized before the equality check. If either id normalizes to an empty value, the operation fails before storage is touched.
765
963
 
766
964
  #### Return Value
767
965
 
768
- Updated resource based on `returnFullRecord` setting (defaults: API='full', transport='no').
966
+ Returns according to `returning`: `none`, `minimal` or `full`. See the POST return-value table.
769
967
 
770
968
  #### HTTP Equivalent
771
969
 
@@ -787,11 +985,11 @@ Accept: application/vnd.api+json
787
985
 
788
986
  #### Examples
789
987
 
790
- **Basic Update (Simplified):**
988
+ **Basic Update (Plain):**
791
989
  ```javascript
792
- // Update only the status (simplified mode)
990
+ // Update only the status (plain mode)
793
991
  const result = await api.resources.articles.patch({
794
- inputRecord: {
992
+ data: {
795
993
  id: '1',
796
994
  status: 'published'
797
995
  }
@@ -804,7 +1002,7 @@ const result = await api.resources.articles.patch({
804
1002
  ```javascript
805
1003
  // Update title and content
806
1004
  const result = await api.resources.articles.patch({
807
- inputRecord: {
1005
+ data: {
808
1006
  id: '1',
809
1007
  title: 'Updated Title',
810
1008
  content: 'Updated content only',
@@ -817,7 +1015,7 @@ const result = await api.resources.articles.patch({
817
1015
  ```javascript
818
1016
  // Update with JSON:API format
819
1017
  const result = await api.resources.articles.patch({
820
- inputRecord: {
1018
+ document: {
821
1019
  data: {
822
1020
  type: 'articles',
823
1021
  id: '1',
@@ -832,15 +1030,15 @@ const result = await api.resources.articles.patch({
832
1030
  }
833
1031
  }
834
1032
  },
835
- simplified: false
1033
+ format: 'jsonapi'
836
1034
  });
837
1035
  ```
838
1036
 
839
1037
  **Update Relationships Only:**
840
1038
  ```javascript
841
- // Change author and add tags (simplified)
1039
+ // Change author and add tags (plain)
842
1040
  const result = await api.resources.articles.patch({
843
- inputRecord: {
1041
+ data: {
844
1042
  id: '1',
845
1043
  author: '30',
846
1044
  tags: ['3', '4', '5']
@@ -852,7 +1050,7 @@ const result = await api.resources.articles.patch({
852
1050
  ```javascript
853
1051
  // Set featured_image to null
854
1052
  const result = await api.resources.articles.patch({
855
- inputRecord: {
1053
+ data: {
856
1054
  id: '1',
857
1055
  featured_image_id: null
858
1056
  }
@@ -867,7 +1065,7 @@ Permanently deletes a resource from the system.
867
1065
 
868
1066
  #### Method Signature
869
1067
  ```javascript
870
- const result = await api.resources.[resourceType].delete(params, context)
1068
+ const result = await api.resources[resourceType].delete(params, context)
871
1069
  ```
872
1070
 
873
1071
  #### Parameters
@@ -914,32 +1112,27 @@ await api.resources.articles.delete({
914
1112
 
915
1113
  **Delete with Transaction:**
916
1114
  ```javascript
917
- // Delete within a transaction
918
- const trx = await knex.transaction();
919
- try {
1115
+ await api.transaction(async transaction => {
920
1116
  // Delete article
921
1117
  await api.resources.articles.delete({
922
1118
  id: '1',
923
- transaction: trx
1119
+ transaction
924
1120
  });
925
1121
 
926
1122
  // Delete related comments
927
1123
  await api.resources.comments.delete({
928
1124
  id: '10',
929
- transaction: trx
1125
+ transaction
930
1126
  });
931
1127
 
932
- await trx.commit();
933
- } catch (error) {
934
- await trx.rollback();
935
- throw error;
936
- }
1128
+ });
937
1129
  ```
938
1130
 
939
1131
  **Note on Transaction Auto-commit:**
940
- The library automatically manages transaction commits when you don't provide one:
941
- - If you provide a transaction, you're responsible for committing/rolling back
942
- - If you don't provide a transaction, the library creates one and auto-commits
1132
+ Writes without a transaction own their completion. To compose writes, pass the
1133
+ handle supplied by `api.transaction`; the helper awaits the callback, commits and
1134
+ then runs completion hooks, or rolls back when the unit fails. Raw transactions and
1135
+ savepoints cannot own library writes.
943
1136
 
944
1137
  ---
945
1138
 
@@ -951,7 +1144,7 @@ Retrieves the actual related resources with full data, not just identifiers.
951
1144
 
952
1145
  #### Method Signature
953
1146
  ```javascript
954
- const result = await api.resources.[resourceType].getRelated(params, context)
1147
+ const result = await api.resources[resourceType].getRelated(params, context)
955
1148
  ```
956
1149
 
957
1150
  #### Parameters
@@ -960,7 +1153,8 @@ const result = await api.resources.[resourceType].getRelated(params, context)
960
1153
  {
961
1154
  id: String|Number, // Required: Parent resource ID
962
1155
  relationshipName: String, // Required: Name of the relationship
963
- queryParams: Object, // Standard query parameters
1156
+ format: String, // plain or jsonapi
1157
+ queryParams: Object, // Selection; collection filters/sort/page for to-many
964
1158
  transaction: Object // Database transaction object
965
1159
  }
966
1160
  ```
@@ -969,14 +1163,18 @@ const result = await api.resources.[resourceType].getRelated(params, context)
969
1163
  |-----------|------|----------|-------------|
970
1164
  | `id` | String\|Number | Yes | Parent resource ID |
971
1165
  | `relationshipName` | String | Yes | Name of the relationship |
972
- | `queryParams` | Object | No | Standard query parameters for related resources |
1166
+ | `format` | String | No | plain or jsonapi; uses the configured default |
1167
+ | `queryParams` | Object | No | Selection options; collection filters/sort/page only for to-many |
973
1168
  | `transaction` | Object | No | Database transaction object |
974
1169
 
975
1170
  `id` is normalized with the effective `normalizeId` function before validation and lookup. If it normalizes to an empty value, the operation fails as `REST_API_RESOURCE` with subtype `not_found`.
976
1171
 
977
1172
  #### Return Value
978
1173
 
979
- JSON:API response with related resources (supports all query features like filtering, pagination, etc.)
1174
+ Uses the selected `format` (default `plain`). A to-one relationship returns one
1175
+ resource or null; a to-many relationship returns a collection envelope. JSON:API
1176
+ wraps either cardinality under `data`. Collection filters/sort/page apply to
1177
+ to-many related resources; to-one reads accept selection options.
980
1178
 
981
1179
  #### HTTP Equivalent
982
1180
 
@@ -996,7 +1194,7 @@ const result = await api.resources.articles.getRelated({
996
1194
  relationshipName: 'author'
997
1195
  });
998
1196
 
999
- // Returns single resource (simplified mode by default):
1197
+ // Returns single resource (plain mode by default):
1000
1198
  // {
1001
1199
  // id: '10',
1002
1200
  // name: 'John Doe',
@@ -1033,7 +1231,7 @@ Retrieves only the resource identifiers for a relationship, not the full resourc
1033
1231
 
1034
1232
  #### Method Signature
1035
1233
  ```javascript
1036
- const result = await api.resources.[resourceType].getRelationship(params, context)
1234
+ const result = await api.resources[resourceType].getRelationship(params, context)
1037
1235
  ```
1038
1236
 
1039
1237
  #### Parameters
@@ -1108,7 +1306,7 @@ Adds new members to a to-many relationship without affecting existing members.
1108
1306
 
1109
1307
  #### Method Signature
1110
1308
  ```javascript
1111
- const result = await api.resources.[resourceType].postRelationship(params, context)
1309
+ const result = await api.resources[resourceType].postRelationship(params, context)
1112
1310
  ```
1113
1311
 
1114
1312
  #### Parameters
@@ -1169,7 +1367,7 @@ Completely replaces a relationship. For to-one relationships, sets the new relat
1169
1367
 
1170
1368
  #### Method Signature
1171
1369
  ```javascript
1172
- const result = await api.resources.[resourceType].patchRelationship(params, context)
1370
+ const result = await api.resources[resourceType].patchRelationship(params, context)
1173
1371
  ```
1174
1372
 
1175
1373
  #### Parameters
@@ -1252,7 +1450,7 @@ Removes specific members from a to-many relationship.
1252
1450
 
1253
1451
  #### Method Signature
1254
1452
  ```javascript
1255
- const result = await api.resources.[resourceType].deleteRelationship(params, context)
1453
+ const result = await api.resources[resourceType].deleteRelationship(params, context)
1256
1454
  ```
1257
1455
 
1258
1456
  #### Parameters
@@ -1307,475 +1505,98 @@ await api.resources.articles.deleteRelationship({
1307
1505
 
1308
1506
  ## Hook System
1309
1507
 
1310
- The library provides a comprehensive hook system for customizing behavior at every stage of request processing.
1311
-
1312
- ### Hook Execution Order by Method
1313
-
1314
- Each API method has its own specific hook execution order. Here's the exact sequence for each method:
1315
-
1316
- #### QUERY Method Hooks
1317
- ```
1318
- 1. beforeData
1319
- 2. beforeDataQuery
1320
- 3. knexQueryFiltering (multiple handlers for built-in filters and optional server-side policies)
1321
- - polymorphicFiltersHook
1322
- - crossTableFiltersHook
1323
- - basicFiltersHook
1324
- 4. enrichRecord (for each record)
1325
- 5. finish
1326
- 6. finishQuery
1327
- ```
1328
-
1329
- #### GET Method Hooks
1330
- ```
1331
- 1. beforeData
1332
- 2. beforeDataGet
1333
- 3. checkDataPermissions
1334
- 4. checkDataPermissionsGet
1335
- 5. enrichRecord
1336
- 6. enrichRecordWithRelationships
1337
- 7. finish
1338
- 8. finishGet
1339
- ```
1340
-
1341
- #### POST Method Hooks
1342
- ```
1343
- 1. beforeProcessing
1344
- 2. beforeProcessingPost
1345
- 3. beforeSchemaValidate
1346
- 4. beforeSchemaValidatePost
1347
- 5. afterSchemaValidatePost
1348
- 6. afterSchemaValidate
1349
- 7. beforeDataCall
1350
- 8. beforeDataCallPost
1351
- 9. [Database INSERT operation]
1352
- 10. afterDataCallPost
1353
- 11. afterDataCall
1354
- 12. finish
1355
- 13. finishPost
1356
- 14. afterCommit (if transaction was created)
1357
- ```
1358
-
1359
- #### PUT Method Hooks
1360
- ```
1361
- 1. beforeProcessing
1362
- 2. beforeProcessingPut
1363
- 3. beforeSchemaValidate
1364
- 4. beforeSchemaValidatePut
1365
- 5. afterSchemaValidatePut
1366
- 6. afterSchemaValidate
1367
- 7. beforeDataCall
1368
- 8. beforeDataCallPut
1369
- 9. [Database UPDATE operation - full replacement]
1370
- 10. afterDataCallPut
1371
- 11. afterDataCall
1372
- 12. finish
1373
- 13. finishPut
1374
- 14. afterCommit (if transaction was created)
1375
- ```
1376
-
1377
- #### PATCH Method Hooks
1378
- ```
1379
- 1. beforeProcessing
1380
- 2. beforeProcessingPatch
1381
- 3. beforeSchemaValidate
1382
- 4. beforeSchemaValidatePatch
1383
- 5. afterSchemaValidatePatch
1384
- 6. afterSchemaValidate
1385
- 7. beforeDataCall
1386
- 8. beforeDataCallPatch
1387
- 9. [Database UPDATE operation - partial update]
1388
- 10. afterDataCallPatch
1389
- 11. afterDataCall
1390
- 12. finish
1391
- 13. finishPatch
1392
- 14. afterCommit (if transaction was created)
1393
- ```
1394
-
1395
- #### DELETE Method Hooks
1396
- ```
1397
- 1. beforeDataCall
1398
- 2. beforeDataCallDelete
1399
- 3. [Database DELETE operation]
1400
- 4. afterDataCallDelete
1401
- 5. afterDataCall
1402
- 6. finish
1403
- 7. finishDelete
1404
- 8. afterCommit (if transaction was created)
1405
- ```
1406
-
1407
- #### Relationship Method Hooks
1408
-
1409
- **getRelated:**
1410
- ```
1411
- 1. checkPermissions
1412
- 2. checkPermissionsGetRelated
1413
- 3. [Delegates to GET or QUERY methods internally]
1414
- ```
1415
-
1416
- **getRelationship:**
1417
- ```
1418
- 1. checkPermissions
1419
- 2. checkPermissionsGetRelationship
1420
- 3. [Delegates to GET method internally]
1421
- ```
1422
-
1423
- **postRelationship:**
1424
- ```
1425
- 1. checkPermissions
1426
- 2. checkPermissionsPostRelationship
1427
- 3. [Relationship manipulation]
1428
- 4. finish
1429
- 5. finishPostRelationship
1430
- ```
1431
-
1432
- **patchRelationship:**
1433
- ```
1434
- 1. checkPermissions
1435
- 2. checkPermissionsPatchRelationship
1436
- 3. [Delegates to PATCH method internally]
1437
- 4. finish
1438
- 5. finishPatchRelationship
1439
- ```
1440
-
1441
- **deleteRelationship:**
1442
- ```
1443
- 1. checkPermissions
1444
- 2. checkPermissionsDeleteRelationship
1445
- 3. [Relationship manipulation]
1446
- 4. finish
1447
- 5. finishDeleteRelationship
1448
- ```
1449
-
1450
- ### Key Differences Between Methods
1451
-
1452
- 1. **Processing Hooks**: Only POST, PUT, and PATCH have `beforeProcessing` hooks
1453
- 2. **Schema Validation**: Only POST, PUT, and PATCH have schema validation hooks
1454
- 3. **Permission Hooks**: GET uses `checkDataPermissions`, while relationship methods use `checkPermissions`
1455
- 4. **Query Filtering**: Collection queries and storage-backed scoped lookups trigger `knexQueryFiltering`; use `RowPolicyPlugin` for mandatory visibility rather than relying on method-specific hook assumptions
1456
- 5. **Enrichment**: Only GET and QUERY have `enrichRecord` hooks
1457
- 6. **Relationships**: Only GET has `enrichRecordWithRelationships`
1458
- 7. **Transactions**: All write methods (POST, PUT, PATCH, DELETE) can trigger `afterCommit`/`afterRollback`
1459
-
1460
- ### Hook Context Objects
1461
-
1462
- Each hook receives a context object with different properties based on the hook type and method:
1463
-
1464
- #### beforeProcessing / beforeProcessing[Method]
1465
-
1466
- ```javascript
1467
- {
1468
- method: 'post', // HTTP method
1469
- resourceType: 'articles', // Resource being accessed
1470
- params: { // Request parameters
1471
- inputRecord: {...},
1472
- queryParams: {...},
1473
- simplified: true
1474
- },
1475
- auth: {...}, // App-provided auth/identity context if present
1476
- transaction: {...}, // Database transaction
1477
- schemaInfo: {...}, // Resource schema
1478
- db: {...} // Database connection
1479
- }
1480
- ```
1481
-
1482
- **What can be modified:**
1483
- - `params` - Modify input data
1484
- - Add custom properties to context
1485
-
1486
- **Example:**
1487
- ```javascript
1488
- api.resource('articles').hook('beforeProcessingPost', async (context) => {
1489
- // Add default status if not provided
1490
- if (context.params.inputRecord && !context.params.inputRecord.status) {
1491
- context.params.inputRecord.status = 'draft';
1492
- }
1493
-
1494
- // Add metadata to context
1495
- context.requestTime = new Date();
1496
- });
1497
- ```
1498
-
1499
- #### beforeSchemaValidate / afterSchemaValidate
1500
-
1501
- ```javascript
1502
- {
1503
- method: 'patch',
1504
- resourceType: 'articles',
1505
- inputData: { // Parsed input data
1506
- attributes: {...},
1507
- relationships: {...}
1508
- },
1509
- existingRecord: {...}, // For update operations
1510
- auth: {...},
1511
- transaction: {...},
1512
- schemaInfo: {...}
1513
- }
1514
- ```
1515
-
1516
- **What can be modified:**
1517
- - `inputData` - Modify before/after validation
1518
- - Throw errors for custom validation
1519
-
1520
- **Example:**
1521
- ```javascript
1522
- api.resource('articles').hook('afterSchemaValidate', async (context) => {
1523
- // Custom validation
1524
- if (context.inputData.attributes.status === 'published' &&
1525
- !context.inputData.attributes.published_at) {
1526
- throw new Error('Published articles must have a published_at date');
1527
- }
1528
- });
1529
- ```
1530
-
1531
- #### checkDataPermissions / checkDataPermissions[Method]
1532
-
1533
- ```javascript
1534
- {
1535
- method: 'delete',
1536
- resourceType: 'articles',
1537
- id: '1', // For single resource operations
1538
- auth: {...},
1539
- existingRecord: {...}, // For update/delete
1540
- transaction: {...}
1541
- }
1542
- ```
1543
-
1544
- **Purpose:** Permission or visibility checks implemented by your app hooks. Throw an error to deny the operation.
1545
-
1546
- **Example:**
1547
- ```javascript
1548
- api.resource('articles').hook('checkDataPermissionsDelete', async (context) => {
1549
- // Only author or admin can delete
1550
- if (context.auth.userId !== context.existingRecord.relationships?.author?.data?.id &&
1551
- !context.auth.isAdmin) {
1552
- throw new Error('Unauthorized to delete this article');
1553
- }
1554
- });
1555
- ```
1556
-
1557
- #### beforeData / afterData
1558
-
1559
- ```javascript
1560
- {
1561
- method: 'get',
1562
- resourceType: 'articles',
1563
- storageParams: { // Parameters for storage layer
1564
- id: '1',
1565
- include: ['author'],
1566
- fields: {...},
1567
- filters: {...}
1568
- },
1569
- result: {...}, // After data operations
1570
- auth: {...},
1571
- transaction: {...},
1572
- schemaInfo: {...}
1573
- }
1574
- ```
1575
-
1576
- **What can be modified:**
1577
- - `storageParams` (beforeData) - Modify query parameters
1578
- - `result` (afterData) - Modify query results
1579
-
1580
- **Example:**
1581
- ```javascript
1582
- api.resource('articles').hook('beforeDataQuery', async (context) => {
1583
- // Add automatic filtering based on user
1584
- if (context.auth.userId && !context.auth.isAdmin) {
1585
- context.storageParams.filters = {
1586
- ...context.storageParams.filters,
1587
- author: context.auth.userId
1588
- };
1589
- }
1590
- });
1591
- ```
1592
-
1593
- #### enrichRecord
1594
-
1595
- ```javascript
1596
- {
1597
- method: 'get',
1598
- resourceType: 'articles',
1599
- record: { // Full JSON:API record
1600
- type: 'articles',
1601
- id: '1',
1602
- attributes: {...},
1603
- relationships: {...}
1604
- },
1605
- isMainResource: true, // vs included resource
1606
- auth: {...},
1607
- requestedFields: [...], // Fields requested via sparse fieldsets
1608
- parentContext: {...} // Parent request context
1609
- }
1610
- ```
1611
-
1612
- **What can be modified:**
1613
- - `record` - Modify the entire record structure
1614
-
1615
- **Example:**
1616
- ```javascript
1617
- api.resource('articles').hook('enrichRecord', async (context) => {
1618
- // Add metadata
1619
- context.record.meta = {
1620
- can_edit: context.auth.userId === context.record.attributes.author_id,
1621
- version: context.record.attributes.version || 1
1622
- };
1623
- });
1624
- ```
1625
-
1626
- #### enrichAttributes
1627
-
1628
- ```javascript
1629
- {
1630
- method: 'get',
1631
- resourceType: 'articles',
1632
- attributes: {...}, // Current attributes
1633
- requestedComputedFields: ['word_count', 'reading_time'],
1634
- isMainResource: true,
1635
- record: {...}, // Full record for reference
1636
- auth: {...},
1637
- parentContext: {...},
1638
- computedDependencies: Set // Fields to remove if not requested
1639
- }
1640
- ```
1641
-
1642
- **What can be modified:**
1643
- - `attributes` - Add/modify attribute values
1644
-
1645
- **Example:**
1646
- ```javascript
1647
- api.resource('articles').hook('enrichAttributes', async (context) => {
1648
- // Add computed fields
1649
- if (context.requestedComputedFields.includes('word_count')) {
1650
- context.attributes.word_count =
1651
- context.attributes.content.split(/\s+/).length;
1652
- }
1653
-
1654
- if (context.requestedComputedFields.includes('reading_time')) {
1655
- const wordsPerMinute = 200;
1656
- context.attributes.reading_time =
1657
- Math.ceil(context.attributes.word_count / wordsPerMinute);
1508
+ Register hooks with `api.customize({ hooks })`, or with a resource's `extras`
1509
+ when declaring it. Handlers receive an injected argument object containing
1510
+ `context`; they do not receive that context as the entire first argument.
1511
+ The library does not provide the `api.resource(...).hook(...)` API used by older
1512
+ examples in this reference.
1513
+
1514
+ ```javascript
1515
+ await api.customize({
1516
+ hooks: {
1517
+ beforeSchemaValidatePost: {
1518
+ functionName: 'trim-article-title',
1519
+ handler: ({ context }) => {
1520
+ if (context.scopeName !== 'articles') return;
1521
+ const attributes = context.inputRecord.data.attributes;
1522
+ if (typeof attributes?.title === 'string') attributes.title = attributes.title.trim();
1523
+ }
1524
+ }
1658
1525
  }
1659
1526
  });
1660
1527
  ```
1661
1528
 
1662
- #### finish / finish[Method]
1663
-
1664
- ```javascript
1665
- {
1666
- method: 'post',
1667
- resourceType: 'articles',
1668
- response: { // Final response object
1669
- data: {...},
1670
- included: [...],
1671
- meta: {...}
1672
- },
1673
- auth: {...}
1674
- }
1675
- ```
1676
-
1677
- **What can be modified:**
1678
- - `response` - Final modifications to response
1679
-
1680
- **Example:**
1681
- ```javascript
1682
- api.resource('articles').hook('finish', async (context) => {
1683
- // Add response metadata
1684
- context.response.meta = {
1685
- ...context.response.meta,
1686
- generated_at: new Date().toISOString(),
1687
- };
1688
- });
1689
- ```
1690
-
1691
- #### afterCommit / afterRollback
1529
+ ### Complete Hook Execution Order
1692
1530
 
1693
- ```javascript
1694
- {
1695
- method: 'post',
1696
- resourceType: 'articles',
1697
- result: {...}, // Operation result
1698
- error: {...}, // For rollback
1699
- auth: {...},
1700
- params: {...} // Original parameters
1701
- }
1702
- ```
1531
+ POST, PUT and PATCH retain their method-specific behavior within this sequence:
1703
1532
 
1704
- **Use cases:**
1705
- - Send emails, notifications
1706
- - Clear caches
1707
- - Log events
1708
- - Cleanup on failure
1533
+ 1. Set up the transaction and response options; run `beforeProcessing`, then the
1534
+ method-specific processing hook.
1535
+ 2. Validate the request/relationships and perform the method's existence checks.
1536
+ Run `beforeSchemaValidate`, then its method-specific hook; validate attributes;
1537
+ run method-specific `afterSchemaValidate`, then `afterSchemaValidate`.
1538
+ 3. Authorize the write; run `beforeDataCall`, then its method-specific hook.
1539
+ 4. Lock the required records, apply setters and perform the storage operation.
1540
+ 5. Run method-specific `afterDataCall`, then `afterDataCall`; update relationships
1541
+ and refresh minimal stored data.
1542
+ 6. Prepare the selected response; run `finish`, then its method-specific hook.
1543
+ 7. An owning operation commits and awaits `afterCommit`. Managed participants
1544
+ defer completion to their owner.
1709
1545
 
1710
- **Example:**
1711
- ```javascript
1712
- api.resource('articles').hook('afterCommit', async (context) => {
1713
- if (context.method === 'post') {
1714
- // Send notification email
1715
- await emailService.sendNewArticleNotification({
1716
- articleId: context.result.data.id,
1717
- authorId: context.auth.userId
1718
- });
1719
- }
1720
- });
1721
- ```
1722
-
1723
- ### Method-Specific Hooks
1724
-
1725
- You can register hooks for specific methods by appending the method name:
1726
-
1727
- ```javascript
1728
- // Runs only for POST requests
1729
- api.resource('articles').hook('beforeDataPost', async (context) => {
1730
- context.inputData.attributes.created_by = context.auth.userId;
1731
- });
1546
+ `returning: 'full'` performs a nested GET during response preparation. Its GET
1547
+ hooks observe `method: 'get'`, while outer write hooks retain their write method.
1548
+ The nested read keeps authentication and transaction identity. None/minimal
1549
+ responses do not imply that validation, setters or finish hooks are skipped.
1732
1550
 
1733
- // Runs only for PATCH requests
1734
- api.resource('articles').hook('beforeDataPatch', async (context) => {
1735
- context.inputData.attributes.updated_by = context.auth.userId;
1736
- context.inputData.attributes.updated_at = new Date().toISOString();
1737
- });
1551
+ GET and QUERY run `beforeData` and their method-specific hook before reading.
1552
+ They run enrichment and finish hooks over the internal JSON:API record before
1553
+ final output conversion. GET additionally runs data-permission checks and
1554
+ `enrichRecordWithRelationships`. DELETE and relationship writes have their own
1555
+ sequences; use the [hook and lifecycle guide](GUIDE/13-hooks-and-lifecycle.md)
1556
+ for those contracts rather than extrapolating POST's stages.
1738
1557
 
1739
- // Runs only for DELETE requests
1740
- api.resource('articles').hook('beforeDataDelete', async (context) => {
1741
- // Archive instead of delete
1742
- context.softDelete = true;
1743
- context.inputData = {
1744
- attributes: {
1745
- deleted_at: new Date().toISOString(),
1746
- deleted_by: context.auth.userId
1747
- }
1748
- };
1749
- });
1750
- ```
1558
+ ### Hook Context Objects
1751
1559
 
1752
- ### Query-Specific Hooks
1560
+ Ordinary operation hooks share the caller's mutable context. They may add custom
1561
+ properties for later hooks and change documented write values at the appropriate
1562
+ stage. Mutations remain visible to the caller after completion. Use a separate
1563
+ context for every independent operation, especially concurrent calls.
1564
+
1565
+ Ordinary CRUD permission hooks receive a wrapper whose `originalContext` points
1566
+ to that operation context. Relationship permission hooks receive the operation
1567
+ directly. Read-enrichment wrappers expose the read context as `parentContext`.
1568
+ Library-created nested calls generally use a shallow copy: top-level replacements
1569
+ are local to the child, while referenced application objects and caches remain
1570
+ shared. This is not deep isolation. See the [complete hook contract](GUIDE/13-hooks-and-lifecycle.md)
1571
+ for precise hook arguments, inheritance and reserved fields.
1572
+
1573
+ Context availability is stage-specific:
1574
+
1575
+ | Stage | Fields and use |
1576
+ | --- | --- |
1577
+ | Write processing/schema validation | `method`, `scopeName`, `inputRecord`, `format`, `returning`, `queryParams`, `schemaInfo`, `transaction`, `db`; either public input becomes the normalized JSON:API `context.inputRecord`. Modify `context.inputRecord.data.attributes` before validation when preparing input. |
1578
+ | After attribute validation | `context.inputRecord.data.attributes` contains validated values; setters run later. `originalInputAttributes` retains the pre-validation attribute snapshot. |
1579
+ | After storage write | POST has assigned its storage ID; after-data hooks see transformed input attributes. Method-specific PUT/PATCH behavior remains visible. |
1580
+ | Read enrichment/finish | `record` is the internal JSON:API document. Final normalization/field filtering and plain conversion happen after finish. |
1581
+ | Write finish | `responseRecord` holds the selected output; minimal stored data is separately available. A full response may already have run nested GET hooks. |
1582
+ | Completion | `afterCommit` follows acknowledged commit; `afterRollback` follows acknowledged rollback. A managed participant finishes its data work before its owner's completion hooks. |
1583
+
1584
+ Application authentication is not defined by the library. Pass trusted
1585
+ application context from the caller/transport; do not infer it from payload data.
1586
+ Use [row policies](GUIDE/17-row-policies.md) for mandatory row visibility.
1587
+ Do not treat a single GET permission hook as a universal query/relationship guard.
1588
+
1589
+ An ordinary failure stops later stages. A post-commit failure cannot undo the
1590
+ write; unknown outcomes require reconciliation. Original and secondary diagnostics
1591
+ remain subject to the [transaction/error contract](#transaction-support).
1753
1592
 
1754
1593
  #### knexQueryFiltering
1755
1594
 
1756
- Special hook for modifying database queries:
1757
-
1758
- ```javascript
1759
- api.resource('articles').hook('knexQueryFiltering', async (context) => {
1760
- const { query, filters, resourceSchema } = context;
1761
-
1762
- // Add custom where clauses
1763
- if (filters.search) {
1764
- query.where(function() {
1765
- this.where('title', 'like', `%${filters.search}%`)
1766
- .orWhere('content', 'like', `%${filters.search}%`);
1767
- });
1768
- }
1769
-
1770
- // Add joins for complex filtering
1771
- if (filters.author_name) {
1772
- query.join('users', 'articles.author_id', 'users.id')
1773
- .where('users.name', 'like', `%${filters.author_name}%`);
1774
- }
1775
- });
1776
- ```
1777
-
1778
- ---
1595
+ Query customization operates on native builders with explicit storage mapping.
1596
+ Use the [query hook migration contract](GUIDE/33-migrating-to-v2.md#native-query-builders-and-explicit-custom-filter-translation)
1597
+ for aliases, logical/physical fields and canonical tenant/resource scoping.
1598
+ Keep mandatory predicates in the supported policy/filtering mechanisms. A raw
1599
+ query fragment is not automatically translated or made tenant-safe.
1779
1600
 
1780
1601
  ## Query Features
1781
1602
 
@@ -1802,83 +1623,61 @@ const result = await api.resources.articles.query({
1802
1623
 
1803
1624
  #### Operator-based Filtering
1804
1625
 
1805
- Filters support various operators when defined in the resource schema:
1626
+ Name filters explicitly in `searchSchema`, selecting the underlying field and
1627
+ operator. Field type names and `filterOperator` values follow their actual
1628
+ contracts; an arbitrary `operators` array does not register filter aliases.
1806
1629
 
1807
1630
  ```javascript
1808
- // Resource schema configuration
1809
1631
  searchSchema: {
1810
- created_at: {
1811
- type: 'datetime',
1812
- operators: ['gt', 'gte', 'lt', 'lte']
1813
- },
1814
- title: {
1815
- type: 'string',
1816
- operators: ['eq', 'like', 'ilike']
1817
- },
1818
- view_count: {
1819
- type: 'number',
1820
- operators: ['eq', 'gt', 'gte', 'lt', 'lte', 'in']
1821
- }
1632
+ publishedAfter: { type: 'dateTime', actualField: 'published_at', filterOperator: '>=' },
1633
+ titleContains: { type: 'string', actualField: 'title', filterOperator: 'contains' },
1634
+ above: { type: 'number', actualField: 'view_count', filterOperator: '>' },
1635
+ articleIds: { type: 'array', actualField: 'id', filterOperator: 'in' }
1822
1636
  }
1637
+ ```
1638
+
1639
+ With those declarations:
1823
1640
 
1824
- // Usage
1641
+ ```javascript
1825
1642
  const result = await api.resources.articles.query({
1826
- queryParams: {
1827
- filters: {
1828
- 'created_at:gte': '2024-01-01',
1829
- 'created_at:lt': '2024-02-01',
1830
- 'title:like': '%javascript%',
1831
- 'view_count:gt': 100
1832
- }
1833
- }
1643
+ queryParams: { filters: { titleContains: 'javascript', above: 100 } }
1834
1644
  });
1835
1645
  ```
1836
1646
 
1837
1647
  #### Array Filters (IN operator)
1838
1648
 
1839
1649
  ```javascript
1840
- // Find articles with specific IDs
1841
1650
  const result = await api.resources.articles.query({
1842
- queryParams: {
1843
- filters: {
1844
- 'id:in': ['1', '2', '3'],
1845
- 'status:in': ['published', 'featured']
1846
- }
1847
- }
1651
+ queryParams: { filters: { articleIds: ['1', '2', '3'] } }
1848
1652
  });
1849
-
1850
- // HTTP equivalent (comma-separated)
1851
- // GET /articles?filter[id:in]=1,2,3&filter[status:in]=published,featured
1852
1653
  ```
1853
1654
 
1655
+ Use the [structured-query transport contract](GUIDE/33-migrating-to-v2.md) when
1656
+ encoding non-scalar filter values for HTTP. Declared field equality/search
1657
+ capabilities and mandatory row policies still apply.
1658
+
1854
1659
  #### Custom Filter Logic
1855
1660
 
1856
- Use the `knexQueryFiltering` hook for complex filtering:
1661
+ Declare custom filters in the resource's `searchSchema`. `applyFilter` receives
1662
+ the native builder, validated filter input and explicit mapping helpers:
1857
1663
 
1858
1664
  ```javascript
1859
- api.resource('articles').hook('knexQueryFiltering', async (context) => {
1860
- const { query, filters } = context;
1861
-
1862
- // Full-text search
1863
- if (filters.q) {
1864
- query.whereRaw("to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', ?)", [filters.q]);
1865
- }
1866
-
1867
- // Date range
1868
- if (filters.date_from && filters.date_to) {
1869
- query.whereBetween('created_at', [filters.date_from, filters.date_to]);
1870
- }
1871
-
1872
- // Complex boolean logic
1873
- if (filters.featured_or_trending) {
1874
- query.where(function() {
1875
- this.where('is_featured', true)
1876
- .orWhere('trending_score', '>', 0.8);
1877
- });
1665
+ searchSchema: {
1666
+ minimumRank: {
1667
+ type: 'number',
1668
+ applyFilter(query, input, { column, value }) {
1669
+ query.where(column('rank'), '>=', value('rank', input));
1670
+ }
1878
1671
  }
1879
- });
1672
+ }
1880
1673
  ```
1881
1674
 
1675
+ Here `rank` must be a declared stored field. Mapping helpers resolve the active
1676
+ alias and storage representation in either storage mode. Callbacks run
1677
+ synchronously and must mutate the supplied builder. Raw SQL and extra joins
1678
+ remain responsible for their own physical columns and visibility semantics.
1679
+ See [custom-filter migration](GUIDE/33-migrating-to-v2.md#native-query-builders-and-explicit-custom-filter-translation).
1680
+
1882
1681
  ### Sorting
1883
1682
 
1884
1683
  Control the order of results using the `sort` parameter.
@@ -1932,93 +1731,55 @@ const result = await api.resources.articles.query({
1932
1731
 
1933
1732
  ### Pagination
1934
1733
 
1935
- The library supports multiple pagination strategies:
1734
+ | `queryParams.page` | Behavior |
1735
+ | --- | --- |
1736
+ | Omitted or `{}` | Capped default limit, no pagination metadata |
1737
+ | `{ number: 2, size: 20 }` | Offset page 2 |
1738
+ | `{ size: 20 }` | First cursor page |
1739
+ | `{ after: cursor, size: 20 }` | Forward cursor page |
1740
+ | `{ before: cursor, size: 20 }` | Backward cursor page |
1741
+
1742
+ Use one of `number`, `after` or `before`. Sizes/numbers must be positive integers;
1743
+ empty or malformed cursors reject. Treat cursors as opaque and follow generated
1744
+ links to preserve the resource, selection, filters, sort and effective size.
1745
+ Do not construct base64 cursor payloads yourself.
1936
1746
 
1937
1747
  #### Offset Pagination
1938
1748
 
1939
1749
  ```javascript
1940
- // Page-based pagination
1941
1750
  const result = await api.resources.articles.query({
1942
- queryParams: {
1943
- page: {
1944
- number: 2,
1945
- size: 20
1946
- }
1947
- }
1751
+ queryParams: { page: { number: 2, size: 20 } }
1948
1752
  });
1949
-
1950
- // Response includes:
1951
- // {
1952
- // data: [...],
1953
- // meta: {
1954
- // page: {
1955
- // total: 150, // Total records (if enablePaginationCounts: true)
1956
- // size: 20, // Page size
1957
- // number: 2, // Current page
1958
- // totalPages: 8 // Total pages (if counts enabled)
1959
- // }
1960
- // },
1961
- // links: {
1962
- // first: '/articles?page[number]=1&page[size]=20',
1963
- // prev: '/articles?page[number]=1&page[size]=20',
1964
- // self: '/articles?page[number]=2&page[size]=20',
1965
- // next: '/articles?page[number]=3&page[size]=20',
1966
- // last: '/articles?page[number]=8&page[size]=20'
1967
- // }
1968
- // }
1969
-
1970
- // HTTP equivalent
1971
- // GET /articles?page[number]=2&page[size]=20
1753
+ // With counts enabled and 150 matching records:
1754
+ // result.meta.pagination = { page: 2, pageSize: 20, pageCount: 8, total: 150, hasMore: true }
1972
1755
  ```
1973
1756
 
1757
+ Pagination metadata is under `meta.pagination`. With counts disabled, it includes
1758
+ `page` and `pageSize`, without
1759
+ promising totals or `hasMore`. The library supplies pagination links appropriate
1760
+ to the available count/boundary information.
1761
+
1974
1762
  #### Cursor Pagination
1975
1763
 
1976
1764
  ```javascript
1977
- // Forward pagination
1978
- const result = await api.resources.articles.query({
1979
- queryParams: {
1980
- page: {
1981
- after: 'eyJpZCI6MTAwLCJjcmVhdGVkX2F0IjoiMjAyNC0wMS0xNSJ9',
1982
- size: 10
1983
- }
1984
- }
1765
+ const firstPage = await api.resources.articles.query({
1766
+ queryParams: { page: { size: 20 } }
1985
1767
  });
1986
-
1987
- // Backward pagination
1988
- const result = await api.resources.articles.query({
1989
- queryParams: {
1990
- page: {
1991
- before: 'eyJpZCI6NTAsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTEwIn0=',
1992
- size: 10
1993
- }
1994
- }
1995
- });
1996
-
1997
- // Response includes:
1998
- // {
1999
- // data: [...],
2000
- // meta: {
2001
- // page: {
2002
- // hasMore: true, // More records available
2003
- // size: 10 // Page size
2004
- // }
2005
- // },
2006
- // links: {
2007
- // prev: '/articles?page[before]=...',
2008
- // self: '/articles?page[after]=...',
2009
- // next: '/articles?page[after]=...'
2010
- // }
2011
- // }
1768
+ // Follow firstPage.links.next through the HTTP client, or use the returned
1769
+ // cursor with the same query configuration for a programmatic next-page call.
2012
1770
  ```
2013
1771
 
1772
+ Cursor pages use an extra row for `hasMore`; they never issue an offset count
1773
+ query. See [pagination and ordering](GUIDE/09-pagination-and-sorting.md)
1774
+ for cursor metadata, link traversal and ordering guarantees.
1775
+
2014
1776
  #### Pagination Configuration
2015
1777
 
2016
1778
  ```javascript
2017
- // Configure in plugin
2018
- const restApiPlugin = new RestApiPlugin({
2019
- queryDefaultLimit: 20, // Default page size
2020
- queryMaxLimit: 100, // Maximum allowed page size
2021
- enablePaginationCounts: true // Enable total counts (may impact performance)
1779
+ await api.use(RestApiPlugin, {
1780
+ queryDefaultLimit: 20,
1781
+ queryMaxLimit: 100,
1782
+ enablePaginationCounts: true
2022
1783
  });
2023
1784
  ```
2024
1785
 
@@ -2105,24 +1866,13 @@ const result = await api.resources.articles.query({
2105
1866
 
2106
1867
  #### Include with Filtering
2107
1868
 
2108
- Some implementations support filtering included resources:
2109
-
2110
- ```javascript
2111
- // Custom hook to filter included resources
2112
- api.resource('articles').hook('afterDataQuery', async (context) => {
2113
- if (context.result.included) {
2114
- // Filter included comments to only show approved
2115
- context.result.included = context.result.included.filter(resource => {
2116
- if (resource.type === 'comments') {
2117
- return resource.attributes.status === 'approved';
2118
- }
2119
- return true;
2120
- });
2121
- }
2122
- });
2123
- ```
2124
-
2125
- ---
1869
+ Use target-resource policies and supported query/filter configuration to control
1870
+ which related records are visible. Filtering `included` after fetching does not
1871
+ remove corresponding linkage, authorize the target rows, or establish correct
1872
+ per-parent limits. The library does not provide an `afterDataQuery` hook for that
1873
+ purpose. See [row policies](GUIDE/17-row-policies.md),
1874
+ [include configuration](GUIDE/33-migrating-to-v2.md#include-configuration-is-validated-before-publication)
1875
+ and the query hook contract above.
2126
1876
 
2127
1877
  ## Configuration Options
2128
1878
 
@@ -2130,40 +1880,17 @@ api.resource('articles').hook('afterDataQuery', async (context) => {
2130
1880
 
2131
1881
  ```javascript
2132
1882
  await api.use(RestApiPlugin, {
2133
- // API behavior
2134
- simplifiedApi: true, // Use simplified mode for programmatic calls (default: true)
2135
- simplifiedTransport: false, // Use JSON:API for HTTP transport (default: false)
2136
- normalizeId: (value) => {
2137
- if (value === null || value === undefined) {
2138
- return null
2139
- }
2140
-
2141
- const normalized = String(value).trim()
2142
- return normalized ? normalized.toUpperCase() : null
2143
- },
2144
-
2145
- // Return record configuration
2146
- returnRecordApi: {
2147
- post: 'full', // Return full record after create (default)
2148
- put: 'full', // Return full record after replace (default)
2149
- patch: 'full' // Return full record after update (default)
2150
- },
2151
-
2152
- returnRecordTransport: {
2153
- post: 'no', // Return 204 for HTTP POST (default)
2154
- put: 'no', // Return 204 for HTTP PUT (default)
2155
- patch: 'no' // Return 204 for HTTP PATCH (default)
2156
- },
2157
-
2158
- // Query limits
2159
- queryDefaultLimit: 20, // Default pagination size
2160
- queryMaxLimit: 100, // Maximum allowed page size
2161
-
2162
- // Include depth
2163
- includeDepthLimit: 3, // Maximum relationship nesting depth
2164
-
2165
- // Performance
2166
- enablePaginationCounts: true // Execute count queries for total pages
1883
+ format: 'plain',
1884
+ returning: 'full',
1885
+ queryDefaultLimit: 20,
1886
+ queryMaxLimit: 100,
1887
+ includeDepthLimit: 3,
1888
+ enablePaginationCounts: true,
1889
+ normalizeId: value => {
1890
+ if (value === null || value === undefined) return null;
1891
+ const normalized = String(value).trim();
1892
+ return normalized ? normalized.toUpperCase() : null;
1893
+ }
2167
1894
  });
2168
1895
  ```
2169
1896
 
@@ -2173,162 +1900,52 @@ await api.use(RestApiPlugin, {
2173
1900
 
2174
1901
  ### Resource Schema Structure
2175
1902
 
1903
+ Pass the resource name separately from its options. `schema` is the field map;
1904
+ relationships and search configuration are sibling resource options.
1905
+
2176
1906
  ```javascript
2177
- api.addResource({
2178
- name: 'articles',
2179
-
2180
- // Primary key configuration
2181
- idProperty: 'id', // Physical primary-key column name for table-backed resources
2182
-
1907
+ await api.addResource('articles', {
1908
+ tableName: 'articles',
1909
+ sortableFields: ['id', 'title'],
2183
1910
  schema: {
2184
- // Attributes
2185
- attributes: {
2186
- title: {
2187
- type: 'string',
2188
- required: true,
2189
- maxLength: 200
2190
- },
2191
- content: {
2192
- type: 'string',
2193
- required: true
2194
- },
2195
- status: {
2196
- type: 'string',
2197
- enum: ['draft', 'published', 'archived'],
2198
- default: 'draft'
2199
- },
2200
- published_at: {
2201
- type: 'datetime',
2202
- nullable: true
2203
- },
2204
- metadata: {
2205
- type: 'object',
2206
- // Custom getter/setter for data transformation
2207
- getter: (value) => JSON.parse(value || '{}'),
2208
- setter: (value) => JSON.stringify(value)
2209
- },
2210
- price: {
2211
- type: 'number',
2212
- // Store as cents, display as dollars
2213
- getter: (value) => value / 100,
2214
- setter: (value) => Math.round(value * 100)
2215
- }
2216
- },
2217
-
2218
- // Virtual fields (excluded from database operations)
2219
- virtualFields: ['temp_data', 'ui_state'],
2220
-
2221
- // Relationships
2222
- relationships: {
2223
- author: {
2224
- type: 'users',
2225
- required: true,
2226
- relationshipType: 'belongsTo',
2227
- foreignKey: 'author_id' // Explicit foreign key
2228
- },
2229
- category: {
2230
- type: 'categories',
2231
- relationshipType: 'belongsTo',
2232
- nullable: true
2233
- },
2234
- tags: {
2235
- type: 'tags',
2236
- relationshipType: 'manyToMany',
2237
- through: 'article_tags', // Junction table
2238
- pivotFields: ['sort_order'] // Additional pivot fields
2239
- },
2240
- comments: {
2241
- type: 'comments',
2242
- relationshipType: 'hasMany',
2243
- foreignKey: 'article_id'
2244
- },
2245
- // Polymorphic relationship
2246
- commentable: {
2247
- polymorphic: true,
2248
- types: ['articles', 'videos', 'photos'],
2249
- typeField: 'commentable_type',
2250
- idField: 'commentable_id'
2251
- }
2252
- },
2253
-
2254
- // Computed fields
2255
- computedFields: {
2256
- word_count: {
2257
- type: 'number',
2258
- compute: (record) => record.content.split(/\s+/).length,
2259
- dependencies: ['content'] // Recompute when content changes
2260
- },
2261
- reading_time: {
2262
- type: 'number',
2263
- compute: (record) => Math.ceil(record.word_count / 200),
2264
- dependencies: ['word_count']
2265
- },
2266
- full_name: {
2267
- type: 'string',
2268
- compute: (record) => `${record.first_name} ${record.last_name}`,
2269
- dependencies: ['first_name', 'last_name']
2270
- }
2271
- },
2272
-
2273
- // Hidden fields (never exposed in API)
2274
- hiddenFields: ['internal_notes', 'admin_flags'],
2275
-
2276
- // Search configuration
2277
- searchSchema: {
2278
- title: {
2279
- type: 'string',
2280
- operators: ['eq', 'like', 'ilike']
2281
- },
2282
- status: {
2283
- type: 'string',
2284
- operators: ['eq', 'in']
2285
- },
2286
- published_at: {
2287
- type: 'datetime',
2288
- operators: ['gt', 'gte', 'lt', 'lte']
2289
- },
2290
- author: {
2291
- type: 'number',
2292
- operators: ['eq', 'in']
2293
- },
2294
- view_count: {
2295
- type: 'number',
2296
- operators: ['eq', 'gt', 'gte', 'lt', 'lte', 'between']
2297
- }
2298
- },
2299
-
2300
- // No built-in permissions option exists here.
2301
- // Use permission hooks for access decisions, and optional plugins
2302
- // such as AutoFilterPlugin for equality-based dataset scoping or
2303
- // RowPolicyPlugin for mandatory SQL visibility predicates.
2304
-
2305
- // Soft delete configuration
2306
- softDelete: {
2307
- field: 'deleted_at',
2308
- includeDeleted: false
2309
- },
2310
-
2311
- // Custom validation
2312
- validate: async (data, method, context) => {
2313
- if (data.status === 'published' && !data.published_at) {
2314
- throw new Error('Published articles must have published_at date');
2315
- }
2316
-
2317
- if (method === 'post' && data.title.length < 10) {
2318
- throw new Error('Title must be at least 10 characters');
2319
- }
1911
+ id: { type: 'id' },
1912
+ title: { type: 'string', required: true, max: 200, search: true },
1913
+ content: { type: 'string', required: true },
1914
+ published_at: { type: 'dateTime', nullable: true },
1915
+ metadata: { type: 'object', nullable: true },
1916
+ internal_notes: { type: 'string', hidden: true },
1917
+ preview: { type: 'boolean', virtual: true },
1918
+ word_count: {
1919
+ type: 'number', computed: true, dependencies: ['content'],
1920
+ compute: ({ attributes }) => attributes.content.trim().split(/\s+/).filter(Boolean).length
2320
1921
  }
1922
+ },
1923
+ searchSchema: {
1924
+ titleContains: { type: 'string', actualField: 'title', filterOperator: 'contains' }
2321
1925
  }
2322
1926
  });
1927
+ await api.resources.articles.createKnexTable();
2323
1928
  ```
2324
1929
 
1930
+ Stored belongs-to fields declare `belongsTo: 'users'` and `as: 'author'` on their
1931
+ schema field. Reverse and many-to-many relationships live in the sibling
1932
+ `relationships` map, for example `{ type: 'hasMany', target: 'comments',
1933
+ foreignKey: 'article_id' }`. A many-to-many definition specifies `target`,
1934
+ `through`, `foreignKey` and `otherKey`; register its target and pivot resources.
1935
+ Use the [relationship guides](GUIDE/02-resources-and-relationships.md) for complete
1936
+ related declarations, nullability and cardinality rules.
1937
+
1938
+ Schema compilation snapshots declarations. Install enrichment hooks before
1939
+ registration; changing an earlier declaration object does not recompile a live
1940
+ resource. See [schema compilation](GUIDE/33-migrating-to-v2.md#schema-compilation-snapshots-declarations)
1941
+ and [table/schema helper contracts](GUIDE/21-schema-and-migrations.md).
1942
+
2325
1943
  ### Important Schema Features
2326
1944
 
2327
1945
  #### ID Property Configuration
2328
1946
  ```javascript
2329
1947
  // Physical primary key column: user_id
2330
- api.addResource({
2331
- name: 'users',
1948
+ await api.addResource('users', {
2332
1949
  idProperty: 'user_id',
2333
1950
  schema: {
2334
1951
  id: { type: 'id', required: true, storage: { column: 'user_id' } },
@@ -2338,7 +1955,7 @@ api.addResource({
2338
1955
  });
2339
1956
  ```
2340
1957
 
2341
- `idProperty` names the physical primary-key column for table-backed resources. At the API layer, the resource id remains `id`, so writes use `inputRecord.id` or `inputRecord.data.id`, reads return `id` or `data.id`, and the resource id is not part of `attributes`.
1958
+ `idProperty` names the physical primary-key column for table-backed resources. At the API layer, the resource id remains `id`, so writes use `data.id` or `document.data.id`, reads return `id` or `data.id`, and the resource id is not part of `attributes`.
2342
1959
 
2343
1960
  #### Resource ID Normalization
2344
1961
 
@@ -2403,8 +2020,7 @@ Table-backed resources use snake_case physical columns by default. For example,
2403
2020
  Use `storage.column` when a field needs a specific column name:
2404
2021
 
2405
2022
  ```javascript
2406
- api.addResource({
2407
- name: 'profiles',
2023
+ await api.addResource('profiles', {
2408
2024
  schema: {
2409
2025
  id: { type: 'id' },
2410
2026
  displayName: { type: 'string', required: true },
@@ -2416,58 +2032,37 @@ api.addResource({
2416
2032
  If physical columns should match logical field names exactly for an entire resource, use `storage: { naming: 'exact' }`.
2417
2033
 
2418
2034
  #### Virtual Fields
2419
- Virtual fields are excluded from database operations but can be used for temporary UI state:
2420
2035
 
2421
- ```javascript
2422
- virtualFields: ['expanded', 'selected', 'temp_calculation']
2423
-
2424
- // These fields are ignored during database operations
2425
- const result = await api.resources.articles.post({
2426
- inputRecord: {
2427
- title: 'New Article',
2428
- expanded: true, // Ignored in database
2429
- selected: false // Ignored in database
2430
- }
2431
- });
2432
- ```
2036
+ Mark an individual schema field with `virtual: true`, for example
2037
+ `preview: { type: 'boolean', virtual: true }`. It is validated but omitted from
2038
+ database writes; it is not a persisted UI-state field. Use a setter when a
2039
+ virtual input needs to prepare other stored attributes. A `virtualFields` array
2040
+ is not the resource declaration contract. See [virtual-field behavior](GUIDE/12-field-transformations.md).
2433
2041
 
2434
2042
  #### Field Transformations
2435
- Use getters and setters for automatic data transformation:
2043
+
2044
+ Getters and setters receive a value and an injected context. Computed fields
2045
+ receive the context object with `attributes`, not a bare record positional
2046
+ argument. Declare dependencies when another field's value is needed.
2436
2047
 
2437
2048
  ```javascript
2438
- attributes: {
2439
- // JSON storage
2440
- settings: {
2441
- type: 'object',
2442
- getter: (value) => JSON.parse(value || '{}'),
2443
- setter: (value) => JSON.stringify(value)
2444
- },
2445
-
2446
- // Encryption
2447
- ssn: {
2448
- type: 'string',
2449
- getter: (value) => decrypt(value),
2450
- setter: (value) => encrypt(value)
2451
- },
2452
-
2453
- // Unit conversion
2454
- temperature_c: {
2455
- type: 'number',
2456
- getter: (value) => value, // Store as Celsius
2457
- setter: (value) => value
2458
- },
2049
+ schema: {
2050
+ temperature_c: { type: 'number' },
2459
2051
  temperature_f: {
2460
- type: 'number',
2461
- virtual: true,
2462
- getter: (record) => (record.temperature_c * 9/5) + 32,
2463
- setter: (value, record) => {
2464
- record.temperature_c = (value - 32) * 5/9;
2465
- }
2466
- }
2052
+ type: 'number', computed: true, dependencies: ['temperature_c'],
2053
+ compute: ({ attributes }) => attributes.temperature_c * 9 / 5 + 32
2054
+ },
2055
+ settings: { type: 'object', nullable: true }
2467
2056
  }
2468
2057
  ```
2469
2058
 
2470
- ---
2059
+ Object/array storage already serializes and decodes JSON; do not add
2060
+ JSON.stringify/JSON.parse callbacks solely to duplicate that conversion.
2061
+ Use a synchronous `storage.serialize` when writes and filter comparisons need
2062
+ a custom storage representation. Getters/setters may be asynchronous; their
2063
+ failures propagate under the documented error contract. See
2064
+ [field transformations](GUIDE/12-field-transformations.md) and
2065
+ [serializer migration](GUIDE/33-migrating-to-v2.md#temporal-values-and-storage-serializers).
2471
2066
 
2472
2067
  ## Error Handling
2473
2068
 
@@ -2475,158 +2070,148 @@ The library uses standard JSON:API error format:
2475
2070
 
2476
2071
  ### Error Response Format
2477
2072
 
2478
- ```javascript
2479
- {
2480
- errors: [
2481
- {
2482
- status: '422',
2483
- code: 'VALIDATION_ERROR',
2484
- title: 'Validation Failed',
2485
- detail: 'The title field is required.',
2486
- source: {
2487
- pointer: '/data/attributes/title'
2488
- },
2489
- meta: {
2490
- field: 'title',
2491
- rule: 'required'
2492
- }
2493
- }
2494
- ]
2495
- }
2496
- ```
2497
-
2498
- ### Common Error Types
2499
-
2500
- #### Validation Errors (422)
2501
- ```javascript
2502
- {
2503
- errors: [{
2504
- status: '422',
2505
- code: 'VALIDATION_ERROR',
2506
- title: 'Validation Failed',
2507
- detail: 'The email field must be a valid email address.',
2508
- source: { pointer: '/data/attributes/email' }
2509
- }]
2510
- }
2511
- ```
2073
+ HTTP connectors return JSON:API `errors` arrays. Fields depend on the mapped
2074
+ error; a programmatic error's `code` is not copied into every HTTP error object.
2075
+ For example, a resource not-found error maps to:
2512
2076
 
2513
- #### Not Found Errors (404)
2514
2077
  ```javascript
2515
2078
  {
2516
- errors: [{
2517
- status: '404',
2518
- code: 'RESOURCE_NOT_FOUND',
2519
- title: 'Resource Not Found',
2520
- detail: 'Article with id 999 not found.'
2521
- }]
2079
+ errors: [{ status: '404', title: 'Not Found', detail: 'Article not found' }]
2522
2080
  }
2523
2081
  ```
2524
2082
 
2525
- #### Permission Errors (403)
2526
- ```javascript
2527
- {
2528
- errors: [{
2529
- status: '403',
2530
- code: 'FORBIDDEN',
2531
- title: 'Forbidden',
2532
- detail: 'You do not have permission to update this article.'
2533
- }]
2534
- }
2535
- ```
2083
+ ### Common Error Types
2536
2084
 
2537
- #### Relationship Errors (400)
2538
- ```javascript
2539
- {
2540
- errors: [{
2541
- status: '400',
2542
- code: 'INVALID_RELATIONSHIP',
2543
- title: 'Invalid Relationship',
2544
- detail: 'Cannot set author to user 999: user does not exist.',
2545
- source: { pointer: '/data/relationships/author' }
2546
- }]
2547
- }
2548
- ```
2085
+ | Programmatic error/code | Default HTTP status |
2086
+ | --- | --- |
2087
+ | `RestApiValidationError` / `REST_API_VALIDATION` | 422 |
2088
+ | `RestApiResourceError`, subtype `not_found` | 404 |
2089
+ | `RestApiResourceError`, subtype `forbidden` | 403 |
2090
+ | `RestApiResourceError`, subtype `conflict` | 409 |
2091
+ | Other resource/relationship subtypes | 400 |
2092
+ | `RestApiPayloadError` | 400, or 413 for a size rejection |
2093
+ | `REST_API_FIELDSET_INVALID`, `REST_API_INCLUDE_INVALID` | 400 |
2094
+ | `REST_API_VERSION_CONFLICT` | 409 |
2095
+ | `REST_API_PRECONDITION_FAILED` | 412 |
2096
+ | Unexpected or temporal output failure | 500 |
2097
+
2098
+ Validation violations produce individual HTTP errors with their message and
2099
+ field location; generic validation errors retain their available details.
2100
+ Relationship validation/access failures follow their actual typed classification,
2101
+ not a universal `INVALID_RELATIONSHIP` code. Write errors additionally carry
2102
+ transaction outcome metadata as described below.
2549
2103
 
2550
2104
  ### Custom Error Handling
2551
2105
 
2552
2106
  Typed API errors thrown by getters, setters, computed fields, file detectors, or upload adapters retain their original code and details. For example, throwing `new RestApiResourceError('Upload forbidden', { subtype: 'forbidden' })` from an upload adapter produces a 403 response and still runs temporary-file cleanup.
2553
2107
 
2554
- ```javascript
2555
- // In hooks
2556
- api.resource('articles').hook('beforeDataPost', async (context) => {
2557
- if (context.inputData.attributes.title.length < 10) {
2558
- const error = new Error('Title too short');
2559
- error.status = 422;
2560
- error.code = 'TITLE_TOO_SHORT';
2561
- error.pointer = '/data/attributes/title';
2562
- throw error;
2563
- }
2564
- });
2565
-
2566
- // Custom error transformation
2567
- api.hook('errorTransform', async (error, context) => {
2568
- return {
2569
- status: error.status || '500',
2570
- code: error.code || 'INTERNAL_ERROR',
2571
- title: error.title || 'Error',
2572
- detail: error.message,
2573
- meta: {
2574
- timestamp: new Date().toISOString(),
2575
- request_id: context.requestId
2108
+ Unexpected setter/getter/computed failures reject the operation with their
2109
+ original `cause` and `context: { scopeName, fieldName, phase }`; on writes, this
2110
+ diagnostic error is the outer `RestApiWriteError`'s cause. HTTP connectors
2111
+ return 500. Getters no longer retain an untransformed value after failure, and
2112
+ computed fields no longer substitute null. Use `RestApiValidationError` for
2113
+ intentional validation rejection (422). Full write-response enrichment fails
2114
+ before owned commit; borrowed transactions remain the owner's responsibility.
2115
+ See the [callback migration guide](GUIDE/33-migrating-to-v2.md#setter-getter-and-computed-field-failures)
2116
+ for examples and intentional fallback handling.
2117
+
2118
+ Resource, relationship, bulk and canonical registry write failures use the root
2119
+ export `RestApiWriteError`. Its immutable `transactionOutcome` is `none`,
2120
+ `pending`, `committed`, `rolledBack` or `unknown`. Its `cause` preserves the
2121
+ original thrown value, including frozen errors, while classification fields
2122
+ such as `code`, `subtype` and `details` remain directly readable. Read errors
2123
+ retain their existing contract.
2124
+
2125
+ HTTP JSON:API write errors include `meta.transactionOutcome`; non-atomic bulk
2126
+ entries include `error.transactionOutcome`. A failed call can represent a
2127
+ committed write when a later hook rejects. Retrying that call can duplicate its
2128
+ effects; neither an HTTP status nor an unknown outcome authorizes automatic
2129
+ replay. See the [transaction migration guide](GUIDE/33-migrating-to-v2.md#transactions-and-errors)
2130
+ for all five meanings and cause-chain handling. If acknowledgement is lost,
2131
+ `unknown` requires application reconciliation before replay or file deletion;
2132
+ see [commit uncertainty](GUIDE/20-transaction-outcomes.md#when-commit-acknowledgement-is-lost).
2133
+ Consumer migration remains separate.
2134
+
2135
+ ```javascript
2136
+ import { RestApiValidationError } from 'json-rest-api';
2137
+
2138
+ await api.customize({
2139
+ hooks: {
2140
+ beforeSchemaValidatePost: {
2141
+ functionName: 'validate-article-title',
2142
+ handler: ({ context }) => {
2143
+ if (context.scopeName !== 'articles') return;
2144
+ const title = context.inputRecord.data.attributes?.title;
2145
+ if (typeof title === 'string' && title.length < 10) {
2146
+ throw new RestApiValidationError('Title too short', { fields: ['title'] });
2147
+ }
2148
+ }
2576
2149
  }
2577
- };
2150
+ }
2578
2151
  });
2579
2152
  ```
2580
2153
 
2154
+ For HTTP customization use the documented transport response/error hooks.
2155
+ There is no library-wide `api.hook('errorTransform', ...)` API. Preserve typed
2156
+ error classification, original causes and write outcomes when customizing output.
2157
+
2581
2158
  ---
2582
2159
 
2583
2160
  ## Advanced Features
2584
2161
 
2585
2162
  ### Transaction Support
2586
2163
 
2587
- All methods support database transactions with automatic management:
2164
+ `api.transaction(callback, context = {})` owns one top-level transaction and
2165
+ awaits completion hooks before returning the callback value unchanged. The
2166
+ callback receives a real Knex transaction for library operations and raw SQL.
2167
+ Await every operation and use a separate context object per enlisted operation.
2588
2168
 
2589
2169
  ```javascript
2590
2170
  // Automatic transaction (recommended)
2591
2171
  const result = await api.resources.articles.post({
2592
- inputRecord: {
2172
+ data: {
2593
2173
  title: 'New Article',
2594
2174
  content: 'Content...'
2595
2175
  }
2596
2176
  // No transaction provided - library creates and auto-commits
2597
2177
  });
2598
2178
 
2599
- // Manual transaction management
2600
- const trx = await knex.transaction();
2601
- try {
2179
+ // Several writes in one managed unit
2180
+ const article = await api.transaction(async transaction => {
2602
2181
  // Create article
2603
2182
  const article = await api.resources.articles.post({
2604
- inputRecord: {
2183
+ data: {
2605
2184
  title: 'New Article',
2606
2185
  content: 'Content...'
2607
2186
  },
2608
- transaction: trx // Provide transaction
2187
+ format: 'plain',
2188
+ transaction
2609
2189
  });
2610
2190
 
2611
2191
  // Create related comments
2612
2192
  for (const commentData of comments) {
2613
2193
  await api.resources.comments.post({
2614
- inputRecord: {
2194
+ data: {
2615
2195
  content: commentData.content,
2616
2196
  article_id: article.id
2617
2197
  },
2618
- transaction: trx // Same transaction
2198
+ format: 'plain',
2199
+ transaction
2619
2200
  });
2620
2201
  }
2621
2202
 
2622
- await trx.commit(); // Manual commit required
2623
- } catch (error) {
2624
- await trx.rollback();
2625
- throw error;
2626
- }
2203
+ return article;
2204
+ });
2627
2205
  ```
2628
2206
 
2629
- **Important:** When you provide a transaction, you're responsible for committing/rolling back. When you don't provide one, the library auto-commits.
2207
+ Return normally to commit; throw to roll back. A caught participating write or
2208
+ observed SQL failure still aborts the unit. Do not call `commit()` or `rollback()`
2209
+ inside the callback. Another `api.transaction()` call creates an independent
2210
+ top-level unit; compose work by forwarding the existing handle. Raw Knex
2211
+ transactions remain available for direct SQL and read-only API calls.
2212
+
2213
+ See the [managed contract](GUIDE/19-managed-transactions.md)
2214
+ and [migration guide](GUIDE/33-migrating-to-v2.md#transactions-and-errors).
2630
2215
 
2631
2216
  ### Knex Schema Helpers
2632
2217
 
@@ -2665,7 +2250,17 @@ Important boundaries:
2665
2250
  - `addKnexFields()` and `alterKnexFields()` are field-only helpers
2666
2251
  - indexes, foreign keys, and check constraints belong on the full table schema surface
2667
2252
 
2668
- For full examples and dialect notes, see [Knex Schema and Migrations](GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md).
2253
+ Generated migrations are drafts for review. By default, the diff preserves
2254
+ undeclared columns, indexes and foreign keys and reports them in `warnings`.
2255
+ Intentional removals require the corresponding boolean under `options`:
2256
+ `allowDropColumns`, `allowDropIndexes` or `allowDropForeignKeys`. Changing an
2257
+ explicitly named index or foreign key can still replace it; inspect `plan` and
2258
+ `warnings` before execution. A column drop cannot retain a dependent index or
2259
+ foreign key. Full snapshots reject generated columns and SQLite partial or
2260
+ expression indexes; the guide lists further dialect restrictions. Diff migrations do not generate a working
2261
+ `down`; write the recovery steps as part of the migration review.
2262
+
2263
+ For full examples and dialect notes, see [Knex Schema and Migrations](GUIDE/21-schema-and-migrations.md).
2669
2264
 
2670
2265
  ### Batch Operations
2671
2266
 
@@ -2674,244 +2269,120 @@ Process multiple operations efficiently:
2674
2269
  ```javascript
2675
2270
  // Batch create with transaction
2676
2271
  const createArticles = async (articlesData) => {
2677
- const trx = await knex.transaction();
2678
- const results = [];
2679
-
2680
- try {
2272
+ return api.transaction(async transaction => {
2273
+ const results = [];
2681
2274
  for (const data of articlesData) {
2682
2275
  const result = await api.resources.articles.post({
2683
- inputRecord: data,
2684
- transaction: trx,
2685
- returnFullRecord: 'minimal' // Optimize for batch
2276
+ data,
2277
+ transaction,
2278
+ format: 'plain',
2279
+ returning: 'minimal'
2686
2280
  });
2687
2281
  results.push(result);
2688
2282
  }
2689
2283
 
2690
- await trx.commit();
2691
2284
  return results;
2692
- } catch (error) {
2693
- await trx.rollback();
2694
- throw error;
2695
- }
2285
+ });
2696
2286
  };
2697
2287
 
2698
2288
  // Batch update
2699
2289
  const updateArticles = async (updates) => {
2700
- const trx = await knex.transaction();
2701
-
2702
- try {
2290
+ return api.transaction(async transaction => {
2703
2291
  for (const { id, data } of updates) {
2704
2292
  await api.resources.articles.patch({
2705
- inputRecord: { id, ...data },
2706
- transaction: trx,
2707
- returnFullRecord: 'no' // Skip return for performance
2293
+ id,
2294
+ data,
2295
+ transaction,
2296
+ format: 'plain',
2297
+ returning: 'none'
2708
2298
  });
2709
2299
  }
2710
2300
 
2711
- await trx.commit();
2712
- } catch (error) {
2713
- await trx.rollback();
2714
- throw error;
2715
- }
2301
+ });
2716
2302
  };
2717
2303
  ```
2718
2304
 
2719
2305
  ### Computed Fields
2720
2306
 
2721
- Add dynamic fields calculated at runtime:
2307
+ Declare computed fields in `schema`, with `computed: true`, `dependencies` and
2308
+ `compute: ({ id, attributes, context }) => value`. Computed values are not stored
2309
+ columns. Callbacks can return promises; requested fieldsets determine which
2310
+ computed values and their dependencies are evaluated. Dependencies establish
2311
+ read preparation/order, not a persistent reactive cache.
2722
2312
 
2723
- ```javascript
2724
- // In resource schema
2725
- computedFields: {
2726
- full_name: {
2727
- type: 'string',
2728
- compute: (record) => `${record.first_name} ${record.last_name}`,
2729
- dependencies: ['first_name', 'last_name']
2730
- },
2731
- age: {
2732
- type: 'number',
2733
- compute: (record) => {
2734
- const birthDate = new Date(record.birth_date);
2735
- const today = new Date();
2736
- return today.getFullYear() - birthDate.getFullYear();
2737
- },
2738
- dependencies: ['birth_date']
2739
- },
2740
- // Async computed field
2741
- stats: {
2742
- type: 'object',
2743
- compute: async (record, context) => {
2744
- return await statsService.getArticleStats(record.id);
2745
- },
2746
- dependencies: []
2747
- }
2748
- }
2749
-
2750
- // Request computed fields
2751
- const result = await api.resources.users.get({
2752
- id: '1',
2753
- queryParams: {
2754
- fields: {
2755
- users: 'first_name,last_name,full_name,age'
2756
- }
2757
- }
2758
- });
2759
- ```
2313
+ The resource schema example above declares `word_count`. Request it through
2314
+ `queryParams.fields`, for example `{ articles: 'title,word_count' }`. See
2315
+ [field transformations](GUIDE/12-field-transformations.md) for hidden
2316
+ dependencies, getter ordering and callback context.
2760
2317
 
2761
2318
  ### Polymorphic Relationships
2762
2319
 
2763
- Support relationships to multiple resource types:
2320
+ Declare a polymorphic belongs-to relationship under the resource's sibling
2321
+ `relationships` option:
2764
2322
 
2765
2323
  ```javascript
2766
- // Schema configuration
2767
2324
  relationships: {
2768
2325
  commentable: {
2769
- polymorphic: true,
2770
- types: ['articles', 'videos', 'photos'],
2771
- typeField: 'commentable_type',
2772
- idField: 'commentable_id'
2773
- }
2774
- }
2775
-
2776
- // Usage
2777
- const result = await api.resources.comments.post({
2778
- inputRecord: {
2779
- content: 'Great article!',
2780
- commentable_type: 'articles',
2781
- commentable_id: '1'
2782
- }
2783
- });
2784
-
2785
- // Query polymorphic relationships
2786
- const result = await api.resources.comments.query({
2787
- queryParams: {
2788
- include: ['commentable'], // Includes the related article/video/photo
2789
- filters: {
2790
- commentable_type: 'articles'
2326
+ belongsToPolymorphic: {
2327
+ types: ['articles', 'videos', 'photos'],
2328
+ typeField: 'commentable_type',
2329
+ idField: 'commentable_id'
2791
2330
  }
2792
2331
  }
2793
- });
2332
+ }
2794
2333
  ```
2795
2334
 
2796
- ### Soft Deletes
2797
-
2798
- Implement soft deletion pattern:
2799
-
2800
- ```javascript
2801
- // Configure in schema
2802
- softDelete: {
2803
- field: 'deleted_at',
2804
- includeDeleted: false // Default behavior
2805
- }
2335
+ For a JSON:API write, send the selected target as relationship linkage,
2336
+ for example `relationships: { commentable: { data: { type: 'articles', id: '1' } } }`
2337
+ inside the document's `data`. The target type must be declared and the referenced
2338
+ record must satisfy visibility/validation rules. Reverse polymorphic relationships
2339
+ use their declared `via` relationship. See the [polymorphic guide](GUIDE/07-polymorphic-relationships.md)
2340
+ for complete schemas and reverse declarations.
2806
2341
 
2807
- // Hook implementation
2808
- api.resource('articles').hook('beforeDataDelete', async (context) => {
2809
- // Convert delete to update
2810
- context.method = 'patch';
2811
- context.inputData = {
2812
- attributes: {
2813
- deleted_at: new Date().toISOString()
2814
- }
2815
- };
2816
- });
2342
+ ### Soft Deletes
2817
2343
 
2818
- // Query including soft-deleted
2819
- const result = await api.resources.articles.query({
2820
- queryParams: {
2821
- filters: {
2822
- include_deleted: true
2823
- }
2824
- }
2825
- });
2826
-
2827
- // Restore soft-deleted record
2828
- const result = await api.resources.articles.patch({
2829
- inputRecord: {
2830
- id: '1',
2831
- deleted_at: null
2832
- }
2833
- });
2834
- ```
2344
+ Soft deletion is an application policy, not a built-in `softDelete` resource
2345
+ option. Declare a nullable timestamp/marker and explicitly PATCH it when archiving.
2346
+ Changing `context.method` during DELETE does not change the executing method into
2347
+ PATCH. Apply a row policy for normal visibility and design a separate authorized
2348
+ restore path; an arbitrary `include_deleted` filter does not enable bypassing that
2349
+ policy. See [row policies](GUIDE/17-row-policies.md) and the transaction
2350
+ contract when archiving must update related records.
2835
2351
 
2836
2352
  ### Field-Level Permissions
2837
2353
 
2838
- Control access to specific fields:
2354
+ Declare `hidden` or `normallyHidden` output fields according to their intended
2355
+ visibility. Output omission is not write authorization. Use trusted application
2356
+ context and the appropriate validation/permission hook to reject unauthorized
2357
+ attribute changes before storage. Global customizations must restrict themselves
2358
+ to the intended `context.scopeName`; write attributes at schema-validation stages
2359
+ are in `context.inputRecord.data.attributes`.
2839
2360
 
2840
- ```javascript
2841
- // In enrichAttributes hook
2842
- api.resource('users').hook('enrichAttributes', async (context) => {
2843
- // Hide sensitive fields for non-admin users
2844
- if (!context.auth.isAdmin) {
2845
- delete context.attributes.email;
2846
- delete context.attributes.phone;
2847
- delete context.attributes.internal_notes;
2848
- }
2849
-
2850
- // Show computed permission fields
2851
- if (context.requestedComputedFields.includes('can_edit')) {
2852
- context.attributes.can_edit =
2853
- context.auth.userId === context.record.id ||
2854
- context.auth.isAdmin;
2855
- }
2856
- });
2857
-
2858
- // In beforeSchemaValidate hook - prevent updates
2859
- api.resource('users').hook('beforeSchemaValidatePatch', async (context) => {
2860
- // Prevent non-admins from updating certain fields
2861
- if (!context.auth.isAdmin) {
2862
- const restrictedFields = ['role', 'permissions', 'verified'];
2863
- for (const field of restrictedFields) {
2864
- if (field in context.inputData.attributes) {
2865
- throw new Error(`Cannot update field: ${field}`);
2866
- }
2867
- }
2868
- }
2869
- });
2870
- ```
2361
+ See [field transformations and visibility](GUIDE/12-field-transformations.md)
2362
+ and [row policies](GUIDE/17-row-policies.md). Computed values must obey the
2363
+ selected fieldset and the application's authorization rules too.
2871
2364
 
2872
2365
  ### Cross-Table Search
2873
2366
 
2874
- The library supports searching across related tables:
2875
-
2876
- ```javascript
2877
- // Using knexQueryFiltering hook
2878
- api.resource('articles').hook('knexQueryFiltering', async (context) => {
2879
- const { query, filters } = context;
2880
-
2881
- // Search across multiple tables
2882
- if (filters.global_search) {
2883
- query.leftJoin('users', 'articles.author_id', 'users.id')
2884
- .leftJoin('categories', 'articles.category_id', 'categories.id')
2885
- .where(function() {
2886
- this.where('articles.title', 'like', `%${filters.global_search}%`)
2887
- .orWhere('articles.content', 'like', `%${filters.global_search}%`)
2888
- .orWhere('users.name', 'like', `%${filters.global_search}%`)
2889
- .orWhere('categories.name', 'like', `%${filters.global_search}%`);
2890
- });
2891
- }
2892
- });
2893
- ```
2367
+ Prefer declared cross-resource search paths so the library can prepare aliases
2368
+ and apply target-resource visibility. A native join in a custom filtering hook
2369
+ does not automatically gain those permission checks. Custom SQL must use the
2370
+ active `context.knexQuery` builder/alias and explicit storage mappings, with
2371
+ bindings for values. See [native query builders](GUIDE/33-migrating-to-v2.md#native-query-builders-and-explicit-custom-filter-translation)
2372
+ and [query projections](GUIDE/15-query-projections.md).
2894
2373
 
2895
2374
  ### Database-Specific Features
2896
2375
 
2897
- The library detects database capabilities and adjusts behavior:
2898
-
2899
- ```javascript
2900
- // Window functions (PostgreSQL, MySQL 8+, SQLite 3.25+)
2901
- api.resource('articles').hook('afterDataQuery', async (context) => {
2902
- // Add ranking if database supports window functions
2903
- if (context.db.supportsWindowFunctions) {
2904
- // Ranking logic using ROW_NUMBER(), RANK(), etc.
2905
- }
2906
- });
2907
-
2908
- // JSON operations (PostgreSQL, MySQL 5.7+)
2909
- api.resource('articles').hook('knexQueryFiltering', async (context) => {
2910
- const { query, filters } = context;
2911
-
2912
- if (filters.metadata_key && context.db.supportsJsonb) {
2913
- // PostgreSQL JSONB query
2914
- query.whereRaw("metadata->>'key' = ?", [filters.metadata_key]);
2915
- }
2916
- });
2917
- ```
2376
+ Both Knex plugins expose the detected window-function capability as
2377
+ `api.knex.capabilities.windowFunctions`. Per-parent include limits require that
2378
+ capability; the library rejects those limits when detection reports no support.
2379
+ The operation's `context.db` is a Knex connection or transaction. It does not
2380
+ provide `supportsWindowFunctions` or `supportsJsonb` flags.
2381
+
2382
+ Use declared scalar JSON-key query fields for structured-value filtering,
2383
+ sorting and cursors. Whole object/array fields are not generic query keys;
2384
+ custom raw SQL remains specific to its actual driver and physical storage.
2385
+ See the [query projection guide](GUIDE/15-query-projections.md) and
2386
+ [backend capabilities](GUIDE/30-backend-capabilities.md) for supported
2387
+ operations, tests and limitations. A dialect branch in a helper is not evidence
2388
+ that every server version or storage combination has been verified.