@simtlix/simfinity-js 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/worktrees/distracted-swirles-4601fe/.claude/settings.local.json +10 -0
- package/.claude/worktrees/distracted-swirles-4601fe/AGGREGATION_CHANGES_SUMMARY.md +235 -0
- package/.claude/worktrees/distracted-swirles-4601fe/AGGREGATION_EXAMPLE.md +567 -0
- package/.claude/worktrees/distracted-swirles-4601fe/LICENSE +201 -0
- package/.claude/worktrees/distracted-swirles-4601fe/README.md +3941 -0
- package/.claude/worktrees/distracted-swirles-4601fe/eslint.config.mjs +71 -0
- package/.claude/worktrees/distracted-swirles-4601fe/package-lock.json +4740 -0
- package/.claude/worktrees/distracted-swirles-4601fe/package.json +41 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/auth/errors.js +44 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/auth/expressions.js +273 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/auth/index.js +391 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/auth/rules.js +274 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLOperator.js +39 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLSort.js +28 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLValue.js +39 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/errors/internal-server.error.js +11 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/errors/simfinity.error.js +15 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/index.js +1925 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/plugins.js +53 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/scalars.js +188 -0
- package/.claude/worktrees/distracted-swirles-4601fe/src/validators.js +250 -0
- package/.claude/worktrees/distracted-swirles-4601fe/yarn.lock +1154 -0
- package/package.json +1 -1
- package/src/index.js +531 -1018
- 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',
|
|
@@ -48,7 +47,6 @@ const GraphQLJSON = new GraphQLScalarType({
|
|
|
48
47
|
},
|
|
49
48
|
});
|
|
50
49
|
|
|
51
|
-
// Adding 'extensions' field into instronspection query
|
|
52
50
|
const RelationType = new GraphQLObjectType({
|
|
53
51
|
name: 'RelationType',
|
|
54
52
|
fields: () => ({
|
|
@@ -82,7 +80,6 @@ const fixedFieldsWithExtensions = () => {
|
|
|
82
80
|
};
|
|
83
81
|
|
|
84
82
|
__Field._fields = fixedFieldsWithExtensions;
|
|
85
|
-
// End of adding 'extensions' field to instrospection query
|
|
86
83
|
|
|
87
84
|
const typesDict = { types: {} };
|
|
88
85
|
const waitingInputType = {};
|
|
@@ -133,9 +130,6 @@ export const preventCreatingCollection = (prevent) => {
|
|
|
133
130
|
preventCollectionCreation = !!prevent;
|
|
134
131
|
};
|
|
135
132
|
|
|
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
133
|
const QLFilter = new GraphQLInputObjectType({
|
|
140
134
|
name: 'QLFilter',
|
|
141
135
|
fields: () => ({
|
|
@@ -263,17 +257,76 @@ const getEffectiveTypeName = (type) => {
|
|
|
263
257
|
|
|
264
258
|
const isGraphQLisoDate = (typeName) => typeName === 'DateTime' || typeName === 'Date' || typeName === 'Time';
|
|
265
259
|
|
|
260
|
+
const unwrapNonNull = (type) => (type instanceof GraphQLNonNull ? type.ofType : type);
|
|
261
|
+
|
|
262
|
+
const unwrapListAndNonNull = (type) => {
|
|
263
|
+
if (type instanceof GraphQLList || type instanceof GraphQLNonNull) {
|
|
264
|
+
return type.ofType;
|
|
265
|
+
}
|
|
266
|
+
return type;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const coerceDateValue = (fieldType, holder, key) => {
|
|
270
|
+
const typeName = getEffectiveTypeName(unwrapNonNull(fieldType));
|
|
271
|
+
if (!isGraphQLisoDate(typeName)) return;
|
|
272
|
+
const raw = holder[key];
|
|
273
|
+
if (Array.isArray(raw)) {
|
|
274
|
+
holder[key] = raw.map((v) => v && new Date(v));
|
|
275
|
+
} else {
|
|
276
|
+
holder[key] = raw && new Date(raw);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const isCustomValidatedScalar = (type) => type instanceof GraphQLScalarType && type.baseScalarType;
|
|
281
|
+
|
|
282
|
+
const matchesScalar = (fieldType, target) => {
|
|
283
|
+
if (fieldType === target) return true;
|
|
284
|
+
if (isNonNullOfTypeForNotScalar(fieldType, target)) return true;
|
|
285
|
+
if (isCustomValidatedScalar(fieldType) && fieldType.baseScalarType === target) return true;
|
|
286
|
+
if (isNonNullOfType(fieldType, GraphQLScalarType)
|
|
287
|
+
&& isCustomValidatedScalar(fieldType.ofType)
|
|
288
|
+
&& fieldType.ofType.baseScalarType === target) return true;
|
|
289
|
+
return false;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const buildRelationLookup = ({
|
|
293
|
+
collectionName, localField, foreignField, alias,
|
|
294
|
+
}) => ({
|
|
295
|
+
lookup: {
|
|
296
|
+
$lookup: {
|
|
297
|
+
from: collectionName, foreignField, localField, as: alias,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
unwind: { $unwind: { path: `$${alias}`, preserveNullAndEmptyArrays: true } },
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const OP_TO_MONGO = {
|
|
304
|
+
LT: (v) => ({ $lt: v }),
|
|
305
|
+
GT: (v) => ({ $gt: v }),
|
|
306
|
+
LTE: (v) => ({ $lte: v }),
|
|
307
|
+
GTE: (v) => ({ $gte: v }),
|
|
308
|
+
NE: (v) => ({ $ne: v }),
|
|
309
|
+
BTW: (v) => ({ $gte: v[0], $lte: v[1] }),
|
|
310
|
+
LIKE: (v) => ({ $regex: `.*${v}.*` }),
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const AGG_OP_TO_MONGO = {
|
|
314
|
+
SUM: (path) => ({ $sum: `$${path}` }),
|
|
315
|
+
COUNT: () => ({ $sum: 1 }),
|
|
316
|
+
AVG: (path) => ({ $avg: `$${path}` }),
|
|
317
|
+
MIN: (path) => ({ $min: `$${path}` }),
|
|
318
|
+
MAX: (path) => ({ $max: `$${path}` }),
|
|
319
|
+
};
|
|
320
|
+
|
|
266
321
|
function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
267
322
|
if (!baseScalarType) {
|
|
268
323
|
throw new Error('baseScalarType is required');
|
|
269
324
|
}
|
|
270
325
|
|
|
271
|
-
// Validate that baseScalarType is a valid GraphQL scalar type
|
|
272
326
|
if (!(baseScalarType instanceof GraphQLScalarType)) {
|
|
273
327
|
throw new Error('baseScalarType must be a valid GraphQL scalar type');
|
|
274
328
|
}
|
|
275
329
|
|
|
276
|
-
// Check if it's one of the standard GraphQL scalar types
|
|
277
330
|
const validScalarTypes = [GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID];
|
|
278
331
|
const isValidStandardType = validScalarTypes.some((type) => baseScalarType === type);
|
|
279
332
|
|
|
@@ -286,10 +339,9 @@ function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
|
286
339
|
Int: Kind.INT,
|
|
287
340
|
Float: Kind.FLOAT,
|
|
288
341
|
Boolean: Kind.BOOLEAN,
|
|
289
|
-
ID: Kind.STRING,
|
|
342
|
+
ID: Kind.STRING,
|
|
290
343
|
};
|
|
291
344
|
|
|
292
|
-
// Try to infer the kind from the baseScalarType name
|
|
293
345
|
const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
|
|
294
346
|
|
|
295
347
|
const scalar = new GraphQLScalarType({
|
|
@@ -340,11 +392,8 @@ const createOneToManyInputType = (inputNamePrefix, fieldEntryName,
|
|
|
340
392
|
inputType, updateInputType, connectionField) => {
|
|
341
393
|
let inputTypeForAdd = inputType;
|
|
342
394
|
|
|
343
|
-
// If a gqltype is provided, create a new input type for 'added'
|
|
344
|
-
// that excludes the field named after the gqltype.
|
|
345
395
|
if (connectionField) {
|
|
346
|
-
|
|
347
|
-
inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, fieldToExclude);
|
|
396
|
+
inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, connectionField);
|
|
348
397
|
}
|
|
349
398
|
|
|
350
399
|
return new GraphQLInputObjectType({
|
|
@@ -616,56 +665,54 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
616
665
|
return { modelArgs, collectionFields };
|
|
617
666
|
};
|
|
618
667
|
|
|
619
|
-
const
|
|
668
|
+
const MAX_TRANSIENT_RETRIES = 5;
|
|
669
|
+
|
|
670
|
+
const withTransaction = async (session, body) => {
|
|
620
671
|
const mySession = session || await mongoose.startSession();
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
672
|
+
for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
|
|
673
|
+
await mySession.startTransaction();
|
|
674
|
+
try {
|
|
675
|
+
const result = await body(mySession);
|
|
676
|
+
await mySession.commitTransaction();
|
|
677
|
+
mySession.endSession();
|
|
678
|
+
return result;
|
|
679
|
+
} catch (error) {
|
|
680
|
+
await mySession.abortTransaction();
|
|
681
|
+
const isTransient = error.errorLabels?.includes('TransientTransactionError');
|
|
682
|
+
if (isTransient && attempt < MAX_TRANSIENT_RETRIES) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
mySession.endSession();
|
|
686
|
+
throw error;
|
|
631
687
|
}
|
|
632
|
-
mySession.endSession();
|
|
633
|
-
throw error;
|
|
634
688
|
}
|
|
689
|
+
mySession.endSession();
|
|
690
|
+
throw new SimfinityError('Transaction exceeded retry limit', 'TRANSACTION_RETRY_EXCEEDED', 500);
|
|
635
691
|
};
|
|
636
692
|
|
|
693
|
+
const executeRegisteredMutation = (args, callback, session) => withTransaction(
|
|
694
|
+
session,
|
|
695
|
+
(mySession) => callback(args, mySession),
|
|
696
|
+
);
|
|
697
|
+
|
|
637
698
|
const iterateonCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
|
|
638
|
-
for (const [collectionFieldKey, collectionField] of
|
|
639
|
-
Object.entries(materializedModel.collectionFields)) {
|
|
699
|
+
for (const [collectionFieldKey, collectionField] of Object.entries(materializedModel.collectionFields)) {
|
|
640
700
|
if (collectionField.added) {
|
|
641
|
-
|
|
642
701
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
643
702
|
collectionField.added, operations.SAVE, context);
|
|
644
703
|
}
|
|
645
704
|
if (collectionField.updated) {
|
|
646
|
-
|
|
647
705
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
648
706
|
collectionField.updated, operations.UPDATE, context);
|
|
649
707
|
}
|
|
650
708
|
if (collectionField.deleted) {
|
|
651
|
-
|
|
652
709
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
653
710
|
collectionField.deleted, operations.DELETE, context);
|
|
654
711
|
}
|
|
655
712
|
}
|
|
656
713
|
};
|
|
657
714
|
|
|
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) => {
|
|
715
|
+
const onDelete = async (Model, controller, id, session, context) => {
|
|
669
716
|
const currentObject = await Model.findById({ _id: id }).session(session).lean();
|
|
670
717
|
|
|
671
718
|
if (controller && controller.onDelete) {
|
|
@@ -675,36 +722,49 @@ const onDeleteSubject = async (Model, controller, id, session, context) => {
|
|
|
675
722
|
return Model.findByIdAndDelete({ _id: id }).session(session);
|
|
676
723
|
};
|
|
677
724
|
|
|
725
|
+
const getEmbeddedFieldNames = (gqltype) => {
|
|
726
|
+
const cached = typesDict.types[gqltype.name];
|
|
727
|
+
if (cached && cached.embeddedFieldNames) return cached.embeddedFieldNames;
|
|
728
|
+
const names = [];
|
|
729
|
+
for (const [fieldName, fieldEntry] of Object.entries(gqltype.getFields())) {
|
|
730
|
+
if (fieldEntry.extensions?.relation?.embedded) names.push(fieldName);
|
|
731
|
+
}
|
|
732
|
+
if (cached) cached.embeddedFieldNames = names;
|
|
733
|
+
return names;
|
|
734
|
+
};
|
|
735
|
+
|
|
678
736
|
const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
|
|
679
737
|
const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
|
|
680
738
|
const objectId = args.id;
|
|
681
|
-
|
|
682
|
-
const currentObject = await Model.findById({ _id: objectId }).lean();
|
|
683
|
-
|
|
684
739
|
const argTypes = gqltype.getFields();
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
740
|
+
const embeddedFieldNames = getEmbeddedFieldNames(gqltype);
|
|
741
|
+
|
|
742
|
+
if (embeddedFieldNames.length > 0) {
|
|
743
|
+
const projection = Object.fromEntries(embeddedFieldNames.map((name) => [name, 1]));
|
|
744
|
+
const currentObject = await Model.findById(objectId, projection).lean();
|
|
745
|
+
if (currentObject) {
|
|
746
|
+
for (const fieldEntryName of embeddedFieldNames) {
|
|
747
|
+
const oldObjectData = currentObject[fieldEntryName];
|
|
748
|
+
const newObjectData = materializedModel.modelArgs[fieldEntryName];
|
|
749
|
+
if (newObjectData) {
|
|
750
|
+
if (Array.isArray(oldObjectData) && Array.isArray(newObjectData)) {
|
|
751
|
+
materializedModel.modelArgs[fieldEntryName] = newObjectData;
|
|
752
|
+
} else {
|
|
753
|
+
materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
|
|
754
|
+
}
|
|
696
755
|
}
|
|
697
756
|
}
|
|
698
757
|
}
|
|
758
|
+
}
|
|
699
759
|
|
|
700
|
-
|
|
701
|
-
|
|
760
|
+
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
761
|
+
if (args[fieldEntryName] === null && !(fieldEntry.type instanceof GraphQLNonNull)) {
|
|
702
762
|
materializedModel.modelArgs = {
|
|
703
763
|
...materializedModel.modelArgs,
|
|
704
764
|
$unset: { ...materializedModel.modelArgs.$unset, [fieldEntryName]: '' },
|
|
705
765
|
};
|
|
706
766
|
}
|
|
707
|
-
}
|
|
767
|
+
}
|
|
708
768
|
|
|
709
769
|
if (controller && controller.onUpdating) {
|
|
710
770
|
await controller.onUpdating(objectId, materializedModel.modelArgs, session, context);
|
|
@@ -780,38 +840,23 @@ export const saveObject = async (typeName, args, session, context) => {
|
|
|
780
840
|
return onSaveObject(type.model, type.gqltype, type.controller, args, session, null, context);
|
|
781
841
|
};
|
|
782
842
|
|
|
783
|
-
const executeOperation =
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
await mySession.startTransaction();
|
|
787
|
-
try {
|
|
788
|
-
let newObject = null;
|
|
843
|
+
const executeOperation = (Model, gqltype, controller, args, operation, actionField, session, context) => withTransaction(
|
|
844
|
+
session,
|
|
845
|
+
async (mySession) => {
|
|
789
846
|
switch (operation) {
|
|
790
847
|
case operations.SAVE:
|
|
791
|
-
|
|
792
|
-
break;
|
|
848
|
+
return onSaveObject(Model, gqltype, controller, args, mySession, null, context);
|
|
793
849
|
case operations.UPDATE:
|
|
794
|
-
|
|
795
|
-
break;
|
|
850
|
+
return onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
|
|
796
851
|
case operations.DELETE:
|
|
797
|
-
|
|
798
|
-
break;
|
|
852
|
+
return onDelete(Model, controller, args, mySession, context);
|
|
799
853
|
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);
|
|
854
|
+
return onStateChanged(Model, gqltype, controller, args, mySession, actionField, context);
|
|
855
|
+
default:
|
|
856
|
+
return null;
|
|
810
857
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
};
|
|
858
|
+
},
|
|
859
|
+
);
|
|
815
860
|
|
|
816
861
|
const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
817
862
|
collectionFieldsList, operationType, context) => {
|
|
@@ -840,7 +885,7 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
840
885
|
break;
|
|
841
886
|
case operations.DELETE:
|
|
842
887
|
operationFunction = async (collectionItem) => {
|
|
843
|
-
await
|
|
888
|
+
await onDelete(typesDict.types[collectionGQLType.name].model,
|
|
844
889
|
typesDict.types[collectionGQLType.name].controller, collectionItem, session, context);
|
|
845
890
|
};
|
|
846
891
|
}
|
|
@@ -888,12 +933,7 @@ const executeScope = async (params) => {
|
|
|
888
933
|
return null;
|
|
889
934
|
}
|
|
890
935
|
|
|
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;
|
|
936
|
+
return scopeFunction({ type, args, operation, context });
|
|
897
937
|
};
|
|
898
938
|
|
|
899
939
|
const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
|
|
@@ -1021,102 +1061,64 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1021
1061
|
return new GraphQLObjectType(rootQueryArgs);
|
|
1022
1062
|
};
|
|
1023
1063
|
|
|
1064
|
+
const listItemMatchesScalar = (listType, target) => {
|
|
1065
|
+
const ofType = listType.ofType;
|
|
1066
|
+
return ofType === target
|
|
1067
|
+
|| (isCustomValidatedScalar(ofType) && ofType.baseScalarType === target);
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
const withUnique = (fieldEntry, mongoType) => (fieldEntry.extensions && fieldEntry.extensions.unique
|
|
1071
|
+
? { type: mongoType, unique: true }
|
|
1072
|
+
: mongoType);
|
|
1073
|
+
|
|
1024
1074
|
const generateSchemaDefinition = (gqlType) => {
|
|
1025
1075
|
const argTypes = gqlType.getFields();
|
|
1026
|
-
|
|
1027
1076
|
const schemaArg = {};
|
|
1028
1077
|
|
|
1029
1078
|
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;
|
|
1079
|
+
const { type } = fieldEntry;
|
|
1035
1080
|
|
|
1036
|
-
if (
|
|
1081
|
+
if (matchesScalar(type, GraphQLID)) {
|
|
1037
1082
|
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)) {
|
|
1083
|
+
} else if (matchesScalar(type, GraphQLString)
|
|
1084
|
+
|| type instanceof GraphQLEnumType
|
|
1085
|
+
|| isNonNullOfType(type, GraphQLEnumType)) {
|
|
1086
|
+
schemaArg[fieldEntryName] = withUnique(fieldEntry, String);
|
|
1087
|
+
} else if (matchesScalar(type, GraphQLInt) || matchesScalar(type, GraphQLFloat)) {
|
|
1088
|
+
schemaArg[fieldEntryName] = withUnique(fieldEntry, Number);
|
|
1089
|
+
} else if (matchesScalar(type, GraphQLBoolean)) {
|
|
1076
1090
|
schemaArg[fieldEntryName] = Boolean;
|
|
1077
|
-
} else if (
|
|
1078
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
1091
|
+
} else if (type instanceof GraphQLObjectType || isNonNullOfType(type, GraphQLObjectType)) {
|
|
1079
1092
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1080
1093
|
if (!fieldEntry.extensions.relation.embedded) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1094
|
+
const key = fieldEntry.extensions.relation.connectionField || fieldEntry.name;
|
|
1095
|
+
schemaArg[key] = mongoose.Schema.Types.ObjectId;
|
|
1083
1096
|
} else {
|
|
1084
|
-
|
|
1085
|
-
if (entryType
|
|
1086
|
-
entryType = entryType.ofType;
|
|
1087
|
-
}
|
|
1088
|
-
if (entryType !== gqlType) {
|
|
1089
|
-
schemaArg[fieldEntryName] = generateSchemaDefinition(entryType);
|
|
1090
|
-
} else {
|
|
1097
|
+
const entryType = unwrapNonNull(type);
|
|
1098
|
+
if (entryType === gqlType) {
|
|
1091
1099
|
throw new Error('A type cannot have a field of its same type and embedded');
|
|
1092
1100
|
}
|
|
1101
|
+
schemaArg[fieldEntryName] = generateSchemaDefinition(entryType);
|
|
1093
1102
|
}
|
|
1094
1103
|
}
|
|
1095
|
-
} else if (
|
|
1104
|
+
} else if (type instanceof GraphQLList) {
|
|
1096
1105
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1097
1106
|
if (fieldEntry.extensions.relation.embedded) {
|
|
1098
|
-
|
|
1099
|
-
if (entryType !== gqlType) {
|
|
1100
|
-
schemaArg[fieldEntryName] = [generateSchemaDefinition(entryType)];
|
|
1101
|
-
} else {
|
|
1107
|
+
if (type.ofType === gqlType) {
|
|
1102
1108
|
throw new Error('A type cannot have a field of its same type and embedded');
|
|
1103
1109
|
}
|
|
1110
|
+
schemaArg[fieldEntryName] = [generateSchemaDefinition(type.ofType)];
|
|
1104
1111
|
}
|
|
1105
|
-
} else if (
|
|
1106
|
-
|| fieldEntry.type.ofType instanceof GraphQLEnumType
|
|
1107
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
|
|
1112
|
+
} else if (listItemMatchesScalar(type, GraphQLString) || type.ofType instanceof GraphQLEnumType) {
|
|
1108
1113
|
schemaArg[fieldEntryName] = [String];
|
|
1109
|
-
} else if (
|
|
1110
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
|
|
1114
|
+
} else if (listItemMatchesScalar(type, GraphQLBoolean)) {
|
|
1111
1115
|
schemaArg[fieldEntryName] = [Boolean];
|
|
1112
|
-
} else if (
|
|
1113
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && (getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt || getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat))) {
|
|
1116
|
+
} else if (listItemMatchesScalar(type, GraphQLInt) || listItemMatchesScalar(type, GraphQLFloat)) {
|
|
1114
1117
|
schemaArg[fieldEntryName] = [Number];
|
|
1115
|
-
} else if (isGraphQLisoDate(getEffectiveTypeName(
|
|
1118
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(type.ofType))) {
|
|
1116
1119
|
schemaArg[fieldEntryName] = [Date];
|
|
1117
1120
|
}
|
|
1118
|
-
} else if (isGraphQLisoDate(getEffectiveTypeName(
|
|
1119
|
-
|| (fieldEntry.type instanceof GraphQLNonNull && isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType)))) {
|
|
1121
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(unwrapNonNull(type)))) {
|
|
1120
1122
|
schemaArg[fieldEntryName] = Date;
|
|
1121
1123
|
}
|
|
1122
1124
|
}
|
|
@@ -1131,24 +1133,17 @@ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
|
1131
1133
|
const currentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
1132
1134
|
|
|
1133
1135
|
if (fieldDefinition === mongoose.Schema.Types.ObjectId) {
|
|
1134
|
-
// Direct ObjectId field
|
|
1135
1136
|
objectIdFields.push(currentPath);
|
|
1136
1137
|
} else if (typeof fieldDefinition === 'object' && fieldDefinition !== null) {
|
|
1137
1138
|
if (Array.isArray(fieldDefinition)) {
|
|
1138
|
-
// Array field - check if it's an array of objects
|
|
1139
1139
|
const arrayElement = fieldDefinition[0];
|
|
1140
1140
|
if (typeof arrayElement === 'object' && arrayElement !== null) {
|
|
1141
|
-
|
|
1142
|
-
const nestedObjectIdFields = findObjectIdFields(arrayElement, currentPath);
|
|
1143
|
-
objectIdFields.push(...nestedObjectIdFields);
|
|
1141
|
+
objectIdFields.push(...findObjectIdFields(arrayElement, currentPath));
|
|
1144
1142
|
}
|
|
1145
1143
|
} else if (fieldDefinition.type === mongoose.Schema.Types.ObjectId) {
|
|
1146
|
-
// Object with ObjectId type
|
|
1147
1144
|
objectIdFields.push(currentPath);
|
|
1148
|
-
} else if (
|
|
1149
|
-
|
|
1150
|
-
const nestedObjectIdFields = findObjectIdFields(fieldDefinition, currentPath);
|
|
1151
|
-
objectIdFields.push(...nestedObjectIdFields);
|
|
1145
|
+
} else if (!fieldDefinition.type) {
|
|
1146
|
+
objectIdFields.push(...findObjectIdFields(fieldDefinition, currentPath));
|
|
1152
1147
|
}
|
|
1153
1148
|
}
|
|
1154
1149
|
}
|
|
@@ -1158,356 +1153,185 @@ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
|
1158
1153
|
|
|
1159
1154
|
const createSchemaWithIndexes = (schemaDefinition) => {
|
|
1160
1155
|
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 => {
|
|
1156
|
+
findObjectIdFields(schemaDefinition).forEach((fieldPath) => {
|
|
1167
1157
|
schema.index({ [fieldPath]: 1 });
|
|
1168
1158
|
});
|
|
1169
|
-
|
|
1170
1159
|
return schema;
|
|
1171
1160
|
};
|
|
1172
1161
|
|
|
1173
|
-
const generateModel = (gqlType, onModelCreated) => {
|
|
1162
|
+
const generateModel = (gqlType, onModelCreated, { createCollection = true } = {}) => {
|
|
1174
1163
|
const schemaDefinition = generateSchemaDefinition(gqlType);
|
|
1175
1164
|
const schema = createSchemaWithIndexes(schemaDefinition);
|
|
1176
1165
|
const model = mongoose.model(gqlType.name, schema, gqlType.name);
|
|
1177
1166
|
if (onModelCreated) {
|
|
1178
1167
|
onModelCreated(model);
|
|
1179
1168
|
}
|
|
1180
|
-
if (!preventCollectionCreation) {
|
|
1169
|
+
if (createCollection && !preventCollectionCreation) {
|
|
1181
1170
|
model.createCollection();
|
|
1182
1171
|
}
|
|
1183
1172
|
return model;
|
|
1184
1173
|
};
|
|
1185
1174
|
|
|
1186
|
-
const
|
|
1187
|
-
const schemaDefinition = generateSchemaDefinition(gqlType);
|
|
1188
|
-
const schema = createSchemaWithIndexes(schemaDefinition);
|
|
1189
|
-
const model = mongoose.model(gqlType.name, schema, gqlType.name);
|
|
1190
|
-
if (onModelCreated) {
|
|
1191
|
-
onModelCreated(model);
|
|
1192
|
-
}
|
|
1193
|
-
// Never create collection for no-endpoint types
|
|
1194
|
-
return model;
|
|
1195
|
-
};
|
|
1175
|
+
const coerceIdArray = (value) => value.map((element) => new mongoose.Types.ObjectId(element));
|
|
1196
1176
|
|
|
1197
1177
|
const buildMatchesClause = (fieldname, operator, value) => {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
matches[fieldname] = { $nin: fixedArray };
|
|
1235
|
-
} else if (operator === QLOperator.getValue('LIKE').value) {
|
|
1236
|
-
matches[fieldname] = { $regex: `.*${value}.*` };
|
|
1178
|
+
const isIdField = fieldname.endsWith('_id');
|
|
1179
|
+
const op = operator || 'EQ';
|
|
1180
|
+
|
|
1181
|
+
if (op === 'EQ') {
|
|
1182
|
+
return { [fieldname]: isIdField ? new mongoose.Types.ObjectId(value) : value };
|
|
1183
|
+
}
|
|
1184
|
+
if (op === 'IN') {
|
|
1185
|
+
return { [fieldname]: { $in: value && isIdField ? coerceIdArray(value) : value } };
|
|
1186
|
+
}
|
|
1187
|
+
if (op === 'NIN') {
|
|
1188
|
+
return { [fieldname]: { $nin: value && isIdField ? coerceIdArray(value) : value } };
|
|
1237
1189
|
}
|
|
1190
|
+
const builder = OP_TO_MONGO[op];
|
|
1191
|
+
return builder ? { [fieldname]: builder(value) } : {};
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
const topLevelRelationLookup = (qlField, fieldType, fieldName) => {
|
|
1195
|
+
const { collectionName } = typesDict.types[fieldType.name].model.collection;
|
|
1196
|
+
const connField = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1197
|
+
const isList = qlField.type instanceof GraphQLList;
|
|
1198
|
+
return buildRelationLookup({
|
|
1199
|
+
collectionName,
|
|
1200
|
+
localField: isList ? '_id' : connField,
|
|
1201
|
+
foreignField: isList ? connField : '_id',
|
|
1202
|
+
alias: fieldName,
|
|
1203
|
+
});
|
|
1204
|
+
};
|
|
1238
1205
|
|
|
1239
|
-
|
|
1206
|
+
const nestedRelationLookup = (pathField, pathFieldType, currentPath, aliasPath, pathFieldName) => {
|
|
1207
|
+
const { collectionName } = typesDict.types[pathFieldType.name].model.collection;
|
|
1208
|
+
const connField = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1209
|
+
const isList = pathField.type instanceof GraphQLList;
|
|
1210
|
+
return buildRelationLookup({
|
|
1211
|
+
collectionName,
|
|
1212
|
+
localField: isList ? `${currentPath}._id` : `${currentPath}.${connField}`,
|
|
1213
|
+
foreignField: isList ? connField : '_id',
|
|
1214
|
+
alias: aliasPath,
|
|
1215
|
+
});
|
|
1240
1216
|
};
|
|
1241
1217
|
|
|
1242
1218
|
const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
1243
1219
|
const aggregateClauses = {};
|
|
1220
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1244
1221
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
fieldType = qlField.type.ofType;
|
|
1222
|
+
if (!(fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType))) {
|
|
1223
|
+
return aggregateClauses;
|
|
1248
1224
|
}
|
|
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
1225
|
|
|
1283
|
-
|
|
1284
|
-
lookup,
|
|
1285
|
-
unwind: { $unwind: { path: `$${fieldName}`, preserveNullAndEmptyArrays: true } },
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1226
|
+
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1289
1227
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
}
|
|
1228
|
+
filterField.terms.forEach((term) => {
|
|
1229
|
+
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1230
|
+
&& !aggregateClauses[fieldName]) {
|
|
1231
|
+
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
1232
|
+
}
|
|
1340
1233
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1234
|
+
let currentGQLPathFieldType = unwrapListAndNonNull(qlField.type);
|
|
1235
|
+
let aliasPath = fieldName;
|
|
1236
|
+
let embeddedPath = '';
|
|
1237
|
+
|
|
1238
|
+
term.path.split('.').forEach((pathFieldName) => {
|
|
1239
|
+
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1240
|
+
if (pathField.type instanceof GraphQLObjectType
|
|
1241
|
+
|| pathField.type instanceof GraphQLList
|
|
1242
|
+
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1243
|
+
const pathFieldType = unwrapListAndNonNull(pathField.type);
|
|
1244
|
+
currentGQLPathFieldType = pathFieldType;
|
|
1245
|
+
|
|
1246
|
+
if (pathField.extensions?.relation && !pathField.extensions.relation.embedded) {
|
|
1247
|
+
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1248
|
+
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1249
|
+
embeddedPath = '';
|
|
1250
|
+
|
|
1251
|
+
if (!aggregateClauses[aliasPath]) {
|
|
1252
|
+
aggregateClauses[aliasPath] = nestedRelationLookup(
|
|
1253
|
+
pathField, pathFieldType, currentPath, aliasPath, pathFieldName,
|
|
1254
|
+
);
|
|
1350
1255
|
}
|
|
1256
|
+
} else {
|
|
1257
|
+
embeddedPath = embeddedPath === '' ? pathFieldName : `${embeddedPath}.${pathFieldName}`;
|
|
1351
1258
|
}
|
|
1352
|
-
}
|
|
1259
|
+
}
|
|
1353
1260
|
});
|
|
1354
|
-
}
|
|
1261
|
+
});
|
|
1355
1262
|
return aggregateClauses;
|
|
1356
1263
|
};
|
|
1357
1264
|
|
|
1358
1265
|
const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
1359
1266
|
const aggregateClauses = {};
|
|
1360
1267
|
const matchesClauses = {};
|
|
1268
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1361
1269
|
|
|
1362
|
-
let fieldType = qlField.type;
|
|
1363
|
-
if (qlField.type instanceof GraphQLList) {
|
|
1364
|
-
fieldType = qlField.type.ofType;
|
|
1365
|
-
}
|
|
1366
1270
|
if (fieldType instanceof GraphQLScalarType
|
|
1367
1271
|
|| isNonNullOfType(fieldType, GraphQLScalarType)
|
|
1368
1272
|
|| fieldType instanceof GraphQLEnumType
|
|
1369
1273
|
|| 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
|
-
}
|
|
1274
|
+
coerceDateValue(fieldType, filterField, 'value');
|
|
1378
1275
|
matchesClauses[fieldName] = buildMatchesClause(fieldName === 'id' ? '_id' : fieldName, filterField.operator, filterField.value);
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
if (fieldType instanceof GraphQLNonNull) {
|
|
1382
|
-
fieldType = qlField.type.ofType;
|
|
1383
|
-
}
|
|
1276
|
+
return { aggregateClauses, matchesClauses };
|
|
1277
|
+
}
|
|
1384
1278
|
|
|
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
|
-
}
|
|
1279
|
+
if (!(fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType))) {
|
|
1280
|
+
return { aggregateClauses, matchesClauses };
|
|
1281
|
+
}
|
|
1413
1282
|
|
|
1414
|
-
|
|
1415
|
-
lookup,
|
|
1416
|
-
unwind: { $unwind: { path: `$${fieldName}`, preserveNullAndEmptyArrays: true } },
|
|
1417
|
-
};
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1283
|
+
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1420
1284
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
-
}
|
|
1285
|
+
filterField.terms.forEach((term) => {
|
|
1286
|
+
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1287
|
+
&& !aggregateClauses[fieldName]) {
|
|
1288
|
+
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
1289
|
+
}
|
|
1495
1290
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1291
|
+
if (term.path.indexOf('.') < 0) {
|
|
1292
|
+
const { type: leafType } = resolvedFieldType.getFields()[term.path];
|
|
1293
|
+
coerceDateValue(leafType, term, 'value');
|
|
1294
|
+
const leafName = resolvedFieldType.getFields()[term.path].name === 'id' ? '_id' : term.path;
|
|
1295
|
+
matchesClauses[fieldName] = buildMatchesClause(`${fieldName}.${leafName}`, term.operator, term.value);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
let currentGQLPathFieldType = unwrapListAndNonNull(qlField.type);
|
|
1300
|
+
let aliasPath = fieldName;
|
|
1301
|
+
let embeddedPath = '';
|
|
1302
|
+
|
|
1303
|
+
term.path.split('.').forEach((pathFieldName) => {
|
|
1304
|
+
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1305
|
+
if (pathField.type instanceof GraphQLScalarType
|
|
1306
|
+
|| isNonNullOfType(pathField.type, GraphQLScalarType)) {
|
|
1307
|
+
coerceDateValue(pathField.type, term, 'value');
|
|
1308
|
+
const leafName = pathFieldName === 'id' ? '_id' : pathFieldName;
|
|
1309
|
+
const mongoPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}.` : '.') + leafName;
|
|
1310
|
+
matchesClauses[`${aliasPath}_${pathFieldName}`] = buildMatchesClause(mongoPath, term.operator, term.value);
|
|
1311
|
+
embeddedPath = '';
|
|
1312
|
+
} else if (pathField.type instanceof GraphQLObjectType
|
|
1313
|
+
|| pathField.type instanceof GraphQLList
|
|
1314
|
+
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1315
|
+
const pathFieldType = unwrapListAndNonNull(pathField.type);
|
|
1316
|
+
currentGQLPathFieldType = pathFieldType;
|
|
1317
|
+
|
|
1318
|
+
if (pathField.extensions?.relation && !pathField.extensions.relation.embedded) {
|
|
1319
|
+
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1320
|
+
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1321
|
+
embeddedPath = '';
|
|
1322
|
+
|
|
1323
|
+
if (!aggregateClauses[aliasPath]) {
|
|
1324
|
+
aggregateClauses[aliasPath] = nestedRelationLookup(
|
|
1325
|
+
pathField, pathFieldType, currentPath, aliasPath, pathFieldName,
|
|
1326
|
+
);
|
|
1506
1327
|
}
|
|
1507
|
-
}
|
|
1328
|
+
} else {
|
|
1329
|
+
embeddedPath = embeddedPath === '' ? pathFieldName : `${embeddedPath}.${pathFieldName}`;
|
|
1330
|
+
}
|
|
1508
1331
|
}
|
|
1509
1332
|
});
|
|
1510
|
-
}
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1511
1335
|
return { aggregateClauses, matchesClauses };
|
|
1512
1336
|
};
|
|
1513
1337
|
|
|
@@ -1521,93 +1345,57 @@ const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, agg
|
|
|
1521
1345
|
const parts = [];
|
|
1522
1346
|
const fields = gqltype.getFields();
|
|
1523
1347
|
|
|
1524
|
-
|
|
1525
|
-
if (filterGroup.conditions && filterGroup.conditions.length > 0) {
|
|
1348
|
+
if (filterGroup.conditions?.length > 0) {
|
|
1526
1349
|
for (const condition of filterGroup.conditions) {
|
|
1527
1350
|
const qlField = fields[condition.field];
|
|
1528
1351
|
if (!qlField) {
|
|
1529
|
-
throw new SimfinityError(
|
|
1530
|
-
`Unknown filter field: ${condition.field}`,
|
|
1531
|
-
'INVALID_FILTER_FIELD',
|
|
1532
|
-
400,
|
|
1533
|
-
);
|
|
1352
|
+
throw new SimfinityError(`Unknown filter field: ${condition.field}`, 'INVALID_FILTER_FIELD', 400);
|
|
1534
1353
|
}
|
|
1535
1354
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
if (fieldType instanceof GraphQLList || fieldType instanceof GraphQLNonNull) {
|
|
1539
|
-
fieldType = fieldType.ofType;
|
|
1540
|
-
}
|
|
1355
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1356
|
+
const isObject = fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType);
|
|
1541
1357
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
// Object/relation field — wrap as QLTypeFilterExpression shape
|
|
1358
|
+
let filterInput;
|
|
1359
|
+
if (isObject) {
|
|
1545
1360
|
if (!condition.path) {
|
|
1546
|
-
throw new SimfinityError(
|
|
1547
|
-
`Filter on object field "${condition.field}" requires a path`,
|
|
1548
|
-
'MISSING_FILTER_PATH',
|
|
1549
|
-
400,
|
|
1550
|
-
);
|
|
1361
|
+
throw new SimfinityError(`Filter on object field "${condition.field}" requires a path`, 'MISSING_FILTER_PATH', 400);
|
|
1551
1362
|
}
|
|
1552
1363
|
filterInput = {
|
|
1553
|
-
terms: [{
|
|
1554
|
-
path: condition.path,
|
|
1555
|
-
operator: condition.operator,
|
|
1556
|
-
value: condition.value,
|
|
1557
|
-
}],
|
|
1364
|
+
terms: [{ path: condition.path, operator: condition.operator, value: condition.value }],
|
|
1558
1365
|
};
|
|
1559
1366
|
} else {
|
|
1560
|
-
|
|
1561
|
-
filterInput = {
|
|
1562
|
-
operator: condition.operator,
|
|
1563
|
-
value: condition.value,
|
|
1564
|
-
};
|
|
1367
|
+
filterInput = { operator: condition.operator, value: condition.value };
|
|
1565
1368
|
}
|
|
1566
1369
|
|
|
1567
1370
|
const result = await buildQueryTerms(filterInput, qlField, condition.field);
|
|
1371
|
+
if (!result) continue;
|
|
1568
1372
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
aggregateClauses.push(aggregate.lookup);
|
|
1574
|
-
aggregateClauses.push(aggregate.unwind);
|
|
1575
|
-
aggregationsIncluded[prop] = true;
|
|
1576
|
-
}
|
|
1373
|
+
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1374
|
+
if (!aggregationsIncluded[prop]) {
|
|
1375
|
+
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1376
|
+
aggregationsIncluded[prop] = true;
|
|
1577
1377
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
for (const
|
|
1581
|
-
|
|
1582
|
-
parts.push({ [matchKey]: match });
|
|
1583
|
-
}
|
|
1378
|
+
}
|
|
1379
|
+
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
1380
|
+
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1381
|
+
parts.push({ [matchKey]: match });
|
|
1584
1382
|
}
|
|
1585
1383
|
}
|
|
1586
1384
|
}
|
|
1587
1385
|
}
|
|
1588
1386
|
|
|
1589
|
-
|
|
1590
|
-
if (filterGroup.AND && filterGroup.AND.length > 0) {
|
|
1387
|
+
if (filterGroup.AND?.length > 0) {
|
|
1591
1388
|
for (const subGroup of filterGroup.AND) {
|
|
1592
|
-
const subMatch = await buildFilterGroupMatch(
|
|
1593
|
-
|
|
1594
|
-
);
|
|
1595
|
-
if (subMatch) {
|
|
1596
|
-
parts.push(subMatch);
|
|
1597
|
-
}
|
|
1389
|
+
const subMatch = await buildFilterGroupMatch(subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1);
|
|
1390
|
+
if (subMatch) parts.push(subMatch);
|
|
1598
1391
|
}
|
|
1599
1392
|
}
|
|
1600
1393
|
|
|
1601
|
-
|
|
1602
|
-
if (filterGroup.OR && filterGroup.OR.length > 0) {
|
|
1394
|
+
if (filterGroup.OR?.length > 0) {
|
|
1603
1395
|
const orParts = [];
|
|
1604
1396
|
for (const subGroup of filterGroup.OR) {
|
|
1605
|
-
const subMatch = await buildFilterGroupMatch(
|
|
1606
|
-
|
|
1607
|
-
);
|
|
1608
|
-
if (subMatch) {
|
|
1609
|
-
orParts.push(subMatch);
|
|
1610
|
-
}
|
|
1397
|
+
const subMatch = await buildFilterGroupMatch(subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1);
|
|
1398
|
+
if (subMatch) orParts.push(subMatch);
|
|
1611
1399
|
}
|
|
1612
1400
|
if (orParts.length === 1) {
|
|
1613
1401
|
parts.push(orParts[0]);
|
|
@@ -1623,105 +1411,44 @@ const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, agg
|
|
|
1623
1411
|
|
|
1624
1412
|
const RESERVED_QUERY_KEYS = new Set(['pagination', 'sort', 'AND', 'OR', 'aggregation']);
|
|
1625
1413
|
|
|
1626
|
-
const
|
|
1627
|
-
const aggregateClauses = [];
|
|
1414
|
+
const collectFiltersAndLookups = async (input, gqltype, aggregateClauses, aggregationsIncluded) => {
|
|
1628
1415
|
const flatMatchConditions = {};
|
|
1629
1416
|
let hasFlat = false;
|
|
1630
|
-
|
|
1631
|
-
let skipClause = { $skip: 0 };
|
|
1632
|
-
let sortClause = {};
|
|
1633
|
-
let addSort = false;
|
|
1634
|
-
const aggregationsIncluded = {};
|
|
1417
|
+
const fields = gqltype.getFields();
|
|
1635
1418
|
|
|
1636
1419
|
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 };
|
|
1420
|
+
if (RESERVED_QUERY_KEYS.has(key)) continue;
|
|
1421
|
+
const qlField = fields[key];
|
|
1422
|
+
const result = await buildQueryTerms(filterField, qlField, key);
|
|
1423
|
+
if (!result) continue;
|
|
1424
|
+
|
|
1425
|
+
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1426
|
+
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1427
|
+
aggregationsIncluded[prop] = true;
|
|
1428
|
+
}
|
|
1429
|
+
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
1430
|
+
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1431
|
+
flatMatchConditions[matchKey] = match;
|
|
1432
|
+
hasFlat = true;
|
|
1665
1433
|
}
|
|
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
1434
|
}
|
|
1696
1435
|
}
|
|
1697
1436
|
|
|
1698
|
-
// Combine flat conditions with AND/OR groups
|
|
1699
1437
|
const topLevelAndParts = [];
|
|
1438
|
+
if (hasFlat) topLevelAndParts.push(flatMatchConditions);
|
|
1700
1439
|
|
|
1701
|
-
if (
|
|
1702
|
-
topLevelAndParts.push(flatMatchConditions);
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
if (input.AND && input.AND.length > 0) {
|
|
1440
|
+
if (input.AND?.length > 0) {
|
|
1706
1441
|
for (const group of input.AND) {
|
|
1707
|
-
const groupMatch = await buildFilterGroupMatch(
|
|
1708
|
-
|
|
1709
|
-
);
|
|
1710
|
-
if (groupMatch) {
|
|
1711
|
-
topLevelAndParts.push(groupMatch);
|
|
1712
|
-
}
|
|
1442
|
+
const groupMatch = await buildFilterGroupMatch(group, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1443
|
+
if (groupMatch) topLevelAndParts.push(groupMatch);
|
|
1713
1444
|
}
|
|
1714
1445
|
}
|
|
1715
1446
|
|
|
1716
|
-
if (input.OR
|
|
1447
|
+
if (input.OR?.length > 0) {
|
|
1717
1448
|
const orParts = [];
|
|
1718
1449
|
for (const group of input.OR) {
|
|
1719
|
-
const groupMatch = await buildFilterGroupMatch(
|
|
1720
|
-
|
|
1721
|
-
);
|
|
1722
|
-
if (groupMatch) {
|
|
1723
|
-
orParts.push(groupMatch);
|
|
1724
|
-
}
|
|
1450
|
+
const groupMatch = await buildFilterGroupMatch(group, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1451
|
+
if (groupMatch) orParts.push(groupMatch);
|
|
1725
1452
|
}
|
|
1726
1453
|
if (orParts.length === 1) {
|
|
1727
1454
|
topLevelAndParts.push(orParts[0]);
|
|
@@ -1730,310 +1457,169 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1730
1457
|
}
|
|
1731
1458
|
}
|
|
1732
1459
|
|
|
1733
|
-
if (topLevelAndParts.length === 1) {
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1460
|
+
if (topLevelAndParts.length === 1) return { $match: topLevelAndParts[0] };
|
|
1461
|
+
if (topLevelAndParts.length > 1) return { $match: { $and: topLevelAndParts } };
|
|
1462
|
+
return null;
|
|
1463
|
+
};
|
|
1738
1464
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1465
|
+
const buildSortClause = (sortTerms, gqltype, aggregateClauses, aggregationsIncluded) => {
|
|
1466
|
+
const sortExpressions = {};
|
|
1467
|
+
const fields = gqltype.getFields();
|
|
1742
1468
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1469
|
+
sortTerms.forEach((sort) => {
|
|
1470
|
+
let fixedSortField = sort.field;
|
|
1471
|
+
if (sort.field.indexOf('.') >= 0) {
|
|
1472
|
+
const sortParts = sort.field.split('.');
|
|
1473
|
+
fixedSortField = sortParts[0];
|
|
1474
|
+
for (let i = 1; i < sortParts.length - 1; i++) {
|
|
1475
|
+
fixedSortField += `_${sortParts[i]}`;
|
|
1476
|
+
}
|
|
1477
|
+
fixedSortField += `.${sortParts[sortParts.length - 1]}`;
|
|
1478
|
+
const qlField = fields[sortParts[0]];
|
|
1479
|
+
const path = sort.field.slice(sort.field.indexOf('.') + 1);
|
|
1480
|
+
const sortAggregations = buildAggregationsForSort({ terms: [{ path }] }, qlField, sortParts[0]);
|
|
1481
|
+
for (const [prop, aggregate] of Object.entries(sortAggregations)) {
|
|
1482
|
+
if (!aggregationsIncluded[prop]) {
|
|
1483
|
+
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
sortExpressions[fixedSortField] = sort.order === 'ASC' ? 1 : -1;
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
return { $sort: sortExpressions };
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
const buildQuery = async (input, gqltype, isCount) => {
|
|
1494
|
+
const aggregateClauses = [];
|
|
1495
|
+
const aggregationsIncluded = {};
|
|
1496
|
+
|
|
1497
|
+
const matchStage = await collectFiltersAndLookups(input, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1498
|
+
if (matchStage) aggregateClauses.push(matchStage);
|
|
1747
1499
|
|
|
1748
1500
|
if (isCount) {
|
|
1749
1501
|
aggregateClauses.push({ $count: 'size' });
|
|
1502
|
+
return aggregateClauses;
|
|
1750
1503
|
}
|
|
1751
1504
|
|
|
1505
|
+
if (input.sort) {
|
|
1506
|
+
aggregateClauses.push(buildSortClause(input.sort.terms, gqltype, aggregateClauses, aggregationsIncluded));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
let limitClause = { $limit: 100 };
|
|
1510
|
+
let skipClause = { $skip: 0 };
|
|
1511
|
+
if (input.pagination?.page && input.pagination?.size) {
|
|
1512
|
+
const skip = input.pagination.size * (input.pagination.page - 1);
|
|
1513
|
+
limitClause = { $limit: input.pagination.size + skip };
|
|
1514
|
+
skipClause = { $skip: skip };
|
|
1515
|
+
}
|
|
1516
|
+
aggregateClauses.push(limitClause, skipClause);
|
|
1517
|
+
|
|
1752
1518
|
return aggregateClauses;
|
|
1753
1519
|
};
|
|
1754
1520
|
|
|
1755
1521
|
const buildFieldPath = (gqltype, fieldPath) => {
|
|
1756
|
-
// This function resolves a field path (e.g., "category" or "country.name")
|
|
1757
|
-
// and returns the MongoDB field path and any necessary lookups
|
|
1758
1522
|
const pathParts = fieldPath.split('.');
|
|
1759
|
-
const
|
|
1523
|
+
const lookupPairs = [];
|
|
1760
1524
|
let currentPath = '';
|
|
1761
1525
|
let currentGQLType = gqltype;
|
|
1762
|
-
|
|
1763
|
-
for (
|
|
1764
|
-
const part = pathParts[i];
|
|
1526
|
+
|
|
1527
|
+
for (const part of pathParts) {
|
|
1765
1528
|
const field = currentGQLType.getFields()[part];
|
|
1766
|
-
|
|
1767
1529
|
if (!field) {
|
|
1768
1530
|
throw new Error(`Field ${part} not found in type ${currentGQLType.name}`);
|
|
1769
1531
|
}
|
|
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
|
-
|
|
1532
|
+
|
|
1533
|
+
const fieldType = unwrapListAndNonNull(field.type);
|
|
1534
|
+
const relation = field.extensions?.relation;
|
|
1535
|
+
|
|
1536
|
+
if (fieldType instanceof GraphQLObjectType && relation && !relation.embedded) {
|
|
1537
|
+
const { collectionName } = typesDict.types[fieldType.name].model.collection;
|
|
1538
|
+
const connField = relation.connectionField || part;
|
|
1785
1539
|
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
|
-
|
|
1540
|
+
const localField = currentPath ? `${currentPath}.${connField}` : connField;
|
|
1541
|
+
|
|
1542
|
+
lookupPairs.push(buildRelationLookup({
|
|
1543
|
+
collectionName, localField, foreignField: '_id', alias: lookupAlias,
|
|
1544
|
+
}));
|
|
1545
|
+
|
|
1801
1546
|
currentPath = lookupAlias;
|
|
1802
1547
|
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
|
|
1548
|
+
} else if (fieldType instanceof GraphQLObjectType && relation?.embedded) {
|
|
1807
1549
|
currentPath = currentPath ? `${currentPath}.${part}` : part;
|
|
1808
1550
|
currentGQLType = fieldType;
|
|
1809
1551
|
} else {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1552
|
+
const leaf = part === 'id' ? '_id' : part;
|
|
1553
|
+
currentPath = currentPath ? `${currentPath}.${leaf}` : leaf;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
return { mongoPath: currentPath, lookupPairs };
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
const appendLookupPairs = (aggregateClauses, aggregationsIncluded, lookupPairs) => {
|
|
1561
|
+
for (const { lookup, unwind } of lookupPairs) {
|
|
1562
|
+
const alias = lookup.$lookup.as;
|
|
1563
|
+
if (!aggregationsIncluded[alias]) {
|
|
1564
|
+
aggregateClauses.push(lookup, unwind);
|
|
1565
|
+
aggregationsIncluded[alias] = true;
|
|
1816
1566
|
}
|
|
1817
1567
|
}
|
|
1818
|
-
|
|
1819
|
-
return { mongoPath: currentPath, lookups: aggregateClauses };
|
|
1820
1568
|
};
|
|
1821
1569
|
|
|
1822
1570
|
const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
1823
1571
|
const aggregateClauses = [];
|
|
1824
|
-
const flatMatchConditions = {};
|
|
1825
|
-
let hasFlat = false;
|
|
1826
1572
|
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
1573
|
|
|
1836
|
-
|
|
1574
|
+
const matchStage = await collectFiltersAndLookups(input, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1575
|
+
if (matchStage) aggregateClauses.push(matchStage);
|
|
1837
1576
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
aggregateClauses.push(aggregate.unwind);
|
|
1842
|
-
aggregationsIncluded[prop] = true;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
for (const [matchClauseKey, matchClause] of Object.entries(result.matchesClauses)) {
|
|
1846
|
-
if (Object.prototype.hasOwnProperty.call(result.matchesClauses, matchClauseKey)) {
|
|
1847
|
-
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
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
|
-
}
|
|
1577
|
+
const { groupId, facts } = aggregationExpression;
|
|
1578
|
+
const groupIdPath = buildFieldPath(gqltype, groupId);
|
|
1579
|
+
appendLookupPairs(aggregateClauses, aggregationsIncluded, groupIdPath.lookupPairs);
|
|
1880
1580
|
|
|
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
|
-
}
|
|
1581
|
+
const groupStage = { $group: { _id: `$${groupIdPath.mongoPath}` } };
|
|
1891
1582
|
|
|
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
|
-
}
|
|
1583
|
+
facts.forEach((fact) => {
|
|
1584
|
+
const factPath = buildFieldPath(gqltype, fact.path);
|
|
1585
|
+
appendLookupPairs(aggregateClauses, aggregationsIncluded, factPath.lookupPairs);
|
|
1908
1586
|
|
|
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;
|
|
1587
|
+
const builder = AGG_OP_TO_MONGO[fact.operation];
|
|
1588
|
+
if (!builder) {
|
|
1589
|
+
throw new Error(`Unknown aggregation operation: ${fact.operation}`);
|
|
1933
1590
|
}
|
|
1591
|
+
groupStage.$group[fact.factName] = builder(factPath.mongoPath);
|
|
1934
1592
|
});
|
|
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}`);
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
groupStage.$group[factName] = mongoOperation;
|
|
1986
|
-
});
|
|
1987
|
-
|
|
1593
|
+
|
|
1988
1594
|
aggregateClauses.push(groupStage);
|
|
1989
|
-
|
|
1990
|
-
// Add a final projection stage to format the output
|
|
1595
|
+
|
|
1991
1596
|
aggregateClauses.push({
|
|
1992
1597
|
$project: {
|
|
1993
1598
|
_id: 0,
|
|
1994
1599
|
groupId: '$_id',
|
|
1995
|
-
facts: Object.fromEntries(facts.map(fact => [fact.factName, `$${fact.factName}`])),
|
|
1600
|
+
facts: Object.fromEntries(facts.map((fact) => [fact.factName, `$${fact.factName}`])),
|
|
1996
1601
|
},
|
|
1997
1602
|
});
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
if (sortTerms
|
|
1603
|
+
|
|
1604
|
+
const sortTerms = input.sort?.terms?.length > 0 ? input.sort.terms : null;
|
|
1605
|
+
if (sortTerms) {
|
|
1606
|
+
const factNames = facts.map((fact) => fact.factName);
|
|
2001
1607
|
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,
|
|
1608
|
+
sortTerms.forEach((sortTerm) => {
|
|
1609
|
+
const field = sortTerm.field || 'groupId';
|
|
1610
|
+
const sortFieldPath = factNames.includes(field) ? `facts.${field}` : 'groupId';
|
|
1611
|
+
sortObject[sortFieldPath] = sortTerm.order === 'ASC' ? 1 : -1;
|
|
2021
1612
|
});
|
|
1613
|
+
aggregateClauses.push({ $sort: sortObject });
|
|
2022
1614
|
} else {
|
|
2023
|
-
|
|
2024
|
-
aggregateClauses.push({
|
|
2025
|
-
$sort: { groupId: 1 },
|
|
2026
|
-
});
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
// Add pagination if provided
|
|
2030
|
-
if (limitClause) {
|
|
2031
|
-
aggregateClauses.push(limitClause);
|
|
1615
|
+
aggregateClauses.push({ $sort: { groupId: 1 } });
|
|
2032
1616
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
1617
|
+
|
|
1618
|
+
if (input.pagination?.page && input.pagination?.size) {
|
|
1619
|
+
const skip = input.pagination.size * (input.pagination.page - 1);
|
|
1620
|
+
aggregateClauses.push({ $limit: input.pagination.size + skip }, { $skip: skip });
|
|
2035
1621
|
}
|
|
2036
|
-
|
|
1622
|
+
|
|
2037
1623
|
return aggregateClauses;
|
|
2038
1624
|
};
|
|
2039
1625
|
|
|
@@ -2046,7 +1632,6 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2046
1632
|
if (!shouldNotBeIncludedInSchema(includedTypes, type.gqltype)) {
|
|
2047
1633
|
const wasAddedAsNoEnpointType = !type.simpleEntityEndpointName;
|
|
2048
1634
|
if (!wasAddedAsNoEnpointType) {
|
|
2049
|
-
// Fixing resolve method in order to be compliant with Mongo _id field
|
|
2050
1635
|
if (type.gqltype.getFields().id && !type.gqltype.getFields().id.resolve) {
|
|
2051
1636
|
type.gqltype.getFields().id.resolve = (parent) => parent._id;
|
|
2052
1637
|
}
|
|
@@ -2055,54 +1640,26 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2055
1640
|
type: type.gqltype,
|
|
2056
1641
|
args: { id: { type: GraphQLID } },
|
|
2057
1642
|
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
1643
|
const params = {
|
|
2062
|
-
type,
|
|
2063
|
-
args,
|
|
2064
|
-
operation: 'get_by_id',
|
|
2065
|
-
context,
|
|
1644
|
+
type, args, operation: 'get_by_id', context,
|
|
2066
1645
|
};
|
|
2067
1646
|
excecuteMiddleware(params);
|
|
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);
|
|
1647
|
+
|
|
1648
|
+
const hasScope = type.gqltype.extensions?.scope?.get_by_id;
|
|
1649
|
+
if (!hasScope) {
|
|
1650
|
+
return type.model.findById(args.id);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const queryArgs = { id: { operator: 'EQ', value: args.id } };
|
|
1654
|
+
await executeScope({
|
|
1655
|
+
type, args: queryArgs, operation: 'get_by_id', context,
|
|
1656
|
+
});
|
|
1657
|
+
const aggregateClauses = await buildQuery(queryArgs, type.gqltype);
|
|
1658
|
+
if (aggregateClauses.length === 0) {
|
|
1659
|
+
return type.model.findOne({ _id: args.id });
|
|
2105
1660
|
}
|
|
1661
|
+
const results = await type.model.aggregate(aggregateClauses);
|
|
1662
|
+
return results[0] || null;
|
|
2106
1663
|
},
|
|
2107
1664
|
};
|
|
2108
1665
|
|
|
@@ -2123,23 +1680,23 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2123
1680
|
excecuteMiddleware(params);
|
|
2124
1681
|
await executeScope(params);
|
|
2125
1682
|
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
|
-
}
|
|
1683
|
+
const wantsCount = !!(args.pagination && args.pagination.count);
|
|
2131
1684
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
1685
|
+
const dataPromise = aggregateClauses.length === 0
|
|
1686
|
+
? type.model.find({})
|
|
1687
|
+
: type.model.aggregate(aggregateClauses);
|
|
1688
|
+
const countPromise = wantsCount
|
|
1689
|
+
? buildQuery(args, type.gqltype, true).then((p) => type.model.aggregate(p))
|
|
1690
|
+
: null;
|
|
1691
|
+
|
|
1692
|
+
const [result, resultCount] = await Promise.all([dataPromise, countPromise]);
|
|
1693
|
+
if (wantsCount) {
|
|
1694
|
+
context.count = resultCount[0] ? resultCount[0].size : 0;
|
|
2137
1695
|
}
|
|
2138
1696
|
return result;
|
|
2139
1697
|
},
|
|
2140
1698
|
};
|
|
2141
1699
|
|
|
2142
|
-
// Add aggregate endpoint
|
|
2143
1700
|
const aggregateArgsObject = { ...argsObject };
|
|
2144
1701
|
aggregateArgsObject.aggregation = {
|
|
2145
1702
|
type: new GraphQLNonNull(QLTypeAggregationExpression),
|
|
@@ -2169,33 +1726,24 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
2169
1726
|
return new GraphQLObjectType(rootQueryArgs);
|
|
2170
1727
|
};
|
|
2171
1728
|
|
|
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 => {
|
|
1729
|
+
export const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomMutations) => {
|
|
1730
|
+
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
2179
1731
|
if (typeInfo.gqltype && !typeInfo.model) {
|
|
2180
1732
|
if (typeInfo.endpoint) {
|
|
2181
|
-
// Generate model with collection for endpoint types (types registered with connect)
|
|
2182
1733
|
typeInfo.model = generateModel(typeInfo.gqltype, typeInfo.onModelCreated);
|
|
2183
1734
|
} else if (typeInfo.needsModel) {
|
|
2184
|
-
|
|
2185
|
-
typeInfo.model = generateModelWithoutCollection(typeInfo.gqltype, null);
|
|
1735
|
+
typeInfo.model = generateModel(typeInfo.gqltype, null, { createCollection: false });
|
|
2186
1736
|
}
|
|
2187
1737
|
}
|
|
2188
1738
|
});
|
|
2189
1739
|
|
|
2190
|
-
|
|
2191
|
-
Object.keys(typesDict.types).forEach(typeName => {
|
|
1740
|
+
Object.keys(typesDict.types).forEach((typeName) => {
|
|
2192
1741
|
if (typesDictForUpdate.types[typeName]) {
|
|
2193
1742
|
typesDictForUpdate.types[typeName].model = typesDict.types[typeName].model;
|
|
2194
1743
|
}
|
|
2195
1744
|
});
|
|
2196
1745
|
|
|
2197
|
-
|
|
2198
|
-
Object.values(typesDict.types).forEach(typeInfo => {
|
|
1746
|
+
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
2199
1747
|
if (typeInfo.gqltype) {
|
|
2200
1748
|
autoGenerateResolvers(typeInfo.gqltype);
|
|
2201
1749
|
}
|
|
@@ -2213,7 +1761,6 @@ export const getType = (typeName) => {
|
|
|
2213
1761
|
if (typeof typeName === 'string') {
|
|
2214
1762
|
return typesDict.types[typeName]?.gqltype;
|
|
2215
1763
|
}
|
|
2216
|
-
// If it's already a GraphQL type object, get by its name
|
|
2217
1764
|
if (typeName && typeName.name) {
|
|
2218
1765
|
return typesDict.types[typeName.name]?.gqltype;
|
|
2219
1766
|
}
|
|
@@ -2233,68 +1780,41 @@ const autoGenerateResolvers = (gqltype) => {
|
|
|
2233
1780
|
const fields = gqltype.getFields();
|
|
2234
1781
|
|
|
2235
1782
|
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
|
-
}
|
|
1783
|
+
if (fieldEntry.resolve) continue;
|
|
1784
|
+
const relation = fieldEntry.extensions?.relation;
|
|
1785
|
+
if (!relation || relation.embedded) continue;
|
|
1786
|
+
|
|
1787
|
+
if (fieldEntry.type instanceof GraphQLList) {
|
|
1788
|
+
const relatedType = fieldEntry.type.ofType;
|
|
1789
|
+
const connectionField = relation.connectionField || fieldName;
|
|
1790
|
+
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
1791
|
+
const argsObject = createArgsForQuery(relatedTypeInfo.gqltype.getFields());
|
|
1792
|
+
delete argsObject[connectionField];
|
|
1793
|
+
|
|
1794
|
+
fieldEntry.args = formatArgs(Object.entries(argsObject));
|
|
1795
|
+
fieldEntry.resolve = async (parent, args) => {
|
|
1796
|
+
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
1797
|
+
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
2296
1798
|
}
|
|
2297
|
-
|
|
1799
|
+
args[connectionField] = {
|
|
1800
|
+
terms: [{ path: 'id', operator: 'EQ', value: parent.id || parent._id }],
|
|
1801
|
+
};
|
|
1802
|
+
const aggregateClauses = await buildQuery(args, relatedTypeInfo.gqltype);
|
|
1803
|
+
return relatedTypeInfo.model.aggregate(aggregateClauses);
|
|
1804
|
+
};
|
|
1805
|
+
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
1806
|
+
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
|
|
1807
|
+
const relatedType = unwrapNonNull(fieldEntry.type);
|
|
1808
|
+
const connectionField = relation.connectionField || fieldName;
|
|
1809
|
+
|
|
1810
|
+
fieldEntry.resolve = async (parent) => {
|
|
1811
|
+
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
1812
|
+
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
1813
|
+
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
1814
|
+
}
|
|
1815
|
+
const relatedId = parent[connectionField] || parent[fieldName];
|
|
1816
|
+
return relatedId ? relatedTypeInfo.model.findById(relatedId) : null;
|
|
1817
|
+
};
|
|
2298
1818
|
}
|
|
2299
1819
|
}
|
|
2300
1820
|
};
|
|
@@ -2306,43 +1826,36 @@ export const connect = (model, gqltype, simpleEntityEndpointName,
|
|
|
2306
1826
|
gqltype,
|
|
2307
1827
|
};
|
|
2308
1828
|
typesDict.types[gqltype.name] = {
|
|
2309
|
-
model
|
|
1829
|
+
model,
|
|
2310
1830
|
gqltype,
|
|
2311
1831
|
simpleEntityEndpointName,
|
|
2312
1832
|
listEntitiesEndpointName,
|
|
2313
1833
|
endpoint: true,
|
|
2314
1834
|
controller,
|
|
2315
1835
|
stateMachine,
|
|
2316
|
-
onModelCreated,
|
|
1836
|
+
onModelCreated,
|
|
2317
1837
|
};
|
|
2318
1838
|
|
|
2319
1839
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
2320
1840
|
};
|
|
2321
1841
|
|
|
2322
1842
|
export const addNoEndpointType = (gqltype) => {
|
|
2323
|
-
waitingInputType[gqltype.name] = {
|
|
2324
|
-
gqltype,
|
|
2325
|
-
};
|
|
1843
|
+
waitingInputType[gqltype.name] = { gqltype };
|
|
2326
1844
|
|
|
2327
|
-
// Check if this type has relationship fields that might need a model
|
|
2328
1845
|
const fields = gqltype.getFields();
|
|
2329
1846
|
let needsModel = false;
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
1847
|
+
for (const fieldEntry of Object.values(fields)) {
|
|
1848
|
+
if (fieldEntry.extensions?.relation
|
|
1849
|
+
&& (fieldEntry.type instanceof GraphQLObjectType
|
|
1850
|
+
|| fieldEntry.type instanceof GraphQLList
|
|
1851
|
+
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType))) {
|
|
2335
1852
|
needsModel = true;
|
|
2336
1853
|
break;
|
|
2337
1854
|
}
|
|
2338
1855
|
}
|
|
2339
1856
|
|
|
2340
1857
|
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
|
|
1858
|
+
gqltype, endpoint: false, model: null, needsModel,
|
|
2346
1859
|
};
|
|
2347
1860
|
|
|
2348
1861
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|