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
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
// To-one loaders attach linkage and selected records; deps.loadNestedIncludes traverses child paths.
|
|
2
|
+
import { whereInIdentifiers } from './identifier-query.js'
|
|
3
|
+
import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
|
|
4
|
+
import { toJsonApiRecord } from './knex-json-api-transformers-querying.js'
|
|
5
|
+
import {
|
|
6
|
+
RELATIONSHIPS_KEY,
|
|
7
|
+
RELATIONSHIP_METADATA_KEY,
|
|
8
|
+
COMPUTED_DEPENDENCIES_KEY,
|
|
9
|
+
RELATIONSHIP_READ_BATCH_SIZE,
|
|
10
|
+
} from '../querying-writing/knex-constants.js'
|
|
11
|
+
import { getOperationDiagnosticContext, wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
12
|
+
import { getPolymorphicLinkage } from '../querying-writing/relationship-contracts.js'
|
|
13
|
+
import {
|
|
14
|
+
resolveScopeStorageAdapter,
|
|
15
|
+
getIdColumnForScope,
|
|
16
|
+
getFieldValueForScope,
|
|
17
|
+
translateColumnForScope,
|
|
18
|
+
buildRelationshipLinks,
|
|
19
|
+
applyScopeFiltersToIncludeQuery,
|
|
20
|
+
groupByPolymorphicType,
|
|
21
|
+
loadRelationshipMetadata,
|
|
22
|
+
} from './include-query-helpers.js'
|
|
23
|
+
/**
|
|
24
|
+
* Loads belongsTo relationships (many-to-one)
|
|
25
|
+
*
|
|
26
|
+
* This function loads the "parent" side of a relationship. For example, if comments
|
|
27
|
+
* belong to articles, this loads the articles for a set of comments.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} scope - The resource object containing:
|
|
30
|
+
* - records: Array<Object> - Records to load relationships for
|
|
31
|
+
* - fieldName: string - The foreign key field name (e.g., 'author_id')
|
|
32
|
+
* - fieldDef: Object - The field definition from the schema
|
|
33
|
+
* - includeName: string - The relationship name (e.g., 'author')
|
|
34
|
+
* - subIncludes: Object - Nested includes to process recursively
|
|
35
|
+
* - included: Map - Map of already included resources
|
|
36
|
+
* - processedPaths: Set - Set of already processed paths
|
|
37
|
+
* - currentPath: string - Current include path for tracking
|
|
38
|
+
* - fields: Object - Sparse fieldsets configuration
|
|
39
|
+
* @param {Object} deps - Dependencies object containing:
|
|
40
|
+
* - context.scopes: Object - The resources object
|
|
41
|
+
* - context.log: Object - Logger instance
|
|
42
|
+
* - context.knex: Object - Knex instance
|
|
43
|
+
* @returns {Promise<void>}
|
|
44
|
+
*/
|
|
45
|
+
export const loadBelongsTo = async (scope, deps) => {
|
|
46
|
+
const { records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
47
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
48
|
+
try {
|
|
49
|
+
log.trace('[INCLUDE] Loading belongsTo:', {
|
|
50
|
+
fieldName,
|
|
51
|
+
includeName,
|
|
52
|
+
recordCount: records.length
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Get the target scope name
|
|
56
|
+
const targetScope = fieldDef.belongsTo
|
|
57
|
+
if (!scopes[targetScope]) {
|
|
58
|
+
try {
|
|
59
|
+
await log.warn('[INCLUDE] Target scope not found:', {
|
|
60
|
+
...getOperationDiagnosticContext(requestContext || {}, { phase: 'include', scopeName, backend: knex.client?.config?.client }), targetScope
|
|
61
|
+
})
|
|
62
|
+
} catch { /* Diagnostics must preserve the missing-target decision. */ }
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Collect all foreign key values
|
|
67
|
+
const foreignKeyValues = records
|
|
68
|
+
.map(record => getFieldValueForScope(scopes, scopeName, record, fieldName))
|
|
69
|
+
.filter(val => val != null) // Filter out null/undefined
|
|
70
|
+
|
|
71
|
+
const uniqueIds = [...new Set(foreignKeyValues)]
|
|
72
|
+
|
|
73
|
+
log.debug('[INCLUDE] Loading belongsTo records:', {
|
|
74
|
+
targetScope,
|
|
75
|
+
idCount: uniqueIds.length
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (uniqueIds.length === 0) {
|
|
79
|
+
// No relationships to load, set all to null
|
|
80
|
+
records.forEach(record => {
|
|
81
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
82
|
+
const relationshipObject = { data: null }
|
|
83
|
+
|
|
84
|
+
// Add links if urlPrefix is configured
|
|
85
|
+
if (scopeName) {
|
|
86
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
90
|
+
})
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get target table info
|
|
95
|
+
const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
|
|
96
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
97
|
+
// Select visible fields, projections and their dependencies.
|
|
98
|
+
const targetScopeObject = scopes[targetScope]
|
|
99
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
100
|
+
const fieldSelectionInfo = await buildFieldSelection(targetScopeObject, {
|
|
101
|
+
context: {
|
|
102
|
+
...(requestContext || {}),
|
|
103
|
+
scopeName: targetScope,
|
|
104
|
+
queryParams: { fields },
|
|
105
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
106
|
+
storageAdapter,
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Load the target records
|
|
111
|
+
const targetRecords = []
|
|
112
|
+
for (let offset = 0; offset < uniqueIds.length; offset += RELATIONSHIP_READ_BATCH_SIZE) {
|
|
113
|
+
let query = knex(targetTableName).whereIn(targetIdColumn, uniqueIds.slice(offset, offset + RELATIONSHIP_READ_BATCH_SIZE))
|
|
114
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
115
|
+
query,
|
|
116
|
+
scope: targetScopeObject,
|
|
117
|
+
fieldSelectionInfo,
|
|
118
|
+
tableName: targetTableName,
|
|
119
|
+
useTablePrefix: false,
|
|
120
|
+
storageAdapter,
|
|
121
|
+
db: knex,
|
|
122
|
+
context: {
|
|
123
|
+
...(requestContext || {}),
|
|
124
|
+
scopeName: targetScope,
|
|
125
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
126
|
+
storageAdapter,
|
|
127
|
+
},
|
|
128
|
+
scopeName: targetScope
|
|
129
|
+
})
|
|
130
|
+
query = selectionState.query
|
|
131
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
132
|
+
query,
|
|
133
|
+
scopes,
|
|
134
|
+
scopeName: targetScope,
|
|
135
|
+
requestContext,
|
|
136
|
+
db: knex,
|
|
137
|
+
tableName: targetTableName
|
|
138
|
+
})
|
|
139
|
+
query = scopedQueryState.query
|
|
140
|
+
for (const record of await query) targetRecords.push(record)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Load relationship metadata for all target records
|
|
144
|
+
await loadRelationshipMetadata(scopes, targetRecords, targetScope)
|
|
145
|
+
|
|
146
|
+
// Create lookup map
|
|
147
|
+
const targetById = Object.create(null)
|
|
148
|
+
targetRecords.forEach(record => {
|
|
149
|
+
targetById[getFieldValueForScope(scopes, targetScope, record, 'id')] = record
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Set relationships on original records
|
|
153
|
+
const targetRecordsToProcess = new Set()
|
|
154
|
+
|
|
155
|
+
records.forEach(record => {
|
|
156
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
157
|
+
|
|
158
|
+
const targetId = getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
159
|
+
const targetRecord = targetById[targetId]
|
|
160
|
+
|
|
161
|
+
if (targetRecord) {
|
|
162
|
+
// Convert to JSON:API format
|
|
163
|
+
const jsonApiRecord = toJsonApiRecord(
|
|
164
|
+
scopes[targetScope],
|
|
165
|
+
targetRecord,
|
|
166
|
+
targetScope
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Add relationships from metadata
|
|
170
|
+
if (targetRecord[RELATIONSHIP_METADATA_KEY]) {
|
|
171
|
+
jsonApiRecord.relationships = targetRecord[RELATIONSHIP_METADATA_KEY]
|
|
172
|
+
// Clean up the temporary property
|
|
173
|
+
delete targetRecord[RELATIONSHIP_METADATA_KEY]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Attach computed dependencies info if sparse fieldsets were used
|
|
177
|
+
if (fieldSelectionInfo?.computedDependencies) {
|
|
178
|
+
jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const relationshipObject = { data: { type: targetScope, id: String(targetId) } }
|
|
182
|
+
|
|
183
|
+
// Add links if urlPrefix is configured
|
|
184
|
+
if (scopeName) {
|
|
185
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
189
|
+
|
|
190
|
+
// Add to included if not already there
|
|
191
|
+
const resourceKey = `${targetScope}:${targetId}`
|
|
192
|
+
if (!included.has(resourceKey)) {
|
|
193
|
+
included.set(resourceKey, jsonApiRecord) // Now includes relationships!
|
|
194
|
+
}
|
|
195
|
+
if (Object.keys(subIncludes).length > 0) {
|
|
196
|
+
targetRecordsToProcess.add(targetRecord)
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
const relationshipObject = { data: null }
|
|
200
|
+
|
|
201
|
+
// Add links if urlPrefix is configured
|
|
202
|
+
if (scopeName) {
|
|
203
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Process nested includes if any
|
|
211
|
+
if (targetRecordsToProcess.size > 0) {
|
|
212
|
+
const nextPath = `${currentPath}.${includeName}`
|
|
213
|
+
await deps.loadNestedIncludes(
|
|
214
|
+
{ records: [...targetRecordsToProcess], scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
215
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
throw wrapUnexpectedError(error, {
|
|
220
|
+
message: `Failed to load belongsTo relationship '${includeName}' for field '${fieldName}'`,
|
|
221
|
+
context: { phase: 'include', fieldName, includeName, targetScope: fieldDef.belongsTo }
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Loads hasOne relationships (one-to-one)
|
|
228
|
+
*
|
|
229
|
+
* Similar to hasMany but expects a single related record.
|
|
230
|
+
* For example, a user that has one profile.
|
|
231
|
+
*
|
|
232
|
+
* @param {Object} scope - The resource object
|
|
233
|
+
* @param {Object} deps - Dependencies object
|
|
234
|
+
*/
|
|
235
|
+
export const loadHasOne = async (scope, deps) => {
|
|
236
|
+
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
237
|
+
const { context: { scopes, log, knex, capabilities, requestContext } } = deps
|
|
238
|
+
|
|
239
|
+
const childLog = log?.child ? log.child({ method: 'loadHasOne' }) : log
|
|
240
|
+
|
|
241
|
+
if (!records || records.length === 0) {
|
|
242
|
+
childLog?.trace?.('[INCLUDE] No records to load hasOne for')
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Get parent IDs
|
|
247
|
+
const mainIds = [...new Set(records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(id => id !== null && id !== undefined))]
|
|
248
|
+
|
|
249
|
+
if (mainIds.length === 0) {
|
|
250
|
+
childLog?.trace?.('[INCLUDE] No parent IDs found, skipping hasOne load')
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const targetScope = relDef.target
|
|
255
|
+
const targetTable = scopes[targetScope].vars.schemaInfo.tableName
|
|
256
|
+
const foreignKey = relDef.foreignKey
|
|
257
|
+
|
|
258
|
+
if (!foreignKey) {
|
|
259
|
+
throw new Error(`Missing foreignKey in hasOne relationship '${includeName}' for scope '${scopeName}'`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
childLog?.debug?.(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
|
|
263
|
+
parentCount: mainIds.length
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Build query for hasOne - expects single result per parent
|
|
267
|
+
const targetScopeObject = scopes[targetScope]
|
|
268
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
269
|
+
|
|
270
|
+
// Select visible fields, projections and their dependencies.
|
|
271
|
+
const fieldSelectionInfo = await buildFieldSelection(targetScopeObject, {
|
|
272
|
+
context: {
|
|
273
|
+
...(requestContext || {}),
|
|
274
|
+
scopeName: targetScope,
|
|
275
|
+
queryParams: { fields },
|
|
276
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
277
|
+
storageAdapter,
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Query related records
|
|
282
|
+
const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
|
|
283
|
+
let query = knex(targetTable).modify(whereInIdentifiers, foreignKeyColumn, mainIds)
|
|
284
|
+
|
|
285
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
286
|
+
query,
|
|
287
|
+
scope: targetScopeObject,
|
|
288
|
+
fieldSelectionInfo,
|
|
289
|
+
tableName: targetTable,
|
|
290
|
+
useTablePrefix: false,
|
|
291
|
+
storageAdapter,
|
|
292
|
+
db: knex,
|
|
293
|
+
context: {
|
|
294
|
+
...(requestContext || {}),
|
|
295
|
+
scopeName: targetScope,
|
|
296
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
297
|
+
storageAdapter,
|
|
298
|
+
},
|
|
299
|
+
scopeName: targetScope
|
|
300
|
+
})
|
|
301
|
+
query = selectionState.query
|
|
302
|
+
|
|
303
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
304
|
+
query,
|
|
305
|
+
scopes,
|
|
306
|
+
scopeName: targetScope,
|
|
307
|
+
requestContext,
|
|
308
|
+
db: knex,
|
|
309
|
+
tableName: targetTable
|
|
310
|
+
})
|
|
311
|
+
query = scopedQueryState.query
|
|
312
|
+
|
|
313
|
+
const relatedRecords = await query
|
|
314
|
+
await loadRelationshipMetadata(scopes, relatedRecords, targetScope)
|
|
315
|
+
|
|
316
|
+
// Create a map of parent ID to related record (should be one-to-one)
|
|
317
|
+
const relatedByParentId = Object.create(null)
|
|
318
|
+
for (const related of relatedRecords) {
|
|
319
|
+
const parentId = getFieldValueForScope(scopes, targetScope, related, foreignKey)
|
|
320
|
+
if (relatedByParentId[parentId]) {
|
|
321
|
+
try {
|
|
322
|
+
await log.warn('[INCLUDE] Multiple records found for hasOne relationship', {
|
|
323
|
+
...getOperationDiagnosticContext(requestContext || {}, { phase: 'include', scopeName, backend: knex.client?.config?.client }), includeName, foreignKey
|
|
324
|
+
})
|
|
325
|
+
} catch { /* Diagnostics must preserve the existing record selection. */ }
|
|
326
|
+
}
|
|
327
|
+
relatedByParentId[parentId] = related
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Process each parent record
|
|
331
|
+
for (const record of records) {
|
|
332
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
333
|
+
const relatedRecord = relatedByParentId[recordId]
|
|
334
|
+
|
|
335
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
336
|
+
|
|
337
|
+
if (relatedRecord) {
|
|
338
|
+
// Transform to JSON:API format
|
|
339
|
+
const transformed = toJsonApiRecord(targetScopeObject, relatedRecord, targetScope)
|
|
340
|
+
if (relatedRecord[RELATIONSHIP_METADATA_KEY]) {
|
|
341
|
+
transformed.relationships = relatedRecord[RELATIONSHIP_METADATA_KEY]
|
|
342
|
+
delete relatedRecord[RELATIONSHIP_METADATA_KEY]
|
|
343
|
+
}
|
|
344
|
+
if (fieldSelectionInfo?.computedDependencies) {
|
|
345
|
+
transformed[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Set single object (not array) for hasOne
|
|
349
|
+
const relatedRecordId = getFieldValueForScope(scopes, targetScope, relatedRecord, 'id')
|
|
350
|
+
record[RELATIONSHIPS_KEY][includeName] = {
|
|
351
|
+
data: { type: targetScope, id: String(relatedRecordId) },
|
|
352
|
+
links: buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Add to included if not already there
|
|
356
|
+
const key = `${targetScope}:${relatedRecordId}`
|
|
357
|
+
if (!included.has(key)) {
|
|
358
|
+
included.set(key, transformed)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Process nested includes if any
|
|
362
|
+
if (subIncludes && Object.keys(subIncludes).length > 0) {
|
|
363
|
+
const newPath = `${currentPath}.${includeName}`
|
|
364
|
+
await deps.loadNestedIncludes(
|
|
365
|
+
{ records: [relatedRecord], scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: newPath, fields },
|
|
366
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
// No related record found
|
|
371
|
+
record[RELATIONSHIPS_KEY][includeName] = {
|
|
372
|
+
data: null,
|
|
373
|
+
links: buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Loads polymorphic belongsTo relationships
|
|
381
|
+
*
|
|
382
|
+
* Handles relationships where a record can belong to different types of parent records.
|
|
383
|
+
* For example, comments that can belong to either articles or videos.
|
|
384
|
+
*
|
|
385
|
+
* @param {Object} scope - The resource object containing:
|
|
386
|
+
* - records: Array<Object> - Records with polymorphic relationships
|
|
387
|
+
* - relName: string - The relationship name
|
|
388
|
+
* - relDef: Object - The relationship definition with belongsToPolymorphic
|
|
389
|
+
* - subIncludes: Object - Nested includes to process recursively
|
|
390
|
+
* - included: Map - Map of already included resources
|
|
391
|
+
* - processedPaths: Set - Set of already processed paths
|
|
392
|
+
* - currentPath: string - Current include path for tracking
|
|
393
|
+
* - fields: Object - Sparse fieldsets configuration
|
|
394
|
+
* @param {Object} deps - Dependencies object containing:
|
|
395
|
+
* - context.scopes: Object - The resources object
|
|
396
|
+
* - context.log: Object - Logger instance
|
|
397
|
+
* - context.knex: Object - Knex instance
|
|
398
|
+
* @returns {Promise<void>}
|
|
399
|
+
*/
|
|
400
|
+
export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
401
|
+
const { records, scopeName, relName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
402
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
403
|
+
try {
|
|
404
|
+
log.trace('[INCLUDE] Loading polymorphic belongsTo:', {
|
|
405
|
+
relName,
|
|
406
|
+
recordCount: records.length
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const { typeField, idField, types } = relDef.belongsToPolymorphic
|
|
410
|
+
|
|
411
|
+
// Group records by their target type using helper
|
|
412
|
+
const grouped = groupByPolymorphicType(
|
|
413
|
+
records,
|
|
414
|
+
typeField,
|
|
415
|
+
idField,
|
|
416
|
+
(record, fieldName) => getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
log.trace('[INCLUDE] Grouped by type:', {
|
|
420
|
+
types: Object.keys(grouped),
|
|
421
|
+
counts: Object.entries(grouped).map(([t, ids]) => `${t}: ${ids.length}`)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// Load each type separately
|
|
425
|
+
for (const [targetType, targetIds] of Object.entries(grouped)) {
|
|
426
|
+
if (targetIds.length === 0) continue
|
|
427
|
+
getPolymorphicLinkage({ type: targetType, id: targetIds[0], types, scopeName, relationshipName: relName })
|
|
428
|
+
|
|
429
|
+
// Get target table information
|
|
430
|
+
const targetSchemaInfo = scopes[targetType].vars.schemaInfo
|
|
431
|
+
const targetTable = targetSchemaInfo?.tableName || targetType
|
|
432
|
+
|
|
433
|
+
// Select visible fields, projections and their dependencies.
|
|
434
|
+
const targetScopeObject = scopes[targetType]
|
|
435
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetType, knex)
|
|
436
|
+
const fieldSelectionInfo = await buildFieldSelection(targetScopeObject, {
|
|
437
|
+
context: {
|
|
438
|
+
...(requestContext || {}),
|
|
439
|
+
scopeName: targetType,
|
|
440
|
+
queryParams: { fields },
|
|
441
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
442
|
+
storageAdapter,
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
log.debug(`[INCLUDE] Loading ${targetType} records:`, {
|
|
447
|
+
idCount: targetIds.length
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
// Query for this type
|
|
451
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetType)
|
|
452
|
+
const targetRecords = []
|
|
453
|
+
for (let offset = 0; offset < targetIds.length; offset += RELATIONSHIP_READ_BATCH_SIZE) {
|
|
454
|
+
let query = knex(targetTable).whereIn(targetIdColumn, targetIds.slice(offset, offset + RELATIONSHIP_READ_BATCH_SIZE))
|
|
455
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
456
|
+
query,
|
|
457
|
+
scope: targetScopeObject,
|
|
458
|
+
fieldSelectionInfo,
|
|
459
|
+
tableName: targetTable,
|
|
460
|
+
useTablePrefix: false,
|
|
461
|
+
storageAdapter,
|
|
462
|
+
db: knex,
|
|
463
|
+
context: {
|
|
464
|
+
...(requestContext || {}),
|
|
465
|
+
scopeName: targetType,
|
|
466
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
467
|
+
storageAdapter,
|
|
468
|
+
},
|
|
469
|
+
scopeName: targetType
|
|
470
|
+
})
|
|
471
|
+
query = selectionState.query
|
|
472
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
473
|
+
query,
|
|
474
|
+
scopes,
|
|
475
|
+
scopeName: targetType,
|
|
476
|
+
requestContext,
|
|
477
|
+
db: knex,
|
|
478
|
+
tableName: targetTable
|
|
479
|
+
})
|
|
480
|
+
query = scopedQueryState.query
|
|
481
|
+
for (const record of await query) targetRecords.push(record)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Load relationship metadata for all target records
|
|
485
|
+
await loadRelationshipMetadata(scopes, targetRecords, targetType)
|
|
486
|
+
|
|
487
|
+
// Create lookup map
|
|
488
|
+
const targetById = Object.create(null)
|
|
489
|
+
targetRecords.forEach(record => {
|
|
490
|
+
targetById[getFieldValueForScope(scopes, targetType, record, 'id')] = record
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
// Add to included and set relationships
|
|
494
|
+
records.forEach(record => {
|
|
495
|
+
if (getFieldValueForScope(scopes, scopeName, record, typeField) === targetType) {
|
|
496
|
+
const targetId = getFieldValueForScope(scopes, scopeName, record, idField)
|
|
497
|
+
const targetRecord = targetById[targetId]
|
|
498
|
+
|
|
499
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
500
|
+
|
|
501
|
+
if (targetRecord) {
|
|
502
|
+
// Add to included
|
|
503
|
+
const resourceKey = `${targetType}:${targetId}`
|
|
504
|
+
if (!included.has(resourceKey)) {
|
|
505
|
+
const jsonApiRecord = toJsonApiRecord(
|
|
506
|
+
scopes[targetType],
|
|
507
|
+
targetRecord,
|
|
508
|
+
targetType
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
// Add relationships from metadata
|
|
512
|
+
if (targetRecord[RELATIONSHIP_METADATA_KEY]) {
|
|
513
|
+
jsonApiRecord.relationships = targetRecord[RELATIONSHIP_METADATA_KEY]
|
|
514
|
+
// Clean up the temporary property
|
|
515
|
+
delete targetRecord[RELATIONSHIP_METADATA_KEY]
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Attach computed dependencies info if sparse fieldsets were used
|
|
519
|
+
if (fieldSelectionInfo?.computedDependencies) {
|
|
520
|
+
jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
included.set(resourceKey, jsonApiRecord)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const relationshipObject = {
|
|
527
|
+
data: { type: targetType, id: String(targetId) }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Add links if urlPrefix is configured
|
|
531
|
+
if (scopeName) {
|
|
532
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
536
|
+
} else {
|
|
537
|
+
const relationshipObject = { data: null }
|
|
538
|
+
|
|
539
|
+
// Add links if urlPrefix is configured
|
|
540
|
+
if (scopeName) {
|
|
541
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
// Process nested includes for this type
|
|
550
|
+
if (Object.keys(subIncludes).length > 0 && targetRecords.length > 0) {
|
|
551
|
+
const nextPath = `${currentPath}.${relName}`
|
|
552
|
+
await deps.loadNestedIncludes(
|
|
553
|
+
{ records: targetRecords, scopeName: targetType, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
554
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Set null for records without relationships
|
|
560
|
+
records.forEach(record => {
|
|
561
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
562
|
+
if (!Object.hasOwn(record[RELATIONSHIPS_KEY], relName)) {
|
|
563
|
+
const relationshipObject = { data: null }
|
|
564
|
+
|
|
565
|
+
// Add links if urlPrefix is configured
|
|
566
|
+
if (scopeName) {
|
|
567
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, relName)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
record[RELATIONSHIPS_KEY][relName] = relationshipObject
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
} catch (error) {
|
|
574
|
+
throw wrapUnexpectedError(error, {
|
|
575
|
+
message: `Failed to load polymorphic belongsTo relationship '${relName}'`,
|
|
576
|
+
context: { phase: 'include', relName, types: relDef?.belongsToPolymorphic?.types }
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
}
|