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,119 +1,19 @@
|
|
|
1
|
+
import { rejectRemovedOptions } from './response-options.js'
|
|
2
|
+
import { RestApiPayloadError } from '../../../../lib/rest-api-errors.js'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* // Input: Basic include and sort
|
|
9
|
-
* const query = parseJsonApiQuery('include=author&sort=-created_at');
|
|
10
|
-
*
|
|
11
|
-
* // Output: Parsed into arrays
|
|
12
|
-
* // {
|
|
13
|
-
* // include: ['author'], // Split by comma
|
|
14
|
-
* // fields: {},
|
|
15
|
-
* // filters: {},
|
|
16
|
-
* // sort: ['-created_at'], // Split by comma
|
|
17
|
-
* // page: {}
|
|
18
|
-
* // }
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // Input: Filters with bracket notation
|
|
22
|
-
* const query = parseJsonApiQuery('filter[status]=published&filter[author_id]=123');
|
|
23
|
-
*
|
|
24
|
-
* // Output: Extracted filter keys
|
|
25
|
-
* // {
|
|
26
|
-
* // include: [],
|
|
27
|
-
* // fields: {},
|
|
28
|
-
* // filters: {
|
|
29
|
-
* // status: 'published', // filter[status] → filters.status
|
|
30
|
-
* // author_id: '123' // Values kept as strings
|
|
31
|
-
* // },
|
|
32
|
-
* // sort: [],
|
|
33
|
-
* // page: {}
|
|
34
|
-
* // }
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* // Input: Sparse fieldsets
|
|
38
|
-
* const query = parseJsonApiQuery('fields[articles]=title,body&fields[users]=name');
|
|
39
|
-
*
|
|
40
|
-
* // Output: Fields kept as comma-separated strings
|
|
41
|
-
* // {
|
|
42
|
-
* // include: [],
|
|
43
|
-
* // fields: {
|
|
44
|
-
* // articles: 'title,body', // NOT split into array
|
|
45
|
-
* // users: 'name' // Validation expects strings
|
|
46
|
-
* // },
|
|
47
|
-
* // filters: {},
|
|
48
|
-
* // sort: [],
|
|
49
|
-
* // page: {}
|
|
50
|
-
* // }
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* // Input: Pagination with numeric conversion
|
|
54
|
-
* const query = parseJsonApiQuery('page[size]=20&page[number]=3&page[cursor]=abc123');
|
|
5
|
+
* Parse JSON:API query parameters independently of a framework's query parser.
|
|
6
|
+
* Ordinary filters and sparse fields retain strings; filter[field][json] carries
|
|
7
|
+
* an explicit JSON value. Only page size/number are otherwise
|
|
8
|
+
* converted to numbers; opaque cursors keep their exact spelling. Retired
|
|
9
|
+
* controls fail clearly. Repeated keys use the final value.
|
|
55
10
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* // include: [],
|
|
59
|
-
* // fields: {},
|
|
60
|
-
* // filters: {},
|
|
61
|
-
* // sort: [],
|
|
62
|
-
* // page: {
|
|
63
|
-
* // size: 20, // "20" → 20 (numeric)
|
|
64
|
-
* // number: 3, // "3" → 3 (numeric)
|
|
65
|
-
* // cursor: 'abc123' // Non-numeric stays string
|
|
66
|
-
* // }
|
|
67
|
-
* // }
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* // Input: Complex real-world query
|
|
71
|
-
* const query = parseJsonApiQuery(
|
|
72
|
-
* 'include=author,comments.user&' +
|
|
73
|
-
* 'filter[status]=published&' +
|
|
74
|
-
* 'filter[created_after]=2024-01-01&' +
|
|
75
|
-
* 'fields[articles]=title,summary&' +
|
|
76
|
-
* 'sort=-created_at,title&' +
|
|
77
|
-
* 'page[size]=10'
|
|
78
|
-
* );
|
|
79
|
-
*
|
|
80
|
-
* // Output: All parameters properly categorized
|
|
81
|
-
* // {
|
|
82
|
-
* // include: ['author', 'comments.user'],
|
|
83
|
-
* // fields: { articles: 'title,summary' },
|
|
84
|
-
* // filters: {
|
|
85
|
-
* // status: 'published',
|
|
86
|
-
* // created_after: '2024-01-01'
|
|
87
|
-
* // },
|
|
88
|
-
* // sort: ['-created_at', 'title'],
|
|
89
|
-
* // page: { size: 10 }
|
|
90
|
-
* // }
|
|
91
|
-
*
|
|
92
|
-
* @description
|
|
93
|
-
* Used by:
|
|
94
|
-
* - express-plugin parses req.query with this
|
|
95
|
-
* - http-plugin parses URL query strings with this
|
|
96
|
-
* - websocket-plugin parses message query parameters with this
|
|
97
|
-
*
|
|
98
|
-
* Purpose:
|
|
99
|
-
* - Provides uniform JSON:API query parsing across all transports
|
|
100
|
-
* - Handles bracket notation without regex complexity
|
|
101
|
-
* - Converts data types appropriately (numbers for pagination)
|
|
102
|
-
* - Ignores non-JSON:API parameters gracefully
|
|
103
|
-
* - Returns consistent structure even for empty input
|
|
104
|
-
*
|
|
105
|
-
* Data flow:
|
|
106
|
-
* 1. Uses URLSearchParams for reliable parsing
|
|
107
|
-
* 2. Categorizes parameters by their prefix pattern
|
|
108
|
-
* 3. Extracts bracketed keys (filter[x] → x)
|
|
109
|
-
* 4. Splits comma-separated values for include/sort
|
|
110
|
-
* 5. Converts numeric strings for pagination
|
|
111
|
-
* 6. Returns normalized structure for REST API
|
|
11
|
+
* @param {string} queryString - Query string without the leading question mark
|
|
12
|
+
* @returns {object} Include/sort arrays and fields/filters/page maps
|
|
112
13
|
*/
|
|
113
14
|
export function parseJsonApiQuery (queryString) {
|
|
114
15
|
if (!queryString) {
|
|
115
16
|
return {
|
|
116
|
-
include: [],
|
|
117
17
|
fields: {},
|
|
118
18
|
filters: {},
|
|
119
19
|
sort: [],
|
|
@@ -122,8 +22,8 @@ export function parseJsonApiQuery (queryString) {
|
|
|
122
22
|
}
|
|
123
23
|
|
|
124
24
|
const params = new URLSearchParams(queryString)
|
|
25
|
+
rejectRemovedOptions(Object.fromEntries(params))
|
|
125
26
|
const result = {
|
|
126
|
-
include: [],
|
|
127
27
|
fields: {},
|
|
128
28
|
filters: {},
|
|
129
29
|
sort: [],
|
|
@@ -131,6 +31,15 @@ export function parseJsonApiQuery (queryString) {
|
|
|
131
31
|
}
|
|
132
32
|
|
|
133
33
|
for (const [key, value] of params) {
|
|
34
|
+
const jsonFilter = key.match(/^filter\[(.+)\]\[json\]$/)
|
|
35
|
+
if (jsonFilter) {
|
|
36
|
+
let parsed
|
|
37
|
+
try { parsed = JSON.parse(value) } catch {
|
|
38
|
+
throw new RestApiPayloadError(`Invalid JSON value for query parameter '${key}'`, { parameter: key, expected: 'valid JSON' })
|
|
39
|
+
}
|
|
40
|
+
Object.defineProperty(result.filters, jsonFilter[1], { value: parsed, enumerable: true, writable: true, configurable: true })
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
134
43
|
if (key === 'include') {
|
|
135
44
|
// Parse include (comma-separated string to array)
|
|
136
45
|
result.include = value.split(',').map(s => s.trim()).filter(s => s.length > 0)
|
|
@@ -141,21 +50,22 @@ export function parseJsonApiQuery (queryString) {
|
|
|
141
50
|
// Parse filter[key] = value into filters: { key: value }
|
|
142
51
|
const filterKey = key.slice(7, -1) // Remove 'filter[' and ']'
|
|
143
52
|
if (filterKey) {
|
|
144
|
-
result.filters
|
|
53
|
+
Object.defineProperty(result.filters, filterKey, { value, enumerable: true, writable: true, configurable: true })
|
|
145
54
|
}
|
|
146
55
|
} else if (key.startsWith('fields[') && key.endsWith(']')) {
|
|
147
56
|
// Parse fields[type] = fields into fields: { type: "field1,field2" }
|
|
148
57
|
// Keep as comma-separated string to match REST API validation expectations
|
|
149
58
|
const fieldType = key.slice(7, -1) // Remove 'fields[' and ']'
|
|
150
59
|
if (fieldType) {
|
|
151
|
-
result.fields
|
|
60
|
+
Object.defineProperty(result.fields, fieldType, { value, enumerable: true, writable: true, configurable: true })
|
|
152
61
|
}
|
|
153
62
|
} else if (key.startsWith('page[') && key.endsWith(']')) {
|
|
154
63
|
// Parse page[size] = 10 into page: { size: 10 }
|
|
155
64
|
const pageKey = key.slice(5, -1) // Remove 'page[' and ']'
|
|
156
65
|
if (pageKey) {
|
|
157
|
-
//
|
|
158
|
-
|
|
66
|
+
// Keep fractional values intact so the existing integer contract rejects them.
|
|
67
|
+
const parsed = ['size', 'number'].includes(pageKey) && value.trim() !== '' && !isNaN(value) ? Number(value) : value
|
|
68
|
+
Object.defineProperty(result.page, pageKey, { value: parsed, enumerable: true, writable: true, configurable: true })
|
|
159
69
|
}
|
|
160
70
|
}
|
|
161
71
|
// Ignore other query parameters that don't match JSON:API patterns
|
|
@@ -177,12 +87,12 @@ const stringifyQueryValue = (value) => {
|
|
|
177
87
|
}
|
|
178
88
|
|
|
179
89
|
const appendCommaList = (params, key, value) => {
|
|
180
|
-
if (!hasSerializableValue(value)) return
|
|
90
|
+
if (!hasSerializableValue(value) && !(key === 'include' && Array.isArray(value))) return
|
|
181
91
|
const serialized = Array.isArray(value)
|
|
182
92
|
? value.map(stringifyQueryValue).filter((entry) => entry.length > 0).join(',')
|
|
183
93
|
: stringifyQueryValue(value)
|
|
184
94
|
|
|
185
|
-
if (serialized) {
|
|
95
|
+
if (serialized || key === 'include') {
|
|
186
96
|
params.set(key, serialized)
|
|
187
97
|
}
|
|
188
98
|
}
|
|
@@ -191,7 +101,12 @@ const appendScopedParams = (params, publicName, values) => {
|
|
|
191
101
|
if (!values || typeof values !== 'object' || Array.isArray(values)) return
|
|
192
102
|
|
|
193
103
|
for (const [key, value] of Object.entries(values)) {
|
|
194
|
-
if (!key ||
|
|
104
|
+
if (!key || value === undefined) continue
|
|
105
|
+
if (publicName === 'filter' && (value === null || (typeof value === 'object' && !(value instanceof Date)))) {
|
|
106
|
+
params.set(`${publicName}[${key}][json]`, JSON.stringify(value))
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (!hasSerializableValue(value)) continue
|
|
195
110
|
params.set(`${publicName}[${key}]`, stringifyQueryValue(value))
|
|
196
111
|
}
|
|
197
112
|
}
|
|
@@ -1,23 +1,13 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { errorMessage } from '../../../../lib/error-context.js'
|
|
4
|
+
import { formatDiagnosticValue } from '../../../../lib/error-formatter.js'
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Parses database version string to comparable format
|
|
3
8
|
*
|
|
4
9
|
* @param {string} versionString - Raw version string from database
|
|
5
|
-
* @returns {
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* // Input: MySQL version
|
|
9
|
-
* parseVersion("8.0.33");
|
|
10
|
-
* // Output: { major: 8, minor: 0, patch: 33 }
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* // Input: PostgreSQL version with extra info
|
|
14
|
-
* parseVersion("14.5 (Ubuntu 14.5-1.pgdg20.04+1)");
|
|
15
|
-
* // Output: { major: 14, minor: 5, patch: 0 }
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* // Input: MariaDB version
|
|
19
|
-
* parseVersion("10.6.12-MariaDB");
|
|
20
|
-
* // Output: { major: 10, minor: 6, patch: 12 }
|
|
10
|
+
* @returns {{major: number, minor: number, patch: number}|null} Parsed version
|
|
21
11
|
*/
|
|
22
12
|
const parseVersion = (versionString) => {
|
|
23
13
|
// MySQL: "8.0.33"
|
|
@@ -29,237 +19,173 @@ const parseVersion = (versionString) => {
|
|
|
29
19
|
if (!match) return null
|
|
30
20
|
|
|
31
21
|
return {
|
|
32
|
-
major:
|
|
33
|
-
minor:
|
|
34
|
-
patch: match[3] ?
|
|
22
|
+
major: Number(match[1]),
|
|
23
|
+
minor: Number(match[2]),
|
|
24
|
+
patch: match[3] ? Number(match[3]) : 0
|
|
35
25
|
}
|
|
36
26
|
}
|
|
37
27
|
|
|
38
28
|
/**
|
|
39
29
|
* Checks if the database supports window functions based on version
|
|
40
30
|
*
|
|
41
|
-
* @param {
|
|
31
|
+
* @param {import('../storage/storage-types.js').StorageDatabase} knex - Knex instance
|
|
32
|
+
* @param {{ version: string }} [databaseInfo] - Already fetched version
|
|
33
|
+
* @param {import('../storage/storage-types.js').CapabilityLogger} [log] - Configured diagnostic sink
|
|
42
34
|
* @returns {Promise<boolean>} True if window functions are supported
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* // Input: MySQL 8.0.33
|
|
46
|
-
* const supports = await supportsWindowFunctions(knex);
|
|
47
|
-
* // Output: true (MySQL 8.0+ supports window functions)
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* // Input: MySQL 5.7.42
|
|
51
|
-
* const supports = await supportsWindowFunctions(knex);
|
|
52
|
-
* // Output: false (MySQL < 8.0 doesn't support window functions)
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* // Input: PostgreSQL (any modern version)
|
|
56
|
-
* const supports = await supportsWindowFunctions(knex);
|
|
57
|
-
* // Output: true (PostgreSQL 8.4+ supports window functions)
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* // Input: SQLite 3.24.0
|
|
61
|
-
* const supports = await supportsWindowFunctions(knex);
|
|
62
|
-
* // Output: false (SQLite needs 3.25.0+)
|
|
63
|
-
*
|
|
64
|
-
* @description
|
|
65
|
-
* Used by:
|
|
66
|
-
* - buildWindowedIncludeSubquery to validate before using window functions
|
|
67
|
-
* - rest-api-knex-plugin to determine query strategies
|
|
68
|
-
*
|
|
69
|
-
* Purpose:
|
|
70
|
-
* - Prevents runtime errors by checking feature support
|
|
71
|
-
* - Enables fallback strategies for older databases
|
|
72
|
-
* - Provides clear error messages about version requirements
|
|
73
|
-
*
|
|
74
|
-
* Data flow:
|
|
75
|
-
* 1. Identifies database client type from Knex config
|
|
76
|
-
* 2. For version-dependent databases, queries version string
|
|
77
|
-
* 3. Parses version and compares against known thresholds
|
|
78
|
-
* 4. Returns boolean indicating support
|
|
79
|
-
* 5. Falls back to false if version can't be determined
|
|
80
|
-
*
|
|
81
|
-
* Version requirements:
|
|
82
|
-
* - PostgreSQL: 8.4+ (all modern versions)
|
|
83
|
-
* - MySQL: 8.0+
|
|
84
|
-
* - MariaDB: 10.2+
|
|
85
|
-
* - SQLite: 3.25.0+
|
|
86
|
-
* - SQL Server: 2005+ (all versions)
|
|
87
|
-
* - Oracle: Supported (long-standing feature)
|
|
88
35
|
*/
|
|
89
|
-
export const supportsWindowFunctions = async (knex) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
36
|
+
export const supportsWindowFunctions = async (knex, databaseInfo, log) => {
|
|
37
|
+
const client = knex.client.config.client
|
|
38
|
+
if (['pg', 'postgresql', 'mssql', 'oracledb'].includes(client)) return true
|
|
39
|
+
if (!['mysql', 'mysql2', 'sqlite3', 'better-sqlite3'].includes(client) && !client.includes('maria')) return false
|
|
40
|
+
|
|
41
|
+
const info = databaseInfo || await getDatabaseInfo(knex, log)
|
|
42
|
+
const parsed = typeof info.version === 'string' ? parseVersion(info.version) : null
|
|
43
|
+
if (!parsed) return false
|
|
44
|
+
if (['mysql', 'mysql2'].includes(client)) return parsed.major >= 8
|
|
45
|
+
if (client.includes('sqlite')) return parsed.major > 3 || (parsed.major === 3 && parsed.minor >= 25)
|
|
46
|
+
return info.version.toLowerCase().includes('mariadb') &&
|
|
47
|
+
(parsed.major > 10 || (parsed.major === 10 && parsed.minor >= 2))
|
|
48
|
+
}
|
|
100
49
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
50
|
+
// Built-in rules; custom field declarations still undergo their own validation.
|
|
51
|
+
/** @type {import('../storage/storage-types.js').SerializationCapabilities} */
|
|
52
|
+
export const SERIALIZATION_CAPABILITIES = Object.freeze({
|
|
53
|
+
customSerializerResult: 'synchronous',
|
|
54
|
+
structuredTypes: Object.freeze(['object', 'array']),
|
|
55
|
+
structuredEncoding: 'json',
|
|
56
|
+
wholeDocumentPredicates: false
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/** @type {import('../storage/storage-types.js').RelationshipCapabilities} */
|
|
60
|
+
export const RELATIONSHIP_CAPABILITIES = Object.freeze({
|
|
61
|
+
attributeKinds: Object.freeze(['belongsTo', 'belongsToPolymorphic']),
|
|
62
|
+
declaredCardinalities: Object.freeze({ hasOne: 'one', hasMany: 'many', manyToMany: 'many' })
|
|
63
|
+
})
|
|
106
64
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Both storage plugins share one version observation during initialization.
|
|
67
|
+
* @param {import('../storage/storage-types.js').StorageDatabase} knex
|
|
68
|
+
* @param {import('../storage/storage-types.js').CapabilityLogger} [log]
|
|
69
|
+
* @returns {Promise<import('../storage/storage-types.js').DatabaseCapabilities>}
|
|
70
|
+
*/
|
|
71
|
+
export const getDatabaseCapabilities = async (knex, log) => {
|
|
72
|
+
const dbInfo = await getDatabaseInfo(knex, log)
|
|
73
|
+
return {
|
|
74
|
+
dbInfo,
|
|
75
|
+
windowFunctions: await supportsWindowFunctions(knex, dbInfo),
|
|
76
|
+
insertResult: getInsertResultMode(knex.client.config.client),
|
|
77
|
+
serialization: SERIALIZATION_CAPABILITIES,
|
|
78
|
+
relationships: RELATIONSHIP_CAPABILITIES,
|
|
79
|
+
temporal: {
|
|
80
|
+
native: getTemporalStorageCapabilities(knex.client.config.client),
|
|
81
|
+
text: getTemporalStorageCapabilities(knex.client.config.client, { textStorage: true })
|
|
82
|
+
},
|
|
83
|
+
schema: getSchemaCapabilities(knex.client.config.client)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
112
86
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return false
|
|
124
|
-
}
|
|
87
|
+
/**
|
|
88
|
+
* @param {unknown} dialect
|
|
89
|
+
* @returns {import('../storage/storage-types.js').InsertResultMode}
|
|
90
|
+
*/
|
|
91
|
+
export const getInsertResultMode = dialect => {
|
|
92
|
+
const client = String(dialect || '').toLowerCase()
|
|
93
|
+
if (['mysql', 'mysql2'].includes(client)) return 'insert-id'
|
|
94
|
+
if (['pg', 'postgresql', 'sqlite3', 'better-sqlite3'].includes(client)) return 'rows'
|
|
95
|
+
return 'driver-defined'
|
|
96
|
+
}
|
|
125
97
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Choose the query form; each caller must still validate its driver's result.
|
|
100
|
+
* @param {import('knex').Knex.QueryBuilder<import('../storage/storage-types.js').StorageRow, unknown>} query
|
|
101
|
+
* @param {string|string[]|Record<string, string|import('knex').Knex.Raw<unknown>>} columns
|
|
102
|
+
* @returns {import('knex').Knex.QueryBuilder<import('../storage/storage-types.js').StorageRow, unknown>}
|
|
103
|
+
*/
|
|
104
|
+
export const applyInsertReturning = (query, columns) => {
|
|
105
|
+
if (getInsertResultMode(query.client.config.client) === 'insert-id') return query
|
|
106
|
+
// Knex supports alias dictionaries at runtime; its declarations omit that overload.
|
|
107
|
+
const returning = /** @type {(columns: string|string[]|Record<string, string|import('knex').Knex.Raw<unknown>>) => typeof query} */ (query.returning)
|
|
108
|
+
return returning.call(query, columns)
|
|
109
|
+
}
|
|
129
110
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Built-in value conversion limits; schemas/custom serializers may differ.
|
|
113
|
+
* @param {unknown} dialect
|
|
114
|
+
* @param {{textStorage?: boolean}} [options]
|
|
115
|
+
* @returns {import('../storage/storage-types.js').TemporalStorageCapabilities}
|
|
116
|
+
*/
|
|
117
|
+
export const getTemporalStorageCapabilities = (dialect, { textStorage = false } = {}) => {
|
|
118
|
+
const client = String(dialect || '').toLowerCase()
|
|
119
|
+
const mysql = ['mysql', 'mysql2'].includes(client)
|
|
120
|
+
return {
|
|
121
|
+
dateMinYear: !textStorage && mysql ? 1000 : 0,
|
|
122
|
+
dateTimeMinYear: mysql ? 1000 : 0,
|
|
123
|
+
maxYear: 9999,
|
|
124
|
+
dateTimeFractionDigits: 3,
|
|
125
|
+
timeFractionDigits: !textStorage && (mysql || ['pg', 'postgresql'].includes(client)) ? 6 : null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
133
128
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
return false
|
|
152
|
-
}
|
|
153
|
-
} catch (error) {
|
|
154
|
-
// Log error with context
|
|
155
|
-
console.error('[supportsWindowFunctions] Unexpected error checking database capabilities:', {
|
|
156
|
-
error: error.message,
|
|
157
|
-
stack: error.stack,
|
|
158
|
-
client: knex?.client?.config?.client || 'unknown'
|
|
159
|
-
})
|
|
160
|
-
// If we can't determine, assume no support for safety
|
|
161
|
-
return false
|
|
129
|
+
/**
|
|
130
|
+
* Describe the existing field-alteration runner, not all database DDL.
|
|
131
|
+
* @param {unknown} dialect
|
|
132
|
+
* @returns {import('../storage/storage-types.js').SchemaCapabilities}
|
|
133
|
+
*/
|
|
134
|
+
export const getSchemaCapabilities = dialect => {
|
|
135
|
+
const client = String(dialect || '').toLowerCase()
|
|
136
|
+
const mysql = /mysql/.test(client)
|
|
137
|
+
const sqlite = client.includes('sqlite')
|
|
138
|
+
const postgres = ['pg', 'postgresql'].includes(client)
|
|
139
|
+
return {
|
|
140
|
+
recognizedDialect: mysql || sqlite || postgres,
|
|
141
|
+
fieldAlteration: mysql ? 'standalone' : sqlite ? 'sqlite-rebuild' : 'transaction',
|
|
142
|
+
callerFieldAlteration: mysql ? 'forbidden' : sqlite ? 'foreign-keys-off' : 'savepoint',
|
|
143
|
+
temporalColumnFractionDigits: mysql || postgres ? 6 : null,
|
|
144
|
+
setValues: mysql
|
|
162
145
|
}
|
|
163
146
|
}
|
|
164
147
|
|
|
165
148
|
/**
|
|
166
149
|
* Gets database info for error messages and capability checks
|
|
167
150
|
*
|
|
168
|
-
* @param {
|
|
169
|
-
* @
|
|
170
|
-
*
|
|
171
|
-
* @example
|
|
172
|
-
* // Input: MySQL connection
|
|
173
|
-
* const info = await getDatabaseInfo(knex);
|
|
174
|
-
* // Output: { client: 'MySQL', version: '8.0.33' }
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* // Input: PostgreSQL connection
|
|
178
|
-
* const info = await getDatabaseInfo(knex);
|
|
179
|
-
* // Output: {
|
|
180
|
-
* // client: 'PostgreSQL',
|
|
181
|
-
* // version: 'PostgreSQL 14.5 (Ubuntu 14.5-1.pgdg20.04+1)'
|
|
182
|
-
* // }
|
|
183
|
-
*
|
|
184
|
-
* @example
|
|
185
|
-
* // Input: Connection error case
|
|
186
|
-
* const info = await getDatabaseInfo(knexWithError);
|
|
187
|
-
* // Output: {
|
|
188
|
-
* // client: 'MySQL',
|
|
189
|
-
* // version: 'unknown',
|
|
190
|
-
* // error: 'Connection refused'
|
|
191
|
-
* // }
|
|
192
|
-
*
|
|
193
|
-
* @description
|
|
194
|
-
* Used by:
|
|
195
|
-
* - Error messages to show which database/version lacks a feature
|
|
196
|
-
* - Capability detection for feature flags
|
|
197
|
-
* - Debugging connection issues
|
|
198
|
-
*
|
|
199
|
-
* Purpose:
|
|
200
|
-
* - Provides human-readable database identification
|
|
201
|
-
* - Helps users understand version requirements
|
|
202
|
-
* - Enables detailed error messages
|
|
203
|
-
* - Gracefully handles query failures
|
|
204
|
-
*
|
|
205
|
-
* Data flow:
|
|
206
|
-
* 1. Identifies client type from Knex config
|
|
207
|
-
* 2. Runs version query specific to each database
|
|
208
|
-
* 3. Returns formatted info object
|
|
209
|
-
* 4. Includes error details if version query fails
|
|
210
|
-
* 5. Falls back to 'unknown' for unsupported databases
|
|
151
|
+
* @param {import('../storage/storage-types.js').StorageDatabase} knex - Knex instance
|
|
152
|
+
* @param {import('../storage/storage-types.js').CapabilityLogger} [log] - Configured diagnostic sink
|
|
153
|
+
* @returns {Promise<import('../storage/storage-types.js').DatabaseInfo>} Database info with client and version
|
|
211
154
|
*/
|
|
212
|
-
export const getDatabaseInfo = async (knex) => {
|
|
155
|
+
export const getDatabaseInfo = async (knex, log) => {
|
|
156
|
+
const client = knex.client.config.client
|
|
157
|
+
let name = client
|
|
158
|
+
if (['mysql', 'mysql2'].includes(client)) name = 'MySQL'
|
|
159
|
+
else if (['sqlite3', 'better-sqlite3'].includes(client)) name = 'SQLite'
|
|
160
|
+
else if (['pg', 'postgresql'].includes(client)) name = 'PostgreSQL'
|
|
213
161
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
case 'sqlite3':
|
|
229
|
-
case 'better-sqlite3':
|
|
230
|
-
try {
|
|
231
|
-
versionString = await knex.raw('SELECT sqlite_version() as version')
|
|
232
|
-
return { client: 'SQLite', version: versionString[0].version }
|
|
233
|
-
} catch (dbError) {
|
|
234
|
-
console.warn(`[getDatabaseInfo] Failed to get SQLite version: ${dbError.message}`)
|
|
235
|
-
return { client: 'SQLite', version: 'unknown', error: dbError.message }
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
case 'pg':
|
|
239
|
-
case 'postgresql':
|
|
240
|
-
try {
|
|
241
|
-
versionString = await knex.raw('SELECT version() as version')
|
|
242
|
-
return { client: 'PostgreSQL', version: versionString.rows[0].version }
|
|
243
|
-
} catch (dbError) {
|
|
244
|
-
console.warn(`[getDatabaseInfo] Failed to get PostgreSQL version: ${dbError.message}`)
|
|
245
|
-
return { client: 'PostgreSQL', version: 'unknown', error: dbError.message }
|
|
246
|
-
}
|
|
247
|
-
|
|
162
|
+
/** @type {unknown} */
|
|
163
|
+
let version
|
|
164
|
+
switch (name) {
|
|
165
|
+
case 'MySQL': {
|
|
166
|
+
const row = await knex.first(knex.raw('VERSION() as version'))
|
|
167
|
+
version = row && 'version' in row ? row.version : undefined
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
case 'SQLite':
|
|
171
|
+
version = (await knex.raw('SELECT sqlite_version() as version'))?.[0]?.version
|
|
172
|
+
break
|
|
173
|
+
case 'PostgreSQL':
|
|
174
|
+
version = (await knex.raw('SELECT version() as version'))?.rows?.[0]?.version
|
|
175
|
+
break
|
|
248
176
|
default:
|
|
249
|
-
return { client, version: 'unknown' }
|
|
177
|
+
if (!client.includes('maria')) return { client: name, version: 'unknown' }
|
|
178
|
+
version = (await knex.raw('SELECT VERSION() as version'))?.[0]?.version
|
|
250
179
|
}
|
|
180
|
+
if (typeof version !== 'string') throw new Error('Database version query did not return a string')
|
|
181
|
+
return { client: name, version }
|
|
251
182
|
} catch (error) {
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
client: knex?.client?.config?.client || 'unknown',
|
|
261
|
-
version: 'unknown',
|
|
262
|
-
error: error.message
|
|
263
|
-
}
|
|
183
|
+
// Capability observation is advisory; a failing diagnostic must not replace it.
|
|
184
|
+
try {
|
|
185
|
+
await log?.warn('Database version observation failed', formatDiagnosticValue({
|
|
186
|
+
operation: 'database-capabilities', phase: 'version-observation', backend: client, error
|
|
187
|
+
}))
|
|
188
|
+
} catch {}
|
|
189
|
+
return { client: name, version: 'unknown', error: errorMessage(error) }
|
|
264
190
|
}
|
|
265
191
|
}
|