json-rest-api 1.0.29 → 2.1.0

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