json-rest-api 1.0.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) 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 +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,3 +1,5 @@
1
+ import { hasKnexTableIndex } from '../dbIntrospection.js'
2
+
1
3
  const STRING_SLOT_COUNT = 10
2
4
  const NUMBER_SLOT_COUNT = 10
3
5
  const BOOLEAN_SLOT_COUNT = 5
@@ -48,17 +50,11 @@ const ensureColumn = async (knex, tableName, columnName, columnBuilder) => {
48
50
  const ensureUniqueIndex = async (knex, tableName, columns, indexName) => {
49
51
  const exists = await knex.schema.hasTable(tableName)
50
52
  if (!exists) return
51
- const hasIndex = await knex.schema.withSchema('main').hasIndex?.(tableName, indexName)
53
+ const hasIndex = await hasKnexTableIndex(knex, tableName, indexName)
52
54
  if (!hasIndex) {
53
- try {
54
- await knex.schema.table(tableName, (table) => {
55
- table.unique(columns, indexName)
56
- })
57
- } catch (error) {
58
- if (!String(error?.message || '').includes('already exists')) {
59
- throw error
60
- }
61
- }
55
+ await knex.schema.table(tableName, (table) => {
56
+ table.unique(columns, indexName)
57
+ })
62
58
  }
63
59
  }
64
60
 
@@ -79,7 +75,7 @@ export const ensureAnyApiSchema = async (knex) => {
79
75
  table.boolean(`boolean_${index}`)
80
76
  }
81
77
  for (let index = 1; index <= DATE_SLOT_COUNT; index += 1) {
82
- table.dateTime(`date_${index}`)
78
+ table.dateTime(`date_${index}`, { precision: 6 })
83
79
  }
84
80
  for (let index = 1; index <= JSON_SLOT_COUNT; index += 1) {
85
81
  table.text(`json_${index}`)
@@ -99,6 +95,18 @@ export const ensureAnyApiSchema = async (knex) => {
99
95
  table.index(['resource'])
100
96
  })
101
97
 
98
+ const client = knex.client.config.client
99
+ if (['pg', 'postgresql', 'mysql', 'mysql2'].includes(client)) {
100
+ const columns = await knex('information_schema.columns')
101
+ .select({ column: 'column_name', precision: 'datetime_precision' })
102
+ .where('table_schema', knex.raw(client === 'pg' || client === 'postgresql' ? 'current_schema()' : 'DATABASE()'))
103
+ .where('table_name', 'any_records')
104
+ .whereIn('column_name', Array.from({ length: DATE_SLOT_COUNT }, (_, index) => `date_${index + 1}`))
105
+ if (columns.length !== DATE_SLOT_COUNT || columns.some(column => Number(column.precision) !== 6)) {
106
+ throw new Error('AnyAPI temporal storage migration required: any_records.date_1 through date_5 must use timestamp/datetime precision 6. See 33-migrating-to-v2.md.')
107
+ }
108
+ }
109
+
102
110
  await ensureColumn(knex, 'any_records', 'logical_id', (table) => {
103
111
  table.string('logical_id')
104
112
  })
@@ -154,7 +162,7 @@ export const ensureAnyApiSchema = async (knex) => {
154
162
 
155
163
  await ensureTable(knex, 'any_field_configs', (table) => {
156
164
  table.increments('id').primary()
157
- table.integer('resource_config_id').notNullable()
165
+ table.integer('resource_config_id').unsigned().notNullable()
158
166
  .references('id').inTable('any_resource_configs').onDelete('CASCADE')
159
167
  table.string('field_name').notNullable()
160
168
  table.string('slot_type').notNullable()
@@ -174,7 +182,7 @@ export const ensureAnyApiSchema = async (knex) => {
174
182
 
175
183
  await ensureTable(knex, 'any_relationship_configs', (table) => {
176
184
  table.increments('id').primary()
177
- table.integer('resource_config_id').notNullable()
185
+ table.integer('resource_config_id').unsigned().notNullable()
178
186
  .references('id').inTable('any_resource_configs').onDelete('CASCADE')
179
187
  table.string('relationship_name').notNullable()
180
188
  table.string('relationship_type').notNullable()
@@ -191,7 +199,7 @@ export const ensureAnyApiSchema = async (knex) => {
191
199
  table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable()
192
200
  table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable()
193
201
 
194
- table.unique(['resource_config_id', 'relationship_name'])
202
+ table.unique(['resource_config_id', 'relationship_name'], 'any_relationship_configs_resource_relationship_unique')
195
203
  table.index(['resource_config_id'])
196
204
  })
197
205
  }
@@ -216,12 +224,13 @@ export const TYPE_TO_POOL = new Map([
216
224
  ['float', 'number'],
217
225
  ['decimal', 'number'],
218
226
  ['boolean', 'boolean'],
219
- ['date', 'date'],
227
+ ['date', 'string'],
220
228
  ['datetime', 'date'],
221
229
  ['dateTime', 'date'],
222
- ['time', 'date'],
230
+ ['time', 'string'],
223
231
  ['epochMilliseconds', 'number'],
224
232
  ['epochSeconds', 'number'],
225
233
  ['json', 'json'],
226
234
  ['object', 'json'],
235
+ ['array', 'json'],
227
236
  ])
@@ -1,6 +1,3 @@
1
- import { RestApiValidationError } from '../../../../../lib/rest-api-errors.js'
2
- import { normalizeValueForDatabaseStorage } from '../../querying-writing/database-value-normalizers.js'
3
-
4
1
  export const normalizeId = (value) => (value === null || value === undefined ? null : String(value))
5
2
 
6
3
  export const findSchemaFieldByAlias = (descriptor, alias) => {
@@ -61,64 +58,3 @@ export const resolveFieldInfo = (descriptor, field) => {
61
58
 
62
59
  return null
63
60
  }
64
-
65
- export const coerceValueForDefinition = (value, definition, { isRelationship } = {}) => {
66
- if (value === null || value === undefined) return null
67
-
68
- if (isRelationship) {
69
- return normalizeId(value)
70
- }
71
-
72
- const type = definition?.type || definition?.dataType
73
- if (!type) {
74
- return value
75
- }
76
-
77
- if (['number', 'integer', 'float', 'decimal'].includes(type)) {
78
- const numeric = Number(value)
79
- return Number.isNaN(numeric) ? value : numeric
80
- }
81
-
82
- if (type === 'boolean') {
83
- if (typeof value === 'boolean') return value
84
- if (typeof value === 'string') {
85
- if (value.toLowerCase() === 'true') return true
86
- if (value.toLowerCase() === 'false') return false
87
- }
88
- return Boolean(value)
89
- }
90
-
91
- if (['string', 'text', 'uuid', 'email'].includes(type)) {
92
- return String(value)
93
- }
94
-
95
- if (['date', 'dateTime', 'time'].includes(type)) {
96
- return normalizeValueForDatabaseStorage(value, type, {
97
- temporalPrecision: definition?.temporalPrecision
98
- })
99
- }
100
-
101
- return value
102
- }
103
-
104
- export const normalizeFilterValues = (rawValue, definition, options) => {
105
- if (Array.isArray(rawValue)) {
106
- return rawValue.map((item) => coerceValueForDefinition(item, definition, options))
107
- }
108
- return [coerceValueForDefinition(rawValue, definition, options)]
109
- }
110
-
111
- export const ensureFilterableField = (descriptor, field) => {
112
- const fieldInfo = resolveFieldInfo(descriptor, field)
113
- if (!fieldInfo?.column) {
114
- throw new RestApiValidationError('Invalid filter field', {
115
- fields: [`filters.${field}`],
116
- violations: [{
117
- field: `filters.${field}`,
118
- rule: 'unknown_field',
119
- message: `Filter on '${field}' is not supported in AnyAPI mode`,
120
- }],
121
- })
122
- }
123
- return fieldInfo
124
- }
@@ -44,6 +44,9 @@ function detectDialect (knex) {
44
44
  if (clientName.includes('mysql')) {
45
45
  return 'mysql2'
46
46
  }
47
+ if (clientName === 'pg' || clientName === 'postgresql') {
48
+ return 'pg'
49
+ }
47
50
 
48
51
  throw new Error(`Unsupported knex client "${clientName || 'unknown'}" for introspectKnexTableSnapshot.`)
49
52
  }
@@ -67,10 +70,11 @@ function toNullableNumber (value) {
67
70
  return Number.isFinite(parsed) ? parsed : null
68
71
  }
69
72
 
70
- function normalizeColumnDefault (value) {
73
+ function normalizeColumnDefault (value, literal = false) {
71
74
  if (value == null) {
72
75
  return null
73
76
  }
77
+ if (literal) return value
74
78
 
75
79
  if (typeof value === 'string') {
76
80
  const text = value.trim()
@@ -89,7 +93,12 @@ function normalizeColumnDefault (value) {
89
93
  return value
90
94
  }
91
95
 
92
- function parseEnumLikeValues (columnType = '', typeName = 'enum') {
96
+ function decodeMysqlStringEscapes (value) {
97
+ const escapes = { 0: '\0', b: '\b', n: '\n', r: '\r', t: '\t', Z: '\x1a' }
98
+ return value.replace(/\\(.)/gs, (_match, char) => escapes[char] ?? char)
99
+ }
100
+
101
+ function parseEnumLikeValues (columnType = '', typeName = 'enum', backslashEscapes = false) {
93
102
  const source = normalizeText(columnType)
94
103
  const normalizedType = `${String(typeName || '').toLowerCase()}(`
95
104
  if (!source.toLowerCase().startsWith(normalizedType) || !source.endsWith(')')) {
@@ -98,21 +107,35 @@ function parseEnumLikeValues (columnType = '', typeName = 'enum') {
98
107
 
99
108
  const body = source.slice(normalizedType.length, -1)
100
109
  const values = []
101
- const pattern = /'((?:''|\\'|[^'])*)'/g
110
+ const pattern = backslashEscapes ? /'((?:''|\\.|[^'\\])*)'/gs : /'((?:''|[^'])*)'/g
102
111
  let match = null
103
112
  while ((match = pattern.exec(body)) != null) {
104
- values.push(match[1].replace(/''/g, "'").replace(/\\'/g, "'"))
113
+ const value = match[1].replace(/''/g, "'")
114
+ values.push(backslashEscapes ? decodeMysqlStringEscapes(value) : value)
105
115
  }
106
116
 
107
117
  return values
108
118
  }
109
119
 
120
+ function normalizeMysqlDefault (row) {
121
+ const value = Object.hasOwn(row, 'columnDefault') ? row.columnDefault : row.column_default
122
+ if (row.dataType !== 'json' || typeof value !== 'string') return value
123
+ let expression = value.trim()
124
+ if (expression.startsWith('(') && expression.endsWith(')')) expression = expression.slice(1, -1).trim()
125
+ // MySQL escapes a generated default expression once more in COLUMN_DEFAULT.
126
+ if (/^(?:_[a-z0-9]+)?\\'/i.test(expression)) expression = decodeMysqlStringEscapes(expression)
127
+ const literal = /^(?:_[a-z0-9]+)?('(?:''|\\.|[^'\\])*')$/is.exec(expression)
128
+ return literal ? parseEnumLikeValues(`enum(${literal[1]})`, 'enum', true)[0] : value
129
+ }
130
+
110
131
  function resolveTypeKind ({ name = '', dataType = '', columnType = '' } = {}) {
111
132
  const normalizedType = normalizeText(dataType).toLowerCase()
112
133
  const normalizedColumnType = normalizeText(columnType).toLowerCase()
113
134
 
114
135
  if (
115
136
  normalizedType === 'varchar' ||
137
+ normalizedType === 'character varying' ||
138
+ normalizedType === 'character' ||
116
139
  normalizedType === 'char' ||
117
140
  normalizedType === 'text' ||
118
141
  normalizedType === 'tinytext' ||
@@ -146,6 +169,7 @@ function resolveTypeKind ({ name = '', dataType = '', columnType = '' } = {}) {
146
169
  normalizedType === 'numeric' ||
147
170
  normalizedType === 'float' ||
148
171
  normalizedType === 'double' ||
172
+ normalizedType === 'double precision' ||
149
173
  normalizedType === 'real'
150
174
  ) {
151
175
  return 'number'
@@ -155,20 +179,21 @@ function resolveTypeKind ({ name = '', dataType = '', columnType = '' } = {}) {
155
179
  return 'boolean'
156
180
  }
157
181
 
158
- if (normalizedType === 'datetime' || normalizedType === 'timestamp') {
182
+ if (['datetime', 'timestamp', 'timestamp with time zone', 'timestamp without time zone'].includes(normalizedType)) {
159
183
  return 'datetime'
160
184
  }
161
185
  if (normalizedType === 'date') {
162
186
  return 'date'
163
187
  }
164
- if (normalizedType === 'time') {
188
+ if (['time', 'time without time zone'].includes(normalizedType)) {
165
189
  return 'time'
166
190
  }
167
- if (normalizedType === 'json') {
191
+ if (normalizedType === 'json' || normalizedType === 'jsonb') {
168
192
  return 'json'
169
193
  }
170
194
  if (
171
195
  normalizedType === 'blob' ||
196
+ normalizedType === 'bytea' ||
172
197
  normalizedType === 'binary' ||
173
198
  normalizedType === 'varbinary'
174
199
  ) {
@@ -184,6 +209,7 @@ function buildColumnSnapshot ({
184
209
  columnType = '',
185
210
  nullable = false,
186
211
  defaultValue = null,
212
+ defaultIsLiteral = false,
187
213
  extra = '',
188
214
  autoIncrement = false,
189
215
  unsigned = false,
@@ -200,7 +226,7 @@ function buildColumnSnapshot ({
200
226
  const normalizedName = normalizeText(name)
201
227
  const normalizedDataType = normalizeText(dataType).toLowerCase()
202
228
  const normalizedColumnType = normalizeText(columnType)
203
- const normalizedDefaultValue = normalizeColumnDefault(defaultValue)
229
+ const normalizedDefaultValue = normalizeColumnDefault(defaultValue, defaultIsLiteral)
204
230
 
205
231
  return {
206
232
  name: normalizedName,
@@ -225,8 +251,8 @@ function buildColumnSnapshot ({
225
251
  characterSetName: normalizeText(characterSetName),
226
252
  collationName: normalizeText(collationName),
227
253
  ordinalPosition: toNullableNumber(ordinalPosition),
228
- enumValues: Array.isArray(enumValues) ? enumValues : parseEnumLikeValues(normalizedColumnType, 'enum'),
229
- setValues: Array.isArray(setValues) ? setValues : parseEnumLikeValues(normalizedColumnType, 'set')
254
+ enumValues: Array.isArray(enumValues) ? enumValues : parseEnumLikeValues(normalizedColumnType, 'enum', true),
255
+ setValues: Array.isArray(setValues) ? setValues : parseEnumLikeValues(normalizedColumnType, 'set', true)
230
256
  }
231
257
  }
232
258
 
@@ -365,16 +391,12 @@ function requireIdColumn (columns, idColumn) {
365
391
  if (!idSpec) {
366
392
  throw new Error(`Could not find id column "${normalizedIdColumn}" in table.`)
367
393
  }
368
- if (idSpec.typeKind !== 'integer') {
369
- throw new Error(`Id column "${normalizedIdColumn}" must use an integer type.`)
394
+ if (!['integer', 'string'].includes(idSpec.typeKind)) {
395
+ throw new Error(`Id column "${normalizedIdColumn}" must use an integer or string type.`)
370
396
  }
371
397
  if (idSpec.nullable) {
372
398
  throw new Error(`Id column "${normalizedIdColumn}" must be not-null.`)
373
399
  }
374
- if (!idSpec.autoIncrement && !idSpec.hasDefault) {
375
- throw new Error(`Id column "${normalizedIdColumn}" must be auto-incrementing or have a database default.`)
376
- }
377
-
378
400
  return normalizedIdColumn
379
401
  }
380
402
 
@@ -760,7 +782,7 @@ function deriveSqliteColumnNumbers (dataType, args = '') {
760
782
  }
761
783
  }
762
784
 
763
- async function introspectSqliteTableSnapshot (knex, { tableName, idColumn }) {
785
+ async function introspectSqliteColumns (knex, tableName) {
764
786
  const quotedTableName = quoteSqliteIdentifier(tableName)
765
787
  const tableRows = normalizeRows(await knex.raw(
766
788
  'SELECT name, sql FROM sqlite_master WHERE type = \'table\' AND name = ? LIMIT 1',
@@ -778,8 +800,8 @@ async function introspectSqliteTableSnapshot (knex, { tableName, idColumn }) {
778
800
  tableEntries.map((entry) => [readLeadingIdentifier(entry), entry])
779
801
  )
780
802
 
781
- const columnRows = normalizeRows(await knex.raw(`PRAGMA table_xinfo(${quotedTableName})`))
782
- .filter((row) => Number(row.hidden || 0) === 0)
803
+ const allColumnRows = normalizeRows(await knex.raw(`PRAGMA table_xinfo(${quotedTableName})`))
804
+ const columnRows = allColumnRows.filter((row) => Number(row.hidden || 0) === 0)
783
805
  if (columnRows.length < 1) {
784
806
  throw new Error(`Could not introspect table "${tableName}".`)
785
807
  }
@@ -820,6 +842,14 @@ async function introspectSqliteTableSnapshot (knex, { tableName, idColumn }) {
820
842
  .sort((left, right) => Number(left.pk) - Number(right.pk))
821
843
  .map((row) => normalizeText(row.name))
822
844
 
845
+ return { columns, primaryKeyColumns, parsedConstraints, hiddenColumns: allColumnRows.filter(row => Number(row.hidden || 0) !== 0) }
846
+ }
847
+
848
+ async function introspectSqliteTableSnapshot (knex, { tableName, idColumn }) {
849
+ const { columns, primaryKeyColumns, parsedConstraints, hiddenColumns } = await introspectSqliteColumns(knex, tableName)
850
+ if (hiddenColumns.length) throw new Error(`Column '${hiddenColumns[0].name}' is generated or hidden and cannot be represented by this table snapshot.`)
851
+ const quotedTableName = quoteSqliteIdentifier(tableName)
852
+
823
853
  const indexListRows = normalizeRows(await knex.raw(`PRAGMA index_list(${quotedTableName})`))
824
854
  const indexRows = []
825
855
  for (const row of indexListRows) {
@@ -829,7 +859,11 @@ async function introspectSqliteTableSnapshot (knex, { tableName, idColumn }) {
829
859
 
830
860
  const indexName = normalizeText(row.name)
831
861
  const quotedIndexName = quoteSqliteIdentifier(indexName)
832
- const columnsForIndex = normalizeRows(await knex.raw(`PRAGMA index_info(${quotedIndexName})`))
862
+ const columnsForIndex = normalizeRows(await knex.raw(`PRAGMA index_xinfo(${quotedIndexName})`))
863
+ .filter(column => Number(column.key) === 1)
864
+ if (Number(row.partial) === 1 || columnsForIndex.some(column => Number(column.cid) < 0 || !column.name || Number(column.desc) === 1 || (column.coll && column.coll !== 'BINARY'))) {
865
+ throw new Error(`Index '${indexName}' cannot be represented by a simple column-index snapshot.`)
866
+ }
833
867
 
834
868
  for (const columnRow of columnsForIndex) {
835
869
  indexRows.push({
@@ -928,6 +962,8 @@ async function introspectMysqlTableSnapshot (knex, { tableName, idColumn }) {
928
962
  if (columnRows.length < 1) {
929
963
  throw new Error(`Could not introspect table "${tableName}" in schema "${schemaName}".`)
930
964
  }
965
+ const generatedColumn = columnRows.find(row => /\b(?:virtual|stored) generated\b/i.test(row.extra || ''))
966
+ if (generatedColumn) throw new Error(`Column '${generatedColumn.columnName}' is generated and cannot be represented by this table snapshot.`)
931
967
 
932
968
  const primaryRows = normalizeRows(
933
969
  await knex.raw(
@@ -957,6 +993,8 @@ async function introspectMysqlTableSnapshot (knex, { tableName, idColumn }) {
957
993
  s.non_unique AS nonUnique,
958
994
  s.index_type AS indexType,
959
995
  s.column_name AS columnName,
996
+ s.sub_part AS subPart,
997
+ s.collation AS collation,
960
998
  s.seq_in_index AS seqInIndex
961
999
  FROM information_schema.statistics s
962
1000
  WHERE s.table_schema = ?
@@ -968,6 +1006,9 @@ async function introspectMysqlTableSnapshot (knex, { tableName, idColumn }) {
968
1006
  )
969
1007
  )
970
1008
 
1009
+ const unsupportedIndex = indexRows.find(row => !row.columnName || row.subPart != null || (row.collation && row.collation !== 'A'))
1010
+ if (unsupportedIndex) throw new Error(`Index '${unsupportedIndex.indexName}' cannot be represented by a simple column-index snapshot.`)
1011
+
971
1012
  const foreignKeyRows = normalizeRows(
972
1013
  await knex.raw(
973
1014
  `
@@ -1016,7 +1057,8 @@ async function introspectMysqlTableSnapshot (knex, { tableName, idColumn }) {
1016
1057
  dataType: row.dataType,
1017
1058
  columnType: row.columnType,
1018
1059
  nullable: normalizeText(row.isNullable).toUpperCase() === 'YES',
1019
- defaultValue: Object.prototype.hasOwnProperty.call(row, 'columnDefault') ? row.columnDefault : row.column_default,
1060
+ defaultValue: normalizeMysqlDefault(row),
1061
+ defaultIsLiteral: true,
1020
1062
  extra: row.extra,
1021
1063
  autoIncrement: normalizeText(row.extra).toLowerCase().includes('auto_increment'),
1022
1064
  unsigned: normalizeText(row.columnType).toLowerCase().includes('unsigned'),
@@ -1055,6 +1097,168 @@ async function introspectMysqlTableSnapshot (knex, { tableName, idColumn }) {
1055
1097
  }
1056
1098
  }
1057
1099
 
1100
+ async function introspectPostgresColumns (knex, tableName, explicitSchema) {
1101
+ const schemaName = explicitSchema || normalizeDbSchemaName(normalizeRows(await knex.raw('SELECT current_schema() AS "schemaName"')))
1102
+ const columnRows = normalizeRows(await knex.raw(`
1103
+ SELECT c.*, pg_catalog.format_type(a.atttypid, a.atttypmod) AS column_type
1104
+ FROM information_schema.columns c
1105
+ JOIN pg_catalog.pg_namespace n ON n.nspname = c.table_schema
1106
+ JOIN pg_catalog.pg_class t ON t.relnamespace = n.oid AND t.relname = c.table_name
1107
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid AND a.attname = c.column_name
1108
+ WHERE c.table_schema = ? AND c.table_name = ?
1109
+ ORDER BY c.ordinal_position
1110
+ `, [schemaName, tableName]))
1111
+ if (columnRows.length < 1) throw new Error(`Could not introspect table "${tableName}" in schema "${schemaName}".`)
1112
+
1113
+ const columns = columnRows.map(row => {
1114
+ const autoIncrement = row.is_identity === 'YES' || /^nextval\(/.test(row.column_default || '')
1115
+ // Decode quoted SQL constants without evaluating default expressions.
1116
+ const constant = /^'((?:''|[^'])*)'::[a-z ]+(?:\(\d+(?:,\d+)?\))?$/.exec(row.column_default || '')
1117
+ const defaultValue = constant ? constant[1].replace(/''/g, "'") : /^NULL(?:::.*)?$/i.test(row.column_default || '') ? null : row.column_default
1118
+ return buildColumnSnapshot({
1119
+ name: row.column_name,
1120
+ dataType: row.data_type,
1121
+ columnType: row.column_type,
1122
+ nullable: row.is_nullable === 'YES',
1123
+ defaultValue,
1124
+ defaultIsLiteral: Boolean(constant),
1125
+ autoIncrement,
1126
+ extra: autoIncrement ? 'auto_increment' : '',
1127
+ maxLength: row.character_maximum_length,
1128
+ numericPrecision: row.numeric_precision,
1129
+ numericScale: row.numeric_scale,
1130
+ datetimePrecision: row.datetime_precision,
1131
+ characterSetName: row.character_set_name,
1132
+ collationName: row.collation_name,
1133
+ ordinalPosition: row.ordinal_position
1134
+ })
1135
+ })
1136
+ const constraints = normalizeRows(await knex.raw(`
1137
+ SELECT c.conname, c.contype, c.confupdtype, c.confdeltype,
1138
+ parent.relname AS referenced_table, parent_ns.nspname AS referenced_schema,
1139
+ pg_catalog.pg_get_expr(c.conbin, c.conrelid) AS clause,
1140
+ ARRAY(SELECT a.attname::text FROM unnest(c.conkey) WITH ORDINALITY AS k(num, position)
1141
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.num
1142
+ ORDER BY k.position) AS columns,
1143
+ ARRAY(SELECT a.attname::text FROM unnest(c.confkey) WITH ORDINALITY AS k(num, position)
1144
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.num
1145
+ ORDER BY k.position) AS referenced_columns
1146
+ FROM pg_catalog.pg_constraint c
1147
+ JOIN pg_catalog.pg_class t ON t.oid = c.conrelid
1148
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace
1149
+ LEFT JOIN pg_catalog.pg_class parent ON parent.oid = c.confrelid
1150
+ LEFT JOIN pg_catalog.pg_namespace parent_ns ON parent_ns.oid = parent.relnamespace
1151
+ WHERE n.nspname = ? AND t.relname = ?
1152
+ ORDER BY c.conname
1153
+ `, [schemaName, tableName]))
1154
+ for (const constraint of constraints) {
1155
+ if (constraint.contype !== 'c' || constraint.columns.length !== 1) continue
1156
+ const match = /^\((?:"((?:""|[^"])*)"|([\w]+)) = ANY \(ARRAY\[(.*)\]\)\)$/.exec(constraint.clause || '')
1157
+ const column = columns.find(column => column.name === constraint.columns[0])
1158
+ if (!match || column?.dataType !== 'text' || (match[1]?.replace(/""/g, '"') || match[2]) !== column.name) continue
1159
+ const literalPattern = /'((?:''|[^'])*)'::text/g
1160
+ const literals = [...match[3].matchAll(literalPattern)]
1161
+ if (!literals.length || match[3].replace(literalPattern, '').replace(/[,\s]/g, '')) continue
1162
+ column.enumValues = literals.map(literal => literal[1].replace(/''/g, "'"))
1163
+ // PostgreSQL rewrites Knex's inline IN check to ANY; retain its logical form.
1164
+ constraint.clause = `"${column.name.replace(/"/g, '""')}" in (${literals.map(literal => `'${literal[1]}'`).join(', ')})`
1165
+ }
1166
+ return { schemaName, columns, constraints, generatedColumns: columnRows.filter(row => row.is_generated === 'ALWAYS') }
1167
+ }
1168
+
1169
+ async function introspectPostgresTableSnapshot (knex, { tableName, idColumn }) {
1170
+ const { schemaName, columns, constraints, generatedColumns } = await introspectPostgresColumns(knex, tableName)
1171
+ if (generatedColumns.length) throw new Error(`Column '${generatedColumns[0].column_name}' is generated and cannot be represented by this table snapshot.`)
1172
+ const indexRows = normalizeRows(await knex.raw(`
1173
+ SELECT idx.relname AS "indexName", CASE WHEN i.indisunique THEN 0 ELSE 1 END AS "nonUnique",
1174
+ am.amname AS "indexType", a.attname AS "columnName", k.position AS "seqInIndex",
1175
+ i.indexprs IS NOT NULL OR i.indpred IS NOT NULL OR i.indnatts <> i.indnkeyatts
1176
+ OR i.indnullsnotdistinct OR NOT i.indisvalid AS unsupported
1177
+ FROM pg_catalog.pg_index i
1178
+ JOIN pg_catalog.pg_class t ON t.oid = i.indrelid
1179
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace
1180
+ JOIN pg_catalog.pg_class idx ON idx.oid = i.indexrelid
1181
+ JOIN pg_catalog.pg_am am ON am.oid = idx.relam
1182
+ CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(num, position)
1183
+ LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.num
1184
+ WHERE n.nspname = ? AND t.relname = ? AND NOT i.indisprimary
1185
+ ORDER BY idx.relname, k.position
1186
+ `, [schemaName, tableName]))
1187
+ const unsupportedIndex = indexRows.find(row => row.unsupported)
1188
+ if (unsupportedIndex) throw new Error(`Index '${unsupportedIndex.indexName}' cannot be represented by a simple column-index snapshot.`)
1189
+ const unsupportedConstraint = constraints.find(row => row.contype === 'x' || (row.contype === 'f' && row.referenced_schema !== schemaName))
1190
+ if (unsupportedConstraint) throw new Error(`Constraint '${unsupportedConstraint.conname}' cannot be represented by this table snapshot.`)
1191
+
1192
+ const primaryKeyColumns = constraints.find(row => row.contype === 'p')?.columns || []
1193
+ const resolvedIdColumn = requireIdColumn(columns, idColumn)
1194
+ requirePrimaryKeyContainsId(primaryKeyColumns, resolvedIdColumn)
1195
+ const actions = { a: 'NO ACTION', r: 'RESTRICT', c: 'CASCADE', n: 'SET NULL', d: 'SET DEFAULT' }
1196
+ const foreignKeyRows = constraints.filter(row => row.contype === 'f').flatMap(row => row.columns.map((column, index) => ({
1197
+ constraintName: row.conname,
1198
+ columnName: column,
1199
+ referencedTableName: row.referenced_table,
1200
+ referencedColumnName: row.referenced_columns[index],
1201
+ ordinalPosition: index + 1,
1202
+ updateRule: actions[row.confupdtype],
1203
+ deleteRule: actions[row.confdeltype]
1204
+ })))
1205
+ return {
1206
+ dialect: 'pg',
1207
+ schemaName,
1208
+ tableName,
1209
+ tableCollation: '',
1210
+ idColumn: resolvedIdColumn,
1211
+ primaryKeyColumns,
1212
+ hasWorkspaceIdColumn: columns.some(column => column.name === 'workspace_id'),
1213
+ hasUserIdColumn: columns.some(column => column.name === 'user_id'),
1214
+ columns,
1215
+ indexes: normalizeIndexes(indexRows),
1216
+ foreignKeys: normalizeForeignKeys(foreignKeyRows, [], tableName),
1217
+ checkConstraints: normalizeCheckConstraints(constraints.filter(row => row.contype === 'c').map(row => ({ name: row.conname, clause: row.clause })), tableName)
1218
+ }
1219
+ }
1220
+
1221
+ export async function hasKnexTableIndex (knex, tableName, indexName) {
1222
+ const client = knex.client.config.client
1223
+ if (['sqlite3', 'better-sqlite3'].includes(client)) {
1224
+ const rows = normalizeRows(await knex.raw('PRAGMA index_list(??)', [tableName]))
1225
+ return rows.some(row => row.name === indexName)
1226
+ }
1227
+ if (['mysql', 'mysql2'].includes(client)) {
1228
+ return Boolean(await knex('information_schema.statistics').first('INDEX_NAME').where({
1229
+ TABLE_SCHEMA: knex.raw('DATABASE()'), TABLE_NAME: tableName, INDEX_NAME: indexName
1230
+ }))
1231
+ }
1232
+ if (['pg', 'postgresql'].includes(client)) {
1233
+ return Boolean(await knex('pg_indexes').first('indexname').where({
1234
+ schemaname: knex.raw('current_schema()'), tablename: tableName, indexname: indexName
1235
+ }))
1236
+ }
1237
+ throw new Error(`Unsupported knex client '${client}' for hasKnexTableIndex`)
1238
+ }
1239
+
1240
+ // Column-only ALTER must not impose the resource snapshot's ID or index restrictions.
1241
+ export async function introspectKnexColumnConstraints (knex, tableName) {
1242
+ requireKnexRaw(knex)
1243
+ const resolvedTableName = normalizeText(tableName)
1244
+ if (!resolvedTableName) throw new TypeError('Column introspection requires tableName.')
1245
+ const dialect = detectDialect(knex)
1246
+ if (dialect === 'sqlite') {
1247
+ const { columns, parsedConstraints } = await introspectSqliteColumns(knex, resolvedTableName)
1248
+ return { columns, checkConstraints: normalizeCheckConstraints(parsedConstraints.checkConstraints, resolvedTableName) }
1249
+ }
1250
+ if (dialect === 'pg') {
1251
+ const parts = resolvedTableName.split('.')
1252
+ if (parts.length > 2 || parts.some(part => !part)) throw new Error('Expected a table name or schema.table for column introspection.')
1253
+ const { columns, constraints } = await introspectPostgresColumns(knex, parts.at(-1), parts.length === 2 ? parts[0] : undefined)
1254
+ return {
1255
+ columns,
1256
+ checkConstraints: normalizeCheckConstraints(constraints.filter(row => row.contype === 'c').map(row => ({ name: row.conname, clause: row.clause })), resolvedTableName)
1257
+ }
1258
+ }
1259
+ throw new Error('Column check introspection is not required for native MySQL enum alterations.')
1260
+ }
1261
+
1058
1262
  export async function introspectKnexTableSnapshot (knex, { tableName = '', idColumn = 'id' } = {}) {
1059
1263
  requireKnexRaw(knex)
1060
1264
  const resolvedTableName = requireTableName(tableName)
@@ -1073,6 +1277,9 @@ export async function introspectKnexTableSnapshot (knex, { tableName = '', idCol
1073
1277
  idColumn
1074
1278
  })
1075
1279
  }
1280
+ if (dialect === 'pg') {
1281
+ return introspectPostgresTableSnapshot(knex, { tableName: resolvedTableName, idColumn })
1282
+ }
1076
1283
 
1077
1284
  throw new Error(`Unsupported dialect "${dialect}" for introspectKnexTableSnapshot.`)
1078
1285
  }