json-rest-api 1.0.29 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,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.