json-rest-api 1.0.22 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/README.md +22 -1
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +196 -38
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +111 -5
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +370 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +26 -34
- package/plugins/core/connectors/express-plugin.js +97 -190
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/anyapi/schema-utils.js +43 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1111 -160
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -3
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +21 -76
- package/plugins/core/lib/querying/knex-relationship-includes.js +468 -215
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +20 -5
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -14
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +133 -0
- package/plugins/core/lib/storage/storage-adapter.js +82 -4
- package/plugins/core/lib/storage/storage-mapping.js +274 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
- package/plugins/core/rest-api-anyapi-knex-plugin.js +531 -312
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +271 -226
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +148 -92
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get.js +29 -14
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
- package/plugins/core/rest-api-plugin-methods/patch.js +56 -12
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/post.js +21 -8
- package/plugins/core/rest-api-plugin-methods/put.js +73 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +6 -0
- package/plugins/core/rest-api-positioning-plugin.js +2 -5
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -4
- package/plugins/storage/local-storage.js +1 -1
- package/plugins/storage/s3-storage.js +5 -2
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +7 -0
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +521 -0
- package/tests/db-table-operations.test.js +786 -0
- package/tests/fastify-plugin.test.js +156 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +278 -176
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/id-normalization.test.js +444 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +11 -11
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +259 -0
- package/audit/00-charter.md +0 -103
- package/audit/01-scope-map.md +0 -84
- package/audit/02-coverage-matrix.csv +0 -174
- package/audit/03-invariants.md +0 -123
- package/audit/04-standards-rubric.md +0 -56
- package/audit/05-competitor-rubric.md +0 -77
- package/audit/06-findings.md +0 -160
- package/audit/07-risk-register.csv +0 -10
- package/audit/08-remediation-roadmap.md +0 -177
- package/audit/09-signoff-check.md +0 -23
- package/audit/10-final-synthesis.md +0 -29
- package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
- package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
- package/audit/evidence/code/boundary-leak-checks.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
- package/audit/evidence/code/critical-pass1-summary.csv +0 -67
- package/audit/evidence/code/critical-pass2-summary.csv +0 -67
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
- package/audit/evidence/code/error-model-signals.txt +0 -98
- package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
- package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
- package/audit/evidence/code/hook-flow-signals.txt +0 -282
- package/audit/evidence/code/idproperty-signals.txt +0 -157
- package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
- package/audit/evidence/code/imports-json-rest-api.txt +0 -354
- package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
- package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
- package/audit/evidence/code/permission-signals.txt +0 -193
- package/audit/evidence/code/query-include-signals.txt +0 -687
- package/audit/evidence/code/transaction-signals.txt +0 -28
- package/audit/evidence/code/validation-signals.txt +0 -650
- package/audit/evidence/code/write-path-signals.txt +0 -61
- package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
- package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
- package/audit/evidence/external/corpus-freeze.md +0 -21
- package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
- package/audit/evidence/external/npm-download-signals.txt +0 -23
- package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
- package/audit/evidence/external/readme-fortune-signals.txt +0 -26
- package/audit/evidence/external/readme-fortune.md +0 -165
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
- package/audit/evidence/external/readme-json-api-serializer.md +0 -690
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
- package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
- package/audit/evidence/external/readme-jsonapi-server.md +0 -104
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
- package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
- package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
- package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
- package/audit/evidence/external/standards-sources.txt +0 -6
- package/audit/evidence/runtime/baseline-commits.txt +0 -2
- package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
- package/audit/evidence/runtime/environment.txt +0 -4
- package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
- package/audit/evidence/runtime/test-exitcodes.txt +0 -4
- package/audit/evidence/runtime/test-summary.md +0 -107
- package/audit/evidence/runtime/trace-anchors.txt +0 -61
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
- package/audit/evidence/tests/failing-tests-summary.txt +0 -25
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
- package/audit/evidence/tests/query-include-test-signals.txt +0 -948
- package/audit/evidence/tests/relationship-test-signals.txt +0 -223
- package/audit/evidence/tests/skipped-tests.txt +0 -10
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
- package/audit/evidence/tests/test-suite-list.txt +0 -32
- package/audit/subsystems/docs-drift.md +0 -49
- package/audit/subsystems/pass2-cross-review.md +0 -131
- package/audit/subsystems/plugins-access.md +0 -51
- package/audit/subsystems/plugins-bulk.md +0 -50
- package/audit/subsystems/plugins-cors.md +0 -48
- package/audit/subsystems/plugins-file.md +0 -53
- package/audit/subsystems/plugins-multihome.md +0 -50
- package/audit/subsystems/plugins-positioning.md +0 -54
- package/audit/subsystems/plugins-socketio.md +0 -50
- package/audit/subsystems/query-engine.md +0 -61
- package/audit/subsystems/relationships.md +0 -55
- package/audit/subsystems/rest-orchestration.md +0 -66
- package/audit/subsystems/schema-core.md +0 -64
- package/audit/subsystems/storage-knex.md +0 -57
- package/audit/subsystems/tests.md +0 -48
- package/audit/subsystems/transport-express.md +0 -55
- package/audit/traces/trace-read-get.md +0 -22
- package/audit/traces/trace-read-query.md +0 -25
- package/audit/traces/trace-relationship-endpoints.md +0 -31
- package/audit/traces/trace-write-delete.md +0 -17
- package/audit/traces/trace-write-patch.md +0 -26
- package/audit/traces/trace-write-post.md +0 -22
- package/audit/traces/trace-write-put.md +0 -23
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
const TABLE_NAME_PATTERN = /^[A-Za-z0-9_]+$/
|
|
2
|
+
const MYSQL_BOOLEAN_TINYINT_PATTERN = /^tinyint\(1\)/i
|
|
3
|
+
|
|
4
|
+
function normalizeText (value) {
|
|
5
|
+
if (value == null) return ''
|
|
6
|
+
return String(value).trim()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toCamelCase (value) {
|
|
10
|
+
const source = normalizeText(value)
|
|
11
|
+
if (!source) return ''
|
|
12
|
+
|
|
13
|
+
return source.replace(/[_-]([a-z0-9])/gi, (_match, next) => next.toUpperCase())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requireKnexRaw (knex) {
|
|
17
|
+
if (!knex || typeof knex.raw !== 'function') {
|
|
18
|
+
throw new TypeError('introspectKnexTableSnapshot requires knex with raw().')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireTableName (value) {
|
|
23
|
+
const tableName = normalizeText(value)
|
|
24
|
+
if (!tableName) {
|
|
25
|
+
throw new TypeError('introspectKnexTableSnapshot requires tableName.')
|
|
26
|
+
}
|
|
27
|
+
if (!TABLE_NAME_PATTERN.test(tableName)) {
|
|
28
|
+
throw new Error(`Invalid table name "${tableName}". Use letters, numbers, and underscore only.`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return tableName
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function detectDialect (knex) {
|
|
35
|
+
const clientName = normalizeText(
|
|
36
|
+
knex?.client?.config?.client ||
|
|
37
|
+
knex?.client?.dialect ||
|
|
38
|
+
knex?.client?.driverName
|
|
39
|
+
).toLowerCase()
|
|
40
|
+
|
|
41
|
+
if (clientName.includes('sqlite')) {
|
|
42
|
+
return 'sqlite'
|
|
43
|
+
}
|
|
44
|
+
if (clientName.includes('mysql')) {
|
|
45
|
+
return 'mysql2'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(`Unsupported knex client "${clientName || 'unknown'}" for introspectKnexTableSnapshot.`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeRows (rawResult) {
|
|
52
|
+
if (Array.isArray(rawResult)) {
|
|
53
|
+
if (rawResult.length > 0 && Array.isArray(rawResult[0])) {
|
|
54
|
+
return rawResult[0]
|
|
55
|
+
}
|
|
56
|
+
return rawResult
|
|
57
|
+
}
|
|
58
|
+
if (rawResult && typeof rawResult === 'object' && Array.isArray(rawResult.rows)) {
|
|
59
|
+
return rawResult.rows
|
|
60
|
+
}
|
|
61
|
+
return []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toNullableNumber (value) {
|
|
65
|
+
if (value == null) return null
|
|
66
|
+
const parsed = Number.parseInt(String(value), 10)
|
|
67
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeColumnDefault (value) {
|
|
71
|
+
if (value == null) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
const text = value.trim()
|
|
77
|
+
if (!text || text.toLowerCase() === 'null') {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
if (text.startsWith("'") && text.endsWith("'")) {
|
|
81
|
+
return text.slice(1, -1).replace(/''/g, "'").replace(/\\'/g, "'")
|
|
82
|
+
}
|
|
83
|
+
if (text.startsWith('"') && text.endsWith('"')) {
|
|
84
|
+
return text.slice(1, -1).replace(/""/g, '"')
|
|
85
|
+
}
|
|
86
|
+
return text
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseEnumLikeValues (columnType = '', typeName = 'enum') {
|
|
93
|
+
const source = normalizeText(columnType)
|
|
94
|
+
const normalizedType = `${String(typeName || '').toLowerCase()}(`
|
|
95
|
+
if (!source.toLowerCase().startsWith(normalizedType) || !source.endsWith(')')) {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const body = source.slice(normalizedType.length, -1)
|
|
100
|
+
const values = []
|
|
101
|
+
const pattern = /'((?:''|\\'|[^'])*)'/g
|
|
102
|
+
let match = null
|
|
103
|
+
while ((match = pattern.exec(body)) != null) {
|
|
104
|
+
values.push(match[1].replace(/''/g, "'").replace(/\\'/g, "'"))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return values
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveTypeKind ({ name = '', dataType = '', columnType = '' } = {}) {
|
|
111
|
+
const normalizedType = normalizeText(dataType).toLowerCase()
|
|
112
|
+
const normalizedColumnType = normalizeText(columnType).toLowerCase()
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
normalizedType === 'varchar' ||
|
|
116
|
+
normalizedType === 'char' ||
|
|
117
|
+
normalizedType === 'text' ||
|
|
118
|
+
normalizedType === 'tinytext' ||
|
|
119
|
+
normalizedType === 'mediumtext' ||
|
|
120
|
+
normalizedType === 'longtext' ||
|
|
121
|
+
normalizedType === 'enum' ||
|
|
122
|
+
normalizedType === 'set'
|
|
123
|
+
) {
|
|
124
|
+
return 'string'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
normalizedType === 'int' ||
|
|
129
|
+
normalizedType === 'integer' ||
|
|
130
|
+
normalizedType === 'smallint' ||
|
|
131
|
+
normalizedType === 'mediumint' ||
|
|
132
|
+
normalizedType === 'bigint'
|
|
133
|
+
) {
|
|
134
|
+
return 'integer'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (normalizedType === 'tinyint') {
|
|
138
|
+
if (MYSQL_BOOLEAN_TINYINT_PATTERN.test(normalizedColumnType)) {
|
|
139
|
+
return 'boolean'
|
|
140
|
+
}
|
|
141
|
+
return 'integer'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
normalizedType === 'decimal' ||
|
|
146
|
+
normalizedType === 'numeric' ||
|
|
147
|
+
normalizedType === 'float' ||
|
|
148
|
+
normalizedType === 'double' ||
|
|
149
|
+
normalizedType === 'real'
|
|
150
|
+
) {
|
|
151
|
+
return 'number'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (normalizedType === 'boolean' || normalizedType === 'bool') {
|
|
155
|
+
return 'boolean'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (normalizedType === 'datetime' || normalizedType === 'timestamp') {
|
|
159
|
+
return 'datetime'
|
|
160
|
+
}
|
|
161
|
+
if (normalizedType === 'date') {
|
|
162
|
+
return 'date'
|
|
163
|
+
}
|
|
164
|
+
if (normalizedType === 'time') {
|
|
165
|
+
return 'time'
|
|
166
|
+
}
|
|
167
|
+
if (normalizedType === 'json') {
|
|
168
|
+
return 'json'
|
|
169
|
+
}
|
|
170
|
+
if (
|
|
171
|
+
normalizedType === 'blob' ||
|
|
172
|
+
normalizedType === 'binary' ||
|
|
173
|
+
normalizedType === 'varbinary'
|
|
174
|
+
) {
|
|
175
|
+
return 'binary'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new Error(`Unsupported column type "${normalizedType}" for column "${name}".`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildColumnSnapshot ({
|
|
182
|
+
name = '',
|
|
183
|
+
dataType = '',
|
|
184
|
+
columnType = '',
|
|
185
|
+
nullable = false,
|
|
186
|
+
defaultValue = null,
|
|
187
|
+
extra = '',
|
|
188
|
+
autoIncrement = false,
|
|
189
|
+
unsigned = false,
|
|
190
|
+
maxLength = null,
|
|
191
|
+
numericPrecision = null,
|
|
192
|
+
numericScale = null,
|
|
193
|
+
datetimePrecision = null,
|
|
194
|
+
characterSetName = '',
|
|
195
|
+
collationName = '',
|
|
196
|
+
ordinalPosition = null,
|
|
197
|
+
enumValues = null,
|
|
198
|
+
setValues = null
|
|
199
|
+
} = {}) {
|
|
200
|
+
const normalizedName = normalizeText(name)
|
|
201
|
+
const normalizedDataType = normalizeText(dataType).toLowerCase()
|
|
202
|
+
const normalizedColumnType = normalizeText(columnType)
|
|
203
|
+
const normalizedDefaultValue = normalizeColumnDefault(defaultValue)
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
name: normalizedName,
|
|
207
|
+
key: toCamelCase(normalizedName),
|
|
208
|
+
dataType: normalizedDataType,
|
|
209
|
+
columnType: normalizedColumnType,
|
|
210
|
+
extra: normalizeText(extra).toLowerCase(),
|
|
211
|
+
typeKind: resolveTypeKind({
|
|
212
|
+
name: normalizedName,
|
|
213
|
+
dataType: normalizedDataType,
|
|
214
|
+
columnType: normalizedColumnType
|
|
215
|
+
}),
|
|
216
|
+
nullable: Boolean(nullable),
|
|
217
|
+
defaultValue: normalizedDefaultValue,
|
|
218
|
+
hasDefault: normalizedDefaultValue != null,
|
|
219
|
+
autoIncrement: Boolean(autoIncrement),
|
|
220
|
+
unsigned: Boolean(unsigned),
|
|
221
|
+
maxLength: toNullableNumber(maxLength),
|
|
222
|
+
numericPrecision: toNullableNumber(numericPrecision),
|
|
223
|
+
numericScale: toNullableNumber(numericScale),
|
|
224
|
+
datetimePrecision: toNullableNumber(datetimePrecision),
|
|
225
|
+
characterSetName: normalizeText(characterSetName),
|
|
226
|
+
collationName: normalizeText(collationName),
|
|
227
|
+
ordinalPosition: toNullableNumber(ordinalPosition),
|
|
228
|
+
enumValues: Array.isArray(enumValues) ? enumValues : parseEnumLikeValues(normalizedColumnType, 'enum'),
|
|
229
|
+
setValues: Array.isArray(setValues) ? setValues : parseEnumLikeValues(normalizedColumnType, 'set')
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizePrimaryKeyColumns (rows = []) {
|
|
234
|
+
return rows
|
|
235
|
+
.map((row) => normalizeText(row.columnName || row.column_name || row.name))
|
|
236
|
+
.filter(Boolean)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function normalizeIndexes (rows = []) {
|
|
240
|
+
const byName = new Map()
|
|
241
|
+
|
|
242
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
243
|
+
const indexName = normalizeText(row.indexName || row.index_name || row.name)
|
|
244
|
+
const columnName = normalizeText(row.columnName || row.column_name || row.name)
|
|
245
|
+
if (!indexName || !columnName) {
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const seqInIndex = toNullableNumber(row.seqInIndex ?? row.seq_in_index ?? row.seqno) || 0
|
|
250
|
+
const nonUnique = Number(row.nonUnique ?? row.non_unique) === 1
|
|
251
|
+
const existing = byName.get(indexName) || {
|
|
252
|
+
name: indexName,
|
|
253
|
+
unique: !nonUnique,
|
|
254
|
+
indexType: normalizeText(row.indexType || row.index_type).toUpperCase(),
|
|
255
|
+
columns: []
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
existing.columns.push({
|
|
259
|
+
name: columnName,
|
|
260
|
+
order: seqInIndex
|
|
261
|
+
})
|
|
262
|
+
byName.set(indexName, existing)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return [...byName.values()]
|
|
266
|
+
.map((index) => ({
|
|
267
|
+
name: index.name,
|
|
268
|
+
unique: index.unique,
|
|
269
|
+
indexType: index.indexType,
|
|
270
|
+
columns: index.columns
|
|
271
|
+
.sort((left, right) => left.order - right.order)
|
|
272
|
+
.map((column) => column.name)
|
|
273
|
+
}))
|
|
274
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeForeignKeys (rows = [], parsedForeignKeys = [], tableName = '') {
|
|
278
|
+
const grouped = new Map()
|
|
279
|
+
|
|
280
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
281
|
+
const groupId = normalizeText(row.groupId ?? row.id ?? row.constraintName ?? row.constraint_name)
|
|
282
|
+
const columnName = normalizeText(row.columnName || row.column_name || row.from)
|
|
283
|
+
const referencedTableName = normalizeText(row.referencedTableName || row.referenced_table_name || row.table)
|
|
284
|
+
const referencedColumnName = normalizeText(row.referencedColumnName || row.referenced_column_name || row.to)
|
|
285
|
+
if (!groupId || !columnName || !referencedTableName || !referencedColumnName) {
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const ordinalPosition = toNullableNumber(row.ordinalPosition ?? row.ordinal_position ?? row.seq) || 0
|
|
290
|
+
const existing = grouped.get(groupId) || {
|
|
291
|
+
groupId,
|
|
292
|
+
name: normalizeText(row.constraintName || row.constraint_name),
|
|
293
|
+
referencedTableName,
|
|
294
|
+
updateRule: normalizeText(row.updateRule || row.update_rule || row.on_update).toUpperCase(),
|
|
295
|
+
deleteRule: normalizeText(row.deleteRule || row.delete_rule || row.on_delete).toUpperCase(),
|
|
296
|
+
columns: []
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
existing.columns.push({
|
|
300
|
+
name: columnName,
|
|
301
|
+
referencedName: referencedColumnName,
|
|
302
|
+
order: ordinalPosition
|
|
303
|
+
})
|
|
304
|
+
grouped.set(groupId, existing)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const entries = [...grouped.values()]
|
|
308
|
+
.sort((left, right) => String(left.groupId).localeCompare(String(right.groupId), undefined, { numeric: true }))
|
|
309
|
+
.map((foreignKey, index) => {
|
|
310
|
+
const normalizedColumns = foreignKey.columns
|
|
311
|
+
.sort((left, right) => left.order - right.order)
|
|
312
|
+
.map((column) => ({
|
|
313
|
+
name: column.name,
|
|
314
|
+
referencedName: column.referencedName
|
|
315
|
+
}))
|
|
316
|
+
|
|
317
|
+
const parsedMatch = parsedForeignKeys.find((parsed, parsedIndex) => {
|
|
318
|
+
if (parsed._used) return false
|
|
319
|
+
if (normalizeText(parsed.referencedTableName) !== foreignKey.referencedTableName) return false
|
|
320
|
+
if ((parsed.columns || []).length !== normalizedColumns.length) return false
|
|
321
|
+
|
|
322
|
+
return parsed.columns.every((column, columnIndex) => {
|
|
323
|
+
const target = normalizedColumns[columnIndex]
|
|
324
|
+
return target &&
|
|
325
|
+
normalizeText(column.name) === target.name &&
|
|
326
|
+
normalizeText(column.referencedName) === target.referencedName
|
|
327
|
+
})
|
|
328
|
+
}) || parsedForeignKeys[index] || null
|
|
329
|
+
|
|
330
|
+
if (parsedMatch) {
|
|
331
|
+
parsedMatch._used = true
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
name: normalizeText(parsedMatch?.name) || normalizeText(foreignKey.name) || `${tableName}_foreign_${index + 1}`,
|
|
336
|
+
referencedTableName: foreignKey.referencedTableName,
|
|
337
|
+
updateRule: foreignKey.updateRule,
|
|
338
|
+
deleteRule: foreignKey.deleteRule,
|
|
339
|
+
columns: normalizedColumns
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
return entries.sort((left, right) => left.name.localeCompare(right.name))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeCheckConstraints (constraints = [], tableName = '') {
|
|
347
|
+
return constraints
|
|
348
|
+
.map((constraint, index) => {
|
|
349
|
+
const name = normalizeText(constraint.name)
|
|
350
|
+
const clause = normalizeText(constraint.clause)
|
|
351
|
+
if (!clause) return null
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
name: name || `${tableName}_check_${index + 1}`,
|
|
355
|
+
clause
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function requireIdColumn (columns, idColumn) {
|
|
363
|
+
const normalizedIdColumn = normalizeText(idColumn) || 'id'
|
|
364
|
+
const idSpec = columns.find((column) => column.name === normalizedIdColumn) || null
|
|
365
|
+
if (!idSpec) {
|
|
366
|
+
throw new Error(`Could not find id column "${normalizedIdColumn}" in table.`)
|
|
367
|
+
}
|
|
368
|
+
if (idSpec.typeKind !== 'integer') {
|
|
369
|
+
throw new Error(`Id column "${normalizedIdColumn}" must use an integer type.`)
|
|
370
|
+
}
|
|
371
|
+
if (idSpec.nullable) {
|
|
372
|
+
throw new Error(`Id column "${normalizedIdColumn}" must be not-null.`)
|
|
373
|
+
}
|
|
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
|
+
return normalizedIdColumn
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function requirePrimaryKeyContainsId (primaryKeyColumns, idColumn) {
|
|
382
|
+
if (!Array.isArray(primaryKeyColumns) || !primaryKeyColumns.includes(idColumn)) {
|
|
383
|
+
throw new Error(`Primary key must include id column "${idColumn}".`)
|
|
384
|
+
}
|
|
385
|
+
if (primaryKeyColumns.length !== 1 || primaryKeyColumns[0] !== idColumn) {
|
|
386
|
+
throw new Error(`Composite primary keys are not supported. Primary key must be only "${idColumn}".`)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function quoteSqliteIdentifier (identifier) {
|
|
391
|
+
return `"${String(identifier || '').replace(/"/g, '""')}"`
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function extractParenthesizedContent (source = '', openIndex = -1) {
|
|
395
|
+
if (openIndex < 0 || source[openIndex] !== '(') {
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let depth = 0
|
|
400
|
+
let inSingleQuote = false
|
|
401
|
+
let inDoubleQuote = false
|
|
402
|
+
let inBacktick = false
|
|
403
|
+
|
|
404
|
+
for (let index = openIndex; index < source.length; index += 1) {
|
|
405
|
+
const char = source[index]
|
|
406
|
+
const next = source[index + 1]
|
|
407
|
+
|
|
408
|
+
if (inSingleQuote) {
|
|
409
|
+
if (char === "'" && next === "'") {
|
|
410
|
+
index += 1
|
|
411
|
+
continue
|
|
412
|
+
}
|
|
413
|
+
if (char === "'") inSingleQuote = false
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
if (inDoubleQuote) {
|
|
417
|
+
if (char === '"' && next === '"') {
|
|
418
|
+
index += 1
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
if (char === '"') inDoubleQuote = false
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
if (inBacktick) {
|
|
425
|
+
if (char === '`') inBacktick = false
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (char === "'") {
|
|
430
|
+
inSingleQuote = true
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
if (char === '"') {
|
|
434
|
+
inDoubleQuote = true
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
if (char === '`') {
|
|
438
|
+
inBacktick = true
|
|
439
|
+
continue
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (char === '(') {
|
|
443
|
+
depth += 1
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
if (char === ')') {
|
|
447
|
+
depth -= 1
|
|
448
|
+
if (depth === 0) {
|
|
449
|
+
return {
|
|
450
|
+
content: source.slice(openIndex + 1, index),
|
|
451
|
+
endIndex: index
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return null
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function splitTopLevelSqlList (source = '') {
|
|
461
|
+
const entries = []
|
|
462
|
+
let buffer = ''
|
|
463
|
+
let depth = 0
|
|
464
|
+
let inSingleQuote = false
|
|
465
|
+
let inDoubleQuote = false
|
|
466
|
+
let inBacktick = false
|
|
467
|
+
|
|
468
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
469
|
+
const char = source[index]
|
|
470
|
+
const next = source[index + 1]
|
|
471
|
+
|
|
472
|
+
if (inSingleQuote) {
|
|
473
|
+
buffer += char
|
|
474
|
+
if (char === "'" && next === "'") {
|
|
475
|
+
buffer += next
|
|
476
|
+
index += 1
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
if (char === "'") inSingleQuote = false
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
if (inDoubleQuote) {
|
|
483
|
+
buffer += char
|
|
484
|
+
if (char === '"' && next === '"') {
|
|
485
|
+
buffer += next
|
|
486
|
+
index += 1
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
489
|
+
if (char === '"') inDoubleQuote = false
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
492
|
+
if (inBacktick) {
|
|
493
|
+
buffer += char
|
|
494
|
+
if (char === '`') inBacktick = false
|
|
495
|
+
continue
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (char === "'") {
|
|
499
|
+
inSingleQuote = true
|
|
500
|
+
buffer += char
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
if (char === '"') {
|
|
504
|
+
inDoubleQuote = true
|
|
505
|
+
buffer += char
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
if (char === '`') {
|
|
509
|
+
inBacktick = true
|
|
510
|
+
buffer += char
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (char === '(') {
|
|
515
|
+
depth += 1
|
|
516
|
+
buffer += char
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
519
|
+
if (char === ')') {
|
|
520
|
+
depth -= 1
|
|
521
|
+
buffer += char
|
|
522
|
+
continue
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (char === ',' && depth === 0) {
|
|
526
|
+
const entry = buffer.trim()
|
|
527
|
+
if (entry) entries.push(entry)
|
|
528
|
+
buffer = ''
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
buffer += char
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const finalEntry = buffer.trim()
|
|
536
|
+
if (finalEntry) entries.push(finalEntry)
|
|
537
|
+
|
|
538
|
+
return entries
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function extractCreateTableEntries (tableSql = '') {
|
|
542
|
+
const source = normalizeText(tableSql)
|
|
543
|
+
const openIndex = source.indexOf('(')
|
|
544
|
+
if (openIndex < 0) return []
|
|
545
|
+
|
|
546
|
+
const body = extractParenthesizedContent(source, openIndex)
|
|
547
|
+
if (!body?.content) return []
|
|
548
|
+
|
|
549
|
+
return splitTopLevelSqlList(body.content)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function unquoteSqlIdentifier (value = '') {
|
|
553
|
+
const source = normalizeText(value)
|
|
554
|
+
if (!source) return ''
|
|
555
|
+
if (
|
|
556
|
+
(source.startsWith('"') && source.endsWith('"')) ||
|
|
557
|
+
(source.startsWith('`') && source.endsWith('`')) ||
|
|
558
|
+
(source.startsWith('[') && source.endsWith(']'))
|
|
559
|
+
) {
|
|
560
|
+
return source.slice(1, -1)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return source
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function parseIdentifierList (source = '') {
|
|
567
|
+
return splitTopLevelSqlList(source)
|
|
568
|
+
.map((entry) => unquoteSqlIdentifier(entry))
|
|
569
|
+
.filter(Boolean)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function readLeadingIdentifier (entry = '') {
|
|
573
|
+
const match = String(entry).match(/^\s*("([^"]|"")*"|`[^`]+`|\[[^\]]+\]|[A-Za-z0-9_]+)/)
|
|
574
|
+
if (!match) return ''
|
|
575
|
+
return unquoteSqlIdentifier(match[1])
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function extractConstraintName (entry = '') {
|
|
579
|
+
const match = String(entry).match(/^\s*CONSTRAINT\s+("([^"]|"")*"|`[^`]+`|\[[^\]]+\]|[A-Za-z0-9_]+)/i)
|
|
580
|
+
if (!match) return ''
|
|
581
|
+
return unquoteSqlIdentifier(match[1])
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function extractFkAction (entry = '', action = 'UPDATE') {
|
|
585
|
+
const pattern = new RegExp(`ON\\s+${action}\\s+(SET\\s+NULL|SET\\s+DEFAULT|NO\\s+ACTION|CASCADE|RESTRICT)`, 'i')
|
|
586
|
+
const match = String(entry).match(pattern)
|
|
587
|
+
return match ? normalizeText(match[1]).replace(/\s+/g, ' ').toUpperCase() : ''
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function parseSqliteForeignKeyEntry (entry = '') {
|
|
591
|
+
const constraintName = extractConstraintName(entry)
|
|
592
|
+
const trimmed = String(entry).trim()
|
|
593
|
+
const working = constraintName
|
|
594
|
+
? trimmed.replace(/^\s*CONSTRAINT\s+("([^"]|"")*"|`[^`]+`|\[[^\]]+\]|[A-Za-z0-9_]+)\s+/i, '')
|
|
595
|
+
: trimmed
|
|
596
|
+
|
|
597
|
+
const upper = working.toUpperCase()
|
|
598
|
+
|
|
599
|
+
if (upper.startsWith('FOREIGN KEY')) {
|
|
600
|
+
const columnsStart = working.indexOf('(')
|
|
601
|
+
const sourceColumns = extractParenthesizedContent(working, columnsStart)
|
|
602
|
+
if (!sourceColumns) return null
|
|
603
|
+
|
|
604
|
+
const afterColumns = working.slice(sourceColumns.endIndex + 1)
|
|
605
|
+
const refMatch = afterColumns.match(/REFERENCES\s+("([^"]|"")*"|`[^`]+`|\[[^\]]+\]|[A-Za-z0-9_]+)\s*/i)
|
|
606
|
+
if (!refMatch) return null
|
|
607
|
+
|
|
608
|
+
const referencedTableName = unquoteSqlIdentifier(refMatch[1])
|
|
609
|
+
const refOpenIndex = afterColumns.indexOf('(', refMatch.index + refMatch[0].length - 1)
|
|
610
|
+
const referencedColumns = extractParenthesizedContent(afterColumns, refOpenIndex)
|
|
611
|
+
if (!referencedColumns) return null
|
|
612
|
+
|
|
613
|
+
const columns = parseIdentifierList(sourceColumns.content)
|
|
614
|
+
const referencedNames = parseIdentifierList(referencedColumns.content)
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
name: constraintName,
|
|
618
|
+
referencedTableName,
|
|
619
|
+
updateRule: extractFkAction(working, 'UPDATE'),
|
|
620
|
+
deleteRule: extractFkAction(working, 'DELETE'),
|
|
621
|
+
columns: columns.map((columnName, index) => ({
|
|
622
|
+
name: columnName,
|
|
623
|
+
referencedName: referencedNames[index] || ''
|
|
624
|
+
}))
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (!/\bREFERENCES\b/i.test(working)) {
|
|
629
|
+
return null
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const columnName = readLeadingIdentifier(working)
|
|
633
|
+
const refMatch = working.match(/REFERENCES\s+("([^"]|"")*"|`[^`]+`|\[[^\]]+\]|[A-Za-z0-9_]+)\s*/i)
|
|
634
|
+
if (!columnName || !refMatch) return null
|
|
635
|
+
|
|
636
|
+
const referencedTableName = unquoteSqlIdentifier(refMatch[1])
|
|
637
|
+
const refOpenIndex = working.indexOf('(', refMatch.index + refMatch[0].length - 1)
|
|
638
|
+
const referencedColumns = extractParenthesizedContent(working, refOpenIndex)
|
|
639
|
+
const referencedNames = referencedColumns ? parseIdentifierList(referencedColumns.content) : ['id']
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
name: constraintName,
|
|
643
|
+
referencedTableName,
|
|
644
|
+
updateRule: extractFkAction(working, 'UPDATE'),
|
|
645
|
+
deleteRule: extractFkAction(working, 'DELETE'),
|
|
646
|
+
columns: [{
|
|
647
|
+
name: columnName,
|
|
648
|
+
referencedName: referencedNames[0] || 'id'
|
|
649
|
+
}]
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function parseSqliteCheckEntry (entry = '') {
|
|
654
|
+
const checkIndex = entry.toUpperCase().indexOf('CHECK')
|
|
655
|
+
if (checkIndex < 0) return null
|
|
656
|
+
|
|
657
|
+
const openIndex = entry.indexOf('(', checkIndex)
|
|
658
|
+
const clause = extractParenthesizedContent(entry, openIndex)
|
|
659
|
+
if (!clause?.content) return null
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
name: extractConstraintName(entry),
|
|
663
|
+
clause: clause.content.trim()
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function parseSqliteInlineEnumValues (entry = '', columnName = '') {
|
|
668
|
+
const normalizedColumnName = normalizeText(columnName)
|
|
669
|
+
if (!normalizedColumnName) return []
|
|
670
|
+
|
|
671
|
+
const escapedColumnName = normalizedColumnName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
672
|
+
const pattern = new RegExp(
|
|
673
|
+
`check\\s*\\(\\s*(?:"${escapedColumnName}"|\`${escapedColumnName}\`|\\[${escapedColumnName}\\]|${escapedColumnName})\\s+in\\s*\\(([^)]*)\\)\\s*\\)`,
|
|
674
|
+
'i'
|
|
675
|
+
)
|
|
676
|
+
const match = String(entry).match(pattern)
|
|
677
|
+
if (!match?.[1]) {
|
|
678
|
+
return []
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return parseEnumLikeValues(`enum(${match[1]})`, 'enum')
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function parseSqliteConstraintMetadata (entries = []) {
|
|
685
|
+
const foreignKeys = []
|
|
686
|
+
const checks = []
|
|
687
|
+
|
|
688
|
+
for (const entry of entries) {
|
|
689
|
+
const foreignKey = parseSqliteForeignKeyEntry(entry)
|
|
690
|
+
if (foreignKey) {
|
|
691
|
+
foreignKeys.push(foreignKey)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const checkConstraint = parseSqliteCheckEntry(entry)
|
|
695
|
+
if (checkConstraint) {
|
|
696
|
+
checks.push(checkConstraint)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
foreignKeys,
|
|
702
|
+
checkConstraints: checks
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function parseSqliteDeclaredType (columnType = '') {
|
|
707
|
+
const source = normalizeText(columnType)
|
|
708
|
+
const match = source.match(/^([A-Za-z0-9_]+)\s*(?:\((.*)\))?/i)
|
|
709
|
+
return {
|
|
710
|
+
dataType: normalizeText(match?.[1]).toLowerCase(),
|
|
711
|
+
args: normalizeText(match?.[2]),
|
|
712
|
+
columnType: source,
|
|
713
|
+
unsigned: /\bunsigned\b/i.test(source)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function deriveSqliteColumnNumbers (dataType, args = '') {
|
|
718
|
+
if (!args) {
|
|
719
|
+
return {
|
|
720
|
+
maxLength: null,
|
|
721
|
+
numericPrecision: null,
|
|
722
|
+
numericScale: null,
|
|
723
|
+
datetimePrecision: null
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const parts = args.split(',').map((entry) => normalizeText(entry))
|
|
728
|
+
if (dataType === 'varchar' || dataType === 'char') {
|
|
729
|
+
return {
|
|
730
|
+
maxLength: toNullableNumber(parts[0]),
|
|
731
|
+
numericPrecision: null,
|
|
732
|
+
numericScale: null,
|
|
733
|
+
datetimePrecision: null
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (dataType === 'decimal' || dataType === 'numeric') {
|
|
738
|
+
return {
|
|
739
|
+
maxLength: null,
|
|
740
|
+
numericPrecision: toNullableNumber(parts[0]),
|
|
741
|
+
numericScale: toNullableNumber(parts[1]),
|
|
742
|
+
datetimePrecision: null
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (dataType === 'datetime' || dataType === 'timestamp' || dataType === 'time') {
|
|
747
|
+
return {
|
|
748
|
+
maxLength: null,
|
|
749
|
+
numericPrecision: null,
|
|
750
|
+
numericScale: null,
|
|
751
|
+
datetimePrecision: toNullableNumber(parts[0])
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
maxLength: null,
|
|
757
|
+
numericPrecision: null,
|
|
758
|
+
numericScale: null,
|
|
759
|
+
datetimePrecision: null
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function introspectSqliteTableSnapshot (knex, { tableName, idColumn }) {
|
|
764
|
+
const quotedTableName = quoteSqliteIdentifier(tableName)
|
|
765
|
+
const tableRows = normalizeRows(await knex.raw(
|
|
766
|
+
'SELECT name, sql FROM sqlite_master WHERE type = \'table\' AND name = ? LIMIT 1',
|
|
767
|
+
[tableName]
|
|
768
|
+
))
|
|
769
|
+
const firstTableRow = Array.isArray(tableRows) ? tableRows[0] : null
|
|
770
|
+
if (!firstTableRow?.sql) {
|
|
771
|
+
throw new Error(`Could not introspect table "${tableName}".`)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const tableSql = String(firstTableRow.sql)
|
|
775
|
+
const tableEntries = extractCreateTableEntries(tableSql)
|
|
776
|
+
const parsedConstraints = parseSqliteConstraintMetadata(tableEntries)
|
|
777
|
+
const tableEntriesByLeadingIdentifier = new Map(
|
|
778
|
+
tableEntries.map((entry) => [readLeadingIdentifier(entry), entry])
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
const columnRows = normalizeRows(await knex.raw(`PRAGMA table_xinfo(${quotedTableName})`))
|
|
782
|
+
.filter((row) => Number(row.hidden || 0) === 0)
|
|
783
|
+
if (columnRows.length < 1) {
|
|
784
|
+
throw new Error(`Could not introspect table "${tableName}".`)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const columns = columnRows
|
|
788
|
+
.sort((left, right) => Number(left.cid) - Number(right.cid))
|
|
789
|
+
.map((row) => {
|
|
790
|
+
const declared = parseSqliteDeclaredType(row.type)
|
|
791
|
+
const derived = deriveSqliteColumnNumbers(declared.dataType, declared.args)
|
|
792
|
+
const upperEntry = tableEntriesByLeadingIdentifier.get(normalizeText(row.name)) || ''
|
|
793
|
+
const inlineEnumValues = declared.dataType === 'text'
|
|
794
|
+
? parseSqliteInlineEnumValues(upperEntry, row.name)
|
|
795
|
+
: []
|
|
796
|
+
const autoIncrement = Number(row.pk) > 0 &&
|
|
797
|
+
declared.dataType === 'integer' &&
|
|
798
|
+
/\bPRIMARY\s+KEY\b/i.test(upperEntry)
|
|
799
|
+
|
|
800
|
+
return buildColumnSnapshot({
|
|
801
|
+
name: row.name,
|
|
802
|
+
dataType: declared.dataType,
|
|
803
|
+
columnType: declared.columnType,
|
|
804
|
+
nullable: Number(row.pk) > 0 ? false : Number(row.notnull) !== 1,
|
|
805
|
+
defaultValue: row.dflt_value,
|
|
806
|
+
extra: autoIncrement ? 'auto_increment' : '',
|
|
807
|
+
autoIncrement,
|
|
808
|
+
unsigned: declared.unsigned,
|
|
809
|
+
maxLength: derived.maxLength,
|
|
810
|
+
numericPrecision: derived.numericPrecision,
|
|
811
|
+
numericScale: derived.numericScale,
|
|
812
|
+
datetimePrecision: derived.datetimePrecision,
|
|
813
|
+
ordinalPosition: Number(row.cid) + 1,
|
|
814
|
+
enumValues: inlineEnumValues
|
|
815
|
+
})
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
const primaryKeyColumns = columnRows
|
|
819
|
+
.filter((row) => Number(row.pk) > 0)
|
|
820
|
+
.sort((left, right) => Number(left.pk) - Number(right.pk))
|
|
821
|
+
.map((row) => normalizeText(row.name))
|
|
822
|
+
|
|
823
|
+
const indexListRows = normalizeRows(await knex.raw(`PRAGMA index_list(${quotedTableName})`))
|
|
824
|
+
const indexRows = []
|
|
825
|
+
for (const row of indexListRows) {
|
|
826
|
+
if (normalizeText(row.origin).toLowerCase() === 'pk') {
|
|
827
|
+
continue
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const indexName = normalizeText(row.name)
|
|
831
|
+
const quotedIndexName = quoteSqliteIdentifier(indexName)
|
|
832
|
+
const columnsForIndex = normalizeRows(await knex.raw(`PRAGMA index_info(${quotedIndexName})`))
|
|
833
|
+
|
|
834
|
+
for (const columnRow of columnsForIndex) {
|
|
835
|
+
indexRows.push({
|
|
836
|
+
indexName,
|
|
837
|
+
nonUnique: Number(row.unique) === 1 ? 0 : 1,
|
|
838
|
+
indexType: '',
|
|
839
|
+
columnName: normalizeText(columnRow.name),
|
|
840
|
+
seqInIndex: Number(columnRow.seqno) + 1
|
|
841
|
+
})
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const foreignKeyRows = normalizeRows(await knex.raw(`PRAGMA foreign_key_list(${quotedTableName})`))
|
|
846
|
+
.map((row) => ({
|
|
847
|
+
groupId: row.id,
|
|
848
|
+
columnName: row.from,
|
|
849
|
+
referencedTableName: row.table,
|
|
850
|
+
referencedColumnName: row.to,
|
|
851
|
+
ordinalPosition: Number(row.seq) + 1,
|
|
852
|
+
updateRule: row.on_update,
|
|
853
|
+
deleteRule: row.on_delete
|
|
854
|
+
}))
|
|
855
|
+
|
|
856
|
+
const resolvedIdColumn = requireIdColumn(columns, idColumn)
|
|
857
|
+
requirePrimaryKeyContainsId(primaryKeyColumns, resolvedIdColumn)
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
dialect: 'sqlite',
|
|
861
|
+
schemaName: 'main',
|
|
862
|
+
tableName,
|
|
863
|
+
tableCollation: '',
|
|
864
|
+
idColumn: resolvedIdColumn,
|
|
865
|
+
primaryKeyColumns,
|
|
866
|
+
hasWorkspaceIdColumn: columns.some((column) => column.name === 'workspace_id'),
|
|
867
|
+
hasUserIdColumn: columns.some((column) => column.name === 'user_id'),
|
|
868
|
+
columns,
|
|
869
|
+
indexes: normalizeIndexes(indexRows),
|
|
870
|
+
foreignKeys: normalizeForeignKeys(foreignKeyRows, parsedConstraints.foreignKeys, tableName),
|
|
871
|
+
checkConstraints: normalizeCheckConstraints(parsedConstraints.checkConstraints, tableName)
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function normalizeDbSchemaName (rows = []) {
|
|
876
|
+
const firstRow = Array.isArray(rows) ? rows[0] : null
|
|
877
|
+
const schemaName = normalizeText(firstRow?.schemaName || firstRow?.schema_name)
|
|
878
|
+
if (!schemaName) {
|
|
879
|
+
throw new Error('Could not resolve current database schema name.')
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return schemaName
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function introspectMysqlTableSnapshot (knex, { tableName, idColumn }) {
|
|
886
|
+
const schemaRows = normalizeRows(await knex.raw('SELECT DATABASE() AS schemaName'))
|
|
887
|
+
const schemaName = normalizeDbSchemaName(schemaRows)
|
|
888
|
+
|
|
889
|
+
const tableRows = normalizeRows(
|
|
890
|
+
await knex.raw(
|
|
891
|
+
`
|
|
892
|
+
SELECT
|
|
893
|
+
t.table_collation AS tableCollation
|
|
894
|
+
FROM information_schema.tables t
|
|
895
|
+
WHERE t.table_schema = ?
|
|
896
|
+
AND t.table_name = ?
|
|
897
|
+
LIMIT 1
|
|
898
|
+
`,
|
|
899
|
+
[schemaName, tableName]
|
|
900
|
+
)
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
const columnRows = normalizeRows(
|
|
904
|
+
await knex.raw(
|
|
905
|
+
`
|
|
906
|
+
SELECT
|
|
907
|
+
c.column_name AS columnName,
|
|
908
|
+
c.data_type AS dataType,
|
|
909
|
+
c.column_type AS columnType,
|
|
910
|
+
c.is_nullable AS isNullable,
|
|
911
|
+
c.column_default AS columnDefault,
|
|
912
|
+
c.extra AS extra,
|
|
913
|
+
c.character_maximum_length AS characterMaximumLength,
|
|
914
|
+
c.character_set_name AS characterSetName,
|
|
915
|
+
c.collation_name AS collationName,
|
|
916
|
+
c.numeric_precision AS numericPrecision,
|
|
917
|
+
c.numeric_scale AS numericScale,
|
|
918
|
+
c.datetime_precision AS datetimePrecision,
|
|
919
|
+
c.ordinal_position AS ordinalPosition
|
|
920
|
+
FROM information_schema.columns c
|
|
921
|
+
WHERE c.table_schema = ?
|
|
922
|
+
AND c.table_name = ?
|
|
923
|
+
ORDER BY c.ordinal_position ASC
|
|
924
|
+
`,
|
|
925
|
+
[schemaName, tableName]
|
|
926
|
+
)
|
|
927
|
+
)
|
|
928
|
+
if (columnRows.length < 1) {
|
|
929
|
+
throw new Error(`Could not introspect table "${tableName}" in schema "${schemaName}".`)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const primaryRows = normalizeRows(
|
|
933
|
+
await knex.raw(
|
|
934
|
+
`
|
|
935
|
+
SELECT
|
|
936
|
+
k.column_name AS columnName,
|
|
937
|
+
k.ordinal_position AS ordinalPosition
|
|
938
|
+
FROM information_schema.table_constraints t
|
|
939
|
+
JOIN information_schema.key_column_usage k
|
|
940
|
+
ON k.constraint_name = t.constraint_name
|
|
941
|
+
AND k.table_schema = t.table_schema
|
|
942
|
+
AND k.table_name = t.table_name
|
|
943
|
+
WHERE t.table_schema = ?
|
|
944
|
+
AND t.table_name = ?
|
|
945
|
+
AND t.constraint_type = 'PRIMARY KEY'
|
|
946
|
+
ORDER BY k.ordinal_position ASC
|
|
947
|
+
`,
|
|
948
|
+
[schemaName, tableName]
|
|
949
|
+
)
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
const indexRows = normalizeRows(
|
|
953
|
+
await knex.raw(
|
|
954
|
+
`
|
|
955
|
+
SELECT
|
|
956
|
+
s.index_name AS indexName,
|
|
957
|
+
s.non_unique AS nonUnique,
|
|
958
|
+
s.index_type AS indexType,
|
|
959
|
+
s.column_name AS columnName,
|
|
960
|
+
s.seq_in_index AS seqInIndex
|
|
961
|
+
FROM information_schema.statistics s
|
|
962
|
+
WHERE s.table_schema = ?
|
|
963
|
+
AND s.table_name = ?
|
|
964
|
+
AND s.index_name <> 'PRIMARY'
|
|
965
|
+
ORDER BY s.index_name ASC, s.seq_in_index ASC
|
|
966
|
+
`,
|
|
967
|
+
[schemaName, tableName]
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
const foreignKeyRows = normalizeRows(
|
|
972
|
+
await knex.raw(
|
|
973
|
+
`
|
|
974
|
+
SELECT
|
|
975
|
+
rc.constraint_name AS constraintName,
|
|
976
|
+
k.column_name AS columnName,
|
|
977
|
+
k.referenced_table_name AS referencedTableName,
|
|
978
|
+
k.referenced_column_name AS referencedColumnName,
|
|
979
|
+
k.ordinal_position AS ordinalPosition,
|
|
980
|
+
rc.update_rule AS updateRule,
|
|
981
|
+
rc.delete_rule AS deleteRule
|
|
982
|
+
FROM information_schema.referential_constraints rc
|
|
983
|
+
JOIN information_schema.key_column_usage k
|
|
984
|
+
ON k.constraint_name = rc.constraint_name
|
|
985
|
+
AND k.constraint_schema = rc.constraint_schema
|
|
986
|
+
AND k.table_name = rc.table_name
|
|
987
|
+
WHERE rc.constraint_schema = ?
|
|
988
|
+
AND rc.table_name = ?
|
|
989
|
+
ORDER BY rc.constraint_name ASC, k.ordinal_position ASC
|
|
990
|
+
`,
|
|
991
|
+
[schemaName, tableName]
|
|
992
|
+
)
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
const checkConstraintRows = normalizeRows(
|
|
996
|
+
await knex.raw(
|
|
997
|
+
`
|
|
998
|
+
SELECT
|
|
999
|
+
tc.constraint_name AS constraintName,
|
|
1000
|
+
cc.check_clause AS checkClause
|
|
1001
|
+
FROM information_schema.table_constraints tc
|
|
1002
|
+
JOIN information_schema.check_constraints cc
|
|
1003
|
+
ON cc.constraint_schema = tc.constraint_schema
|
|
1004
|
+
AND cc.constraint_name = tc.constraint_name
|
|
1005
|
+
WHERE tc.table_schema = ?
|
|
1006
|
+
AND tc.table_name = ?
|
|
1007
|
+
AND tc.constraint_type = 'CHECK'
|
|
1008
|
+
ORDER BY tc.constraint_name ASC
|
|
1009
|
+
`,
|
|
1010
|
+
[schemaName, tableName]
|
|
1011
|
+
)
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
const columns = columnRows.map((row) => buildColumnSnapshot({
|
|
1015
|
+
name: row.columnName,
|
|
1016
|
+
dataType: row.dataType,
|
|
1017
|
+
columnType: row.columnType,
|
|
1018
|
+
nullable: normalizeText(row.isNullable).toUpperCase() === 'YES',
|
|
1019
|
+
defaultValue: Object.prototype.hasOwnProperty.call(row, 'columnDefault') ? row.columnDefault : row.column_default,
|
|
1020
|
+
extra: row.extra,
|
|
1021
|
+
autoIncrement: normalizeText(row.extra).toLowerCase().includes('auto_increment'),
|
|
1022
|
+
unsigned: normalizeText(row.columnType).toLowerCase().includes('unsigned'),
|
|
1023
|
+
maxLength: row.characterMaximumLength,
|
|
1024
|
+
numericPrecision: row.numericPrecision,
|
|
1025
|
+
numericScale: row.numericScale,
|
|
1026
|
+
datetimePrecision: row.datetimePrecision,
|
|
1027
|
+
characterSetName: row.characterSetName,
|
|
1028
|
+
collationName: row.collationName,
|
|
1029
|
+
ordinalPosition: row.ordinalPosition
|
|
1030
|
+
}))
|
|
1031
|
+
|
|
1032
|
+
const resolvedIdColumn = requireIdColumn(columns, idColumn)
|
|
1033
|
+
const primaryKeyColumns = normalizePrimaryKeyColumns(primaryRows)
|
|
1034
|
+
requirePrimaryKeyContainsId(primaryKeyColumns, resolvedIdColumn)
|
|
1035
|
+
|
|
1036
|
+
return {
|
|
1037
|
+
dialect: 'mysql2',
|
|
1038
|
+
schemaName,
|
|
1039
|
+
tableName,
|
|
1040
|
+
tableCollation: normalizeText((Array.isArray(tableRows) ? tableRows[0] : null)?.tableCollation),
|
|
1041
|
+
idColumn: resolvedIdColumn,
|
|
1042
|
+
primaryKeyColumns,
|
|
1043
|
+
hasWorkspaceIdColumn: columns.some((column) => column.name === 'workspace_id'),
|
|
1044
|
+
hasUserIdColumn: columns.some((column) => column.name === 'user_id'),
|
|
1045
|
+
columns,
|
|
1046
|
+
indexes: normalizeIndexes(indexRows),
|
|
1047
|
+
foreignKeys: normalizeForeignKeys(foreignKeyRows, [], tableName),
|
|
1048
|
+
checkConstraints: normalizeCheckConstraints(
|
|
1049
|
+
checkConstraintRows.map((row) => ({
|
|
1050
|
+
name: row.constraintName,
|
|
1051
|
+
clause: row.checkClause
|
|
1052
|
+
})),
|
|
1053
|
+
tableName
|
|
1054
|
+
)
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export async function introspectKnexTableSnapshot (knex, { tableName = '', idColumn = 'id' } = {}) {
|
|
1059
|
+
requireKnexRaw(knex)
|
|
1060
|
+
const resolvedTableName = requireTableName(tableName)
|
|
1061
|
+
const dialect = detectDialect(knex)
|
|
1062
|
+
|
|
1063
|
+
if (dialect === 'sqlite') {
|
|
1064
|
+
return introspectSqliteTableSnapshot(knex, {
|
|
1065
|
+
tableName: resolvedTableName,
|
|
1066
|
+
idColumn
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (dialect === 'mysql2') {
|
|
1071
|
+
return introspectMysqlTableSnapshot(knex, {
|
|
1072
|
+
tableName: resolvedTableName,
|
|
1073
|
+
idColumn
|
|
1074
|
+
})
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
throw new Error(`Unsupported dialect "${dialect}" for introspectKnexTableSnapshot.`)
|
|
1078
|
+
}
|