json-rest-api 1.0.22 → 1.0.24

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 (290) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/README.md +22 -1
  4. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  5. package/docs/API.md +196 -38
  6. package/docs/COMPARISON.md +2 -2
  7. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +111 -5
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  9. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  10. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  11. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  12. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  13. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  14. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  15. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  16. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +370 -0
  17. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  18. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  19. package/docs/GUIDE/index.md +18 -9
  20. package/docs/QUICKSTART.md +5 -1
  21. package/docs/index.md +1 -1
  22. package/index.js +3 -2
  23. package/lib/enhanced-logger.js +1 -1
  24. package/package.json +6 -2
  25. package/plugins/core/bulk-operations-plugin.js +26 -34
  26. package/plugins/core/connectors/express-plugin.js +97 -190
  27. package/plugins/core/connectors/fastify-plugin.js +206 -0
  28. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  29. package/plugins/core/connectors/lib/connector-core.js +195 -0
  30. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  31. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  32. package/plugins/core/lib/anyapi/schema-utils.js +43 -4
  33. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
  34. package/plugins/core/lib/dbIntrospection.js +1078 -0
  35. package/plugins/core/lib/dbTablesOperations.js +1111 -160
  36. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  37. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -3
  38. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  39. package/plugins/core/lib/querying/knex-query-helpers.js +21 -76
  40. package/plugins/core/lib/querying/knex-relationship-includes.js +468 -215
  41. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  42. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  43. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  44. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  45. package/plugins/core/lib/querying/url-helpers.js +8 -2
  46. package/plugins/core/lib/querying-writing/compile-schemas.js +20 -5
  47. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -14
  48. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  49. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  50. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  51. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  52. package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
  53. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  54. package/plugins/core/lib/storage/canonical-storage-mapping.js +133 -0
  55. package/plugins/core/lib/storage/storage-adapter.js +82 -4
  56. package/plugins/core/lib/storage/storage-mapping.js +274 -0
  57. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
  58. package/plugins/core/rest-api-anyapi-knex-plugin.js +531 -312
  59. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  60. package/plugins/core/rest-api-knex-plugin.js +271 -226
  61. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  62. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  63. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
  64. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  65. package/plugins/core/rest-api-plugin-methods/common.js +148 -92
  66. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
  67. package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
  68. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  69. package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
  70. package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
  71. package/plugins/core/rest-api-plugin-methods/get.js +29 -14
  72. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
  73. package/plugins/core/rest-api-plugin-methods/patch.js +56 -12
  74. package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
  75. package/plugins/core/rest-api-plugin-methods/post.js +21 -8
  76. package/plugins/core/rest-api-plugin-methods/put.js +73 -21
  77. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  78. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  79. package/plugins/core/rest-api-plugin.js +6 -0
  80. package/plugins/core/rest-api-positioning-plugin.js +2 -5
  81. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  82. package/plugins/core/socketio-plugin.js +1 -4
  83. package/plugins/storage/local-storage.js +1 -1
  84. package/plugins/storage/s3-storage.js +5 -2
  85. package/quickTest.js +1 -0
  86. package/scripts/run-docs.js +80 -84
  87. package/test-debug.js +1 -1
  88. package/tests/README.md +7 -0
  89. package/tests/anyapi-basic.test.js +3 -0
  90. package/tests/autofilter.test.js +463 -0
  91. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  92. package/tests/custom-id-simplified-post.test.js +75 -0
  93. package/tests/custom-idproperty-relationships.test.js +33 -0
  94. package/tests/db-introspection.test.js +521 -0
  95. package/tests/db-table-operations.test.js +786 -0
  96. package/tests/fastify-plugin.test.js +156 -0
  97. package/tests/field-getters.test.js +1 -2
  98. package/tests/field-setters.test.js +0 -4
  99. package/tests/fixtures/api-configs.js +278 -176
  100. package/tests/helpers/fake-fastify.js +99 -0
  101. package/tests/http-connectors-parity.test.js +235 -0
  102. package/tests/id-normalization.test.js +444 -0
  103. package/tests/logical-id-write-validation.test.js +151 -0
  104. package/tests/pagination-cursor-multifield.test.js +11 -11
  105. package/tests/projected-fields.test.js +319 -0
  106. package/tests/relationships.test.js +3 -3
  107. package/tests/searchschema-knex-behavior.test.js +187 -0
  108. package/tests/searchschema-merge.test.js +21 -69
  109. package/tests/storage-mapping.test.js +259 -0
  110. package/audit/00-charter.md +0 -103
  111. package/audit/01-scope-map.md +0 -84
  112. package/audit/02-coverage-matrix.csv +0 -174
  113. package/audit/03-invariants.md +0 -123
  114. package/audit/04-standards-rubric.md +0 -56
  115. package/audit/05-competitor-rubric.md +0 -77
  116. package/audit/06-findings.md +0 -160
  117. package/audit/07-risk-register.csv +0 -10
  118. package/audit/08-remediation-roadmap.md +0 -177
  119. package/audit/09-signoff-check.md +0 -23
  120. package/audit/10-final-synthesis.md +0 -29
  121. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  122. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  123. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  157. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  158. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  159. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  160. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  161. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  162. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  163. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  164. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  165. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  166. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  167. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  168. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  169. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  170. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  171. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  172. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  173. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  174. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  175. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  176. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  177. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  178. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  179. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  180. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  181. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  182. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  183. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  184. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  185. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  186. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  187. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  188. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  189. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  190. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  191. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  192. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  193. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  194. package/audit/evidence/code/error-model-signals.txt +0 -98
  195. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  196. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  197. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  198. package/audit/evidence/code/idproperty-signals.txt +0 -157
  199. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  200. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  201. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  202. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  203. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  204. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  205. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  206. package/audit/evidence/code/permission-signals.txt +0 -193
  207. package/audit/evidence/code/query-include-signals.txt +0 -687
  208. package/audit/evidence/code/transaction-signals.txt +0 -28
  209. package/audit/evidence/code/validation-signals.txt +0 -650
  210. package/audit/evidence/code/write-path-signals.txt +0 -61
  211. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  212. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  213. package/audit/evidence/external/corpus-freeze.md +0 -21
  214. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  215. package/audit/evidence/external/npm-download-signals.txt +0 -23
  216. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  217. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  218. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  219. package/audit/evidence/external/readme-fortune.md +0 -165
  220. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  221. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  222. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  223. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  224. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  225. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  226. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  227. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  228. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  229. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  230. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  231. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  232. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  233. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  234. package/audit/evidence/external/standards-sources.txt +0 -6
  235. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  236. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  237. package/audit/evidence/runtime/environment.txt +0 -4
  238. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  239. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  240. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  241. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  242. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  243. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  244. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  245. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  246. package/audit/evidence/runtime/test-summary.md +0 -107
  247. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  248. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  249. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  250. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  251. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  252. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  253. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  254. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  255. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  256. package/audit/evidence/tests/skipped-tests.txt +0 -10
  257. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  258. package/audit/evidence/tests/test-suite-list.txt +0 -32
  259. package/audit/subsystems/docs-drift.md +0 -49
  260. package/audit/subsystems/pass2-cross-review.md +0 -131
  261. package/audit/subsystems/plugins-access.md +0 -51
  262. package/audit/subsystems/plugins-bulk.md +0 -50
  263. package/audit/subsystems/plugins-cors.md +0 -48
  264. package/audit/subsystems/plugins-file.md +0 -53
  265. package/audit/subsystems/plugins-multihome.md +0 -50
  266. package/audit/subsystems/plugins-positioning.md +0 -54
  267. package/audit/subsystems/plugins-socketio.md +0 -50
  268. package/audit/subsystems/query-engine.md +0 -61
  269. package/audit/subsystems/relationships.md +0 -55
  270. package/audit/subsystems/rest-orchestration.md +0 -66
  271. package/audit/subsystems/schema-core.md +0 -64
  272. package/audit/subsystems/storage-knex.md +0 -57
  273. package/audit/subsystems/tests.md +0 -48
  274. package/audit/subsystems/transport-express.md +0 -55
  275. package/audit/traces/trace-read-get.md +0 -22
  276. package/audit/traces/trace-read-query.md +0 -25
  277. package/audit/traces/trace-relationship-endpoints.md +0 -31
  278. package/audit/traces/trace-write-delete.md +0 -17
  279. package/audit/traces/trace-write-patch.md +0 -26
  280. package/audit/traces/trace-write-post.md +0 -22
  281. package/audit/traces/trace-write-put.md +0 -23
  282. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  283. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  284. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  285. package/plugins/core/rest-api-access.js +0 -459
  286. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  287. package/tests/multihome.test.js +0 -553
  288. package/tests/multihome.test.js.bak +0 -550
  289. package/tests/ownership-optout.test.js +0 -76
  290. package/tests/rest-api-access.test.js +0 -206
@@ -1,19 +1,475 @@
1
1
  /**
2
- * @file Creates Knex table definitions from json-rest-schema definitions
2
+ * @file Creates Knex table definitions and migration plans from json-rest-schema definitions
3
3
  */
4
4
 
5
- /**
6
- * Maps json-rest-schema types to Knex column types
7
- * @param {object} table - The Knex table builder instance
8
- * @param {string} columnName - The name of the column
9
- * @param {object} definition - The schema definition for the field
10
- * @returns {object} The Knex column builder instance
11
- */
12
- function mapTypeToKnex (table, columnName, definition) {
13
- const { type, precision, scale, maxLength, unsigned } = definition
5
+ import { buildStorageInfo } from './storage/storage-mapping.js'
6
+
7
+ const MYSQL_DIALECT_PATTERN = /mysql/i
8
+
9
+ function normalizeText (value) {
10
+ if (value == null) return ''
11
+ return String(value).trim()
12
+ }
13
+
14
+ function quoteJsString (value) {
15
+ const escaped = String(value)
16
+ .replace(/\\/g, '\\\\')
17
+ .replace(/'/g, "\\'")
18
+ .replace(/\r/g, '\\r')
19
+ .replace(/\n/g, '\\n')
20
+
21
+ return `'${escaped}'`
22
+ }
23
+
24
+ function formatArrayLiteral (values = []) {
25
+ return `[${values.map((value) => quoteJsString(value)).join(', ')}]`
26
+ }
27
+
28
+ function formatPlainObjectLiteral (value) {
29
+ const entries = Object.entries(value).map(([key, entryValue]) => {
30
+ return `${JSON.stringify(key)}: ${formatCodeLiteral(entryValue)}`
31
+ })
32
+
33
+ return `{ ${entries.join(', ')} }`
34
+ }
35
+
36
+ function formatCodeLiteral (value) {
37
+ if (value === undefined) {
38
+ throw new Error('Cannot generate a migration literal from undefined.')
39
+ }
40
+
41
+ if (typeof value === 'function') {
42
+ throw new Error('Cannot generate deterministic migrations for function defaults.')
43
+ }
44
+
45
+ if (typeof value === 'string') {
46
+ return quoteJsString(value)
47
+ }
48
+
49
+ if (typeof value === 'number' || typeof value === 'bigint') {
50
+ return String(value)
51
+ }
52
+
53
+ if (typeof value === 'boolean') {
54
+ return value ? 'true' : 'false'
55
+ }
56
+
57
+ if (value === null) {
58
+ return 'null'
59
+ }
60
+
61
+ if (Array.isArray(value)) {
62
+ return `[${value.map((entry) => formatCodeLiteral(entry)).join(', ')}]`
63
+ }
64
+
65
+ if (typeof value === 'object') {
66
+ return formatPlainObjectLiteral(value)
67
+ }
68
+
69
+ return JSON.stringify(value)
70
+ }
71
+
72
+ function detectKnexDialect (knex) {
73
+ return normalizeText(
74
+ knex?.client?.config?.client ||
75
+ knex?.client?.dialect ||
76
+ knex?.client?.driverName
77
+ ).toLowerCase()
78
+ }
79
+
80
+ function isMysqlDialect (dialect = '') {
81
+ return MYSQL_DIALECT_PATTERN.test(String(dialect || ''))
82
+ }
83
+
84
+ function ensureStringArray (value, label) {
85
+ const values = Array.isArray(value) ? value : [value]
86
+ const normalized = values
87
+ .map((entry) => normalizeText(entry))
88
+ .filter(Boolean)
89
+
90
+ if (normalized.length < 1) {
91
+ throw new Error(`${label} must define at least one column.`)
92
+ }
93
+
94
+ return normalized
95
+ }
96
+
97
+ function buildSetColumnType (values = []) {
98
+ const normalized = values.map((value) => normalizeText(value)).filter(Boolean)
99
+ if (normalized.length < 1) {
100
+ throw new Error('setValues must contain at least one value.')
101
+ }
102
+
103
+ return `set(${normalized.map((value) => quoteJsString(value)).join(', ')})`
104
+ }
105
+
106
+ function createTableBuilderContext (schemaStructure = {}, idColumn = 'id', storage = undefined) {
107
+ return {
108
+ schemaStructure,
109
+ idProperty: idColumn,
110
+ storageInfo: buildStorageInfo({
111
+ schemaStructure,
112
+ idProperty: idColumn,
113
+ storage
114
+ })
115
+ }
116
+ }
117
+
118
+ function resolveFieldOrColumnName (tableContext, fieldOrColumn) {
119
+ const normalized = normalizeText(fieldOrColumn)
120
+ if (!normalized) return ''
121
+
122
+ if (normalized === 'id') {
123
+ return tableContext.storageInfo.idColumn
124
+ }
125
+
126
+ return tableContext.storageInfo.fields[normalized]?.column || normalized
127
+ }
128
+
129
+ function getColumnNameForDefinition (tableContext, fieldName) {
130
+ return resolveFieldOrColumnName(tableContext, fieldName)
131
+ }
132
+
133
+ function normalizeTopLevelIndexes (schemaLike, tableName, tableContext) {
134
+ const indexes = Array.isArray(schemaLike?.indexes) ? schemaLike.indexes : []
14
135
 
15
- switch (type) {
136
+ return indexes.map((indexDef, indexPosition) => {
137
+ if (!indexDef || typeof indexDef !== 'object' || Array.isArray(indexDef)) {
138
+ throw new Error(`Index metadata for table '${tableName}' must be an object.`)
139
+ }
140
+
141
+ const columns = ensureStringArray(indexDef.columns, `Index ${indexPosition + 1}`)
142
+ .map((column) => resolveFieldOrColumnName(tableContext, column))
143
+ const unique = indexDef.unique === true
144
+ const indexType = normalizeText(indexDef.indexType).toUpperCase()
145
+ const name = normalizeText(indexDef.name) || `${unique ? 'uq' : 'idx'}_${tableName}_${columns.join('_')}`
146
+
147
+ if (unique && indexType) {
148
+ throw new Error(`Unique index '${name}' on table '${tableName}' cannot define indexType.`)
149
+ }
150
+
151
+ return {
152
+ name,
153
+ unique,
154
+ columns,
155
+ indexType
156
+ }
157
+ })
158
+ }
159
+
160
+ function normalizeFieldIndexes (schemaStructure, tableName, tableContext) {
161
+ const indexes = []
162
+
163
+ for (const [fieldName, definition] of Object.entries(schemaStructure)) {
164
+ const columnName = getColumnNameForDefinition(tableContext, fieldName)
165
+
166
+ if (definition.unique === true) {
167
+ indexes.push({
168
+ name: `uq_${tableName}_${columnName}`,
169
+ unique: true,
170
+ columns: [columnName],
171
+ indexType: ''
172
+ })
173
+ }
174
+
175
+ if (definition.index === true) {
176
+ indexes.push({
177
+ name: `idx_${tableName}_${columnName}`,
178
+ unique: false,
179
+ columns: [columnName],
180
+ indexType: ''
181
+ })
182
+ }
183
+ }
184
+
185
+ return indexes
186
+ }
187
+
188
+ function normalizeTopLevelForeignKeys (schemaLike, tableName, tableContext) {
189
+ const foreignKeys = Array.isArray(schemaLike?.foreignKeys) ? schemaLike.foreignKeys : []
190
+
191
+ return foreignKeys.map((foreignKeyDef, foreignKeyPosition) => {
192
+ if (!foreignKeyDef || typeof foreignKeyDef !== 'object' || Array.isArray(foreignKeyDef)) {
193
+ throw new Error(`Foreign key metadata for table '${tableName}' must be an object.`)
194
+ }
195
+
196
+ const columns = ensureStringArray(foreignKeyDef.columns, `Foreign key ${foreignKeyPosition + 1}`)
197
+ .map((column) => resolveFieldOrColumnName(tableContext, column))
198
+ const referencedTableName = normalizeText(
199
+ foreignKeyDef.referencedTableName ||
200
+ foreignKeyDef.table ||
201
+ foreignKeyDef.references?.table
202
+ )
203
+
204
+ if (!referencedTableName) {
205
+ throw new Error(`Foreign key ${foreignKeyPosition + 1} on table '${tableName}' requires referencedTableName.`)
206
+ }
207
+
208
+ const referencedColumns = ensureStringArray(
209
+ foreignKeyDef.referencedColumns ||
210
+ foreignKeyDef.references?.columns ||
211
+ foreignKeyDef.references?.column ||
212
+ 'id',
213
+ `Foreign key ${foreignKeyPosition + 1}`
214
+ )
215
+ const name = normalizeText(foreignKeyDef.name) || `${tableName}_${columns.join('_')}_foreign`
216
+
217
+ return {
218
+ name,
219
+ columns,
220
+ referencedTableName,
221
+ referencedColumns,
222
+ deleteRule: normalizeText(foreignKeyDef.deleteRule || foreignKeyDef.onDelete).toUpperCase(),
223
+ updateRule: normalizeText(foreignKeyDef.updateRule || foreignKeyDef.onUpdate).toUpperCase()
224
+ }
225
+ })
226
+ }
227
+
228
+ function normalizeFieldForeignKeys (schemaStructure, tableName, tableContext) {
229
+ const foreignKeys = []
230
+
231
+ for (const [fieldName, definition] of Object.entries(schemaStructure)) {
232
+ if (!definition.references) continue
233
+
234
+ const columnName = getColumnNameForDefinition(tableContext, fieldName)
235
+ foreignKeys.push({
236
+ name: normalizeText(definition.references.name) || `${tableName}_${columnName}_foreign`,
237
+ columns: [columnName],
238
+ referencedTableName: normalizeText(definition.references.table),
239
+ referencedColumns: [normalizeText(definition.references.column || 'id')],
240
+ deleteRule: normalizeText(definition.references.onDelete).toUpperCase(),
241
+ updateRule: normalizeText(definition.references.onUpdate).toUpperCase()
242
+ })
243
+ }
244
+
245
+ return foreignKeys
246
+ }
247
+
248
+ function normalizeCheckConstraints (schemaLike, tableName) {
249
+ const constraints = Array.isArray(schemaLike?.checkConstraints) ? schemaLike.checkConstraints : []
250
+
251
+ return constraints.map((constraintDef, constraintIndex) => {
252
+ if (!constraintDef || typeof constraintDef !== 'object' || Array.isArray(constraintDef)) {
253
+ throw new Error(`Check constraint metadata for table '${tableName}' must be an object.`)
254
+ }
255
+
256
+ const clause = normalizeText(constraintDef.clause)
257
+ if (!clause) {
258
+ throw new Error(`Check constraint ${constraintIndex + 1} on table '${tableName}' requires a clause.`)
259
+ }
260
+
261
+ return {
262
+ name: normalizeText(constraintDef.name) || `${tableName}_check_${constraintIndex + 1}`,
263
+ clause
264
+ }
265
+ })
266
+ }
267
+
268
+ function dedupeBySignature (entries, signatureFn, sortFn) {
269
+ const deduped = new Map()
270
+
271
+ for (const entry of entries) {
272
+ deduped.set(signatureFn(entry), entry)
273
+ }
274
+
275
+ return [...deduped.values()].sort(sortFn)
276
+ }
277
+
278
+ function compareByName (left, right) {
279
+ return left.name.localeCompare(right.name)
280
+ }
281
+
282
+ function normalizeColumnShape (definition = {}) {
283
+ if (Array.isArray(definition.enum) && definition.enum.length > 0) {
284
+ return 'enum'
285
+ }
286
+ if (Array.isArray(definition.setValues) && definition.setValues.length > 0) {
287
+ return 'set'
288
+ }
289
+
290
+ switch (definition.type) {
291
+ case 'id':
292
+ return 'integer'
293
+ case 'number':
294
+ return definition.precision !== undefined && definition.scale !== undefined ? 'decimal' : 'float'
295
+ case 'boolean':
296
+ return 'boolean'
297
+ case 'date':
298
+ return 'date'
299
+ case 'dateTime':
300
+ return 'datetime'
301
+ case 'time':
302
+ return 'time'
303
+ case 'timestamp':
304
+ return 'integer'
305
+ case 'array':
306
+ case 'object':
307
+ case 'serialize':
308
+ return 'json'
309
+ case 'blob':
310
+ case 'file':
311
+ return 'binary'
312
+ case 'string':
313
+ case 'none':
314
+ default:
315
+ return 'string'
316
+ }
317
+ }
318
+
319
+ function normalizeDesiredTypeKind (definition = {}) {
320
+ if (Array.isArray(definition.enum) && definition.enum.length > 0) {
321
+ return 'string'
322
+ }
323
+ if (Array.isArray(definition.setValues) && definition.setValues.length > 0) {
324
+ return 'string'
325
+ }
326
+
327
+ switch (definition.type) {
328
+ case 'id':
329
+ return 'integer'
330
+ case 'number':
331
+ return 'number'
332
+ case 'boolean':
333
+ return 'boolean'
334
+ case 'date':
335
+ return 'date'
336
+ case 'dateTime':
337
+ return 'datetime'
338
+ case 'time':
339
+ return 'time'
340
+ case 'array':
341
+ case 'object':
342
+ case 'serialize':
343
+ return 'json'
344
+ case 'blob':
345
+ case 'file':
346
+ return 'binary'
347
+ case 'timestamp':
348
+ return 'integer'
16
349
  case 'string':
350
+ case 'none':
351
+ default:
352
+ return 'string'
353
+ }
354
+ }
355
+
356
+ function normalizeComparableDefault (value) {
357
+ if (value === undefined) return undefined
358
+ if (typeof value === 'boolean') return value ? '1' : '0'
359
+ if (value === null) return null
360
+ if (typeof value === 'object') return JSON.stringify(value)
361
+ return String(value)
362
+ }
363
+
364
+ function normalizeDesiredColumn (tableContext, fieldName, definition, options = {}) {
365
+ const nullable = definition.nullable === true
366
+ ? true
367
+ : !(definition.required === true || definition.nullable === false)
368
+ const shape = normalizeColumnShape(definition)
369
+
370
+ return {
371
+ fieldName,
372
+ name: getColumnNameForDefinition(tableContext, fieldName),
373
+ definition,
374
+ shape,
375
+ typeKind: normalizeDesiredTypeKind(definition),
376
+ nullable,
377
+ required: definition.required === true,
378
+ hasDefault: definition.defaultTo !== undefined,
379
+ defaultValue: normalizeComparableDefault(definition.defaultTo),
380
+ unsigned: definition.type === 'id' ? definition.unsigned !== false : definition.unsigned === true,
381
+ maxLength: definition.maxLength ?? null,
382
+ numericPrecision: definition.precision ?? null,
383
+ numericScale: definition.scale ?? null,
384
+ datetimePrecision: definition.temporalPrecision ?? null,
385
+ enumValues: Array.isArray(definition.enum) ? definition.enum.map((value) => String(value)) : [],
386
+ setValues: Array.isArray(definition.setValues) ? definition.setValues.map((value) => String(value)) : [],
387
+ autoIncrement: options.autoIncrement === true
388
+ }
389
+ }
390
+
391
+ function buildImplicitIdDefinition (idColumn) {
392
+ return {
393
+ type: 'id',
394
+ primary: true,
395
+ storage: {
396
+ column: idColumn
397
+ }
398
+ }
399
+ }
400
+
401
+ function resolveTableSchemaContext (schemaLike, options = {}) {
402
+ const schemaStructure = schemaLike?.structure || schemaLike || {}
403
+ const configuredIdColumn = options.idProperty || schemaStructure.id?.storage?.column || 'id'
404
+ const storage = options.storage ?? schemaLike?.storage
405
+ const tableContext = createTableBuilderContext(schemaStructure, configuredIdColumn, storage)
406
+
407
+ let resourceIdField = null
408
+ for (const [fieldName, definition] of Object.entries(schemaStructure)) {
409
+ if (definition?.type !== 'id') continue
410
+ const columnName = getColumnNameForDefinition(tableContext, fieldName)
411
+ if (columnName === configuredIdColumn) {
412
+ resourceIdField = fieldName
413
+ break
414
+ }
415
+ }
416
+
417
+ const implicitIndexes = normalizeFieldIndexes(schemaStructure, options.tableName || 'table', tableContext)
418
+ const explicitIndexes = normalizeTopLevelIndexes(schemaLike, options.tableName || 'table', tableContext)
419
+ const implicitForeignKeys = normalizeFieldForeignKeys(schemaStructure, options.tableName || 'table', tableContext)
420
+ const explicitForeignKeys = normalizeTopLevelForeignKeys(schemaLike, options.tableName || 'table', tableContext)
421
+ const checkConstraints = normalizeCheckConstraints(schemaLike, options.tableName || 'table')
422
+
423
+ return {
424
+ schemaStructure,
425
+ idColumn: configuredIdColumn,
426
+ resourceIdField,
427
+ hasPrimaryIdField: resourceIdField
428
+ ? schemaStructure[resourceIdField]?.primary === true
429
+ : false,
430
+ tableContext,
431
+ indexes: dedupeBySignature(
432
+ [...implicitIndexes, ...explicitIndexes],
433
+ (entry) => `${entry.name}|${entry.unique ? 'unique' : 'index'}|${entry.columns.join(',')}`,
434
+ compareByName
435
+ ),
436
+ foreignKeys: dedupeBySignature(
437
+ [...implicitForeignKeys, ...explicitForeignKeys],
438
+ (entry) => `${entry.name}|${entry.columns.join(',')}|${entry.referencedTableName}|${entry.referencedColumns.join(',')}`,
439
+ compareByName
440
+ ),
441
+ checkConstraints: dedupeBySignature(
442
+ checkConstraints,
443
+ (entry) => `${entry.name}|${entry.clause}`,
444
+ compareByName
445
+ )
446
+ }
447
+ }
448
+
449
+ function mapTypeToKnex (table, columnName, definition, options = {}) {
450
+ const {
451
+ precision,
452
+ scale,
453
+ maxLength,
454
+ unsigned,
455
+ temporalPrecision
456
+ } = definition
457
+ const enumValues = Array.isArray(definition.enum) ? definition.enum : []
458
+ const setValues = Array.isArray(definition.setValues) ? definition.setValues : []
459
+ const dialect = normalizeText(options.dialect).toLowerCase()
460
+
461
+ if (setValues.length > 0) {
462
+ if (!isMysqlDialect(dialect)) {
463
+ throw new Error(`Field '${columnName}' uses setValues, which requires a MySQL-compatible knex client.`)
464
+ }
465
+ return table.specificType(columnName, buildSetColumnType(setValues))
466
+ }
467
+
468
+ switch (definition.type) {
469
+ case 'string':
470
+ if (enumValues.length > 0) {
471
+ return table.enu(columnName, enumValues)
472
+ }
17
473
  return maxLength ? table.string(columnName, maxLength) : table.string(columnName)
18
474
 
19
475
  case 'number':
@@ -22,9 +478,10 @@ function mapTypeToKnex (table, columnName, definition) {
22
478
  }
23
479
  return table.float(columnName)
24
480
 
25
- case 'id':
481
+ case 'id': {
26
482
  const col = table.integer(columnName)
27
483
  return unsigned !== false ? col.unsigned() : col
484
+ }
28
485
 
29
486
  case 'boolean':
30
487
  return table.boolean(columnName)
@@ -33,10 +490,14 @@ function mapTypeToKnex (table, columnName, definition) {
33
490
  return table.date(columnName)
34
491
 
35
492
  case 'dateTime':
36
- return table.datetime(columnName)
493
+ return temporalPrecision != null
494
+ ? table.datetime(columnName, { precision: temporalPrecision })
495
+ : table.datetime(columnName)
37
496
 
38
497
  case 'time':
39
- return table.time(columnName)
498
+ return temporalPrecision != null
499
+ ? table.time(columnName, { precision: temporalPrecision })
500
+ : table.time(columnName)
40
501
 
41
502
  case 'timestamp':
42
503
  return table.integer(columnName)
@@ -56,61 +517,368 @@ function mapTypeToKnex (table, columnName, definition) {
56
517
  }
57
518
  }
58
519
 
59
- /**
60
- * Applies schema constraints to a Knex column
61
- * @param {object} column - The Knex column builder instance
62
- * @param {object} definition - The schema definition for the field
63
- */
64
- function applyConstraints (column, definition) {
65
- // Nullability
520
+ function applyColumnConstraints (column, definition) {
66
521
  if (definition.nullable === true) {
67
522
  column.nullable()
68
523
  } else if (definition.required === true || definition.nullable === false) {
69
524
  column.notNullable()
70
525
  }
71
526
 
72
- // Default value
73
527
  if (definition.defaultTo !== undefined) {
74
528
  column.defaultTo(definition.defaultTo)
75
529
  }
76
530
 
77
- // Database-specific constraints
78
- if (definition.unique === true) {
79
- column.unique()
80
- }
81
-
82
531
  if (definition.primary === true) {
83
532
  column.primary()
84
533
  }
85
534
 
86
- if (definition.index === true) {
87
- column.index()
535
+ if (definition.comment) {
536
+ column.comment(definition.comment)
537
+ }
538
+ }
539
+
540
+ function applyTableMetadata (table, tableSchemaContext) {
541
+ for (const index of tableSchemaContext.indexes) {
542
+ if (index.unique) {
543
+ table.unique(index.columns, index.name)
544
+ continue
545
+ }
546
+
547
+ const indexOptions = index.indexType
548
+ ? { indexType: index.indexType }
549
+ : undefined
550
+ table.index(index.columns, index.name, indexOptions)
88
551
  }
89
552
 
90
- if (definition.references) {
91
- column.references(definition.references.column || 'id')
92
- .inTable(definition.references.table)
553
+ for (const foreignKey of tableSchemaContext.foreignKeys) {
554
+ const reference = table.foreign(foreignKey.columns, foreignKey.name)
555
+ .references(foreignKey.referencedColumns)
556
+ .inTable(foreignKey.referencedTableName)
93
557
 
94
- if (definition.references.onDelete) {
95
- column.onDelete(definition.references.onDelete)
558
+ if (foreignKey.deleteRule) {
559
+ reference.onDelete(foreignKey.deleteRule)
96
560
  }
97
561
 
98
- if (definition.references.onUpdate) {
99
- column.onUpdate(definition.references.onUpdate)
562
+ if (foreignKey.updateRule) {
563
+ reference.onUpdate(foreignKey.updateRule)
100
564
  }
101
565
  }
102
566
 
567
+ for (const constraint of tableSchemaContext.checkConstraints) {
568
+ table.check(constraint.clause, [], constraint.name)
569
+ }
570
+ }
571
+
572
+ function buildColumnBuilderCode (columnName, definition, options = {}) {
573
+ const dialect = normalizeText(options.dialect).toLowerCase()
574
+ const enumValues = Array.isArray(definition.enum) ? definition.enum : []
575
+ const setValues = Array.isArray(definition.setValues) ? definition.setValues : []
576
+ let line = ''
577
+
578
+ if (setValues.length > 0) {
579
+ if (dialect && !isMysqlDialect(dialect)) {
580
+ throw new Error(`Field '${columnName}' uses setValues, which requires a MySQL-compatible migration target.`)
581
+ }
582
+ line = `table.specificType(${quoteJsString(columnName)}, ${quoteJsString(buildSetColumnType(setValues))})`
583
+ } else {
584
+ switch (definition.type) {
585
+ case 'string':
586
+ if (enumValues.length > 0) {
587
+ line = `table.enu(${quoteJsString(columnName)}, ${formatArrayLiteral(enumValues)})`
588
+ } else {
589
+ line = definition.maxLength
590
+ ? `table.string(${quoteJsString(columnName)}, ${definition.maxLength})`
591
+ : `table.string(${quoteJsString(columnName)})`
592
+ }
593
+ break
594
+
595
+ case 'number':
596
+ if (definition.precision !== undefined && definition.scale !== undefined) {
597
+ line = `table.decimal(${quoteJsString(columnName)}, ${definition.precision}, ${definition.scale})`
598
+ } else {
599
+ line = `table.float(${quoteJsString(columnName)})`
600
+ }
601
+ break
602
+
603
+ case 'id':
604
+ line = `table.integer(${quoteJsString(columnName)})`
605
+ if (definition.unsigned !== false) {
606
+ line += '.unsigned()'
607
+ }
608
+ break
609
+
610
+ case 'boolean':
611
+ line = `table.boolean(${quoteJsString(columnName)})`
612
+ break
613
+
614
+ case 'date':
615
+ line = `table.date(${quoteJsString(columnName)})`
616
+ break
617
+
618
+ case 'dateTime':
619
+ if (definition.temporalPrecision != null) {
620
+ line = `table.datetime(${quoteJsString(columnName)}, { precision: ${definition.temporalPrecision} })`
621
+ } else {
622
+ line = `table.datetime(${quoteJsString(columnName)})`
623
+ }
624
+ break
625
+
626
+ case 'time':
627
+ if (definition.temporalPrecision != null) {
628
+ line = `table.time(${quoteJsString(columnName)}, { precision: ${definition.temporalPrecision} })`
629
+ } else {
630
+ line = `table.time(${quoteJsString(columnName)})`
631
+ }
632
+ break
633
+
634
+ case 'timestamp':
635
+ line = `table.integer(${quoteJsString(columnName)})`
636
+ break
637
+
638
+ case 'array':
639
+ case 'object':
640
+ case 'serialize':
641
+ line = `table.json(${quoteJsString(columnName)})`
642
+ break
643
+
644
+ case 'blob':
645
+ case 'file':
646
+ line = `table.binary(${quoteJsString(columnName)})`
647
+ break
648
+
649
+ case 'none':
650
+ default:
651
+ line = `table.string(${quoteJsString(columnName)})`
652
+ }
653
+ }
654
+
655
+ if (definition.nullable === true) {
656
+ line += '.nullable()'
657
+ } else if (definition.required === true || definition.nullable === false) {
658
+ line += '.notNullable()'
659
+ }
660
+
661
+ if (definition.defaultTo !== undefined) {
662
+ line += `.defaultTo(${formatCodeLiteral(definition.defaultTo)})`
663
+ }
664
+
665
+ if (definition.primary === true && options.includePrimary !== false) {
666
+ line += '.primary()'
667
+ }
668
+
103
669
  if (definition.comment) {
104
- column.comment(definition.comment)
670
+ line += `.comment(${quoteJsString(definition.comment)})`
671
+ }
672
+
673
+ if (options.alter === true) {
674
+ line += '.alter()'
105
675
  }
676
+
677
+ return line
678
+ }
679
+
680
+ function buildIndexLine (index) {
681
+ if (index.unique) {
682
+ return `table.unique(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)})`
683
+ }
684
+
685
+ if (index.indexType) {
686
+ return `table.index(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)}, ${formatCodeLiteral({ indexType: index.indexType })})`
687
+ }
688
+
689
+ return `table.index(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)})`
690
+ }
691
+
692
+ function buildForeignKeyLine (foreignKey) {
693
+ let line = `table.foreign(${formatCodeLiteral(foreignKey.columns)}, ${quoteJsString(foreignKey.name)})`
694
+ line += `.references(${formatCodeLiteral(foreignKey.referencedColumns)})`
695
+ line += `.inTable(${quoteJsString(foreignKey.referencedTableName)})`
696
+
697
+ if (foreignKey.deleteRule) {
698
+ line += `.onDelete(${quoteJsString(foreignKey.deleteRule)})`
699
+ }
700
+
701
+ if (foreignKey.updateRule) {
702
+ line += `.onUpdate(${quoteJsString(foreignKey.updateRule)})`
703
+ }
704
+
705
+ return line
706
+ }
707
+
708
+ function buildCheckConstraintLine (constraint) {
709
+ return `table.check(${quoteJsString(constraint.clause)}, [], ${quoteJsString(constraint.name)})`
710
+ }
711
+
712
+ function buildDesiredColumnsMap (tableSchemaContext, options = {}) {
713
+ const desiredColumns = new Map()
714
+
715
+ if (!tableSchemaContext.hasPrimaryIdField && options.autoIncrement === true) {
716
+ const implicitIdDefinition = buildImplicitIdDefinition(tableSchemaContext.idColumn)
717
+ desiredColumns.set(
718
+ tableSchemaContext.idColumn,
719
+ normalizeDesiredColumn(
720
+ createTableBuilderContext({ id: implicitIdDefinition }, tableSchemaContext.idColumn),
721
+ 'id',
722
+ implicitIdDefinition,
723
+ { autoIncrement: true }
724
+ )
725
+ )
726
+ }
727
+
728
+ for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
729
+ if (fieldName === tableSchemaContext.resourceIdField && !tableSchemaContext.hasPrimaryIdField && options.autoIncrement === true) {
730
+ continue
731
+ }
732
+
733
+ desiredColumns.set(
734
+ getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName),
735
+ normalizeDesiredColumn(tableSchemaContext.tableContext, fieldName, definition)
736
+ )
737
+ }
738
+
739
+ return desiredColumns
740
+ }
741
+
742
+ function valuesEqual (left, right) {
743
+ return JSON.stringify(left) === JSON.stringify(right)
744
+ }
745
+
746
+ function normalizeSnapshotDefaultValue (column = {}) {
747
+ if (!column.hasDefault) return undefined
748
+ return normalizeComparableDefault(column.defaultValue)
749
+ }
750
+
751
+ function collectDestructiveColumnWarnings (currentColumn, desiredColumn) {
752
+ const warnings = []
753
+ const columnName = desiredColumn.name
754
+
755
+ if (currentColumn.nullable === true && desiredColumn.nullable === false) {
756
+ warnings.push(`Column '${columnName}' changes from nullable to not-null and may fail on existing rows.`)
757
+ }
758
+
759
+ if (
760
+ currentColumn.maxLength != null &&
761
+ desiredColumn.maxLength != null &&
762
+ desiredColumn.maxLength < currentColumn.maxLength
763
+ ) {
764
+ warnings.push(`Column '${columnName}' reduces maxLength from ${currentColumn.maxLength} to ${desiredColumn.maxLength}.`)
765
+ }
766
+
767
+ if (
768
+ currentColumn.numericPrecision != null &&
769
+ desiredColumn.numericPrecision != null &&
770
+ desiredColumn.numericPrecision < currentColumn.numericPrecision
771
+ ) {
772
+ warnings.push(`Column '${columnName}' reduces numeric precision from ${currentColumn.numericPrecision} to ${desiredColumn.numericPrecision}.`)
773
+ }
774
+
775
+ if (
776
+ currentColumn.numericScale != null &&
777
+ desiredColumn.numericScale != null &&
778
+ desiredColumn.numericScale < currentColumn.numericScale
779
+ ) {
780
+ warnings.push(`Column '${columnName}' reduces numeric scale from ${currentColumn.numericScale} to ${desiredColumn.numericScale}.`)
781
+ }
782
+
783
+ if (!valuesEqual(currentColumn.enumValues || [], desiredColumn.enumValues || [])) {
784
+ warnings.push(`Column '${columnName}' changes enum values and may invalidate existing data.`)
785
+ }
786
+
787
+ if (!valuesEqual(currentColumn.setValues || [], desiredColumn.setValues || [])) {
788
+ warnings.push(`Column '${columnName}' changes setValues and may invalidate existing data.`)
789
+ }
790
+
791
+ if (normalizeText(currentColumn.typeKind) !== normalizeText(desiredColumn.typeKind)) {
792
+ warnings.push(`Column '${columnName}' changes type semantics from '${currentColumn.typeKind}' to '${desiredColumn.typeKind}'.`)
793
+ }
794
+
795
+ return warnings
796
+ }
797
+
798
+ function columnNeedsAlter (currentColumn, desiredColumn, options = {}) {
799
+ const currentNumericShape = (() => {
800
+ const dataType = normalizeText(currentColumn.dataType)
801
+ if (dataType === 'decimal' || dataType === 'numeric') return 'decimal'
802
+ if (dataType === 'float' || dataType === 'double' || dataType === 'real') return 'float'
803
+ return ''
804
+ })()
805
+ const compareUnsigned = isMysqlDialect(normalizeText(options.dialect).toLowerCase())
806
+
807
+ return (
808
+ normalizeText(currentColumn.typeKind) !== normalizeText(desiredColumn.typeKind) ||
809
+ (desiredColumn.typeKind === 'number' && currentNumericShape && currentNumericShape !== normalizeText(desiredColumn.shape)) ||
810
+ Boolean(currentColumn.nullable) !== Boolean(desiredColumn.nullable) ||
811
+ (compareUnsigned && Boolean(currentColumn.unsigned) !== Boolean(desiredColumn.unsigned)) ||
812
+ (currentColumn.maxLength ?? null) !== (desiredColumn.maxLength ?? null) ||
813
+ (currentColumn.numericPrecision ?? null) !== (desiredColumn.numericPrecision ?? null) ||
814
+ (currentColumn.numericScale ?? null) !== (desiredColumn.numericScale ?? null) ||
815
+ normalizeSnapshotDefaultValue(currentColumn) !== desiredColumn.defaultValue ||
816
+ Boolean(currentColumn.hasDefault) !== Boolean(desiredColumn.hasDefault) ||
817
+ !valuesEqual(currentColumn.enumValues || [], desiredColumn.enumValues || []) ||
818
+ !valuesEqual(currentColumn.setValues || [], desiredColumn.setValues || [])
819
+ )
820
+ }
821
+
822
+ function mapByName (entries = []) {
823
+ return new Map((entries || []).map((entry) => [entry.name, entry]))
824
+ }
825
+
826
+ function createEmptyDiffPlan () {
827
+ return {
828
+ addColumns: [],
829
+ alterColumns: [],
830
+ dropColumns: [],
831
+ addIndexes: [],
832
+ dropIndexes: [],
833
+ addForeignKeys: [],
834
+ dropForeignKeys: [],
835
+ addCheckConstraints: [],
836
+ warnings: []
837
+ }
838
+ }
839
+
840
+ function assertNoTopLevelTableMetadata (schemaLike, helperName) {
841
+ const metadataKeys = ['indexes', 'foreignKeys', 'checkConstraints']
842
+
843
+ for (const key of metadataKeys) {
844
+ if (Array.isArray(schemaLike?.[key]) && schemaLike[key].length > 0) {
845
+ throw new Error(`${helperName} does not accept top-level ${key}. Use createKnexTable(), generateKnexMigration(), or generateKnexMigrationDiff() for table metadata.`)
846
+ }
847
+ }
848
+ }
849
+
850
+ function buildUnsupportedConstraintWarning (constraintType, name, dialect) {
851
+ return `Skipping ${constraintType} '${name}' because automatic ALTER support is not available for dialect '${dialect || 'unknown'}'.`
852
+ }
853
+
854
+ function isSqliteDialectName (dialect = '') {
855
+ return String(dialect || '').toLowerCase().includes('sqlite')
856
+ }
857
+
858
+ function normalizeConstraintClause (clause = '') {
859
+ return normalizeText(clause)
860
+ .replace(/[`"]/g, '')
861
+ .replace(/\s*\(\s*/g, '(')
862
+ .replace(/\s*\)\s*/g, ')')
863
+ .replace(/\s*,\s*/g, ',')
864
+ .replace(/\s+/g, ' ')
865
+ .toLowerCase()
866
+ }
867
+
868
+ function isInlineEnumCheckConstraint (currentCheck = {}, desiredColumn = {}) {
869
+ if (!desiredColumn.enumValues || desiredColumn.enumValues.length < 1) {
870
+ return false
871
+ }
872
+
873
+ const expectedClause = `${desiredColumn.name} in (${desiredColumn.enumValues.map((value) => quoteJsString(value)).join(', ')})`
874
+ return normalizeConstraintClause(currentCheck.clause) === normalizeConstraintClause(expectedClause)
106
875
  }
107
876
 
108
877
  /**
109
878
  * Creates a Knex table from a json-rest-schema definition
110
879
  * @param {object} knex - The Knex instance
111
- * @param {string} tableName - The name of the table to create
112
- * @param {object} schema - The json-rest-schema instance
113
- * @param {string} [idProperty='id'] - The name of the ID column
880
+ * @param {object} schemaInfo - Resource schema metadata
881
+ * @param {object} tableSchemaInstance - The json-rest-schema instance or schema-like table metadata object
114
882
  * @param {object} [options={}] - Additional options
115
883
  * @param {boolean} [options.autoIncrement=true] - Whether to use auto-incrementing IDs
116
884
  * @param {boolean} [options.timestamps=false] - Whether to add created_at/updated_at columns
@@ -118,62 +886,71 @@ function applyConstraints (column, definition) {
118
886
  */
119
887
  export async function createKnexTable (knex, schemaInfo, tableSchemaInstance, options = {}) {
120
888
  const { autoIncrement = true, timestamps = false } = options
121
-
122
889
  const tableName = schemaInfo.tableName
123
- const idProperty = schemaInfo.idProperty
124
- const tableSchemaStructure = tableSchemaInstance.structure
890
+ const dialect = detectKnexDialect(knex)
891
+ const tableSchemaContext = resolveTableSchemaContext(tableSchemaInstance, {
892
+ idProperty: schemaInfo.idProperty,
893
+ storage: schemaInfo.storage,
894
+ tableName
895
+ })
125
896
 
126
897
  return knex.schema.createTable(tableName, (table) => {
127
- // Check if schema has the idProperty field with primary key
128
- const hasIdField = tableSchemaStructure[idProperty] && tableSchemaStructure[idProperty].primary === true
129
-
130
- // Add auto-incrementing ID if no primary key is defined and autoIncrement is true
131
- if (!hasIdField && autoIncrement) {
132
- table.increments(idProperty).primary()
898
+ if (!tableSchemaContext.hasPrimaryIdField && autoIncrement) {
899
+ table.increments(tableSchemaContext.idColumn).primary()
133
900
  }
134
901
 
135
- // Process each field in the schema
136
- for (const [fieldName, definition] of Object.entries(tableSchemaStructure)) {
137
- // Skip if this is the ID field and we already handled it
138
- if (fieldName === idProperty && !hasIdField && autoIncrement) {
902
+ for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
903
+ if (fieldName === tableSchemaContext.resourceIdField && !tableSchemaContext.hasPrimaryIdField && autoIncrement) {
139
904
  continue
140
905
  }
141
906
 
142
- // Create the column with the appropriate type
143
- const column = mapTypeToKnex(table, fieldName, definition)
144
-
145
- // Apply constraints
146
- applyConstraints(column, definition)
907
+ const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
908
+ const column = mapTypeToKnex(table, columnName, definition, { dialect })
909
+ applyColumnConstraints(column, definition)
147
910
  }
148
911
 
149
- // Add timestamps if requested
912
+ applyTableMetadata(table, tableSchemaContext)
913
+
150
914
  if (timestamps) {
151
915
  table.timestamps(true, true)
152
916
  }
153
917
  })
154
918
  }
155
919
 
156
- export async function addKnexFields (knex, tableName, schema) {
920
+ export async function addKnexFields (knex, tableName, schema, options = {}) {
921
+ assertNoTopLevelTableMetadata(schema, 'addKnexFields')
922
+ const dialect = detectKnexDialect(knex)
923
+ const tableSchemaContext = resolveTableSchemaContext(schema, {
924
+ storage: options.storage,
925
+ ...options,
926
+ tableName
927
+ })
928
+
157
929
  return knex.schema.alterTable(tableName, (table) => {
158
- for (const [fieldName, definition] of Object.entries(schema.structure)) {
159
- const column = mapTypeToKnex(table, fieldName, definition)
160
- applyConstraints(column, definition)
930
+ for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
931
+ const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
932
+ const column = mapTypeToKnex(table, columnName, definition, { dialect })
933
+ applyColumnConstraints(column, definition)
161
934
  }
162
935
  })
163
936
  }
164
937
 
165
938
  // Helper function to alter multiple fields in an existing table
166
939
  export async function alterKnexFields (knex, tableName, fields, options = {}) {
167
- return knex.schema.alterTable(tableName, (table) => {
168
- for (const [fieldName, definition] of Object.entries(fields)) {
169
- // Create the column with alter flag
170
- const column = mapTypeToKnex(table, fieldName, definition)
940
+ assertNoTopLevelTableMetadata(fields, 'alterKnexFields')
941
+ const dialect = detectKnexDialect(knex)
942
+ const tableSchemaContext = resolveTableSchemaContext(fields, {
943
+ storage: options.storage,
944
+ ...options,
945
+ tableName
946
+ })
171
947
 
172
- // Mark this as an alteration
948
+ return knex.schema.alterTable(tableName, (table) => {
949
+ for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
950
+ const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
951
+ const column = mapTypeToKnex(table, columnName, definition, { dialect })
173
952
  column.alter()
174
-
175
- // Apply constraints (these will be reapplied with the alter)
176
- applyConstraints(column, definition)
953
+ applyColumnConstraints(column, definition)
177
954
  }
178
955
  })
179
956
  }
@@ -181,124 +958,298 @@ export async function alterKnexFields (knex, tableName, fields, options = {}) {
181
958
  /**
182
959
  * Generates a Knex migration string from a json-rest-schema definition
183
960
  * @param {string} tableName - The name of the table
184
- * @param {object} schema - The json-rest-schema instance
961
+ * @param {object} schema - The json-rest-schema instance or schema-like table metadata object
185
962
  * @param {object} [options={}] - Additional options
186
963
  * @returns {string} The migration code as a string
187
964
  */
188
965
  export function generateKnexMigration (tableName, schema, options = {}) {
189
- const { autoIncrement = true, timestamps = false } = options
966
+ const { autoIncrement = true, timestamps = false, dialect = '' } = options
967
+ const tableSchemaContext = resolveTableSchemaContext(schema, {
968
+ ...options,
969
+ storage: options.storage,
970
+ tableName
971
+ })
190
972
 
191
- let migration = `exports.up = function(knex) {
192
- return knex.schema.createTable('${tableName}', (table) => {\n`
973
+ const lines = []
193
974
 
194
- // Check if schema has an 'id' field with primary key
195
- const hasIdField = schema.structure.id && schema.structure.id.primary === true
975
+ if (!tableSchemaContext.hasPrimaryIdField && autoIncrement) {
976
+ lines.push(`table.increments(${quoteJsString(tableSchemaContext.idColumn)}).primary()`)
977
+ }
978
+
979
+ for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
980
+ if (fieldName === tableSchemaContext.resourceIdField && !tableSchemaContext.hasPrimaryIdField && autoIncrement) {
981
+ continue
982
+ }
983
+
984
+ const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
985
+ lines.push(buildColumnBuilderCode(columnName, definition, {
986
+ dialect
987
+ }))
988
+ }
989
+
990
+ for (const index of tableSchemaContext.indexes) {
991
+ lines.push(buildIndexLine(index))
992
+ }
993
+
994
+ for (const foreignKey of tableSchemaContext.foreignKeys) {
995
+ lines.push(buildForeignKeyLine(foreignKey))
996
+ }
196
997
 
197
- // Add auto-incrementing ID if needed
198
- if (!hasIdField && autoIncrement) {
199
- migration += ' table.increments(\'id\').primary();\n'
998
+ for (const checkConstraint of tableSchemaContext.checkConstraints) {
999
+ lines.push(buildCheckConstraintLine(checkConstraint))
200
1000
  }
201
1001
 
202
- // Process each field
203
- for (const [fieldName, definition] of Object.entries(schema.structure)) {
204
- if (fieldName === 'id' && !hasIdField && autoIncrement) {
1002
+ if (timestamps) {
1003
+ lines.push('table.timestamps(true, true)')
1004
+ }
1005
+
1006
+ return `exports.up = function(knex) {
1007
+ return knex.schema.createTable(${quoteJsString(tableName)}, (table) => {
1008
+ ${lines.map((line) => ` ${line};`).join('\n')}
1009
+ });
1010
+ };
1011
+
1012
+ exports.down = function(knex) {
1013
+ return knex.schema.dropTable(${quoteJsString(tableName)});
1014
+ };`
1015
+ }
1016
+
1017
+ /**
1018
+ * Generates a deterministic Knex alter migration by diffing a live table snapshot against a desired schema.
1019
+ * @param {string} tableName - The table name being diffed
1020
+ * @param {object} currentSnapshot - The normalized table snapshot returned by introspection
1021
+ * @param {object} schema - The desired schema-like table metadata object
1022
+ * @param {object} [options={}] - Diff options
1023
+ * @param {boolean} [options.autoIncrement=true] - Whether the resource expects an implicit auto-increment id column
1024
+ * @param {boolean} [options.allowDropColumns=false] - Whether removed columns should be dropped automatically
1025
+ * @param {string} [options.dialect=''] - Optional target dialect override
1026
+ * @returns {{ migration: string, warnings: string[], plan: object }} Diff result
1027
+ */
1028
+ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, options = {}) {
1029
+ const {
1030
+ autoIncrement = true,
1031
+ allowDropColumns = false,
1032
+ dialect = ''
1033
+ } = options
1034
+ const resolvedDialect = normalizeText(dialect || currentSnapshot?.dialect).toLowerCase()
1035
+ const tableSchemaContext = resolveTableSchemaContext(schema, {
1036
+ ...options,
1037
+ storage: options.storage,
1038
+ tableName
1039
+ })
1040
+ const desiredColumns = buildDesiredColumnsMap(tableSchemaContext, { autoIncrement })
1041
+ const currentColumns = mapByName(currentSnapshot?.columns || [])
1042
+ const desiredIndexes = mapByName(tableSchemaContext.indexes)
1043
+ const currentIndexes = mapByName(currentSnapshot?.indexes || [])
1044
+ const desiredForeignKeys = mapByName(tableSchemaContext.foreignKeys)
1045
+ const currentForeignKeys = mapByName(currentSnapshot?.foreignKeys || [])
1046
+ const desiredChecks = mapByName(tableSchemaContext.checkConstraints)
1047
+ const currentChecks = mapByName(currentSnapshot?.checkConstraints || [])
1048
+ const plan = createEmptyDiffPlan()
1049
+
1050
+ for (const [columnName, desiredColumn] of desiredColumns.entries()) {
1051
+ const currentColumn = currentColumns.get(columnName)
1052
+
1053
+ if (!currentColumn) {
1054
+ if (desiredColumn.autoIncrement) {
1055
+ plan.warnings.push(`Skipping implicit auto-increment id column '${columnName}' because it cannot be added safely to an existing table.`)
1056
+ continue
1057
+ }
1058
+
1059
+ if (desiredColumn.setValues.length > 0 && !isMysqlDialect(resolvedDialect)) {
1060
+ plan.warnings.push(`Skipping setValues column '${columnName}' because it requires a MySQL-compatible ALTER target.`)
1061
+ continue
1062
+ }
1063
+
1064
+ plan.addColumns.push(desiredColumn)
205
1065
  continue
206
1066
  }
207
1067
 
208
- let line = ' '
1068
+ if (desiredColumn.autoIncrement) {
1069
+ continue
1070
+ }
209
1071
 
210
- // Map type
211
- switch (definition.type) {
212
- case 'string':
213
- line += definition.maxLength
214
- ? `table.string('${fieldName}', ${definition.maxLength})`
215
- : `table.string('${fieldName}')`
216
- break
217
- case 'number':
218
- if (definition.precision !== undefined && definition.scale !== undefined) {
219
- line += `table.decimal('${fieldName}', ${definition.precision}, ${definition.scale})`
220
- } else {
221
- line += `table.float('${fieldName}')`
222
- }
223
- break
224
- case 'id':
225
- line += `table.integer('${fieldName}')`
226
- if (definition.unsigned !== false) line += '.unsigned()'
227
- break
228
- case 'boolean':
229
- line += `table.boolean('${fieldName}')`
230
- break
231
- case 'date':
232
- line += `table.date('${fieldName}')`
233
- break
234
- case 'dateTime':
235
- line += `table.datetime('${fieldName}')`
236
- break
237
- case 'time':
238
- line += `table.time('${fieldName}')`
239
- break
240
- case 'timestamp':
241
- line += `table.integer('${fieldName}')`
242
- break
243
- case 'array':
244
- case 'object':
245
- case 'serialize':
246
- line += `table.json('${fieldName}')`
247
- break
248
- case 'blob':
249
- case 'file':
250
- line += `table.binary('${fieldName}')`
251
- break
252
- default:
253
- line += `table.string('${fieldName}')`
1072
+ if (!columnNeedsAlter(currentColumn, desiredColumn, { dialect: resolvedDialect })) {
1073
+ continue
1074
+ }
1075
+
1076
+ if (desiredColumn.setValues.length > 0 && !isMysqlDialect(resolvedDialect)) {
1077
+ plan.warnings.push(`Skipping setValues alteration for column '${columnName}' because it requires a MySQL-compatible ALTER target.`)
1078
+ continue
254
1079
  }
255
1080
 
256
- // Add constraints
257
- if (definition.nullable === true) {
258
- line += '.nullable()'
259
- } else if (definition.required === true || definition.nullable === false) {
260
- line += '.notNullable()'
1081
+ plan.alterColumns.push(desiredColumn)
1082
+ plan.warnings.push(...collectDestructiveColumnWarnings(currentColumn, desiredColumn))
1083
+ }
1084
+
1085
+ for (const currentColumn of currentColumns.values()) {
1086
+ if (desiredColumns.has(currentColumn.name)) {
1087
+ continue
261
1088
  }
262
1089
 
263
- if (definition.defaultTo !== undefined) {
264
- const defaultValue = typeof definition.defaultTo === 'string'
265
- ? `'${definition.defaultTo}'`
266
- : definition.defaultTo
267
- line += `.defaultTo(${defaultValue})`
1090
+ const warning = `Column '${currentColumn.name}' exists in the live table but not in the desired schema.`
1091
+ if (allowDropColumns) {
1092
+ plan.dropColumns.push(currentColumn)
1093
+ plan.warnings.push(`${warning} It will be dropped because allowDropColumns=true.`)
1094
+ } else {
1095
+ plan.warnings.push(`${warning} Skipping automatic drop.`)
268
1096
  }
1097
+ }
269
1098
 
270
- if (definition.unique === true) line += '.unique()'
271
- if (definition.primary === true) line += '.primary()'
272
- if (definition.index === true) line += '.index()'
1099
+ for (const desiredIndex of desiredIndexes.values()) {
1100
+ const currentIndex = currentIndexes.get(desiredIndex.name)
1101
+ const sameIndexType = normalizeText(currentIndex?.indexType).toUpperCase() === desiredIndex.indexType
1102
+ const indexChanged = Boolean(currentIndex) && (
1103
+ !valuesEqual(currentIndex.columns, desiredIndex.columns) ||
1104
+ Boolean(currentIndex.unique) !== Boolean(desiredIndex.unique) ||
1105
+ !sameIndexType
1106
+ )
273
1107
 
274
- if (definition.references) {
275
- const refCol = definition.references.column || 'id'
276
- line += `.references('${refCol}').inTable('${definition.references.table}')`
277
- if (definition.references.onDelete) {
278
- line += `.onDelete('${definition.references.onDelete}')`
279
- }
280
- if (definition.references.onUpdate) {
281
- line += `.onUpdate('${definition.references.onUpdate}')`
1108
+ if (!currentIndex || indexChanged) {
1109
+ plan.addIndexes.push(desiredIndex)
1110
+ if (indexChanged) {
1111
+ plan.dropIndexes.push(currentIndex)
282
1112
  }
283
1113
  }
1114
+ }
284
1115
 
285
- if (definition.comment) {
286
- line += `.comment('${definition.comment}')`
1116
+ for (const currentIndex of currentIndexes.values()) {
1117
+ if (!desiredIndexes.has(currentIndex.name)) {
1118
+ plan.dropIndexes.push(currentIndex)
287
1119
  }
1120
+ }
1121
+
1122
+ for (const desiredForeignKey of desiredForeignKeys.values()) {
1123
+ const currentForeignKey = currentForeignKeys.get(desiredForeignKey.name)
1124
+ const sameColumns = valuesEqual(
1125
+ (currentForeignKey?.columns || []).map((entry) => entry.name),
1126
+ desiredForeignKey.columns
1127
+ )
1128
+ const sameReferencedColumns = valuesEqual(
1129
+ (currentForeignKey?.columns || []).map((entry) => entry.referencedName),
1130
+ desiredForeignKey.referencedColumns
1131
+ )
1132
+ const sameTable = normalizeText(currentForeignKey?.referencedTableName) === desiredForeignKey.referencedTableName
1133
+ const sameDeleteRule = normalizeText(currentForeignKey?.deleteRule).toUpperCase() === desiredForeignKey.deleteRule
1134
+ const sameUpdateRule = normalizeText(currentForeignKey?.updateRule).toUpperCase() === desiredForeignKey.updateRule
288
1135
 
289
- migration += line + ';\n'
1136
+ if (!currentForeignKey || !sameColumns || !sameReferencedColumns || !sameTable || !sameDeleteRule || !sameUpdateRule) {
1137
+ plan.addForeignKeys.push(desiredForeignKey)
1138
+ }
290
1139
  }
291
1140
 
292
- if (timestamps) {
293
- migration += ' table.timestamps(true, true);\n'
1141
+ for (const currentForeignKey of currentForeignKeys.values()) {
1142
+ if (!desiredForeignKeys.has(currentForeignKey.name)) {
1143
+ plan.dropForeignKeys.push(currentForeignKey)
1144
+ }
294
1145
  }
295
1146
 
296
- migration += ` });
297
- };
1147
+ for (const desiredCheck of desiredChecks.values()) {
1148
+ const currentCheck = currentChecks.get(desiredCheck.name)
1149
+ if (currentCheck?.clause === desiredCheck.clause) {
1150
+ continue
1151
+ }
298
1152
 
299
- exports.down = function(knex) {
300
- return knex.schema.dropTable('${tableName}');
301
- };`
1153
+ if (isSqliteDialectName(resolvedDialect)) {
1154
+ plan.warnings.push(buildUnsupportedConstraintWarning('check constraint', desiredCheck.name, resolvedDialect))
1155
+ continue
1156
+ }
1157
+
1158
+ plan.addCheckConstraints.push(desiredCheck)
1159
+ }
302
1160
 
303
- return migration
1161
+ for (const currentCheck of currentChecks.values()) {
1162
+ const matchesDesiredEnum = [...desiredColumns.values()].some((desiredColumn) => {
1163
+ return isInlineEnumCheckConstraint(currentCheck, desiredColumn)
1164
+ })
1165
+ if (matchesDesiredEnum) {
1166
+ continue
1167
+ }
1168
+
1169
+ if (!desiredChecks.has(currentCheck.name)) {
1170
+ plan.warnings.push(buildUnsupportedConstraintWarning('check constraint removal', currentCheck.name, resolvedDialect))
1171
+ continue
1172
+ }
1173
+
1174
+ if (desiredChecks.get(currentCheck.name)?.clause !== currentCheck.clause) {
1175
+ plan.warnings.push(buildUnsupportedConstraintWarning('check constraint alteration', currentCheck.name, resolvedDialect))
1176
+ }
1177
+ }
1178
+
1179
+ plan.addColumns.sort(compareByName)
1180
+ plan.alterColumns.sort(compareByName)
1181
+ plan.dropColumns.sort(compareByName)
1182
+ plan.addIndexes.sort(compareByName)
1183
+ plan.dropIndexes.sort(compareByName)
1184
+ plan.addForeignKeys.sort(compareByName)
1185
+ plan.dropForeignKeys.sort(compareByName)
1186
+ plan.addCheckConstraints.sort(compareByName)
1187
+ plan.warnings = [...new Set(plan.warnings)]
1188
+
1189
+ const blocks = []
1190
+
1191
+ const addOrAlterColumnLines = [
1192
+ ...plan.addColumns.map((column) => buildColumnBuilderCode(column.name, column.definition, { dialect: resolvedDialect })),
1193
+ ...plan.alterColumns.map((column) => buildColumnBuilderCode(column.name, column.definition, {
1194
+ dialect: resolvedDialect,
1195
+ alter: true,
1196
+ includePrimary: false
1197
+ }))
1198
+ ]
1199
+
1200
+ if (plan.dropForeignKeys.length > 0) {
1201
+ blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
1202
+ ${plan.dropForeignKeys.map((foreignKey) => ` table.dropForeign(${formatCodeLiteral(foreignKey.columns.map((entry) => entry.name))}, ${quoteJsString(foreignKey.name)});`).join('\n')}
1203
+ });`)
1204
+ }
1205
+
1206
+ if (plan.dropIndexes.length > 0) {
1207
+ blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
1208
+ ${plan.dropIndexes.map((index) => {
1209
+ if (index.unique) {
1210
+ return ` table.dropUnique(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)});`
1211
+ }
1212
+ return ` table.dropIndex(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)});`
1213
+ }).join('\n')}
1214
+ });`)
1215
+ }
1216
+
1217
+ if (plan.dropColumns.length > 0) {
1218
+ blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
1219
+ ${plan.dropColumns.map((column) => ` table.dropColumn(${quoteJsString(column.name)});`).join('\n')}
1220
+ });`)
1221
+ }
1222
+
1223
+ if (addOrAlterColumnLines.length > 0) {
1224
+ blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
1225
+ ${addOrAlterColumnLines.map((line) => ` ${line};`).join('\n')}
1226
+ });`)
1227
+ }
1228
+
1229
+ const addConstraintLines = [
1230
+ ...plan.addIndexes.map((index) => buildIndexLine(index)),
1231
+ ...plan.addForeignKeys.map((foreignKey) => buildForeignKeyLine(foreignKey)),
1232
+ ...plan.addCheckConstraints.map((constraint) => buildCheckConstraintLine(constraint))
1233
+ ]
1234
+
1235
+ if (addConstraintLines.length > 0) {
1236
+ blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
1237
+ ${addConstraintLines.map((line) => ` ${line};`).join('\n')}
1238
+ });`)
1239
+ }
1240
+
1241
+ const warningComment = plan.warnings.length > 0
1242
+ ? ` // Warnings:\n${plan.warnings.map((warning) => ` // - ${warning}`).join('\n')}\n`
1243
+ : ''
1244
+
1245
+ return {
1246
+ migration: `exports.up = async function(knex) {
1247
+ ${warningComment}${blocks.length > 0 ? `${blocks.join('\n\n')}\n` : ' return Promise.resolve();\n'}};
1248
+
1249
+ exports.down = async function() {
1250
+ throw new Error(${quoteJsString(`Down migration was not generated automatically for table '${tableName}'.`)});
1251
+ };`,
1252
+ warnings: plan.warnings,
1253
+ plan
1254
+ }
304
1255
  }