json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- 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
|
|
53
|
+
const hasIndex = await hasKnexTableIndex(knex, tableName, indexName)
|
|
52
54
|
if (!hasIndex) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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', '
|
|
227
|
+
['date', 'string'],
|
|
220
228
|
['datetime', 'date'],
|
|
221
229
|
['dateTime', 'date'],
|
|
222
|
-
['time', '
|
|
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
|
|
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 = /'((?:''
|
|
110
|
+
const pattern = backslashEscapes ? /'((?:''|\\.|[^'\\])*)'/gs : /'((?:''|[^'])*)'/g
|
|
102
111
|
let match = null
|
|
103
112
|
while ((match = pattern.exec(body)) != null) {
|
|
104
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
782
|
-
|
|
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
|
|
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:
|
|
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
|
}
|