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