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,21 +1,29 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import {
|
|
2
|
-
getFieldValue as
|
|
3
|
+
getFieldValue as getTableFieldValue,
|
|
3
4
|
getIdColumn,
|
|
4
5
|
getStorageInfo,
|
|
5
6
|
getStorageColumn,
|
|
6
7
|
translateAttributesForStorage,
|
|
8
|
+
serializeFieldValueForStorage,
|
|
9
|
+
assertWritableKnexColumns,
|
|
7
10
|
} from './storage-mapping.js'
|
|
8
11
|
import {
|
|
9
12
|
getCanonicalFieldValue,
|
|
10
13
|
getCanonicalResourceIdColumn,
|
|
11
14
|
translateCanonicalAttributesForStorage,
|
|
12
15
|
} from './canonical-storage-mapping.js'
|
|
16
|
+
import { assertScalarQueryField } from '../querying-writing/field-utils.js'
|
|
13
17
|
import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
|
|
14
18
|
|
|
19
|
+
/** @import { BaseStorageAdapterOptions, CanonicalDescriptor, SelectTranslator, StorageAdapter, StorageAdapterOptions, StorageAdapterLookupOptions, StorageSchemaInfo, StorageFieldDefinition } from './storage-types.js' */
|
|
20
|
+
|
|
21
|
+
/** @template T @param {T} value @returns {T} */
|
|
15
22
|
const passthrough = (value) => value
|
|
23
|
+
/** @type {StorageAdapter['translateFilterValue']} */
|
|
16
24
|
const identityTranslate = (_field, value) => value
|
|
17
|
-
const identityScope = (query) => query
|
|
18
25
|
|
|
26
|
+
/** @type {StorageAdapter['selectColumns']} */
|
|
19
27
|
const selectColumnsOnBuilder = (builder, columns) => {
|
|
20
28
|
if (!columns) return builder
|
|
21
29
|
if (Array.isArray(columns)) {
|
|
@@ -26,16 +34,16 @@ const selectColumnsOnBuilder = (builder, columns) => {
|
|
|
26
34
|
return builder
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
/** @param {unknown} value @returns {unknown} */
|
|
31
38
|
const normalizeBelongsToValue = (value) => {
|
|
32
39
|
if (value === null || value === undefined) return value
|
|
33
40
|
if (Array.isArray(value)) {
|
|
34
|
-
return
|
|
41
|
+
return value.map(entry => normalizeBelongsToValue(entry))
|
|
35
42
|
}
|
|
36
43
|
return String(value)
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
/** @param {unknown} value @returns {boolean} */
|
|
39
47
|
const normalizeBooleanValue = (value) => {
|
|
40
48
|
if (typeof value === 'boolean') return value
|
|
41
49
|
if (typeof value === 'string') {
|
|
@@ -50,13 +58,16 @@ const normalizeBooleanValue = (value) => {
|
|
|
50
58
|
return Boolean(value)
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
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 } = {}) => {
|
|
54
68
|
if (value === null || value === undefined) return value
|
|
55
69
|
if (Array.isArray(value)) {
|
|
56
|
-
return
|
|
57
|
-
value,
|
|
58
|
-
(entry) => normalizeFilterValueForDefinition(entry, definition, { isRelationship })
|
|
59
|
-
)
|
|
70
|
+
return value.map(entry => normalizeFilterValueForDefinition(entry, definition, { isRelationship, databaseClient, textStorage }))
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
if (isRelationship) {
|
|
@@ -79,6 +90,8 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
|
|
|
79
90
|
|
|
80
91
|
if (['date', 'dateTime', 'time'].includes(type)) {
|
|
81
92
|
return normalizeValueForDatabaseStorage(value, type, {
|
|
93
|
+
databaseClient,
|
|
94
|
+
textStorage,
|
|
82
95
|
temporalPrecision: definition?.temporalPrecision
|
|
83
96
|
})
|
|
84
97
|
}
|
|
@@ -86,11 +99,9 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
|
|
|
86
99
|
return value
|
|
87
100
|
}
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
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} */
|
|
94
105
|
export const createSelectTranslator = (adapter) => {
|
|
95
106
|
if (!adapter) return null
|
|
96
107
|
|
|
@@ -119,40 +130,23 @@ export const createSelectTranslator = (adapter) => {
|
|
|
119
130
|
}
|
|
120
131
|
}
|
|
121
132
|
|
|
122
|
-
|
|
123
|
-
if (!adapter || !fields) return fields
|
|
124
|
-
|
|
125
|
-
const translateField = createSelectTranslator(adapter)
|
|
126
|
-
|
|
127
|
-
return fields.map((field) => {
|
|
128
|
-
if (typeof field !== 'string') return field
|
|
129
|
-
if (field === '*') return '*'
|
|
130
|
-
|
|
131
|
-
const aliasMatch = field.match(/\s+as\s+/i)
|
|
132
|
-
if (aliasMatch) {
|
|
133
|
-
const [source, alias] = field.split(/\s+as\s+/i)
|
|
134
|
-
const translatedSource = translateSourceColumn(source.trim(), adapter)
|
|
135
|
-
return `${translatedSource} as ${alias.trim()}`
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return translateField(field)
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
133
|
+
/** @param {BaseStorageAdapterOptions} options @returns {StorageAdapter} */
|
|
142
134
|
const baseAdapter = ({
|
|
143
135
|
knex,
|
|
144
136
|
tableName,
|
|
145
137
|
idColumn,
|
|
146
138
|
translateColumn,
|
|
147
139
|
translateFilterValue = identityTranslate,
|
|
148
|
-
|
|
140
|
+
translateCursorValue = identityTranslate,
|
|
141
|
+
applyResourceScope = passthrough,
|
|
149
142
|
toStorageRow = passthrough,
|
|
150
143
|
getFieldValue = (record, fieldName) => record?.[fieldName],
|
|
151
144
|
isCanonical = false,
|
|
152
145
|
}) => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
/** @type {StorageAdapter['buildBaseQuery']} */
|
|
147
|
+
const buildBaseQuery = ({ transaction, tableAlias } = {}) => {
|
|
148
|
+
const query = (transaction || knex)(tableAlias ? { [tableAlias]: tableName } : tableName)
|
|
149
|
+
return applyResourceScope(query, tableAlias)
|
|
156
150
|
}
|
|
157
151
|
|
|
158
152
|
return {
|
|
@@ -161,28 +155,45 @@ const baseAdapter = ({
|
|
|
161
155
|
getIdColumn: () => idColumn,
|
|
162
156
|
translateColumn,
|
|
163
157
|
translateFilterValue,
|
|
158
|
+
translateCursorValue,
|
|
164
159
|
applyResourceScope,
|
|
165
160
|
toStorageRow,
|
|
166
161
|
getFieldValue,
|
|
167
162
|
buildBaseQuery,
|
|
168
|
-
selectColumns:
|
|
163
|
+
selectColumns: selectColumnsOnBuilder,
|
|
169
164
|
}
|
|
170
165
|
}
|
|
171
166
|
|
|
172
|
-
|
|
167
|
+
/** @param {StorageAdapterOptions} options @returns {StorageAdapter} */
|
|
168
|
+
const createTableAdapter = ({ knex, schemaInfo }) => {
|
|
169
|
+
assertWritableKnexColumns(getStorageInfo(schemaInfo))
|
|
173
170
|
const tableName = schemaInfo.tableName
|
|
174
171
|
const idColumn = getIdColumn(schemaInfo)
|
|
175
172
|
|
|
173
|
+
/** @type {StorageAdapter['translateColumn']} */
|
|
176
174
|
const translateColumn = (field) => getStorageColumn(schemaInfo, field)
|
|
177
|
-
|
|
178
|
-
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']} */
|
|
179
188
|
const translateFilterValue = (field, value) => {
|
|
180
189
|
const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
181
190
|
const fieldName = searchField?.actualField || field
|
|
182
191
|
const schemaField = schemaInfo.schemaStructure?.[fieldName]
|
|
192
|
+
assertScalarQueryField(schemaField, fieldName, 'filter')
|
|
183
193
|
const fieldStorage = getStorageInfo(schemaInfo).fields[fieldName]
|
|
184
194
|
if (fieldStorage?.serialize) {
|
|
185
|
-
|
|
195
|
+
/** @param {unknown} entry */
|
|
196
|
+
const serialize = (entry) => serializeFieldValueForStorage(entry, {
|
|
186
197
|
fieldName,
|
|
187
198
|
columnName: fieldStorage.column,
|
|
188
199
|
definition: fieldStorage.definition,
|
|
@@ -198,7 +209,7 @@ const createLegacyAdapter = ({ knex, schemaInfo }) => {
|
|
|
198
209
|
schemaField?.belongsTo ||
|
|
199
210
|
schemaField?.belongsToPolymorphic
|
|
200
211
|
)
|
|
201
|
-
return normalizeFilterValueForDefinition(value, definition, { isRelationship })
|
|
212
|
+
return normalizeFilterValueForDefinition(value, definition, { isRelationship, databaseClient: knex.client.config.client })
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
return baseAdapter({
|
|
@@ -207,25 +218,28 @@ const createLegacyAdapter = ({ knex, schemaInfo }) => {
|
|
|
207
218
|
idColumn,
|
|
208
219
|
translateColumn,
|
|
209
220
|
translateFilterValue,
|
|
210
|
-
|
|
221
|
+
translateCursorValue,
|
|
222
|
+
applyResourceScope: passthrough,
|
|
211
223
|
toStorageRow,
|
|
212
224
|
getFieldValue,
|
|
213
225
|
isCanonical: false,
|
|
214
226
|
})
|
|
215
227
|
}
|
|
216
228
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const canonical = descriptor.canonical
|
|
229
|
+
/** @param {StorageAdapterOptions & { descriptor: CanonicalDescriptor }} options @returns {StorageAdapter} */
|
|
230
|
+
const createCanonicalAdapter = ({ knex, schemaInfo, descriptor }) => {
|
|
231
|
+
const canonical = descriptor.canonical
|
|
220
232
|
const canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
221
233
|
const fieldsInfo = descriptor.fields || {}
|
|
222
234
|
const belongsToInfo = descriptor.belongsTo || {}
|
|
223
235
|
const idProperty = schemaInfo.idProperty || descriptor.idProperty || 'id'
|
|
236
|
+
/** @param {string} field */
|
|
224
237
|
const isLogicalIdField = (field) => field === 'id' || field === idProperty
|
|
225
238
|
const descriptorWithIdProperty = descriptor.idProperty === idProperty
|
|
226
239
|
? descriptor
|
|
227
240
|
: { ...descriptor, idProperty }
|
|
228
241
|
|
|
242
|
+
/** @type {StorageAdapter['translateColumn']} */
|
|
229
243
|
const translateColumn = (field) => {
|
|
230
244
|
if (!field) return field
|
|
231
245
|
if (isLogicalIdField(field)) return getCanonicalResourceIdColumn(descriptor)
|
|
@@ -251,7 +265,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
251
265
|
if (!canonicalEntry && field.endsWith('_type')) {
|
|
252
266
|
const alias = field.slice(0, -5)
|
|
253
267
|
const aliasEntry = canonicalFieldMap[alias]
|
|
254
|
-
if (aliasEntry?.typeSlot) return aliasEntry.typeSlot
|
|
268
|
+
if (typeof aliasEntry === 'object' && aliasEntry?.typeSlot) return aliasEntry.typeSlot
|
|
255
269
|
}
|
|
256
270
|
|
|
257
271
|
const fieldInfo = fieldsInfo[field]
|
|
@@ -263,9 +277,27 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
263
277
|
return field
|
|
264
278
|
}
|
|
265
279
|
|
|
280
|
+
/** @type {StorageAdapter['translateFilterValue']} */
|
|
266
281
|
const translateFilterValue = (field, value) => {
|
|
267
282
|
const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
268
|
-
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)
|
|
269
301
|
const isRelationship = Boolean(
|
|
270
302
|
searchField?.isRelationship ||
|
|
271
303
|
schemaField?.belongsTo ||
|
|
@@ -273,20 +305,33 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
273
305
|
)
|
|
274
306
|
const definition = searchField || schemaField || {}
|
|
275
307
|
|
|
276
|
-
return normalizeFilterValueForDefinition(value, definition, { isRelationship })
|
|
308
|
+
return normalizeFilterValueForDefinition(value, definition, { isRelationship, databaseClient: knex.client.config.client, textStorage: fieldsInfo[fieldName]?.slotType === 'string' })
|
|
277
309
|
}
|
|
278
310
|
|
|
279
|
-
|
|
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}.` : ''
|
|
280
325
|
return query
|
|
281
|
-
.where(canonical.tenantColumn
|
|
282
|
-
.where(canonical.resourceColumn
|
|
326
|
+
.where(`${prefix}${canonical.tenantColumn}`, descriptor.tenant)
|
|
327
|
+
.where(`${prefix}${canonical.resourceColumn}`, descriptor.resource)
|
|
283
328
|
}
|
|
284
329
|
|
|
285
|
-
|
|
330
|
+
/** @type {StorageAdapter['toStorageRow']} */
|
|
331
|
+
const toStorageRow = (attributes, options = {}) => translateCanonicalAttributesForStorage(attributes, descriptor, { ...options, schemaInfo, databaseClient: knex.client.config.client })
|
|
332
|
+
/** @type {StorageAdapter['getFieldValue']} */
|
|
286
333
|
const getFieldValue = (record, fieldName) => (
|
|
287
|
-
isLogicalIdField(fieldName)
|
|
288
|
-
? getCanonicalFieldValue(record, descriptorWithIdProperty, fieldName)
|
|
289
|
-
: getCanonicalFieldValue(record, descriptor, fieldName)
|
|
334
|
+
getCanonicalFieldValue(record, isLogicalIdField(fieldName) ? descriptorWithIdProperty : descriptor, fieldName)
|
|
290
335
|
)
|
|
291
336
|
|
|
292
337
|
return baseAdapter({
|
|
@@ -295,6 +340,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
295
340
|
idColumn: getCanonicalResourceIdColumn(descriptor),
|
|
296
341
|
translateColumn,
|
|
297
342
|
translateFilterValue,
|
|
343
|
+
translateCursorValue,
|
|
298
344
|
applyResourceScope,
|
|
299
345
|
toStorageRow,
|
|
300
346
|
getFieldValue,
|
|
@@ -302,14 +348,34 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
|
302
348
|
})
|
|
303
349
|
}
|
|
304
350
|
|
|
351
|
+
/** @param {StorageAdapterOptions} options @returns {StorageAdapter} */
|
|
305
352
|
export const createStorageAdapter = ({ knex, schemaInfo }) => {
|
|
306
353
|
if (!schemaInfo) {
|
|
307
354
|
throw new Error('createStorageAdapter requires schemaInfo')
|
|
308
355
|
}
|
|
309
356
|
|
|
310
|
-
|
|
311
|
-
|
|
357
|
+
const descriptor = schemaInfo.descriptor
|
|
358
|
+
if (descriptor?.canonical?.tableName) {
|
|
359
|
+
return createCanonicalAdapter({ knex, schemaInfo, descriptor })
|
|
312
360
|
}
|
|
313
361
|
|
|
314
|
-
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
|
+
}
|
|
315
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
|