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,11 +1,40 @@
|
|
|
1
1
|
import { createSchema } from 'json-rest-schema'
|
|
2
|
-
import { RestApiTemporalDataError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
+
import { RestApiTemporalDataError, RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
3
|
+
import { filterResponseFields } from './field-utils.js'
|
|
4
|
+
import { wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
5
|
+
import { COMPUTED_DEPENDENCIES_KEY } from './knex-constants.js'
|
|
6
|
+
import { getTemporalStorageCapabilities, SERIALIZATION_CAPABILITIES } from './database-capabilities.js'
|
|
3
7
|
|
|
4
8
|
const DATE_VALUE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
|
5
9
|
const RFC3339_DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/
|
|
6
|
-
const SQL_DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2}) ((?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?)
|
|
10
|
+
const SQL_DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2}) ((?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?)(?:([+-])([01]\d|2[0-3])(?::([0-5]\d))?(?::([0-5]\d))?)?( BC)?$/
|
|
7
11
|
const INTEGER_VALUE_PATTERN = /^-?\d+$/
|
|
8
12
|
const temporalContractCache = new Map()
|
|
13
|
+
const CACHED_TEMPORAL_TYPES = new Set(['date', 'dateTime', 'time', 'epochMilliseconds', 'epochSeconds'])
|
|
14
|
+
|
|
15
|
+
export function applyDatabaseReadOptions (query) {
|
|
16
|
+
const client = query.client.config.client
|
|
17
|
+
if (client === 'pg' || client === 'postgresql') {
|
|
18
|
+
const types = query.client.config.connection?.types || query.client.driver.types
|
|
19
|
+
query.options({
|
|
20
|
+
types: {
|
|
21
|
+
getTypeParser: (oid, format) => [1082, 1114, 1184].includes(oid) && format !== 'binary'
|
|
22
|
+
? value => value
|
|
23
|
+
: types.getTypeParser(oid, format)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
} else if (client === 'mysql' || client === 'mysql2') {
|
|
27
|
+
query.options({ dateStrings: true, supportBigNumbers: true })
|
|
28
|
+
}
|
|
29
|
+
return query
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// SQLite's driver decodes INTEGER as Number. Preserve only values it cannot represent.
|
|
33
|
+
// Keep this in SELECT/RETURNING expressions; predicates and ordering use native columns.
|
|
34
|
+
export function databaseIdentityExpression (db, column) {
|
|
35
|
+
if (!['better-sqlite3', 'sqlite3'].includes(db.client.config.client)) return column
|
|
36
|
+
return db.raw("CASE WHEN typeof(??) = 'integer' AND (?? > 9007199254740991 OR ?? < -9007199254740991) THEN CAST(?? AS TEXT) ELSE ?? END", [column, column, column, column, column])
|
|
37
|
+
}
|
|
9
38
|
|
|
10
39
|
const limitFractionalSecondPrecision = (value, temporalPrecision) => {
|
|
11
40
|
if (!Number.isInteger(temporalPrecision)) return value
|
|
@@ -21,15 +50,18 @@ const limitFractionalSecondPrecision = (value, temporalPrecision) => {
|
|
|
21
50
|
|
|
22
51
|
const getTemporalContract = (type, temporalPrecision) => {
|
|
23
52
|
const cacheKey = `${type}:${Number.isInteger(temporalPrecision) ? temporalPrecision : 'default'}`
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
// Cache common SQL precision; unusual declarations retain validation without permanent entries.
|
|
54
|
+
const cacheable = CACHED_TEMPORAL_TYPES.has(type) &&
|
|
55
|
+
(!Number.isInteger(temporalPrecision) || (temporalPrecision >= 0 && temporalPrecision <= 6))
|
|
56
|
+
if (cacheable && temporalContractCache.has(cacheKey)) return temporalContractCache.get(cacheKey)
|
|
57
|
+
const contract = createSchema({
|
|
58
|
+
value: {
|
|
59
|
+
type,
|
|
60
|
+
...(Number.isInteger(temporalPrecision) ? { temporalPrecision } : {})
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
if (cacheable) temporalContractCache.set(cacheKey, contract)
|
|
64
|
+
return contract
|
|
33
65
|
}
|
|
34
66
|
|
|
35
67
|
const throwTemporalDataError = ({ fieldName, resourceType, type, source }) => {
|
|
@@ -71,12 +103,12 @@ const parseEpochDatabaseValue = (value) => {
|
|
|
71
103
|
|
|
72
104
|
const parseKnownDatabaseDate = (value, { fieldName, resourceType, type, source }) => {
|
|
73
105
|
if (value instanceof Date) {
|
|
74
|
-
if (!Number.isNaN(value.getTime())) return value
|
|
106
|
+
if (!Number.isNaN(value.getTime())) return value.toISOString()
|
|
75
107
|
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
76
108
|
}
|
|
77
109
|
|
|
78
110
|
const epochDate = parseEpochDatabaseValue(value)
|
|
79
|
-
if (epochDate) return epochDate
|
|
111
|
+
if (epochDate) return epochDate.toISOString()
|
|
80
112
|
|
|
81
113
|
if (typeof value !== 'string') {
|
|
82
114
|
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
@@ -86,9 +118,17 @@ const parseKnownDatabaseDate = (value, { fieldName, resourceType, type, source }
|
|
|
86
118
|
const sqlMatch = SQL_DATETIME_PATTERN.exec(trimmed)
|
|
87
119
|
const rfc3339Match = RFC3339_DATETIME_PATTERN.exec(trimmed)
|
|
88
120
|
let dateTimeValue = null
|
|
121
|
+
let offsetSeconds = 0
|
|
89
122
|
|
|
90
123
|
if (sqlMatch) {
|
|
91
|
-
|
|
124
|
+
const date = sqlMatch[7] && sqlMatch[1].startsWith('0001-')
|
|
125
|
+
? sqlMatch[1].replace(/^0001/, '0000')
|
|
126
|
+
: sqlMatch[1]
|
|
127
|
+
if (sqlMatch[7] && !sqlMatch[1].startsWith('0001-')) {
|
|
128
|
+
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
129
|
+
}
|
|
130
|
+
dateTimeValue = `${date}T${sqlMatch[2]}Z`
|
|
131
|
+
offsetSeconds = (Number(sqlMatch[4] || 0) * 3600 + Number(sqlMatch[5] || 0) * 60 + Number(sqlMatch[6] || 0)) * (sqlMatch[3] === '-' ? -1 : 1)
|
|
92
132
|
} else if (rfc3339Match) {
|
|
93
133
|
dateTimeValue = trimmed
|
|
94
134
|
}
|
|
@@ -102,11 +142,15 @@ const parseKnownDatabaseDate = (value, { fieldName, resourceType, type, source }
|
|
|
102
142
|
resourceType,
|
|
103
143
|
source
|
|
104
144
|
})
|
|
105
|
-
const parsed = new Date(dateTimeValue)
|
|
145
|
+
const parsed = new Date(new Date(dateTimeValue).getTime() - offsetSeconds * 1000)
|
|
106
146
|
if (Number.isNaN(parsed.getTime())) {
|
|
107
147
|
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
108
148
|
}
|
|
109
|
-
|
|
149
|
+
// Date handles the offset, but cannot retain digits beyond milliseconds.
|
|
150
|
+
const fraction = /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(dateTimeValue)?.[1]
|
|
151
|
+
return fraction
|
|
152
|
+
? parsed.toISOString().replace(/\.\d{3}Z$/, `.${fraction.padEnd(3, '0')}Z`)
|
|
153
|
+
: parsed.toISOString()
|
|
110
154
|
}
|
|
111
155
|
|
|
112
156
|
/**
|
|
@@ -130,27 +174,31 @@ export function normalizeDateValue (value, type, {
|
|
|
130
174
|
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
131
175
|
timeValue = limitFractionalSecondPrecision(value.toISOString().slice(11, -1), temporalPrecision)
|
|
132
176
|
}
|
|
133
|
-
|
|
177
|
+
const validated = validateTemporalJsonValue(timeValue, type, {
|
|
134
178
|
temporalPrecision,
|
|
135
179
|
fieldName,
|
|
136
180
|
resourceType,
|
|
137
181
|
source
|
|
138
182
|
})
|
|
183
|
+
const [whole, fraction = ''] = validated.split('.')
|
|
184
|
+
const seconds = whole.length === 5 ? `${whole}:00` : whole
|
|
185
|
+
const digits = Number.isInteger(temporalPrecision) ? fraction.padEnd(temporalPrecision, '0') : fraction.replace(/0+$/, '')
|
|
186
|
+
return `${seconds}${digits ? `.${digits}` : ''}`
|
|
139
187
|
}
|
|
140
188
|
|
|
141
|
-
|
|
142
|
-
|
|
189
|
+
const dateValue = typeof value === 'string' ? value.trim().replace(/^0001-(\d{2}-\d{2}) BC$/, '0000-$1') : value
|
|
190
|
+
if (type === 'date' && typeof dateValue === 'string' && DATE_VALUE_PATTERN.test(dateValue)) {
|
|
191
|
+
return validateTemporalJsonValue(dateValue, type, {
|
|
143
192
|
fieldName,
|
|
144
193
|
resourceType,
|
|
145
194
|
source
|
|
146
195
|
})
|
|
147
196
|
}
|
|
148
197
|
|
|
149
|
-
const
|
|
150
|
-
const isoValue = dateValue.toISOString()
|
|
198
|
+
const isoValue = parseKnownDatabaseDate(value, { fieldName, resourceType, type, source })
|
|
151
199
|
const normalized = type === 'date'
|
|
152
200
|
? isoValue.slice(0, 10)
|
|
153
|
-
: limitFractionalSecondPrecision(isoValue, temporalPrecision)
|
|
201
|
+
: limitFractionalSecondPrecision(isoValue.replace(/(\.\d{3}\d*?)0+Z$/, '$1Z'), temporalPrecision)
|
|
154
202
|
return validateTemporalJsonValue(normalized, type, {
|
|
155
203
|
temporalPrecision,
|
|
156
204
|
fieldName,
|
|
@@ -160,30 +208,57 @@ export function normalizeDateValue (value, type, {
|
|
|
160
208
|
}
|
|
161
209
|
|
|
162
210
|
/**
|
|
163
|
-
* Converts
|
|
211
|
+
* Converts structured JSON and temporal values to the values expected by
|
|
164
212
|
* database drivers. JSON schema validation keeps date and dateTime values as
|
|
165
213
|
* strings; database writes and comparisons must normalize them consistently.
|
|
166
214
|
* Time values remain strings because database drivers accept their wire shape.
|
|
215
|
+
* @param {unknown} value
|
|
216
|
+
* @param {string | undefined} type
|
|
217
|
+
* @param {{ databaseClient?: string, textStorage?: boolean, temporalPrecision?: number, fieldName?: string, resourceType?: string, source?: string }} [options]
|
|
218
|
+
* @returns {unknown}
|
|
167
219
|
*/
|
|
168
220
|
export function normalizeValueForDatabaseStorage (value, type, {
|
|
221
|
+
databaseClient,
|
|
222
|
+
textStorage = false,
|
|
169
223
|
temporalPrecision,
|
|
170
224
|
fieldName,
|
|
171
225
|
resourceType,
|
|
172
226
|
source = 'storage'
|
|
173
227
|
} = {}) {
|
|
228
|
+
if (SERIALIZATION_CAPABILITIES.structuredTypes.includes(type)) {
|
|
229
|
+
const structured = normalizeStructuredValue(value, type, { fieldName, resourceType, source })
|
|
230
|
+
try {
|
|
231
|
+
return structured == null ? structured : JSON.stringify(structured)
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw wrapUnexpectedError(error, {
|
|
234
|
+
message: `Cannot serialize ${type} field '${[resourceType, fieldName].filter(Boolean).join('.')}' as JSON`,
|
|
235
|
+
context: { fieldName, resourceType, source, phase: 'serialization' }
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
174
239
|
if (!['date', 'dateTime', 'time'].includes(type)) return value
|
|
175
240
|
if (value === null || value === undefined) return null
|
|
241
|
+
const capabilities = getTemporalStorageCapabilities(databaseClient, { textStorage })
|
|
176
242
|
|
|
177
243
|
if (type === 'time') {
|
|
178
244
|
if (typeof value !== 'string') {
|
|
179
245
|
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
180
246
|
}
|
|
181
|
-
|
|
247
|
+
const validated = validateTemporalJsonValue(value, type, {
|
|
182
248
|
temporalPrecision,
|
|
183
249
|
fieldName,
|
|
184
250
|
resourceType,
|
|
185
251
|
source
|
|
186
252
|
})
|
|
253
|
+
const normalized = normalizeDateValue(validated, type, { temporalPrecision, fieldName, resourceType, source })
|
|
254
|
+
if (capabilities.timeFractionDigits !== null && (normalized.split('.')[1]?.length || 0) > capabilities.timeFractionDigits) {
|
|
255
|
+
const field = fieldName ? `data.attributes.${fieldName}` : 'value'
|
|
256
|
+
throw new RestApiValidationError('SQL time storage supports at most six fractional digits.', {
|
|
257
|
+
fields: [field],
|
|
258
|
+
violations: [{ field, rule: 'storage_precision', message: 'Use a text storage mapping for finer time precision.' }]
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
return normalized
|
|
187
262
|
}
|
|
188
263
|
|
|
189
264
|
let publicValue = value
|
|
@@ -202,91 +277,78 @@ export function normalizeValueForDatabaseStorage (value, type, {
|
|
|
202
277
|
resourceType,
|
|
203
278
|
source
|
|
204
279
|
})
|
|
280
|
+
if (type === 'date' && textStorage) return validatedValue
|
|
281
|
+
const fraction = type === 'dateTime'
|
|
282
|
+
? /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(validatedValue)?.[1]
|
|
283
|
+
: null
|
|
284
|
+
if (fraction && /[1-9]/.test(fraction.slice(capabilities.dateTimeFractionDigits))) {
|
|
285
|
+
const field = fieldName ? `data.attributes.${fieldName}` : 'value'
|
|
286
|
+
throw new RestApiValidationError('Built-in dateTime storage supports millisecond precision.', {
|
|
287
|
+
fields: [field],
|
|
288
|
+
violations: [{
|
|
289
|
+
field,
|
|
290
|
+
rule: 'storage_precision',
|
|
291
|
+
message: 'Submillisecond values require a custom storage serializer.'
|
|
292
|
+
}]
|
|
293
|
+
})
|
|
294
|
+
}
|
|
205
295
|
const databaseValue = new Date(
|
|
206
296
|
type === 'date' ? `${validatedValue}T00:00:00Z` : validatedValue
|
|
207
297
|
)
|
|
208
298
|
if (Number.isNaN(databaseValue.getTime())) {
|
|
209
299
|
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
210
300
|
}
|
|
301
|
+
if (databaseValue.getUTCFullYear() < 0 || databaseValue.getUTCFullYear() > capabilities.maxYear) {
|
|
302
|
+
const field = fieldName ? `data.attributes.${fieldName}` : 'value'
|
|
303
|
+
throw new RestApiValidationError('UTC dateTime must remain within years 0000 through 9999.', {
|
|
304
|
+
fields: [field],
|
|
305
|
+
violations: [{ field, rule: 'storage_range', message: 'The timezone offset moves this value outside the public dateTime range.' }]
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
if (databaseClient === 'pg' || databaseClient === 'postgresql') {
|
|
309
|
+
const iso = databaseValue.toISOString()
|
|
310
|
+
const sqlValue = type === 'date' ? iso.slice(0, 10) : iso.replace('T', ' ')
|
|
311
|
+
return sqlValue.startsWith('0000-') ? `${sqlValue.replace(/^0000/, '0001')} BC` : sqlValue
|
|
312
|
+
}
|
|
313
|
+
if (databaseClient === 'mysql' || databaseClient === 'mysql2') {
|
|
314
|
+
if (databaseValue.getUTCFullYear() < (type === 'date' ? capabilities.dateMinYear : capabilities.dateTimeMinYear)) {
|
|
315
|
+
const field = fieldName ? `data.attributes.${fieldName}` : 'value'
|
|
316
|
+
throw new RestApiValidationError('MySQL date storage supports years 1000 through 9999.', {
|
|
317
|
+
fields: [field],
|
|
318
|
+
violations: [{ field, rule: 'storage_range', message: 'Use a custom storage mapping for dates outside the MySQL date range.' }]
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
const iso = databaseValue.toISOString()
|
|
322
|
+
return type === 'date' ? iso.slice(0, 10) : iso.replace('T', ' ').replace(/Z$/, '')
|
|
323
|
+
}
|
|
211
324
|
return databaseValue
|
|
212
325
|
}
|
|
213
326
|
|
|
327
|
+
export function normalizeStructuredValue (value, type, { fieldName, resourceType, source = 'database' } = {}) {
|
|
328
|
+
if (value === null || value === undefined) return value
|
|
329
|
+
try {
|
|
330
|
+
// Writes receive logical values; only reads decode a driver's JSON text.
|
|
331
|
+
const parsed = source !== 'storage' && typeof value === 'string' ? JSON.parse(value) : value
|
|
332
|
+
const valid = type === 'array'
|
|
333
|
+
? Array.isArray(parsed)
|
|
334
|
+
: parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) &&
|
|
335
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(parsed))
|
|
336
|
+
if (!valid) throw new TypeError(`Expected a JSON ${type}`)
|
|
337
|
+
return parsed
|
|
338
|
+
} catch (error) {
|
|
339
|
+
throw wrapUnexpectedError(error, {
|
|
340
|
+
message: `Invalid ${source} value for ${type} field '${[resourceType, fieldName].filter(Boolean).join('.')}'`,
|
|
341
|
+
context: { fieldName, resourceType, source, phase: 'normalization' }
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
214
346
|
/**
|
|
215
347
|
* Normalizes database values in an attributes object
|
|
216
348
|
*
|
|
217
349
|
* @param {Object} attributes - The attributes object to normalize
|
|
218
350
|
* @param {Object} schemaStructure - The schema structure defining field types
|
|
219
351
|
* @returns {Object} The normalized attributes object
|
|
220
|
-
*
|
|
221
|
-
* @example
|
|
222
|
-
* // Input: Raw database values
|
|
223
|
-
* const attributes = {
|
|
224
|
-
* id: 1,
|
|
225
|
-
* is_active: 1, // Boolean as 0/1
|
|
226
|
-
* created_at: '2024-01-15 10:30:00', // MySQL datetime
|
|
227
|
-
* birth_date: '1990-05-20', // Date string
|
|
228
|
-
* shift_time: '09:00:00' // Time string
|
|
229
|
-
* };
|
|
230
|
-
*
|
|
231
|
-
* const schema = {
|
|
232
|
-
* is_active: { type: 'boolean' },
|
|
233
|
-
* created_at: { type: 'dateTime' },
|
|
234
|
-
* birth_date: { type: 'date' },
|
|
235
|
-
* shift_time: { type: 'time' }
|
|
236
|
-
* };
|
|
237
|
-
*
|
|
238
|
-
* const normalized = normalizeAttributes(attributes, schema);
|
|
239
|
-
*
|
|
240
|
-
* // Output: JSON-safe values
|
|
241
|
-
* // {
|
|
242
|
-
* // id: 1,
|
|
243
|
-
* // is_active: true, // 1 → true
|
|
244
|
-
* // created_at: '2024-01-15T10:30:00.000Z',
|
|
245
|
-
* // birth_date: '1990-05-20',
|
|
246
|
-
* // shift_time: '09:00:00'
|
|
247
|
-
* // }
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* // Input: Boolean edge cases
|
|
251
|
-
* const attributes = {
|
|
252
|
-
* flag1: 0, // Number 0
|
|
253
|
-
* flag2: '1', // String '1'
|
|
254
|
-
* flag3: true, // Already boolean
|
|
255
|
-
* flag4: null // Null value
|
|
256
|
-
* };
|
|
257
|
-
*
|
|
258
|
-
* const normalized = normalizeAttributes(attributes, {
|
|
259
|
-
* flag1: { type: 'boolean' },
|
|
260
|
-
* flag2: { type: 'boolean' },
|
|
261
|
-
* flag3: { type: 'boolean' },
|
|
262
|
-
* flag4: { type: 'boolean' }
|
|
263
|
-
* });
|
|
264
|
-
*
|
|
265
|
-
* // Output:
|
|
266
|
-
* // {
|
|
267
|
-
* // flag1: false, // 0 → false
|
|
268
|
-
* // flag2: true, // '1' → true
|
|
269
|
-
* // flag3: true, // unchanged
|
|
270
|
-
* // flag4: null // null preserved
|
|
271
|
-
* // }
|
|
272
|
-
*
|
|
273
|
-
* @description
|
|
274
|
-
* Used by:
|
|
275
|
-
* - normalizeRecordAttributes for each record's attributes
|
|
276
|
-
* - Applied after fetching data from database
|
|
277
|
-
*
|
|
278
|
-
* Purpose:
|
|
279
|
-
* - Handles databases without native boolean support (SQLite, older MySQL)
|
|
280
|
-
* - Normalizes date formats from different databases
|
|
281
|
-
* - Ensures consistent data types in API responses
|
|
282
|
-
* - Preserves null values appropriately
|
|
283
|
-
*
|
|
284
|
-
* Data flow:
|
|
285
|
-
* 1. Creates copy of attributes to avoid mutation
|
|
286
|
-
* 2. Iterates through each field with schema definition
|
|
287
|
-
* 3. Normalizes booleans: 1/0 or '1'/'0' to true/false
|
|
288
|
-
* 4. Normalizes dates using normalizeDateValue
|
|
289
|
-
* 5. Returns new object with normalized values
|
|
290
352
|
*/
|
|
291
353
|
export function normalizeAttributes (attributes, schemaStructure, {
|
|
292
354
|
resourceType,
|
|
@@ -305,6 +367,25 @@ export function normalizeAttributes (attributes, schemaStructure, {
|
|
|
305
367
|
// Skip if no field definition found
|
|
306
368
|
if (!fieldDef) continue
|
|
307
369
|
|
|
370
|
+
if (['object', 'array'].includes(fieldDef.type)) {
|
|
371
|
+
// A custom getter owns decoding its stored representation.
|
|
372
|
+
if (source === 'database' && typeof fieldDef.getter === 'function') continue
|
|
373
|
+
normalized[fieldName] = normalizeStructuredValue(value, fieldDef.type, { fieldName, resourceType, source })
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (fieldDef.type === 'file' && value instanceof Uint8Array) {
|
|
378
|
+
if (source === 'database' && typeof fieldDef.getter === 'function') continue
|
|
379
|
+
try {
|
|
380
|
+
normalized[fieldName] = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(value)
|
|
381
|
+
} catch (error) {
|
|
382
|
+
throw wrapUnexpectedError(error, {
|
|
383
|
+
message: `Invalid ${source} file handle '${[resourceType, fieldName].filter(Boolean).join('.')}'`,
|
|
384
|
+
context: { fieldName, resourceType, source, phase: 'normalization' }
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
308
389
|
// Normalize boolean values
|
|
309
390
|
if (fieldDef.type === 'boolean') {
|
|
310
391
|
if (value === 1 || value === '1') {
|
|
@@ -323,6 +404,12 @@ export function normalizeAttributes (attributes, schemaStructure, {
|
|
|
323
404
|
resourceType,
|
|
324
405
|
source
|
|
325
406
|
})
|
|
407
|
+
} else if (['epochMilliseconds', 'epochSeconds'].includes(fieldDef.type) && value != null) {
|
|
408
|
+
normalized[fieldName] = validateTemporalJsonValue(
|
|
409
|
+
typeof value === 'bigint' ? String(value) : value,
|
|
410
|
+
fieldDef.type,
|
|
411
|
+
{ fieldName, resourceType, source }
|
|
412
|
+
)
|
|
326
413
|
}
|
|
327
414
|
}
|
|
328
415
|
|
|
@@ -330,111 +417,46 @@ export function normalizeAttributes (attributes, schemaStructure, {
|
|
|
330
417
|
}
|
|
331
418
|
|
|
332
419
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* @example
|
|
340
|
-
* // Input: JSON:API response with various data types
|
|
341
|
-
* const response = {
|
|
342
|
-
* data: {
|
|
343
|
-
* type: 'articles',
|
|
344
|
-
* id: '1',
|
|
345
|
-
* attributes: {
|
|
346
|
-
* title: 'My Article',
|
|
347
|
-
* is_published: 1, // Boolean as number
|
|
348
|
-
* published_at: '2024-01-15 10:00:00' // MySQL datetime
|
|
349
|
-
* }
|
|
350
|
-
* },
|
|
351
|
-
* included: [{
|
|
352
|
-
* type: 'users',
|
|
353
|
-
* id: '10',
|
|
354
|
-
* attributes: {
|
|
355
|
-
* name: 'John',
|
|
356
|
-
* is_admin: 0, // Boolean as number
|
|
357
|
-
* last_login: '2024-01-14 15:30:00'
|
|
358
|
-
* }
|
|
359
|
-
* }]
|
|
360
|
-
* };
|
|
361
|
-
*
|
|
362
|
-
* const normalized = normalizeRecordAttributes(response, scopes);
|
|
363
|
-
*
|
|
364
|
-
* // Output: All booleans and dates normalized
|
|
365
|
-
* // {
|
|
366
|
-
* // data: {
|
|
367
|
-
* // type: 'articles',
|
|
368
|
-
* // id: '1',
|
|
369
|
-
* // attributes: {
|
|
370
|
-
* // title: 'My Article',
|
|
371
|
-
* // is_published: true,
|
|
372
|
-
* // published_at: '2024-01-15T10:00:00.000Z'
|
|
373
|
-
* // }
|
|
374
|
-
* // },
|
|
375
|
-
* // included: [{
|
|
376
|
-
* // type: 'users',
|
|
377
|
-
* // id: '10',
|
|
378
|
-
* // attributes: {
|
|
379
|
-
* // name: 'John',
|
|
380
|
-
* // is_admin: false,
|
|
381
|
-
* // last_login: '2024-01-14T15:30:00.000Z'
|
|
382
|
-
* // }
|
|
383
|
-
* // }]
|
|
384
|
-
* // }
|
|
420
|
+
* Normalize a JSON:API document or one plain resource using compiled output fields.
|
|
421
|
+
* JSON:API primary/included attributes are replaced on the supplied document;
|
|
422
|
+
* plain resources are copied recursively with shared/cyclic identities tracked.
|
|
423
|
+
* The source distinguishes database conversion from final-response validation.
|
|
424
|
+
* Final response normalization removes internal enrichment metadata and reapplies
|
|
425
|
+
* the supplied fieldset so dependency-only values do not escape in output.
|
|
385
426
|
*
|
|
386
|
-
* @
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* {
|
|
396
|
-
* type: 'comments',
|
|
397
|
-
* id: '2',
|
|
398
|
-
* attributes: { approved: 0, created_at: '2024-01-02 10:00:00' }
|
|
399
|
-
* }
|
|
400
|
-
* ]
|
|
401
|
-
* };
|
|
402
|
-
*
|
|
403
|
-
* // Each record in the array is normalized
|
|
404
|
-
*
|
|
405
|
-
* @description
|
|
406
|
-
* Used by:
|
|
407
|
-
* - rest-api-knex-plugin after fetching data, before sending response
|
|
408
|
-
* - Applied to both GET (single) and QUERY (collection) responses
|
|
409
|
-
*
|
|
410
|
-
* Purpose:
|
|
411
|
-
* - Ensures consistent data types in API responses
|
|
412
|
-
* - Handles both primary data and included resources
|
|
413
|
-
* - Works with single records and collections
|
|
414
|
-
* - Uses schema definitions to determine field types
|
|
415
|
-
*
|
|
416
|
-
* Data flow:
|
|
417
|
-
* 1. Checks if data is array (collection) or object (single)
|
|
418
|
-
* 2. For each record, looks up its schema by type
|
|
419
|
-
* 3. Calls normalizeAttributes with schema structure
|
|
420
|
-
* 4. Processes included array the same way
|
|
421
|
-
* 5. Returns complete response with normalized values
|
|
427
|
+
* @template T
|
|
428
|
+
* @param {T} record - JSON:API document, or a plain resource in simplified mode
|
|
429
|
+
* @param {Object} scopes - Resource registry for schema lookup
|
|
430
|
+
* @param {Object} [options]
|
|
431
|
+
* @param {string} [options.source='database'] - Value origin for normalization
|
|
432
|
+
* @param {boolean} [options.simplified=false] - Internal plain-resource selector
|
|
433
|
+
* @param {string} [options.resourceType] - Required for plain-resource schema lookup
|
|
434
|
+
* @param {Object} [options.fields] - Selected fieldsets, if supplied
|
|
435
|
+
* @returns {T} Normalized document or plain resource
|
|
422
436
|
*/
|
|
423
|
-
export function normalizeRecordAttributes (record, scopes, {
|
|
437
|
+
export function normalizeRecordAttributes (record, scopes, {
|
|
438
|
+
source = 'database',
|
|
439
|
+
simplified = false,
|
|
440
|
+
resourceType,
|
|
441
|
+
fields
|
|
442
|
+
} = {}) {
|
|
424
443
|
if (!record || !scopes) {
|
|
425
444
|
return record
|
|
426
445
|
}
|
|
427
446
|
|
|
447
|
+
if (simplified) {
|
|
448
|
+
return normalizeSimplifiedRecord(record, resourceType, scopes, { source, fields })
|
|
449
|
+
}
|
|
450
|
+
|
|
428
451
|
const normalizeEntry = (entry) => {
|
|
452
|
+
// Discard enrichment metadata after finish hooks have run.
|
|
453
|
+
if (source === 'response' && entry && typeof entry === 'object') delete entry[COMPUTED_DEPENDENCIES_KEY]
|
|
429
454
|
const schemaInfo = scopes[entry?.type]?.vars?.schemaInfo
|
|
430
455
|
if (!schemaInfo || !entry?.attributes) return
|
|
431
456
|
|
|
432
457
|
entry.attributes = normalizeAttributes(
|
|
433
458
|
entry.attributes,
|
|
434
|
-
|
|
435
|
-
...(schemaInfo.schemaStructure || {}),
|
|
436
|
-
...(schemaInfo.computed || {})
|
|
437
|
-
},
|
|
459
|
+
schemaInfo.outputFields,
|
|
438
460
|
{
|
|
439
461
|
resourceType: entry.type,
|
|
440
462
|
source
|
|
@@ -462,5 +484,34 @@ export function normalizeRecordAttributes (record, scopes, { source = 'database'
|
|
|
462
484
|
}
|
|
463
485
|
}
|
|
464
486
|
|
|
487
|
+
if (fields) filterResponseFields(record, fields)
|
|
465
488
|
return record
|
|
466
489
|
}
|
|
490
|
+
|
|
491
|
+
function normalizeSimplifiedRecord (record, resourceType, scopes, options, visited = new WeakMap()) {
|
|
492
|
+
if (!record || typeof record !== 'object') return record
|
|
493
|
+
if (visited.has(record)) return visited.get(record)
|
|
494
|
+
const schemaInfo = scopes[resourceType]?.vars?.schemaInfo
|
|
495
|
+
if (!schemaInfo) return record
|
|
496
|
+
|
|
497
|
+
const normalized = normalizeAttributes(record, schemaInfo.outputFields, { ...options, resourceType })
|
|
498
|
+
visited.set(record, normalized)
|
|
499
|
+
for (const [name, definition] of Object.entries(schemaInfo.outputRelationships || {})) {
|
|
500
|
+
const normalizeRelated = (entry) => normalizeSimplifiedRecord(
|
|
501
|
+
entry,
|
|
502
|
+
definition.belongsToPolymorphic
|
|
503
|
+
? entry?._type
|
|
504
|
+
: definition.belongsTo || definition.target || name,
|
|
505
|
+
scopes,
|
|
506
|
+
options,
|
|
507
|
+
visited
|
|
508
|
+
)
|
|
509
|
+
if (Object.hasOwn(normalized, name)) {
|
|
510
|
+
normalized[name] = Array.isArray(normalized[name])
|
|
511
|
+
? normalized[name].map(normalizeRelated)
|
|
512
|
+
: normalizeRelated(normalized[name])
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (options.fields) filterResponseFields(normalized, options.fields, { simplified: true, resourceType })
|
|
516
|
+
return normalized
|
|
517
|
+
}
|
|
@@ -1,72 +1,42 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { DataWriteHelpers, DataReadHelpers } from '../storage/storage-types.js' */
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* guiding users to install a storage implementation.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* // When no storage plugin is installed:
|
|
11
|
-
* try {
|
|
12
|
-
* await api.resources.articles.get({ id: 1 });
|
|
13
|
-
* } catch (error) {
|
|
14
|
-
* console.log(error.message);
|
|
15
|
-
* // "No storage implementation for get. Install a storage plugin."
|
|
16
|
-
* }
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* // After installing rest-api-knex-plugin:
|
|
20
|
-
* api.use(RestApiKnexPlugin, { knex });
|
|
21
|
-
* // Now data helpers are replaced with actual implementations
|
|
22
|
-
* const article = await api.resources.articles.get({ id: 1 });
|
|
23
|
-
* // Works! Returns article from database
|
|
24
|
-
*
|
|
25
|
-
* Used by:
|
|
26
|
-
* - rest-api-plugin sets these as initial data helpers
|
|
27
|
-
* - Storage plugins replace these with actual implementations
|
|
28
|
-
*
|
|
29
|
-
* Purpose:
|
|
30
|
-
* - Provides clear error messages when storage is missing
|
|
31
|
-
* - Ensures REST API plugin can be installed independently
|
|
32
|
-
* - Allows storage plugins to be swapped without changing API
|
|
33
|
-
*
|
|
34
|
-
* Data flow:
|
|
35
|
-
* 1. REST API plugin initializes with these defaults
|
|
36
|
-
* 2. User tries to use API without storage plugin
|
|
37
|
-
* 3. Clear error message guides them to install storage
|
|
38
|
-
* 4. Storage plugin replaces these with real implementations
|
|
4
|
+
* Missing-storage placeholders installed by RestApiPlugin.
|
|
5
|
+
* Storage plugins replace these helpers. They are not a complete custom-backend
|
|
6
|
+
* interface: resource operations also require transaction and query integration.
|
|
7
|
+
* Earlier lifecycle validation may reject a call before one of these runs.
|
|
8
|
+
* @satisfies {DataWriteHelpers<never, never> & DataReadHelpers<never>}
|
|
39
9
|
*/
|
|
40
10
|
export const defaultDataHelpers = {
|
|
41
|
-
dataExists: async function (
|
|
11
|
+
dataExists: async function (request) {
|
|
42
12
|
throw new Error('No storage implementation for exists. Install a storage plugin.')
|
|
43
13
|
},
|
|
44
14
|
|
|
45
|
-
dataGet: async function (
|
|
15
|
+
dataGet: async function (request) {
|
|
46
16
|
throw new Error('No storage implementation for get. Install a storage plugin.')
|
|
47
17
|
},
|
|
48
18
|
|
|
49
|
-
dataGetMinimal: async function (
|
|
19
|
+
dataGetMinimal: async function (request) {
|
|
50
20
|
throw new Error('No storage implementation for getMinimal. Install a storage plugin.')
|
|
51
21
|
},
|
|
52
22
|
|
|
53
|
-
dataQuery: async function (
|
|
23
|
+
dataQuery: async function (request) {
|
|
54
24
|
throw new Error('No storage implementation for query. Install a storage plugin.')
|
|
55
25
|
},
|
|
56
26
|
|
|
57
|
-
dataPost: async function (
|
|
27
|
+
dataPost: async function (request) {
|
|
58
28
|
throw new Error('No storage implementation for post. Install a storage plugin.')
|
|
59
29
|
},
|
|
60
30
|
|
|
61
|
-
dataPatch: async function (
|
|
31
|
+
dataPatch: async function (request) {
|
|
62
32
|
throw new Error('No storage implementation for patch. Install a storage plugin.')
|
|
63
33
|
},
|
|
64
34
|
|
|
65
|
-
dataPut: async function (
|
|
35
|
+
dataPut: async function (request) {
|
|
66
36
|
throw new Error('No storage implementation for put. Install a storage plugin.')
|
|
67
37
|
},
|
|
68
38
|
|
|
69
|
-
dataDelete: async function (
|
|
39
|
+
dataDelete: async function (request) {
|
|
70
40
|
throw new Error('No storage implementation for delete. Install a storage plugin.')
|
|
71
41
|
}
|
|
72
42
|
}
|