json-rest-api 1.0.29 → 2.1.0

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