json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -0,0 +1,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.
|