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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Version-field migrations"
|
|
3
|
+
chapter: 22
|
|
4
|
+
chapter_label: "22"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 22. Version-field migrations
|
|
8
|
+
|
|
9
|
+
This explicit schema/data procedure is verified on SQLite, PostgreSQL and MySQL
|
|
10
|
+
in both storage modes. The library implements revision conditions, membership
|
|
11
|
+
invalidation and optional HTTP validators. Stored revisions and HTTP
|
|
12
|
+
representation validators have different scopes; see the
|
|
13
|
+
[API migration guide](33-migrating-to-v2.md) for their contracts. Apply this
|
|
14
|
+
procedure to each application database before enabling conditional writes.
|
|
15
|
+
|
|
16
|
+
## Choose the field deliberately
|
|
17
|
+
|
|
18
|
+
Declare a stored string attribute, such as `revision`, and later select it with
|
|
19
|
+
`versionField: 'revision'`. The field must be distinct from the primary ID and
|
|
20
|
+
cannot have a getter, setter, storage serializer, computed/virtual behavior or
|
|
21
|
+
relationship. The library creates opaque UUID tokens. Clients must compare and
|
|
22
|
+
submit the entire token, without parsing, trimming or changing its case.
|
|
23
|
+
|
|
24
|
+
Keep the selected output visibility explicit. A hidden revision remains hidden;
|
|
25
|
+
enabling versioning does not expose it. Clients using conditional writes need
|
|
26
|
+
an authorized way to read the revision. Full write responses or a subsequent
|
|
27
|
+
GET can provide it; minimal and none responses do not promise a new token.
|
|
28
|
+
|
|
29
|
+
## What changes a revision
|
|
30
|
+
|
|
31
|
+
The revision tracks public resource writes and declared relationship membership.
|
|
32
|
+
It is not a hash of the complete response and is not an HTTP ETag.
|
|
33
|
+
|
|
34
|
+
| Operation | Revision behavior |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| POST or unconditional PUT-create | Initializes a fresh UUID. Reusing a deleted ID never reuses its old revision. |
|
|
37
|
+
| PATCH or PUT-update | Assigns a fresh UUID, including unconditional writes and writes that submit unchanged attribute values. Clients must not treat tokens as operation counts. |
|
|
38
|
+
| Conditional PUT-create | Fails as not found; a condition cannot match a nonexistent row. |
|
|
39
|
+
| DELETE | Checks the supplied condition before deletion; surviving versioned relationship parents are invalidated. |
|
|
40
|
+
| Public relationship mutation | Rotates the owner's revision and affected configured inverse revisions. Explicitly retained many-to-many targets may also receive new tokens. |
|
|
41
|
+
| Child creation, reparenting, detachment or deletion | Rotates affected configured has-many/has-one parent revisions, including declared polymorphic inverses. |
|
|
42
|
+
| Child attribute change with unchanged membership | Does not rotate the parent's revision merely because included child attributes changed. |
|
|
43
|
+
| Ordinary pivot-resource write | Invalidates affected configured parents when either membership key changes. Canonical through-resource records remain separate from canonical links. |
|
|
44
|
+
| Bulk PATCH or DELETE | Applies the same rules per child. Optional `expectedVersions` must contain one token per input entry. |
|
|
45
|
+
| Transaction rollback | Restores revisions together with the associated writes and links. |
|
|
46
|
+
|
|
47
|
+
Submit conditions through `expectedVersion`, not as an attribute update. Omitting
|
|
48
|
+
the condition permits an unconditional write; versioning does not require every
|
|
49
|
+
client to submit a token. JSON:API and plain formats use the same stored revision
|
|
50
|
+
and preserve the field's visibility rules. SQL writers, triggers and transitive
|
|
51
|
+
database cascades require application-owned revision maintenance; the library
|
|
52
|
+
cannot infer their arbitrary effects on other resources or representations.
|
|
53
|
+
|
|
54
|
+
Each successful write consumes the previous revision even inside one transaction.
|
|
55
|
+
Use the returned revision for a second conditional write in that transaction;
|
|
56
|
+
reusing the first token conflicts. Rolling back the transaction restores the
|
|
57
|
+
revision that existed before its writes.
|
|
58
|
+
|
|
59
|
+
## Prepare existing data
|
|
60
|
+
|
|
61
|
+
Stop all writers, including old application instances, jobs and raw SQL writers.
|
|
62
|
+
Take the application's normal backup. Keep writers stopped through field
|
|
63
|
+
allocation, backfill, configuration deployment and readiness checks. Do not
|
|
64
|
+
enable version conditions against partially migrated data.
|
|
65
|
+
|
|
66
|
+
For ordinary Knex storage, add a nullable string column in an explicit schema
|
|
67
|
+
migration. Resolve any declared physical column mapping rather than assuming
|
|
68
|
+
the attribute and column names coincide. Allocate at least 128 characters if
|
|
69
|
+
using VARCHAR. Do not set a single shared default revision for existing rows.
|
|
70
|
+
Schema DDL and the data backfill are separate migration steps: transactional
|
|
71
|
+
DDL support differs by database.
|
|
72
|
+
|
|
73
|
+
For canonical storage, allocate a stored string field through `addKnexFields`
|
|
74
|
+
on the unversioned resource. This records its canonical slot allocation; do not
|
|
75
|
+
choose a `string_N` slot by inspection of empty record values. Read the resulting
|
|
76
|
+
descriptor's `fields.revision.slot` and use that actual column for the backfill.
|
|
77
|
+
Every canonical record query must include both `tenant_id` and `resource`, and
|
|
78
|
+
pagination must use physical `any_records.id`, not tenant-local `logical_id`.
|
|
79
|
+
|
|
80
|
+
Backfill SQL NULL revisions with a fresh UUID for each row in a caller-owned
|
|
81
|
+
transaction. Preserve existing valid string tokens so retries do not invalidate
|
|
82
|
+
clients unnecessarily. Reject empty or overlong existing values and resolve
|
|
83
|
+
them explicitly; do not silently overwrite them. If any validation or write
|
|
84
|
+
fails, let the error escape the transaction callback so the entire data phase
|
|
85
|
+
rolls back. The migration must not call public PATCH merely to populate tokens:
|
|
86
|
+
that would run application write hooks and side effects.
|
|
87
|
+
|
|
88
|
+
The repository's backfill example (source checkout: `examples/migrations/resource-versions.js`)
|
|
89
|
+
implements this data phase in pages of 100 records. Copy it into the application
|
|
90
|
+
migration directory and import it locally; it is an example, not a new library
|
|
91
|
+
export. Use an active raw Knex transaction for this one-off SQL migration.
|
|
92
|
+
|
|
93
|
+
For a dedicated ordinary table, after adding its nullable column:
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
import { backfillResourceVersions } from './resource-versions.js'
|
|
97
|
+
|
|
98
|
+
await knex.transaction(transaction => backfillResourceVersions(transaction, {
|
|
99
|
+
tableName: 'books',
|
|
100
|
+
idColumn: 'id',
|
|
101
|
+
versionColumn: 'revision',
|
|
102
|
+
scope: {}
|
|
103
|
+
}))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Replace the physical names with the application's actual mapping. An explicit
|
|
107
|
+
empty scope means the whole dedicated table. For canonical storage, allocate
|
|
108
|
+
the field and resolve its persisted slot first:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
await api.resources.books.addKnexFields({
|
|
112
|
+
fields: { revision: { type: 'string', nullable: true } }
|
|
113
|
+
})
|
|
114
|
+
const tenant = api.anyapi.tenantId
|
|
115
|
+
const descriptor = await api.anyapi.registry.getDescriptor(tenant, 'books', {
|
|
116
|
+
bypassCache: true
|
|
117
|
+
})
|
|
118
|
+
await knex.transaction(transaction => backfillResourceVersions(transaction, {
|
|
119
|
+
tableName: 'any_records',
|
|
120
|
+
idColumn: 'id',
|
|
121
|
+
versionColumn: descriptor.fields.revision.slot,
|
|
122
|
+
scope: { tenant_id: tenant, resource: 'books' }
|
|
123
|
+
}))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Run field allocation once as its own migration step. Rerun only the data phase
|
|
127
|
+
to check backfill retry behavior; adding an already allocated field is an error.
|
|
128
|
+
The returned `scanned` and `initialized` counts let the migration record exactly
|
|
129
|
+
what it inspected and initialized. A successful retry preserves all tokens and
|
|
130
|
+
returns `initialized: 0`.
|
|
131
|
+
|
|
132
|
+
## Deploy and check readiness
|
|
133
|
+
|
|
134
|
+
Persist the field declaration in application source, including canonical field
|
|
135
|
+
configuration where applicable, then restart with `versionField: 'revision'`.
|
|
136
|
+
Selecting this option alone does not perform schema migration or backfill.
|
|
137
|
+
Restart resource registration against existing tables; do not run
|
|
138
|
+
`createKnexTable` again for an ordinary table that already exists.
|
|
139
|
+
Canonical `alterKnexFields` is not supported; do not use it to enforce SQL
|
|
140
|
+
NOT NULL on a shared slot. Application `required: true` can be enabled in the
|
|
141
|
+
new resource declaration after the backfill.
|
|
142
|
+
|
|
143
|
+
Verify every selected row has a valid string token, unrelated tenants/resources
|
|
144
|
+
are unchanged, and retrying the backfill initializes zero additional rows.
|
|
145
|
+
Check that a new POST initializes a token, unconditional PATCH rotates it,
|
|
146
|
+
a current conditional PATCH succeeds, and a stale one returns
|
|
147
|
+
`REST_API_VERSION_CONFLICT` without changing stored data. Test rollback before
|
|
148
|
+
allowing writers to resume.
|
|
149
|
+
|
|
150
|
+
Clients send `expectedVersion` alongside the single-write options, or aligned
|
|
151
|
+
`expectedVersions` for bulk PATCH/DELETE. They must not submit the revision among
|
|
152
|
+
the record attributes. A conflict requires fetching and reconciling the current
|
|
153
|
+
record; blindly retrying with a newly fetched token can overwrite another
|
|
154
|
+
client's edits. These tokens are not HTTP ETags.
|
|
155
|
+
|
|
156
|
+
If application deployment is rolled back, stop writers first. Old unconditional
|
|
157
|
+
writers do not maintain the new contract. Before re-enabling conditional clients,
|
|
158
|
+
rotate revisions for rows that might have changed under those writers, or
|
|
159
|
+
restore the pre-migration backup as part of the application's rollback plan.
|
|
160
|
+
The nullable added column can remain during rollback; dropping it is not
|
|
161
|
+
necessary to disable conditional writes.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Pivot-resource writes
|
|
165
|
+
|
|
166
|
+
For ordinary storage, public writes to a declared many-to-many through resource
|
|
167
|
+
also invalidate affected versioned parent revisions. Both keys matter: changing
|
|
168
|
+
the member while retaining the parent still changes that parent's membership.
|
|
169
|
+
|
|
170
|
+
Canonical through-resource records are separate from canonical relationship
|
|
171
|
+
links. Use public relationship methods to change canonical memberships; creating
|
|
172
|
+
a through-resource record does not create a canonical link. Direct SQL and other
|
|
173
|
+
writers that bypass the public mutation paths still need explicit revision
|
|
174
|
+
maintenance.
|
|
175
|
+
|
|
176
|
+
## Deleting linked resources
|
|
177
|
+
|
|
178
|
+
Deleting a resource through the public API removes its many-to-many links in
|
|
179
|
+
the same transaction. Recreating the same resource ID starts without those old
|
|
180
|
+
memberships. Surviving versioned parents receive new revisions when their
|
|
181
|
+
membership changes. A caller rollback restores the deleted record, links and
|
|
182
|
+
parent revisions together.
|
|
183
|
+
|
|
184
|
+
Ordinary storage removes remaining declared pivot references after deleting the
|
|
185
|
+
resource row; canonical storage removes links matching its tenant, resource and
|
|
186
|
+
ID in either direction. Applications must not depend on deleted IDs retaining
|
|
187
|
+
memberships for later reuse. This does not provide revision maintenance for
|
|
188
|
+
arbitrary database triggers or SQL writers outside the public API.
|
|
189
|
+
|
|
190
|
+
Earlier releases deliberately left ordinary pivot rows for application cleanup.
|
|
191
|
+
That behavior changes: declared membership rows are removed automatically,
|
|
192
|
+
including for unversioned resources and custom IDs. Related resource records
|
|
193
|
+
are preserved; deleting a book's author links does not delete its authors.
|
|
194
|
+
|
|
195
|
+
Explicit ordinary SQL foreign keys retain their configured behavior: `RESTRICT`
|
|
196
|
+
can reject deletion, leaving both membership and revisions unchanged; `CASCADE`
|
|
197
|
+
can remove the pivot, with the surviving parent's revision updated by the public
|
|
198
|
+
delete transaction. These two rules are tested on SQLite, PostgreSQL and MySQL.
|
|
199
|
+
|
|
200
|
+
## Verification
|
|
201
|
+
|
|
202
|
+
The executable example and allocation/restart workflow pass six checks in each
|
|
203
|
+
of six database/storage combinations (36 total), with no failures or skips.
|
|
204
|
+
Tests cover 103 rows across multiple pages, distinct initialization tokens,
|
|
205
|
+
preservation of existing tokens, retry, rollback after late invalid data,
|
|
206
|
+
caller-owned rollback, filtered records, overlapping canonical tenants/resources,
|
|
207
|
+
and post-migration conditional updates and creation. See
|
|
208
|
+
the test suite (source checkout: `tests/conformance-version-migration.test.js`).
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Fastify integration"
|
|
3
|
+
chapter: 23
|
|
4
|
+
chapter_label: "23"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 23. Fastify integration
|
|
8
|
+
|
|
9
|
+
`json-rest-api` includes a Fastify connector. Integration tests exercise
|
|
10
|
+
Fastify with ordinary Knex and canonical AnyAPI SQLite storage. The current
|
|
11
|
+
verification runtime is Node 24; see [backend capabilities](30-backend-capabilities.md)
|
|
12
|
+
for the supported matrix and limits.
|
|
13
|
+
|
|
14
|
+
## Runnable connector example
|
|
15
|
+
|
|
16
|
+
Install `fastify` alongside the library and database dependencies. Begin with
|
|
17
|
+
an API instance that has `RestApiPlugin` and one storage plugin installed, as in
|
|
18
|
+
[initial setup](01-getting-started.md). Use a fresh database and run the next
|
|
19
|
+
three blocks before registering other resources.
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import fastify from 'fastify'
|
|
23
|
+
import { FastifyPlugin } from 'json-rest-api'
|
|
24
|
+
|
|
25
|
+
const app = fastify()
|
|
26
|
+
await api.use(FastifyPlugin, { app, mountPath: '/api' })
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
await api.addResource('books', {
|
|
31
|
+
schema: { title: { type: 'string', required: true } }
|
|
32
|
+
})
|
|
33
|
+
await api.resources.books.createKnexTable()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Fastify's `inject` runs its HTTP routing, parsers and handlers without opening a
|
|
37
|
+
listening socket. This makes the example runnable as a short verification script.
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
let createdResponse, listResponse, invalidResponse, malformedResponse
|
|
41
|
+
try {
|
|
42
|
+
await app.ready()
|
|
43
|
+
createdResponse = await app.inject({
|
|
44
|
+
method: 'POST', url: '/api/books',
|
|
45
|
+
headers: { 'content-type': 'application/vnd.api+json' },
|
|
46
|
+
payload: { data: { type: 'books', attributes: { title: 'Fastify example' } } }
|
|
47
|
+
})
|
|
48
|
+
listResponse = await app.inject({ method: 'GET', url: '/api/books?page[number]=1&page[size]=10' })
|
|
49
|
+
invalidResponse = await app.inject({
|
|
50
|
+
method: 'POST', url: '/api/books',
|
|
51
|
+
headers: { 'content-type': 'application/vnd.api+json' },
|
|
52
|
+
payload: { data: { type: 'books', attributes: {} } }
|
|
53
|
+
})
|
|
54
|
+
malformedResponse = await app.inject({
|
|
55
|
+
method: 'POST', url: '/api/books',
|
|
56
|
+
headers: { 'content-type': 'application/vnd.api+json' }, payload: '{'
|
|
57
|
+
})
|
|
58
|
+
console.log(createdResponse.statusCode, listResponse.json(), invalidResponse.statusCode, malformedResponse.statusCode)
|
|
59
|
+
} finally { await app.close() }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Creation returns 201 and a JSON:API resource even when programmatic calls default
|
|
63
|
+
to plain records. The list contains one book and reports a total of 1. Missing
|
|
64
|
+
required title returns 422, while malformed JSON returns 400. Neither rejected
|
|
65
|
+
request creates another book.
|
|
66
|
+
|
|
67
|
+
In a running application, call `await app.listen({ port: 3000 })` after declaring
|
|
68
|
+
resources instead of running the injection block. Close the Fastify app and
|
|
69
|
+
destroy its database connection during application shutdown.
|
|
70
|
+
|
|
71
|
+
## What the connector does
|
|
72
|
+
|
|
73
|
+
The Fastify connector listens to the same `addRoute` hook as the Express connector and registers the generated REST routes on the Fastify instance you pass in `pluginOptions.app`.
|
|
74
|
+
|
|
75
|
+
It publishes the transport schema exported by `json-rest-schema` and uses the
|
|
76
|
+
existing compiled request contract through Fastify's route validator compiler:
|
|
77
|
+
|
|
78
|
+
- `POST` routes use the schema export in `create` mode
|
|
79
|
+
- `PUT` routes use the schema export in `replace` mode
|
|
80
|
+
- `PATCH` routes use the schema export in `patch` mode
|
|
81
|
+
- relationship `POST` / `PATCH` / `DELETE` routes use JSON:API relationship document schemas
|
|
82
|
+
|
|
83
|
+
Those field schemas are wrapped in the JSON:API document envelope expected by
|
|
84
|
+
`json-rest-api`. Resource routes select JSON:API input/output and full write
|
|
85
|
+
responses explicitly, regardless of the programmatic or resource defaults.
|
|
86
|
+
Relationship writes and resource DELETE return 204 with no body.
|
|
87
|
+
|
|
88
|
+
The route validator resolves the current cached contract for each payload, so
|
|
89
|
+
canonical field additions also update resource write validation. The JSON
|
|
90
|
+
Schema published when registering the route is an initialization snapshot;
|
|
91
|
+
finalize declarations before generating route-based API documentation.
|
|
92
|
+
|
|
93
|
+
The route validator uses the same contract as programmatic calls. This avoids
|
|
94
|
+
Ajv rejecting `json-rest-schema` metadata at server startup, stripping unknown
|
|
95
|
+
fields, or applying different coercion rules before the resource sees the input.
|
|
96
|
+
The connector registers routes and JSON parsers in a child Fastify scope. The
|
|
97
|
+
host application's parser and validator configuration is not replaced. Register
|
|
98
|
+
resources before calling `app.ready()` or `app.listen()`. Custom routes
|
|
99
|
+
without resource metadata keep their existing unschematized behavior.
|
|
100
|
+
|
|
101
|
+
## What gets rejected early
|
|
102
|
+
|
|
103
|
+
Fastify can now stop malformed write payloads before they reach the app layer:
|
|
104
|
+
|
|
105
|
+
- missing `data`
|
|
106
|
+
- invalid `data.type`
|
|
107
|
+
- invalid `data.id` on `PUT` / `PATCH`
|
|
108
|
+
- malformed attribute payloads
|
|
109
|
+
- unknown attribute fields
|
|
110
|
+
- invalid scalar types according to the resource schema
|
|
111
|
+
- malformed relationship write documents
|
|
112
|
+
|
|
113
|
+
The transport schema also reflects the existing write contract:
|
|
114
|
+
|
|
115
|
+
- output-only computed fields are removed from `data.attributes`
|
|
116
|
+
- direct `belongsTo` foreign keys are removed from `data.attributes`
|
|
117
|
+
- relationship linkage still belongs under `data.relationships`
|
|
118
|
+
|
|
119
|
+
Invalid request documents use the library's typed validation error and HTTP 422
|
|
120
|
+
response. Malformed JSON uses HTTP 400. Resource attribute validation, setters,
|
|
121
|
+
permissions, storage and transaction decisions still run in the resource method.
|
|
122
|
+
Transport request hooks run before document validation; errors with an existing
|
|
123
|
+
request context pass through the transport response hook.
|
|
124
|
+
|
|
125
|
+
## HTTP boundaries
|
|
126
|
+
|
|
127
|
+
The connector accepts `application/vnd.api+json` and `application/json` request
|
|
128
|
+
bodies, and sends `application/vnd.api+json` responses without a charset
|
|
129
|
+
parameter. Both input types use the same JSON parser inside the connector's
|
|
130
|
+
scope. Bodyless resource DELETE works with either content type. Invalid JSON is
|
|
131
|
+
400; a valid JSON value that is not a resource document is 422.
|
|
132
|
+
|
|
133
|
+
Unsupported write media types are rejected with 415 before parsing. JSON:API
|
|
134
|
+
`charset` parameters and extensions are rejected; unknown profiles are ignored.
|
|
135
|
+
An incompatible `Accept` header produces 406, including an explicit zero quality
|
|
136
|
+
for JSON:API. Valid wildcards and JSON:API alternatives are accepted. Responses
|
|
137
|
+
include `Vary: Accept`, preserving other values set by response hooks. These
|
|
138
|
+
rules follow [JSON:API content negotiation](https://jsonapi.org/format/#content-negotiation).
|
|
139
|
+
|
|
140
|
+
Configure the body size limit on the Fastify instance (`fastify({ bodyLimit:
|
|
141
|
+
1048576 })`). The Express connector's corresponding option is
|
|
142
|
+
`requestSizeLimit`. Oversized bodies produce 413 before storage runs.
|
|
143
|
+
|
|
144
|
+
Unknown routes under `mountPath` return a JSON:API 404 and run the transport
|
|
145
|
+
response hook. `handle404: false` leaves these responses to the host application.
|
|
146
|
+
Host routes and parsers outside the API prefix keep their own behavior.
|
|
147
|
+
|
|
148
|
+
Fastify handles malformed URL encodings before plugin lifecycle hooks. Its
|
|
149
|
+
default is a native Fastify 400 document. Applications requiring JSON:API at that
|
|
150
|
+
boundary can configure the host's
|
|
151
|
+
[`routerOptions.onBadUrl`](https://fastify.dev/docs/latest/Reference/Server/#onbadurl)
|
|
152
|
+
when constructing the server:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
const app = fastify({
|
|
156
|
+
routerOptions: {
|
|
157
|
+
onBadUrl (path, req, res) {
|
|
158
|
+
res.writeHead(400, { 'Content-Type': 'application/vnd.api+json' })
|
|
159
|
+
res.end(JSON.stringify({
|
|
160
|
+
errors: [{ status: '400', title: 'Bad Request', detail: 'Malformed URL' }]
|
|
161
|
+
}))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This is a host-wide setting using raw Node request/response objects. The
|
|
168
|
+
connector cannot install it after receiving an existing Fastify instance.
|
|
169
|
+
|
|
170
|
+
## Executed coverage
|
|
171
|
+
|
|
172
|
+
`tests/http-connectors-parity.test.js` exercises real Fastify injection and
|
|
173
|
+
Express 4/5 request handling against both storage modes. It checks resource CRUD,
|
|
174
|
+
PUT creation/replacement, relationship routes, includes, sparse fields,
|
|
175
|
+
pagination links, scalar coercion/nulls, defaults, headers, trusted URL overrides,
|
|
176
|
+
validation, parsing, body limits, content negotiation, asynchronous request-hook
|
|
177
|
+
failures, and host route/parser isolation. Registration/schema tests also use a
|
|
178
|
+
real Fastify instance. Multipart and real database coverage remain separate
|
|
179
|
+
roadmap requirements.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "CORS"
|
|
3
|
+
chapter: 24
|
|
4
|
+
chapter_label: "24"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 24. CORS
|
|
8
|
+
|
|
9
|
+
`CorsPlugin` adds CORS response headers and handles OPTIONS requests beneath the
|
|
10
|
+
HTTP connector's API prefix. Install Express or Fastify before this plugin.
|
|
11
|
+
Register resources and plugins before starting the server.
|
|
12
|
+
|
|
13
|
+
CORS controls whether browser JavaScript can read a cross-origin response.
|
|
14
|
+
Authentication, authorization and CSRF protection remain application concerns;
|
|
15
|
+
an ordinary request from a disallowed origin can still execute. A denied
|
|
16
|
+
preflight stops the browser from sending the subsequent request. See the
|
|
17
|
+
[Fetch CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol).
|
|
18
|
+
|
|
19
|
+
## Basic setup
|
|
20
|
+
|
|
21
|
+
This runnable example uses an in-memory database. The response is visible to
|
|
22
|
+
`https://app.example.com`, including when a request returns an error.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { JsonRestApi } from 'json-rest-api'
|
|
26
|
+
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin, CorsPlugin } from 'json-rest-api'
|
|
27
|
+
import express from 'express'
|
|
28
|
+
import knex from 'knex'
|
|
29
|
+
|
|
30
|
+
const db = knex({
|
|
31
|
+
client: 'better-sqlite3',
|
|
32
|
+
connection: { filename: ':memory:' },
|
|
33
|
+
useNullAsDefault: true
|
|
34
|
+
})
|
|
35
|
+
const app = express()
|
|
36
|
+
const api = new JsonRestApi({ name: 'cors-example' })
|
|
37
|
+
await api.use(RestApiPlugin)
|
|
38
|
+
await api.use(RestApiKnexPlugin, { knex: db })
|
|
39
|
+
await api.use(ExpressPlugin, { mountPath: '/api' })
|
|
40
|
+
await api.use(CorsPlugin, {
|
|
41
|
+
origin: 'https://app.example.com',
|
|
42
|
+
credentials: true,
|
|
43
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
44
|
+
exposedHeaders: ['Location', 'Link'],
|
|
45
|
+
maxAge: 600
|
|
46
|
+
})
|
|
47
|
+
await api.addResource('countries', {
|
|
48
|
+
schema: { id: { type: 'id' }, name: { type: 'string', required: true } }
|
|
49
|
+
})
|
|
50
|
+
await api.resources.countries.createKnexTable()
|
|
51
|
+
api.http.express.mount(app)
|
|
52
|
+
const server = app.listen(3000)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Close `server` and call `db.destroy()` when shutting down. The database and its
|
|
56
|
+
records in this example last only for the lifetime of the process.
|
|
57
|
+
|
|
58
|
+
For Fastify, replace the Express import, app creation, connector installation and
|
|
59
|
+
mount/listen lines with the following respective equivalents. Keep the resource
|
|
60
|
+
and CORS setup before `listen()`:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import fastify from 'fastify'
|
|
64
|
+
import { FastifyPlugin } from 'json-rest-api'
|
|
65
|
+
|
|
66
|
+
const app = fastify()
|
|
67
|
+
await api.use(FastifyPlugin, { app, mountPath: '/api' })
|
|
68
|
+
// Install CorsPlugin and register resources here.
|
|
69
|
+
await app.listen({ port: 3000 })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration options
|
|
73
|
+
|
|
74
|
+
Pass options directly to `api.use(CorsPlugin, options)`.
|
|
75
|
+
|
|
76
|
+
| Option | Default | Behavior |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `origin` | `'*'` | Exact string, regular expression, array of candidates, function, or `false` to disable origin permission |
|
|
79
|
+
| `credentials` | `true` | Adds `Access-Control-Allow-Credentials: true` for allowed origins |
|
|
80
|
+
| `methods` | GET, POST, PUT, PATCH, DELETE, OPTIONS | Array advertised in preflight `Access-Control-Allow-Methods` |
|
|
81
|
+
| `allowedHeaders` | Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override, Accept, Origin | Array advertised in preflight `Access-Control-Allow-Headers` |
|
|
82
|
+
| `exposedHeaders` | X-Total-Count, X-Page-Count, Link, Location | Array of response headers available to browser JavaScript; `[]` omits this header |
|
|
83
|
+
| `maxAge` | `86400` | Preflight cache lifetime in seconds; `0` is respected |
|
|
84
|
+
| `optionsSuccessStatus` | `204` | Successful OPTIONS status; use `200` if required by your client |
|
|
85
|
+
|
|
86
|
+
The default permits every origin, including credentialed requests. With
|
|
87
|
+
`origin: '*'` and `credentials: true`, the plugin reflects the request's Origin.
|
|
88
|
+
Use an explicit list for applications with credentialed browser sessions. For
|
|
89
|
+
public responses without credentials:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
await api.use(CorsPlugin, { origin: '*', credentials: false })
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This returns the literal `Access-Control-Allow-Origin: *` and omits the credential
|
|
96
|
+
header. A browser credentialed fetch requires an explicit allowed origin;
|
|
97
|
+
the literal wildcard is insufficient. The
|
|
98
|
+
[Fetch credentials table](https://fetch.spec.whatwg.org/#cors-protocol-and-credentials)
|
|
99
|
+
shows how these response headers interact.
|
|
100
|
+
|
|
101
|
+
An origin function receives the request Origin and returns a boolean or a
|
|
102
|
+
promise for one. It is evaluated once per HTTP response, including preflight:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
const allowedOrigins = new Set(['https://app.example.com', 'https://admin.example.com'])
|
|
106
|
+
await api.use(CorsPlugin, {
|
|
107
|
+
origin: async origin => allowedOrigins.has(origin),
|
|
108
|
+
credentials: true
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A false result grants no CORS permission. A thrown/rejected predicate produces a
|
|
113
|
+
500 JSON:API error without an allowed-origin header; it is not retried while
|
|
114
|
+
constructing that response. Array candidates are awaited in order. Regular
|
|
115
|
+
expressions match independently for each request without modifying `lastIndex`.
|
|
116
|
+
|
|
117
|
+
`api.vars.cors` contains the current configuration. Changes apply to later
|
|
118
|
+
requests. Prefer updating an origin allowlist used by your predicate rather than
|
|
119
|
+
reinstalling the plugin or duplicating its OPTIONS route.
|
|
120
|
+
|
|
121
|
+
## Preflights and rejected requests
|
|
122
|
+
|
|
123
|
+
Allowed OPTIONS requests return the configured status, origin, methods, headers
|
|
124
|
+
and cache age. Disallowed origins receive 403 with a JSON:API error document:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{"errors":[{"status":"403","title":"Forbidden","detail":"CORS origin not allowed"}]}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The plugin advertises the configured methods and headers; the browser checks
|
|
131
|
+
whether the requested method/headers are allowed. It does not add or authorize
|
|
132
|
+
resource methods. Application request hooks also run for OPTIONS: an
|
|
133
|
+
application that requires authentication on every request should deliberately
|
|
134
|
+
handle browser preflights in its authentication hook, since a preflight does not
|
|
135
|
+
carry the later request's credentials.
|
|
136
|
+
|
|
137
|
+
CORS headers are applied to connector responses for malformed JSON (400), body
|
|
138
|
+
limits (413), unsupported media types (415), incompatible Accept (406), rejected
|
|
139
|
+
request hooks, resource validation, missing records, API 404s and successful
|
|
140
|
+
writes/deletes. A disallowed ordinary request receives no CORS permission, but
|
|
141
|
+
its resource operation is still governed by normal application authorization.
|
|
142
|
+
|
|
143
|
+
When a response reflects or filters Origin, `Vary` includes Origin even for
|
|
144
|
+
absent or denied origins. Host, custom-route and response-hook vary fields are
|
|
145
|
+
merged case-insensitively with the connector's Accept field; `Vary: *` stays `*`.
|
|
146
|
+
Header order is not significant.
|
|
147
|
+
|
|
148
|
+
The CORS hook appends to headers already present when it runs. A later custom
|
|
149
|
+
hook that replaces `response.headers.vary` must preserve any existing fields.
|
|
150
|
+
Use the runtime's `beforeFunction`/`afterFunction` placement options when hook
|
|
151
|
+
order matters; numeric ordering options are not supported.
|
|
152
|
+
|
|
153
|
+
## Response hooks and host boundaries
|
|
154
|
+
|
|
155
|
+
`transport:response` is attempted once for each connector-generated HTTP
|
|
156
|
+
response, including early parser/media errors and `context.reject(...)`.
|
|
157
|
+
On early errors, `context.transport.request.body` can be undefined because the
|
|
158
|
+
parser or resource method never ran. Use `context.transport.response.status`
|
|
159
|
+
when examining the attempted response. If a response hook throws, the connector
|
|
160
|
+
maps the error without running the hook chain a second time. A failure at this
|
|
161
|
+
stage cannot undo a write that has already committed.
|
|
162
|
+
|
|
163
|
+
Host routes outside the API prefix, host middleware that sends its own response,
|
|
164
|
+
and a hook that takes over the raw response are outside this lifecycle. Fastify
|
|
165
|
+
rejects malformed URL escapes before connector hooks; its native 400 response
|
|
166
|
+
therefore has no connector CORS headers. Configure the host's error boundary if
|
|
167
|
+
needed; see the [Fastify guide](23-fastify.md). Express route-parameter decode
|
|
168
|
+
errors do reach the connector error handler.
|
|
169
|
+
|
|
170
|
+
Socket.IO has its own handshake/transport CORS configuration. Configure it
|
|
171
|
+
through `startSocketServer` or the Socket.IO plugin options; this HTTP plugin
|
|
172
|
+
does not replace Socket.IO authentication or subscription permissions.
|