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,733 @@
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.
311
+
312
+ Additions are stored with their search schema, so they survive a restart
313
+ without being declared again. Persisted JSON cannot hold callbacks: getters,
314
+ setters, function defaults, serializers and `compute` functions exist only in the
315
+ process that added them. A server that loads such a field without declaring it
316
+ in code hides the field and logs a warning instead of serving it without its
317
+ callbacks. Declare fields with callbacks in app source on every server; a
318
+ declared field always takes precedence over a stored addition of the same name.
319
+
320
+ Explicit maps now take effect during registration. Repeating `createKnexTable()`
321
+ ensures canonical tables and refreshes schema information without replacing
322
+ field metadata or removing additions.
323
+
324
+ Canonical registration allocates slots from the enriched compiled definitions.
325
+ Stored additions that the declaration does not repeat are compiled from their
326
+ persisted JSON. Schema/search hooks run once per registration, `addKnexFields`
327
+ compilation or reload that changes the field set. Repeated table creation does
328
+ not rerun them. Search hook changes therefore survive descriptor refresh.
329
+
330
+ Registration never moves a stored field: existing fields keep their slots
331
+ whatever the declaration order, and new fields take the first free slot. A field
332
+ the declaration omits keeps its mapping while the resource has records, because
333
+ servers still running older code may use it; this declaration stops publishing
334
+ it. Unpopulated omitted fields are released. Changing a populated field's storage
335
+ type is still rejected and needs an explicit data migration. See the
336
+ [schema enrichment migration steps](33-migrating-to-v2.md#schema-enrichment-and-canonical-registration),
337
+ including the consequences for hooks that were previously overwritten.
338
+
339
+ Registering an unchanged declaration writes nothing. Concurrent schema changes
340
+ to one resource queue on its metadata row, and a unique index stops two fields
341
+ from sharing a slot. Each committed change increments the resource's
342
+ `schema_version`. On MySQL, the registry runs its own transactions at READ
343
+ COMMITTED, because REPEATABLE READ makes concurrent creation of different
344
+ resources or tenants deadlock. A transaction you pass in keeps its own isolation
345
+ level.
346
+
347
+ ### AnyAPI across several servers
348
+
349
+ Each server compiles resources into memory. When one server adds fields, the
350
+ others keep their published schema until they reload it:
351
+
352
+ ```js
353
+ await api.use(RestApiAnyapiKnexPlugin, {
354
+ knex,
355
+ tenantId,
356
+ // Runs after each committed change; publish it to the other servers.
357
+ onSchemaChanged: change => notifyServers(change),
358
+ // Safety net for missed notifications.
359
+ schemaCheckInterval: 5000
360
+ })
361
+
362
+ // On a server that receives a notification:
363
+ await api.anyapi.handleSchemaChange(change)
364
+ ```
365
+
366
+ `handleSchemaChange({ tenant, resource, version })` reloads the resource only when
367
+ the version is newer than the published one, and returns whether it did.
368
+ `api.resources.items.reloadSchema()` reloads unconditionally and resolves to the
369
+ published version. Reloads are read-only: they combine the stored definition
370
+ with this server's declaration and never re-register it.
371
+
372
+ With `schemaCheckInterval`, request methods compare the stored version at most
373
+ once per interval; `0` checks on every request. A request rejected by validation
374
+ also checks the version once and, if the resource changed, reloads and retries.
375
+ A write whose transaction may have committed is never retried. Calls given a
376
+ caller transaction skip both checks, because that transaction may hold
377
+ uncommitted metadata. A rejected `onSchemaChanged` callback is logged; the
378
+ committed change stands.
379
+
380
+ A server can serve resources it never declared. `await api.anyapi.loadResource('items')`
381
+ loads a stored resource, with the resources it relates to, and resolves to it, or to
382
+ `null` when none is stored. `api.anyapi.loadResources()` loads every stored resource
383
+ of the tenant. A stored resource whose non-field options needed callbacks, such as
384
+ `normalizeId`, must be declared in code. To load resources on demand over HTTP,
385
+ call `loadResource` from middleware placed before the API router.
386
+
387
+ ### AnyAPI tenant instances
388
+
389
+ One API instance per tenant stays cheap when the instances share their start-up work:
390
+
391
+ ```js
392
+ // In a migration:
393
+ await ensureAnyApiSchema(knex, { slots: { string: 60, number: 30, boolean: 20, date: 20, json: 10, belongsTo: 15 } })
394
+
395
+ // In the application:
396
+ const registry = new AnyapiRegistry({ knex, cacheSize: 5000 })
397
+ const tenantApi = async tenantId => {
398
+ const api = new JsonRestApi()
399
+ await api.use(RestApiPlugin)
400
+ await api.use(RestApiAnyapiKnexPlugin, { knex, tenantId, registry, ensureSchema: false, ownsKnex: false })
401
+ await api.anyapi.loadResources()
402
+ return api
403
+ }
404
+ ```
405
+
406
+ A shared registry reads database capabilities and the slot layout once, and holds
407
+ one descriptor cache, bounded by `cacheSize` (default 100). `ensureSchema: false`
408
+ skips table checks and the logical ID backfill at start-up and in `createKnexTable()`;
409
+ migrations must then run `ensureAnyApiSchema`. `ownsKnex: false` keeps `api.release()`
410
+ from destroying the shared Knex instance. A dropped instance needs no other cleanup.
411
+
412
+ ### AnyAPI slot limits
413
+
414
+ Slot limits belong to the database: every server reads the slot columns that
415
+ `any_records` actually has. The defaults are 10 string, 10 number, 5 boolean, 5
416
+ date, 5 JSON and 5 belongsTo slots. `ensureAnyApiSchema(knex, { slots })`, or the
417
+ plugin's `slots` option when it ensures the schema, adds missing columns. Lower
418
+ counts never drop columns. A server whose pool is exhausted rereads the layout
419
+ once, so a migration that adds slots takes effect without a restart.
420
+
421
+ On PostgreSQL, adding a nullable column without a default does not rewrite the
422
+ table. MySQL can add columns instantly from 8.0.29; earlier versions may copy the
423
+ table. MySQL limits a row to 65,535 bytes and each belongsTo slot adds two
424
+ string columns, so check the row size before raising that pool substantially.
425
+
426
+ Deep `AnyapiRegistry.allocateField` and `allocateFields` calls persist the field
427
+ definitions and their allocation together. `allocateFields` adds a batch under one
428
+ version; pass each field's uncompiled `declaration` to store it for other servers. Borrowed transactions own their reads/writes and bypass the
429
+ global descriptor cache; the registry does not commit, roll back or publish their
430
+ uncommitted descriptors globally. After commit, call
431
+ `invalidateDescriptor(tenant, resource)` before an ordinary cached read. Deep
432
+ registry calls do not refresh an existing API's compiled schema; use the resource
433
+ helper for in-process additions.
434
+
435
+ Owned registry writes preserve their original rejection when rollback also
436
+ fails. The registry passes the original error and secondary `cleanupErrors` to
437
+ its error logger with the tenant and resource. It guards that logging call;
438
+ these diagnostics do not add fields to a frozen error. Registry writes now reject
439
+ with `RestApiWriteError`, preserving that original error as `cause` and carrying
440
+ the transaction outcome. The [migration guide](33-migrating-to-v2.md) explains the
441
+ error-class change and the outcome contract's current verification status.
442
+
443
+ A failed owned write invalidates its descriptor cache entry. In particular, a
444
+ commit call can reject after the database has completed the transaction; the
445
+ registry skips further rollback and reloads metadata on the next uncached read.
446
+ Driver completion alone does not prove commit or rollback, so callers must not
447
+ assume a rejected operation is safe to retry. Descriptor cache keys distinguish
448
+ the full tenant/resource pair, including identifiers containing `::`. This
449
+ in-memory cache correction requires no stored-data migration.
450
+
451
+ Unexpected descriptor read failures retain their original `cause` and context
452
+ identifying the tenant, resource (`scopeName`) and `descriptor` phase. Typed
453
+ API errors retain their identity on reads and become the cause of
454
+ `RestApiWriteError` when propagated through a write. Relationship processing propagates these
455
+ failures instead of returning partial linkage or includes. Direct registry
456
+ lookups return null for absent metadata; operations requiring that relationship
457
+ descriptor reject a missing descriptor. Schema errors and database failures
458
+ are distinct from a valid relationship with no matching records.
459
+
460
+ ## Live Table Snapshots
461
+
462
+ Use `introspectKnexTableSnapshot()` to inspect the physical table shape:
463
+
464
+ ```js
465
+ const snapshot = await api.resources.memberships.introspectKnexTableSnapshot()
466
+ ```
467
+
468
+ Returned shape:
469
+
470
+ ```js
471
+ {
472
+ dialect: 'sqlite',
473
+ schemaName: 'main',
474
+ tableName: 'memberships',
475
+ tableCollation: '',
476
+ idColumn: 'id',
477
+ primaryKeyColumns: ['id'],
478
+ hasWorkspaceIdColumn: true,
479
+ hasUserIdColumn: true,
480
+ columns: [...],
481
+ indexes: [...],
482
+ foreignKeys: [...],
483
+ checkConstraints: [...]
484
+ }
485
+ ```
486
+
487
+ Column entries are normalized and include information such as:
488
+
489
+ - physical column name
490
+ - logical key
491
+ - type kind
492
+ - nullability
493
+ - default value
494
+ - precision / scale / datetime precision
495
+ - enum values
496
+ - set values
497
+
498
+ Current live introspection support:
499
+
500
+ - SQLite
501
+ - PostgreSQL via `pg` (verified on PostgreSQL 16)
502
+ - MySQL via `mysql2` (verified on MySQL 8; this does not establish MariaDB coverage)
503
+
504
+ PostgreSQL inspection reads the current schema's
505
+ [columns](https://www.postgresql.org/docs/16/infoschema-columns.html),
506
+ [indexes](https://www.postgresql.org/docs/16/catalog-pg-index.html) and
507
+ [constraints](https://www.postgresql.org/docs/16/catalog-pg-constraint.html).
508
+ It recognizes serial/identity allocation, mapped integer/string IDs, native temporal
509
+ precision, JSON/binary types and Knex's inline enum checks. Table and schema
510
+ names are bound in catalog queries.
511
+
512
+ The snapshot requires a single, non-null integer or string primary key.
513
+ Database allocation/default is optional: application-supplied IDs are supported,
514
+ and the snapshot still records the actual `autoIncrement` and default metadata.
515
+ PostgreSQL partial/expression indexes, included
516
+ index columns, nondefault null uniqueness, invalid indexes, exclusion constraints
517
+ and foreign keys into another schema are rejected when they cannot be represented
518
+ by this snapshot. Do not interpret that limited snapshot model as a database
519
+ limitation. Broader schema capabilities remain under verification.
520
+
521
+ ## Create Migrations
522
+
523
+ Use `generateKnexMigration()` to emit a Knex migration string from the resource definition:
524
+
525
+ ```js
526
+ const migration = await api.resources.memberships.generateKnexMigration()
527
+ console.log(migration)
528
+ ```
529
+
530
+ This generates `exports.up` and `exports.down` for full table creation/drop.
531
+ The generated source is CommonJS: save it with a `.cjs` extension in an ESM
532
+ application, and configure Knex's migration loader to include that extension.
533
+ Review the generated migration before running it; generation does not execute DDL.
534
+
535
+ Static defaults retain their value, including empty/quoted/padded strings,
536
+ booleans, SQL null and BigInt literals. A function-valued `defaultTo` belongs to
537
+ runtime validation: table creation and migration generation do not call it or
538
+ freeze its result as a database default.
539
+
540
+ New dateTime/time columns default to precision 6 unless `temporalPrecision` is
541
+ declared. The time declaration explicitly includes its precision because the
542
+ Knex PostgreSQL time builder does not apply the precision argument. SQLite's
543
+ Knex decimal columns are stored as `float`; native decimal precision/scale
544
+ metadata is not available there.
545
+
546
+ It includes the same table metadata as `createKnexTable()`:
547
+
548
+ - mapped columns
549
+ - mapped id column
550
+ - indexes
551
+ - foreign keys
552
+ - check constraints
553
+ - enum columns
554
+ - supported MySQL-specific set columns
555
+
556
+ ## Diff Migrations
557
+
558
+ Use `generateKnexMigrationDiff()` to draft a migration by comparing one resource's
559
+ desired schema against its live table snapshot. Review the plan, warnings and
560
+ generated code before placing the file in your application's Knex migrations:
561
+
562
+ ```js
563
+ const diff = await api.resources.memberships.generateKnexMigrationDiff()
564
+ ```
565
+
566
+ Returned shape:
567
+
568
+ ```js
569
+ {
570
+ migration,
571
+ warnings,
572
+ plan
573
+ }
574
+ ```
575
+
576
+ `plan` is normalized into:
577
+
578
+ - `addColumns`
579
+ - `alterColumns`
580
+ - `dropColumns`
581
+ - `addIndexes`
582
+ - `dropIndexes`
583
+ - `addForeignKeys`
584
+ - `dropForeignKeys`
585
+ - `addCheckConstraints`
586
+ - `dropCheckConstraints` (PostgreSQL inline-enum replacements)
587
+
588
+ The generated migration is intentionally additive-first:
589
+
590
+ - create missing columns
591
+ - alter columns when supported
592
+ - recreate changed indexes when needed
593
+ - add missing foreign keys, or drop and recreate a changed named foreign key
594
+ - add supported check constraints
595
+
596
+ Columns, indexes and foreign keys omitted from the resource schema are preserved
597
+ by default, with warnings. A resource may describe only part of an application's
598
+ database contract. To remove omitted metadata deliberately, pass explicit booleans:
599
+
600
+ ```js
601
+ const diff = await api.resources.memberships.generateKnexMigrationDiff({
602
+ options: {
603
+ allowDropColumns: true,
604
+ allowDropIndexes: true,
605
+ allowDropForeignKeys: true
606
+ }
607
+ })
608
+ ```
609
+
610
+ Enable only the removals you have reviewed. These flags cover every omitted item
611
+ of that kind in this table, so review each planned removal. A changed definition
612
+ under an existing index or foreign-key name is an explicit replacement and adds
613
+ a warning without needing a drop flag. Dropping a column while retaining its
614
+ index or foreign key rejects generation; remove that dependency explicitly too.
615
+ Necessary MySQL indexes supporting declared foreign keys remain preserved.
616
+
617
+ New not-null columns without a static non-null database default produce a
618
+ backfill warning, even when an application hook or function default would supply
619
+ values for future writes. Bigint-to-integer changes on native databases warn
620
+ about overflow. Generation does not inspect existing row values to prove either
621
+ change will succeed.
622
+
623
+ The diff compares effective storage declarations, including the default string
624
+ length and temporal precision. Native integer/float/binary metadata does not
625
+ produce an alteration merely because it reports implicit size information.
626
+ Boolean and decimal defaults compare by value; textual defaults retain their
627
+ contents. Equivalent check formatting does not propose a duplicate constraint,
628
+ and comparison preserves string-literal case.
629
+
630
+ PostgreSQL enum changes alter the text column and replace its inline check in
631
+ separate statements. `plan.dropCheckConstraints` makes those removals visible.
632
+ Use the normal transactional Knex migration execution: if existing values violate
633
+ the new enum, PostgreSQL can roll back the check removal and column change.
634
+ MySQL uses native enum alteration; its rejected narrowing case preserves the
635
+ old values and definition, but this does not promise transactional MySQL DDL.
636
+
637
+ Changing only native datetime/time precision now produces an executable
638
+ alteration. Reducing precision adds a data-loss warning. Tests execute increasing
639
+ precision and additive changes with existing rows, verify preserved values, then
640
+ require an empty second diff. MySQL non-unique indexes needed by retained foreign
641
+ keys are kept even when they were created implicitly by the database.
642
+
643
+ Enum values and static text/JSON/array defaults preserve apostrophes, backslashes
644
+ and question marks on the tested drivers. PostgreSQL literals avoid Knex's
645
+ parameter-marker rewriting, and MySQL introspection decodes generated JSON
646
+ defaults and escaped enum values without proposing another alteration. Quoted
647
+ constraint identifiers preserve case and spaces; changing `role` does not remove
648
+ a check on PostgreSQL's distinct `Role` column.
649
+
650
+ Direct table helpers require `schema` to contain `structure: fields`, including
651
+ add/alter helpers. Keep table metadata beside `structure`; bare field maps reject
652
+ before SQL. A `json-rest-schema` instance already provides `structure`. Public
653
+ resource calls retain `addKnexFields({ fields })` and `alterKnexFields({ fields })`.
654
+ See [the migration example](33-migrating-to-v2.md#table-helper-schemas-use-an-explicit-structure-wrapper).
655
+
656
+ Standalone `generateKnexMigration(tableName, schema, options)` calls must supply
657
+ `options.dialect` when enum/default values need dialect-specific escaping. The
658
+ resource-level generator already obtains it from the configured Knex client.
659
+
660
+ When a MySQL migration replaces or removes a supporting index, its plan includes
661
+ dropping the dependent foreign key first and restoring it after the new indexes
662
+ exist. This also allows removing an unwanted unique index while retaining
663
+ referential integrity. MySQL may create a replacement non-unique supporting
664
+ index. Composite keys retain their declared column order and referential actions.
665
+ These migrations use multiple DDL statements: pause application writes during
666
+ execution, and account for MySQL's implicit commits when planning recovery.
667
+
668
+ ## Important Limits
669
+
670
+ This is a schema-vs-live-table diff, not a migration-history engine.
671
+
672
+ It answers:
673
+
674
+ - what the table looks like now
675
+ - what the resource says it should look like
676
+ - what Knex migration changes it can propose for review
677
+
678
+ It does not answer:
679
+
680
+ - what changed since a previous migration file
681
+ - how to reconstruct migration history
682
+ - whether the proposed change is safe for all existing data and application code
683
+ - how to synchronize an entire database from a resource schema
684
+
685
+ Other important limits:
686
+
687
+ - destructive changes are surfaced as warnings
688
+ - omitted columns, indexes and foreign keys are preserved unless their respective
689
+ drop option is explicitly enabled
690
+ - partial and expression indexes cannot be represented by the simple SQLite or
691
+ PostgreSQL snapshot and reject introspection instead of producing a false match
692
+ - SQLite descending or non-binary-collation indexes, and MySQL expression,
693
+ prefix or descending indexes, also reject full snapshots
694
+ - generated columns reject full table snapshots on SQLite, MySQL and PostgreSQL;
695
+ the narrower reader used by direct field alterations remains separate
696
+ - SQLite check-constraint add/drop/alter support is warning-only
697
+ - changing/removing an existing named check is warning-only on all dialects;
698
+ PostgreSQL inline-enum replacements are handled separately
699
+ - changing the allowed enum values on SQLite is warning-only: its native ALTER
700
+ cannot replace the check, and Knex's column rebuild retains the old check.
701
+ Supply a reviewed table-rebuild migration for that change. The generated diff
702
+ leaves the existing enum column unchanged and reports why; it does not claim
703
+ that the new values became valid. Changing only an enum default is supported
704
+ - `setValues` diffing is only emitted for MySQL-compatible targets
705
+ - diff migrations do not auto-generate a destructive `down`
706
+
707
+ Passing the real-table suite does not verify every alteration: arbitrary SQL
708
+ default expressions, complex dependencies from other tables, native PostgreSQL
709
+ enum types and manual SQLite enum rebuilds remain outside its verified surface. Review the
710
+ generated SQL and warnings for the actual database. See
711
+ [contributing](../contributing.md#real-databases-and-redis) for native test commands.
712
+
713
+ PostgreSQL index sort direction, operator classes and collation details are not
714
+ fully represented by this snapshot. Inspect those definitions separately before
715
+ replacing an index; a successful snapshot is not a complete dependency or SQL
716
+ feature audit.
717
+
718
+ ## When to Use Which Helper
719
+
720
+ - `createKnexTable()`
721
+ - bootstrap a table directly from the resource
722
+
723
+ - `addKnexFields()` / `alterKnexFields()`
724
+ - small column-only local changes
725
+
726
+ - `introspectKnexTableSnapshot()`
727
+ - inspect the real physical table
728
+
729
+ - `generateKnexMigration()`
730
+ - emit a full create migration from the current resource definition
731
+
732
+ - `generateKnexMigrationDiff()`
733
+ - emit an additive migration against the live table