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,243 +1,65 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transformation functions for simplified API mode
|
|
3
|
-
*
|
|
4
|
-
* @description
|
|
5
|
-
* This module enables "simplified mode" - a developer-friendly alternative
|
|
6
|
-
* to verbose JSON:API format. It allows APIs to:
|
|
7
|
-
* - Accept plain JavaScript objects as input
|
|
8
|
-
* - Return flat response objects instead of nested JSON:API
|
|
9
|
-
* - Automatically convert between formats transparently
|
|
10
|
-
* - Maintain JSON:API compliance internally
|
|
11
|
-
*/
|
|
1
|
+
/** Plain-record and JSON:API conversions at the resource boundary. */
|
|
12
2
|
|
|
13
3
|
import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
14
4
|
|
|
5
|
+
const indexIncludedResources = (included, data) => {
|
|
6
|
+
const byType = new Map()
|
|
7
|
+
let primaryRecords = []
|
|
8
|
+
if (Array.isArray(data)) {
|
|
9
|
+
primaryRecords = data
|
|
10
|
+
} else if (data) {
|
|
11
|
+
primaryRecords = [data]
|
|
12
|
+
}
|
|
13
|
+
// Index primary records first so they win when included repeats an identity.
|
|
14
|
+
for (const record of [...primaryRecords, ...(included || [])]) {
|
|
15
|
+
const { type, id } = record
|
|
16
|
+
if (!byType.has(type)) byType.set(type, new Map())
|
|
17
|
+
const byId = byType.get(type)
|
|
18
|
+
if (!byId.has(id)) byId.set(id, record)
|
|
19
|
+
}
|
|
20
|
+
return byType
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {Object}
|
|
23
|
-
* @param {Object} deps
|
|
24
|
-
* @
|
|
25
|
-
*
|
|
26
|
-
* @
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* }
|
|
33
|
-
* const result = transformSimplifiedToJsonApi(
|
|
34
|
-
* { inputRecord: input },
|
|
35
|
-
* { context: { scopeName: 'articles', schemaStructure: schema } }
|
|
36
|
-
* );
|
|
37
|
-
*
|
|
38
|
-
* // Output: Proper JSON:API structure
|
|
39
|
-
* // {
|
|
40
|
-
* // data: {
|
|
41
|
-
* // type: 'articles',
|
|
42
|
-
* // id: '123',
|
|
43
|
-
* // attributes: {
|
|
44
|
-
* // title: 'My Article',
|
|
45
|
-
* // content: 'Article content here'
|
|
46
|
-
* // }
|
|
47
|
-
* // }
|
|
48
|
-
* // }
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* // Input: Object with relationship name (preferred way)
|
|
52
|
-
* const schema = {
|
|
53
|
-
* title: { type: 'string' },
|
|
54
|
-
* author_id: {
|
|
55
|
-
* type: 'number',
|
|
56
|
-
* belongsTo: 'users',
|
|
57
|
-
* as: 'author'
|
|
58
|
-
* }
|
|
59
|
-
* };
|
|
60
|
-
* const input = {
|
|
61
|
-
* id: '456',
|
|
62
|
-
* title: 'Another Article',
|
|
63
|
-
* author: 789 // Using relationship name
|
|
64
|
-
* };
|
|
65
|
-
*
|
|
66
|
-
* // Output: Relationship becomes JSON:API relationship
|
|
67
|
-
* // {
|
|
68
|
-
* // data: {
|
|
69
|
-
* // type: 'articles',
|
|
70
|
-
* // id: '456',
|
|
71
|
-
* // attributes: {
|
|
72
|
-
* // title: 'Another Article' // author removed from attributes
|
|
73
|
-
* // },
|
|
74
|
-
* // relationships: {
|
|
75
|
-
* // author: {
|
|
76
|
-
* // data: { type: 'users', id: '789' }
|
|
77
|
-
* // }
|
|
78
|
-
* // }
|
|
79
|
-
* // }
|
|
80
|
-
* // }
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* // Input: Object with relationship name (only supported way)
|
|
84
|
-
* const input = {
|
|
85
|
-
* id: '456',
|
|
86
|
-
* title: 'Another Article',
|
|
87
|
-
* author: 789 // Using relationship name
|
|
88
|
-
* };
|
|
89
|
-
* // Output: Same JSON:API structure as above
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* // Input: Many-to-many relationship
|
|
93
|
-
* const relationships = {
|
|
94
|
-
* tags: {
|
|
95
|
-
* type: 'manyToMany',
|
|
96
|
-
* through: 'article_tags',
|
|
97
|
-
* foreignKey: 'article_id',
|
|
98
|
-
* otherKey: 'tag_id',
|
|
99
|
-
* target: 'tags'
|
|
100
|
-
* }
|
|
101
|
-
* };
|
|
102
|
-
* const input = {
|
|
103
|
-
* title: 'Tagged Article',
|
|
104
|
-
* tags: ['10', '20', '30'] // Array of IDs
|
|
105
|
-
* };
|
|
106
|
-
*
|
|
107
|
-
* // Output: Array becomes relationship data array
|
|
108
|
-
* // {
|
|
109
|
-
* // data: {
|
|
110
|
-
* // type: 'articles',
|
|
111
|
-
* // attributes: {
|
|
112
|
-
* // title: 'Tagged Article'
|
|
113
|
-
* // },
|
|
114
|
-
* // relationships: {
|
|
115
|
-
* // tags: {
|
|
116
|
-
* // data: [
|
|
117
|
-
* // { type: 'tags', id: '10' },
|
|
118
|
-
* // { type: 'tags', id: '20' },
|
|
119
|
-
* // { type: 'tags', id: '30' }
|
|
120
|
-
* // ]
|
|
121
|
-
* // }
|
|
122
|
-
* // }
|
|
123
|
-
* // }
|
|
124
|
-
* // }
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* // Input: Already JSON:API formatted
|
|
128
|
-
* const input = {
|
|
129
|
-
* data: {
|
|
130
|
-
* type: 'articles',
|
|
131
|
-
* id: '1',
|
|
132
|
-
* attributes: { title: 'Already formatted' }
|
|
133
|
-
* }
|
|
134
|
-
* };
|
|
135
|
-
* // Output: Returned unchanged (detected by data.type)
|
|
136
|
-
*
|
|
137
|
-
* @example
|
|
138
|
-
* // Input: Polymorphic relationship with minimal object format
|
|
139
|
-
* const relationships = {
|
|
140
|
-
* commentable: {
|
|
141
|
-
* belongsToPolymorphic: {
|
|
142
|
-
* types: ['posts', 'videos'],
|
|
143
|
-
* typeField: 'commentable_type',
|
|
144
|
-
* idField: 'commentable_id'
|
|
145
|
-
* }
|
|
146
|
-
* }
|
|
147
|
-
* };
|
|
148
|
-
* const input = {
|
|
149
|
-
* text: 'Great content!',
|
|
150
|
-
* commentable: { id: '456', _type: 'posts' } // Minimal object format
|
|
151
|
-
* };
|
|
152
|
-
*
|
|
153
|
-
* // Output: Proper JSON:API structure for polymorphic
|
|
154
|
-
* // {
|
|
155
|
-
* // data: {
|
|
156
|
-
* // type: 'comments',
|
|
157
|
-
* // attributes: {
|
|
158
|
-
* // text: 'Great content!'
|
|
159
|
-
* // },
|
|
160
|
-
* // relationships: {
|
|
161
|
-
* // commentable: {
|
|
162
|
-
* // data: { type: 'posts', id: '456' }
|
|
163
|
-
* // }
|
|
164
|
-
* // }
|
|
165
|
-
* // }
|
|
166
|
-
* // }
|
|
167
|
-
*
|
|
168
|
-
* @example
|
|
169
|
-
* // Input: Clearing polymorphic relationship
|
|
170
|
-
* const input = {
|
|
171
|
-
* text: 'Updated comment',
|
|
172
|
-
* commentable: null // Clear the relationship
|
|
173
|
-
* };
|
|
174
|
-
*
|
|
175
|
-
* // Output: Relationship set to null
|
|
176
|
-
* // {
|
|
177
|
-
* // data: {
|
|
178
|
-
* // type: 'comments',
|
|
179
|
-
* // attributes: {
|
|
180
|
-
* // text: 'Updated comment'
|
|
181
|
-
* // },
|
|
182
|
-
* // relationships: {
|
|
183
|
-
* // commentable: { data: null }
|
|
184
|
-
* // }
|
|
185
|
-
* // }
|
|
186
|
-
* // }
|
|
187
|
-
*
|
|
188
|
-
* @description
|
|
189
|
-
* Used by:
|
|
190
|
-
* - rest-api-plugin for POST/PUT/PATCH when simplified: true
|
|
191
|
-
* - Processes input before validation and storage
|
|
192
|
-
*
|
|
193
|
-
* Purpose:
|
|
194
|
-
* - Provides developer-friendly plain object API
|
|
195
|
-
* - Automatically detects and converts relationships
|
|
196
|
-
* - Supports relationship alias notation
|
|
197
|
-
* - Supports polymorphic relationships with minimal object format
|
|
198
|
-
* - Maintains JSON:API compliance internally
|
|
199
|
-
* - Allows gradual migration from simple to JSON:API
|
|
200
|
-
*
|
|
201
|
-
* Data flow:
|
|
202
|
-
* 1. Checks if already JSON:API (has data.type)
|
|
203
|
-
* 2. Extracts ID from input object
|
|
204
|
-
* 3. Processes belongsTo relationships from schema
|
|
205
|
-
* 4. Processes hasMany/manyToMany from relationships
|
|
206
|
-
* 5. Processes polymorphic belongsTo from relationships
|
|
207
|
-
* 6. Remaining fields become attributes
|
|
208
|
-
* 6. Builds proper JSON:API document structure
|
|
24
|
+
* Convert plain input to a JSON:API document without mutating the input.
|
|
25
|
+
* Relationship aliases become linkage; remaining keys become attributes.
|
|
26
|
+
* A plain field named data is ordinary input, not automatic format detection.
|
|
27
|
+
* Physical belongsTo keys are rejected in favor of relationship aliases.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} scope
|
|
30
|
+
* @param {Object} scope.inputRecord - Plain input record
|
|
31
|
+
* @param {Object} deps
|
|
32
|
+
* @param {Object} deps.context
|
|
33
|
+
* @param {string} deps.context.scopeName - Resource type
|
|
34
|
+
* @param {Object} deps.context.schemaStructure - Compiled writable fields
|
|
35
|
+
* @param {Object} [deps.context.schemaRelationships] - Relationship definitions
|
|
36
|
+
* @returns {Object} JSON:API document for subsequent validation
|
|
37
|
+
* @example
|
|
38
|
+
* transformSimplifiedToJsonApi({ inputRecord: { id: '7', title: 'Book' } }, {
|
|
39
|
+
* context: { scopeName: 'books', schemaStructure: {}, schemaRelationships: {} }
|
|
40
|
+
* }) // { data: { type: 'books', id: '7', attributes: { title: 'Book' } } }
|
|
209
41
|
*/
|
|
210
42
|
export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
211
|
-
// Extract needed values from scope and deps
|
|
212
43
|
const input = scope.inputRecord
|
|
213
44
|
const scopeName = deps.context.scopeName
|
|
214
45
|
const schema = deps.context.schemaStructure
|
|
215
46
|
const relationships = deps.context.schemaRelationships
|
|
216
47
|
|
|
217
|
-
// If already JSON:API format, return as-is
|
|
218
|
-
if (input?.data?.type) {
|
|
219
|
-
return input
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const attributes = {}
|
|
223
48
|
const relationshipsData = {}
|
|
224
|
-
const tempInput = { ...input }
|
|
49
|
+
const tempInput = { ...input }
|
|
225
50
|
|
|
226
51
|
const id = tempInput.id
|
|
227
|
-
delete tempInput.id
|
|
52
|
+
delete tempInput.id
|
|
228
53
|
|
|
229
|
-
//
|
|
230
|
-
// Iterate through the schema structure to find belongsTo fields
|
|
54
|
+
// Relationship input uses aliases; physical foreign keys are rejected.
|
|
231
55
|
for (const [fieldName, fieldDef] of Object.entries(schema)) {
|
|
232
56
|
if (fieldDef.belongsTo) {
|
|
233
|
-
const
|
|
234
|
-
let valueToProcess
|
|
57
|
+
const relName = fieldDef.as || fieldName
|
|
235
58
|
|
|
236
|
-
// Check if the user provided the foreign key name (e.g., country_id) - NO LONGER ALLOWED
|
|
237
59
|
if (tempInput[fieldName] !== undefined) {
|
|
238
60
|
throw new RestApiValidationError(
|
|
239
61
|
`Foreign key field '${fieldName}' is no longer supported for relationship input. ` +
|
|
240
|
-
`Use the relationship name '${
|
|
62
|
+
`Use the relationship name '${relName}' instead.`,
|
|
241
63
|
{
|
|
242
64
|
fields: [fieldName],
|
|
243
65
|
violations: [{
|
|
@@ -249,16 +71,11 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
249
71
|
)
|
|
250
72
|
}
|
|
251
73
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (valueToProcess !== undefined) {
|
|
260
|
-
relationshipsData[relAlias] = {
|
|
261
|
-
data: valueToProcess ? { type: fieldDef.belongsTo, id: String(valueToProcess) } : null
|
|
74
|
+
const value = tempInput[relName]
|
|
75
|
+
if (value !== undefined) {
|
|
76
|
+
delete tempInput[relName]
|
|
77
|
+
relationshipsData[relName] = {
|
|
78
|
+
data: value === null ? null : { type: fieldDef.belongsTo, id: value }
|
|
262
79
|
}
|
|
263
80
|
}
|
|
264
81
|
}
|
|
@@ -278,22 +95,22 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
278
95
|
// - For hasMany/hasOne: the targetType is specified in the target property
|
|
279
96
|
const targetType = relConfig.type === 'manyToMany' ? relName : relConfig.target
|
|
280
97
|
relationshipsData[relName] = {
|
|
281
|
-
data: value.map(relId => ({ type: targetType, id:
|
|
98
|
+
data: value.map(relId => ({ type: targetType, id: relId }))
|
|
282
99
|
}
|
|
283
100
|
} else if (relConfig.type === 'hasOne' && value !== undefined) {
|
|
284
101
|
relationshipsData[relName] = {
|
|
285
|
-
data: value ? { type: relConfig.target, id:
|
|
102
|
+
data: value === null ? null : { type: relConfig.target, id: value }
|
|
286
103
|
}
|
|
287
104
|
} else if (relConfig.belongsTo && value !== undefined) {
|
|
288
105
|
relationshipsData[relName] = {
|
|
289
|
-
data: value ? { type: relConfig.belongsTo, id:
|
|
106
|
+
data: value === null ? null : { type: relConfig.belongsTo, id: value }
|
|
290
107
|
}
|
|
291
108
|
} else if (relConfig.belongsToPolymorphic && value !== undefined) {
|
|
292
109
|
if (value === null) {
|
|
293
110
|
relationshipsData[relName] = { data: null }
|
|
294
|
-
} else if (typeof value === 'object' && value
|
|
111
|
+
} else if (typeof value === 'object' && !Array.isArray(value) && Object.hasOwn(value, 'id') && value._type) {
|
|
295
112
|
relationshipsData[relName] = {
|
|
296
|
-
data: { type: value._type, id:
|
|
113
|
+
data: { type: value._type, id: value.id }
|
|
297
114
|
}
|
|
298
115
|
} else {
|
|
299
116
|
throw new RestApiValidationError(
|
|
@@ -313,12 +130,12 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
313
130
|
}
|
|
314
131
|
|
|
315
132
|
// 3. Any remaining fields in tempInput are attributes
|
|
316
|
-
|
|
133
|
+
const attributes = tempInput
|
|
317
134
|
|
|
318
135
|
return {
|
|
319
136
|
data: {
|
|
320
137
|
type: scopeName,
|
|
321
|
-
...(id && { id
|
|
138
|
+
...(id !== undefined && { id }),
|
|
322
139
|
...(Object.keys(attributes).length > 0 && { attributes }),
|
|
323
140
|
...(Object.keys(relationshipsData).length > 0 && { relationships: relationshipsData })
|
|
324
141
|
}
|
|
@@ -326,181 +143,39 @@ export const transformSimplifiedToJsonApi = (scope, deps) => {
|
|
|
326
143
|
}
|
|
327
144
|
|
|
328
145
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* @param {Object}
|
|
335
|
-
* @param {Object}
|
|
336
|
-
* @param {Object} deps
|
|
337
|
-
* @
|
|
338
|
-
*
|
|
339
|
-
* @
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* content: 'Content here'
|
|
348
|
-
* }
|
|
349
|
-
* }
|
|
350
|
-
* };
|
|
351
|
-
* const result = transformJsonApiToSimplified(
|
|
352
|
-
* { record: jsonApi },
|
|
353
|
-
* { context: { schemaStructure: schema } }
|
|
354
|
-
* );
|
|
355
|
-
*
|
|
356
|
-
* // Output: Flattened object
|
|
357
|
-
* // {
|
|
358
|
-
* // id: '123',
|
|
359
|
-
* // title: 'My Article', // Attributes flattened
|
|
360
|
-
* // content: 'Content here'
|
|
361
|
-
* // }
|
|
362
|
-
*
|
|
363
|
-
* @example
|
|
364
|
-
* // Input: Resource with belongsTo relationship
|
|
365
|
-
* const schema = {
|
|
366
|
-
* title: { type: 'string' },
|
|
367
|
-
* author_id: { type: 'number', belongsTo: 'users', as: 'author' }
|
|
368
|
-
* };
|
|
369
|
-
* const jsonApi = {
|
|
370
|
-
* data: {
|
|
371
|
-
* type: 'articles',
|
|
372
|
-
* id: '456',
|
|
373
|
-
* attributes: { title: 'Article with Author' },
|
|
374
|
-
* relationships: {
|
|
375
|
-
* author: {
|
|
376
|
-
* data: { type: 'users', id: '789' }
|
|
377
|
-
* }
|
|
378
|
-
* }
|
|
379
|
-
* }
|
|
380
|
-
* };
|
|
381
|
-
*
|
|
382
|
-
* // Output: Relationship becomes foreign key
|
|
383
|
-
* // {
|
|
384
|
-
* // id: '456',
|
|
385
|
-
* // title: 'Article with Author',
|
|
386
|
-
* // author_id: '789' // Restored from relationship
|
|
387
|
-
* // }
|
|
388
|
-
*
|
|
389
|
-
* @example
|
|
390
|
-
* // Input: Collection response
|
|
391
|
-
* const jsonApi = {
|
|
392
|
-
* data: [
|
|
393
|
-
* {
|
|
394
|
-
* type: 'articles',
|
|
395
|
-
* id: '1',
|
|
396
|
-
* attributes: { title: 'First Article' }
|
|
397
|
-
* },
|
|
398
|
-
* {
|
|
399
|
-
* type: 'articles',
|
|
400
|
-
* id: '2',
|
|
401
|
-
* attributes: { title: 'Second Article' }
|
|
402
|
-
* }
|
|
403
|
-
* ],
|
|
404
|
-
* meta: { total: 2 },
|
|
405
|
-
* links: { next: '/articles?page=2' }
|
|
406
|
-
* };
|
|
407
|
-
*
|
|
408
|
-
* // Output: Preserves meta and links
|
|
409
|
-
* // {
|
|
410
|
-
* // data: [
|
|
411
|
-
* // { id: '1', title: 'First Article' },
|
|
412
|
-
* // { id: '2', title: 'Second Article' }
|
|
413
|
-
* // ],
|
|
414
|
-
* // meta: { total: 2 },
|
|
415
|
-
* // links: { next: '/articles?page=2' }
|
|
416
|
-
* // }
|
|
417
|
-
*
|
|
418
|
-
* @example
|
|
419
|
-
* // Input: With included resources
|
|
420
|
-
* const jsonApi = {
|
|
421
|
-
* data: {
|
|
422
|
-
* type: 'articles',
|
|
423
|
-
* id: '400',
|
|
424
|
-
* attributes: { title: 'JavaScript Guide' },
|
|
425
|
-
* relationships: {
|
|
426
|
-
* author: {
|
|
427
|
-
* data: { type: 'authors', id: '75' }
|
|
428
|
-
* },
|
|
429
|
-
* chapters: {
|
|
430
|
-
* data: [
|
|
431
|
-
* { type: 'chapters', id: '1001' },
|
|
432
|
-
* { type: 'chapters', id: '1002' }
|
|
433
|
-
* ]
|
|
434
|
-
* }
|
|
435
|
-
* }
|
|
436
|
-
* },
|
|
437
|
-
* included: [
|
|
438
|
-
* {
|
|
439
|
-
* type: 'authors',
|
|
440
|
-
* id: '75',
|
|
441
|
-
* attributes: { name: 'Jane Smith' }
|
|
442
|
-
* },
|
|
443
|
-
* {
|
|
444
|
-
* type: 'chapters',
|
|
445
|
-
* id: '1001',
|
|
446
|
-
* attributes: { number: 1, title: 'Introduction' }
|
|
447
|
-
* },
|
|
448
|
-
* {
|
|
449
|
-
* type: 'chapters',
|
|
450
|
-
* id: '1002',
|
|
451
|
-
* attributes: { number: 2, title: 'Basics' }
|
|
452
|
-
* }
|
|
453
|
-
* ]
|
|
454
|
-
* };
|
|
455
|
-
*
|
|
456
|
-
* // Output: Expands included resources
|
|
457
|
-
* // {
|
|
458
|
-
* // id: '400',
|
|
459
|
-
* // title: 'JavaScript Guide',
|
|
460
|
-
* // author_id: '75',
|
|
461
|
-
* // author: { // Expanded from included
|
|
462
|
-
* // id: '75',
|
|
463
|
-
* // name: 'Jane Smith'
|
|
464
|
-
* // },
|
|
465
|
-
* // chapters: [{ id: '1001' }, { id: '1002' }],
|
|
466
|
-
* // chapters: [ // Expanded array
|
|
467
|
-
* // { id: '1001', number: 1, title: 'Introduction' },
|
|
468
|
-
* // { id: '1002', number: 2, title: 'Basics' }
|
|
469
|
-
* // ]
|
|
470
|
-
* // }
|
|
471
|
-
*
|
|
472
|
-
* @description
|
|
473
|
-
* Used by:
|
|
474
|
-
* - rest-api-plugin for GET/QUERY when simplified: true
|
|
475
|
-
* - Transforms responses before sending to client
|
|
476
|
-
*
|
|
477
|
-
* Purpose:
|
|
478
|
-
* - Provides cleaner response format for clients
|
|
479
|
-
* - Creates minimal relationship objects from relationships
|
|
480
|
-
* - Expands included resources for convenience
|
|
481
|
-
* - Maintains compatibility with collection metadata
|
|
482
|
-
* - Handles both single and array responses
|
|
483
|
-
*
|
|
484
|
-
* Data flow:
|
|
485
|
-
* 1. Detects array vs single resource
|
|
486
|
-
* 2. For arrays, preserves meta/links at root
|
|
487
|
-
* 3. Calls transformSingleJsonApiToSimplified for each
|
|
488
|
-
* 4. Returns simplified structure
|
|
146
|
+
* Flatten a JSON:API response using one resource index for the whole document.
|
|
147
|
+
* Collections retain their data wrapper, meta and links; single resources
|
|
148
|
+
* return the plain record directly. Missing/null data passes through unchanged.
|
|
149
|
+
* Relationship output uses alias-named objects, not physical foreign-key fields.
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} scope
|
|
152
|
+
* @param {Object} scope.record - JSON:API response document
|
|
153
|
+
* @param {Object} deps
|
|
154
|
+
* @param {Object} deps.context
|
|
155
|
+
* @param {Object} [deps.context.schemaStructure] - Compiled resource fields
|
|
156
|
+
* @param {Object} [deps.context.schemaRelationships] - Relationship definitions
|
|
157
|
+
* @param {Object} deps.context.scopes - Resource registry for included records
|
|
158
|
+
* @returns {Object} Plain record or collection envelope
|
|
159
|
+
* @example
|
|
160
|
+
* transformJsonApiToSimplified({ record: {
|
|
161
|
+
* data: { type: 'books', id: '7', attributes: { title: 'Book' },
|
|
162
|
+
* relationships: { author: { data: { type: 'authors', id: '2' } } } }
|
|
163
|
+
* } }, { context: {} }) // { id: '7', title: 'Book', author: { id: '2' } }
|
|
489
164
|
*/
|
|
490
165
|
export const transformJsonApiToSimplified = (scope, deps) => {
|
|
491
|
-
// Extract needed values from scope and deps
|
|
492
166
|
const jsonApi = scope.record
|
|
493
167
|
const schema = deps.context.schemaStructure
|
|
494
168
|
const relationships = deps.context.schemaRelationships
|
|
495
169
|
const scopes = deps.context.scopes
|
|
496
170
|
|
|
497
171
|
if (!jsonApi?.data) return jsonApi
|
|
172
|
+
const includedByType = indexIncludedResources(jsonApi.included, jsonApi.data)
|
|
498
173
|
|
|
499
174
|
// Handle array response (QUERY)
|
|
500
175
|
if (Array.isArray(jsonApi.data)) {
|
|
501
176
|
const simplifiedData = jsonApi.data.map(item =>
|
|
502
177
|
transformSingleJsonApiToSimplified(
|
|
503
|
-
{ data: item,
|
|
178
|
+
{ data: item, includedByType },
|
|
504
179
|
{ context: { schemaStructure: schema, schemaRelationships: relationships, scopes } }
|
|
505
180
|
)
|
|
506
181
|
)
|
|
@@ -523,328 +198,54 @@ export const transformJsonApiToSimplified = (scope, deps) => {
|
|
|
523
198
|
|
|
524
199
|
// Handle single response (no change for single resources)
|
|
525
200
|
return transformSingleJsonApiToSimplified(
|
|
526
|
-
{ data: jsonApi.data,
|
|
201
|
+
{ data: jsonApi.data, includedByType },
|
|
527
202
|
{ context: { schemaStructure: schema, schemaRelationships: relationships, scopes } }
|
|
528
203
|
)
|
|
529
204
|
}
|
|
530
205
|
|
|
531
206
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
* @param {Object}
|
|
538
|
-
* @param {Object}
|
|
539
|
-
* @param {Object}
|
|
540
|
-
* @param {
|
|
541
|
-
* @
|
|
542
|
-
*
|
|
543
|
-
* @
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
* id: '100',
|
|
548
|
-
* attributes: {
|
|
549
|
-
* text: 'Great article!',
|
|
550
|
-
* created_at: '2024-01-15'
|
|
551
|
-
* },
|
|
552
|
-
* relationships: {
|
|
553
|
-
* author: {
|
|
554
|
-
* data: { type: 'users', id: '50' }
|
|
555
|
-
* },
|
|
556
|
-
* article: {
|
|
557
|
-
* data: { type: 'articles', id: '200' }
|
|
558
|
-
* }
|
|
559
|
-
* }
|
|
560
|
-
* };
|
|
561
|
-
* const schema = {
|
|
562
|
-
* text: { type: 'string' },
|
|
563
|
-
* created_at: { type: 'datetime' },
|
|
564
|
-
* author_id: { type: 'number', belongsTo: 'users', as: 'author' },
|
|
565
|
-
* article_id: { type: 'number', belongsTo: 'articles', as: 'article' }
|
|
566
|
-
* };
|
|
567
|
-
* const result = transformSingleJsonApiToSimplified(
|
|
568
|
-
* { data: data, included: null },
|
|
569
|
-
* { context: { schemaStructure: schema } }
|
|
570
|
-
* );
|
|
571
|
-
*
|
|
572
|
-
* // Output: Minimal relationship objects
|
|
573
|
-
* // {
|
|
574
|
-
* // id: '100',
|
|
575
|
-
* // text: 'Great article!',
|
|
576
|
-
* // created_at: '2024-01-15',
|
|
577
|
-
* // author: { id: '50' }, // Minimal object from relationships.author
|
|
578
|
-
* // article: { id: '200' } // Minimal object from relationships.article
|
|
579
|
-
* // }
|
|
580
|
-
*
|
|
581
|
-
* @example
|
|
582
|
-
* // Input: Many-to-many relationships
|
|
583
|
-
* const data = {
|
|
584
|
-
* type: 'articles',
|
|
585
|
-
* id: '300',
|
|
586
|
-
* attributes: { title: 'My Article' },
|
|
587
|
-
* relationships: {
|
|
588
|
-
* tags: {
|
|
589
|
-
* data: [
|
|
590
|
-
* { type: 'tags', id: '1' },
|
|
591
|
-
* { type: 'tags', id: '2' },
|
|
592
|
-
* { type: 'tags', id: '3' }
|
|
593
|
-
* ]
|
|
594
|
-
* }
|
|
595
|
-
* }
|
|
596
|
-
* };
|
|
597
|
-
* const relationships = {
|
|
598
|
-
* tags: { manyToMany: { via: 'article_tags' } }
|
|
599
|
-
* };
|
|
600
|
-
*
|
|
601
|
-
* // Output: Minimal relationship objects
|
|
602
|
-
* // {
|
|
603
|
-
* // id: '300',
|
|
604
|
-
* // title: 'My Article',
|
|
605
|
-
* // tags: [{ id: '1' }, { id: '2' }, { id: '3' }] // Minimal objects
|
|
606
|
-
* // }
|
|
607
|
-
*
|
|
608
|
-
* @example
|
|
609
|
-
* // Input: Polymorphic relationship
|
|
610
|
-
* const data = {
|
|
611
|
-
* type: 'reviews',
|
|
612
|
-
* id: '500',
|
|
613
|
-
* attributes: { rating: 5, comment: 'Great!' },
|
|
614
|
-
* relationships: {
|
|
615
|
-
* reviewable: {
|
|
616
|
-
* data: { type: 'authors', id: '75' }
|
|
617
|
-
* }
|
|
618
|
-
* }
|
|
619
|
-
* };
|
|
620
|
-
* const relationships = {
|
|
621
|
-
* reviewable: {
|
|
622
|
-
* belongsToPolymorphic: {
|
|
623
|
-
* types: ['authors', 'publishers'],
|
|
624
|
-
* typeField: 'reviewable_type',
|
|
625
|
-
* idField: 'reviewable_id'
|
|
626
|
-
* }
|
|
627
|
-
* }
|
|
628
|
-
* };
|
|
629
|
-
*
|
|
630
|
-
* // Output: Minimal polymorphic object with type
|
|
631
|
-
* // {
|
|
632
|
-
* // id: '500',
|
|
633
|
-
* // rating: 5,
|
|
634
|
-
* // comment: 'Great!',
|
|
635
|
-
* // reviewable: { id: '75', type: 'authors' } // Minimal object with type
|
|
636
|
-
* // }
|
|
637
|
-
*
|
|
638
|
-
* @example
|
|
639
|
-
* // Input: Polymorphic relationship with included resource
|
|
640
|
-
* const dataWithInclude = {
|
|
641
|
-
* type: 'reviews',
|
|
642
|
-
* id: '500',
|
|
643
|
-
* attributes: { rating: 5, comment: 'Great!' },
|
|
644
|
-
* relationships: {
|
|
645
|
-
* reviewable: {
|
|
646
|
-
* data: { type: 'authors', id: '75' }
|
|
647
|
-
* }
|
|
648
|
-
* }
|
|
649
|
-
* };
|
|
650
|
-
* const included = [
|
|
651
|
-
* {
|
|
652
|
-
* type: 'authors',
|
|
653
|
-
* id: '75',
|
|
654
|
-
* attributes: { name: 'Jane Smith', bio: 'Fantastic author' }
|
|
655
|
-
* }
|
|
656
|
-
* ];
|
|
657
|
-
*
|
|
658
|
-
* // Output: Full polymorphic object (type field removed as redundant)
|
|
659
|
-
* // {
|
|
660
|
-
* // id: '500',
|
|
661
|
-
* // rating: 5,
|
|
662
|
-
* // comment: 'Great!',
|
|
663
|
-
* // reviewable: { // Full object when included
|
|
664
|
-
* // id: '75',
|
|
665
|
-
* // name: 'Jane Smith',
|
|
666
|
-
* // bio: 'Fantastic author'
|
|
667
|
-
* // }
|
|
668
|
-
* // }
|
|
669
|
-
*
|
|
670
|
-
* @example
|
|
671
|
-
* // Input: With included resources (recursive expansion)
|
|
672
|
-
* const data = {
|
|
673
|
-
* type: 'books',
|
|
674
|
-
* id: '400',
|
|
675
|
-
* attributes: { title: 'JavaScript Guide' },
|
|
676
|
-
* relationships: {
|
|
677
|
-
* author: {
|
|
678
|
-
* data: { type: 'authors', id: '75' }
|
|
679
|
-
* },
|
|
680
|
-
* chapters: {
|
|
681
|
-
* data: [
|
|
682
|
-
* { type: 'chapters', id: '1001' },
|
|
683
|
-
* { type: 'chapters', id: '1002' }
|
|
684
|
-
* ]
|
|
685
|
-
* }
|
|
686
|
-
* }
|
|
687
|
-
* };
|
|
688
|
-
* const included = [
|
|
689
|
-
* {
|
|
690
|
-
* type: 'authors',
|
|
691
|
-
* id: '75',
|
|
692
|
-
* attributes: { name: 'Jane Smith' },
|
|
693
|
-
* relationships: {
|
|
694
|
-
* company: {
|
|
695
|
-
* data: { type: 'companies', id: '5' }
|
|
696
|
-
* }
|
|
697
|
-
* }
|
|
698
|
-
* },
|
|
699
|
-
* {
|
|
700
|
-
* type: 'chapters',
|
|
701
|
-
* id: '1001',
|
|
702
|
-
* attributes: { number: 1, title: 'Introduction' }
|
|
703
|
-
* },
|
|
704
|
-
* {
|
|
705
|
-
* type: 'chapters',
|
|
706
|
-
* id: '1002',
|
|
707
|
-
* attributes: { number: 2, title: 'Basics' }
|
|
708
|
-
* }
|
|
709
|
-
* ];
|
|
710
|
-
*
|
|
711
|
-
* // Output: Recursively expanded
|
|
712
|
-
* // {
|
|
713
|
-
* // id: '400',
|
|
714
|
-
* // title: 'JavaScript Guide',
|
|
715
|
-
* // author: { // Expanded and transformed
|
|
716
|
-
* // id: '75',
|
|
717
|
-
* // name: 'Jane Smith',
|
|
718
|
-
* // company: { id: '5' } // Author's relationships processed as minimal objects!
|
|
719
|
-
* // },
|
|
720
|
-
* // chapters: [
|
|
721
|
-
* // { id: '1001', number: 1, title: 'Introduction' },
|
|
722
|
-
* // { id: '1002', number: 2, title: 'Basics' }
|
|
723
|
-
* // ]
|
|
724
|
-
* // }
|
|
725
|
-
*
|
|
726
|
-
* @description
|
|
727
|
-
* Used by:
|
|
728
|
-
* - transformJsonApiToSimplified for each resource
|
|
729
|
-
* - Calls itself recursively for included resources
|
|
730
|
-
*
|
|
731
|
-
* Purpose:
|
|
732
|
-
* - Core transformation logic for single resources
|
|
733
|
-
* - Creates minimal relationship objects from relationships
|
|
734
|
-
* - Creates arrays of minimal relationship objects for to-many relationships
|
|
735
|
-
* - Recursively expands and transforms included data
|
|
736
|
-
* - Handles polymorphic relationships
|
|
737
|
-
*
|
|
738
|
-
* Data flow:
|
|
739
|
-
* 1. Copies ID and attributes to result
|
|
740
|
-
* 2. Processes each relationship:
|
|
741
|
-
* - BelongsTo: creates minimal relationship object
|
|
742
|
-
* - Polymorphic: creates minimal relationship object with type
|
|
743
|
-
* - HasMany/ManyToMany: creates array of minimal relationship objects
|
|
744
|
-
* 3. If included data exists:
|
|
745
|
-
* - Finds matching included resources
|
|
746
|
-
* - Recursively transforms them (preserving their relationships!)
|
|
747
|
-
* - Adds transformed objects/arrays to result
|
|
748
|
-
*
|
|
749
|
-
* @private
|
|
207
|
+
* Flatten one resource and expand linkage found in the shared document index.
|
|
208
|
+
* Absent to-one linkage is omitted. Unexpanded references retain their ID;
|
|
209
|
+
* polymorphic references also retain _type. Cycle detection is path-local so
|
|
210
|
+
* repeated siblings expand independently while back-references stay minimal.
|
|
211
|
+
*
|
|
212
|
+
* @param {Object} scope
|
|
213
|
+
* @param {Object} scope.data - JSON:API resource
|
|
214
|
+
* @param {Array<Object>} [scope.included] - Used when no shared index is supplied
|
|
215
|
+
* @param {Map} [scope.includedByType] - Shared type/ID lookup
|
|
216
|
+
* @param {Object} deps
|
|
217
|
+
* @param {Object} deps.context
|
|
218
|
+
* @param {Object} [deps.context.schemaRelationships] - Relationship definitions
|
|
219
|
+
* @param {Object} deps.context.scopes - Registry for nested resource metadata
|
|
220
|
+
* @param {Set<string>} [ancestors] - Resource identities on the current path
|
|
221
|
+
* @returns {Object} Plain record with minimal or expanded relationship objects
|
|
750
222
|
*/
|
|
751
|
-
export const transformSingleJsonApiToSimplified = (scope, deps) => {
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
const included = scope.included
|
|
755
|
-
const schema = deps.context.schemaStructure
|
|
756
|
-
const relationships = deps.context.schemaRelationships
|
|
223
|
+
export const transformSingleJsonApiToSimplified = (scope, deps, ancestors = new Set()) => {
|
|
224
|
+
const { data, includedByType = indexIncludedResources(scope.included, scope.data) } = scope
|
|
225
|
+
const relationships = deps.context.schemaRelationships || {}
|
|
757
226
|
const scopes = deps.context.scopes
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
// Create minimal relationship object (no more foreign key field)
|
|
779
|
-
simplified[relName] = { id: relData.data.id }
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Check if this is a polymorphic relationship from the relationships config
|
|
784
|
-
const rel = relationships?.[relName]
|
|
785
|
-
if (rel?.belongsToPolymorphic && relData.data) {
|
|
786
|
-
// Create minimal relationship object for polymorphic (no more type/id fields)
|
|
787
|
-
simplified[relName] = { id: relData.data.id, _type: relData.data.type }
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Handle to-many relationships (create minimal objects with just IDs)
|
|
791
|
-
if (rel?.type === 'hasMany' || rel?.type === 'manyToMany') {
|
|
792
|
-
if (relData.data && Array.isArray(relData.data)) {
|
|
793
|
-
simplified[relName] = relData.data.map(item => ({ id: item.id }))
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Handle includes (nested objects)
|
|
798
|
-
if (included && relData.data) {
|
|
799
|
-
const findIncluded = (ref) =>
|
|
800
|
-
included.find(inc => inc.type === ref.type && inc.id === ref.id)
|
|
801
|
-
|
|
802
|
-
if (Array.isArray(relData.data)) {
|
|
803
|
-
const nestedData = relData.data.map(findIncluded).filter(Boolean)
|
|
804
|
-
if (nestedData.length > 0) {
|
|
805
|
-
simplified[relName] = nestedData.map(item => {
|
|
806
|
-
// For each included record, transform it recursively to restore its relationship fields
|
|
807
|
-
const itemSimplified = transformSingleJsonApiToSimplified(
|
|
808
|
-
{ data: item, included: null },
|
|
809
|
-
{
|
|
810
|
-
context: {
|
|
811
|
-
schemaStructure: scopes?.[item.type]?.vars?.schemaInfo?.schema || {},
|
|
812
|
-
schemaRelationships: scopes?.[item.type]?.vars?.schemaInfo?.schemaRelationships || {},
|
|
813
|
-
scopes
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
)
|
|
817
|
-
// For polymorphic relationships, add _type field to identify the resource type
|
|
818
|
-
if (rel?.belongsToPolymorphic) {
|
|
819
|
-
itemSimplified._type = item.type
|
|
820
|
-
}
|
|
821
|
-
return itemSimplified
|
|
822
|
-
})
|
|
823
|
-
}
|
|
824
|
-
} else {
|
|
825
|
-
const nestedData = findIncluded(relData.data)
|
|
826
|
-
if (nestedData) {
|
|
827
|
-
// Transform the single included record to restore its relationship fields
|
|
828
|
-
const itemSimplified = transformSingleJsonApiToSimplified(
|
|
829
|
-
{ data: nestedData, included: null },
|
|
830
|
-
{
|
|
831
|
-
context: {
|
|
832
|
-
schemaStructure: scopes?.[nestedData.type]?.vars?.schemaInfo?.schema || {},
|
|
833
|
-
schemaRelationships: scopes?.[nestedData.type]?.vars?.schemaInfo?.schemaRelationships || {},
|
|
834
|
-
scopes
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
)
|
|
838
|
-
// For polymorphic relationships, add _type field to identify the resource type
|
|
839
|
-
if (rel?.belongsToPolymorphic) {
|
|
840
|
-
itemSimplified._type = nestedData.type
|
|
841
|
-
}
|
|
842
|
-
simplified[relName] = itemSimplified
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
227
|
+
const identity = JSON.stringify([data.type, data.id])
|
|
228
|
+
const path = new Set(ancestors)
|
|
229
|
+
path.add(identity)
|
|
230
|
+
|
|
231
|
+
const simplified = { ...(data.id != null ? { id: data.id } : {}), ...data.attributes }
|
|
232
|
+
for (const [name, relationship] of Object.entries(data.relationships || {})) {
|
|
233
|
+
if (relationship?.data == null) continue
|
|
234
|
+
const polymorphic = relationships[name]?.belongsToPolymorphic
|
|
235
|
+
const convert = reference => {
|
|
236
|
+
const minimal = { id: reference.id, ...(polymorphic ? { _type: reference.type } : {}) }
|
|
237
|
+
// Expand repeated siblings independently; stop only cycles on this path.
|
|
238
|
+
if (path.has(JSON.stringify([reference.type, reference.id]))) return minimal
|
|
239
|
+
const target = includedByType.get(reference.type)?.get(reference.id)
|
|
240
|
+
if (!target) return minimal
|
|
241
|
+
const schemaInfo = scopes?.[target.type]?.vars?.schemaInfo
|
|
242
|
+
const nested = transformSingleJsonApiToSimplified({ data: target, includedByType }, {
|
|
243
|
+
context: { schemaStructure: schemaInfo?.schemaStructure, schemaRelationships: schemaInfo?.schemaRelationships, scopes }
|
|
244
|
+
}, path)
|
|
245
|
+
if (polymorphic) nested._type = reference.type
|
|
246
|
+
return nested
|
|
846
247
|
}
|
|
248
|
+
simplified[name] = Array.isArray(relationship.data) ? relationship.data.map(convert) : convert(relationship.data)
|
|
847
249
|
}
|
|
848
|
-
|
|
849
250
|
return simplified
|
|
850
251
|
}
|