json-rest-api 1.0.29 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) 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 +634 -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 +100 -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 +3827 -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 +36 -0
  44. package/index.js +1 -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 +64 -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 +213 -164
  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 +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,634 @@
1
+ ---
2
+ title: "Schema and migrations"
3
+ chapter: 21
4
+ chapter_label: "21"
5
+ ---
6
+
7
+ # 21. Schema and migrations
8
+
9
+ `RestApiKnexPlugin` exposes a small schema-management surface for table-backed resources.
10
+
11
+ Use it when you want to:
12
+
13
+ - create tables directly from resource definitions
14
+ - add or alter plain columns on existing tables
15
+ - inspect a live table snapshot
16
+ - generate a create migration from a resource
17
+ - generate an additive diff migration from a live table
18
+
19
+ This is a Knex/table feature surface. It does not exist for non-table resources.
20
+
21
+ ## Overview
22
+
23
+ Once a resource is backed by `RestApiKnexPlugin`, these scope methods are available:
24
+
25
+ - `createKnexTable()`
26
+ - `addKnexFields({ fields })`
27
+ - `alterKnexFields({ fields })`
28
+ - `introspectKnexTableSnapshot()`
29
+ - `generateKnexMigration()`
30
+ - `generateKnexMigrationDiff()`
31
+
32
+ Example:
33
+
34
+ ```js
35
+ await api.addResource('memberships', {
36
+ schema: {
37
+ id: { type: 'id' },
38
+ workspaceId: { type: 'id', required: true },
39
+ userId: { type: 'id', required: true },
40
+ role: { type: 'string', enum: ['owner', 'member'], required: true, defaultTo: 'member' }
41
+ },
42
+
43
+ indexes: [
44
+ {
45
+ name: 'uq_memberships_workspace_user',
46
+ unique: true,
47
+ columns: ['workspaceId', 'userId']
48
+ }
49
+ ],
50
+
51
+ foreignKeys: [
52
+ {
53
+ name: 'fk_memberships_workspace_user',
54
+ columns: ['workspaceId', 'userId'],
55
+ referencedTableName: 'workspace_users',
56
+ referencedColumns: ['workspace_id', 'user_id'],
57
+ deleteRule: 'CASCADE',
58
+ updateRule: 'RESTRICT'
59
+ }
60
+ ],
61
+
62
+ checkConstraints: [
63
+ {
64
+ name: 'chk_memberships_workspace_positive',
65
+ clause: 'workspace_id > 0'
66
+ }
67
+ ],
68
+
69
+ tableName: 'memberships'
70
+ })
71
+ ```
72
+
73
+ ## Default Column Naming
74
+
75
+ Table-backed resources keep logical field names in the resource schema and map them to physical columns for Knex operations.
76
+
77
+ By default, physical columns use snake_case:
78
+
79
+ - `workspaceId` -> `workspace_id`
80
+ - `userId` -> `user_id`
81
+ - `createdAt` -> `created_at`
82
+
83
+ This keeps the API surface expressive without repeating `storage.column` on every camelCase field.
84
+
85
+ You only need `storage.column` when a field should use a non-standard column name:
86
+
87
+ ```js
88
+ await api.addResource('profiles', {
89
+ schema: {
90
+ id: { type: 'id' },
91
+ displayName: { type: 'string', required: true },
92
+ legacyRef: { type: 'string', storage: { column: 'legacy_profile_ref' } }
93
+ },
94
+ tableName: 'profiles'
95
+ })
96
+ ```
97
+
98
+ If you need physical columns to match the logical field names exactly for a whole resource, opt out explicitly:
99
+
100
+ ```js
101
+ await api.addResource('verbatim_profiles', {
102
+ storage: { naming: 'exact' },
103
+ schema: {
104
+ id: { type: 'id' },
105
+ displayName: { type: 'string', required: true }
106
+ },
107
+ tableName: 'verbatim_profiles'
108
+ })
109
+ ```
110
+
111
+ ## Logical IDs and `idProperty`
112
+
113
+ `idProperty` names the physical primary-key column for table-backed resources. The API contract still uses the logical resource id.
114
+
115
+ ```js
116
+ await api.addResource('profiles', {
117
+ idProperty: 'user_id',
118
+ schema: {
119
+ id: { type: 'id', required: true, storage: { column: 'user_id' } },
120
+ displayName: { type: 'string', required: true },
121
+ loginCount: { type: 'number', defaultTo: 0 }
122
+ },
123
+ tableName: 'profiles'
124
+ })
125
+ ```
126
+
127
+ With this definition:
128
+
129
+ - writes send the resource id as `id` or `data.id`
130
+ - reads return the resource id as `id` or `data.id`
131
+ - the primary key is stored in `user_id`
132
+ - `displayName` and `loginCount` are stored in `display_name` and `login_count`
133
+
134
+ The resource id is not part of `attributes`:
135
+
136
+ ```js
137
+ await api.resources.profiles.post({
138
+ format: 'jsonapi',
139
+ document: {
140
+ data: {
141
+ type: 'profiles',
142
+ id: '42',
143
+ attributes: {
144
+ displayName: 'Mercury'
145
+ }
146
+ }
147
+ }
148
+ })
149
+ ```
150
+
151
+ ### ID normalization
152
+
153
+ If your primary keys need canonicalization beyond the default trim-and-stringify behavior, configure `normalizeId`.
154
+
155
+ ```js
156
+ await api.use(RestApiPlugin, {
157
+ normalizeId: (value) => {
158
+ if (value === null || value === undefined) return null
159
+
160
+ const normalized = String(value).trim()
161
+ return normalized ? normalized.toUpperCase() : null
162
+ }
163
+ })
164
+ ```
165
+
166
+ You can override that per resource:
167
+
168
+ ```js
169
+ await api.addResource('profiles', {
170
+ idProperty: 'user_id',
171
+ normalizeId: (value) => {
172
+ if (value === null || value === undefined) return null
173
+
174
+ const normalized = String(value).trim()
175
+ return normalized ? normalized.toLowerCase() : null
176
+ },
177
+ schema: {
178
+ id: { type: 'id', required: true, storage: { column: 'user_id' } },
179
+ displayName: { type: 'string', required: true }
180
+ },
181
+ tableName: 'profiles'
182
+ })
183
+ ```
184
+
185
+ For table-backed resources, `normalizeId` is applied before:
186
+
187
+ - reading a record by id
188
+ - replacing, patching, or deleting a record
189
+ - persisting an explicit `POST` resource id
190
+ - validating or writing relationship identifiers that point at the resource
191
+
192
+ If the normalizer returns an empty value, existing-resource operations fail as `not_found`, while explicit `POST` document ids fail validation on `data.id`.
193
+
194
+ ## Create Tables
195
+
196
+ Create the table directly from the resource definition:
197
+
198
+ ```js
199
+ await api.resources.memberships.createKnexTable()
200
+ ```
201
+
202
+ `createKnexTable()` understands:
203
+
204
+ - storage-mapped column names
205
+ - mapped logical id columns
206
+ - top-level `indexes`
207
+ - top-level `foreignKeys`
208
+ - top-level `checkConstraints`
209
+ - enum columns
210
+ - MySQL-style `setValues`
211
+
212
+ This is the same table metadata used by migration generation and diffing.
213
+
214
+ ## Field-Only Helpers
215
+
216
+ Two helpers only operate on columns:
217
+
218
+ ```js
219
+ await api.resources.memberships.addKnexFields({
220
+ fields: {
221
+ noteCount: { type: 'number', defaultTo: 0, storage: { column: 'note_count' } }
222
+ }
223
+ })
224
+
225
+ await api.resources.memberships.alterKnexFields({
226
+ fields: {
227
+ noteCount: { type: 'number', defaultTo: 7, storage: { column: 'note_count' } }
228
+ }
229
+ })
230
+ ```
231
+
232
+ Important:
233
+
234
+ - these helpers are field-only
235
+ - they reject top-level `indexes`, `foreignKeys`, and `checkConstraints`
236
+ - use `createKnexTable()`, `generateKnexMigration()`, or `generateKnexMigrationDiff()` for table metadata
237
+
238
+ Both regular-table helpers use the resource's `storage.naming` setting and each
239
+ supplied field's `storage.column`. For example, with `storage: { naming: 'exact' }`,
240
+ adding or altering `loginCount` addresses `loginCount`, not `login_count`.
241
+ Supply the complete intended column definition when altering a field, including
242
+ its explicit mapping, nullability and default. These regular helpers execute DDL;
243
+ keep the resource declaration in app source aligned with the migrated table.
244
+ They do not publish a new compiled resource schema as AnyAPI field additions do.
245
+
246
+ Direct `alterKnexFields` now handles PostgreSQL enum checks separately from the
247
+ column type. PostgreSQL alterations run in a transaction; passing a transaction
248
+ to the deep helper uses a savepoint and leaves the outer commit to its owner.
249
+ An owned PostgreSQL alteration uses the managed completion runner: errors retain
250
+ the original cause and `transactionOutcome`, failed completion prevents reuse of
251
+ its connection, and COMMIT acknowledged as ROLLBACK rejects. A failed or lost
252
+ savepoint RELEASE leaves the outer transaction to its caller; roll back that
253
+ owner rather than assuming the failed alteration has already been undone.
254
+
255
+ An incompatible enum or failed not-null change rolls back the whole field batch.
256
+ The column reader does not impose the full resource snapshot's single-column-ID
257
+ restriction and preserves unrelated indexes.
258
+
259
+ SQLite uses Knex's transactional table rebuild. Changing an enum's allowed
260
+ values, adding an enum to an existing string, or removing an enum throws before
261
+ any field in that call changes; supply a reviewed rebuild migration instead.
262
+ Changing only its default works. For the deep helper, a borrowed SQLite
263
+ transaction must start with `foreign_keys=OFF`, since SQLite cannot disable
264
+ foreign keys inside a transaction. The owner must validate `PRAGMA foreign_key_check`
265
+ before committing and restore enforcement afterward. Standalone calls let Knex
266
+ manage enforcement around its rebuild, preserving referencing rows. The library
267
+ holds that connection lease through error cleanup. If the driver reports an
268
+ unfinished transaction, cleanup rolls it back and restores the original
269
+ foreign-key setting before the connection can be borrowed again. A connection
270
+ whose state cannot be verified or whose cleanup fails is marked disposed.
271
+ Cleanup failure throws an `AggregateError` with the original error as `cause`
272
+ and the original/cleanup errors in order. This does not establish an uncertain
273
+ COMMIT's outcome or retry the alteration.
274
+
275
+ MySQL executes the field batch as one ALTER statement. Passing an existing
276
+ transaction to the deep helper throws before DDL, because MySQL would implicitly
277
+ commit it. Run schema changes separately from application transactions.
278
+
279
+ ### AnyAPI field additions
280
+
281
+ AnyAPI allocates canonical slots and refreshes its compiled resource schema:
282
+
283
+ ```js
284
+ await api.resources.items.addKnexFields({
285
+ fields: {
286
+ note: { type: 'string', nullable: true, search: true },
287
+ score: { type: 'integer', defaultTo: 0 }
288
+ },
289
+ canonicalFieldsMap: { note: 'string_4' },
290
+ searchSchema: {
291
+ noteContains: { type: 'string', actualField: 'note', filterOperator: 'contains' }
292
+ }
293
+ })
294
+ ```
295
+
296
+ This optional map may cover a subset of the new stored fields; remaining fields
297
+ use the existing allocator. An initial resource map must still cover all stored
298
+ fields. Invalid, occupied or unknown slots and existing field names reject the
299
+ addition. Computed and virtual fields need no slots and cannot have overrides.
300
+ AnyAPI `alterKnexFields` remains explicitly unsupported.
301
+
302
+ One call uses one owned metadata transaction. Allocation failure rolls back the
303
+ batch and leaves the runtime schema unchanged. Added getters, setters, defaults,
304
+ serializers, computed fields and filters use the existing schema compiler;
305
+ cached request contracts are refreshed. Ordinary table helpers retain their
306
+ separate DDL behavior.
307
+
308
+ Existing rows are not backfilled: unused slots stay null, and defaults apply to
309
+ subsequent writes. Function defaults do not run during allocation. Backfill old
310
+ rows explicitly when needed. Declare the expanded schema and complete slot map
311
+ in app source for restart, including callbacks that persisted JSON cannot store.
312
+
313
+ Explicit maps now take effect during registration. Repeating `createKnexTable()`
314
+ ensures canonical tables and refreshes schema information without replacing
315
+ field metadata or removing additions.
316
+
317
+ Canonical registration allocates slots from the enriched compiled definitions;
318
+ it does not compile a second schema from persisted JSON. Schema/search hooks run
319
+ once per registration or `addKnexFields` compilation. Repeated table creation
320
+ does not rerun them. Search hook changes therefore survive descriptor refresh.
321
+
322
+ Re-registration rejects removing or remapping existing stored fields while the
323
+ resource has records. Preserve a complete `canonicalFieldsMap` when declaration
324
+ order changes, and perform an explicit data migration for layout changes. See
325
+ the [schema enrichment migration steps](33-migrating-to-v2.md#schema-enrichment-and-canonical-registration),
326
+ including the consequences for hooks that were previously overwritten.
327
+
328
+ Deep `AnyapiRegistry.allocateField` calls persist the field definition and its
329
+ allocation together. Borrowed transactions own their reads/writes and bypass the
330
+ global descriptor cache; the registry does not commit, roll back or publish their
331
+ uncommitted descriptors globally. After commit, call
332
+ `invalidateDescriptor(tenant, resource)` before an ordinary cached read. Deep
333
+ registry calls do not refresh an existing API's compiled schema; use the resource
334
+ helper for in-process additions.
335
+
336
+ Owned registry writes preserve their original rejection when rollback also
337
+ fails. The registry passes the original error and secondary `cleanupErrors` to
338
+ its error logger with the tenant and resource. It guards that logging call;
339
+ these diagnostics do not add fields to a frozen error. Registry writes now reject
340
+ with `RestApiWriteError`, preserving that original error as `cause` and carrying
341
+ the transaction outcome. The [migration guide](33-migrating-to-v2.md) explains the
342
+ error-class change and the outcome contract's current verification status.
343
+
344
+ A failed owned write invalidates its descriptor cache entry. In particular, a
345
+ commit call can reject after the database has completed the transaction; the
346
+ registry skips further rollback and reloads metadata on the next uncached read.
347
+ Driver completion alone does not prove commit or rollback, so callers must not
348
+ assume a rejected operation is safe to retry. Descriptor cache keys distinguish
349
+ the full tenant/resource pair, including identifiers containing `::`. This
350
+ in-memory cache correction requires no stored-data migration.
351
+
352
+ Unexpected descriptor read failures retain their original `cause` and context
353
+ identifying the tenant, resource (`scopeName`) and `descriptor` phase. Typed
354
+ API errors retain their identity on reads and become the cause of
355
+ `RestApiWriteError` when propagated through a write. Relationship processing propagates these
356
+ failures instead of returning partial linkage or includes. Direct registry
357
+ lookups return null for absent metadata; operations requiring that relationship
358
+ descriptor reject a missing descriptor. Schema errors and database failures
359
+ are distinct from a valid relationship with no matching records.
360
+
361
+ ## Live Table Snapshots
362
+
363
+ Use `introspectKnexTableSnapshot()` to inspect the physical table shape:
364
+
365
+ ```js
366
+ const snapshot = await api.resources.memberships.introspectKnexTableSnapshot()
367
+ ```
368
+
369
+ Returned shape:
370
+
371
+ ```js
372
+ {
373
+ dialect: 'sqlite',
374
+ schemaName: 'main',
375
+ tableName: 'memberships',
376
+ tableCollation: '',
377
+ idColumn: 'id',
378
+ primaryKeyColumns: ['id'],
379
+ hasWorkspaceIdColumn: true,
380
+ hasUserIdColumn: true,
381
+ columns: [...],
382
+ indexes: [...],
383
+ foreignKeys: [...],
384
+ checkConstraints: [...]
385
+ }
386
+ ```
387
+
388
+ Column entries are normalized and include information such as:
389
+
390
+ - physical column name
391
+ - logical key
392
+ - type kind
393
+ - nullability
394
+ - default value
395
+ - precision / scale / datetime precision
396
+ - enum values
397
+ - set values
398
+
399
+ Current live introspection support:
400
+
401
+ - SQLite
402
+ - PostgreSQL via `pg` (verified on PostgreSQL 16)
403
+ - MySQL via `mysql2` (verified on MySQL 8; this does not establish MariaDB coverage)
404
+
405
+ PostgreSQL inspection reads the current schema's
406
+ [columns](https://www.postgresql.org/docs/16/infoschema-columns.html),
407
+ [indexes](https://www.postgresql.org/docs/16/catalog-pg-index.html) and
408
+ [constraints](https://www.postgresql.org/docs/16/catalog-pg-constraint.html).
409
+ It recognizes serial/identity allocation, mapped integer/string IDs, native temporal
410
+ precision, JSON/binary types and Knex's inline enum checks. Table and schema
411
+ names are bound in catalog queries.
412
+
413
+ The snapshot requires a single, non-null integer or string primary key.
414
+ Database allocation/default is optional: application-supplied IDs are supported,
415
+ and the snapshot still records the actual `autoIncrement` and default metadata.
416
+ PostgreSQL partial/expression indexes, included
417
+ index columns, nondefault null uniqueness, invalid indexes, exclusion constraints
418
+ and foreign keys into another schema are rejected when they cannot be represented
419
+ by this snapshot. Do not interpret that limited snapshot model as a database
420
+ limitation. Broader schema capabilities remain under verification.
421
+
422
+ ## Create Migrations
423
+
424
+ Use `generateKnexMigration()` to emit a Knex migration string from the resource definition:
425
+
426
+ ```js
427
+ const migration = await api.resources.memberships.generateKnexMigration()
428
+ console.log(migration)
429
+ ```
430
+
431
+ This generates `exports.up` and `exports.down` for full table creation/drop.
432
+ The generated source is CommonJS: save it with a `.cjs` extension in an ESM
433
+ application, and configure Knex's migration loader to include that extension.
434
+ Review the generated migration before running it; generation does not execute DDL.
435
+
436
+ Static defaults retain their value, including empty/quoted/padded strings,
437
+ booleans, SQL null and BigInt literals. A function-valued `defaultTo` belongs to
438
+ runtime validation: table creation and migration generation do not call it or
439
+ freeze its result as a database default.
440
+
441
+ New dateTime/time columns default to precision 6 unless `temporalPrecision` is
442
+ declared. The time declaration explicitly includes its precision because the
443
+ Knex PostgreSQL time builder does not apply the precision argument. SQLite's
444
+ Knex decimal columns are stored as `float`; native decimal precision/scale
445
+ metadata is not available there.
446
+
447
+ It includes the same table metadata as `createKnexTable()`:
448
+
449
+ - mapped columns
450
+ - mapped id column
451
+ - indexes
452
+ - foreign keys
453
+ - check constraints
454
+ - enum columns
455
+ - supported MySQL-specific set columns
456
+
457
+ ## Diff Migrations
458
+
459
+ Use `generateKnexMigrationDiff()` to draft a migration by comparing one resource's
460
+ desired schema against its live table snapshot. Review the plan, warnings and
461
+ generated code before placing the file in your application's Knex migrations:
462
+
463
+ ```js
464
+ const diff = await api.resources.memberships.generateKnexMigrationDiff()
465
+ ```
466
+
467
+ Returned shape:
468
+
469
+ ```js
470
+ {
471
+ migration,
472
+ warnings,
473
+ plan
474
+ }
475
+ ```
476
+
477
+ `plan` is normalized into:
478
+
479
+ - `addColumns`
480
+ - `alterColumns`
481
+ - `dropColumns`
482
+ - `addIndexes`
483
+ - `dropIndexes`
484
+ - `addForeignKeys`
485
+ - `dropForeignKeys`
486
+ - `addCheckConstraints`
487
+ - `dropCheckConstraints` (PostgreSQL inline-enum replacements)
488
+
489
+ The generated migration is intentionally additive-first:
490
+
491
+ - create missing columns
492
+ - alter columns when supported
493
+ - recreate changed indexes when needed
494
+ - add missing foreign keys, or drop and recreate a changed named foreign key
495
+ - add supported check constraints
496
+
497
+ Columns, indexes and foreign keys omitted from the resource schema are preserved
498
+ by default, with warnings. A resource may describe only part of an application's
499
+ database contract. To remove omitted metadata deliberately, pass explicit booleans:
500
+
501
+ ```js
502
+ const diff = await api.resources.memberships.generateKnexMigrationDiff({
503
+ options: {
504
+ allowDropColumns: true,
505
+ allowDropIndexes: true,
506
+ allowDropForeignKeys: true
507
+ }
508
+ })
509
+ ```
510
+
511
+ Enable only the removals you have reviewed. These flags cover every omitted item
512
+ of that kind in this table, so review each planned removal. A changed definition
513
+ under an existing index or foreign-key name is an explicit replacement and adds
514
+ a warning without needing a drop flag. Dropping a column while retaining its
515
+ index or foreign key rejects generation; remove that dependency explicitly too.
516
+ Necessary MySQL indexes supporting declared foreign keys remain preserved.
517
+
518
+ New not-null columns without a static non-null database default produce a
519
+ backfill warning, even when an application hook or function default would supply
520
+ values for future writes. Bigint-to-integer changes on native databases warn
521
+ about overflow. Generation does not inspect existing row values to prove either
522
+ change will succeed.
523
+
524
+ The diff compares effective storage declarations, including the default string
525
+ length and temporal precision. Native integer/float/binary metadata does not
526
+ produce an alteration merely because it reports implicit size information.
527
+ Boolean and decimal defaults compare by value; textual defaults retain their
528
+ contents. Equivalent check formatting does not propose a duplicate constraint,
529
+ and comparison preserves string-literal case.
530
+
531
+ PostgreSQL enum changes alter the text column and replace its inline check in
532
+ separate statements. `plan.dropCheckConstraints` makes those removals visible.
533
+ Use the normal transactional Knex migration execution: if existing values violate
534
+ the new enum, PostgreSQL can roll back the check removal and column change.
535
+ MySQL uses native enum alteration; its rejected narrowing case preserves the
536
+ old values and definition, but this does not promise transactional MySQL DDL.
537
+
538
+ Changing only native datetime/time precision now produces an executable
539
+ alteration. Reducing precision adds a data-loss warning. Tests execute increasing
540
+ precision and additive changes with existing rows, verify preserved values, then
541
+ require an empty second diff. MySQL non-unique indexes needed by retained foreign
542
+ keys are kept even when they were created implicitly by the database.
543
+
544
+ Enum values and static text/JSON/array defaults preserve apostrophes, backslashes
545
+ and question marks on the tested drivers. PostgreSQL literals avoid Knex's
546
+ parameter-marker rewriting, and MySQL introspection decodes generated JSON
547
+ defaults and escaped enum values without proposing another alteration. Quoted
548
+ constraint identifiers preserve case and spaces; changing `role` does not remove
549
+ a check on PostgreSQL's distinct `Role` column.
550
+
551
+ Direct table helpers require `schema` to contain `structure: fields`, including
552
+ add/alter helpers. Keep table metadata beside `structure`; bare field maps reject
553
+ before SQL. A `json-rest-schema` instance already provides `structure`. Public
554
+ resource calls retain `addKnexFields({ fields })` and `alterKnexFields({ fields })`.
555
+ See [the migration example](33-migrating-to-v2.md#table-helper-schemas-use-an-explicit-structure-wrapper).
556
+
557
+ Standalone `generateKnexMigration(tableName, schema, options)` calls must supply
558
+ `options.dialect` when enum/default values need dialect-specific escaping. The
559
+ resource-level generator already obtains it from the configured Knex client.
560
+
561
+ When a MySQL migration replaces or removes a supporting index, its plan includes
562
+ dropping the dependent foreign key first and restoring it after the new indexes
563
+ exist. This also allows removing an unwanted unique index while retaining
564
+ referential integrity. MySQL may create a replacement non-unique supporting
565
+ index. Composite keys retain their declared column order and referential actions.
566
+ These migrations use multiple DDL statements: pause application writes during
567
+ execution, and account for MySQL's implicit commits when planning recovery.
568
+
569
+ ## Important Limits
570
+
571
+ This is a schema-vs-live-table diff, not a migration-history engine.
572
+
573
+ It answers:
574
+
575
+ - what the table looks like now
576
+ - what the resource says it should look like
577
+ - what Knex migration changes it can propose for review
578
+
579
+ It does not answer:
580
+
581
+ - what changed since a previous migration file
582
+ - how to reconstruct migration history
583
+ - whether the proposed change is safe for all existing data and application code
584
+ - how to synchronize an entire database from a resource schema
585
+
586
+ Other important limits:
587
+
588
+ - destructive changes are surfaced as warnings
589
+ - omitted columns, indexes and foreign keys are preserved unless their respective
590
+ drop option is explicitly enabled
591
+ - partial and expression indexes cannot be represented by the simple SQLite or
592
+ PostgreSQL snapshot and reject introspection instead of producing a false match
593
+ - SQLite descending or non-binary-collation indexes, and MySQL expression,
594
+ prefix or descending indexes, also reject full snapshots
595
+ - generated columns reject full table snapshots on SQLite, MySQL and PostgreSQL;
596
+ the narrower reader used by direct field alterations remains separate
597
+ - SQLite check-constraint add/drop/alter support is warning-only
598
+ - changing/removing an existing named check is warning-only on all dialects;
599
+ PostgreSQL inline-enum replacements are handled separately
600
+ - changing the allowed enum values on SQLite is warning-only: its native ALTER
601
+ cannot replace the check, and Knex's column rebuild retains the old check.
602
+ Supply a reviewed table-rebuild migration for that change. The generated diff
603
+ leaves the existing enum column unchanged and reports why; it does not claim
604
+ that the new values became valid. Changing only an enum default is supported
605
+ - `setValues` diffing is only emitted for MySQL-compatible targets
606
+ - diff migrations do not auto-generate a destructive `down`
607
+
608
+ Passing the real-table suite does not verify every alteration: arbitrary SQL
609
+ default expressions, complex dependencies from other tables, native PostgreSQL
610
+ enum types and manual SQLite enum rebuilds remain outside its verified surface. Review the
611
+ generated SQL and warnings for the actual database. See
612
+ [contributing](../contributing.md#real-databases-and-redis) for native test commands.
613
+
614
+ PostgreSQL index sort direction, operator classes and collation details are not
615
+ fully represented by this snapshot. Inspect those definitions separately before
616
+ replacing an index; a successful snapshot is not a complete dependency or SQL
617
+ feature audit.
618
+
619
+ ## When to Use Which Helper
620
+
621
+ - `createKnexTable()`
622
+ - bootstrap a table directly from the resource
623
+
624
+ - `addKnexFields()` / `alterKnexFields()`
625
+ - small column-only local changes
626
+
627
+ - `introspectKnexTableSnapshot()`
628
+ - inspect the real physical table
629
+
630
+ - `generateKnexMigration()`
631
+ - emit a full create migration from the current resource definition
632
+
633
+ - `generateKnexMigrationDiff()`
634
+ - emit an additive migration against the live table