json-rest-api 1.0.10 → 1.0.13
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/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +7 -11
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
|
@@ -81,6 +81,28 @@ import { buildWindowedIncludeQuery, applyStandardIncludeConfig, buildOrderByClau
|
|
|
81
81
|
import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY, COMPUTED_DEPENDENCIES_KEY, DEFAULT_QUERY_LIMIT } from '../querying-writing/knex-constants.js';
|
|
82
82
|
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js';
|
|
83
83
|
|
|
84
|
+
const translateSelectFieldsForAdapter = (fields, adapter) => {
|
|
85
|
+
if (!adapter || !fields) return fields;
|
|
86
|
+
|
|
87
|
+
const translateField = (field) => {
|
|
88
|
+
if (typeof field !== 'string') return field;
|
|
89
|
+
|
|
90
|
+
if (field === '*') return '*';
|
|
91
|
+
|
|
92
|
+
const aliasMatch = field.match(/\s+as\s+/i);
|
|
93
|
+
if (aliasMatch) {
|
|
94
|
+
const [source, alias] = field.split(/\s+as\s+/i);
|
|
95
|
+
const translatedSource = adapter.translateColumn(source.trim()) || source.trim();
|
|
96
|
+
return `${translatedSource} as ${alias.trim()}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const translated = adapter.translateColumn(field);
|
|
100
|
+
return translated || field;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return fields.map(translateField);
|
|
104
|
+
};
|
|
105
|
+
|
|
84
106
|
/**
|
|
85
107
|
* Groups records by their polymorphic type for efficient batch loading
|
|
86
108
|
*
|
|
@@ -263,7 +285,7 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
263
285
|
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo;
|
|
264
286
|
if (!schemaInfo) return;
|
|
265
287
|
|
|
266
|
-
const schema = schemaInfo.
|
|
288
|
+
const schema = schemaInfo.schemaInstance.structure || schemaInfo.schemaInstance;
|
|
267
289
|
const relationships = schemaInfo.schemaRelationships || {};
|
|
268
290
|
|
|
269
291
|
// Process each record
|
|
@@ -287,11 +309,14 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
287
309
|
}
|
|
288
310
|
}
|
|
289
311
|
|
|
290
|
-
// Process hasMany relationships
|
|
312
|
+
// Process hasMany, hasOne and manyToMany relationships
|
|
291
313
|
for (const [relName, relDef] of Object.entries(relationships)) {
|
|
292
|
-
if (relDef.hasMany) {
|
|
314
|
+
if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
293
315
|
// Add empty relationship data - will be populated if explicitly included
|
|
294
316
|
record[RELATIONSHIP_METADATA_KEY][relName] = { data: [] };
|
|
317
|
+
} else if (relDef.type === 'hasOne') {
|
|
318
|
+
// hasOne expects a single object or null
|
|
319
|
+
record[RELATIONSHIP_METADATA_KEY][relName] = { data: null };
|
|
295
320
|
} else if (relDef.belongsToPolymorphic) {
|
|
296
321
|
// Process polymorphic relationships defined in relationships section
|
|
297
322
|
const { typeField, idField } = relDef.belongsToPolymorphic;
|
|
@@ -379,7 +404,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
379
404
|
const relationshipObject = { data: null };
|
|
380
405
|
|
|
381
406
|
// Add links if urlPrefix is configured
|
|
382
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
407
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
383
408
|
if (scopeName) {
|
|
384
409
|
relationshipObject.links = {
|
|
385
410
|
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
@@ -395,16 +420,18 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
395
420
|
// Get target table info
|
|
396
421
|
const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
|
|
397
422
|
const targetIdProperty = scopes[targetScope].vars.schemaInfo.idProperty
|
|
398
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.
|
|
423
|
+
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance;
|
|
399
424
|
|
|
400
425
|
// Build field selection for sparse fieldsets
|
|
401
426
|
const targetScopeObject = scopes[targetScope];
|
|
427
|
+
const storageAdapter = targetScopeObject.vars.storageAdapter;
|
|
402
428
|
const fieldSelectionInfo = fields?.[targetScope] ?
|
|
403
429
|
await buildFieldSelection(targetScopeObject, {
|
|
404
430
|
context: {
|
|
405
431
|
scopeName: targetScope,
|
|
406
432
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
407
|
-
schemaInfo: targetScopeObject.vars.schemaInfo
|
|
433
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
434
|
+
storageAdapter,
|
|
408
435
|
}
|
|
409
436
|
}) :
|
|
410
437
|
null;
|
|
@@ -412,7 +439,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
412
439
|
// Load the target records
|
|
413
440
|
let query = knex(targetTableName).whereIn(targetIdProperty, uniqueIds);
|
|
414
441
|
if (fieldSelectionInfo) {
|
|
415
|
-
|
|
442
|
+
const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter);
|
|
443
|
+
query = query.select(selectFields);
|
|
416
444
|
} else if (targetIdProperty !== 'id') {
|
|
417
445
|
// If no field selection but custom idProperty, we need to alias it
|
|
418
446
|
query = query.select('*', `${targetIdProperty} as id`);
|
|
@@ -460,7 +488,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
460
488
|
const relationshipObject = { data: { type: targetScope, id: String(targetId) } };
|
|
461
489
|
|
|
462
490
|
// Add links if urlPrefix is configured
|
|
463
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
491
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
464
492
|
if (scopeName) {
|
|
465
493
|
relationshipObject.links = {
|
|
466
494
|
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
@@ -484,7 +512,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
484
512
|
const relationshipObject = { data: null };
|
|
485
513
|
|
|
486
514
|
// Add links if urlPrefix is configured
|
|
487
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
515
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
488
516
|
if (scopeName) {
|
|
489
517
|
relationshipObject.links = {
|
|
490
518
|
self: `${urlPrefix}/${scopeName}/${record[idProperty]}/relationships/${includeName}`,
|
|
@@ -554,11 +582,13 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
554
582
|
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope;
|
|
555
583
|
const { scopes, log, knex, capabilities } = deps.context;
|
|
556
584
|
try {
|
|
557
|
-
log.trace('[INCLUDE] Loading hasMany relationship:', {
|
|
585
|
+
log.trace('[INCLUDE] Loading hasMany/manyToMany relationship:', {
|
|
558
586
|
scopeName,
|
|
559
587
|
includeName,
|
|
560
588
|
recordCount: records.length,
|
|
561
|
-
|
|
589
|
+
relDef,
|
|
590
|
+
isManyToMany: relDef.type === 'manyToMany',
|
|
591
|
+
hasMany: relDef.type === 'hasMany' ? relDef.target : undefined
|
|
562
592
|
});
|
|
563
593
|
|
|
564
594
|
// Collect all parent IDs
|
|
@@ -570,18 +600,20 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
570
600
|
}
|
|
571
601
|
|
|
572
602
|
|
|
573
|
-
// Check if this is a many-to-many relationship
|
|
574
|
-
if (relDef.
|
|
603
|
+
// Check if this is a many-to-many relationship
|
|
604
|
+
if (relDef.type === 'manyToMany') {
|
|
575
605
|
|
|
576
606
|
// Handle many-to-many relationship
|
|
577
|
-
const pivotTable = scopes[relDef.through].vars.schemaInfo.tableName
|
|
607
|
+
const pivotTable = scopes[relDef.through].vars.schemaInfo.tableName;
|
|
578
608
|
const foreignKey = relDef.foreignKey;
|
|
579
609
|
const otherKey = relDef.otherKey;
|
|
580
610
|
|
|
581
611
|
if (!foreignKey || !otherKey) {
|
|
582
612
|
throw new Error(`Missing foreignKey or otherKey in many-to-many relationship '${relName}' for scope '${scopeName}'`);
|
|
583
613
|
}
|
|
584
|
-
|
|
614
|
+
|
|
615
|
+
// For manyToMany, the target scope is the relationship name itself
|
|
616
|
+
const targetScope = includeName;
|
|
585
617
|
const targetTable = scopes[targetScope].vars.schemaInfo.tableName
|
|
586
618
|
|
|
587
619
|
log.debug(`[INCLUDE] Loading pivot records from ${pivotTable}:`, {
|
|
@@ -602,7 +634,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
602
634
|
const relationshipObject = { data: [] };
|
|
603
635
|
|
|
604
636
|
// Add links if urlPrefix is configured
|
|
605
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
637
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
606
638
|
if (scopeName) {
|
|
607
639
|
relationshipObject.links = {
|
|
608
640
|
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
@@ -624,15 +656,17 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
624
656
|
});
|
|
625
657
|
|
|
626
658
|
// Step 3: Build field selection for sparse fieldsets
|
|
627
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.
|
|
659
|
+
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance;
|
|
628
660
|
const targetScopeObject = scopes[targetScope];
|
|
661
|
+
const storageAdapter = targetScopeObject.vars.storageAdapter;
|
|
629
662
|
const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty;
|
|
630
663
|
const fieldSelectionInfo = fields?.[targetScope] ?
|
|
631
664
|
await buildFieldSelection(targetScopeObject, {
|
|
632
665
|
context: {
|
|
633
666
|
scopeName: targetScope,
|
|
634
667
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
635
|
-
schemaInfo: targetScopeObject.vars.schemaInfo
|
|
668
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
669
|
+
storageAdapter,
|
|
636
670
|
}
|
|
637
671
|
}) :
|
|
638
672
|
null;
|
|
@@ -698,7 +732,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
698
732
|
let query = knex(targetTable).whereIn(targetIdProperty, targetIds);
|
|
699
733
|
|
|
700
734
|
if (fieldSelectionInfo) {
|
|
701
|
-
|
|
735
|
+
const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter);
|
|
736
|
+
query = query.select(selectFields);
|
|
702
737
|
} else if (targetIdProperty !== 'id') {
|
|
703
738
|
// If no field selection but custom idProperty, we need to alias it
|
|
704
739
|
query = query.select('*', `${targetIdProperty} as id`);
|
|
@@ -775,7 +810,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
775
810
|
const relationshipObject = { data: relData };
|
|
776
811
|
|
|
777
812
|
// Add links if urlPrefix is configured
|
|
778
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
813
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
779
814
|
if (scopeName) {
|
|
780
815
|
relationshipObject.links = {
|
|
781
816
|
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
@@ -807,9 +842,9 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
807
842
|
}
|
|
808
843
|
}
|
|
809
844
|
|
|
810
|
-
} else {
|
|
811
|
-
// Handle
|
|
812
|
-
const targetScope = relDef.
|
|
845
|
+
} else if (relDef.type === 'hasMany') {
|
|
846
|
+
// Handle one-to-many relationship
|
|
847
|
+
const targetScope = relDef.target;
|
|
813
848
|
const targetTable = scopes[targetScope].vars.schemaInfo.tableName;
|
|
814
849
|
const foreignKey = relDef.foreignKey;
|
|
815
850
|
|
|
@@ -823,15 +858,17 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
823
858
|
});
|
|
824
859
|
|
|
825
860
|
// Build field selection for sparse fieldsets
|
|
826
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.
|
|
861
|
+
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance;
|
|
827
862
|
const targetScopeObject = scopes[targetScope];
|
|
863
|
+
const storageAdapter = targetScopeObject.vars.storageAdapter;
|
|
828
864
|
const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty;
|
|
829
865
|
const fieldSelectionInfo = fields?.[targetScope] ?
|
|
830
866
|
await buildFieldSelection(targetScopeObject, {
|
|
831
867
|
context: {
|
|
832
868
|
scopeName: targetScope,
|
|
833
869
|
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
834
|
-
schemaInfo: targetScopeObject.vars.schemaInfo
|
|
870
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
871
|
+
storageAdapter,
|
|
835
872
|
}
|
|
836
873
|
}) :
|
|
837
874
|
null;
|
|
@@ -843,12 +880,16 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
843
880
|
if (relDef.include?.strategy === 'window') {
|
|
844
881
|
try {
|
|
845
882
|
// Try to build window function query
|
|
883
|
+
const selectFields = fieldSelectionInfo
|
|
884
|
+
? translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter)
|
|
885
|
+
: null;
|
|
886
|
+
|
|
846
887
|
query = buildWindowedIncludeQuery(
|
|
847
888
|
knex,
|
|
848
889
|
targetTable,
|
|
849
890
|
foreignKey,
|
|
850
891
|
mainIds,
|
|
851
|
-
|
|
892
|
+
selectFields,
|
|
852
893
|
relDef.include || {},
|
|
853
894
|
capabilities,
|
|
854
895
|
targetScopeObject.vars // Pass target scope vars
|
|
@@ -874,7 +915,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
874
915
|
query = knex(targetTable).whereIn(foreignKey, mainIds);
|
|
875
916
|
|
|
876
917
|
if (fieldSelectionInfo) {
|
|
877
|
-
|
|
918
|
+
const selectFieldsStd = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter);
|
|
919
|
+
query = query.select(selectFieldsStd);
|
|
878
920
|
}
|
|
879
921
|
|
|
880
922
|
// Apply standard config with defaults
|
|
@@ -948,7 +990,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
948
990
|
const relationshipObject = { data: relData };
|
|
949
991
|
|
|
950
992
|
// Add links if urlPrefix is configured
|
|
951
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
993
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
952
994
|
if (scopeName) {
|
|
953
995
|
relationshipObject.links = {
|
|
954
996
|
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
@@ -1001,6 +1043,134 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
1001
1043
|
}
|
|
1002
1044
|
};
|
|
1003
1045
|
|
|
1046
|
+
/**
|
|
1047
|
+
* Loads hasOne relationships (one-to-one)
|
|
1048
|
+
*
|
|
1049
|
+
* Similar to hasMany but expects a single related record.
|
|
1050
|
+
* For example, a user that has one profile.
|
|
1051
|
+
*
|
|
1052
|
+
* @param {Object} scope - The hooked-api scope object
|
|
1053
|
+
* @param {Object} deps - Dependencies object
|
|
1054
|
+
*/
|
|
1055
|
+
export const loadHasOne = async (scope, deps) => {
|
|
1056
|
+
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope;
|
|
1057
|
+
const { context: { scopes, log, knex, capabilities } } = deps;
|
|
1058
|
+
|
|
1059
|
+
log = log?.child ? log.child({ method: 'loadHasOne' }) : console;
|
|
1060
|
+
|
|
1061
|
+
if (!records || records.length === 0) {
|
|
1062
|
+
log.trace('[INCLUDE] No records to load hasOne for');
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Get parent IDs
|
|
1067
|
+
const idProperty = scopes[scopeName]?.vars?.schemaInfo?.idProperty || 'id';
|
|
1068
|
+
const mainIds = [...new Set(records.map(r => r[idProperty]).filter(Boolean))];
|
|
1069
|
+
|
|
1070
|
+
if (mainIds.length === 0) {
|
|
1071
|
+
log.trace('[INCLUDE] No parent IDs found, skipping hasOne load');
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const targetScope = relDef.target;
|
|
1076
|
+
const targetTable = scopes[targetScope].vars.schemaInfo.tableName;
|
|
1077
|
+
const foreignKey = relDef.foreignKey;
|
|
1078
|
+
|
|
1079
|
+
if (!foreignKey) {
|
|
1080
|
+
throw new Error(`Missing foreignKey in hasOne relationship '${includeName}' for scope '${scopeName}'`);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
log.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
|
|
1084
|
+
whereIn: mainIds
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// Build query for hasOne - expects single result per parent
|
|
1088
|
+
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance;
|
|
1089
|
+
const targetScopeObject = scopes[targetScope];
|
|
1090
|
+
const storageAdapter = targetScopeObject.vars.storageAdapter;
|
|
1091
|
+
const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty;
|
|
1092
|
+
|
|
1093
|
+
// Build field selection for sparse fieldsets
|
|
1094
|
+
const fieldSelectionInfo = fields?.[targetScope] ?
|
|
1095
|
+
await buildFieldSelection(targetScopeObject, {
|
|
1096
|
+
context: {
|
|
1097
|
+
scopeName: targetScope,
|
|
1098
|
+
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
1099
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1100
|
+
storageAdapter,
|
|
1101
|
+
}
|
|
1102
|
+
}) :
|
|
1103
|
+
null;
|
|
1104
|
+
|
|
1105
|
+
// Query related records
|
|
1106
|
+
let query = knex(targetTable).whereIn(foreignKey, mainIds);
|
|
1107
|
+
|
|
1108
|
+
if (fieldSelectionInfo?.fieldsToSelect) {
|
|
1109
|
+
const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter);
|
|
1110
|
+
query = query.select(selectFields);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const relatedRecords = await query;
|
|
1114
|
+
|
|
1115
|
+
// Create a map of parent ID to related record (should be one-to-one)
|
|
1116
|
+
const relatedByParentId = {};
|
|
1117
|
+
for (const related of relatedRecords) {
|
|
1118
|
+
const parentId = related[foreignKey];
|
|
1119
|
+
if (relatedByParentId[parentId]) {
|
|
1120
|
+
log.warn(`[INCLUDE] Multiple records found for hasOne relationship '${includeName}' with ${foreignKey}=${parentId}`);
|
|
1121
|
+
}
|
|
1122
|
+
relatedByParentId[parentId] = related;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Process each parent record
|
|
1126
|
+
for (const record of records) {
|
|
1127
|
+
const recordId = record[idProperty];
|
|
1128
|
+
const relatedRecord = relatedByParentId[recordId];
|
|
1129
|
+
|
|
1130
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {};
|
|
1131
|
+
|
|
1132
|
+
if (relatedRecord) {
|
|
1133
|
+
// Transform to JSON:API format
|
|
1134
|
+
const transformed = await toJsonApiRecord(
|
|
1135
|
+
targetScopeObject,
|
|
1136
|
+
{
|
|
1137
|
+
context: {
|
|
1138
|
+
record: relatedRecord,
|
|
1139
|
+
scopeName: targetScope,
|
|
1140
|
+
schemaInfo: targetScopeObject.vars.schemaInfo
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
// Set single object (not array) for hasOne
|
|
1146
|
+
record[RELATIONSHIPS_KEY][includeName] = {
|
|
1147
|
+
data: { type: targetScope, id: String(relatedRecord[targetIdProperty]) }
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
// Add to included if not already there
|
|
1151
|
+
const key = `${targetScope}:${relatedRecord[targetIdProperty]}`;
|
|
1152
|
+
if (!included.has(key)) {
|
|
1153
|
+
included.set(key, transformed);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Process nested includes if any
|
|
1157
|
+
if (subIncludes && Object.keys(subIncludes).length > 0) {
|
|
1158
|
+
const newPath = `${currentPath}.${includeName}`;
|
|
1159
|
+
if (!processedPaths.has(newPath)) {
|
|
1160
|
+
processedPaths.add(newPath);
|
|
1161
|
+
await processIncludes(
|
|
1162
|
+
{ records: [relatedRecord], scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: newPath, fields },
|
|
1163
|
+
{ context: { scopes, log, knex, capabilities } }
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
} else {
|
|
1168
|
+
// No related record found
|
|
1169
|
+
record[RELATIONSHIPS_KEY][includeName] = { data: null };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1004
1174
|
/**
|
|
1005
1175
|
* Loads polymorphic belongsTo relationships
|
|
1006
1176
|
*
|
|
@@ -1062,12 +1232,14 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1062
1232
|
// Build field selection for sparse fieldsets
|
|
1063
1233
|
const targetScopeObject = scopes[targetType];
|
|
1064
1234
|
// const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty;
|
|
1235
|
+
const storageAdapter = targetScopeObject.vars.storageAdapter;
|
|
1065
1236
|
const fieldSelectionInfo = fields?.[targetType] ?
|
|
1066
1237
|
await buildFieldSelection(targetScopeObject, {
|
|
1067
1238
|
context: {
|
|
1068
1239
|
scopeName: targetType,
|
|
1069
1240
|
queryParams: { fields: { [targetType]: fields[targetType] } },
|
|
1070
|
-
schemaInfo: targetScopeObject.vars.schemaInfo
|
|
1241
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1242
|
+
storageAdapter,
|
|
1071
1243
|
}
|
|
1072
1244
|
}) :
|
|
1073
1245
|
null;
|
|
@@ -1081,7 +1253,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1081
1253
|
const targetIdProperty = scopes[targetType]?.vars?.schemaInfo?.idProperty || 'id';
|
|
1082
1254
|
let query = knex(targetTable).whereIn(targetIdProperty, targetIds);
|
|
1083
1255
|
if (fieldSelectionInfo) {
|
|
1084
|
-
|
|
1256
|
+
const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter);
|
|
1257
|
+
query = query.select(selectFields);
|
|
1085
1258
|
} else if (targetIdProperty !== 'id') {
|
|
1086
1259
|
// If no field selection but custom idProperty, we need to alias it
|
|
1087
1260
|
query = query.select('*', `${targetIdProperty} as id`);
|
|
@@ -1135,7 +1308,7 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1135
1308
|
};
|
|
1136
1309
|
|
|
1137
1310
|
// Add links if urlPrefix is configured
|
|
1138
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
1311
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
1139
1312
|
if (scopeName) {
|
|
1140
1313
|
relationshipObject.links = {
|
|
1141
1314
|
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
@@ -1148,7 +1321,7 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1148
1321
|
const relationshipObject = { data: null };
|
|
1149
1322
|
|
|
1150
1323
|
// Add links if urlPrefix is configured
|
|
1151
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
1324
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
1152
1325
|
if (scopeName) {
|
|
1153
1326
|
relationshipObject.links = {
|
|
1154
1327
|
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
@@ -1180,7 +1353,7 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1180
1353
|
const relationshipObject = { data: null };
|
|
1181
1354
|
|
|
1182
1355
|
// Add links if urlPrefix is configured
|
|
1183
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
1356
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
1184
1357
|
if (scopeName) {
|
|
1185
1358
|
relationshipObject.links = {
|
|
1186
1359
|
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${relName}`,
|
|
@@ -1244,7 +1417,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1244
1417
|
recordCount: records.length
|
|
1245
1418
|
});
|
|
1246
1419
|
|
|
1247
|
-
const targetScope = relDef.
|
|
1420
|
+
const targetScope = relDef.target;
|
|
1248
1421
|
const viaRelName = relDef.via;
|
|
1249
1422
|
|
|
1250
1423
|
// Get the polymorphic field info from target scope
|
|
@@ -1274,14 +1447,19 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1274
1447
|
});
|
|
1275
1448
|
|
|
1276
1449
|
// Build field selection for sparse fieldsets
|
|
1277
|
-
const targetSchema = scopes[targetScope].vars.schemaInfo.
|
|
1450
|
+
const targetSchema = scopes[targetScope].vars.schemaInfo.schemaInstance;
|
|
1278
1451
|
const targetScopeObject = scopes[targetScope];
|
|
1452
|
+
const storageAdapter = targetScopeObject.vars.storageAdapter;
|
|
1279
1453
|
const targetIdProperty = targetScopeObject.vars.schemaInfo.idProperty;
|
|
1280
1454
|
const fieldSelectionInfo = fields?.[targetScope] ?
|
|
1281
|
-
await buildFieldSelection(
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1455
|
+
await buildFieldSelection(targetScopeObject, {
|
|
1456
|
+
context: {
|
|
1457
|
+
scopeName: targetScope,
|
|
1458
|
+
queryParams: { fields: { [targetScope]: fields[targetScope] } },
|
|
1459
|
+
schemaInfo: targetScopeObject.vars.schemaInfo,
|
|
1460
|
+
storageAdapter,
|
|
1461
|
+
}
|
|
1462
|
+
}) :
|
|
1285
1463
|
null;
|
|
1286
1464
|
|
|
1287
1465
|
// Query for records pointing back to our scope
|
|
@@ -1290,7 +1468,8 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1290
1468
|
.whereIn(idField, parentIds);
|
|
1291
1469
|
|
|
1292
1470
|
if (fieldSelectionInfo) {
|
|
1293
|
-
|
|
1471
|
+
const selectFields = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, storageAdapter);
|
|
1472
|
+
query = query.select(selectFields);
|
|
1294
1473
|
} else if (targetIdProperty !== 'id') {
|
|
1295
1474
|
// If no field selection but custom idProperty, we need to alias it
|
|
1296
1475
|
query = query.select('*', `${targetIdProperty} as id`);
|
|
@@ -1359,7 +1538,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1359
1538
|
const relationshipObject = { data: relData };
|
|
1360
1539
|
|
|
1361
1540
|
// Add links if urlPrefix is configured
|
|
1362
|
-
const urlPrefix = scopes[scopeName]?.vars?.
|
|
1541
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || '';
|
|
1363
1542
|
if (scopeName) {
|
|
1364
1543
|
relationshipObject.links = {
|
|
1365
1544
|
self: `${urlPrefix}/${scopeName}/${record.id}/relationships/${includeName}`,
|
|
@@ -1386,7 +1565,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1386
1565
|
scopeName,
|
|
1387
1566
|
includeName,
|
|
1388
1567
|
via: relDef?.via,
|
|
1389
|
-
targetScope: relDef?.
|
|
1568
|
+
targetScope: relDef?.target,
|
|
1390
1569
|
recordCount: records?.length || 0,
|
|
1391
1570
|
error: error.message,
|
|
1392
1571
|
stack: error.stack
|
|
@@ -1397,7 +1576,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1397
1576
|
`Failed to load reverse polymorphic relationship '${includeName}' via '${relDef?.via}' for scope '${scopeName}': ${error.message}`
|
|
1398
1577
|
);
|
|
1399
1578
|
enhancedError.originalError = error;
|
|
1400
|
-
enhancedError.context = { scopeName, includeName, via: relDef?.via, targetScope: relDef?.
|
|
1579
|
+
enhancedError.context = { scopeName, includeName, via: relDef?.via, targetScope: relDef?.target };
|
|
1401
1580
|
throw enhancedError;
|
|
1402
1581
|
}
|
|
1403
1582
|
};
|
|
@@ -1443,7 +1622,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1443
1622
|
return;
|
|
1444
1623
|
}
|
|
1445
1624
|
|
|
1446
|
-
const {
|
|
1625
|
+
const { schemaInstance: schemaInstance, schemaRelationships } = schemaInfo;
|
|
1447
1626
|
|
|
1448
1627
|
// Process each include
|
|
1449
1628
|
for (const [includeName, subIncludes] of Object.entries(includeTree)) {
|
|
@@ -1461,7 +1640,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1461
1640
|
|
|
1462
1641
|
try {
|
|
1463
1642
|
// Look for belongsTo relationships in schema fields
|
|
1464
|
-
for (const [fieldName, fieldDef] of Object.entries(
|
|
1643
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaInstance.structure || {})) {
|
|
1465
1644
|
if (fieldDef.as === includeName && fieldDef.belongsTo) {
|
|
1466
1645
|
await loadBelongsTo(
|
|
1467
1646
|
{ records, scopeName, fieldName, fieldDef, includeName, subIncludes, included, processedPaths, currentPath, fields, idProperty },
|
|
@@ -1477,7 +1656,19 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1477
1656
|
const relDef = schemaRelationships[includeName];
|
|
1478
1657
|
|
|
1479
1658
|
if (relDef) {
|
|
1480
|
-
if (relDef.
|
|
1659
|
+
if (relDef.type === 'hasOne') {
|
|
1660
|
+
// Handle hasOne relationship
|
|
1661
|
+
log.debug('[INCLUDE] About to call loadHasOne:', {
|
|
1662
|
+
scopeName,
|
|
1663
|
+
includeName,
|
|
1664
|
+
target: relDef.target
|
|
1665
|
+
});
|
|
1666
|
+
await loadHasOne(
|
|
1667
|
+
{ records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1668
|
+
{ context: { scopes, log, knex, capabilities } }
|
|
1669
|
+
);
|
|
1670
|
+
handled = true;
|
|
1671
|
+
} else if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
1481
1672
|
// Check if it's a reverse polymorphic (via)
|
|
1482
1673
|
if (relDef.via) {
|
|
1483
1674
|
await loadReversePolymorphic(
|
|
@@ -1485,6 +1676,13 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1485
1676
|
{ context: { scopes, log, knex, capabilities } }
|
|
1486
1677
|
);
|
|
1487
1678
|
} else {
|
|
1679
|
+
log.debug('[INCLUDE] About to call loadHasMany:', {
|
|
1680
|
+
scopeName,
|
|
1681
|
+
includeName,
|
|
1682
|
+
relDefKeys: Object.keys(relDef || {}),
|
|
1683
|
+
hasManyToMany: relDef.type === 'manyToMany',
|
|
1684
|
+
hasMany: relDef.type === 'hasMany' ? relDef.target : undefined
|
|
1685
|
+
});
|
|
1488
1686
|
await loadHasMany(
|
|
1489
1687
|
{ records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields },
|
|
1490
1688
|
{ context: { scopes, log, knex, capabilities } }
|
|
@@ -1516,7 +1714,7 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1516
1714
|
log.warn('[INCLUDE] Unknown relationship:', {
|
|
1517
1715
|
scopeName,
|
|
1518
1716
|
includeName,
|
|
1519
|
-
availableFields: Object.keys(
|
|
1717
|
+
availableFields: Object.keys(schemaInstance.structure || {}).filter(k => schemaInstance.structure[k].as),
|
|
1520
1718
|
availableRelationships: Object.keys(schemaRelationships || {})
|
|
1521
1719
|
});
|
|
1522
1720
|
}
|
|
@@ -1659,9 +1857,9 @@ export const buildIncludedResources = async (scope, deps) => {
|
|
|
1659
1857
|
* without including the full related resources. This provides a consistent API surface where
|
|
1660
1858
|
* clients always know what relationships exist and their IDs.
|
|
1661
1859
|
*
|
|
1662
|
-
* 2. **Simplified Mode Support**: In simplified mode, relationship IDs are transformed into
|
|
1663
|
-
*
|
|
1664
|
-
* appear when using ?include, creating an inconsistent API where fields appear/disappear.
|
|
1860
|
+
* 2. **Simplified Mode Support**: In simplified mode, relationship IDs are transformed into
|
|
1861
|
+
* minimal objects (e.g., `reviews: [{id: '1'}, {id: '2'}, {id: '3'}]`). Without this function,
|
|
1862
|
+
* these objects only appear when using ?include, creating an inconsistent API where fields appear/disappear.
|
|
1665
1863
|
*
|
|
1666
1864
|
* 3. **Performance Balance**: Loading just IDs is much cheaper than loading full records. This gives
|
|
1667
1865
|
* clients the ability to know what relationships exist without the cost of fetching all data.
|
|
@@ -1696,7 +1894,7 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1696
1894
|
for (const [relName, relDef] of Object.entries(relationships)) {
|
|
1697
1895
|
let idsMap = {};
|
|
1698
1896
|
|
|
1699
|
-
if (relDef.
|
|
1897
|
+
if (relDef.type === 'hasMany' && !relDef.via) {
|
|
1700
1898
|
// Regular one-to-many
|
|
1701
1899
|
// Example: publisher hasMany authors
|
|
1702
1900
|
const foreignKey = relDef.foreignKey;
|
|
@@ -1704,8 +1902,8 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1704
1902
|
if (!foreignKey) {
|
|
1705
1903
|
throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${scopeName}'`);
|
|
1706
1904
|
}
|
|
1707
|
-
const targetScope = scopes[relDef.
|
|
1708
|
-
const targetTable = targetScope?.vars?.schemaInfo?.tableName || relDef.
|
|
1905
|
+
const targetScope = scopes[relDef.target];
|
|
1906
|
+
const targetTable = targetScope?.vars?.schemaInfo?.tableName || relDef.target;
|
|
1709
1907
|
const targetIdProperty = targetScope?.vars?.schemaInfo?.idProperty || 'id';
|
|
1710
1908
|
|
|
1711
1909
|
const results = await knex(targetTable)
|
|
@@ -1718,35 +1916,10 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1718
1916
|
idsMap[parentId].push(String(row.id));
|
|
1719
1917
|
});
|
|
1720
1918
|
|
|
1721
|
-
} else if (relDef.
|
|
1722
|
-
// Many-to-many
|
|
1723
|
-
// Example:
|
|
1724
|
-
const foreignKey = relDef
|
|
1725
|
-
|
|
1726
|
-
if (!foreignKey) {
|
|
1727
|
-
throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${scopeName}'`);
|
|
1728
|
-
}
|
|
1729
|
-
const otherKey = relDef.otherKey || `${relDef.hasMany.slice(0, -1)}_id`;
|
|
1730
|
-
|
|
1731
|
-
// Get the actual table name from the pivot scope
|
|
1732
|
-
const pivotScope = scopes[relDef.through];
|
|
1733
|
-
const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || relDef.through;
|
|
1734
|
-
|
|
1735
|
-
const results = await knex(pivotTable)
|
|
1736
|
-
.whereIn(foreignKey, recordIds)
|
|
1737
|
-
.select(foreignKey, otherKey);
|
|
1738
|
-
|
|
1739
|
-
results.forEach(row => {
|
|
1740
|
-
const parentId = String(row[foreignKey]);
|
|
1741
|
-
const childId = String(row[otherKey]);
|
|
1742
|
-
if (!idsMap[parentId]) idsMap[parentId] = [];
|
|
1743
|
-
idsMap[parentId].push(childId);
|
|
1744
|
-
});
|
|
1745
|
-
|
|
1746
|
-
} else if (relDef.manyToMany) {
|
|
1747
|
-
// Many-to-many (alternate syntax)
|
|
1748
|
-
// Example: { manyToMany: { through: 'article_tags', foreignKey: 'article_id', otherKey: 'tag_id' } }
|
|
1749
|
-
const { through, foreignKey, otherKey } = relDef.manyToMany;
|
|
1919
|
+
} else if (relDef.type === 'manyToMany') {
|
|
1920
|
+
// Many-to-many relationship
|
|
1921
|
+
// Example: { type: 'manyToMany', through: 'article_tags', foreignKey: 'article_id', otherKey: 'tag_id' }
|
|
1922
|
+
const { through, foreignKey, otherKey } = relDef;
|
|
1750
1923
|
const fk = foreignKey;
|
|
1751
1924
|
const ok = otherKey;
|
|
1752
1925
|
|
|
@@ -1769,10 +1942,10 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1769
1942
|
idsMap[parentId].push(childId);
|
|
1770
1943
|
});
|
|
1771
1944
|
|
|
1772
|
-
} else if (relDef.hasMany && relDef.via) {
|
|
1945
|
+
} else if (relDef.type === 'hasMany' && relDef.via) {
|
|
1773
1946
|
// Polymorphic reverse (via)
|
|
1774
1947
|
// Example: publishers hasMany reviews via reviewable (where reviews.reviewable_type = 'publishers')
|
|
1775
|
-
const targetScope = relDef.
|
|
1948
|
+
const targetScope = relDef.target;
|
|
1776
1949
|
const targetRelationships = scopes[targetScope]?.vars?.schemaInfo?.schemaRelationships;
|
|
1777
1950
|
const viaRel = targetRelationships?.[relDef.via];
|
|
1778
1951
|
|
|
@@ -1797,14 +1970,16 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1797
1970
|
}
|
|
1798
1971
|
|
|
1799
1972
|
// Apply the collected IDs to all records
|
|
1800
|
-
if (Object.keys(idsMap).length > 0 || relDef.hasMany || relDef.manyToMany) {
|
|
1973
|
+
if (Object.keys(idsMap).length > 0 || relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
1801
1974
|
records.forEach(record => {
|
|
1802
1975
|
if (!record[RELATIONSHIPS_KEY]) {
|
|
1803
1976
|
record[RELATIONSHIPS_KEY] = {};
|
|
1804
1977
|
}
|
|
1805
1978
|
|
|
1806
1979
|
const ids = idsMap[String(record.id)] || [];
|
|
1807
|
-
|
|
1980
|
+
// For manyToMany, the target type is the relationship name itself
|
|
1981
|
+
// For hasMany/hasOne, the target type is specified in the target property
|
|
1982
|
+
const targetType = (relDef.type === 'manyToMany') ? relName : relDef.target;
|
|
1808
1983
|
|
|
1809
1984
|
record[RELATIONSHIPS_KEY][relName] = {
|
|
1810
1985
|
data: ids.map(id => ({
|
|
@@ -1815,4 +1990,4 @@ export const loadRelationshipIdentifiers = async (records, scopeName, scopes, kn
|
|
|
1815
1990
|
});
|
|
1816
1991
|
}
|
|
1817
1992
|
}
|
|
1818
|
-
};
|
|
1993
|
+
};
|