json-rest-api 1.0.28 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +746 -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} +90 -31
- 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 -174
- 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 +67 -330
- 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 +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
- 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 +111 -287
- 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 +109 -44
- 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 +142 -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 -2509
- 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 -1069
- 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 -385
- 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 -145
- 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 +38 -69
- 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 +39 -82
- 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 -1238
- 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 -150
- 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 -118
- package/tests/fixtures/api-configs.js +0 -2509
- 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-cursor-multifield.test.js +0 -643
- 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/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,20 +1,29 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import {
|
|
2
|
-
getFieldValue as
|
|
3
|
+
getFieldValue as getTableFieldValue,
|
|
3
4
|
getIdColumn,
|
|
5
|
+
getStorageInfo,
|
|
4
6
|
getStorageColumn,
|
|
5
7
|
translateAttributesForStorage,
|
|
8
|
+
serializeFieldValueForStorage,
|
|
9
|
+
assertWritableKnexColumns,
|
|
6
10
|
} from './storage-mapping.js'
|
|
7
11
|
import {
|
|
8
12
|
getCanonicalFieldValue,
|
|
9
13
|
getCanonicalResourceIdColumn,
|
|
10
14
|
translateCanonicalAttributesForStorage,
|
|
11
15
|
} from './canonical-storage-mapping.js'
|
|
16
|
+
import { assertScalarQueryField } from '../querying-writing/field-utils.js'
|
|
12
17
|
import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
|
|
13
18
|
|
|
19
|
+
/** @import { BaseStorageAdapterOptions, CanonicalDescriptor, SelectTranslator, StorageAdapter, StorageAdapterOptions, StorageAdapterLookupOptions, StorageSchemaInfo, StorageFieldDefinition } from './storage-types.js' */
|
|
20
|
+
|
|
21
|
+
/** @template T @param {T} value @returns {T} */
|
|
14
22
|
const passthrough = (value) => value
|
|
23
|
+
/** @type {StorageAdapter['translateFilterValue']} */
|
|
15
24
|
const identityTranslate = (_field, value) => value
|
|
16
|
-
const identityScope = (query) => query
|
|
17
25
|
|
|
26
|
+
/** @type {StorageAdapter['selectColumns']} */
|
|
18
27
|
const selectColumnsOnBuilder = (builder, columns) => {
|
|
19
28
|
if (!columns) return builder
|
|
20
29
|
if (Array.isArray(columns)) {
|
|
@@ -25,16 +34,16 @@ const selectColumnsOnBuilder = (builder, columns) => {
|
|
|
25
34
|
return builder
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
/** @param {unknown} value @returns {unknown} */
|
|
30
38
|
const normalizeBelongsToValue = (value) => {
|
|
31
39
|
if (value === null || value === undefined) return value
|
|
32
40
|
if (Array.isArray(value)) {
|
|
33
|
-
return
|
|
41
|
+
return value.map(entry => normalizeBelongsToValue(entry))
|
|
34
42
|
}
|
|
35
43
|
return String(value)
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
/** @param {unknown} value @returns {boolean} */
|
|
38
47
|
const normalizeBooleanValue = (value) => {
|
|
39
48
|
if (typeof value === 'boolean') return value
|
|
40
49
|
if (typeof value === 'string') {
|
|
@@ -49,13 +58,16 @@ const normalizeBooleanValue = (value) => {
|
|
|
49
58
|
return Boolean(value)
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
/**
|
|
62
|
+
* @param {unknown} value
|
|
63
|
+
* @param {StorageFieldDefinition} [definition]
|
|
64
|
+
* @param {{ isRelationship?: boolean, databaseClient?: string, textStorage?: boolean }} [options]
|
|
65
|
+
* @returns {unknown}
|
|
66
|
+
*/
|
|
67
|
+
const normalizeFilterValueForDefinition = (value, definition = {}, { isRelationship = false, databaseClient, textStorage } = {}) => {
|
|
53
68
|
if (value === null || value === undefined) return value
|
|
54
69
|
if (Array.isArray(value)) {
|
|
55
|
-
return
|
|
56
|
-
value,
|
|
57
|
-
(entry) => normalizeFilterValueForDefinition(entry, definition, { isRelationship })
|
|
58
|
-
)
|
|
70
|
+
return value.map(entry => normalizeFilterValueForDefinition(entry, definition, { isRelationship, databaseClient, textStorage }))
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
if (isRelationship) {
|
|
@@ -78,6 +90,8 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
|
|
|
78
90
|
|
|
79
91
|
if (['date', 'dateTime', 'time'].includes(type)) {
|
|
80
92
|
return normalizeValueForDatabaseStorage(value, type, {
|
|
93
|
+
databaseClient,
|
|
94
|
+
textStorage,
|
|
81
95
|
temporalPrecision: definition?.temporalPrecision
|
|
82
96
|
})
|
|
83
97
|
}
|
|
@@ -85,11 +99,9 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
|
|
|
85
99
|
return value
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
102
|
+
/** @overload @param {StorageAdapter} adapter @returns {SelectTranslator} */
|
|
103
|
+
/** @overload @param {StorageAdapter | null | undefined} adapter @returns {SelectTranslator | null} */
|
|
104
|
+
/** @param {StorageAdapter | null | undefined} adapter @returns {SelectTranslator | null} */
|
|
93
105
|
export const createSelectTranslator = (adapter) => {
|
|
94
106
|
if (!adapter) return null
|
|
95
107
|
|
|
@@ -118,40 +130,23 @@ export const createSelectTranslator = (adapter) => {
|
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
132
|
|
|
121
|
-
|
|
122
|
-
if (!adapter || !fields) return fields
|
|
123
|
-
|
|
124
|
-
const translateField = createSelectTranslator(adapter)
|
|
125
|
-
|
|
126
|
-
return fields.map((field) => {
|
|
127
|
-
if (typeof field !== 'string') return field
|
|
128
|
-
if (field === '*') return '*'
|
|
129
|
-
|
|
130
|
-
const aliasMatch = field.match(/\s+as\s+/i)
|
|
131
|
-
if (aliasMatch) {
|
|
132
|
-
const [source, alias] = field.split(/\s+as\s+/i)
|
|
133
|
-
const translatedSource = translateSourceColumn(source.trim(), adapter)
|
|
134
|
-
return `${translatedSource} as ${alias.trim()}`
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return translateField(field)
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
133
|
+
/** @param {BaseStorageAdapterOptions} options @returns {StorageAdapter} */
|
|
141
134
|
const baseAdapter = ({
|
|
142
135
|
knex,
|
|
143
136
|
tableName,
|
|
144
137
|
idColumn,
|
|
145
138
|
translateColumn,
|
|
146
139
|
translateFilterValue = identityTranslate,
|
|
147
|
-
|
|
140
|
+
translateCursorValue = identityTranslate,
|
|
141
|
+
applyResourceScope = passthrough,
|
|
148
142
|
toStorageRow = passthrough,
|
|
149
143
|
getFieldValue = (record, fieldName) => record?.[fieldName],
|
|
150
144
|
isCanonical = false,
|
|
151
145
|
}) => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
/** @type {StorageAdapter['buildBaseQuery']} */
|
|
147
|
+
const buildBaseQuery = ({ transaction, tableAlias } = {}) => {
|
|
148
|
+
const query = (transaction || knex)(tableAlias ? { [tableAlias]: tableName } : tableName)
|
|
149
|
+
return applyResourceScope(query, tableAlias)
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
return {
|
|
@@ -160,31 +155,61 @@ const baseAdapter = ({
|
|
|
160
155
|
getIdColumn: () => idColumn,
|
|
161
156
|
translateColumn,
|
|
162
157
|
translateFilterValue,
|
|
158
|
+
translateCursorValue,
|
|
163
159
|
applyResourceScope,
|
|
164
160
|
toStorageRow,
|
|
165
161
|
getFieldValue,
|
|
166
162
|
buildBaseQuery,
|
|
167
|
-
selectColumns:
|
|
163
|
+
selectColumns: selectColumnsOnBuilder,
|
|
168
164
|
}
|
|
169
165
|
}
|
|
170
166
|
|
|
171
|
-
|
|
167
|
+
/** @param {StorageAdapterOptions} options @returns {StorageAdapter} */
|
|
168
|
+
const createTableAdapter = ({ knex, schemaInfo }) => {
|
|
169
|
+
assertWritableKnexColumns(getStorageInfo(schemaInfo))
|
|
172
170
|
const tableName = schemaInfo.tableName
|
|
173
171
|
const idColumn = getIdColumn(schemaInfo)
|
|
174
172
|
|
|
173
|
+
/** @type {StorageAdapter['translateColumn']} */
|
|
175
174
|
const translateColumn = (field) => getStorageColumn(schemaInfo, field)
|
|
176
|
-
|
|
177
|
-
const
|
|
175
|
+
/** @type {StorageAdapter['toStorageRow']} */
|
|
176
|
+
const toStorageRow = (attributes, options = {}) => translateAttributesForStorage(attributes, schemaInfo, { ...options, databaseClient: knex.client.config.client })
|
|
177
|
+
/** @type {StorageAdapter['getFieldValue']} */
|
|
178
|
+
const getFieldValue = (record, fieldName) => getTableFieldValue(record, schemaInfo, fieldName)
|
|
179
|
+
/** @type {StorageAdapter['translateCursorValue']} */
|
|
180
|
+
const translateCursorValue = (field, value) => {
|
|
181
|
+
const definition = schemaInfo.schemaStructure?.[field]
|
|
182
|
+
return normalizeFilterValueForDefinition(value, definition, {
|
|
183
|
+
isRelationship: Boolean(definition?.belongsTo || definition?.belongsToPolymorphic),
|
|
184
|
+
databaseClient: knex.client.config.client
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
/** @type {StorageAdapter['translateFilterValue']} */
|
|
178
188
|
const translateFilterValue = (field, value) => {
|
|
179
189
|
const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
180
|
-
const
|
|
190
|
+
const fieldName = searchField?.actualField || field
|
|
191
|
+
const schemaField = schemaInfo.schemaStructure?.[fieldName]
|
|
192
|
+
assertScalarQueryField(schemaField, fieldName, 'filter')
|
|
193
|
+
const fieldStorage = getStorageInfo(schemaInfo).fields[fieldName]
|
|
194
|
+
if (fieldStorage?.serialize) {
|
|
195
|
+
/** @param {unknown} entry */
|
|
196
|
+
const serialize = (entry) => serializeFieldValueForStorage(entry, {
|
|
197
|
+
fieldName,
|
|
198
|
+
columnName: fieldStorage.column,
|
|
199
|
+
definition: fieldStorage.definition,
|
|
200
|
+
schemaInfo,
|
|
201
|
+
context: null,
|
|
202
|
+
operation: 'filter'
|
|
203
|
+
})
|
|
204
|
+
return Array.isArray(value) ? value.map(serialize) : serialize(value)
|
|
205
|
+
}
|
|
181
206
|
const definition = searchField || schemaField || {}
|
|
182
207
|
const isRelationship = Boolean(
|
|
183
208
|
searchField?.isRelationship ||
|
|
184
209
|
schemaField?.belongsTo ||
|
|
185
210
|
schemaField?.belongsToPolymorphic
|
|
186
211
|
)
|
|
187
|
-
return normalizeFilterValueForDefinition(value, definition, { isRelationship })
|
|
212
|
+
return normalizeFilterValueForDefinition(value, definition, { isRelationship, databaseClient: knex.client.config.client })
|
|
188
213
|
}
|
|
189
214
|
|
|
190
215
|
return baseAdapter({
|
|
@@ -193,25 +218,28 @@ const createLegacyAdapter = ({ knex, schemaInfo }) => {
|
|
|
193
218
|
idColumn,
|
|
194
219
|
translateColumn,
|
|
195
220
|
translateFilterValue,
|
|
196
|
-
|
|
221
|
+
translateCursorValue,
|
|
222
|
+
applyResourceScope: passthrough,
|
|
197
223
|
toStorageRow,
|
|
198
224
|
getFieldValue,
|
|
199
225
|
isCanonical: false,
|
|
200
226
|
})
|
|
201
227
|
}
|
|
202
228
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const canonical = descriptor.canonical
|
|
229
|
+
/** @param {StorageAdapterOptions & { descriptor: CanonicalDescriptor }} options @returns {StorageAdapter} */
|
|
230
|
+
const createCanonicalAdapter = ({ knex, schemaInfo, descriptor }) => {
|
|
231
|
+
const canonical = descriptor.canonical
|
|
206
232
|
const canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
207
233
|
const fieldsInfo = descriptor.fields || {}
|
|
208
234
|
const belongsToInfo = descriptor.belongsTo || {}
|
|
209
235
|
const idProperty = schemaInfo.idProperty || descriptor.idProperty || 'id'
|
|
236
|
+
/** @param {string} field */
|
|
210
237
|
const isLogicalIdField = (field) => field === 'id' || field === idProperty
|
|
211
238
|
const descriptorWithIdProperty = descriptor.idProperty === idProperty
|
|
212
239
|
? descriptor
|
|
213
240
|
: { ...descriptor, idProperty }
|
|
214
241
|
|
|
242
|
+
/** @type {StorageAdapter['translateColumn']} */
|
|
215
243
|
const translateColumn = (field) => {
|
|
216
244
|
if (!field) return field
|
|
217
245
|
if (isLogicalIdField(field)) return getCanonicalResourceIdColumn(descriptor)
|
|
@@ -237,7 +265,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
237
265
|
if (!canonicalEntry && field.endsWith('_type')) {
|
|
238
266
|
const alias = field.slice(0, -5)
|
|
239
267
|
const aliasEntry = canonicalFieldMap[alias]
|
|
240
|
-
if (aliasEntry?.typeSlot) return aliasEntry.typeSlot
|
|
268
|
+
if (typeof aliasEntry === 'object' && aliasEntry?.typeSlot) return aliasEntry.typeSlot
|
|
241
269
|
}
|
|
242
270
|
|
|
243
271
|
const fieldInfo = fieldsInfo[field]
|
|
@@ -249,9 +277,27 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
249
277
|
return field
|
|
250
278
|
}
|
|
251
279
|
|
|
280
|
+
/** @type {StorageAdapter['translateFilterValue']} */
|
|
252
281
|
const translateFilterValue = (field, value) => {
|
|
253
282
|
const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
254
|
-
const
|
|
283
|
+
const fieldName = searchField?.actualField || field
|
|
284
|
+
const schemaField = schemaInfo.schemaStructure?.[fieldName]
|
|
285
|
+
assertScalarQueryField(schemaField, fieldName, 'filter')
|
|
286
|
+
if (schemaField?.storage?.serialize) {
|
|
287
|
+
/** @param {unknown} entry */
|
|
288
|
+
const serialize = entry => {
|
|
289
|
+
const serialized = serializeFieldValueForStorage(entry, {
|
|
290
|
+
fieldName,
|
|
291
|
+
columnName: translateColumn(fieldName),
|
|
292
|
+
definition: schemaField,
|
|
293
|
+
schemaInfo,
|
|
294
|
+
operation: 'filter'
|
|
295
|
+
})
|
|
296
|
+
return schemaField.type === 'id' ? normalizeBelongsToValue(serialized) : serialized
|
|
297
|
+
}
|
|
298
|
+
return Array.isArray(value) ? value.map(serialize) : serialize(value)
|
|
299
|
+
}
|
|
300
|
+
if (isLogicalIdField(fieldName) || schemaField?.type === 'id') return normalizeBelongsToValue(value)
|
|
255
301
|
const isRelationship = Boolean(
|
|
256
302
|
searchField?.isRelationship ||
|
|
257
303
|
schemaField?.belongsTo ||
|
|
@@ -259,20 +305,33 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
259
305
|
)
|
|
260
306
|
const definition = searchField || schemaField || {}
|
|
261
307
|
|
|
262
|
-
return normalizeFilterValueForDefinition(value, definition, { isRelationship })
|
|
308
|
+
return normalizeFilterValueForDefinition(value, definition, { isRelationship, databaseClient: knex.client.config.client, textStorage: fieldsInfo[fieldName]?.slotType === 'string' })
|
|
263
309
|
}
|
|
264
310
|
|
|
265
|
-
|
|
311
|
+
/** @type {StorageAdapter['translateCursorValue']} */
|
|
312
|
+
const translateCursorValue = (field, value) => {
|
|
313
|
+
const definition = schemaInfo.schemaStructure?.[field]
|
|
314
|
+
if (isLogicalIdField(field) || definition?.type === 'id') return normalizeBelongsToValue(value)
|
|
315
|
+
return normalizeFilterValueForDefinition(value, definition, {
|
|
316
|
+
isRelationship: Boolean(definition?.belongsTo || definition?.belongsToPolymorphic),
|
|
317
|
+
databaseClient: knex.client.config.client,
|
|
318
|
+
textStorage: fieldsInfo[field]?.slotType === 'string'
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** @type {StorageAdapter['applyResourceScope']} */
|
|
323
|
+
const applyResourceScope = (query, tableAlias) => {
|
|
324
|
+
const prefix = tableAlias ? `${tableAlias}.` : ''
|
|
266
325
|
return query
|
|
267
|
-
.where(canonical.tenantColumn
|
|
268
|
-
.where(canonical.resourceColumn
|
|
326
|
+
.where(`${prefix}${canonical.tenantColumn}`, descriptor.tenant)
|
|
327
|
+
.where(`${prefix}${canonical.resourceColumn}`, descriptor.resource)
|
|
269
328
|
}
|
|
270
329
|
|
|
271
|
-
|
|
330
|
+
/** @type {StorageAdapter['toStorageRow']} */
|
|
331
|
+
const toStorageRow = (attributes, options = {}) => translateCanonicalAttributesForStorage(attributes, descriptor, { ...options, schemaInfo, databaseClient: knex.client.config.client })
|
|
332
|
+
/** @type {StorageAdapter['getFieldValue']} */
|
|
272
333
|
const getFieldValue = (record, fieldName) => (
|
|
273
|
-
isLogicalIdField(fieldName)
|
|
274
|
-
? getCanonicalFieldValue(record, descriptorWithIdProperty, fieldName)
|
|
275
|
-
: getCanonicalFieldValue(record, descriptor, fieldName)
|
|
334
|
+
getCanonicalFieldValue(record, isLogicalIdField(fieldName) ? descriptorWithIdProperty : descriptor, fieldName)
|
|
276
335
|
)
|
|
277
336
|
|
|
278
337
|
return baseAdapter({
|
|
@@ -281,6 +340,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
281
340
|
idColumn: getCanonicalResourceIdColumn(descriptor),
|
|
282
341
|
translateColumn,
|
|
283
342
|
translateFilterValue,
|
|
343
|
+
translateCursorValue,
|
|
284
344
|
applyResourceScope,
|
|
285
345
|
toStorageRow,
|
|
286
346
|
getFieldValue,
|
|
@@ -288,14 +348,34 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
288
348
|
})
|
|
289
349
|
}
|
|
290
350
|
|
|
351
|
+
/** @param {StorageAdapterOptions} options @returns {StorageAdapter} */
|
|
291
352
|
export const createStorageAdapter = ({ knex, schemaInfo }) => {
|
|
292
353
|
if (!schemaInfo) {
|
|
293
354
|
throw new Error('createStorageAdapter requires schemaInfo')
|
|
294
355
|
}
|
|
295
356
|
|
|
296
|
-
|
|
297
|
-
|
|
357
|
+
const descriptor = schemaInfo.descriptor
|
|
358
|
+
if (descriptor?.canonical?.tableName) {
|
|
359
|
+
return createCanonicalAdapter({ knex, schemaInfo, descriptor })
|
|
298
360
|
}
|
|
299
361
|
|
|
300
|
-
return
|
|
362
|
+
return createTableAdapter({ knex, schemaInfo })
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** @param {StorageAdapterLookupOptions} options @returns {(scopeName: string) => StorageAdapter | null} */
|
|
366
|
+
export function createStorageAdapterLookup ({ knex, getResource }) {
|
|
367
|
+
/** @type {Map<string, { schemaInfo: StorageSchemaInfo, adapter: StorageAdapter }>} */
|
|
368
|
+
const adapters = new Map()
|
|
369
|
+
return scopeName => {
|
|
370
|
+
if (!scopeName) return null
|
|
371
|
+
const resource = getResource(scopeName)
|
|
372
|
+
const schemaInfo = resource?.vars?.schemaInfo
|
|
373
|
+
if (!schemaInfo) return null
|
|
374
|
+
const cached = adapters.get(scopeName)
|
|
375
|
+
if (cached && cached.schemaInfo === schemaInfo) return cached.adapter
|
|
376
|
+
const adapter = createStorageAdapter({ knex, schemaInfo })
|
|
377
|
+
adapters.set(scopeName, { adapter, schemaInfo })
|
|
378
|
+
if (resource?.vars) resource.vars.storageAdapter = adapter
|
|
379
|
+
return adapter
|
|
380
|
+
}
|
|
301
381
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
|
|
3
|
+
import { assertFieldNameMap } from '../querying-writing/field-utils.js'
|
|
2
4
|
|
|
5
|
+
/** @import { FieldStorage, StorageConfig, StorageFieldDefinition, StorageInfo, StorageNaming, StorageRow, StorageSchemaInfo, StorageSerializerDetails, StorageWriteOptions } from './storage-types.js' */
|
|
6
|
+
|
|
7
|
+
/** @type {StorageInfo} */
|
|
3
8
|
const EMPTY_STORAGE_INFO = Object.freeze({
|
|
4
9
|
idColumn: 'id',
|
|
5
10
|
storage: Object.freeze({
|
|
@@ -9,6 +14,7 @@ const EMPTY_STORAGE_INFO = Object.freeze({
|
|
|
9
14
|
columns: Object.freeze({})
|
|
10
15
|
})
|
|
11
16
|
|
|
17
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
12
18
|
const isPlainObject = (value) => (
|
|
13
19
|
value !== null &&
|
|
14
20
|
typeof value === 'object' &&
|
|
@@ -18,6 +24,7 @@ const isPlainObject = (value) => (
|
|
|
18
24
|
const SNAKE_CASE_STORAGE_NAMING = 'snake_case'
|
|
19
25
|
const EXACT_STORAGE_NAMING = 'exact'
|
|
20
26
|
|
|
27
|
+
/** @type {Readonly<Partial<Record<string, StorageNaming>>>} */
|
|
21
28
|
const STORAGE_NAMING_ALIASES = Object.freeze({
|
|
22
29
|
snake: SNAKE_CASE_STORAGE_NAMING,
|
|
23
30
|
snake_case: SNAKE_CASE_STORAGE_NAMING,
|
|
@@ -27,11 +34,13 @@ const STORAGE_NAMING_ALIASES = Object.freeze({
|
|
|
27
34
|
verbatim: EXACT_STORAGE_NAMING
|
|
28
35
|
})
|
|
29
36
|
|
|
37
|
+
/** @param {unknown} value */
|
|
30
38
|
const toSnakeCase = (value) => String(value)
|
|
31
39
|
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
32
40
|
.replace(/([A-Z]+)([A-Z][a-z0-9])/g, '$1_$2')
|
|
33
41
|
.toLowerCase()
|
|
34
42
|
|
|
43
|
+
/** @param {unknown} [storage] @returns {StorageInfo['storage']} */
|
|
35
44
|
export const normalizeStorageConfig = (storage = undefined) => {
|
|
36
45
|
if (storage === undefined || storage === null) {
|
|
37
46
|
return {
|
|
@@ -46,7 +55,9 @@ export const normalizeStorageConfig = (storage = undefined) => {
|
|
|
46
55
|
const rawNaming = storage.naming
|
|
47
56
|
const naming = rawNaming === undefined
|
|
48
57
|
? SNAKE_CASE_STORAGE_NAMING
|
|
49
|
-
: STORAGE_NAMING_ALIASES
|
|
58
|
+
: typeof rawNaming === 'string' && Object.hasOwn(STORAGE_NAMING_ALIASES, rawNaming)
|
|
59
|
+
? STORAGE_NAMING_ALIASES[rawNaming]
|
|
60
|
+
: undefined
|
|
50
61
|
|
|
51
62
|
if (!naming) {
|
|
52
63
|
throw new Error(
|
|
@@ -60,6 +71,7 @@ export const normalizeStorageConfig = (storage = undefined) => {
|
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
|
|
74
|
+
/** @param {string} fieldName @param {{ idColumn?: string, storageConfig?: StorageConfig }} [options] */
|
|
63
75
|
const resolveDefaultStorageColumn = (fieldName, { idColumn = 'id', storageConfig = undefined } = {}) => {
|
|
64
76
|
if (fieldName === 'id') {
|
|
65
77
|
return idColumn
|
|
@@ -73,6 +85,12 @@ const resolveDefaultStorageColumn = (fieldName, { idColumn = 'id', storageConfig
|
|
|
73
85
|
return toSnakeCase(fieldName)
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} fieldName
|
|
90
|
+
* @param {StorageFieldDefinition} [definition]
|
|
91
|
+
* @param {{ idColumn?: string, storageConfig?: StorageConfig }} [options]
|
|
92
|
+
* @returns {Omit<FieldStorage, 'definition'>}
|
|
93
|
+
*/
|
|
76
94
|
export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = 'id', storageConfig = undefined } = {}) => {
|
|
77
95
|
const fieldStorage = definition.storage
|
|
78
96
|
|
|
@@ -105,10 +123,14 @@ export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = '
|
|
|
105
123
|
}
|
|
106
124
|
}
|
|
107
125
|
|
|
126
|
+
/** @param {Partial<Pick<StorageSchemaInfo, 'schemaStructure' | 'idProperty' | 'storage'>>} [options] @returns {StorageInfo} */
|
|
108
127
|
export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id', storage = undefined } = {}) => {
|
|
128
|
+
assertFieldNameMap(schemaStructure, 'storage schema')
|
|
109
129
|
const idColumn = idProperty || 'id'
|
|
110
130
|
const storageConfig = normalizeStorageConfig(storage)
|
|
131
|
+
/** @type {StorageInfo['fields']} */
|
|
111
132
|
const fields = {}
|
|
133
|
+
/** @type {StorageInfo['columns']} */
|
|
112
134
|
const columns = {}
|
|
113
135
|
const usedColumns = new Map([[idColumn, 'id']])
|
|
114
136
|
|
|
@@ -151,6 +173,7 @@ export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id', stor
|
|
|
151
173
|
}
|
|
152
174
|
}
|
|
153
175
|
|
|
176
|
+
/** @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {StorageInfo} */
|
|
154
177
|
export const getStorageInfo = (schemaInfo = {}) => {
|
|
155
178
|
if (schemaInfo.storageInfo) {
|
|
156
179
|
return schemaInfo.storageInfo
|
|
@@ -167,10 +190,12 @@ export const getStorageInfo = (schemaInfo = {}) => {
|
|
|
167
190
|
})
|
|
168
191
|
}
|
|
169
192
|
|
|
193
|
+
/** @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {string} */
|
|
170
194
|
export const getIdColumn = (schemaInfo = {}) => {
|
|
171
195
|
return getStorageInfo(schemaInfo).idColumn || schemaInfo.idProperty || 'id'
|
|
172
196
|
}
|
|
173
197
|
|
|
198
|
+
/** @param {Partial<StorageSchemaInfo>} schemaInfo @param {string} fieldName @returns {string} */
|
|
174
199
|
export const getStorageColumn = (schemaInfo = {}, fieldName) => {
|
|
175
200
|
if (!fieldName) return fieldName
|
|
176
201
|
if (fieldName === 'id') return getIdColumn(schemaInfo)
|
|
@@ -179,14 +204,7 @@ export const getStorageColumn = (schemaInfo = {}, fieldName) => {
|
|
|
179
204
|
return fieldStorage?.column || fieldName
|
|
180
205
|
}
|
|
181
206
|
|
|
182
|
-
|
|
183
|
-
if (!columnName) return columnName
|
|
184
|
-
if (columnName === getIdColumn(schemaInfo)) return 'id'
|
|
185
|
-
|
|
186
|
-
const logicalField = getStorageInfo(schemaInfo).columns[columnName]
|
|
187
|
-
return logicalField || columnName
|
|
188
|
-
}
|
|
189
|
-
|
|
207
|
+
/** @param {StorageRow | null | undefined} record @param {Partial<StorageSchemaInfo>} schemaInfo @param {string} fieldName @returns {unknown} */
|
|
190
208
|
export const getFieldValue = (record, schemaInfo = {}, fieldName) => {
|
|
191
209
|
if (!record || !fieldName) return undefined
|
|
192
210
|
|
|
@@ -212,18 +230,57 @@ export const getFieldValue = (record, schemaInfo = {}, fieldName) => {
|
|
|
212
230
|
return record[columnName]
|
|
213
231
|
}
|
|
214
232
|
|
|
215
|
-
|
|
233
|
+
const translated = translateRecordFromStorage(record, schemaInfo)
|
|
234
|
+
return Object.hasOwn(translated, fieldName) ? translated[fieldName] : undefined
|
|
216
235
|
}
|
|
217
236
|
|
|
237
|
+
/** @param {StorageInfo} storageInfo */
|
|
238
|
+
export const assertWritableKnexColumns = (storageInfo) => {
|
|
239
|
+
// Knex's update compiler discards these keys, even in null-prototype input objects.
|
|
240
|
+
const unsupported = new Set(['constructor', 'prototype', '__proto__'])
|
|
241
|
+
for (const [fieldName, field] of Object.entries(storageInfo.fields)) {
|
|
242
|
+
if (field.persisted && unsupported.has(field.column)) {
|
|
243
|
+
throw new Error(`Field '${fieldName}' maps to unsupported Knex storage column '${field.column}'. Set storage.column to a different column name; the public field name can stay the same.`)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (unsupported.has(storageInfo.idColumn)) {
|
|
247
|
+
throw new Error(`Unsupported Knex id column '${storageInfo.idColumn}'. Set idProperty to a different column name.`)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @param {unknown} value @param {Partial<StorageSerializerDetails> & { textStorage?: boolean }} [options] @returns {unknown} */
|
|
252
|
+
export const serializeFieldValueForStorage = (value, {
|
|
253
|
+
fieldName, columnName, definition = {}, schemaInfo, context = null, operation = null, databaseClient, textStorage
|
|
254
|
+
} = {}) => {
|
|
255
|
+
if (definition.storage?.serialize) {
|
|
256
|
+
const serialized = definition.storage.serialize(value, { fieldName, columnName, definition, schemaInfo, context, operation })
|
|
257
|
+
// Probe the optional property directly: proxies can supply it without a `has` result.
|
|
258
|
+
if (serialized && typeof (/** @type {{ then?: unknown }} */ (serialized)).then === 'function') {
|
|
259
|
+
// Own the rejected promise even though asynchronous serializers are invalid.
|
|
260
|
+
Promise.resolve(serialized).catch(() => {})
|
|
261
|
+
throw new Error(`Field '${fieldName}' storage.serialize must be synchronous. Use a field setter for asynchronous preparation.`)
|
|
262
|
+
}
|
|
263
|
+
return serialized
|
|
264
|
+
}
|
|
265
|
+
return normalizeValueForDatabaseStorage(value, definition.type, {
|
|
266
|
+
databaseClient,
|
|
267
|
+
textStorage,
|
|
268
|
+
temporalPrecision: definition.temporalPrecision,
|
|
269
|
+
fieldName,
|
|
270
|
+
resourceType: context?.scopeName || schemaInfo?.tableName
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** @param {StorageRow | null | undefined} attributes @param {Partial<StorageSchemaInfo>} schemaInfo @param {StorageWriteOptions} [options] @returns {StorageRow} */
|
|
218
275
|
export const translateAttributesForStorage = (attributes, schemaInfo = {}, options = {}) => {
|
|
219
276
|
if (!attributes) return attributes || {}
|
|
220
277
|
|
|
221
|
-
const { context = null, operation = null } = options
|
|
278
|
+
const { context = null, operation = null, databaseClient } = options
|
|
222
279
|
const storageInfo = getStorageInfo(schemaInfo)
|
|
223
280
|
const computed = schemaInfo.computed || {}
|
|
224
281
|
|
|
225
282
|
return Object.entries(attributes).reduce((translated, [fieldName, value]) => {
|
|
226
|
-
if (fieldName
|
|
283
|
+
if (Object.hasOwn(computed, fieldName)) {
|
|
227
284
|
return translated
|
|
228
285
|
}
|
|
229
286
|
|
|
@@ -235,30 +292,23 @@ export const translateAttributesForStorage = (attributes, schemaInfo = {}, optio
|
|
|
235
292
|
const columnName = fieldName === 'id'
|
|
236
293
|
? storageInfo.idColumn
|
|
237
294
|
: fieldStorage?.column || fieldName
|
|
238
|
-
const serializedValue =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
columnName,
|
|
242
|
-
definition: fieldStorage.definition,
|
|
243
|
-
schemaInfo,
|
|
244
|
-
context,
|
|
245
|
-
operation
|
|
246
|
-
})
|
|
247
|
-
: normalizeValueForDatabaseStorage(value, fieldStorage?.definition?.type, {
|
|
248
|
-
temporalPrecision: fieldStorage?.definition?.temporalPrecision,
|
|
249
|
-
fieldName,
|
|
250
|
-
resourceType: context?.scopeName || schemaInfo.tableName
|
|
251
|
-
})
|
|
295
|
+
const serializedValue = serializeFieldValueForStorage(value, {
|
|
296
|
+
fieldName, columnName, definition: fieldStorage?.definition, schemaInfo, context, operation, databaseClient
|
|
297
|
+
})
|
|
252
298
|
|
|
253
299
|
translated[columnName] = serializedValue
|
|
254
300
|
return translated
|
|
255
|
-
}, {})
|
|
301
|
+
}, /** @type {StorageRow} */ ({}))
|
|
256
302
|
}
|
|
257
303
|
|
|
304
|
+
/** @overload @param {StorageRow} record @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {StorageRow} */
|
|
305
|
+
/** @overload @param {StorageRow | null | undefined} record @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {StorageRow | null | undefined} */
|
|
306
|
+
/** @param {StorageRow | null | undefined} record @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {StorageRow | null | undefined} */
|
|
258
307
|
export const translateRecordFromStorage = (record, schemaInfo = {}) => {
|
|
259
308
|
if (!record || !schemaInfo) return record
|
|
260
309
|
|
|
261
310
|
const storageInfo = getStorageInfo(schemaInfo)
|
|
311
|
+
/** @type {StorageRow} */
|
|
262
312
|
const translated = {}
|
|
263
313
|
|
|
264
314
|
for (const [columnName, value] of Object.entries(record)) {
|
|
@@ -267,7 +317,7 @@ export const translateRecordFromStorage = (record, schemaInfo = {}) => {
|
|
|
267
317
|
continue
|
|
268
318
|
}
|
|
269
319
|
|
|
270
|
-
const logicalField = storageInfo.columns[columnName]
|
|
320
|
+
const logicalField = Object.hasOwn(storageInfo.columns, columnName) ? storageInfo.columns[columnName] : undefined
|
|
271
321
|
if (logicalField) {
|
|
272
322
|
translated[logicalField] = value
|
|
273
323
|
continue
|