@simtlix/simfinity-js 2.5.0 → 2.6.3
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/package.json +1 -1
- package/src/index.js +630 -1065
- package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +0 -23
- package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +0 -235
- package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +0 -567
- package/.claude/worktrees/agitated-kepler/LICENSE +0 -201
- package/.claude/worktrees/agitated-kepler/README.md +0 -3941
- package/.claude/worktrees/agitated-kepler/eslint.config.mjs +0 -71
- package/.claude/worktrees/agitated-kepler/package-lock.json +0 -4740
- package/.claude/worktrees/agitated-kepler/package.json +0 -41
- package/.claude/worktrees/agitated-kepler/src/auth/errors.js +0 -44
- package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +0 -273
- package/.claude/worktrees/agitated-kepler/src/auth/index.js +0 -391
- package/.claude/worktrees/agitated-kepler/src/auth/rules.js +0 -274
- package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +0 -39
- package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +0 -28
- package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +0 -39
- package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +0 -11
- package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +0 -15
- package/.claude/worktrees/agitated-kepler/src/index.js +0 -2412
- package/.claude/worktrees/agitated-kepler/src/plugins.js +0 -53
- package/.claude/worktrees/agitated-kepler/src/scalars.js +0 -188
- package/.claude/worktrees/agitated-kepler/src/validators.js +0 -250
- package/.claude/worktrees/agitated-kepler/yarn.lock +0 -1154
- package/.cursor/rules/simfinity-architecture.mdc +0 -71
- package/.cursor/rules/simfinity-auth-module.mdc +0 -100
- package/.cursor/rules/simfinity-coding-standards.mdc +0 -62
- package/.cursor/rules/simfinity-core-functions.mdc +0 -84
- package/.cursor/rules/simfinity-documentation.mdc +0 -59
- package/.cursor/rules/simfinity-extensions.mdc +0 -121
- package/.cursor/rules/simfinity-testing.mdc +0 -90
- package/eslint.config.mjs +0 -71
- package/git-report.js +0 -224
package/src/index.js
CHANGED
|
@@ -13,7 +13,6 @@ import QLSort from './const/QLSort.js';
|
|
|
13
13
|
|
|
14
14
|
mongoose.set('strictQuery', false);
|
|
15
15
|
|
|
16
|
-
// Custom JSON scalar type for aggregation results
|
|
17
16
|
const GraphQLJSON = new GraphQLScalarType({
|
|
18
17
|
name: 'JSON',
|
|
19
18
|
description: 'The `JSON` scalar type represents JSON values as specified by ECMA-404',
|
|
@@ -29,6 +28,7 @@ const GraphQLJSON = new GraphQLScalarType({
|
|
|
29
28
|
case Kind.BOOLEAN:
|
|
30
29
|
return ast.value;
|
|
31
30
|
case Kind.INT:
|
|
31
|
+
return parseInt(ast.value, 10);
|
|
32
32
|
case Kind.FLOAT:
|
|
33
33
|
return parseFloat(ast.value);
|
|
34
34
|
case Kind.OBJECT: {
|
|
@@ -48,7 +48,6 @@ const GraphQLJSON = new GraphQLScalarType({
|
|
|
48
48
|
},
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
// Adding 'extensions' field into instronspection query
|
|
52
51
|
const RelationType = new GraphQLObjectType({
|
|
53
52
|
name: 'RelationType',
|
|
54
53
|
fields: () => ({
|
|
@@ -82,7 +81,6 @@ const fixedFieldsWithExtensions = () => {
|
|
|
82
81
|
};
|
|
83
82
|
|
|
84
83
|
__Field._fields = fixedFieldsWithExtensions;
|
|
85
|
-
// End of adding 'extensions' field to instrospection query
|
|
86
84
|
|
|
87
85
|
const typesDict = { types: {} };
|
|
88
86
|
const waitingInputType = {};
|
|
@@ -133,9 +131,6 @@ export const preventCreatingCollection = (prevent) => {
|
|
|
133
131
|
preventCollectionCreation = !!prevent;
|
|
134
132
|
};
|
|
135
133
|
|
|
136
|
-
/* Schema defines data on the Graph like object types(book type), relation between
|
|
137
|
-
these object types and describes how it can reach into the graph to interact with
|
|
138
|
-
the data to retrieve or mutate the data */
|
|
139
134
|
const QLFilter = new GraphQLInputObjectType({
|
|
140
135
|
name: 'QLFilter',
|
|
141
136
|
fields: () => ({
|
|
@@ -263,17 +258,91 @@ const getEffectiveTypeName = (type) => {
|
|
|
263
258
|
|
|
264
259
|
const isGraphQLisoDate = (typeName) => typeName === 'DateTime' || typeName === 'Date' || typeName === 'Time';
|
|
265
260
|
|
|
261
|
+
const unwrapNonNull = (type) => (type instanceof GraphQLNonNull ? type.ofType : type);
|
|
262
|
+
|
|
263
|
+
const unwrapListAndNonNull = (type) => {
|
|
264
|
+
if (type instanceof GraphQLList || type instanceof GraphQLNonNull) {
|
|
265
|
+
return type.ofType;
|
|
266
|
+
}
|
|
267
|
+
return type;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const coerceDateValue = (fieldType, holder, key) => {
|
|
271
|
+
const typeName = getEffectiveTypeName(unwrapNonNull(fieldType));
|
|
272
|
+
if (!isGraphQLisoDate(typeName)) return;
|
|
273
|
+
const raw = holder[key];
|
|
274
|
+
if (Array.isArray(raw)) {
|
|
275
|
+
holder[key] = raw.map((v) => v && new Date(v));
|
|
276
|
+
} else {
|
|
277
|
+
holder[key] = raw && new Date(raw);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const isCustomValidatedScalar = (type) => type instanceof GraphQLScalarType && type.baseScalarType;
|
|
282
|
+
|
|
283
|
+
const matchesScalar = (fieldType, target) => {
|
|
284
|
+
if (fieldType === target) return true;
|
|
285
|
+
if (isNonNullOfTypeForNotScalar(fieldType, target)) return true;
|
|
286
|
+
if (isCustomValidatedScalar(fieldType) && fieldType.baseScalarType === target) return true;
|
|
287
|
+
if (isNonNullOfType(fieldType, GraphQLScalarType)
|
|
288
|
+
&& isCustomValidatedScalar(fieldType.ofType)
|
|
289
|
+
&& fieldType.ofType.baseScalarType === target) return true;
|
|
290
|
+
return false;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const buildRelationLookup = ({
|
|
294
|
+
collectionName, localField, foreignField, alias,
|
|
295
|
+
}) => ({
|
|
296
|
+
lookup: {
|
|
297
|
+
$lookup: {
|
|
298
|
+
from: collectionName, foreignField, localField, as: alias,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
unwind: { $unwind: { path: `$${alias}`, preserveNullAndEmptyArrays: true } },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const escapeRegex = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
305
|
+
|
|
306
|
+
const PATH_SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
307
|
+
|
|
308
|
+
const assertValidFilterPath = (path) => {
|
|
309
|
+
if (typeof path !== 'string' || path.length === 0) {
|
|
310
|
+
throw new SimfinityError('Filter path must be a non-empty string', 'INVALID_FILTER_PATH', 400);
|
|
311
|
+
}
|
|
312
|
+
for (const segment of path.split('.')) {
|
|
313
|
+
if (!PATH_SEGMENT_RE.test(segment)) {
|
|
314
|
+
throw new SimfinityError(`Invalid filter path segment: "${segment}"`, 'INVALID_FILTER_PATH', 400);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const OP_TO_MONGO = {
|
|
320
|
+
LT: (v) => ({ $lt: v }),
|
|
321
|
+
GT: (v) => ({ $gt: v }),
|
|
322
|
+
LTE: (v) => ({ $lte: v }),
|
|
323
|
+
GTE: (v) => ({ $gte: v }),
|
|
324
|
+
NE: (v) => ({ $ne: v }),
|
|
325
|
+
BTW: (v) => ({ $gte: v[0], $lte: v[1] }),
|
|
326
|
+
LIKE: (v) => ({ $regex: `.*${escapeRegex(v)}.*` }),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const AGG_OP_TO_MONGO = {
|
|
330
|
+
SUM: (path) => ({ $sum: `$${path}` }),
|
|
331
|
+
COUNT: () => ({ $sum: 1 }),
|
|
332
|
+
AVG: (path) => ({ $avg: `$${path}` }),
|
|
333
|
+
MIN: (path) => ({ $min: `$${path}` }),
|
|
334
|
+
MAX: (path) => ({ $max: `$${path}` }),
|
|
335
|
+
};
|
|
336
|
+
|
|
266
337
|
function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
267
338
|
if (!baseScalarType) {
|
|
268
339
|
throw new Error('baseScalarType is required');
|
|
269
340
|
}
|
|
270
341
|
|
|
271
|
-
// Validate that baseScalarType is a valid GraphQL scalar type
|
|
272
342
|
if (!(baseScalarType instanceof GraphQLScalarType)) {
|
|
273
343
|
throw new Error('baseScalarType must be a valid GraphQL scalar type');
|
|
274
344
|
}
|
|
275
345
|
|
|
276
|
-
// Check if it's one of the standard GraphQL scalar types
|
|
277
346
|
const validScalarTypes = [GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID];
|
|
278
347
|
const isValidStandardType = validScalarTypes.some((type) => baseScalarType === type);
|
|
279
348
|
|
|
@@ -286,10 +355,9 @@ function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
|
286
355
|
Int: Kind.INT,
|
|
287
356
|
Float: Kind.FLOAT,
|
|
288
357
|
Boolean: Kind.BOOLEAN,
|
|
289
|
-
ID: Kind.STRING,
|
|
358
|
+
ID: Kind.STRING,
|
|
290
359
|
};
|
|
291
360
|
|
|
292
|
-
// Try to infer the kind from the baseScalarType name
|
|
293
361
|
const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
|
|
294
362
|
|
|
295
363
|
const scalar = new GraphQLScalarType({
|
|
@@ -327,7 +395,13 @@ function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
|
327
395
|
const createTypeWithExcludedField = (inputNamePrefix, originalType, fieldToExclude) => {
|
|
328
396
|
const originalFields = originalType.getFields();
|
|
329
397
|
const newFields = Object.fromEntries(
|
|
330
|
-
Object.entries(originalFields)
|
|
398
|
+
Object.entries(originalFields)
|
|
399
|
+
.filter(([fieldName]) => fieldName !== fieldToExclude)
|
|
400
|
+
.map(([fieldName, field]) => [fieldName, {
|
|
401
|
+
type: field.type,
|
|
402
|
+
description: field.description,
|
|
403
|
+
defaultValue: field.defaultValue,
|
|
404
|
+
}]),
|
|
331
405
|
);
|
|
332
406
|
|
|
333
407
|
return new GraphQLInputObjectType({
|
|
@@ -340,11 +414,8 @@ const createOneToManyInputType = (inputNamePrefix, fieldEntryName,
|
|
|
340
414
|
inputType, updateInputType, connectionField) => {
|
|
341
415
|
let inputTypeForAdd = inputType;
|
|
342
416
|
|
|
343
|
-
// If a gqltype is provided, create a new input type for 'added'
|
|
344
|
-
// that excludes the field named after the gqltype.
|
|
345
417
|
if (connectionField) {
|
|
346
|
-
|
|
347
|
-
inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, fieldToExclude);
|
|
418
|
+
inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, connectionField);
|
|
348
419
|
}
|
|
349
420
|
|
|
350
421
|
return new GraphQLInputObjectType({
|
|
@@ -397,9 +468,9 @@ const buildInputType = (gqltype) => {
|
|
|
397
468
|
|
|
398
469
|
if (!fieldEntry.extensions || !fieldEntry.extensions.readOnly) {
|
|
399
470
|
const hasStateMachine = !!typesDict.types[gqltype.name].stateMachine;
|
|
400
|
-
const
|
|
471
|
+
const stateFieldManagedByStateMachine = !!(fieldEntryName === 'state' && hasStateMachine);
|
|
401
472
|
|
|
402
|
-
if (!
|
|
473
|
+
if (!stateFieldManagedByStateMachine) {
|
|
403
474
|
if (fieldEntry.type instanceof GraphQLScalarType
|
|
404
475
|
|| fieldEntry.type instanceof GraphQLEnumType
|
|
405
476
|
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
@@ -435,8 +506,8 @@ const buildInputType = (gqltype) => {
|
|
|
435
506
|
if (fieldEntry.type.ofType === gqltype) {
|
|
436
507
|
selfReferenceCollections[fieldEntryName] = fieldEntry;
|
|
437
508
|
} else {
|
|
438
|
-
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, gqltype.name
|
|
439
|
-
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, gqltype.name
|
|
509
|
+
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, `${gqltype.name}A`, fieldEntry.extensions?.relation?.connectionField);
|
|
510
|
+
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, `${gqltype.name}U`, fieldEntry.extensions?.relation?.connectionField);
|
|
440
511
|
if (listInputTypeForAdd && listInputTypeForUpdate) {
|
|
441
512
|
fieldArg.type = listInputTypeForAdd;
|
|
442
513
|
fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
@@ -508,29 +579,35 @@ const getInputType = (type) => typesDict.types[type.name].inputType;
|
|
|
508
579
|
export { getInputType };
|
|
509
580
|
|
|
510
581
|
const buildPendingInputTypes = (waitingForInputType) => {
|
|
511
|
-
|
|
512
|
-
let
|
|
513
|
-
|
|
514
|
-
Object.
|
|
515
|
-
const
|
|
582
|
+
let pending = waitingForInputType;
|
|
583
|
+
let previousPendingCount = Object.keys(pending).length + 1;
|
|
584
|
+
|
|
585
|
+
while (Object.keys(pending).length > 0) {
|
|
586
|
+
const currentCount = Object.keys(pending).length;
|
|
587
|
+
if (currentCount >= previousPendingCount) {
|
|
588
|
+
const unresolved = Object.keys(pending).join(', ');
|
|
589
|
+
throw new SimfinityError(
|
|
590
|
+
`Could not build input types for: ${unresolved}. Check for circular or misconfigured relations.`,
|
|
591
|
+
'INPUT_TYPE_UNRESOLVED',
|
|
592
|
+
500,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
previousPendingCount = currentCount;
|
|
516
596
|
|
|
517
|
-
|
|
518
|
-
|
|
597
|
+
const stillWaiting = {};
|
|
598
|
+
for (const [key, value] of Object.entries(pending)) {
|
|
599
|
+
const { gqltype } = value;
|
|
600
|
+
if (typesDict.types[gqltype.name].inputType) continue;
|
|
519
601
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
typesDict.types[gqltype.name].inputType =
|
|
523
|
-
typesDictForUpdate.types[gqltype.name].inputType =
|
|
524
|
-
.inputTypeBodyForUpdate;
|
|
602
|
+
const result = buildInputType(gqltype);
|
|
603
|
+
if (result && result.inputTypeBody && result.inputTypeBodyForUpdate) {
|
|
604
|
+
typesDict.types[gqltype.name].inputType = result.inputTypeBody;
|
|
605
|
+
typesDictForUpdate.types[gqltype.name].inputType = result.inputTypeBodyForUpdate;
|
|
525
606
|
} else {
|
|
526
|
-
|
|
527
|
-
isThereAtLeastOneWaiting = true;
|
|
607
|
+
stillWaiting[key] = value;
|
|
528
608
|
}
|
|
529
609
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (isThereAtLeastOneWaiting) {
|
|
533
|
-
buildPendingInputTypes(stillWaitingInputType);
|
|
610
|
+
pending = stillWaiting;
|
|
534
611
|
}
|
|
535
612
|
};
|
|
536
613
|
|
|
@@ -573,9 +650,11 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
573
650
|
null, operation, session)).modelArgs;
|
|
574
651
|
}
|
|
575
652
|
} else {
|
|
576
|
-
|
|
577
|
-
.
|
|
578
|
-
|
|
653
|
+
throw new SimfinityError(
|
|
654
|
+
`Field ${gqltype.name}.${fieldEntryName} is an object type but does not define extensions.relation`,
|
|
655
|
+
'MISSING_RELATION_EXTENSION',
|
|
656
|
+
500,
|
|
657
|
+
);
|
|
579
658
|
}
|
|
580
659
|
} else if (fieldEntry.type instanceof GraphQLList) {
|
|
581
660
|
const { ofType } = fieldEntry.type;
|
|
@@ -616,56 +695,58 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
616
695
|
return { modelArgs, collectionFields };
|
|
617
696
|
};
|
|
618
697
|
|
|
619
|
-
const
|
|
698
|
+
const MAX_TRANSIENT_RETRIES = 5;
|
|
699
|
+
|
|
700
|
+
const withTransaction = async (session, body) => {
|
|
701
|
+
const ownsSession = !session;
|
|
620
702
|
const mySession = session || await mongoose.startSession();
|
|
621
|
-
await mySession.startTransaction();
|
|
622
703
|
try {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
704
|
+
for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
|
|
705
|
+
await mySession.startTransaction();
|
|
706
|
+
try {
|
|
707
|
+
const result = await body(mySession);
|
|
708
|
+
await mySession.commitTransaction();
|
|
709
|
+
return result;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
await mySession.abortTransaction();
|
|
712
|
+
const isTransient = error.errorLabels?.includes('TransientTransactionError');
|
|
713
|
+
if (isTransient && attempt < MAX_TRANSIENT_RETRIES) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
throw new SimfinityError('Transaction exceeded retry limit', 'TRANSACTION_RETRY_EXCEEDED', 500);
|
|
720
|
+
} finally {
|
|
721
|
+
if (ownsSession) {
|
|
722
|
+
mySession.endSession();
|
|
631
723
|
}
|
|
632
|
-
mySession.endSession();
|
|
633
|
-
throw error;
|
|
634
724
|
}
|
|
635
725
|
};
|
|
636
726
|
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
727
|
+
const executeRegisteredMutation = (args, callback, context, session) => withTransaction(
|
|
728
|
+
session,
|
|
729
|
+
(mySession) => callback(args, mySession, context),
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
const iterateOnCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
|
|
733
|
+
for (const [collectionFieldKey, collectionField] of Object.entries(materializedModel.collectionFields)) {
|
|
640
734
|
if (collectionField.added) {
|
|
641
|
-
|
|
642
735
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
643
736
|
collectionField.added, operations.SAVE, context);
|
|
644
737
|
}
|
|
645
738
|
if (collectionField.updated) {
|
|
646
|
-
|
|
647
739
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
648
740
|
collectionField.updated, operations.UPDATE, context);
|
|
649
741
|
}
|
|
650
742
|
if (collectionField.deleted) {
|
|
651
|
-
|
|
652
743
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
653
744
|
collectionField.deleted, operations.DELETE, context);
|
|
654
745
|
}
|
|
655
746
|
}
|
|
656
747
|
};
|
|
657
748
|
|
|
658
|
-
const
|
|
659
|
-
const deletedObject = await Model.findById({ _id: args }).session(session).lean();
|
|
660
|
-
|
|
661
|
-
if (controller && controller.onDelete) {
|
|
662
|
-
await controller.onDelete(deletedObject, session, context);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
return Model.findByIdAndDelete({ _id: args }).session(session);
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
const onDeleteSubject = async (Model, controller, id, session, context) => {
|
|
749
|
+
const onDelete = async (Model, controller, id, session, context) => {
|
|
669
750
|
const currentObject = await Model.findById({ _id: id }).session(session).lean();
|
|
670
751
|
|
|
671
752
|
if (controller && controller.onDelete) {
|
|
@@ -675,36 +756,49 @@ const onDeleteSubject = async (Model, controller, id, session, context) => {
|
|
|
675
756
|
return Model.findByIdAndDelete({ _id: id }).session(session);
|
|
676
757
|
};
|
|
677
758
|
|
|
759
|
+
const getEmbeddedFieldNames = (gqltype) => {
|
|
760
|
+
const cached = typesDict.types[gqltype.name];
|
|
761
|
+
if (cached && cached.embeddedFieldNames) return cached.embeddedFieldNames;
|
|
762
|
+
const names = [];
|
|
763
|
+
for (const [fieldName, fieldEntry] of Object.entries(gqltype.getFields())) {
|
|
764
|
+
if (fieldEntry.extensions?.relation?.embedded) names.push(fieldName);
|
|
765
|
+
}
|
|
766
|
+
if (cached) cached.embeddedFieldNames = names;
|
|
767
|
+
return names;
|
|
768
|
+
};
|
|
769
|
+
|
|
678
770
|
const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
|
|
679
771
|
const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
|
|
680
772
|
const objectId = args.id;
|
|
681
|
-
|
|
682
|
-
const currentObject = await Model.findById({ _id: objectId }).lean();
|
|
683
|
-
|
|
684
773
|
const argTypes = gqltype.getFields();
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
774
|
+
const embeddedFieldNames = getEmbeddedFieldNames(gqltype);
|
|
775
|
+
|
|
776
|
+
if (embeddedFieldNames.length > 0) {
|
|
777
|
+
const projection = Object.fromEntries(embeddedFieldNames.map((name) => [name, 1]));
|
|
778
|
+
const currentObject = await Model.findById(objectId, projection).session(session).lean();
|
|
779
|
+
if (currentObject) {
|
|
780
|
+
for (const fieldEntryName of embeddedFieldNames) {
|
|
781
|
+
const oldObjectData = currentObject[fieldEntryName];
|
|
782
|
+
const newObjectData = materializedModel.modelArgs[fieldEntryName];
|
|
783
|
+
if (newObjectData) {
|
|
784
|
+
if (Array.isArray(oldObjectData) && Array.isArray(newObjectData)) {
|
|
785
|
+
materializedModel.modelArgs[fieldEntryName] = newObjectData;
|
|
786
|
+
} else {
|
|
787
|
+
materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
|
|
788
|
+
}
|
|
696
789
|
}
|
|
697
790
|
}
|
|
698
791
|
}
|
|
792
|
+
}
|
|
699
793
|
|
|
700
|
-
|
|
701
|
-
|
|
794
|
+
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
795
|
+
if (args[fieldEntryName] === null && !(fieldEntry.type instanceof GraphQLNonNull)) {
|
|
702
796
|
materializedModel.modelArgs = {
|
|
703
797
|
...materializedModel.modelArgs,
|
|
704
798
|
$unset: { ...materializedModel.modelArgs.$unset, [fieldEntryName]: '' },
|
|
705
799
|
};
|
|
706
800
|
}
|
|
707
|
-
}
|
|
801
|
+
}
|
|
708
802
|
|
|
709
803
|
if (controller && controller.onUpdating) {
|
|
710
804
|
await controller.onUpdating(objectId, materializedModel.modelArgs, session, context);
|
|
@@ -715,7 +809,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
715
809
|
).session(session);
|
|
716
810
|
|
|
717
811
|
if (materializedModel.collectionFields) {
|
|
718
|
-
await
|
|
812
|
+
await iterateOnCollectionFields(materializedModel, gqltype, objectId, session, context);
|
|
719
813
|
}
|
|
720
814
|
|
|
721
815
|
if (controller && controller.onUpdated) {
|
|
@@ -726,7 +820,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
726
820
|
};
|
|
727
821
|
|
|
728
822
|
const onStateChanged = async (Model, gqltype, controller, args, session, actionField, context) => {
|
|
729
|
-
const storedModel = await Model.findById(args.id);
|
|
823
|
+
const storedModel = await Model.findById(args.id).session(session);
|
|
730
824
|
if (!storedModel) {
|
|
731
825
|
throw new SimfinityError(`${gqltype.name} ${args.id} is not valid`, 'NOT_VALID_ID', 404);
|
|
732
826
|
}
|
|
@@ -762,10 +856,9 @@ const onSaveObject = async (Model, gqltype, controller, args, session, linkToPar
|
|
|
762
856
|
result = result.toObject();
|
|
763
857
|
|
|
764
858
|
if (materializedModel.collectionFields) {
|
|
765
|
-
await
|
|
859
|
+
await iterateOnCollectionFields(materializedModel, gqltype, newObject._id, session, context);
|
|
766
860
|
}
|
|
767
861
|
|
|
768
|
-
|
|
769
862
|
if (controller && controller.onSaved) {
|
|
770
863
|
await controller.onSaved(result, args, session, context);
|
|
771
864
|
}
|
|
@@ -780,38 +873,23 @@ export const saveObject = async (typeName, args, session, context) => {
|
|
|
780
873
|
return onSaveObject(type.model, type.gqltype, type.controller, args, session, null, context);
|
|
781
874
|
};
|
|
782
875
|
|
|
783
|
-
const executeOperation =
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
await mySession.startTransaction();
|
|
787
|
-
try {
|
|
788
|
-
let newObject = null;
|
|
876
|
+
const executeOperation = (Model, gqltype, controller, args, operation, actionField, session, context) => withTransaction(
|
|
877
|
+
session,
|
|
878
|
+
async (mySession) => {
|
|
789
879
|
switch (operation) {
|
|
790
880
|
case operations.SAVE:
|
|
791
|
-
|
|
792
|
-
break;
|
|
881
|
+
return onSaveObject(Model, gqltype, controller, args, mySession, null, context);
|
|
793
882
|
case operations.UPDATE:
|
|
794
|
-
|
|
795
|
-
break;
|
|
883
|
+
return onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
|
|
796
884
|
case operations.DELETE:
|
|
797
|
-
|
|
798
|
-
break;
|
|
885
|
+
return onDelete(Model, controller, args, mySession, context);
|
|
799
886
|
case operations.STATE_CHANGED:
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
await mySession.commitTransaction();
|
|
804
|
-
mySession.endSession();
|
|
805
|
-
return newObject;
|
|
806
|
-
} catch (error) {
|
|
807
|
-
await mySession.abortTransaction();
|
|
808
|
-
if (error.errorLabels && error.errorLabels.includes('TransientTransactionError')) {
|
|
809
|
-
return executeOperation(Model, gqltype, controller, args, operation, actionField, mySession, context);
|
|
887
|
+
return onStateChanged(Model, gqltype, controller, args, mySession, actionField, context);
|
|
888
|
+
default:
|
|
889
|
+
return null;
|
|
810
890
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
};
|
|
891
|
+
},
|
|
892
|
+
);
|
|
815
893
|
|
|
816
894
|
const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
817
895
|
collectionFieldsList, operationType, context) => {
|
|
@@ -840,7 +918,7 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
840
918
|
break;
|
|
841
919
|
case operations.DELETE:
|
|
842
920
|
operationFunction = async (collectionItem) => {
|
|
843
|
-
await
|
|
921
|
+
await onDelete(typesDict.types[collectionGQLType.name].model,
|
|
844
922
|
typesDict.types[collectionGQLType.name].controller, collectionItem, session, context);
|
|
845
923
|
};
|
|
846
924
|
}
|
|
@@ -853,27 +931,25 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
853
931
|
const shouldNotBeIncludedInSchema = (includedTypes,
|
|
854
932
|
type) => includedTypes && !includedTypes.includes(type);
|
|
855
933
|
|
|
856
|
-
const
|
|
934
|
+
const executeMiddleware = async (context) => {
|
|
857
935
|
const buildNext = (middlewaresParam) => {
|
|
858
936
|
if (!middlewaresParam) {
|
|
859
|
-
return () => {};
|
|
937
|
+
return async () => {};
|
|
860
938
|
}
|
|
861
|
-
|
|
939
|
+
return async () => {
|
|
862
940
|
const middleware = middlewaresParam[0];
|
|
863
941
|
if (middleware) {
|
|
864
|
-
middleware(context, buildNext(middlewaresParam.slice(1)));
|
|
942
|
+
await middleware(context, buildNext(middlewaresParam.slice(1)));
|
|
865
943
|
}
|
|
866
944
|
};
|
|
867
|
-
return next;
|
|
868
945
|
};
|
|
869
946
|
|
|
870
|
-
|
|
871
|
-
middleware();
|
|
947
|
+
await buildNext(middlewares)();
|
|
872
948
|
};
|
|
873
949
|
|
|
874
950
|
const executeScope = async (params) => {
|
|
875
951
|
const { type, args, operation, context } = params;
|
|
876
|
-
|
|
952
|
+
|
|
877
953
|
if (!type || !type.gqltype || !type.gqltype.extensions) {
|
|
878
954
|
return null;
|
|
879
955
|
}
|
|
@@ -888,12 +964,7 @@ const executeScope = async (params) => {
|
|
|
888
964
|
return null;
|
|
889
965
|
}
|
|
890
966
|
|
|
891
|
-
|
|
892
|
-
const result = await scopeFunction({ type, args, operation, context });
|
|
893
|
-
|
|
894
|
-
// For get_by_id, the scope function returns additional filters to merge
|
|
895
|
-
// For find and aggregate, it modifies args in place
|
|
896
|
-
return result;
|
|
967
|
+
return scopeFunction({ type, args, operation, context });
|
|
897
968
|
};
|
|
898
969
|
|
|
899
970
|
const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
|
|
@@ -920,7 +991,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
920
991
|
context,
|
|
921
992
|
};
|
|
922
993
|
|
|
923
|
-
|
|
994
|
+
await executeMiddleware(params);
|
|
924
995
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
925
996
|
args.input, operations.SAVE, null, null, context);
|
|
926
997
|
},
|
|
@@ -937,7 +1008,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
937
1008
|
context,
|
|
938
1009
|
};
|
|
939
1010
|
|
|
940
|
-
|
|
1011
|
+
await executeMiddleware(params);
|
|
941
1012
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
942
1013
|
args.id, operations.DELETE, null, null, context);
|
|
943
1014
|
},
|
|
@@ -962,7 +1033,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
962
1033
|
context,
|
|
963
1034
|
};
|
|
964
1035
|
|
|
965
|
-
|
|
1036
|
+
await executeMiddleware(params);
|
|
966
1037
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
967
1038
|
args.input, operations.UPDATE, null, null, context);
|
|
968
1039
|
},
|
|
@@ -984,7 +1055,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
984
1055
|
context,
|
|
985
1056
|
};
|
|
986
1057
|
|
|
987
|
-
|
|
1058
|
+
await executeMiddleware(params);
|
|
988
1059
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
989
1060
|
args.input, operations.STATE_CHANGED, actionField, null, context);
|
|
990
1061
|
},
|
|
@@ -1011,8 +1082,8 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1011
1082
|
entry,
|
|
1012
1083
|
context,
|
|
1013
1084
|
};
|
|
1014
|
-
|
|
1015
|
-
return executeRegisteredMutation(args.input, registeredMutation.callback);
|
|
1085
|
+
await executeMiddleware(params);
|
|
1086
|
+
return executeRegisteredMutation(args.input, registeredMutation.callback, context);
|
|
1016
1087
|
},
|
|
1017
1088
|
};
|
|
1018
1089
|
}
|
|
@@ -1021,102 +1092,64 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1021
1092
|
return new GraphQLObjectType(rootQueryArgs);
|
|
1022
1093
|
};
|
|
1023
1094
|
|
|
1095
|
+
const listItemMatchesScalar = (listType, target) => {
|
|
1096
|
+
const ofType = listType.ofType;
|
|
1097
|
+
return ofType === target
|
|
1098
|
+
|| (isCustomValidatedScalar(ofType) && ofType.baseScalarType === target);
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
const withUnique = (fieldEntry, mongoType) => (fieldEntry.extensions && fieldEntry.extensions.unique
|
|
1102
|
+
? { type: mongoType, unique: true }
|
|
1103
|
+
: mongoType);
|
|
1104
|
+
|
|
1024
1105
|
const generateSchemaDefinition = (gqlType) => {
|
|
1025
1106
|
const argTypes = gqlType.getFields();
|
|
1026
|
-
|
|
1027
1107
|
const schemaArg = {};
|
|
1028
1108
|
|
|
1029
1109
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
1030
|
-
|
|
1031
|
-
const getBaseScalarType = (scalarType) => scalarType.baseScalarType || scalarType;
|
|
1032
|
-
|
|
1033
|
-
// Helper function to check if a type is a custom validated scalar
|
|
1034
|
-
const isCustomValidatedScalar = (type) => type instanceof GraphQLScalarType && type.baseScalarType;
|
|
1110
|
+
const { type } = fieldEntry;
|
|
1035
1111
|
|
|
1036
|
-
if (
|
|
1112
|
+
if (matchesScalar(type, GraphQLID)) {
|
|
1037
1113
|
schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
|
|
1038
|
-
} else if (
|
|
1039
|
-
||
|
|
1040
|
-
|| (
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
schemaArg[fieldEntryName] = String;
|
|
1046
|
-
}
|
|
1047
|
-
} else if (fieldEntry.type instanceof GraphQLEnumType
|
|
1048
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
1049
|
-
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
1050
|
-
schemaArg[fieldEntryName] = { type: String, unique: true };
|
|
1051
|
-
} else {
|
|
1052
|
-
schemaArg[fieldEntryName] = String;
|
|
1053
|
-
}
|
|
1054
|
-
} else if (fieldEntry.type === GraphQLInt
|
|
1055
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLInt)
|
|
1056
|
-
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLInt)
|
|
1057
|
-
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt)) {
|
|
1058
|
-
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
1059
|
-
schemaArg[fieldEntryName] = { type: Number, unique: true };
|
|
1060
|
-
} else {
|
|
1061
|
-
schemaArg[fieldEntryName] = Number;
|
|
1062
|
-
}
|
|
1063
|
-
} else if (fieldEntry.type === GraphQLFloat
|
|
1064
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLFloat)
|
|
1065
|
-
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLFloat)
|
|
1066
|
-
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat)) {
|
|
1067
|
-
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
1068
|
-
schemaArg[fieldEntryName] = { type: Number, unique: true };
|
|
1069
|
-
} else {
|
|
1070
|
-
schemaArg[fieldEntryName] = Number;
|
|
1071
|
-
}
|
|
1072
|
-
} else if (fieldEntry.type === GraphQLBoolean
|
|
1073
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLBoolean)
|
|
1074
|
-
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLBoolean)
|
|
1075
|
-
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
|
|
1114
|
+
} else if (matchesScalar(type, GraphQLString)
|
|
1115
|
+
|| type instanceof GraphQLEnumType
|
|
1116
|
+
|| isNonNullOfType(type, GraphQLEnumType)) {
|
|
1117
|
+
schemaArg[fieldEntryName] = withUnique(fieldEntry, String);
|
|
1118
|
+
} else if (matchesScalar(type, GraphQLInt) || matchesScalar(type, GraphQLFloat)) {
|
|
1119
|
+
schemaArg[fieldEntryName] = withUnique(fieldEntry, Number);
|
|
1120
|
+
} else if (matchesScalar(type, GraphQLBoolean)) {
|
|
1076
1121
|
schemaArg[fieldEntryName] = Boolean;
|
|
1077
|
-
} else if (
|
|
1078
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
1122
|
+
} else if (type instanceof GraphQLObjectType || isNonNullOfType(type, GraphQLObjectType)) {
|
|
1079
1123
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1080
1124
|
if (!fieldEntry.extensions.relation.embedded) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1125
|
+
const key = fieldEntry.extensions.relation.connectionField || fieldEntry.name;
|
|
1126
|
+
schemaArg[key] = mongoose.Schema.Types.ObjectId;
|
|
1083
1127
|
} else {
|
|
1084
|
-
|
|
1085
|
-
if (entryType
|
|
1086
|
-
entryType = entryType.ofType;
|
|
1087
|
-
}
|
|
1088
|
-
if (entryType !== gqlType) {
|
|
1089
|
-
schemaArg[fieldEntryName] = generateSchemaDefinition(entryType);
|
|
1090
|
-
} else {
|
|
1128
|
+
const entryType = unwrapNonNull(type);
|
|
1129
|
+
if (entryType === gqlType) {
|
|
1091
1130
|
throw new Error('A type cannot have a field of its same type and embedded');
|
|
1092
1131
|
}
|
|
1132
|
+
schemaArg[fieldEntryName] = generateSchemaDefinition(entryType);
|
|
1093
1133
|
}
|
|
1094
1134
|
}
|
|
1095
|
-
} else if (
|
|
1135
|
+
} else if (type instanceof GraphQLList) {
|
|
1096
1136
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1097
1137
|
if (fieldEntry.extensions.relation.embedded) {
|
|
1098
|
-
|
|
1099
|
-
if (entryType !== gqlType) {
|
|
1100
|
-
schemaArg[fieldEntryName] = [generateSchemaDefinition(entryType)];
|
|
1101
|
-
} else {
|
|
1138
|
+
if (type.ofType === gqlType) {
|
|
1102
1139
|
throw new Error('A type cannot have a field of its same type and embedded');
|
|
1103
1140
|
}
|
|
1141
|
+
schemaArg[fieldEntryName] = [generateSchemaDefinition(type.ofType)];
|
|
1104
1142
|
}
|
|
1105
|
-
} else if (
|
|
1106
|
-
|| fieldEntry.type.ofType instanceof GraphQLEnumType
|
|
1107
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
|
|
1143
|
+
} else if (listItemMatchesScalar(type, GraphQLString) || type.ofType instanceof GraphQLEnumType) {
|
|
1108
1144
|
schemaArg[fieldEntryName] = [String];
|
|
1109
|
-
} else if (
|
|
1110
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
|
|
1145
|
+
} else if (listItemMatchesScalar(type, GraphQLBoolean)) {
|
|
1111
1146
|
schemaArg[fieldEntryName] = [Boolean];
|
|
1112
|
-
} else if (
|
|
1113
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && (getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt || getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat))) {
|
|
1147
|
+
} else if (listItemMatchesScalar(type, GraphQLInt) || listItemMatchesScalar(type, GraphQLFloat)) {
|
|
1114
1148
|
schemaArg[fieldEntryName] = [Number];
|
|
1115
|
-
} else if (isGraphQLisoDate(getEffectiveTypeName(
|
|
1149
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(type.ofType))) {
|
|
1116
1150
|
schemaArg[fieldEntryName] = [Date];
|
|
1117
1151
|
}
|
|
1118
|
-
} else if (isGraphQLisoDate(getEffectiveTypeName(
|
|
1119
|
-
|| (fieldEntry.type instanceof GraphQLNonNull && isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType)))) {
|
|
1152
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(unwrapNonNull(type)))) {
|
|
1120
1153
|
schemaArg[fieldEntryName] = Date;
|
|
1121
1154
|
}
|
|
1122
1155
|
}
|
|
@@ -1126,388 +1159,230 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
1126
1159
|
|
|
1127
1160
|
const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
1128
1161
|
const objectIdFields = [];
|
|
1129
|
-
|
|
1162
|
+
|
|
1130
1163
|
for (const [fieldName, fieldDefinition] of Object.entries(schemaDefinition)) {
|
|
1131
1164
|
const currentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
1132
|
-
|
|
1165
|
+
|
|
1133
1166
|
if (fieldDefinition === mongoose.Schema.Types.ObjectId) {
|
|
1134
|
-
// Direct ObjectId field
|
|
1135
1167
|
objectIdFields.push(currentPath);
|
|
1136
1168
|
} else if (typeof fieldDefinition === 'object' && fieldDefinition !== null) {
|
|
1137
1169
|
if (Array.isArray(fieldDefinition)) {
|
|
1138
|
-
// Array field - check if it's an array of objects
|
|
1139
1170
|
const arrayElement = fieldDefinition[0];
|
|
1140
1171
|
if (typeof arrayElement === 'object' && arrayElement !== null) {
|
|
1141
|
-
|
|
1142
|
-
const nestedObjectIdFields = findObjectIdFields(arrayElement, currentPath);
|
|
1143
|
-
objectIdFields.push(...nestedObjectIdFields);
|
|
1172
|
+
objectIdFields.push(...findObjectIdFields(arrayElement, currentPath));
|
|
1144
1173
|
}
|
|
1145
1174
|
} else if (fieldDefinition.type === mongoose.Schema.Types.ObjectId) {
|
|
1146
|
-
// Object with ObjectId type
|
|
1147
1175
|
objectIdFields.push(currentPath);
|
|
1148
|
-
} else if (
|
|
1149
|
-
|
|
1150
|
-
const nestedObjectIdFields = findObjectIdFields(fieldDefinition, currentPath);
|
|
1151
|
-
objectIdFields.push(...nestedObjectIdFields);
|
|
1176
|
+
} else if (!fieldDefinition.type) {
|
|
1177
|
+
objectIdFields.push(...findObjectIdFields(fieldDefinition, currentPath));
|
|
1152
1178
|
}
|
|
1153
1179
|
}
|
|
1154
1180
|
}
|
|
1155
|
-
|
|
1181
|
+
|
|
1156
1182
|
return objectIdFields;
|
|
1157
1183
|
};
|
|
1158
1184
|
|
|
1159
1185
|
const createSchemaWithIndexes = (schemaDefinition) => {
|
|
1160
1186
|
const schema = new mongoose.Schema(schemaDefinition);
|
|
1161
|
-
|
|
1162
|
-
// Find all ObjectId fields in the schema
|
|
1163
|
-
const objectIdFields = findObjectIdFields(schemaDefinition);
|
|
1164
|
-
|
|
1165
|
-
// Create indexes for all ObjectId fields
|
|
1166
|
-
objectIdFields.forEach(fieldPath => {
|
|
1187
|
+
findObjectIdFields(schemaDefinition).forEach((fieldPath) => {
|
|
1167
1188
|
schema.index({ [fieldPath]: 1 });
|
|
1168
1189
|
});
|
|
1169
|
-
|
|
1170
1190
|
return schema;
|
|
1171
1191
|
};
|
|
1172
1192
|
|
|
1173
|
-
const generateModel = (gqlType, onModelCreated) => {
|
|
1193
|
+
const generateModel = (gqlType, onModelCreated, { createCollection = true } = {}) => {
|
|
1174
1194
|
const schemaDefinition = generateSchemaDefinition(gqlType);
|
|
1175
1195
|
const schema = createSchemaWithIndexes(schemaDefinition);
|
|
1176
1196
|
const model = mongoose.model(gqlType.name, schema, gqlType.name);
|
|
1177
1197
|
if (onModelCreated) {
|
|
1178
1198
|
onModelCreated(model);
|
|
1179
1199
|
}
|
|
1180
|
-
if (!preventCollectionCreation) {
|
|
1200
|
+
if (createCollection && !preventCollectionCreation) {
|
|
1181
1201
|
model.createCollection();
|
|
1182
1202
|
}
|
|
1183
1203
|
return model;
|
|
1184
1204
|
};
|
|
1185
1205
|
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
const model = mongoose.model(gqlType.name, schema, gqlType.name);
|
|
1190
|
-
if (onModelCreated) {
|
|
1191
|
-
onModelCreated(model);
|
|
1206
|
+
const coerceIdArray = (value) => value.map((element) => {
|
|
1207
|
+
if (element === null || element === undefined) {
|
|
1208
|
+
throw new SimfinityError('ID value cannot be null in collection filter', 'INVALID_FILTER_VALUE', 400);
|
|
1192
1209
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
};
|
|
1210
|
+
return new mongoose.Types.ObjectId(element);
|
|
1211
|
+
});
|
|
1196
1212
|
|
|
1197
1213
|
const buildMatchesClause = (fieldname, operator, value) => {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
} else if (operator === QLOperator.getValue('GT').value) {
|
|
1208
|
-
matches[fieldname] = { $gt: value };
|
|
1209
|
-
} else if (operator === QLOperator.getValue('LTE').value) {
|
|
1210
|
-
matches[fieldname] = { $lte: value };
|
|
1211
|
-
} else if (operator === QLOperator.getValue('GTE').value) {
|
|
1212
|
-
matches[fieldname] = { $gte: value };
|
|
1213
|
-
} else if (operator === QLOperator.getValue('NE').value) {
|
|
1214
|
-
matches[fieldname] = { $ne: value };
|
|
1215
|
-
} else if (operator === QLOperator.getValue('BTW').value) {
|
|
1216
|
-
matches[fieldname] = { $gte: value[0], $lte: value[1] };
|
|
1217
|
-
} else if (operator === QLOperator.getValue('IN').value) {
|
|
1218
|
-
let fixedArray = value;
|
|
1219
|
-
if (value && fieldname.endsWith('_id')) {
|
|
1220
|
-
fixedArray = [];
|
|
1221
|
-
value.forEach((element) => {
|
|
1222
|
-
fixedArray.push(new mongoose.Types.ObjectId(element));
|
|
1223
|
-
});
|
|
1224
|
-
}
|
|
1225
|
-
matches[fieldname] = { $in: fixedArray };
|
|
1226
|
-
} else if (operator === QLOperator.getValue('NIN').value) {
|
|
1227
|
-
let fixedArray = value;
|
|
1228
|
-
if (value && fieldname.endsWith('_id')) {
|
|
1229
|
-
fixedArray = [];
|
|
1230
|
-
value.forEach((element) => {
|
|
1231
|
-
fixedArray.push(new mongoose.Types.ObjectId(element));
|
|
1232
|
-
});
|
|
1214
|
+
const isIdField = fieldname.endsWith('_id');
|
|
1215
|
+
const op = operator || 'EQ';
|
|
1216
|
+
|
|
1217
|
+
if (op === 'EQ') {
|
|
1218
|
+
return { [fieldname]: isIdField ? new mongoose.Types.ObjectId(value) : value };
|
|
1219
|
+
}
|
|
1220
|
+
if (op === 'IN' || op === 'NIN') {
|
|
1221
|
+
if (!Array.isArray(value)) {
|
|
1222
|
+
throw new SimfinityError(`${op} requires an array value for ${fieldname}`, 'INVALID_FILTER_VALUE', 400);
|
|
1233
1223
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
matches[fieldname] = { $regex: `.*${value}.*` };
|
|
1224
|
+
const coerced = isIdField ? coerceIdArray(value) : value;
|
|
1225
|
+
return { [fieldname]: { [op === 'IN' ? '$in' : '$nin']: coerced } };
|
|
1237
1226
|
}
|
|
1227
|
+
const builder = OP_TO_MONGO[op];
|
|
1228
|
+
return builder ? { [fieldname]: builder(value) } : {};
|
|
1229
|
+
};
|
|
1238
1230
|
|
|
1239
|
-
|
|
1231
|
+
const topLevelRelationLookup = (qlField, fieldType, fieldName) => {
|
|
1232
|
+
const { collectionName } = typesDict.types[fieldType.name].model.collection;
|
|
1233
|
+
const connField = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1234
|
+
const isList = qlField.type instanceof GraphQLList;
|
|
1235
|
+
return buildRelationLookup({
|
|
1236
|
+
collectionName,
|
|
1237
|
+
localField: isList ? '_id' : connField,
|
|
1238
|
+
foreignField: isList ? connField : '_id',
|
|
1239
|
+
alias: fieldName,
|
|
1240
|
+
});
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
const nestedRelationLookup = (pathField, pathFieldType, currentPath, aliasPath, pathFieldName) => {
|
|
1244
|
+
const { collectionName } = typesDict.types[pathFieldType.name].model.collection;
|
|
1245
|
+
const connField = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1246
|
+
const isList = pathField.type instanceof GraphQLList;
|
|
1247
|
+
return buildRelationLookup({
|
|
1248
|
+
collectionName,
|
|
1249
|
+
localField: isList ? `${currentPath}._id` : `${currentPath}.${connField}`,
|
|
1250
|
+
foreignField: isList ? connField : '_id',
|
|
1251
|
+
alias: aliasPath,
|
|
1252
|
+
});
|
|
1240
1253
|
};
|
|
1241
1254
|
|
|
1242
1255
|
const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
1243
1256
|
const aggregateClauses = {};
|
|
1257
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1244
1258
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
fieldType = qlField.type.ofType;
|
|
1259
|
+
if (!(fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType))) {
|
|
1260
|
+
return aggregateClauses;
|
|
1248
1261
|
}
|
|
1249
|
-
if (fieldType instanceof GraphQLObjectType
|
|
1250
|
-
|| isNonNullOfType(fieldType, GraphQLObjectType)) {
|
|
1251
|
-
if (fieldType instanceof GraphQLNonNull) {
|
|
1252
|
-
fieldType = qlField.type.ofType;
|
|
1253
|
-
}
|
|
1254
|
-
filterField.terms.forEach((term) => {
|
|
1255
|
-
if (qlField.extensions && qlField.extensions.relation
|
|
1256
|
-
&& !qlField.extensions.relation.embedded) {
|
|
1257
|
-
const { model } = typesDict.types[fieldType.name];
|
|
1258
|
-
const { collectionName } = model.collection;
|
|
1259
|
-
const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1260
|
-
if (!aggregateClauses[fieldName]) {
|
|
1261
|
-
let lookup = {};
|
|
1262
|
-
|
|
1263
|
-
if (qlField.type instanceof GraphQLList) {
|
|
1264
|
-
lookup = {
|
|
1265
|
-
$lookup: {
|
|
1266
|
-
from: collectionName,
|
|
1267
|
-
foreignField: localFieldName,
|
|
1268
|
-
localField: '_id',
|
|
1269
|
-
as: fieldName,
|
|
1270
|
-
},
|
|
1271
|
-
};
|
|
1272
|
-
} else {
|
|
1273
|
-
lookup = {
|
|
1274
|
-
$lookup: {
|
|
1275
|
-
from: collectionName,
|
|
1276
|
-
foreignField: '_id',
|
|
1277
|
-
localField: localFieldName,
|
|
1278
|
-
as: fieldName,
|
|
1279
|
-
},
|
|
1280
|
-
};
|
|
1281
|
-
}
|
|
1282
1262
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1263
|
+
if (!Array.isArray(filterField?.terms) || filterField.terms.length === 0) {
|
|
1264
|
+
return aggregateClauses;
|
|
1265
|
+
}
|
|
1289
1266
|
|
|
1290
|
-
|
|
1291
|
-
if (currentGQLPathFieldType instanceof GraphQLList
|
|
1292
|
-
|| currentGQLPathFieldType instanceof GraphQLNonNull) {
|
|
1293
|
-
currentGQLPathFieldType = currentGQLPathFieldType.ofType;
|
|
1294
|
-
}
|
|
1295
|
-
let aliasPath = fieldName;
|
|
1296
|
-
let embeddedPath = '';
|
|
1297
|
-
|
|
1298
|
-
term.path.split('.').forEach((pathFieldName) => {
|
|
1299
|
-
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1300
|
-
if (pathField.type instanceof GraphQLObjectType
|
|
1301
|
-
|| pathField.type instanceof GraphQLList
|
|
1302
|
-
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1303
|
-
let pathFieldType = pathField.type;
|
|
1304
|
-
if (pathField.type instanceof GraphQLList || pathField.type instanceof GraphQLNonNull) {
|
|
1305
|
-
pathFieldType = pathField.type.ofType;
|
|
1306
|
-
}
|
|
1307
|
-
currentGQLPathFieldType = pathFieldType;
|
|
1308
|
-
if (pathField.extensions && pathField.extensions.relation
|
|
1309
|
-
&& !pathField.extensions.relation.embedded) {
|
|
1310
|
-
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1311
|
-
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1312
|
-
|
|
1313
|
-
embeddedPath = '';
|
|
1314
|
-
|
|
1315
|
-
const pathModel = typesDict.types[pathFieldType.name].model;
|
|
1316
|
-
const fieldPathCollectionName = pathModel.collection.collectionName;
|
|
1317
|
-
const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1318
|
-
|
|
1319
|
-
if (!aggregateClauses[aliasPath]) {
|
|
1320
|
-
let lookup = {};
|
|
1321
|
-
if (pathField.type instanceof GraphQLList) {
|
|
1322
|
-
lookup = {
|
|
1323
|
-
$lookup: {
|
|
1324
|
-
from: fieldPathCollectionName,
|
|
1325
|
-
foreignField: pathLocalFieldName,
|
|
1326
|
-
localField: `${currentPath}._id`,
|
|
1327
|
-
as: aliasPath,
|
|
1328
|
-
},
|
|
1329
|
-
};
|
|
1330
|
-
} else {
|
|
1331
|
-
lookup = {
|
|
1332
|
-
$lookup: {
|
|
1333
|
-
from: fieldPathCollectionName,
|
|
1334
|
-
foreignField: '_id',
|
|
1335
|
-
localField: `${currentPath}.${pathLocalFieldName}`,
|
|
1336
|
-
as: aliasPath,
|
|
1337
|
-
},
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1267
|
+
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1340
1268
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1269
|
+
filterField.terms.forEach((term) => {
|
|
1270
|
+
assertValidFilterPath(term.path);
|
|
1271
|
+
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1272
|
+
&& !aggregateClauses[fieldName]) {
|
|
1273
|
+
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
let currentGQLPathFieldType = unwrapListAndNonNull(qlField.type);
|
|
1277
|
+
let aliasPath = fieldName;
|
|
1278
|
+
let embeddedPath = '';
|
|
1279
|
+
|
|
1280
|
+
term.path.split('.').forEach((pathFieldName) => {
|
|
1281
|
+
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1282
|
+
if (pathField.type instanceof GraphQLObjectType
|
|
1283
|
+
|| pathField.type instanceof GraphQLList
|
|
1284
|
+
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1285
|
+
const pathFieldType = unwrapListAndNonNull(pathField.type);
|
|
1286
|
+
currentGQLPathFieldType = pathFieldType;
|
|
1287
|
+
|
|
1288
|
+
if (pathField.extensions?.relation && !pathField.extensions.relation.embedded) {
|
|
1289
|
+
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1290
|
+
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1291
|
+
embeddedPath = '';
|
|
1292
|
+
|
|
1293
|
+
if (!aggregateClauses[aliasPath]) {
|
|
1294
|
+
aggregateClauses[aliasPath] = nestedRelationLookup(
|
|
1295
|
+
pathField, pathFieldType, currentPath, aliasPath, pathFieldName,
|
|
1296
|
+
);
|
|
1350
1297
|
}
|
|
1298
|
+
} else {
|
|
1299
|
+
embeddedPath = embeddedPath === '' ? pathFieldName : `${embeddedPath}.${pathFieldName}`;
|
|
1351
1300
|
}
|
|
1352
|
-
}
|
|
1301
|
+
}
|
|
1353
1302
|
});
|
|
1354
|
-
}
|
|
1303
|
+
});
|
|
1355
1304
|
return aggregateClauses;
|
|
1356
1305
|
};
|
|
1357
1306
|
|
|
1358
1307
|
const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
1359
1308
|
const aggregateClauses = {};
|
|
1360
1309
|
const matchesClauses = {};
|
|
1310
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1361
1311
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
fieldType = qlField.type.ofType;
|
|
1312
|
+
if (filterField == null) {
|
|
1313
|
+
return { aggregateClauses, matchesClauses };
|
|
1365
1314
|
}
|
|
1315
|
+
|
|
1366
1316
|
if (fieldType instanceof GraphQLScalarType
|
|
1367
1317
|
|| isNonNullOfType(fieldType, GraphQLScalarType)
|
|
1368
1318
|
|| fieldType instanceof GraphQLEnumType
|
|
1369
1319
|
|| isNonNullOfType(fieldType, GraphQLEnumType)) {
|
|
1370
|
-
|
|
1371
|
-
if (isGraphQLisoDate(fieldTypeName)) {
|
|
1372
|
-
if (Array.isArray(filterField.value)) {
|
|
1373
|
-
filterField.value = filterField.value.map((value) => value && new Date(value));
|
|
1374
|
-
} else {
|
|
1375
|
-
filterField.value = filterField.value && new Date(filterField.value);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1320
|
+
coerceDateValue(fieldType, filterField, 'value');
|
|
1378
1321
|
matchesClauses[fieldName] = buildMatchesClause(fieldName === 'id' ? '_id' : fieldName, filterField.operator, filterField.value);
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
if (fieldType instanceof GraphQLNonNull) {
|
|
1382
|
-
fieldType = qlField.type.ofType;
|
|
1383
|
-
}
|
|
1322
|
+
return { aggregateClauses, matchesClauses };
|
|
1323
|
+
}
|
|
1384
1324
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const { model } = typesDict.types[fieldType.name];
|
|
1389
|
-
const { collectionName } = model.collection;
|
|
1390
|
-
const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1391
|
-
if (!aggregateClauses[fieldName]) {
|
|
1392
|
-
let lookup = {};
|
|
1393
|
-
|
|
1394
|
-
if (qlField.type instanceof GraphQLList) {
|
|
1395
|
-
lookup = {
|
|
1396
|
-
$lookup: {
|
|
1397
|
-
from: collectionName,
|
|
1398
|
-
foreignField: localFieldName,
|
|
1399
|
-
localField: '_id',
|
|
1400
|
-
as: fieldName,
|
|
1401
|
-
},
|
|
1402
|
-
};
|
|
1403
|
-
} else {
|
|
1404
|
-
lookup = {
|
|
1405
|
-
$lookup: {
|
|
1406
|
-
from: collectionName,
|
|
1407
|
-
foreignField: '_id',
|
|
1408
|
-
localField: localFieldName,
|
|
1409
|
-
as: fieldName,
|
|
1410
|
-
},
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1325
|
+
if (!(fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType))) {
|
|
1326
|
+
return { aggregateClauses, matchesClauses };
|
|
1327
|
+
}
|
|
1413
1328
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
};
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1329
|
+
if (!Array.isArray(filterField.terms) || filterField.terms.length === 0) {
|
|
1330
|
+
return { aggregateClauses, matchesClauses };
|
|
1331
|
+
}
|
|
1420
1332
|
|
|
1421
|
-
|
|
1422
|
-
const { type } = fieldType.getFields()[term.path];
|
|
1423
|
-
const typeName = type instanceof GraphQLNonNull ? getEffectiveTypeName(type.ofType) : getEffectiveTypeName(type);
|
|
1424
|
-
if (isGraphQLisoDate(typeName)) {
|
|
1425
|
-
if (Array.isArray(term.value)) {
|
|
1426
|
-
term.value = term.value.map((value) => value && new Date(value));
|
|
1427
|
-
} else {
|
|
1428
|
-
term.value = term.value && new Date(term.value);
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
matchesClauses[fieldName] = buildMatchesClause(`${fieldName}.${fieldType.getFields()[term.path].name === 'id' ? '_id' : term.path}`, term.operator, term.value);
|
|
1432
|
-
} else {
|
|
1433
|
-
let currentGQLPathFieldType = qlField.type;
|
|
1434
|
-
if (currentGQLPathFieldType instanceof GraphQLList
|
|
1435
|
-
|| currentGQLPathFieldType instanceof GraphQLNonNull) {
|
|
1436
|
-
currentGQLPathFieldType = currentGQLPathFieldType.ofType;
|
|
1437
|
-
}
|
|
1438
|
-
let aliasPath = fieldName;
|
|
1439
|
-
let embeddedPath = '';
|
|
1440
|
-
|
|
1441
|
-
term.path.split('.').forEach((pathFieldName) => {
|
|
1442
|
-
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1443
|
-
if (pathField.type instanceof GraphQLScalarType
|
|
1444
|
-
|| isNonNullOfType(pathField.type, GraphQLScalarType)) {
|
|
1445
|
-
const typeName = pathField.type instanceof GraphQLNonNull ? getEffectiveTypeName(pathField.type.ofType) : getEffectiveTypeName(pathField.type);
|
|
1446
|
-
if (isGraphQLisoDate(typeName)) {
|
|
1447
|
-
if (Array.isArray(term.value)) {
|
|
1448
|
-
term.value = term.value.map((value) => value && new Date(value));
|
|
1449
|
-
} else {
|
|
1450
|
-
term.value = term.value && new Date(term.value);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
matchesClauses[`${aliasPath}_${pathFieldName}`] = buildMatchesClause(aliasPath + (embeddedPath !== '' ? `.${embeddedPath}.` : '.') + (pathFieldName === 'id' ? '_id' : pathFieldName), term.operator, term.value);
|
|
1454
|
-
embeddedPath = '';
|
|
1455
|
-
} else if (pathField.type instanceof GraphQLObjectType
|
|
1456
|
-
|| pathField.type instanceof GraphQLList
|
|
1457
|
-
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1458
|
-
let pathFieldType = pathField.type;
|
|
1459
|
-
if (pathField.type instanceof GraphQLList || pathField.type instanceof GraphQLNonNull) {
|
|
1460
|
-
pathFieldType = pathField.type.ofType;
|
|
1461
|
-
}
|
|
1462
|
-
currentGQLPathFieldType = pathFieldType;
|
|
1463
|
-
if (pathField.extensions && pathField.extensions.relation
|
|
1464
|
-
&& !pathField.extensions.relation.embedded) {
|
|
1465
|
-
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1466
|
-
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1467
|
-
|
|
1468
|
-
embeddedPath = '';
|
|
1469
|
-
|
|
1470
|
-
const pathModel = typesDict.types[pathFieldType.name].model;
|
|
1471
|
-
const fieldPathCollectionName = pathModel.collection.collectionName;
|
|
1472
|
-
const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1473
|
-
|
|
1474
|
-
if (!aggregateClauses[aliasPath]) {
|
|
1475
|
-
let lookup = {};
|
|
1476
|
-
if (pathField.type instanceof GraphQLList) {
|
|
1477
|
-
lookup = {
|
|
1478
|
-
$lookup: {
|
|
1479
|
-
from: fieldPathCollectionName,
|
|
1480
|
-
foreignField: pathLocalFieldName,
|
|
1481
|
-
localField: `${currentPath}._id`,
|
|
1482
|
-
as: aliasPath,
|
|
1483
|
-
},
|
|
1484
|
-
};
|
|
1485
|
-
} else {
|
|
1486
|
-
lookup = {
|
|
1487
|
-
$lookup: {
|
|
1488
|
-
from: fieldPathCollectionName,
|
|
1489
|
-
foreignField: '_id',
|
|
1490
|
-
localField: `${currentPath}.${pathLocalFieldName}`,
|
|
1491
|
-
as: aliasPath,
|
|
1492
|
-
},
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1333
|
+
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1495
1334
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1335
|
+
filterField.terms.forEach((term) => {
|
|
1336
|
+
assertValidFilterPath(term.path);
|
|
1337
|
+
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1338
|
+
&& !aggregateClauses[fieldName]) {
|
|
1339
|
+
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (term.path.indexOf('.') < 0) {
|
|
1343
|
+
const { type: leafType } = resolvedFieldType.getFields()[term.path];
|
|
1344
|
+
coerceDateValue(leafType, term, 'value');
|
|
1345
|
+
const leafName = resolvedFieldType.getFields()[term.path].name === 'id' ? '_id' : term.path;
|
|
1346
|
+
matchesClauses[fieldName] = buildMatchesClause(`${fieldName}.${leafName}`, term.operator, term.value);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
let currentGQLPathFieldType = unwrapListAndNonNull(qlField.type);
|
|
1351
|
+
let aliasPath = fieldName;
|
|
1352
|
+
let embeddedPath = '';
|
|
1353
|
+
|
|
1354
|
+
term.path.split('.').forEach((pathFieldName) => {
|
|
1355
|
+
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1356
|
+
if (pathField.type instanceof GraphQLScalarType
|
|
1357
|
+
|| isNonNullOfType(pathField.type, GraphQLScalarType)) {
|
|
1358
|
+
coerceDateValue(pathField.type, term, 'value');
|
|
1359
|
+
const leafName = pathFieldName === 'id' ? '_id' : pathFieldName;
|
|
1360
|
+
const mongoPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}.` : '.') + leafName;
|
|
1361
|
+
matchesClauses[`${aliasPath}_${pathFieldName}`] = buildMatchesClause(mongoPath, term.operator, term.value);
|
|
1362
|
+
embeddedPath = '';
|
|
1363
|
+
} else if (pathField.type instanceof GraphQLObjectType
|
|
1364
|
+
|| pathField.type instanceof GraphQLList
|
|
1365
|
+
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1366
|
+
const pathFieldType = unwrapListAndNonNull(pathField.type);
|
|
1367
|
+
currentGQLPathFieldType = pathFieldType;
|
|
1368
|
+
|
|
1369
|
+
if (pathField.extensions?.relation && !pathField.extensions.relation.embedded) {
|
|
1370
|
+
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1371
|
+
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1372
|
+
embeddedPath = '';
|
|
1373
|
+
|
|
1374
|
+
if (!aggregateClauses[aliasPath]) {
|
|
1375
|
+
aggregateClauses[aliasPath] = nestedRelationLookup(
|
|
1376
|
+
pathField, pathFieldType, currentPath, aliasPath, pathFieldName,
|
|
1377
|
+
);
|
|
1506
1378
|
}
|
|
1507
|
-
}
|
|
1379
|
+
} else {
|
|
1380
|
+
embeddedPath = embeddedPath === '' ? pathFieldName : `${embeddedPath}.${pathFieldName}`;
|
|
1381
|
+
}
|
|
1508
1382
|
}
|
|
1509
1383
|
});
|
|
1510
|
-
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1511
1386
|
return { aggregateClauses, matchesClauses };
|
|
1512
1387
|
};
|
|
1513
1388
|
|
|
@@ -1521,93 +1396,57 @@ const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, agg
|
|
|
1521
1396
|
const parts = [];
|
|
1522
1397
|
const fields = gqltype.getFields();
|
|
1523
1398
|
|
|
1524
|
-
|
|
1525
|
-
if (filterGroup.conditions && filterGroup.conditions.length > 0) {
|
|
1399
|
+
if (filterGroup.conditions?.length > 0) {
|
|
1526
1400
|
for (const condition of filterGroup.conditions) {
|
|
1527
1401
|
const qlField = fields[condition.field];
|
|
1528
1402
|
if (!qlField) {
|
|
1529
|
-
throw new SimfinityError(
|
|
1530
|
-
`Unknown filter field: ${condition.field}`,
|
|
1531
|
-
'INVALID_FILTER_FIELD',
|
|
1532
|
-
400,
|
|
1533
|
-
);
|
|
1403
|
+
throw new SimfinityError(`Unknown filter field: ${condition.field}`, 'INVALID_FILTER_FIELD', 400);
|
|
1534
1404
|
}
|
|
1535
1405
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
if (fieldType instanceof GraphQLList || fieldType instanceof GraphQLNonNull) {
|
|
1539
|
-
fieldType = fieldType.ofType;
|
|
1540
|
-
}
|
|
1406
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1407
|
+
const isObject = fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType);
|
|
1541
1408
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
// Object/relation field — wrap as QLTypeFilterExpression shape
|
|
1409
|
+
let filterInput;
|
|
1410
|
+
if (isObject) {
|
|
1545
1411
|
if (!condition.path) {
|
|
1546
|
-
throw new SimfinityError(
|
|
1547
|
-
`Filter on object field "${condition.field}" requires a path`,
|
|
1548
|
-
'MISSING_FILTER_PATH',
|
|
1549
|
-
400,
|
|
1550
|
-
);
|
|
1412
|
+
throw new SimfinityError(`Filter on object field "${condition.field}" requires a path`, 'MISSING_FILTER_PATH', 400);
|
|
1551
1413
|
}
|
|
1552
1414
|
filterInput = {
|
|
1553
|
-
terms: [{
|
|
1554
|
-
path: condition.path,
|
|
1555
|
-
operator: condition.operator,
|
|
1556
|
-
value: condition.value,
|
|
1557
|
-
}],
|
|
1415
|
+
terms: [{ path: condition.path, operator: condition.operator, value: condition.value }],
|
|
1558
1416
|
};
|
|
1559
1417
|
} else {
|
|
1560
|
-
|
|
1561
|
-
filterInput = {
|
|
1562
|
-
operator: condition.operator,
|
|
1563
|
-
value: condition.value,
|
|
1564
|
-
};
|
|
1418
|
+
filterInput = { operator: condition.operator, value: condition.value };
|
|
1565
1419
|
}
|
|
1566
1420
|
|
|
1567
1421
|
const result = await buildQueryTerms(filterInput, qlField, condition.field);
|
|
1422
|
+
if (!result) continue;
|
|
1568
1423
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
aggregateClauses.push(aggregate.lookup);
|
|
1574
|
-
aggregateClauses.push(aggregate.unwind);
|
|
1575
|
-
aggregationsIncluded[prop] = true;
|
|
1576
|
-
}
|
|
1424
|
+
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1425
|
+
if (!aggregationsIncluded[prop]) {
|
|
1426
|
+
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1427
|
+
aggregationsIncluded[prop] = true;
|
|
1577
1428
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
for (const
|
|
1581
|
-
|
|
1582
|
-
parts.push({ [matchKey]: match });
|
|
1583
|
-
}
|
|
1429
|
+
}
|
|
1430
|
+
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
1431
|
+
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1432
|
+
parts.push({ [matchKey]: match });
|
|
1584
1433
|
}
|
|
1585
1434
|
}
|
|
1586
1435
|
}
|
|
1587
1436
|
}
|
|
1588
1437
|
|
|
1589
|
-
|
|
1590
|
-
if (filterGroup.AND && filterGroup.AND.length > 0) {
|
|
1438
|
+
if (filterGroup.AND?.length > 0) {
|
|
1591
1439
|
for (const subGroup of filterGroup.AND) {
|
|
1592
|
-
const subMatch = await buildFilterGroupMatch(
|
|
1593
|
-
|
|
1594
|
-
);
|
|
1595
|
-
if (subMatch) {
|
|
1596
|
-
parts.push(subMatch);
|
|
1597
|
-
}
|
|
1440
|
+
const subMatch = await buildFilterGroupMatch(subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1);
|
|
1441
|
+
if (subMatch) parts.push(subMatch);
|
|
1598
1442
|
}
|
|
1599
1443
|
}
|
|
1600
1444
|
|
|
1601
|
-
|
|
1602
|
-
if (filterGroup.OR && filterGroup.OR.length > 0) {
|
|
1445
|
+
if (filterGroup.OR?.length > 0) {
|
|
1603
1446
|
const orParts = [];
|
|
1604
1447
|
for (const subGroup of filterGroup.OR) {
|
|
1605
|
-
const subMatch = await buildFilterGroupMatch(
|
|
1606
|
-
|
|
1607
|
-
);
|
|
1608
|
-
if (subMatch) {
|
|
1609
|
-
orParts.push(subMatch);
|
|
1610
|
-
}
|
|
1448
|
+
const subMatch = await buildFilterGroupMatch(subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1);
|
|
1449
|
+
if (subMatch) orParts.push(subMatch);
|
|
1611
1450
|
}
|
|
1612
1451
|
if (orParts.length === 1) {
|
|
1613
1452
|
parts.push(orParts[0]);
|
|
@@ -1623,105 +1462,44 @@ const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, agg
|
|
|
1623
1462
|
|
|
1624
1463
|
const RESERVED_QUERY_KEYS = new Set(['pagination', 'sort', 'AND', 'OR', 'aggregation']);
|
|
1625
1464
|
|
|
1626
|
-
const
|
|
1627
|
-
const aggregateClauses = [];
|
|
1465
|
+
const collectFiltersAndLookups = async (input, gqltype, aggregateClauses, aggregationsIncluded) => {
|
|
1628
1466
|
const flatMatchConditions = {};
|
|
1629
1467
|
let hasFlat = false;
|
|
1630
|
-
|
|
1631
|
-
let skipClause = { $skip: 0 };
|
|
1632
|
-
let sortClause = {};
|
|
1633
|
-
let addSort = false;
|
|
1634
|
-
const aggregationsIncluded = {};
|
|
1468
|
+
const fields = gqltype.getFields();
|
|
1635
1469
|
|
|
1636
1470
|
for (const [key, filterField] of Object.entries(input)) {
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
if (Object.prototype.hasOwnProperty.call(result.matchesClauses, matchClauseKey)) {
|
|
1651
|
-
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1652
|
-
if (Object.prototype.hasOwnProperty.call(matchClause, matchKey)) {
|
|
1653
|
-
flatMatchConditions[matchKey] = match;
|
|
1654
|
-
hasFlat = true;
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
} else if (key === 'pagination') {
|
|
1661
|
-
if (filterField.page && filterField.size) {
|
|
1662
|
-
const skip = filterField.size * (filterField.page - 1);
|
|
1663
|
-
limitClause = { $limit: filterField.size + skip };
|
|
1664
|
-
skipClause = { $skip: skip };
|
|
1471
|
+
if (RESERVED_QUERY_KEYS.has(key)) continue;
|
|
1472
|
+
const qlField = fields[key];
|
|
1473
|
+
const result = await buildQueryTerms(filterField, qlField, key);
|
|
1474
|
+
if (!result) continue;
|
|
1475
|
+
|
|
1476
|
+
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1477
|
+
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1478
|
+
aggregationsIncluded[prop] = true;
|
|
1479
|
+
}
|
|
1480
|
+
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
1481
|
+
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1482
|
+
flatMatchConditions[matchKey] = match;
|
|
1483
|
+
hasFlat = true;
|
|
1665
1484
|
}
|
|
1666
|
-
} else if (key === 'sort') {
|
|
1667
|
-
const sortExpressions = {};
|
|
1668
|
-
filterField.terms.forEach((sort) => {
|
|
1669
|
-
let fixedSortField = sort.field;
|
|
1670
|
-
|
|
1671
|
-
if (sort.field.indexOf('.') >= 0) {
|
|
1672
|
-
const sortParts = sort.field.split('.');
|
|
1673
|
-
|
|
1674
|
-
fixedSortField = sortParts[0];
|
|
1675
|
-
|
|
1676
|
-
for (let i = 1; i < sortParts.length - 1; i++) {
|
|
1677
|
-
fixedSortField += `_${sortParts[i]}`;
|
|
1678
|
-
}
|
|
1679
|
-
fixedSortField += `.${sortParts[sortParts.length - 1]}`;
|
|
1680
|
-
const qlField = gqltype.getFields()[sortParts[0]];
|
|
1681
|
-
const path = sort.field.slice(sort.field.indexOf('.') + 1);
|
|
1682
|
-
const aggreagtionsForSort = buildAggregationsForSort({ terms: [{ path }] }, qlField, sortParts[0]);
|
|
1683
|
-
for (const [prop, aggregate] of Object.entries(aggreagtionsForSort)) {
|
|
1684
|
-
if (!aggregationsIncluded[prop]) {
|
|
1685
|
-
aggregateClauses.push(aggregate.lookup);
|
|
1686
|
-
aggregateClauses.push(aggregate.unwind);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
sortExpressions[fixedSortField] = sort.order === 'ASC' ? 1 : -1;
|
|
1692
|
-
});
|
|
1693
|
-
sortClause = { $sort: sortExpressions };
|
|
1694
|
-
addSort = true;
|
|
1695
1485
|
}
|
|
1696
1486
|
}
|
|
1697
1487
|
|
|
1698
|
-
// Combine flat conditions with AND/OR groups
|
|
1699
1488
|
const topLevelAndParts = [];
|
|
1489
|
+
if (hasFlat) topLevelAndParts.push(flatMatchConditions);
|
|
1700
1490
|
|
|
1701
|
-
if (
|
|
1702
|
-
topLevelAndParts.push(flatMatchConditions);
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
if (input.AND && input.AND.length > 0) {
|
|
1491
|
+
if (input.AND?.length > 0) {
|
|
1706
1492
|
for (const group of input.AND) {
|
|
1707
|
-
const groupMatch = await buildFilterGroupMatch(
|
|
1708
|
-
|
|
1709
|
-
);
|
|
1710
|
-
if (groupMatch) {
|
|
1711
|
-
topLevelAndParts.push(groupMatch);
|
|
1712
|
-
}
|
|
1493
|
+
const groupMatch = await buildFilterGroupMatch(group, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1494
|
+
if (groupMatch) topLevelAndParts.push(groupMatch);
|
|
1713
1495
|
}
|
|
1714
1496
|
}
|
|
1715
1497
|
|
|
1716
|
-
if (input.OR
|
|
1498
|
+
if (input.OR?.length > 0) {
|
|
1717
1499
|
const orParts = [];
|
|
1718
1500
|
for (const group of input.OR) {
|
|
1719
|
-
const groupMatch = await buildFilterGroupMatch(
|
|
1720
|
-
|
|
1721
|
-
);
|
|
1722
|
-
if (groupMatch) {
|
|
1723
|
-
orParts.push(groupMatch);
|
|
1724
|
-
}
|
|
1501
|
+
const groupMatch = await buildFilterGroupMatch(group, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1502
|
+
if (groupMatch) orParts.push(groupMatch);
|
|
1725
1503
|
}
|
|
1726
1504
|
if (orParts.length === 1) {
|
|
1727
1505
|
topLevelAndParts.push(orParts[0]);
|
|
@@ -1730,310 +1508,170 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1730
1508
|
}
|
|
1731
1509
|
}
|
|
1732
1510
|
|
|
1733
|
-
if (topLevelAndParts.length === 1) {
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1511
|
+
if (topLevelAndParts.length === 1) return { $match: topLevelAndParts[0] };
|
|
1512
|
+
if (topLevelAndParts.length > 1) return { $match: { $and: topLevelAndParts } };
|
|
1513
|
+
return null;
|
|
1514
|
+
};
|
|
1738
1515
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1516
|
+
const buildSortClause = (sortTerms, gqltype, aggregateClauses, aggregationsIncluded) => {
|
|
1517
|
+
const sortExpressions = {};
|
|
1518
|
+
const fields = gqltype.getFields();
|
|
1742
1519
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1520
|
+
sortTerms.forEach((sort) => {
|
|
1521
|
+
assertValidFilterPath(sort.field);
|
|
1522
|
+
let fixedSortField = sort.field;
|
|
1523
|
+
if (sort.field.indexOf('.') >= 0) {
|
|
1524
|
+
const sortParts = sort.field.split('.');
|
|
1525
|
+
fixedSortField = sortParts[0];
|
|
1526
|
+
for (let i = 1; i < sortParts.length - 1; i++) {
|
|
1527
|
+
fixedSortField += `_${sortParts[i]}`;
|
|
1528
|
+
}
|
|
1529
|
+
fixedSortField += `.${sortParts[sortParts.length - 1]}`;
|
|
1530
|
+
const qlField = fields[sortParts[0]];
|
|
1531
|
+
const path = sort.field.slice(sort.field.indexOf('.') + 1);
|
|
1532
|
+
const sortAggregations = buildAggregationsForSort({ terms: [{ path }] }, qlField, sortParts[0]);
|
|
1533
|
+
for (const [prop, aggregate] of Object.entries(sortAggregations)) {
|
|
1534
|
+
if (!aggregationsIncluded[prop]) {
|
|
1535
|
+
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
sortExpressions[fixedSortField] = sort.order === 'ASC' ? 1 : -1;
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
return { $sort: sortExpressions };
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
const buildQuery = async (input, gqltype, isCount) => {
|
|
1546
|
+
const aggregateClauses = [];
|
|
1547
|
+
const aggregationsIncluded = {};
|
|
1548
|
+
|
|
1549
|
+
const matchStage = await collectFiltersAndLookups(input, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1550
|
+
if (matchStage) aggregateClauses.push(matchStage);
|
|
1747
1551
|
|
|
1748
1552
|
if (isCount) {
|
|
1749
1553
|
aggregateClauses.push({ $count: 'size' });
|
|
1554
|
+
return aggregateClauses;
|
|
1750
1555
|
}
|
|
1751
1556
|
|
|
1557
|
+
if (input.sort) {
|
|
1558
|
+
aggregateClauses.push(buildSortClause(input.sort.terms, gqltype, aggregateClauses, aggregationsIncluded));
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
let skipClause = { $skip: 0 };
|
|
1562
|
+
let limitClause = { $limit: 100 };
|
|
1563
|
+
if (input.pagination?.page && input.pagination?.size) {
|
|
1564
|
+
skipClause = { $skip: input.pagination.size * (input.pagination.page - 1) };
|
|
1565
|
+
limitClause = { $limit: input.pagination.size };
|
|
1566
|
+
}
|
|
1567
|
+
aggregateClauses.push(skipClause, limitClause);
|
|
1568
|
+
|
|
1752
1569
|
return aggregateClauses;
|
|
1753
1570
|
};
|
|
1754
1571
|
|
|
1755
1572
|
const buildFieldPath = (gqltype, fieldPath) => {
|
|
1756
|
-
|
|
1757
|
-
// and returns the MongoDB field path and any necessary lookups
|
|
1573
|
+
assertValidFilterPath(fieldPath);
|
|
1758
1574
|
const pathParts = fieldPath.split('.');
|
|
1759
|
-
const
|
|
1575
|
+
const lookupPairs = [];
|
|
1760
1576
|
let currentPath = '';
|
|
1761
1577
|
let currentGQLType = gqltype;
|
|
1762
|
-
|
|
1763
|
-
for (
|
|
1764
|
-
const part = pathParts[i];
|
|
1578
|
+
|
|
1579
|
+
for (const part of pathParts) {
|
|
1765
1580
|
const field = currentGQLType.getFields()[part];
|
|
1766
|
-
|
|
1767
1581
|
if (!field) {
|
|
1768
1582
|
throw new Error(`Field ${part} not found in type ${currentGQLType.name}`);
|
|
1769
1583
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
if ((fieldType instanceof GraphQLObjectType) &&
|
|
1778
|
-
field.extensions && field.extensions.relation &&
|
|
1779
|
-
!field.extensions.relation.embedded) {
|
|
1780
|
-
|
|
1781
|
-
const relatedModel = typesDict.types[fieldType.name].model;
|
|
1782
|
-
const collectionName = relatedModel.collection.collectionName;
|
|
1783
|
-
const connectionField = field.extensions.relation.connectionField || part;
|
|
1784
|
-
|
|
1584
|
+
|
|
1585
|
+
const fieldType = unwrapListAndNonNull(field.type);
|
|
1586
|
+
const relation = field.extensions?.relation;
|
|
1587
|
+
|
|
1588
|
+
if (fieldType instanceof GraphQLObjectType && relation && !relation.embedded) {
|
|
1589
|
+
const { collectionName } = typesDict.types[fieldType.name].model.collection;
|
|
1590
|
+
const connField = relation.connectionField || part;
|
|
1785
1591
|
const lookupAlias = currentPath ? `${currentPath}_${part}` : part;
|
|
1786
|
-
const localField = currentPath ? `${currentPath}.${
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
localField,
|
|
1793
|
-
as: lookupAlias,
|
|
1794
|
-
},
|
|
1795
|
-
});
|
|
1796
|
-
|
|
1797
|
-
aggregateClauses.push({
|
|
1798
|
-
$unwind: { path: `$${lookupAlias}`, preserveNullAndEmptyArrays: true },
|
|
1799
|
-
});
|
|
1800
|
-
|
|
1592
|
+
const localField = currentPath ? `${currentPath}.${connField}` : connField;
|
|
1593
|
+
|
|
1594
|
+
lookupPairs.push(buildRelationLookup({
|
|
1595
|
+
collectionName, localField, foreignField: '_id', alias: lookupAlias,
|
|
1596
|
+
}));
|
|
1597
|
+
|
|
1801
1598
|
currentPath = lookupAlias;
|
|
1802
1599
|
currentGQLType = fieldType;
|
|
1803
|
-
} else if (fieldType instanceof GraphQLObjectType &&
|
|
1804
|
-
field.extensions && field.extensions.relation &&
|
|
1805
|
-
field.extensions.relation.embedded) {
|
|
1806
|
-
// Embedded object - just append to path
|
|
1600
|
+
} else if (fieldType instanceof GraphQLObjectType && relation?.embedded) {
|
|
1807
1601
|
currentPath = currentPath ? `${currentPath}.${part}` : part;
|
|
1808
1602
|
currentGQLType = fieldType;
|
|
1809
1603
|
} else {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1604
|
+
const leaf = part === 'id' ? '_id' : part;
|
|
1605
|
+
currentPath = currentPath ? `${currentPath}.${leaf}` : leaf;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
return { mongoPath: currentPath, lookupPairs };
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
const appendLookupPairs = (aggregateClauses, aggregationsIncluded, lookupPairs) => {
|
|
1613
|
+
for (const { lookup, unwind } of lookupPairs) {
|
|
1614
|
+
const alias = lookup.$lookup.as;
|
|
1615
|
+
if (!aggregationsIncluded[alias]) {
|
|
1616
|
+
aggregateClauses.push(lookup, unwind);
|
|
1617
|
+
aggregationsIncluded[alias] = true;
|
|
1816
1618
|
}
|
|
1817
1619
|
}
|
|
1818
|
-
|
|
1819
|
-
return { mongoPath: currentPath, lookups: aggregateClauses };
|
|
1820
1620
|
};
|
|
1821
1621
|
|
|
1822
1622
|
const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
1823
1623
|
const aggregateClauses = [];
|
|
1824
|
-
const flatMatchConditions = {};
|
|
1825
|
-
let hasFlat = false;
|
|
1826
1624
|
const aggregationsIncluded = {};
|
|
1827
|
-
const sortTerms = []; // Store multiple sort terms
|
|
1828
|
-
let limitClause = null;
|
|
1829
|
-
let skipClause = null;
|
|
1830
|
-
|
|
1831
|
-
// Build filter and lookup clauses (similar to buildQuery)
|
|
1832
|
-
for (const [key, filterField] of Object.entries(input)) {
|
|
1833
|
-
if (Object.prototype.hasOwnProperty.call(input, key) && !RESERVED_QUERY_KEYS.has(key)) {
|
|
1834
|
-
const qlField = gqltype.getFields()[key];
|
|
1835
1625
|
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
if (result) {
|
|
1839
|
-
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1840
|
-
aggregateClauses.push(aggregate.lookup);
|
|
1841
|
-
aggregateClauses.push(aggregate.unwind);
|
|
1842
|
-
aggregationsIncluded[prop] = true;
|
|
1843
|
-
}
|
|
1626
|
+
const matchStage = await collectFiltersAndLookups(input, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1627
|
+
if (matchStage) aggregateClauses.push(matchStage);
|
|
1844
1628
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
if (Object.prototype.hasOwnProperty.call(matchClause, matchKey)) {
|
|
1849
|
-
flatMatchConditions[matchKey] = match;
|
|
1850
|
-
hasFlat = true;
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
} else if (key === 'sort' && filterField && filterField.terms && filterField.terms.length > 0) {
|
|
1857
|
-
// Extract all sort terms
|
|
1858
|
-
filterField.terms.forEach(sortTerm => {
|
|
1859
|
-
sortTerms.push({
|
|
1860
|
-
field: sortTerm.field || 'groupId',
|
|
1861
|
-
direction: sortTerm.order === 'ASC' ? 1 : -1,
|
|
1862
|
-
});
|
|
1863
|
-
});
|
|
1864
|
-
} else if (key === 'pagination' && filterField) {
|
|
1865
|
-
// Handle pagination (ignore count parameter)
|
|
1866
|
-
if (filterField.page && filterField.size) {
|
|
1867
|
-
const skip = filterField.size * (filterField.page - 1);
|
|
1868
|
-
limitClause = { $limit: filterField.size + skip };
|
|
1869
|
-
skipClause = { $skip: skip };
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
// Combine flat conditions with AND/OR groups
|
|
1875
|
-
const topLevelAndParts = [];
|
|
1876
|
-
|
|
1877
|
-
if (hasFlat) {
|
|
1878
|
-
topLevelAndParts.push(flatMatchConditions);
|
|
1879
|
-
}
|
|
1629
|
+
const { groupId, facts } = aggregationExpression;
|
|
1630
|
+
const groupIdPath = buildFieldPath(gqltype, groupId);
|
|
1631
|
+
appendLookupPairs(aggregateClauses, aggregationsIncluded, groupIdPath.lookupPairs);
|
|
1880
1632
|
|
|
1881
|
-
|
|
1882
|
-
for (const group of input.AND) {
|
|
1883
|
-
const groupMatch = await buildFilterGroupMatch(
|
|
1884
|
-
group, gqltype, aggregateClauses, aggregationsIncluded,
|
|
1885
|
-
);
|
|
1886
|
-
if (groupMatch) {
|
|
1887
|
-
topLevelAndParts.push(groupMatch);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1633
|
+
const groupStage = { $group: { _id: `$${groupIdPath.mongoPath}` } };
|
|
1891
1634
|
|
|
1892
|
-
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
const groupMatch = await buildFilterGroupMatch(
|
|
1896
|
-
group, gqltype, aggregateClauses, aggregationsIncluded,
|
|
1897
|
-
);
|
|
1898
|
-
if (groupMatch) {
|
|
1899
|
-
orParts.push(groupMatch);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
if (orParts.length === 1) {
|
|
1903
|
-
topLevelAndParts.push(orParts[0]);
|
|
1904
|
-
} else if (orParts.length > 1) {
|
|
1905
|
-
topLevelAndParts.push({ $or: orParts });
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1635
|
+
facts.forEach((fact) => {
|
|
1636
|
+
const factPath = buildFieldPath(gqltype, fact.path);
|
|
1637
|
+
appendLookupPairs(aggregateClauses, aggregationsIncluded, factPath.lookupPairs);
|
|
1908
1638
|
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
aggregateClauses.push({ $match: { $and: topLevelAndParts } });
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
// Now build the aggregation with $group
|
|
1916
|
-
const { groupId, facts } = aggregationExpression;
|
|
1917
|
-
|
|
1918
|
-
// Resolve the groupId field path
|
|
1919
|
-
const groupIdPath = buildFieldPath(gqltype, groupId);
|
|
1920
|
-
|
|
1921
|
-
// Add any lookups needed for the groupId field
|
|
1922
|
-
groupIdPath.lookups.forEach(lookup => {
|
|
1923
|
-
const lookupKey = Object.keys(lookup)[0];
|
|
1924
|
-
const lookupAlias = lookup[lookupKey].as;
|
|
1925
|
-
if (!aggregationsIncluded[lookupAlias]) {
|
|
1926
|
-
aggregateClauses.push(lookup);
|
|
1927
|
-
// Check if next item is an unwind for this lookup
|
|
1928
|
-
const unwindItem = groupIdPath.lookups[groupIdPath.lookups.indexOf(lookup) + 1];
|
|
1929
|
-
if (unwindItem && unwindItem.$unwind) {
|
|
1930
|
-
aggregateClauses.push(unwindItem);
|
|
1931
|
-
}
|
|
1932
|
-
aggregationsIncluded[lookupAlias] = true;
|
|
1933
|
-
}
|
|
1934
|
-
});
|
|
1935
|
-
|
|
1936
|
-
// Build the $group stage
|
|
1937
|
-
const groupStage = {
|
|
1938
|
-
$group: {
|
|
1939
|
-
_id: `$${groupIdPath.mongoPath}`,
|
|
1940
|
-
},
|
|
1941
|
-
};
|
|
1942
|
-
|
|
1943
|
-
// Add aggregation operations for each fact
|
|
1944
|
-
facts.forEach(fact => {
|
|
1945
|
-
const { operation, factName, path } = fact;
|
|
1946
|
-
const factPath = buildFieldPath(gqltype, path);
|
|
1947
|
-
|
|
1948
|
-
// Add any lookups needed for the fact field
|
|
1949
|
-
factPath.lookups.forEach(lookup => {
|
|
1950
|
-
const lookupKey = Object.keys(lookup)[0];
|
|
1951
|
-
const lookupAlias = lookup[lookupKey].as;
|
|
1952
|
-
if (!aggregationsIncluded[lookupAlias]) {
|
|
1953
|
-
aggregateClauses.push(lookup);
|
|
1954
|
-
// Check if next item is an unwind for this lookup
|
|
1955
|
-
const unwindItem = factPath.lookups[factPath.lookups.indexOf(lookup) + 1];
|
|
1956
|
-
if (unwindItem && unwindItem.$unwind) {
|
|
1957
|
-
aggregateClauses.push(unwindItem);
|
|
1958
|
-
}
|
|
1959
|
-
aggregationsIncluded[lookupAlias] = true;
|
|
1960
|
-
}
|
|
1961
|
-
});
|
|
1962
|
-
|
|
1963
|
-
// Map GraphQL operations to MongoDB aggregation operators
|
|
1964
|
-
let mongoOperation;
|
|
1965
|
-
switch (operation) {
|
|
1966
|
-
case 'SUM':
|
|
1967
|
-
mongoOperation = { $sum: `$${factPath.mongoPath}` };
|
|
1968
|
-
break;
|
|
1969
|
-
case 'COUNT':
|
|
1970
|
-
mongoOperation = { $sum: 1 };
|
|
1971
|
-
break;
|
|
1972
|
-
case 'AVG':
|
|
1973
|
-
mongoOperation = { $avg: `$${factPath.mongoPath}` };
|
|
1974
|
-
break;
|
|
1975
|
-
case 'MIN':
|
|
1976
|
-
mongoOperation = { $min: `$${factPath.mongoPath}` };
|
|
1977
|
-
break;
|
|
1978
|
-
case 'MAX':
|
|
1979
|
-
mongoOperation = { $max: `$${factPath.mongoPath}` };
|
|
1980
|
-
break;
|
|
1981
|
-
default:
|
|
1982
|
-
throw new Error(`Unknown aggregation operation: ${operation}`);
|
|
1639
|
+
const builder = AGG_OP_TO_MONGO[fact.operation];
|
|
1640
|
+
if (!builder) {
|
|
1641
|
+
throw new Error(`Unknown aggregation operation: ${fact.operation}`);
|
|
1983
1642
|
}
|
|
1984
|
-
|
|
1985
|
-
groupStage.$group[factName] = mongoOperation;
|
|
1643
|
+
groupStage.$group[fact.factName] = builder(factPath.mongoPath);
|
|
1986
1644
|
});
|
|
1987
|
-
|
|
1645
|
+
|
|
1988
1646
|
aggregateClauses.push(groupStage);
|
|
1989
|
-
|
|
1990
|
-
// Add a final projection stage to format the output
|
|
1647
|
+
|
|
1991
1648
|
aggregateClauses.push({
|
|
1992
1649
|
$project: {
|
|
1993
1650
|
_id: 0,
|
|
1994
1651
|
groupId: '$_id',
|
|
1995
|
-
facts: Object.fromEntries(facts.map(fact => [fact.factName, `$${fact.factName}`])),
|
|
1652
|
+
facts: Object.fromEntries(facts.map((fact) => [fact.factName, `$${fact.factName}`])),
|
|
1996
1653
|
},
|
|
1997
1654
|
});
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
if (sortTerms
|
|
1655
|
+
|
|
1656
|
+
const sortTerms = input.sort?.terms?.length > 0 ? input.sort.terms : null;
|
|
1657
|
+
if (sortTerms) {
|
|
1658
|
+
const factNames = facts.map((fact) => fact.factName);
|
|
2001
1659
|
const sortObject = {};
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
if (sortTerm.field !== 'groupId') {
|
|
2008
|
-
// Check if the field is one of the fact names
|
|
2009
|
-
if (factNames.includes(sortTerm.field)) {
|
|
2010
|
-
sortFieldPath = `facts.${sortTerm.field}`;
|
|
2011
|
-
}
|
|
2012
|
-
// If not found, default to groupId (already set)
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
sortObject[sortFieldPath] = sortTerm.direction;
|
|
2016
|
-
});
|
|
2017
|
-
|
|
2018
|
-
// Add sort stage with all sort fields
|
|
2019
|
-
aggregateClauses.push({
|
|
2020
|
-
$sort: sortObject,
|
|
1660
|
+
sortTerms.forEach((sortTerm) => {
|
|
1661
|
+
const field = sortTerm.field || 'groupId';
|
|
1662
|
+
const sortFieldPath = factNames.includes(field) ? `facts.${field}` : 'groupId';
|
|
1663
|
+
sortObject[sortFieldPath] = sortTerm.order === 'ASC' ? 1 : -1;
|
|
2021
1664
|
});
|
|
1665
|
+
aggregateClauses.push({ $sort: sortObject });
|
|
2022
1666
|
} else {
|
|
2023
|
-
|
|
2024
|
-
aggregateClauses.push({
|
|
2025
|
-
$sort: { groupId: 1 },
|
|
2026
|
-
});
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
// Add pagination if provided
|
|
2030
|
-
if (limitClause) {
|
|
2031
|
-
aggregateClauses.push(limitClause);
|
|
1667
|
+
aggregateClauses.push({ $sort: { groupId: 1 } });
|
|
2032
1668
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
1669
|
+
|
|
1670
|
+
if (input.pagination?.page && input.pagination?.size) {
|
|
1671
|
+
const skip = input.pagination.size * (input.pagination.page - 1);
|
|
1672
|
+
aggregateClauses.push({ $skip: skip }, { $limit: input.pagination.size });
|
|
2035
1673
|
}
|
|
2036
|
-
|
|
1674
|
+
|
|
2037
1675
|
return aggregateClauses;
|
|
2038
1676
|
};
|
|
2039
1677
|
|
|
@@ -2046,7 +1684,6 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2046
1684
|
if (!shouldNotBeIncludedInSchema(includedTypes, type.gqltype)) {
|
|
2047
1685
|
const wasAddedAsNoEnpointType = !type.simpleEntityEndpointName;
|
|
2048
1686
|
if (!wasAddedAsNoEnpointType) {
|
|
2049
|
-
// Fixing resolve method in order to be compliant with Mongo _id field
|
|
2050
1687
|
if (type.gqltype.getFields().id && !type.gqltype.getFields().id.resolve) {
|
|
2051
1688
|
type.gqltype.getFields().id.resolve = (parent) => parent._id;
|
|
2052
1689
|
}
|
|
@@ -2055,54 +1692,26 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2055
1692
|
type: type.gqltype,
|
|
2056
1693
|
args: { id: { type: GraphQLID } },
|
|
2057
1694
|
async resolve(parent, args, context) {
|
|
2058
|
-
/* Here we define how to get data from database source
|
|
2059
|
-
this will return the type with id passed in argument
|
|
2060
|
-
by the user */
|
|
2061
1695
|
const params = {
|
|
2062
|
-
type,
|
|
2063
|
-
args,
|
|
2064
|
-
operation: 'get_by_id',
|
|
2065
|
-
context,
|
|
1696
|
+
type, args, operation: 'get_by_id', context,
|
|
2066
1697
|
};
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
args: queryArgs,
|
|
2082
|
-
operation: 'get_by_id',
|
|
2083
|
-
context,
|
|
2084
|
-
};
|
|
2085
|
-
|
|
2086
|
-
// Execute scope which will modify queryArgs in place
|
|
2087
|
-
await executeScope(scopeParams);
|
|
2088
|
-
|
|
2089
|
-
// Build aggregation pipeline from the combined filters
|
|
2090
|
-
const aggregateClauses = await buildQuery(queryArgs, type.gqltype);
|
|
2091
|
-
|
|
2092
|
-
// Execute the query and get the first result
|
|
2093
|
-
let result;
|
|
2094
|
-
if (aggregateClauses.length === 0) {
|
|
2095
|
-
result = await type.model.findOne({ _id: args.id });
|
|
2096
|
-
} else {
|
|
2097
|
-
const results = await type.model.aggregate(aggregateClauses);
|
|
2098
|
-
result = results.length > 0 ? results[0] : null;
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
return result;
|
|
2102
|
-
} else {
|
|
2103
|
-
// No scope defined, use the original findById
|
|
2104
|
-
return await type.model.findById(args.id);
|
|
1698
|
+
await executeMiddleware(params);
|
|
1699
|
+
|
|
1700
|
+
const hasScope = type.gqltype.extensions?.scope?.get_by_id;
|
|
1701
|
+
if (!hasScope) {
|
|
1702
|
+
return type.model.findById(args.id);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
const queryArgs = { id: { operator: 'EQ', value: args.id } };
|
|
1706
|
+
await executeScope({
|
|
1707
|
+
type, args: queryArgs, operation: 'get_by_id', context,
|
|
1708
|
+
});
|
|
1709
|
+
const aggregateClauses = await buildQuery(queryArgs, type.gqltype);
|
|
1710
|
+
if (aggregateClauses.length === 0) {
|
|
1711
|
+
return type.model.findOne({ _id: args.id });
|
|
2105
1712
|
}
|
|
1713
|
+
const results = await type.model.aggregate(aggregateClauses);
|
|
1714
|
+
return results[0] || null;
|
|
2106
1715
|
},
|
|
2107
1716
|
};
|
|
2108
1717
|
|
|
@@ -2120,26 +1729,26 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2120
1729
|
operation: 'find',
|
|
2121
1730
|
context,
|
|
2122
1731
|
};
|
|
2123
|
-
|
|
1732
|
+
await executeMiddleware(params);
|
|
2124
1733
|
await executeScope(params);
|
|
2125
1734
|
const aggregateClauses = await buildQuery(args, type.gqltype);
|
|
2126
|
-
|
|
2127
|
-
const aggregateClausesForCount = await buildQuery(args, type.gqltype, true);
|
|
2128
|
-
const resultCount = await type.model.aggregate(aggregateClausesForCount);
|
|
2129
|
-
context.count = resultCount[0] ? resultCount[0].size : 0;
|
|
2130
|
-
}
|
|
1735
|
+
const wantsCount = !!(args.pagination && args.pagination.count);
|
|
2131
1736
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
1737
|
+
const dataPromise = aggregateClauses.length === 0
|
|
1738
|
+
? type.model.find({})
|
|
1739
|
+
: type.model.aggregate(aggregateClauses);
|
|
1740
|
+
const countPromise = wantsCount
|
|
1741
|
+
? buildQuery(args, type.gqltype, true).then((p) => type.model.aggregate(p))
|
|
1742
|
+
: null;
|
|
1743
|
+
|
|
1744
|
+
const [result, resultCount] = await Promise.all([dataPromise, countPromise]);
|
|
1745
|
+
if (wantsCount) {
|
|
1746
|
+
context.count = resultCount[0] ? resultCount[0].size : 0;
|
|
2137
1747
|
}
|
|
2138
1748
|
return result;
|
|
2139
1749
|
},
|
|
2140
1750
|
};
|
|
2141
1751
|
|
|
2142
|
-
// Add aggregate endpoint
|
|
2143
1752
|
const aggregateArgsObject = { ...argsObject };
|
|
2144
1753
|
aggregateArgsObject.aggregation = {
|
|
2145
1754
|
type: new GraphQLNonNull(QLTypeAggregationExpression),
|
|
@@ -2155,7 +1764,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2155
1764
|
operation: 'aggregate',
|
|
2156
1765
|
context,
|
|
2157
1766
|
};
|
|
2158
|
-
|
|
1767
|
+
await executeMiddleware(params);
|
|
2159
1768
|
await executeScope(params);
|
|
2160
1769
|
const aggregateClauses = await buildAggregationQuery(args, type.gqltype, args.aggregation);
|
|
2161
1770
|
const result = await type.model.aggregate(aggregateClauses);
|
|
@@ -2169,33 +1778,24 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2169
1778
|
return new GraphQLObjectType(rootQueryArgs);
|
|
2170
1779
|
};
|
|
2171
1780
|
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
export const createSchema = (includedQueryTypes,
|
|
2175
|
-
includedMutationTypes, includedCustomMutations) => {
|
|
2176
|
-
|
|
2177
|
-
// Generate models for all registered types now that all types are available
|
|
2178
|
-
Object.values(typesDict.types).forEach(typeInfo => {
|
|
1781
|
+
export const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomMutations) => {
|
|
1782
|
+
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
2179
1783
|
if (typeInfo.gqltype && !typeInfo.model) {
|
|
2180
1784
|
if (typeInfo.endpoint) {
|
|
2181
|
-
// Generate model with collection for endpoint types (types registered with connect)
|
|
2182
1785
|
typeInfo.model = generateModel(typeInfo.gqltype, typeInfo.onModelCreated);
|
|
2183
1786
|
} else if (typeInfo.needsModel) {
|
|
2184
|
-
|
|
2185
|
-
typeInfo.model = generateModelWithoutCollection(typeInfo.gqltype, null);
|
|
1787
|
+
typeInfo.model = generateModel(typeInfo.gqltype, null, { createCollection: false });
|
|
2186
1788
|
}
|
|
2187
1789
|
}
|
|
2188
1790
|
});
|
|
2189
1791
|
|
|
2190
|
-
|
|
2191
|
-
Object.keys(typesDict.types).forEach(typeName => {
|
|
1792
|
+
Object.keys(typesDict.types).forEach((typeName) => {
|
|
2192
1793
|
if (typesDictForUpdate.types[typeName]) {
|
|
2193
1794
|
typesDictForUpdate.types[typeName].model = typesDict.types[typeName].model;
|
|
2194
1795
|
}
|
|
2195
1796
|
});
|
|
2196
1797
|
|
|
2197
|
-
|
|
2198
|
-
Object.values(typesDict.types).forEach(typeInfo => {
|
|
1798
|
+
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
2199
1799
|
if (typeInfo.gqltype) {
|
|
2200
1800
|
autoGenerateResolvers(typeInfo.gqltype);
|
|
2201
1801
|
}
|
|
@@ -2213,7 +1813,6 @@ export const getType = (typeName) => {
|
|
|
2213
1813
|
if (typeof typeName === 'string') {
|
|
2214
1814
|
return typesDict.types[typeName]?.gqltype;
|
|
2215
1815
|
}
|
|
2216
|
-
// If it's already a GraphQL type object, get by its name
|
|
2217
1816
|
if (typeName && typeName.name) {
|
|
2218
1817
|
return typesDict.types[typeName.name]?.gqltype;
|
|
2219
1818
|
}
|
|
@@ -2233,68 +1832,41 @@ const autoGenerateResolvers = (gqltype) => {
|
|
|
2233
1832
|
const fields = gqltype.getFields();
|
|
2234
1833
|
|
|
2235
1834
|
for (const [fieldName, fieldEntry] of Object.entries(fields)) {
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
delete argsObject[connectionField];
|
|
2253
|
-
const argsArray = Object.entries(argsObject);
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
const graphqlArgs = formatArgs(argsArray);
|
|
2257
|
-
|
|
2258
|
-
fieldEntry.args = graphqlArgs;
|
|
2259
|
-
|
|
2260
|
-
fieldEntry.resolve = async (parent, args) => {
|
|
2261
|
-
// Lazy lookup of the related model
|
|
2262
|
-
|
|
2263
|
-
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
2264
|
-
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
args[connectionField] = {
|
|
2268
|
-
terms: [{
|
|
2269
|
-
path: 'id',
|
|
2270
|
-
operator: 'EQ',
|
|
2271
|
-
value: parent.id || parent._id,
|
|
2272
|
-
}],
|
|
2273
|
-
};
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
const aggregateClauses = await buildQuery(args, relatedTypeInfo.gqltype);
|
|
2277
|
-
|
|
2278
|
-
return await relatedTypeInfo.model.aggregate(aggregateClauses);
|
|
2279
|
-
};
|
|
2280
|
-
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
2281
|
-
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
|
|
2282
|
-
// Single object field - generate resolve for one-to-one relationship
|
|
2283
|
-
const relatedType = fieldEntry.type instanceof GraphQLNonNull ? fieldEntry.type.ofType : fieldEntry.type;
|
|
2284
|
-
const connectionField = relation.connectionField || fieldName;
|
|
2285
|
-
|
|
2286
|
-
fieldEntry.resolve = async (parent) => {
|
|
2287
|
-
// Lazy lookup of the related model
|
|
2288
|
-
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
2289
|
-
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
2290
|
-
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
2291
|
-
}
|
|
2292
|
-
const relatedId = parent[connectionField] || parent[fieldName];
|
|
2293
|
-
return relatedId ? await relatedTypeInfo.model.findById(relatedId) : null;
|
|
2294
|
-
};
|
|
2295
|
-
}
|
|
1835
|
+
if (fieldEntry.resolve) continue;
|
|
1836
|
+
const relation = fieldEntry.extensions?.relation;
|
|
1837
|
+
if (!relation || relation.embedded) continue;
|
|
1838
|
+
|
|
1839
|
+
if (fieldEntry.type instanceof GraphQLList) {
|
|
1840
|
+
const relatedType = fieldEntry.type.ofType;
|
|
1841
|
+
const connectionField = relation.connectionField || fieldName;
|
|
1842
|
+
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
1843
|
+
const argsObject = createArgsForQuery(relatedTypeInfo.gqltype.getFields());
|
|
1844
|
+
delete argsObject[connectionField];
|
|
1845
|
+
|
|
1846
|
+
fieldEntry.args = formatArgs(Object.entries(argsObject));
|
|
1847
|
+
fieldEntry.resolve = async (parent, args) => {
|
|
1848
|
+
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
1849
|
+
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
2296
1850
|
}
|
|
2297
|
-
|
|
1851
|
+
args[connectionField] = {
|
|
1852
|
+
terms: [{ path: 'id', operator: 'EQ', value: parent.id || parent._id }],
|
|
1853
|
+
};
|
|
1854
|
+
const aggregateClauses = await buildQuery(args, relatedTypeInfo.gqltype);
|
|
1855
|
+
return relatedTypeInfo.model.aggregate(aggregateClauses);
|
|
1856
|
+
};
|
|
1857
|
+
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
1858
|
+
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
|
|
1859
|
+
const relatedType = unwrapNonNull(fieldEntry.type);
|
|
1860
|
+
const connectionField = relation.connectionField || fieldName;
|
|
1861
|
+
|
|
1862
|
+
fieldEntry.resolve = async (parent) => {
|
|
1863
|
+
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
1864
|
+
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
1865
|
+
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
1866
|
+
}
|
|
1867
|
+
const relatedId = parent[connectionField] || parent[fieldName];
|
|
1868
|
+
return relatedId ? relatedTypeInfo.model.findById(relatedId) : null;
|
|
1869
|
+
};
|
|
2298
1870
|
}
|
|
2299
1871
|
}
|
|
2300
1872
|
};
|
|
@@ -2306,43 +1878,36 @@ export const connect = (model, gqltype, simpleEntityEndpointName,
|
|
|
2306
1878
|
gqltype,
|
|
2307
1879
|
};
|
|
2308
1880
|
typesDict.types[gqltype.name] = {
|
|
2309
|
-
model
|
|
1881
|
+
model,
|
|
2310
1882
|
gqltype,
|
|
2311
1883
|
simpleEntityEndpointName,
|
|
2312
1884
|
listEntitiesEndpointName,
|
|
2313
1885
|
endpoint: true,
|
|
2314
1886
|
controller,
|
|
2315
1887
|
stateMachine,
|
|
2316
|
-
onModelCreated,
|
|
1888
|
+
onModelCreated,
|
|
2317
1889
|
};
|
|
2318
1890
|
|
|
2319
1891
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
2320
1892
|
};
|
|
2321
1893
|
|
|
2322
1894
|
export const addNoEndpointType = (gqltype) => {
|
|
2323
|
-
waitingInputType[gqltype.name] = {
|
|
2324
|
-
gqltype,
|
|
2325
|
-
};
|
|
1895
|
+
waitingInputType[gqltype.name] = { gqltype };
|
|
2326
1896
|
|
|
2327
|
-
// Check if this type has relationship fields that might need a model
|
|
2328
1897
|
const fields = gqltype.getFields();
|
|
2329
1898
|
let needsModel = false;
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
1899
|
+
for (const fieldEntry of Object.values(fields)) {
|
|
1900
|
+
if (fieldEntry.extensions?.relation
|
|
1901
|
+
&& (fieldEntry.type instanceof GraphQLObjectType
|
|
1902
|
+
|| fieldEntry.type instanceof GraphQLList
|
|
1903
|
+
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType))) {
|
|
2335
1904
|
needsModel = true;
|
|
2336
1905
|
break;
|
|
2337
1906
|
}
|
|
2338
1907
|
}
|
|
2339
1908
|
|
|
2340
1909
|
typesDict.types[gqltype.name] = {
|
|
2341
|
-
gqltype,
|
|
2342
|
-
endpoint: false,
|
|
2343
|
-
// Model will be generated later in createSchema if needed
|
|
2344
|
-
model: null,
|
|
2345
|
-
needsModel, // Store whether this type needs a model
|
|
1910
|
+
gqltype, endpoint: false, model: null, needsModel,
|
|
2346
1911
|
};
|
|
2347
1912
|
|
|
2348
1913
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|