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,11 +1,40 @@
|
|
|
1
1
|
import { createSchema } from 'json-rest-schema'
|
|
2
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 }) => {
|
|
@@ -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,14 +142,14 @@ 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.
|
|
110
150
|
const fraction = /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(dateTimeValue)?.[1]
|
|
111
151
|
return fraction
|
|
112
|
-
? parsed.toISOString().replace(/\.\d{3}Z$/, `.${fraction}Z`)
|
|
152
|
+
? parsed.toISOString().replace(/\.\d{3}Z$/, `.${fraction.padEnd(3, '0')}Z`)
|
|
113
153
|
: parsed.toISOString()
|
|
114
154
|
}
|
|
115
155
|
|
|
@@ -134,16 +174,21 @@ export function normalizeDateValue (value, type, {
|
|
|
134
174
|
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
135
175
|
timeValue = limitFractionalSecondPrecision(value.toISOString().slice(11, -1), temporalPrecision)
|
|
136
176
|
}
|
|
137
|
-
|
|
177
|
+
const validated = validateTemporalJsonValue(timeValue, type, {
|
|
138
178
|
temporalPrecision,
|
|
139
179
|
fieldName,
|
|
140
180
|
resourceType,
|
|
141
181
|
source
|
|
142
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}` : ''}`
|
|
143
187
|
}
|
|
144
188
|
|
|
145
|
-
|
|
146
|
-
|
|
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, {
|
|
147
192
|
fieldName,
|
|
148
193
|
resourceType,
|
|
149
194
|
source
|
|
@@ -153,7 +198,7 @@ export function normalizeDateValue (value, type, {
|
|
|
153
198
|
const isoValue = parseKnownDatabaseDate(value, { fieldName, resourceType, type, source })
|
|
154
199
|
const normalized = type === 'date'
|
|
155
200
|
? isoValue.slice(0, 10)
|
|
156
|
-
: limitFractionalSecondPrecision(isoValue, temporalPrecision)
|
|
201
|
+
: limitFractionalSecondPrecision(isoValue.replace(/(\.\d{3}\d*?)0+Z$/, '$1Z'), temporalPrecision)
|
|
157
202
|
return validateTemporalJsonValue(normalized, type, {
|
|
158
203
|
temporalPrecision,
|
|
159
204
|
fieldName,
|
|
@@ -163,30 +208,57 @@ export function normalizeDateValue (value, type, {
|
|
|
163
208
|
}
|
|
164
209
|
|
|
165
210
|
/**
|
|
166
|
-
* Converts
|
|
211
|
+
* Converts structured JSON and temporal values to the values expected by
|
|
167
212
|
* database drivers. JSON schema validation keeps date and dateTime values as
|
|
168
213
|
* strings; database writes and comparisons must normalize them consistently.
|
|
169
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}
|
|
170
219
|
*/
|
|
171
220
|
export function normalizeValueForDatabaseStorage (value, type, {
|
|
221
|
+
databaseClient,
|
|
222
|
+
textStorage = false,
|
|
172
223
|
temporalPrecision,
|
|
173
224
|
fieldName,
|
|
174
225
|
resourceType,
|
|
175
226
|
source = 'storage'
|
|
176
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
|
+
}
|
|
177
239
|
if (!['date', 'dateTime', 'time'].includes(type)) return value
|
|
178
240
|
if (value === null || value === undefined) return null
|
|
241
|
+
const capabilities = getTemporalStorageCapabilities(databaseClient, { textStorage })
|
|
179
242
|
|
|
180
243
|
if (type === 'time') {
|
|
181
244
|
if (typeof value !== 'string') {
|
|
182
245
|
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
183
246
|
}
|
|
184
|
-
|
|
247
|
+
const validated = validateTemporalJsonValue(value, type, {
|
|
185
248
|
temporalPrecision,
|
|
186
249
|
fieldName,
|
|
187
250
|
resourceType,
|
|
188
251
|
source
|
|
189
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
|
|
190
262
|
}
|
|
191
263
|
|
|
192
264
|
let publicValue = value
|
|
@@ -205,10 +277,11 @@ export function normalizeValueForDatabaseStorage (value, type, {
|
|
|
205
277
|
resourceType,
|
|
206
278
|
source
|
|
207
279
|
})
|
|
280
|
+
if (type === 'date' && textStorage) return validatedValue
|
|
208
281
|
const fraction = type === 'dateTime'
|
|
209
282
|
? /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(validatedValue)?.[1]
|
|
210
283
|
: null
|
|
211
|
-
if (fraction && /[1-9]/.test(fraction.slice(
|
|
284
|
+
if (fraction && /[1-9]/.test(fraction.slice(capabilities.dateTimeFractionDigits))) {
|
|
212
285
|
const field = fieldName ? `data.attributes.${fieldName}` : 'value'
|
|
213
286
|
throw new RestApiValidationError('Built-in dateTime storage supports millisecond precision.', {
|
|
214
287
|
fields: [field],
|
|
@@ -225,85 +298,57 @@ export function normalizeValueForDatabaseStorage (value, type, {
|
|
|
225
298
|
if (Number.isNaN(databaseValue.getTime())) {
|
|
226
299
|
throwTemporalDataError({ fieldName, resourceType, type, source })
|
|
227
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
|
+
}
|
|
228
324
|
return databaseValue
|
|
229
325
|
}
|
|
230
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
|
+
|
|
231
346
|
/**
|
|
232
347
|
* Normalizes database values in an attributes object
|
|
233
348
|
*
|
|
234
349
|
* @param {Object} attributes - The attributes object to normalize
|
|
235
350
|
* @param {Object} schemaStructure - The schema structure defining field types
|
|
236
351
|
* @returns {Object} The normalized attributes object
|
|
237
|
-
*
|
|
238
|
-
* @example
|
|
239
|
-
* // Input: Raw database values
|
|
240
|
-
* const attributes = {
|
|
241
|
-
* id: 1,
|
|
242
|
-
* is_active: 1, // Boolean as 0/1
|
|
243
|
-
* created_at: '2024-01-15 10:30:00', // MySQL datetime
|
|
244
|
-
* birth_date: '1990-05-20', // Date string
|
|
245
|
-
* shift_time: '09:00:00' // Time string
|
|
246
|
-
* };
|
|
247
|
-
*
|
|
248
|
-
* const schema = {
|
|
249
|
-
* is_active: { type: 'boolean' },
|
|
250
|
-
* created_at: { type: 'dateTime' },
|
|
251
|
-
* birth_date: { type: 'date' },
|
|
252
|
-
* shift_time: { type: 'time' }
|
|
253
|
-
* };
|
|
254
|
-
*
|
|
255
|
-
* const normalized = normalizeAttributes(attributes, schema);
|
|
256
|
-
*
|
|
257
|
-
* // Output: JSON-safe values
|
|
258
|
-
* // {
|
|
259
|
-
* // id: 1,
|
|
260
|
-
* // is_active: true, // 1 → true
|
|
261
|
-
* // created_at: '2024-01-15T10:30:00.000Z',
|
|
262
|
-
* // birth_date: '1990-05-20',
|
|
263
|
-
* // shift_time: '09:00:00'
|
|
264
|
-
* // }
|
|
265
|
-
*
|
|
266
|
-
* @example
|
|
267
|
-
* // Input: Boolean edge cases
|
|
268
|
-
* const attributes = {
|
|
269
|
-
* flag1: 0, // Number 0
|
|
270
|
-
* flag2: '1', // String '1'
|
|
271
|
-
* flag3: true, // Already boolean
|
|
272
|
-
* flag4: null // Null value
|
|
273
|
-
* };
|
|
274
|
-
*
|
|
275
|
-
* const normalized = normalizeAttributes(attributes, {
|
|
276
|
-
* flag1: { type: 'boolean' },
|
|
277
|
-
* flag2: { type: 'boolean' },
|
|
278
|
-
* flag3: { type: 'boolean' },
|
|
279
|
-
* flag4: { type: 'boolean' }
|
|
280
|
-
* });
|
|
281
|
-
*
|
|
282
|
-
* // Output:
|
|
283
|
-
* // {
|
|
284
|
-
* // flag1: false, // 0 → false
|
|
285
|
-
* // flag2: true, // '1' → true
|
|
286
|
-
* // flag3: true, // unchanged
|
|
287
|
-
* // flag4: null // null preserved
|
|
288
|
-
* // }
|
|
289
|
-
*
|
|
290
|
-
* @description
|
|
291
|
-
* Used by:
|
|
292
|
-
* - normalizeRecordAttributes for each record's attributes
|
|
293
|
-
* - Applied after fetching data from database
|
|
294
|
-
*
|
|
295
|
-
* Purpose:
|
|
296
|
-
* - Handles databases without native boolean support (SQLite, older MySQL)
|
|
297
|
-
* - Normalizes date formats from different databases
|
|
298
|
-
* - Ensures consistent data types in API responses
|
|
299
|
-
* - Preserves null values appropriately
|
|
300
|
-
*
|
|
301
|
-
* Data flow:
|
|
302
|
-
* 1. Creates copy of attributes to avoid mutation
|
|
303
|
-
* 2. Iterates through each field with schema definition
|
|
304
|
-
* 3. Normalizes booleans: 1/0 or '1'/'0' to true/false
|
|
305
|
-
* 4. Normalizes dates using normalizeDateValue
|
|
306
|
-
* 5. Returns new object with normalized values
|
|
307
352
|
*/
|
|
308
353
|
export function normalizeAttributes (attributes, schemaStructure, {
|
|
309
354
|
resourceType,
|
|
@@ -322,6 +367,25 @@ export function normalizeAttributes (attributes, schemaStructure, {
|
|
|
322
367
|
// Skip if no field definition found
|
|
323
368
|
if (!fieldDef) continue
|
|
324
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
|
+
|
|
325
389
|
// Normalize boolean values
|
|
326
390
|
if (fieldDef.type === 'boolean') {
|
|
327
391
|
if (value === 1 || value === '1') {
|
|
@@ -353,120 +417,46 @@ export function normalizeAttributes (attributes, schemaStructure, {
|
|
|
353
417
|
}
|
|
354
418
|
|
|
355
419
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* @example
|
|
363
|
-
* // Input: JSON:API response with various data types
|
|
364
|
-
* const response = {
|
|
365
|
-
* data: {
|
|
366
|
-
* type: 'articles',
|
|
367
|
-
* id: '1',
|
|
368
|
-
* attributes: {
|
|
369
|
-
* title: 'My Article',
|
|
370
|
-
* is_published: 1, // Boolean as number
|
|
371
|
-
* published_at: '2024-01-15 10:00:00' // MySQL datetime
|
|
372
|
-
* }
|
|
373
|
-
* },
|
|
374
|
-
* included: [{
|
|
375
|
-
* type: 'users',
|
|
376
|
-
* id: '10',
|
|
377
|
-
* attributes: {
|
|
378
|
-
* name: 'John',
|
|
379
|
-
* is_admin: 0, // Boolean as number
|
|
380
|
-
* last_login: '2024-01-14 15:30:00'
|
|
381
|
-
* }
|
|
382
|
-
* }]
|
|
383
|
-
* };
|
|
384
|
-
*
|
|
385
|
-
* const normalized = normalizeRecordAttributes(response, scopes);
|
|
386
|
-
*
|
|
387
|
-
* // Output: All booleans and dates normalized
|
|
388
|
-
* // {
|
|
389
|
-
* // data: {
|
|
390
|
-
* // type: 'articles',
|
|
391
|
-
* // id: '1',
|
|
392
|
-
* // attributes: {
|
|
393
|
-
* // title: 'My Article',
|
|
394
|
-
* // is_published: true,
|
|
395
|
-
* // published_at: '2024-01-15T10:00:00.000Z'
|
|
396
|
-
* // }
|
|
397
|
-
* // },
|
|
398
|
-
* // included: [{
|
|
399
|
-
* // type: 'users',
|
|
400
|
-
* // id: '10',
|
|
401
|
-
* // attributes: {
|
|
402
|
-
* // name: 'John',
|
|
403
|
-
* // is_admin: false,
|
|
404
|
-
* // last_login: '2024-01-14T15:30:00.000Z'
|
|
405
|
-
* // }
|
|
406
|
-
* // }]
|
|
407
|
-
* // }
|
|
408
|
-
*
|
|
409
|
-
* @example
|
|
410
|
-
* // Input: Collection response (array of records)
|
|
411
|
-
* const response = {
|
|
412
|
-
* data: [
|
|
413
|
-
* {
|
|
414
|
-
* type: 'comments',
|
|
415
|
-
* id: '1',
|
|
416
|
-
* attributes: { approved: 1, created_at: '2024-01-01 09:00:00' }
|
|
417
|
-
* },
|
|
418
|
-
* {
|
|
419
|
-
* type: 'comments',
|
|
420
|
-
* id: '2',
|
|
421
|
-
* attributes: { approved: 0, created_at: '2024-01-02 10:00:00' }
|
|
422
|
-
* }
|
|
423
|
-
* ]
|
|
424
|
-
* };
|
|
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.
|
|
425
426
|
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* @
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
* - Handles both primary data and included resources
|
|
436
|
-
* - Works with single records and collections
|
|
437
|
-
* - Uses schema definitions to determine field types
|
|
438
|
-
*
|
|
439
|
-
* Data flow:
|
|
440
|
-
* 1. Checks if data is array (collection) or object (single)
|
|
441
|
-
* 2. For each record, looks up its schema by type
|
|
442
|
-
* 3. Calls normalizeAttributes with schema structure
|
|
443
|
-
* 4. Processes included array the same way
|
|
444
|
-
* 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
|
|
445
436
|
*/
|
|
446
437
|
export function normalizeRecordAttributes (record, scopes, {
|
|
447
438
|
source = 'database',
|
|
448
439
|
simplified = false,
|
|
449
|
-
resourceType
|
|
440
|
+
resourceType,
|
|
441
|
+
fields
|
|
450
442
|
} = {}) {
|
|
451
443
|
if (!record || !scopes) {
|
|
452
444
|
return record
|
|
453
445
|
}
|
|
454
446
|
|
|
455
447
|
if (simplified) {
|
|
456
|
-
return normalizeSimplifiedRecord(record, resourceType, scopes, { source })
|
|
448
|
+
return normalizeSimplifiedRecord(record, resourceType, scopes, { source, fields })
|
|
457
449
|
}
|
|
458
450
|
|
|
459
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]
|
|
460
454
|
const schemaInfo = scopes[entry?.type]?.vars?.schemaInfo
|
|
461
455
|
if (!schemaInfo || !entry?.attributes) return
|
|
462
456
|
|
|
463
457
|
entry.attributes = normalizeAttributes(
|
|
464
458
|
entry.attributes,
|
|
465
|
-
|
|
466
|
-
...(schemaInfo.schemaStructure || {}),
|
|
467
|
-
...(schemaInfo.computed || {}),
|
|
468
|
-
...(scopes[entry.type]?.vars?.queryFields || {})
|
|
469
|
-
},
|
|
459
|
+
schemaInfo.outputFields,
|
|
470
460
|
{
|
|
471
461
|
resourceType: entry.type,
|
|
472
462
|
source
|
|
@@ -494,6 +484,7 @@ export function normalizeRecordAttributes (record, scopes, {
|
|
|
494
484
|
}
|
|
495
485
|
}
|
|
496
486
|
|
|
487
|
+
if (fields) filterResponseFields(record, fields)
|
|
497
488
|
return record
|
|
498
489
|
}
|
|
499
490
|
|
|
@@ -503,16 +494,9 @@ function normalizeSimplifiedRecord (record, resourceType, scopes, options, visit
|
|
|
503
494
|
const schemaInfo = scopes[resourceType]?.vars?.schemaInfo
|
|
504
495
|
if (!schemaInfo) return record
|
|
505
496
|
|
|
506
|
-
const
|
|
507
|
-
const normalized = normalizeAttributes(record, schema, { ...options, resourceType })
|
|
497
|
+
const normalized = normalizeAttributes(record, schemaInfo.outputFields, { ...options, resourceType })
|
|
508
498
|
visited.set(record, normalized)
|
|
509
|
-
const
|
|
510
|
-
for (const definition of Object.values(schema)) {
|
|
511
|
-
if (definition.as && (definition.belongsTo || definition.belongsToPolymorphic)) {
|
|
512
|
-
relationships[definition.as] = definition
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
for (const [name, definition] of Object.entries(relationships)) {
|
|
499
|
+
for (const [name, definition] of Object.entries(schemaInfo.outputRelationships || {})) {
|
|
516
500
|
const normalizeRelated = (entry) => normalizeSimplifiedRecord(
|
|
517
501
|
entry,
|
|
518
502
|
definition.belongsToPolymorphic
|
|
@@ -528,5 +512,6 @@ function normalizeSimplifiedRecord (record, resourceType, scopes, options, visit
|
|
|
528
512
|
: normalizeRelated(normalized[name])
|
|
529
513
|
}
|
|
530
514
|
}
|
|
515
|
+
if (options.fields) filterResponseFields(normalized, options.fields, { simplified: true, resourceType })
|
|
531
516
|
return normalized
|
|
532
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
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ResourceConversionSchema, StorageAdapter, StorageContext, StorageDatabase, StorageFieldDefinition, StorageQuery } from '../storage/storage-types.js'
|
|
2
|
+
import type { CompiledQueryField, ProjectionRuntime } from './query-field-types.js'
|
|
3
|
+
import type { Fieldsets } from './field-utils.js'
|
|
4
|
+
|
|
5
|
+
export interface SelectionSchema extends Partial<ResourceConversionSchema> {
|
|
6
|
+
queryFields?: Record<string, CompiledQueryField>
|
|
7
|
+
outputRelationships?: Record<string, StorageFieldDefinition>
|
|
8
|
+
readDependencies?: Record<string, readonly string[]>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SelectionScope {
|
|
12
|
+
vars?: { schemaInfo?: SelectionSchema }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SelectionContext extends StorageContext {
|
|
16
|
+
scopeName: string
|
|
17
|
+
queryParams?: { fields?: Fieldsets; sort?: unknown }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FieldSelection {
|
|
21
|
+
fieldsToSelect: string[]
|
|
22
|
+
queryFieldsToSelect: string[]
|
|
23
|
+
requestedFields: readonly string[] | null
|
|
24
|
+
computedDependencies: string[]
|
|
25
|
+
idProperty: 'id'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SelectionQueryRequest {
|
|
29
|
+
query: StorageQuery
|
|
30
|
+
scope?: SelectionScope | null
|
|
31
|
+
fieldSelectionInfo?: Partial<FieldSelection> | null
|
|
32
|
+
tableName: string
|
|
33
|
+
useTablePrefix?: boolean
|
|
34
|
+
storageAdapter?: StorageAdapter | null
|
|
35
|
+
db?: StorageDatabase | null
|
|
36
|
+
context?: StorageContext | null
|
|
37
|
+
scopeName?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SelectedQuery {
|
|
41
|
+
query: StorageQuery
|
|
42
|
+
queryFieldRuntimeByField: Map<string, ProjectionRuntime>
|
|
43
|
+
}
|