json-rest-api 1.0.29 → 2.1.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 +733 -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 +103 -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 +3838 -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 +37 -0
- package/index.js +3 -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 +70 -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 +586 -418
- 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 +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- 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 +290 -2475
- 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/anyapi.d.ts +30 -0
- 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 +65 -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,133 +1,48 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { ResourceConversionScope, StorageRow } from '../storage/storage-types.js' */
|
|
3
|
+
/** @import { JsonApiResource, JsonApiRelationship } from '../../../../types/representations.js' */
|
|
1
4
|
import { toJsonApiRecord } from '../querying/knex-json-api-transformers-querying.js'
|
|
2
|
-
import { getSchemaStructure } from './knex-constants.js'
|
|
3
5
|
import { translateRecordFromStorage } from '../storage/storage-mapping.js'
|
|
6
|
+
import { getPolymorphicLinkage } from './relationship-contracts.js'
|
|
4
7
|
|
|
8
|
+
/** @overload @param {ResourceConversionScope} scope @param {StorageRow} record @param {string} scopeName @returns {JsonApiResource} */
|
|
9
|
+
/** @overload @param {ResourceConversionScope} scope @param {StorageRow | null | undefined} record @param {string} scopeName @returns {JsonApiResource | null} */
|
|
5
10
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param {
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {string} scopeName
|
|
11
|
-
* @returns {
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* // Input: Database record with foreign keys
|
|
15
|
-
* const dbRecord = {
|
|
16
|
-
* id: 1,
|
|
17
|
-
* title: 'Hello World',
|
|
18
|
-
* author_id: 2,
|
|
19
|
-
* publisher_id: 3,
|
|
20
|
-
* category_id: null
|
|
21
|
-
* };
|
|
22
|
-
* const result = toJsonApiRecordWithBelongsTo(scope, dbRecord, 'articles');
|
|
23
|
-
*
|
|
24
|
-
* // Output: JSON:API with relationship objects
|
|
25
|
-
* // {
|
|
26
|
-
* // type: 'articles',
|
|
27
|
-
* // id: '1',
|
|
28
|
-
* // attributes: {
|
|
29
|
-
* // title: 'Hello World'
|
|
30
|
-
* // // Note: author_id, publisher_id, category_id removed
|
|
31
|
-
* // },
|
|
32
|
-
* // relationships: {
|
|
33
|
-
* // author: {
|
|
34
|
-
* // data: { type: 'authors', id: '2' }
|
|
35
|
-
* // },
|
|
36
|
-
* // publisher: {
|
|
37
|
-
* // data: { type: 'publishers', id: '3' }
|
|
38
|
-
* // },
|
|
39
|
-
* // category: {
|
|
40
|
-
* // data: null // Explicit null for empty relationship
|
|
41
|
-
* // }
|
|
42
|
-
* // }
|
|
43
|
-
* // }
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* // Input: Polymorphic relationships
|
|
47
|
-
* const dbRecord = {
|
|
48
|
-
* id: 5,
|
|
49
|
-
* content: 'Great post!',
|
|
50
|
-
* commentable_type: 'articles',
|
|
51
|
-
* commentable_id: 10
|
|
52
|
-
* };
|
|
53
|
-
* // With schema defining polymorphic relationship:
|
|
54
|
-
* // relationships: {
|
|
55
|
-
* // commentable: {
|
|
56
|
-
* // belongsToPolymorphic: true,
|
|
57
|
-
* // typeField: 'commentable_type',
|
|
58
|
-
* // idField: 'commentable_id'
|
|
59
|
-
* // }
|
|
60
|
-
* // }
|
|
61
|
-
*
|
|
62
|
-
* // Output: Polymorphic relationship in JSON:API
|
|
63
|
-
* // {
|
|
64
|
-
* // type: 'comments',
|
|
65
|
-
* // id: '5',
|
|
66
|
-
* // attributes: { content: 'Great post!' },
|
|
67
|
-
* // relationships: {
|
|
68
|
-
* // commentable: {
|
|
69
|
-
* // data: { type: 'articles', id: '10' }
|
|
70
|
-
* // }
|
|
71
|
-
* // }
|
|
72
|
-
* // }
|
|
73
|
-
*
|
|
74
|
-
* @description
|
|
75
|
-
* Used by:
|
|
76
|
-
* - dataGetMinimal for lightweight single record fetches
|
|
77
|
-
* - dataDelete to return deleted record structure
|
|
78
|
-
* - Internal operations needing quick JSON:API conversion
|
|
79
|
-
*
|
|
80
|
-
* Purpose:
|
|
81
|
-
* - Provides JSON:API structure without loading related data
|
|
82
|
-
* - Transforms foreign keys into relationship objects
|
|
83
|
-
* - Handles both regular and polymorphic belongsTo
|
|
84
|
-
* - Maintains explicit nulls for empty relationships
|
|
85
|
-
*
|
|
86
|
-
* Data flow:
|
|
87
|
-
* 1. Calls toJsonApiRecord for basic transformation
|
|
88
|
-
* 2. Scans schema for belongsTo field definitions
|
|
89
|
-
* 3. Converts foreign key values to relationship objects
|
|
90
|
-
* 4. Processes polymorphic relationships separately
|
|
91
|
-
* 5. Returns complete JSON:API resource object
|
|
11
|
+
* Convert one storage row and its belongs-to linkage without loading targets.
|
|
12
|
+
* Empty foreign keys become null linkage; backing fields stay out of attributes.
|
|
13
|
+
* @param {ResourceConversionScope} scope
|
|
14
|
+
* @param {StorageRow | null | undefined} record
|
|
15
|
+
* @param {string} scopeName
|
|
16
|
+
* @returns {JsonApiResource | null}
|
|
92
17
|
*/
|
|
93
|
-
export
|
|
18
|
+
export function toJsonApiRecordWithBelongsTo (scope, record, scopeName) {
|
|
94
19
|
if (!record) return null
|
|
95
20
|
|
|
96
21
|
const schemaInfo = scope.vars.schemaInfo
|
|
97
22
|
const logicalRecord = translateRecordFromStorage(record, schemaInfo)
|
|
98
23
|
|
|
99
24
|
// Get the basic JSON:API structure (without relationships)
|
|
100
|
-
const jsonApiRecord = toJsonApiRecord(scope,
|
|
25
|
+
const jsonApiRecord = toJsonApiRecord(scope, record, scopeName)
|
|
101
26
|
|
|
102
27
|
// Extract schema info from scope
|
|
103
28
|
const {
|
|
104
|
-
|
|
29
|
+
schemaStructure,
|
|
105
30
|
schemaRelationships: relationships
|
|
106
31
|
} = schemaInfo
|
|
107
32
|
|
|
108
|
-
// Get schema structure
|
|
109
|
-
const schemaStructure = getSchemaStructure(schemaInstance)
|
|
110
|
-
|
|
111
33
|
// Initialize relationships object
|
|
112
|
-
|
|
34
|
+
/** @type {Record<string, JsonApiRelationship>} */
|
|
35
|
+
const relationshipData = jsonApiRecord.relationships = {}
|
|
113
36
|
|
|
114
37
|
// Process regular belongsTo relationships from schema
|
|
115
38
|
for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
|
|
116
39
|
if (fieldDef.belongsTo && fieldDef.as) {
|
|
117
40
|
const fieldValue = logicalRecord[fieldName]
|
|
118
41
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
id: String(fieldValue)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
// Explicitly null relationship
|
|
128
|
-
jsonApiRecord.relationships[fieldDef.as] = {
|
|
129
|
-
data: null
|
|
130
|
-
}
|
|
42
|
+
relationshipData[fieldDef.as] = {
|
|
43
|
+
data: fieldValue === null || fieldValue === undefined
|
|
44
|
+
? null
|
|
45
|
+
: { type: fieldDef.belongsTo, id: String(fieldValue) }
|
|
131
46
|
}
|
|
132
47
|
}
|
|
133
48
|
}
|
|
@@ -135,21 +50,17 @@ export const toJsonApiRecordWithBelongsTo = (scope, record, scopeName) => {
|
|
|
135
50
|
// Process polymorphic belongsTo relationships
|
|
136
51
|
Object.entries(relationships || {}).forEach(([relName, relDef]) => {
|
|
137
52
|
if (relDef.belongsToPolymorphic) {
|
|
138
|
-
const typeValue = logicalRecord[relDef.typeField]
|
|
139
|
-
const idValue = logicalRecord[relDef.idField]
|
|
53
|
+
const typeValue = logicalRecord[relDef.belongsToPolymorphic.typeField]
|
|
54
|
+
const idValue = logicalRecord[relDef.belongsToPolymorphic.idField]
|
|
140
55
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Explicitly null relationship
|
|
150
|
-
jsonApiRecord.relationships[relName] = {
|
|
151
|
-
data: null
|
|
152
|
-
}
|
|
56
|
+
relationshipData[relName] = {
|
|
57
|
+
data: getPolymorphicLinkage({
|
|
58
|
+
type: typeValue,
|
|
59
|
+
id: idValue,
|
|
60
|
+
types: relDef.belongsToPolymorphic.types,
|
|
61
|
+
scopeName,
|
|
62
|
+
relationshipName: relName
|
|
63
|
+
})
|
|
153
64
|
}
|
|
154
65
|
}
|
|
155
66
|
})
|
|
@@ -1,5 +1,67 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { StorageDatabase, StorageFieldDefinition } from '../storage/storage-types.js' */
|
|
3
|
+
/** @import { CompiledQueryField, ProjectionExpression, ProjectionRequest, ProjectionRuntime, QueryFieldCompilation, QueryFieldDeclaration } from './query-field-types.js' */
|
|
1
4
|
import { getStorageColumn } from '../storage/storage-mapping.js'
|
|
5
|
+
import { wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
2
6
|
|
|
7
|
+
// PostgreSQL json has no equality operator for DISTINCT parent selections.
|
|
8
|
+
/** @template {string | ProjectionExpression} T @param {T} expression @param {StorageFieldDefinition | undefined} definition @param {StorageDatabase | null} db @returns {T | import('knex').Knex.Raw<unknown>} */
|
|
9
|
+
export function normalizeStructuredSelect (expression, definition, db) {
|
|
10
|
+
if (!db || !['pg', 'postgresql'].includes(db.client.config.client) || !['object', 'array'].includes(definition?.type || '')) return expression
|
|
11
|
+
return typeof expression === 'string'
|
|
12
|
+
? db.raw('to_jsonb(??)', [expression])
|
|
13
|
+
: db.raw('to_jsonb((?))', [expression])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Compile projection declarations against the final resource namespace.
|
|
17
|
+
* @param {Record<string, QueryFieldDeclaration>} rawQueryFields
|
|
18
|
+
* @param {QueryFieldCompilation} options
|
|
19
|
+
* @returns {Record<string, CompiledQueryField>}
|
|
20
|
+
*/
|
|
21
|
+
export function compileQueryFields (rawQueryFields, { scopeName, schemaStructure, computed, schemaRelationships, idProperty }) {
|
|
22
|
+
if (!rawQueryFields || typeof rawQueryFields !== 'object' || Array.isArray(rawQueryFields)) {
|
|
23
|
+
throw new Error(`Invalid queryFields configuration for scope '${scopeName}'. Expected an object.`)
|
|
24
|
+
}
|
|
25
|
+
const reservedNames = new Set([
|
|
26
|
+
'id', idProperty,
|
|
27
|
+
...Object.keys(schemaStructure),
|
|
28
|
+
...Object.keys(computed),
|
|
29
|
+
...Object.keys(schemaRelationships)
|
|
30
|
+
])
|
|
31
|
+
for (const definition of Object.values(schemaStructure)) {
|
|
32
|
+
if (definition.belongsTo && definition.as) reservedNames.add(definition.as)
|
|
33
|
+
}
|
|
34
|
+
/** @type {Array<[string, CompiledQueryField]>} */
|
|
35
|
+
const normalized = []
|
|
36
|
+
for (const [fieldName, fieldDef] of Object.entries(rawQueryFields)) {
|
|
37
|
+
if (!fieldDef || typeof fieldDef !== 'object' || Array.isArray(fieldDef)) {
|
|
38
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must be an object.`)
|
|
39
|
+
}
|
|
40
|
+
if (reservedNames.has(fieldName)) {
|
|
41
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' conflicts with an existing schema, computed, or relationship name.`)
|
|
42
|
+
}
|
|
43
|
+
if (!fieldDef.type) {
|
|
44
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must have a type.`)
|
|
45
|
+
}
|
|
46
|
+
if (fieldDef.storage !== undefined) {
|
|
47
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' cannot declare storage. Projections select SQL values directly; put storage serializers on stored fields and use getters for public formatting.`)
|
|
48
|
+
}
|
|
49
|
+
const select = fieldDef.select || fieldDef.project
|
|
50
|
+
if (typeof select !== 'function') {
|
|
51
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' must define a select() function.`)
|
|
52
|
+
}
|
|
53
|
+
normalized.push([fieldName, {
|
|
54
|
+
...fieldDef,
|
|
55
|
+
select,
|
|
56
|
+
sortable: fieldDef.sortable === true,
|
|
57
|
+
hidden: fieldDef.hidden === true,
|
|
58
|
+
normallyHidden: fieldDef.normallyHidden === true
|
|
59
|
+
}])
|
|
60
|
+
}
|
|
61
|
+
return Object.fromEntries(normalized)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {string} columnName @param {string} tableName */
|
|
3
65
|
function qualifyColumnReference (columnName, tableName) {
|
|
4
66
|
if (!tableName || !columnName || columnName === '*' || String(columnName).includes('.')) {
|
|
5
67
|
return columnName
|
|
@@ -8,6 +70,7 @@ function qualifyColumnReference (columnName, tableName) {
|
|
|
8
70
|
return `${tableName}.${columnName}`
|
|
9
71
|
}
|
|
10
72
|
|
|
73
|
+
/** @param {ProjectionExpression} expression */
|
|
11
74
|
function normalizeQueryFieldSql (expression) {
|
|
12
75
|
if (expression && typeof expression.toSQL === 'function') {
|
|
13
76
|
const compiled = expression.toSQL()
|
|
@@ -20,6 +83,12 @@ function normalizeQueryFieldSql (expression) {
|
|
|
20
83
|
throw new Error('Query field expressions must return knex raw, knex ref, or a knex query builder.')
|
|
21
84
|
}
|
|
22
85
|
|
|
86
|
+
/** @param {unknown} value @returns {value is ProjectionExpression} */
|
|
87
|
+
export function isImmediateExpression (value) {
|
|
88
|
+
return value !== null && typeof value === 'object' && typeof Reflect.get(value, 'toSQL') === 'function'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @param {ProjectionRequest} [options] @returns {Promise<Map<string, ProjectionRuntime>>} */
|
|
23
92
|
export async function buildQueryFieldRuntimes ({
|
|
24
93
|
queryFieldNames = [],
|
|
25
94
|
queryFields = {},
|
|
@@ -30,6 +99,7 @@ export async function buildQueryFieldRuntimes ({
|
|
|
30
99
|
context = null,
|
|
31
100
|
scopeName = ''
|
|
32
101
|
} = {}) {
|
|
102
|
+
/** @type {Map<string, ProjectionRuntime>} */
|
|
33
103
|
const runtimes = new Map()
|
|
34
104
|
const seen = new Set()
|
|
35
105
|
|
|
@@ -44,44 +114,50 @@ export async function buildQueryFieldRuntimes ({
|
|
|
44
114
|
throw new Error(`Unknown query field '${fieldName}' requested for '${scopeName}'.`)
|
|
45
115
|
}
|
|
46
116
|
|
|
117
|
+
/** @param {string} logicalField */
|
|
47
118
|
const column = (logicalField) => {
|
|
48
119
|
const translated = storageAdapter?.translateColumn?.(logicalField) || getStorageColumn(schemaInfo, logicalField)
|
|
49
120
|
return qualifyColumnReference(translated, tableName)
|
|
50
121
|
}
|
|
51
122
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
selectResult
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
? selectResult
|
|
71
|
-
: await selectResult
|
|
72
|
-
|
|
73
|
-
if (expression === undefined || expression === null) {
|
|
74
|
-
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' returned an empty select expression.`)
|
|
75
|
-
}
|
|
123
|
+
try {
|
|
124
|
+
const selectResult = fieldDef.select({
|
|
125
|
+
knex: db,
|
|
126
|
+
db,
|
|
127
|
+
context,
|
|
128
|
+
scopeName,
|
|
129
|
+
tableName,
|
|
130
|
+
fieldName,
|
|
131
|
+
schemaInfo,
|
|
132
|
+
adapter: storageAdapter,
|
|
133
|
+
column,
|
|
134
|
+
ref: (logicalField) => db?.ref ? db.ref(column(logicalField)) : column(logicalField)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const expression = isImmediateExpression(selectResult)
|
|
138
|
+
? selectResult
|
|
139
|
+
: await selectResult
|
|
76
140
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
expression,
|
|
82
|
-
sql,
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
if (expression === undefined || expression === null) {
|
|
142
|
+
throw new Error(`Query field '${fieldName}' in scope '${scopeName}' returned an empty select expression.`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const selectedExpression = normalizeStructuredSelect(expression, fieldDef, db)
|
|
146
|
+
const { sql, bindings } = normalizeQueryFieldSql(selectedExpression)
|
|
147
|
+
runtimes.set(fieldName, {
|
|
148
|
+
fieldName,
|
|
149
|
+
definition: fieldDef,
|
|
150
|
+
schemaInfo,
|
|
151
|
+
expression: selectedExpression,
|
|
152
|
+
sql,
|
|
153
|
+
bindings
|
|
154
|
+
})
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw wrapUnexpectedError(error, {
|
|
157
|
+
message: `Failed to select query field '${fieldName}' for scope '${scopeName}'`,
|
|
158
|
+
context: { scopeName, fieldName, phase: 'projection' }
|
|
159
|
+
})
|
|
160
|
+
}
|
|
85
161
|
}
|
|
86
162
|
|
|
87
163
|
return runtimes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Knex } from 'knex'
|
|
2
|
+
import type { StorageAdapter, StorageContext, StorageDatabase, StorageFieldDefinition, StorageRow, StorageSchemaInfo } from '../storage/storage-types.js'
|
|
3
|
+
|
|
4
|
+
export type ProjectionExpression = Knex.Raw<unknown> | Knex.QueryBuilder<StorageRow, unknown>
|
|
5
|
+
|
|
6
|
+
export interface ProjectionContext {
|
|
7
|
+
knex: StorageDatabase | null
|
|
8
|
+
db: StorageDatabase | null
|
|
9
|
+
context: StorageContext | null
|
|
10
|
+
scopeName: string
|
|
11
|
+
tableName: string
|
|
12
|
+
fieldName: string
|
|
13
|
+
schemaInfo: Partial<StorageSchemaInfo>
|
|
14
|
+
adapter: StorageAdapter | null
|
|
15
|
+
column: (logicalField: string) => string
|
|
16
|
+
ref: (logicalField: string) => string | Knex.Ref<string, Record<string, string>>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ProjectionSelector = (context: ProjectionContext) => ProjectionExpression
|
|
20
|
+
|
|
21
|
+
export interface QueryFieldDeclaration extends StorageFieldDefinition {
|
|
22
|
+
select?: ProjectionSelector
|
|
23
|
+
project?: ProjectionSelector
|
|
24
|
+
sortable?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CompiledQueryField extends QueryFieldDeclaration {
|
|
28
|
+
select: ProjectionSelector
|
|
29
|
+
sortable: boolean
|
|
30
|
+
hidden: boolean
|
|
31
|
+
normallyHidden: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface QueryFieldCompilation {
|
|
35
|
+
scopeName: string
|
|
36
|
+
schemaStructure: Record<string, StorageFieldDefinition>
|
|
37
|
+
computed: Record<string, StorageFieldDefinition>
|
|
38
|
+
schemaRelationships: Record<string, unknown>
|
|
39
|
+
idProperty: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ProjectionRequest {
|
|
43
|
+
queryFieldNames?: readonly string[]
|
|
44
|
+
queryFields?: Record<string, CompiledQueryField>
|
|
45
|
+
schemaInfo?: Partial<StorageSchemaInfo>
|
|
46
|
+
tableName?: string
|
|
47
|
+
storageAdapter?: StorageAdapter | null
|
|
48
|
+
db?: StorageDatabase | null
|
|
49
|
+
context?: StorageContext | null
|
|
50
|
+
scopeName?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ProjectionRuntime {
|
|
54
|
+
fieldName: string
|
|
55
|
+
definition: CompiledQueryField
|
|
56
|
+
schemaInfo: Partial<StorageSchemaInfo>
|
|
57
|
+
expression: ProjectionExpression
|
|
58
|
+
sql: string
|
|
59
|
+
bindings: readonly unknown[]
|
|
60
|
+
}
|
|
@@ -1,23 +1,43 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { StorageFieldDefinition } from '../storage/storage-types.js' */
|
|
1
3
|
import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
4
|
+
import { wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
5
|
+
import { RELATIONSHIP_CAPABILITIES } from './database-capabilities.js'
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return 'one'
|
|
7
|
+
/** @param {{ type: unknown, id: unknown, types?: readonly string[] | null, scopeName?: string, relationshipName: string }} options */
|
|
8
|
+
export function getPolymorphicLinkage ({ type, id, types, scopeName, relationshipName }) {
|
|
9
|
+
if (type == null || id == null) return null
|
|
10
|
+
if (typeof type !== 'string' || !Array.isArray(types) || !types.includes(type)) {
|
|
11
|
+
throw wrapUnexpectedError(new Error(`Undeclared target type '${type}'`), {
|
|
12
|
+
message: `Invalid stored relationship '${scopeName}.${relationshipName}'`,
|
|
13
|
+
context: { scopeName, relationshipName, phase: 'relationshipData' }
|
|
14
|
+
})
|
|
12
15
|
}
|
|
16
|
+
return { type, id: String(id) }
|
|
17
|
+
}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* @template {StorageFieldDefinition} Definition
|
|
21
|
+
* @param {{ outputRelationships?: Record<string, Definition> }} schemaInfo
|
|
22
|
+
* @param {string} relationshipName
|
|
23
|
+
*/
|
|
24
|
+
export const findRelationshipDefinition = (schemaInfo, relationshipName) => {
|
|
25
|
+
const relationships = schemaInfo.outputRelationships || {}
|
|
26
|
+
return Object.hasOwn(relationships, relationshipName) ? relationships[relationshipName] || null : null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @param {StorageFieldDefinition | null} [relDef] */
|
|
30
|
+
export function getRelationshipCardinality (relDef) {
|
|
31
|
+
if (!relDef) return null
|
|
17
32
|
|
|
18
|
-
return
|
|
33
|
+
if (RELATIONSHIP_CAPABILITIES.attributeKinds.some(kind => relDef[kind])) return 'one'
|
|
34
|
+
const cardinalities = RELATIONSHIP_CAPABILITIES.declaredCardinalities
|
|
35
|
+
return typeof relDef.type === 'string' && Object.hasOwn(cardinalities, relDef.type)
|
|
36
|
+
? cardinalities[/** @type {keyof typeof cardinalities} */ (relDef.type)]
|
|
37
|
+
: null
|
|
19
38
|
}
|
|
20
39
|
|
|
40
|
+
/** @param {{ relationshipName: string, relDef?: StorageFieldDefinition | null, data: unknown, fieldPath?: string }} options */
|
|
21
41
|
export function validateRelationshipDataCardinality ({
|
|
22
42
|
relationshipName,
|
|
23
43
|
relDef,
|