json-rest-api 1.0.29 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) 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 +634 -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 +100 -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 +3827 -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 +36 -0
  44. package/index.js +1 -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 +64 -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 +213 -164
  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 +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,479 @@
1
+ ---
2
+ title: "Hooks and lifecycle"
3
+ chapter: 13
4
+ chapter_label: "13"
5
+ ---
6
+
7
+ # 13. Hooks and lifecycle
8
+
9
+ Hooks extend particular lifecycle stages. Their names, ordering and context
10
+ shape depend on the operation. A write can invoke read hooks while preparing
11
+ its full response, and relationship writes can invoke nested resource writes.
12
+
13
+ An operation passes the caller's mutable context through its resource hooks.
14
+ Hooks can cache information, add properties for later hooks, and change the
15
+ documented writable fields at the appropriate stage. Those changes remain
16
+ visible to the caller after success or failure. There is no immutable context
17
+ wrapper or separate hook cache API.
18
+
19
+ Use [Writing Plugins](29-writing-plugins.md) for reusable extensions,
20
+ [field transformations](12-field-transformations.md) for setters/getters and
21
+ computed output, and [row policies](17-row-policies.md) for mandatory
22
+ visibility constraints.
23
+
24
+ ## Register and observe hooks
25
+
26
+ The following blocks form one runnable example. Insert them in order into the
27
+ [starting script](03-running-example.md), before starting its server,
28
+ on a fresh database. Register named handlers through `api.customize`.
29
+
30
+ ```javascript
31
+ await api.addResource('notes', {
32
+ schema: {
33
+ title: { type: 'string', required: true },
34
+ canEdit: { type: 'boolean', virtual: true }
35
+ }
36
+ })
37
+ await api.resources.notes.createKnexTable()
38
+ const events = []
39
+ api.customize({
40
+ hooks: {
41
+ beforeProcessingPost: {
42
+ functionName: 'prepare-note-title',
43
+ handler: async ({ context }) => {
44
+ if (context.scopeName !== 'notes') return
45
+ const title = context.inputRecord?.data?.attributes?.title
46
+ context.noteTitleCache = typeof title === 'string' ? title.trim() : undefined
47
+ }
48
+ },
49
+ beforeSchemaValidatePost: {
50
+ functionName: 'normalize-note-title',
51
+ handler: async ({ context }) => {
52
+ if (context.scopeName !== 'notes') return
53
+ const attributes = context.inputRecord.data.attributes
54
+ if (context.noteTitleCache !== undefined) attributes.title = context.noteTitleCache
55
+ events.push(`validate:${context.method}`)
56
+ }
57
+ },
58
+ beforeDataCallPost: {
59
+ functionName: 'observe-note-write',
60
+ handler: async ({ context }) => {
61
+ if (context.scopeName === 'notes') events.push(`write:${context.method}`)
62
+ }
63
+ },
64
+ beforeDataGet: {
65
+ functionName: 'observe-note-read',
66
+ handler: async ({ context }) => {
67
+ if (context.scopeName === 'notes') events.push(`read:${context.method}`)
68
+ }
69
+ },
70
+ finishPost: {
71
+ functionName: 'observe-note-finish',
72
+ handler: async ({ context }) => {
73
+ if (context.scopeName !== 'notes') return
74
+ context.savedNoteId = String(context.id)
75
+ events.push(`finish:${context.method}`)
76
+ }
77
+ },
78
+ afterCommit: {
79
+ functionName: 'observe-note-commit',
80
+ handler: async ({ context }) => {
81
+ if (context.scopeName === 'notes') events.push(`commit:${context.method}`)
82
+ }
83
+ }
84
+ }
85
+ })
86
+ ```
87
+
88
+ The first hook caches information; a later hook uses it to change the pending
89
+ title before validation; the finish hook adds information for the caller.
90
+ The input type is checked before using a string method because processing hooks
91
+ run before validation. For an ordinary field transformation, prefer a schema
92
+ setter; this example demonstrates communication between hook stages.
93
+
94
+ ```javascript
95
+ const noteContext = {}
96
+ const fullNote = await api.resources.notes.post({
97
+ data: { title: ' First note ' }, format: 'plain', returning: 'full'
98
+ }, noteContext)
99
+ const fullEvents = events.splice(0)
100
+ console.log(fullNote.title, noteContext.savedNoteId, fullEvents)
101
+ ```
102
+
103
+ The title is `First note`. The event sequence is `validate:post`, `write:post`,
104
+ `read:get`, `finish:post`, `commit:post`. The full response invokes a nested GET
105
+ before the write finishes; its context has method `get`.
106
+
107
+ ```javascript
108
+ const minimalNote = await api.resources.notes.post({
109
+ data: { title: ' Second note ' }, format: 'plain', returning: 'minimal'
110
+ })
111
+ const minimalEvents = events.splice(0)
112
+ console.log(minimalNote, minimalEvents)
113
+ ```
114
+
115
+ The minimal result contains `id` and `type: 'notes'`. The events omit `read:get`.
116
+ `returning: 'none'` also avoids that response GET. Do not put mandatory write
117
+ validation in read hooks: it would depend on the requested return mode.
118
+
119
+ ## Cache permission information across hooks
120
+
121
+ The input-preparation recipe above already shows two hooks sharing a cache.
122
+ Permissions can use the same approach. These next recipes extend that runnable
123
+ example. The lookup below stands in for an application authorization service;
124
+ `auth.userId` must come from trusted application code.
125
+
126
+ ```javascript
127
+ import { RestApiResourceError } from 'json-rest-api'
128
+
129
+ let permissionLookups = 0
130
+ async function loadNotePermissions (userId) {
131
+ permissionLookups++
132
+ return { canEdit: userId === 'editor' }
133
+ }
134
+
135
+ await api.customize({ hooks: {
136
+ checkPermissions: {
137
+ functionName: 'authorize-note-edit',
138
+ handler: async ({ scopeName, context }) => {
139
+ if (scopeName !== 'notes') return
140
+ const operation = context.originalContext ?? context
141
+ operation.notePermissions ??= await loadNotePermissions(operation.auth?.userId)
142
+ if (context.method === 'patch' && !operation.notePermissions.canEdit) {
143
+ throw new RestApiResourceError('Editing notes is forbidden', { subtype: 'forbidden' })
144
+ }
145
+ }
146
+ }
147
+ } })
148
+
149
+ const permissionContext = { auth: { userId: 'editor' } }
150
+ const editedNote = await api.resources.notes.patch({
151
+ id: fullNote.id, data: { title: 'Edited note' }
152
+ }, permissionContext)
153
+ const lookupsAfterEdit = permissionLookups
154
+ ```
155
+
156
+ The write and its full-response GET perform one lookup in total. The permission
157
+ hook writes to the originating operation context through `originalContext` for
158
+ ordinary CRUD. Relationship permission hooks receive the operation directly,
159
+ so the fallback handles that boundary too. A nested response GET inherits the
160
+ cached object by reference. Later hooks read `notePermissions` from the relevant
161
+ operation; they do not need another lookup or a global cache.
162
+
163
+ This recipe permits reads and gates PATCH only. Define the allowed methods for
164
+ your application explicitly; use row policies when authorization must filter
165
+ collections or relationship targets. A cached decision is valid only for the
166
+ identity and conditions it was calculated for. Use a fresh context for a new call.
167
+
168
+ ## Enrich a declared output field
169
+
170
+ `canEdit` was declared as a virtual boolean above, so it can appear in output
171
+ without a stored column. This hook obtains the cached permission through the
172
+ enrichment wrapper's `parentContext`:
173
+
174
+ ```javascript
175
+ await api.customize({ hooks: {
176
+ enrichAttributes: {
177
+ functionName: 'show-note-edit-permission',
178
+ handler: ({ scopeName, context }) => {
179
+ if (scopeName !== 'notes') return
180
+ context.attributes.canEdit = context.parentContext.notePermissions?.canEdit ?? false
181
+ }
182
+ }
183
+ } })
184
+
185
+ const editableNote = await api.resources.notes.get({ id: fullNote.id }, {
186
+ auth: { userId: 'editor' }
187
+ })
188
+ const readOnlyNote = await api.resources.notes.get({ id: fullNote.id }, {
189
+ auth: { userId: 'reader' }
190
+ })
191
+ ```
192
+
193
+ The returned `canEdit` values are true and false. This output is information for
194
+ the caller; the write permission hook enforces access. Final field selection
195
+ still applies, so a sparse request for `title` alone omits `canEdit`. Prefer a
196
+ declared computed field for a value derived from resource attributes; use
197
+ enrichment when it depends on request-specific information such as this cache.
198
+
199
+ ## Perform an effect after commit
200
+
201
+ The original example observes commit order. An application effect belongs at
202
+ that same boundary, rather than in a finish hook. This runnable stand-in records
203
+ notifications in an array; replace `publishNoteChanged` with your application
204
+ integration when using the recipe.
205
+
206
+ ```javascript
207
+ const notifications = []
208
+ async function publishNoteChanged (change) {
209
+ notifications.push(change)
210
+ }
211
+
212
+ await api.customize({ hooks: {
213
+ afterCommit: {
214
+ functionName: 'publish-note-change',
215
+ handler: async ({ scopeName, context }) => {
216
+ if (scopeName !== 'notes' || context.method !== 'patch') return
217
+ await publishNoteChanged({
218
+ id: String(context.id), title: context.minimalRecord.attributes.title
219
+ })
220
+ }
221
+ }
222
+ } })
223
+
224
+ let notificationsBeforeCommit
225
+ await api.transaction(async transaction => {
226
+ await api.resources.notes.patch({
227
+ id: fullNote.id, data: { title: 'Ready to share' }, transaction
228
+ }, { auth: { userId: 'editor' } })
229
+ notificationsBeforeCommit = notifications.length
230
+ })
231
+ const notificationsAfterCommit = notifications.slice()
232
+ ```
233
+
234
+ There are no notifications inside the transaction callback, and one after
235
+ `api.transaction` resolves. A rollback does not run this hook. An effect failure
236
+ after commit cannot roll back the saved note; it is reported with a committed
237
+ outcome. This is not durable delivery across process failure. Use a
238
+ transactional outbox when reliable eventual delivery is a requirement.
239
+
240
+ ## Context depends on the boundary
241
+
242
+ `data` selects plain application input and `document` selects a JSON:API input
243
+ document. `format: 'plain'` and `format: 'jsonapi'` select output independently.
244
+ Resource POST/PUT/PATCH normalize either input into `context.inputRecord`, an
245
+ internal JSON:API document, before their processing hooks. The public
246
+ `inputRecord` argument has been removed; the hook field retains its name and
247
+ shape. It is not yet validated at the early processing boundary. Reads and
248
+ DELETE do not have their own write document; relationship writes accept
249
+ `relationshipData` instead.
250
+
251
+ | Boundary | Where to find operation data |
252
+ | --- | --- |
253
+ | Resource processing, validation, data and finish hooks | `context` is the same object supplied as the method's second argument, or a fresh object when omitted. Inspect `method` and `scopeName` before using method-specific properties. |
254
+ | Resource authorization through `checkPermissions` | Ordinary CRUD uses a permission wrapper whose `originalContext` points to the operation. Relationship methods invoke the hook with their operation context directly. Use `context.originalContext ?? context` for information shared with later operation hooks. |
255
+ | Attribute enrichment | Hooks share a per-resource enrichment wrapper. `context.attributes` is its output attribute object; `context.parentContext` is the read operation context. Included resources receive their own enrichment calls. |
256
+ | Full write response | A nested GET has a separate, shallow-copied context. It shares the write's transaction and referenced application objects. |
257
+
258
+ `inputRecord.data.attributes` contains write attributes. After setters run,
259
+ after-data hooks see transformed values. `minimalRecord` is internal stored
260
+ resource data; `responseRecord` holds the selected write output once prepared.
261
+ POST assigns its generated ID before after-data hooks. PATCH processing hooks
262
+ can run before `context.id` is populated; use later validated stages when an ID
263
+ is required. PUT selects its create/update branch before schema validation.
264
+
265
+ Do not use internal representation flags as application options. The public
266
+ options are `format` and `returning`; see [API migration](33-migrating-to-v2.md).
267
+ Getters and computed callbacks have their own documented
268
+ [contexts and dependency rules](12-field-transformations.md#dependency-contract).
269
+
270
+ ## Field ownership and mutation
271
+
272
+ The context is mutable, but not every property is an application input. Use
273
+ application-specific names for cached values and additional information so they
274
+ do not collide with the library fields below. The library does not freeze the
275
+ context or enforce ownership through proxies.
276
+
277
+ | Field | Owner and availability | Supported mutation and effect |
278
+ | --- | --- | --- |
279
+ | Application properties, such as `auth`, `noteTitleCache` or `savedNoteId` | Caller and application hooks; available once assigned. | Add or update them to communicate between hooks. Trust/authentication comes from application code, never automatically from write data. |
280
+ | `inputRecord.data.attributes` | Library-normalized write input, available at processing hooks. | Prepare attributes in `beforeProcessing*` or `beforeSchemaValidate*`; schema validation follows. After-validation changes are not schema-validated again. Setters run after `beforeDataCall*`. Changes after the storage call do not update the database. |
281
+ | `inputRecord.data.relationships` | Library-normalized write input. | Prepare linkage in `beforeProcessing*`, before document validation, relationship authorization and relationship planning. Later changes do not rebuild the already prepared relationship operations. |
282
+ | `record` | Library-created JSON:API read result, available after fetching. | Read enrichment and finish hooks can adjust output. Final normalization, visibility checks and field projection still apply. This does not change stored values. |
283
+ | `attributes` on an enrichment wrapper | Library-created output attributes for one resource. | `enrichAttributes` hooks can update declared output fields; the returned attributes remain subject to final projection. Use `parentContext` for operation-wide cached information. |
284
+ | `responseRecord` | Selected write output, available at write finish; absent for `returning: 'none'`. | Finish hooks can adjust the response before final normalization and commit. A returned write result is copied before completion hooks; mutating this field in `afterCommit` does not change that result. |
285
+ | `method`, `scopeName`, `id`, `relationshipName`, `isUpdate` | Library-owned operation identity and branch selection. Availability varies by method and stage. | Inspect rather than overwrite. Changing them does not select a different operation or safely retarget a write. POST's ID is available after storage; normalize it with `String(context.id)` when comparing it to public IDs. |
286
+ | `schemaInfo`, `params`, `queryParams`, `format`, `returning`, `simplified` | Library-owned compiled schema, request parameters and selected response behavior. | Set public options in method arguments. Do not overwrite these fields to redirect processing. Use schema declarations, row policies and documented query hooks for customization. |
287
+ | `minimalRecord`, `originalMinimalRecord`, `originalInputAttributes`, `originalRecord` | Library-owned stored data and snapshots; availability depends on the stage. | Inspect them when available. Do not change snapshots to influence validation, permissions or persistence. |
288
+ | `transaction`, `db`, `shouldCommit`, `transactionOutcome`, `transactionCommitted`, `error`, `cleanupErrors` | Library-owned transaction and failure state. | Inspect through the documented transaction/error contract. Pass transaction handles through method arguments; do not replace these fields, forge outcomes or erase diagnostics. |
289
+ | `knexQuery`, `storageAdapter` | Temporary state at native query hook boundaries, also visible to nested query permission checks. | `knexQuery` is a filtering envelope; its `query` property holds the native builder. Use the [documented filtering contract](29-writing-plugins.md#knexqueryfiltering), and do not retain a builder for a later operation. |
290
+
291
+ Fields belonging to optional plugins follow those plugins' documented hooks.
292
+ Other implementation fields are not extension points simply because they are
293
+ visible on the context. A hook can throw to reject an operation; a late change
294
+ to validated attributes must not be used to bypass authorization or validation.
295
+
296
+ Use a fresh context for every independent operation, especially concurrent
297
+ calls. The same object is not an isolated request template: it contains loaded
298
+ records and transaction state after a call. To share authentication or a cache,
299
+ pass those values deliberately in new context objects. Nested objects are not
300
+ deep-cloned, so concurrent application hooks must coordinate shared mutations.
301
+
302
+ ## Editor help for known hooks
303
+
304
+ Known hook names provide stage-specific context types when registered through
305
+ `api.customize` or resource hooks. For a separately declared handler, select its
306
+ hook name explicitly:
307
+
308
+ ```typescript
309
+ import type { HookHandler } from 'json-rest-api'
310
+
311
+ type NoteContext = {
312
+ auth?: { userId: string }
313
+ notePermissions?: { canEdit: boolean }
314
+ }
315
+
316
+ const showEditPermission: HookHandler<'enrichAttributes', NoteContext> = ({ context }) => {
317
+ context.attributes.canEdit = context.parentContext.notePermissions?.canEdit ?? false
318
+ }
319
+ ```
320
+
321
+ Register `showEditPermission` as the handler of the named enrichment hook in the
322
+ recipe above. Use `RuntimeHooks<NoteContext>` to annotate a separate hook map.
323
+ JavaScript projects can use the same types through JSDoc and editor type checking.
324
+
325
+ The editor distinguishes an enrichment wrapper's `parentContext` from a
326
+ permission wrapper's `originalContext`, and marks library-owned fields such as
327
+ `method` and `transaction` readonly. Application cache properties and documented
328
+ write/output values remain mutable. Readonly annotations do not freeze runtime
329
+ objects or add another hook mechanism.
330
+
331
+ These types describe known boundaries, not a proof of the whole lifecycle.
332
+ Early input and arbitrary attribute values still need narrowing; a custom plugin
333
+ owns its custom hook context. Types cannot prove authorization, mutation order,
334
+ transaction completion or the safety of shared nested objects. Keep the runtime
335
+ contracts and focused hook tests alongside the editor guidance.
336
+
337
+ ## Nested context inheritance
338
+
339
+ Built-in nested operations use separate context objects with shallow inheritance:
340
+
341
+ | Boundary | Inherited information | Separate working state |
342
+ | --- | --- | --- |
343
+ | Full write-response GET | Application properties and referenced cache/auth objects; the active transaction is passed explicitly. Write input is available for virtual-field response enrichment. | GET sets its own method, ID, schema, query selection and read result. Replacing a child top-level property does not replace the parent's property. |
344
+ | Relationship PATCH and reverse relationship child writes | Application properties and referenced objects; the active transaction is passed explicitly. | Child writes set their own method, input, schema and stored records. The parent retains its own operation identity and finish context. |
345
+ | Bulk children | Application properties and referenced objects; atomic children share the batch transaction. | Each child has its own context with `bulkOperation` and `bulkIndex`. Child cleanup diagnostics are collected by the batch. |
346
+ | Ordinary CRUD permission checks and attribute enrichment | A reference to the relevant operation through `originalContext` or `parentContext`. | A fresh wrapper for that permission/enrichment call. Adding a wrapper property does not add it to the originating context. Relationship permission hooks instead receive the relationship operation context directly. |
347
+ | Included-resource and relationship visibility queries | Application context and the active connection/transaction. | The library sets the target resource/schema and query selection; parent filters are not blindly applied as target filters. |
348
+
349
+ Shallow inheritance means a child sees an existing `context.cache` object by
350
+ reference: updating that object's contents is visible to the parent, whereas
351
+ assigning a different `context.cache` affects only the child. Library snapshots,
352
+ input and transaction bookkeeping remain subject to the ownership rules above.
353
+ Do not treat a child's copied write fields as that child's input: a nested GET
354
+ can retain information about the enclosing write for response enrichment.
355
+
356
+ For an application-initiated nested call, construct a new context containing the
357
+ application properties the child actually needs, and pass `transaction` in the
358
+ method arguments when it must participate in the parent's transaction. Passing
359
+ the parent's context object directly would let the child overwrite its method,
360
+ ID and other working fields. A blanket spread also inherits existing internal
361
+ fields; it is not a substitute for choosing the information to share.
362
+
363
+ ## Resource write order
364
+
365
+ POST, PUT-create, PUT-update and PATCH follow this sequence. The method suffix
366
+ is `Post`, `Put` or `Patch`, for example `beforeDataCallPatch`.
367
+
368
+ 1. Create or borrow a transaction and select response behavior.
369
+ 2. `beforeProcessing`, then the method-specific processing hook.
370
+ 3. Validate the request contract and relationships, and perform the
371
+ method-specific existence/replacement checks. Run `beforeSchemaValidate`,
372
+ then its method-specific hook; validate attributes; run the method-specific
373
+ `afterSchemaValidate`, then `afterSchemaValidate`.
374
+ 4. Authorize through `checkPermissions`; run `beforeDataCall`, then its
375
+ method-specific hook.
376
+ 5. Acquire required relationship locks, apply setters and write to storage.
377
+ 6. Run the method-specific `afterDataCall`, then `afterDataCall`.
378
+ 7. Apply relationship changes and refresh the minimal stored record.
379
+ 8. Prepare the response; run `finish`, then its method-specific hook.
380
+ 9. Commit an owned transaction and await its `afterCommit` chain.
381
+
382
+ DELETE has no processing, schema-validation or setter stage. Lookup and
383
+ permission checks precede `beforeDataCall` and `beforeDataCallDelete`.
384
+ Deletion precedes `afterDataCallDelete`, `afterDataCall`, `finish` and
385
+ `finishDelete`. DELETE returns no record.
386
+
387
+ The write hooks are named `beforeDataCall` and `afterDataCall`. `beforeData`
388
+ is a read hook. Registering `beforeDataPost` or `afterDataPost` does not attach
389
+ a handler to these resource write stages.
390
+
391
+ ## Read order and output enrichment
392
+
393
+ GET authorizes through `checkPermissions`, then runs `beforeData` and
394
+ `beforeDataGet`. After fetching, it runs `checkDataPermissions`,
395
+ `checkDataPermissionsGet` and `enrichRecord`. Field getters and computed output
396
+ run during attribute enrichment, followed by `enrichAttributes`.
397
+ `enrichRecordWithRelationships`, `finish` and `finishGet` follow. Final
398
+ normalization, field projection and representation conversion happen afterward.
399
+
400
+ QUERY authorizes through `checkPermissions`, runs `beforeData` and
401
+ `beforeDataQuery`, fetches the collection, then runs `enrichRecord`.
402
+ Attribute enrichment covers primary and included records. `finish` and
403
+ `finishQuery` precede final projection and conversion. Do not infer QUERY
404
+ hook names or stages from the GET sequence.
405
+
406
+ Use declared computed fields for derived attributes so dependencies and sparse
407
+ fieldsets work together. An `enrichAttributes` hook operates on one attribute
408
+ object, whereas `enrichRecord` operates on the response document. Adding an
409
+ undeclared output key does not establish a supported field or bypass final
410
+ field filtering. Finish hooks are late observers; they are not a substitute
411
+ for schema validation or query policy.
412
+
413
+ ## Relationship and bulk composition
414
+
415
+ Relationship POST and DELETE run `beforeDataCall`, followed by
416
+ `beforeDataCallPostRelationship` or `beforeDataCallDeleteRelationship`, after
417
+ resolving the visible parent and before mutation. Their context exposes
418
+ `scopeName`, `id`, `transaction`, and `minimalRecord`. A shared `beforeDataCall`
419
+ hook must account for `postRelationship` and `deleteRelationship` methods too.
420
+ Socket.IO uses this boundary to query subscription membership before a change;
421
+ its eligibility queries also run the ordinary query lifecycle in the write's
422
+ transaction with the subscriber's trusted context.
423
+
424
+ Relationship writes acquire database locks after the `beforeDataCall` hooks.
425
+ Belongs-to and polymorphic targets are rechecked while locking them. Collection
426
+ mutations also preserve a parent row version so concurrent replacements cannot
427
+ use stale membership; a database UPDATE trigger may run even when the parent's
428
+ attribute values did not change. These internal operations do not invoke another
429
+ resource hook. Keep borrowed transactions short, and leave rollback/retry of
430
+ deadlock or serialization failures to the outer owner. See
431
+ [relationship migration notes](33-migrating-to-v2.md#review-relationship-writes)
432
+ for the driver behavior and retry boundaries.
433
+
434
+ PATCH relationship authorizes through `checkPermissions` and
435
+ `checkPermissionsPatchRelationship`, then invokes a source-resource PATCH with
436
+ `returning: 'none'`, followed by `finish` and `finishPatchRelationship`.
437
+ POST/DELETE relationship invoke their own permission, before-data and finish
438
+ hooks. Reverse relationship mutations can run child PATCH lifecycles inside
439
+ the parent's operation. Direct pivot/link changes do not synthesize
440
+ pivot-resource hook calls.
441
+
442
+ Bulk operations invoke child resource lifecycles. Atomic children share a
443
+ transaction; non-atomic children complete separate transactions. Do not count
444
+ hook invocations as independent user requests or assume one context per batch.
445
+
446
+ ## Completion and failure
447
+
448
+ Each hook is awaited. A failing ordinary hook stops later stages of its chain.
449
+ For an owned pre-commit failure, the operation attempts rollback;
450
+ `afterRollback` runs only after acknowledged rollback. After acknowledged
451
+ commit, an `afterCommit` failure reports a committed outcome and cannot undo
452
+ the mutation. Uncertain driver completion runs neither outcome hook.
453
+
454
+ With an explicit `api.transaction` handle, participating calls finish their
455
+ write stages while completion hooks wait for the owner. A rejected participant
456
+ poisons the managed unit even if its caller catches the error. Atomic children
457
+ complete in enlistment order after commit and reverse order after rollback.
458
+ Keep external side effects consistent with those boundaries; a finish hook
459
+ runs before commit.
460
+
461
+ See [managed transactions](19-managed-transactions.md) and
462
+ [transaction outcomes](20-transaction-outcomes.md) for ownership, typed failures
463
+ and retry guidance. `RestApiWriteError.cause` retains the error received at the
464
+ write boundary; it cannot recover an error replaced earlier by an extension
465
+ or the hook dispatcher.
466
+
467
+ ## Registration-time extensions
468
+
469
+ Resource compilation exposes `schema:enrich` for stored attribute definitions,
470
+ `searchSchema:enrich` for filters, and then `computedSchema:enrich` for computed
471
+ output definitions. These run at registration and during canonical field
472
+ addition. Derived fields should use the computed hook so supported recompilation
473
+ rebuilds them. See the [computed enrichment contract and migration example](33-migrating-to-v2.md#computed-field-enrichment).
474
+
475
+
476
+ For plugin installation, scope registration, custom methods and helpers, use
477
+ [Writing Plugins](29-writing-plugins.md). Register resource-specific
478
+ behavior explicitly and guard global hooks by scope and method. Keep request
479
+ state on its operation context rather than in shared plugin variables.
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: "Custom resource methods"
3
+ chapter: 14
4
+ chapter_label: "14"
5
+ ---
6
+
7
+ # 14. Custom resource methods
8
+
9
+ The built-in resource CRUD lifecycle currently supports the two Knex storage
10
+ plugins: ordinary tables and canonical AnyAPI storage. It is not a generic
11
+ remote-service or NoSQL adapter interface. Assigning eight `data*` helpers does
12
+ not supply a complete backend.
13
+
14
+ The previous in-memory example did exactly that and fails its first POST with
15
+ `No transaction factory is installed`. Supplying a dummy transaction would not
16
+ make its changes atomic or implement the remaining storage requirements.
17
+
18
+ ## Choose the integration boundary
19
+
20
+ | Need | Existing approach |
21
+ | --- | --- |
22
+ | Disposable, in-memory resource CRUD with normal JSON:API behavior | Use `better-sqlite3` with `filename: ':memory:'` and `RestApiKnexPlugin`. |
23
+ | Multiple logical resources in shared SQL tables | Use `RestApiAnyapiKnexPlugin`; initialize its canonical schema and select a fixed tenant namespace. |
24
+ | Application-specific calls to a service or other data source | Add an explicit plugin method and implement its input, authorization and result contract. |
25
+ | A new storage engine behind all resource methods | Treat it as a core integration project and verify the full storage/transaction contract below. |
26
+
27
+ See [initial setup](01-getting-started.md) for a complete in-memory SQLite
28
+ example, and [backend capabilities](30-backend-capabilities.md) for the tested SQL
29
+ combinations and their limits. In-memory SQLite remains a database; it provides
30
+ real transactions rather than pretending a JavaScript Map can roll back.
31
+
32
+ ## A custom service method
33
+
34
+ This example uses local resource methods without installing database
35
+ storage. Run the three blocks together in an ESM file in an application with
36
+ `json-rest-api` installed. The injected loader returns a small
37
+ fixed result so the example is executable without an external service.
38
+ Replace that loader with application-owned service access when integrating.
39
+
40
+ ```javascript
41
+ import { JsonRestApi } from 'json-rest-api'
42
+ import { RestApiPlugin } from 'json-rest-api'
43
+
44
+ const serviceApi = new JsonRestApi({ name: 'service-example' })
45
+ await serviceApi.use(RestApiPlugin)
46
+ const AvailabilityPlugin = {
47
+ name: 'availability-service',
48
+ dependencies: ['rest-api'],
49
+ install ({ addResourceMethod, pluginOptions }) {
50
+ addResourceMethod('lookupAvailability', async ({ params, context }) => {
51
+ if (context.auth?.canCheckAvailability !== true) throw new Error('Availability access denied')
52
+ if (typeof params.sku !== 'string' || !params.sku.trim()) throw new Error('sku is required')
53
+ return pluginOptions.loadAvailability(params.sku)
54
+ })
55
+ }
56
+ }
57
+ await serviceApi.use(AvailabilityPlugin, {
58
+ loadAvailability: async sku => ({ sku, available: sku === 'BOOK-1' })
59
+ })
60
+ ```
61
+
62
+ The method explicitly checks its application authorization and input. The
63
+ loader is trusted application configuration, not client-supplied code.
64
+
65
+ ```javascript
66
+ await serviceApi.addResource('catalog', {
67
+ schema: { sku: { type: 'string', required: true } }
68
+ })
69
+ ```
70
+
71
+ Registration provides the scope for the custom method; it does not install
72
+ storage or make the built-in CRUD methods usable.
73
+
74
+ ```javascript
75
+ const availability = await serviceApi.resources.catalog.lookupAvailability(
76
+ { sku: 'BOOK-1' },
77
+ { auth: { canCheckAvailability: true } }
78
+ )
79
+ console.log(availability)
80
+ ```
81
+
82
+ The result is `{ sku: 'BOOK-1', available: true }`. This method has its own return
83
+ shape. It does not automatically acquire resource CRUD validation, permissions,
84
+ JSON:API formatting, includes, pagination, transactions or HTTP routes. A custom
85
+ HTTP endpoint must translate and authorize its request and response explicitly.
86
+ Do not route a remote write through a local transaction and claim the remote
87
+ service participates in its rollback.
88
+
89
+ See [Writing Plugins](29-writing-plugins.md) for registration and custom
90
+ methods, and [hooks](13-hooks-and-lifecycle.md) for the distinct
91
+ built-in resource lifecycle.
92
+
93
+ ## What a complete storage integration must cover
94
+
95
+ The current implementation depends on more than individual CRUD helpers:
96
+
97
+ - Schema compilation, logical IDs, field/column translation, serializers and
98
+ storage metadata must agree across reads, writes, filters and sorting.
99
+ - `dataGetMinimal` supports both individual and batched lookup paths. Visibility
100
+ checks and relationship validation need the same policy behavior as normal
101
+ reads, including hidden/missing equivalence.
102
+ - Queries must preserve authorized counts, stable pagination, sparse fields,
103
+ computed dependencies, includes and per-parent limits. Filtering a response
104
+ after pagination does not satisfy this contract.
105
+ - Relationship mutations need target validation, membership storage, reverse
106
+ changes, locks and version invalidation. Core currently uses Knex builders
107
+ and storage adapters directly for parts of these operations.
108
+ - Writes need an actual transaction factory and the managed ownership contract,
109
+ including acknowledged commit/rollback, uncertain completion, completion
110
+ hooks and failure diagnostics. Remote side effects need their own semantics.
111
+ - Bulk operations and schema changes must preserve their documented atomicity
112
+ and publication boundaries. Supporting a single-record happy path is not
113
+ sufficient evidence for the backend.
114
+
115
+ The source checkout's `plugins/core/lib/storage/storage-types.d.ts` describes
116
+ internal Knex-oriented boundaries; it is not a storage-agnostic public driver
117
+ specification. The ordinary and canonical storage plugins are the current
118
+ implementations to inspect. Do not copy an outdated list of helpers as a
119
+ substitute for reviewing their callers.
120
+
121
+ A new backend needs conformance tests for its supported capabilities and explicit
122
+ rejections for unsupported operations. Use the source checkout's
123
+ `tests/README.md` and [contributing](../contributing.md) to select the relevant
124
+ fixtures and matrices. The current library does not ship a verified generic
125
+ in-memory Map, MongoDB, DynamoDB or remote JSON:API backend.