@simtlix/simfinity-js 2.4.6 → 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/agitated-kepler/.claude/settings.local.json +23 -0
- package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
- package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
- package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
- package/.claude/worktrees/agitated-kepler/README.md +3941 -0
- package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
- package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
- package/.claude/worktrees/agitated-kepler/package.json +41 -0
- package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
- package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
- package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
- package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
- package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
- package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
- package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
- package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
- package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
- package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
- package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
- 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/.cursor/rules/simfinity-core-functions.mdc +3 -1
- package/README.md +202 -0
- package/package.json +1 -1
- package/src/index.js +623 -896
- 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: () => ({
|
|
@@ -167,6 +161,25 @@ const QLTypeFilterExpression = new GraphQLInputObjectType({
|
|
|
167
161
|
}),
|
|
168
162
|
});
|
|
169
163
|
|
|
164
|
+
const QLFilterCondition = new GraphQLInputObjectType({
|
|
165
|
+
name: 'QLFilterCondition',
|
|
166
|
+
fields: () => ({
|
|
167
|
+
field: { type: new GraphQLNonNull(GraphQLString) },
|
|
168
|
+
operator: { type: QLOperator },
|
|
169
|
+
value: { type: QLValue },
|
|
170
|
+
path: { type: GraphQLString },
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const QLFilterGroup = new GraphQLInputObjectType({
|
|
175
|
+
name: 'QLFilterGroup',
|
|
176
|
+
fields: () => ({
|
|
177
|
+
AND: { type: new GraphQLList(QLFilterGroup) },
|
|
178
|
+
OR: { type: new GraphQLList(QLFilterGroup) },
|
|
179
|
+
conditions: { type: new GraphQLList(QLFilterCondition) },
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
|
|
170
183
|
const QLPagination = new GraphQLInputObjectType({
|
|
171
184
|
name: 'QLPagination',
|
|
172
185
|
fields: () => ({
|
|
@@ -244,17 +257,76 @@ const getEffectiveTypeName = (type) => {
|
|
|
244
257
|
|
|
245
258
|
const isGraphQLisoDate = (typeName) => typeName === 'DateTime' || typeName === 'Date' || typeName === 'Time';
|
|
246
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
|
+
|
|
247
321
|
function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
248
322
|
if (!baseScalarType) {
|
|
249
323
|
throw new Error('baseScalarType is required');
|
|
250
324
|
}
|
|
251
325
|
|
|
252
|
-
// Validate that baseScalarType is a valid GraphQL scalar type
|
|
253
326
|
if (!(baseScalarType instanceof GraphQLScalarType)) {
|
|
254
327
|
throw new Error('baseScalarType must be a valid GraphQL scalar type');
|
|
255
328
|
}
|
|
256
329
|
|
|
257
|
-
// Check if it's one of the standard GraphQL scalar types
|
|
258
330
|
const validScalarTypes = [GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID];
|
|
259
331
|
const isValidStandardType = validScalarTypes.some((type) => baseScalarType === type);
|
|
260
332
|
|
|
@@ -267,10 +339,9 @@ function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
|
267
339
|
Int: Kind.INT,
|
|
268
340
|
Float: Kind.FLOAT,
|
|
269
341
|
Boolean: Kind.BOOLEAN,
|
|
270
|
-
ID: Kind.STRING,
|
|
342
|
+
ID: Kind.STRING,
|
|
271
343
|
};
|
|
272
344
|
|
|
273
|
-
// Try to infer the kind from the baseScalarType name
|
|
274
345
|
const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
|
|
275
346
|
|
|
276
347
|
const scalar = new GraphQLScalarType({
|
|
@@ -321,11 +392,8 @@ const createOneToManyInputType = (inputNamePrefix, fieldEntryName,
|
|
|
321
392
|
inputType, updateInputType, connectionField) => {
|
|
322
393
|
let inputTypeForAdd = inputType;
|
|
323
394
|
|
|
324
|
-
// If a gqltype is provided, create a new input type for 'added'
|
|
325
|
-
// that excludes the field named after the gqltype.
|
|
326
395
|
if (connectionField) {
|
|
327
|
-
|
|
328
|
-
inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, fieldToExclude);
|
|
396
|
+
inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, connectionField);
|
|
329
397
|
}
|
|
330
398
|
|
|
331
399
|
return new GraphQLInputObjectType({
|
|
@@ -597,56 +665,54 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
597
665
|
return { modelArgs, collectionFields };
|
|
598
666
|
};
|
|
599
667
|
|
|
600
|
-
const
|
|
668
|
+
const MAX_TRANSIENT_RETRIES = 5;
|
|
669
|
+
|
|
670
|
+
const withTransaction = async (session, body) => {
|
|
601
671
|
const mySession = session || await mongoose.startSession();
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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;
|
|
612
687
|
}
|
|
613
|
-
mySession.endSession();
|
|
614
|
-
throw error;
|
|
615
688
|
}
|
|
689
|
+
mySession.endSession();
|
|
690
|
+
throw new SimfinityError('Transaction exceeded retry limit', 'TRANSACTION_RETRY_EXCEEDED', 500);
|
|
616
691
|
};
|
|
617
692
|
|
|
693
|
+
const executeRegisteredMutation = (args, callback, session) => withTransaction(
|
|
694
|
+
session,
|
|
695
|
+
(mySession) => callback(args, mySession),
|
|
696
|
+
);
|
|
697
|
+
|
|
618
698
|
const iterateonCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
|
|
619
|
-
for (const [collectionFieldKey, collectionField] of
|
|
620
|
-
Object.entries(materializedModel.collectionFields)) {
|
|
699
|
+
for (const [collectionFieldKey, collectionField] of Object.entries(materializedModel.collectionFields)) {
|
|
621
700
|
if (collectionField.added) {
|
|
622
|
-
|
|
623
701
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
624
702
|
collectionField.added, operations.SAVE, context);
|
|
625
703
|
}
|
|
626
704
|
if (collectionField.updated) {
|
|
627
|
-
|
|
628
705
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
629
706
|
collectionField.updated, operations.UPDATE, context);
|
|
630
707
|
}
|
|
631
708
|
if (collectionField.deleted) {
|
|
632
|
-
|
|
633
709
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
634
710
|
collectionField.deleted, operations.DELETE, context);
|
|
635
711
|
}
|
|
636
712
|
}
|
|
637
713
|
};
|
|
638
714
|
|
|
639
|
-
const
|
|
640
|
-
const deletedObject = await Model.findById({ _id: args }).session(session).lean();
|
|
641
|
-
|
|
642
|
-
if (controller && controller.onDelete) {
|
|
643
|
-
await controller.onDelete(deletedObject, session, context);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
return Model.findByIdAndDelete({ _id: args }).session(session);
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
const onDeleteSubject = async (Model, controller, id, session, context) => {
|
|
715
|
+
const onDelete = async (Model, controller, id, session, context) => {
|
|
650
716
|
const currentObject = await Model.findById({ _id: id }).session(session).lean();
|
|
651
717
|
|
|
652
718
|
if (controller && controller.onDelete) {
|
|
@@ -656,36 +722,49 @@ const onDeleteSubject = async (Model, controller, id, session, context) => {
|
|
|
656
722
|
return Model.findByIdAndDelete({ _id: id }).session(session);
|
|
657
723
|
};
|
|
658
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
|
+
|
|
659
736
|
const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
|
|
660
737
|
const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
|
|
661
738
|
const objectId = args.id;
|
|
662
|
-
|
|
663
|
-
const currentObject = await Model.findById({ _id: objectId }).lean();
|
|
664
|
-
|
|
665
739
|
const argTypes = gqltype.getFields();
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
+
}
|
|
677
755
|
}
|
|
678
756
|
}
|
|
679
757
|
}
|
|
758
|
+
}
|
|
680
759
|
|
|
681
|
-
|
|
682
|
-
|
|
760
|
+
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
761
|
+
if (args[fieldEntryName] === null && !(fieldEntry.type instanceof GraphQLNonNull)) {
|
|
683
762
|
materializedModel.modelArgs = {
|
|
684
763
|
...materializedModel.modelArgs,
|
|
685
764
|
$unset: { ...materializedModel.modelArgs.$unset, [fieldEntryName]: '' },
|
|
686
765
|
};
|
|
687
766
|
}
|
|
688
|
-
}
|
|
767
|
+
}
|
|
689
768
|
|
|
690
769
|
if (controller && controller.onUpdating) {
|
|
691
770
|
await controller.onUpdating(objectId, materializedModel.modelArgs, session, context);
|
|
@@ -761,38 +840,23 @@ export const saveObject = async (typeName, args, session, context) => {
|
|
|
761
840
|
return onSaveObject(type.model, type.gqltype, type.controller, args, session, null, context);
|
|
762
841
|
};
|
|
763
842
|
|
|
764
|
-
const executeOperation =
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
await mySession.startTransaction();
|
|
768
|
-
try {
|
|
769
|
-
let newObject = null;
|
|
843
|
+
const executeOperation = (Model, gqltype, controller, args, operation, actionField, session, context) => withTransaction(
|
|
844
|
+
session,
|
|
845
|
+
async (mySession) => {
|
|
770
846
|
switch (operation) {
|
|
771
847
|
case operations.SAVE:
|
|
772
|
-
|
|
773
|
-
break;
|
|
848
|
+
return onSaveObject(Model, gqltype, controller, args, mySession, null, context);
|
|
774
849
|
case operations.UPDATE:
|
|
775
|
-
|
|
776
|
-
break;
|
|
850
|
+
return onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
|
|
777
851
|
case operations.DELETE:
|
|
778
|
-
|
|
779
|
-
break;
|
|
852
|
+
return onDelete(Model, controller, args, mySession, context);
|
|
780
853
|
case operations.STATE_CHANGED:
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
await mySession.commitTransaction();
|
|
785
|
-
mySession.endSession();
|
|
786
|
-
return newObject;
|
|
787
|
-
} catch (error) {
|
|
788
|
-
await mySession.abortTransaction();
|
|
789
|
-
if (error.errorLabels && error.errorLabels.includes('TransientTransactionError')) {
|
|
790
|
-
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;
|
|
791
857
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
};
|
|
858
|
+
},
|
|
859
|
+
);
|
|
796
860
|
|
|
797
861
|
const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
798
862
|
collectionFieldsList, operationType, context) => {
|
|
@@ -821,7 +885,7 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
821
885
|
break;
|
|
822
886
|
case operations.DELETE:
|
|
823
887
|
operationFunction = async (collectionItem) => {
|
|
824
|
-
await
|
|
888
|
+
await onDelete(typesDict.types[collectionGQLType.name].model,
|
|
825
889
|
typesDict.types[collectionGQLType.name].controller, collectionItem, session, context);
|
|
826
890
|
};
|
|
827
891
|
}
|
|
@@ -869,12 +933,7 @@ const executeScope = async (params) => {
|
|
|
869
933
|
return null;
|
|
870
934
|
}
|
|
871
935
|
|
|
872
|
-
|
|
873
|
-
const result = await scopeFunction({ type, args, operation, context });
|
|
874
|
-
|
|
875
|
-
// For get_by_id, the scope function returns additional filters to merge
|
|
876
|
-
// For find and aggregate, it modifies args in place
|
|
877
|
-
return result;
|
|
936
|
+
return scopeFunction({ type, args, operation, context });
|
|
878
937
|
};
|
|
879
938
|
|
|
880
939
|
const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
|
|
@@ -1002,102 +1061,64 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1002
1061
|
return new GraphQLObjectType(rootQueryArgs);
|
|
1003
1062
|
};
|
|
1004
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
|
+
|
|
1005
1074
|
const generateSchemaDefinition = (gqlType) => {
|
|
1006
1075
|
const argTypes = gqlType.getFields();
|
|
1007
|
-
|
|
1008
1076
|
const schemaArg = {};
|
|
1009
1077
|
|
|
1010
1078
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
1011
|
-
|
|
1012
|
-
const getBaseScalarType = (scalarType) => scalarType.baseScalarType || scalarType;
|
|
1013
|
-
|
|
1014
|
-
// Helper function to check if a type is a custom validated scalar
|
|
1015
|
-
const isCustomValidatedScalar = (type) => type instanceof GraphQLScalarType && type.baseScalarType;
|
|
1079
|
+
const { type } = fieldEntry;
|
|
1016
1080
|
|
|
1017
|
-
if (
|
|
1081
|
+
if (matchesScalar(type, GraphQLID)) {
|
|
1018
1082
|
schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
|
|
1019
|
-
} else if (
|
|
1020
|
-
||
|
|
1021
|
-
|| (
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
schemaArg[fieldEntryName] = String;
|
|
1027
|
-
}
|
|
1028
|
-
} else if (fieldEntry.type instanceof GraphQLEnumType
|
|
1029
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
1030
|
-
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
1031
|
-
schemaArg[fieldEntryName] = { type: String, unique: true };
|
|
1032
|
-
} else {
|
|
1033
|
-
schemaArg[fieldEntryName] = String;
|
|
1034
|
-
}
|
|
1035
|
-
} else if (fieldEntry.type === GraphQLInt
|
|
1036
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLInt)
|
|
1037
|
-
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLInt)
|
|
1038
|
-
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt)) {
|
|
1039
|
-
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
1040
|
-
schemaArg[fieldEntryName] = { type: Number, unique: true };
|
|
1041
|
-
} else {
|
|
1042
|
-
schemaArg[fieldEntryName] = Number;
|
|
1043
|
-
}
|
|
1044
|
-
} else if (fieldEntry.type === GraphQLFloat
|
|
1045
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLFloat)
|
|
1046
|
-
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLFloat)
|
|
1047
|
-
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat)) {
|
|
1048
|
-
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
1049
|
-
schemaArg[fieldEntryName] = { type: Number, unique: true };
|
|
1050
|
-
} else {
|
|
1051
|
-
schemaArg[fieldEntryName] = Number;
|
|
1052
|
-
}
|
|
1053
|
-
} else if (fieldEntry.type === GraphQLBoolean
|
|
1054
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLBoolean)
|
|
1055
|
-
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLBoolean)
|
|
1056
|
-
|| (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)) {
|
|
1057
1090
|
schemaArg[fieldEntryName] = Boolean;
|
|
1058
|
-
} else if (
|
|
1059
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
1091
|
+
} else if (type instanceof GraphQLObjectType || isNonNullOfType(type, GraphQLObjectType)) {
|
|
1060
1092
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1061
1093
|
if (!fieldEntry.extensions.relation.embedded) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1094
|
+
const key = fieldEntry.extensions.relation.connectionField || fieldEntry.name;
|
|
1095
|
+
schemaArg[key] = mongoose.Schema.Types.ObjectId;
|
|
1064
1096
|
} else {
|
|
1065
|
-
|
|
1066
|
-
if (entryType
|
|
1067
|
-
entryType = entryType.ofType;
|
|
1068
|
-
}
|
|
1069
|
-
if (entryType !== gqlType) {
|
|
1070
|
-
schemaArg[fieldEntryName] = generateSchemaDefinition(entryType);
|
|
1071
|
-
} else {
|
|
1097
|
+
const entryType = unwrapNonNull(type);
|
|
1098
|
+
if (entryType === gqlType) {
|
|
1072
1099
|
throw new Error('A type cannot have a field of its same type and embedded');
|
|
1073
1100
|
}
|
|
1101
|
+
schemaArg[fieldEntryName] = generateSchemaDefinition(entryType);
|
|
1074
1102
|
}
|
|
1075
1103
|
}
|
|
1076
|
-
} else if (
|
|
1104
|
+
} else if (type instanceof GraphQLList) {
|
|
1077
1105
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1078
1106
|
if (fieldEntry.extensions.relation.embedded) {
|
|
1079
|
-
|
|
1080
|
-
if (entryType !== gqlType) {
|
|
1081
|
-
schemaArg[fieldEntryName] = [generateSchemaDefinition(entryType)];
|
|
1082
|
-
} else {
|
|
1107
|
+
if (type.ofType === gqlType) {
|
|
1083
1108
|
throw new Error('A type cannot have a field of its same type and embedded');
|
|
1084
1109
|
}
|
|
1110
|
+
schemaArg[fieldEntryName] = [generateSchemaDefinition(type.ofType)];
|
|
1085
1111
|
}
|
|
1086
|
-
} else if (
|
|
1087
|
-
|| fieldEntry.type.ofType instanceof GraphQLEnumType
|
|
1088
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
|
|
1112
|
+
} else if (listItemMatchesScalar(type, GraphQLString) || type.ofType instanceof GraphQLEnumType) {
|
|
1089
1113
|
schemaArg[fieldEntryName] = [String];
|
|
1090
|
-
} else if (
|
|
1091
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
|
|
1114
|
+
} else if (listItemMatchesScalar(type, GraphQLBoolean)) {
|
|
1092
1115
|
schemaArg[fieldEntryName] = [Boolean];
|
|
1093
|
-
} else if (
|
|
1094
|
-
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && (getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt || getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat))) {
|
|
1116
|
+
} else if (listItemMatchesScalar(type, GraphQLInt) || listItemMatchesScalar(type, GraphQLFloat)) {
|
|
1095
1117
|
schemaArg[fieldEntryName] = [Number];
|
|
1096
|
-
} else if (isGraphQLisoDate(getEffectiveTypeName(
|
|
1118
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(type.ofType))) {
|
|
1097
1119
|
schemaArg[fieldEntryName] = [Date];
|
|
1098
1120
|
}
|
|
1099
|
-
} else if (isGraphQLisoDate(getEffectiveTypeName(
|
|
1100
|
-
|| (fieldEntry.type instanceof GraphQLNonNull && isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType)))) {
|
|
1121
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(unwrapNonNull(type)))) {
|
|
1101
1122
|
schemaArg[fieldEntryName] = Date;
|
|
1102
1123
|
}
|
|
1103
1124
|
}
|
|
@@ -1112,24 +1133,17 @@ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
|
1112
1133
|
const currentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
1113
1134
|
|
|
1114
1135
|
if (fieldDefinition === mongoose.Schema.Types.ObjectId) {
|
|
1115
|
-
// Direct ObjectId field
|
|
1116
1136
|
objectIdFields.push(currentPath);
|
|
1117
1137
|
} else if (typeof fieldDefinition === 'object' && fieldDefinition !== null) {
|
|
1118
1138
|
if (Array.isArray(fieldDefinition)) {
|
|
1119
|
-
// Array field - check if it's an array of objects
|
|
1120
1139
|
const arrayElement = fieldDefinition[0];
|
|
1121
1140
|
if (typeof arrayElement === 'object' && arrayElement !== null) {
|
|
1122
|
-
|
|
1123
|
-
const nestedObjectIdFields = findObjectIdFields(arrayElement, currentPath);
|
|
1124
|
-
objectIdFields.push(...nestedObjectIdFields);
|
|
1141
|
+
objectIdFields.push(...findObjectIdFields(arrayElement, currentPath));
|
|
1125
1142
|
}
|
|
1126
1143
|
} else if (fieldDefinition.type === mongoose.Schema.Types.ObjectId) {
|
|
1127
|
-
// Object with ObjectId type
|
|
1128
1144
|
objectIdFields.push(currentPath);
|
|
1129
|
-
} else if (
|
|
1130
|
-
|
|
1131
|
-
const nestedObjectIdFields = findObjectIdFields(fieldDefinition, currentPath);
|
|
1132
|
-
objectIdFields.push(...nestedObjectIdFields);
|
|
1145
|
+
} else if (!fieldDefinition.type) {
|
|
1146
|
+
objectIdFields.push(...findObjectIdFields(fieldDefinition, currentPath));
|
|
1133
1147
|
}
|
|
1134
1148
|
}
|
|
1135
1149
|
}
|
|
@@ -1139,696 +1153,473 @@ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
|
1139
1153
|
|
|
1140
1154
|
const createSchemaWithIndexes = (schemaDefinition) => {
|
|
1141
1155
|
const schema = new mongoose.Schema(schemaDefinition);
|
|
1142
|
-
|
|
1143
|
-
// Find all ObjectId fields in the schema
|
|
1144
|
-
const objectIdFields = findObjectIdFields(schemaDefinition);
|
|
1145
|
-
|
|
1146
|
-
// Create indexes for all ObjectId fields
|
|
1147
|
-
objectIdFields.forEach(fieldPath => {
|
|
1156
|
+
findObjectIdFields(schemaDefinition).forEach((fieldPath) => {
|
|
1148
1157
|
schema.index({ [fieldPath]: 1 });
|
|
1149
1158
|
});
|
|
1150
|
-
|
|
1151
1159
|
return schema;
|
|
1152
1160
|
};
|
|
1153
1161
|
|
|
1154
|
-
const generateModel = (gqlType, onModelCreated) => {
|
|
1162
|
+
const generateModel = (gqlType, onModelCreated, { createCollection = true } = {}) => {
|
|
1155
1163
|
const schemaDefinition = generateSchemaDefinition(gqlType);
|
|
1156
1164
|
const schema = createSchemaWithIndexes(schemaDefinition);
|
|
1157
1165
|
const model = mongoose.model(gqlType.name, schema, gqlType.name);
|
|
1158
1166
|
if (onModelCreated) {
|
|
1159
1167
|
onModelCreated(model);
|
|
1160
1168
|
}
|
|
1161
|
-
if (!preventCollectionCreation) {
|
|
1169
|
+
if (createCollection && !preventCollectionCreation) {
|
|
1162
1170
|
model.createCollection();
|
|
1163
1171
|
}
|
|
1164
1172
|
return model;
|
|
1165
1173
|
};
|
|
1166
1174
|
|
|
1167
|
-
const
|
|
1168
|
-
const schemaDefinition = generateSchemaDefinition(gqlType);
|
|
1169
|
-
const schema = createSchemaWithIndexes(schemaDefinition);
|
|
1170
|
-
const model = mongoose.model(gqlType.name, schema, gqlType.name);
|
|
1171
|
-
if (onModelCreated) {
|
|
1172
|
-
onModelCreated(model);
|
|
1173
|
-
}
|
|
1174
|
-
// Never create collection for no-endpoint types
|
|
1175
|
-
return model;
|
|
1176
|
-
};
|
|
1175
|
+
const coerceIdArray = (value) => value.map((element) => new mongoose.Types.ObjectId(element));
|
|
1177
1176
|
|
|
1178
1177
|
const buildMatchesClause = (fieldname, operator, value) => {
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
} else if (operator === QLOperator.getValue('LTE').value) {
|
|
1191
|
-
matches[fieldname] = { $lte: value };
|
|
1192
|
-
} else if (operator === QLOperator.getValue('GTE').value) {
|
|
1193
|
-
matches[fieldname] = { $gte: value };
|
|
1194
|
-
} else if (operator === QLOperator.getValue('NE').value) {
|
|
1195
|
-
matches[fieldname] = { $ne: value };
|
|
1196
|
-
} else if (operator === QLOperator.getValue('BTW').value) {
|
|
1197
|
-
matches[fieldname] = { $gte: value[0], $lte: value[1] };
|
|
1198
|
-
} else if (operator === QLOperator.getValue('IN').value) {
|
|
1199
|
-
let fixedArray = value;
|
|
1200
|
-
if (value && fieldname.endsWith('_id')) {
|
|
1201
|
-
fixedArray = [];
|
|
1202
|
-
value.forEach((element) => {
|
|
1203
|
-
fixedArray.push(new mongoose.Types.ObjectId(element));
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
|
-
matches[fieldname] = { $in: fixedArray };
|
|
1207
|
-
} else if (operator === QLOperator.getValue('NIN').value) {
|
|
1208
|
-
let fixedArray = value;
|
|
1209
|
-
if (value && fieldname.endsWith('_id')) {
|
|
1210
|
-
fixedArray = [];
|
|
1211
|
-
value.forEach((element) => {
|
|
1212
|
-
fixedArray.push(new mongoose.Types.ObjectId(element));
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
matches[fieldname] = { $nin: fixedArray };
|
|
1216
|
-
} else if (operator === QLOperator.getValue('LIKE').value) {
|
|
1217
|
-
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 } };
|
|
1218
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
|
+
};
|
|
1219
1205
|
|
|
1220
|
-
|
|
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
|
+
});
|
|
1221
1216
|
};
|
|
1222
1217
|
|
|
1223
1218
|
const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
1224
1219
|
const aggregateClauses = {};
|
|
1220
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1225
1221
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
fieldType = qlField.type.ofType;
|
|
1222
|
+
if (!(fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType))) {
|
|
1223
|
+
return aggregateClauses;
|
|
1229
1224
|
}
|
|
1230
|
-
if (fieldType instanceof GraphQLObjectType
|
|
1231
|
-
|| isNonNullOfType(fieldType, GraphQLObjectType)) {
|
|
1232
|
-
if (fieldType instanceof GraphQLNonNull) {
|
|
1233
|
-
fieldType = qlField.type.ofType;
|
|
1234
|
-
}
|
|
1235
|
-
filterField.terms.forEach((term) => {
|
|
1236
|
-
if (qlField.extensions && qlField.extensions.relation
|
|
1237
|
-
&& !qlField.extensions.relation.embedded) {
|
|
1238
|
-
const { model } = typesDict.types[fieldType.name];
|
|
1239
|
-
const { collectionName } = model.collection;
|
|
1240
|
-
const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1241
|
-
if (!aggregateClauses[fieldName]) {
|
|
1242
|
-
let lookup = {};
|
|
1243
|
-
|
|
1244
|
-
if (qlField.type instanceof GraphQLList) {
|
|
1245
|
-
lookup = {
|
|
1246
|
-
$lookup: {
|
|
1247
|
-
from: collectionName,
|
|
1248
|
-
foreignField: localFieldName,
|
|
1249
|
-
localField: '_id',
|
|
1250
|
-
as: fieldName,
|
|
1251
|
-
},
|
|
1252
|
-
};
|
|
1253
|
-
} else {
|
|
1254
|
-
lookup = {
|
|
1255
|
-
$lookup: {
|
|
1256
|
-
from: collectionName,
|
|
1257
|
-
foreignField: '_id',
|
|
1258
|
-
localField: localFieldName,
|
|
1259
|
-
as: fieldName,
|
|
1260
|
-
},
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
1225
|
|
|
1264
|
-
|
|
1265
|
-
lookup,
|
|
1266
|
-
unwind: { $unwind: { path: `$${fieldName}`, preserveNullAndEmptyArrays: true } },
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1226
|
+
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1270
1227
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
let aliasPath = fieldName;
|
|
1277
|
-
let embeddedPath = '';
|
|
1278
|
-
|
|
1279
|
-
term.path.split('.').forEach((pathFieldName) => {
|
|
1280
|
-
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1281
|
-
if (pathField.type instanceof GraphQLObjectType
|
|
1282
|
-
|| pathField.type instanceof GraphQLList
|
|
1283
|
-
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1284
|
-
let pathFieldType = pathField.type;
|
|
1285
|
-
if (pathField.type instanceof GraphQLList || pathField.type instanceof GraphQLNonNull) {
|
|
1286
|
-
pathFieldType = pathField.type.ofType;
|
|
1287
|
-
}
|
|
1288
|
-
currentGQLPathFieldType = pathFieldType;
|
|
1289
|
-
if (pathField.extensions && pathField.extensions.relation
|
|
1290
|
-
&& !pathField.extensions.relation.embedded) {
|
|
1291
|
-
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1292
|
-
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1293
|
-
|
|
1294
|
-
embeddedPath = '';
|
|
1295
|
-
|
|
1296
|
-
const pathModel = typesDict.types[pathFieldType.name].model;
|
|
1297
|
-
const fieldPathCollectionName = pathModel.collection.collectionName;
|
|
1298
|
-
const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1299
|
-
|
|
1300
|
-
if (!aggregateClauses[aliasPath]) {
|
|
1301
|
-
let lookup = {};
|
|
1302
|
-
if (pathField.type instanceof GraphQLList) {
|
|
1303
|
-
lookup = {
|
|
1304
|
-
$lookup: {
|
|
1305
|
-
from: fieldPathCollectionName,
|
|
1306
|
-
foreignField: pathLocalFieldName,
|
|
1307
|
-
localField: `${currentPath}._id`,
|
|
1308
|
-
as: aliasPath,
|
|
1309
|
-
},
|
|
1310
|
-
};
|
|
1311
|
-
} else {
|
|
1312
|
-
lookup = {
|
|
1313
|
-
$lookup: {
|
|
1314
|
-
from: fieldPathCollectionName,
|
|
1315
|
-
foreignField: '_id',
|
|
1316
|
-
localField: `${currentPath}.${pathLocalFieldName}`,
|
|
1317
|
-
as: aliasPath,
|
|
1318
|
-
},
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
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
|
+
}
|
|
1321
1233
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
+
);
|
|
1331
1255
|
}
|
|
1256
|
+
} else {
|
|
1257
|
+
embeddedPath = embeddedPath === '' ? pathFieldName : `${embeddedPath}.${pathFieldName}`;
|
|
1332
1258
|
}
|
|
1333
|
-
}
|
|
1259
|
+
}
|
|
1334
1260
|
});
|
|
1335
|
-
}
|
|
1261
|
+
});
|
|
1336
1262
|
return aggregateClauses;
|
|
1337
1263
|
};
|
|
1338
1264
|
|
|
1339
1265
|
const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
1340
1266
|
const aggregateClauses = {};
|
|
1341
1267
|
const matchesClauses = {};
|
|
1268
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1342
1269
|
|
|
1343
|
-
let fieldType = qlField.type;
|
|
1344
|
-
if (qlField.type instanceof GraphQLList) {
|
|
1345
|
-
fieldType = qlField.type.ofType;
|
|
1346
|
-
}
|
|
1347
1270
|
if (fieldType instanceof GraphQLScalarType
|
|
1348
1271
|
|| isNonNullOfType(fieldType, GraphQLScalarType)
|
|
1349
1272
|
|| fieldType instanceof GraphQLEnumType
|
|
1350
1273
|
|| isNonNullOfType(fieldType, GraphQLEnumType)) {
|
|
1351
|
-
|
|
1352
|
-
if (isGraphQLisoDate(fieldTypeName)) {
|
|
1353
|
-
if (Array.isArray(filterField.value)) {
|
|
1354
|
-
filterField.value = filterField.value.map((value) => value && new Date(value));
|
|
1355
|
-
} else {
|
|
1356
|
-
filterField.value = filterField.value && new Date(filterField.value);
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1274
|
+
coerceDateValue(fieldType, filterField, 'value');
|
|
1359
1275
|
matchesClauses[fieldName] = buildMatchesClause(fieldName === 'id' ? '_id' : fieldName, filterField.operator, filterField.value);
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
if (fieldType instanceof GraphQLNonNull) {
|
|
1363
|
-
fieldType = qlField.type.ofType;
|
|
1364
|
-
}
|
|
1276
|
+
return { aggregateClauses, matchesClauses };
|
|
1277
|
+
}
|
|
1365
1278
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
const { model } = typesDict.types[fieldType.name];
|
|
1370
|
-
const { collectionName } = model.collection;
|
|
1371
|
-
const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1372
|
-
if (!aggregateClauses[fieldName]) {
|
|
1373
|
-
let lookup = {};
|
|
1374
|
-
|
|
1375
|
-
if (qlField.type instanceof GraphQLList) {
|
|
1376
|
-
lookup = {
|
|
1377
|
-
$lookup: {
|
|
1378
|
-
from: collectionName,
|
|
1379
|
-
foreignField: localFieldName,
|
|
1380
|
-
localField: '_id',
|
|
1381
|
-
as: fieldName,
|
|
1382
|
-
},
|
|
1383
|
-
};
|
|
1384
|
-
} else {
|
|
1385
|
-
lookup = {
|
|
1386
|
-
$lookup: {
|
|
1387
|
-
from: collectionName,
|
|
1388
|
-
foreignField: '_id',
|
|
1389
|
-
localField: localFieldName,
|
|
1390
|
-
as: fieldName,
|
|
1391
|
-
},
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1279
|
+
if (!(fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType))) {
|
|
1280
|
+
return { aggregateClauses, matchesClauses };
|
|
1281
|
+
}
|
|
1394
1282
|
|
|
1395
|
-
|
|
1396
|
-
lookup,
|
|
1397
|
-
unwind: { $unwind: { path: `$${fieldName}`, preserveNullAndEmptyArrays: true } },
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1283
|
+
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1401
1284
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
term.value = term.value.map((value) => value && new Date(value));
|
|
1408
|
-
} else {
|
|
1409
|
-
term.value = term.value && new Date(term.value);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
matchesClauses[fieldName] = buildMatchesClause(`${fieldName}.${fieldType.getFields()[term.path].name === 'id' ? '_id' : term.path}`, term.operator, term.value);
|
|
1413
|
-
} else {
|
|
1414
|
-
let currentGQLPathFieldType = qlField.type;
|
|
1415
|
-
if (currentGQLPathFieldType instanceof GraphQLList
|
|
1416
|
-
|| currentGQLPathFieldType instanceof GraphQLNonNull) {
|
|
1417
|
-
currentGQLPathFieldType = currentGQLPathFieldType.ofType;
|
|
1418
|
-
}
|
|
1419
|
-
let aliasPath = fieldName;
|
|
1420
|
-
let embeddedPath = '';
|
|
1421
|
-
|
|
1422
|
-
term.path.split('.').forEach((pathFieldName) => {
|
|
1423
|
-
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1424
|
-
if (pathField.type instanceof GraphQLScalarType
|
|
1425
|
-
|| isNonNullOfType(pathField.type, GraphQLScalarType)) {
|
|
1426
|
-
const typeName = pathField.type instanceof GraphQLNonNull ? getEffectiveTypeName(pathField.type.ofType) : getEffectiveTypeName(pathField.type);
|
|
1427
|
-
if (isGraphQLisoDate(typeName)) {
|
|
1428
|
-
if (Array.isArray(term.value)) {
|
|
1429
|
-
term.value = term.value.map((value) => value && new Date(value));
|
|
1430
|
-
} else {
|
|
1431
|
-
term.value = term.value && new Date(term.value);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
matchesClauses[`${aliasPath}_${pathFieldName}`] = buildMatchesClause(aliasPath + (embeddedPath !== '' ? `.${embeddedPath}.` : '.') + (pathFieldName === 'id' ? '_id' : pathFieldName), term.operator, term.value);
|
|
1435
|
-
embeddedPath = '';
|
|
1436
|
-
} else if (pathField.type instanceof GraphQLObjectType
|
|
1437
|
-
|| pathField.type instanceof GraphQLList
|
|
1438
|
-
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1439
|
-
let pathFieldType = pathField.type;
|
|
1440
|
-
if (pathField.type instanceof GraphQLList || pathField.type instanceof GraphQLNonNull) {
|
|
1441
|
-
pathFieldType = pathField.type.ofType;
|
|
1442
|
-
}
|
|
1443
|
-
currentGQLPathFieldType = pathFieldType;
|
|
1444
|
-
if (pathField.extensions && pathField.extensions.relation
|
|
1445
|
-
&& !pathField.extensions.relation.embedded) {
|
|
1446
|
-
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1447
|
-
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1448
|
-
|
|
1449
|
-
embeddedPath = '';
|
|
1450
|
-
|
|
1451
|
-
const pathModel = typesDict.types[pathFieldType.name].model;
|
|
1452
|
-
const fieldPathCollectionName = pathModel.collection.collectionName;
|
|
1453
|
-
const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1454
|
-
|
|
1455
|
-
if (!aggregateClauses[aliasPath]) {
|
|
1456
|
-
let lookup = {};
|
|
1457
|
-
if (pathField.type instanceof GraphQLList) {
|
|
1458
|
-
lookup = {
|
|
1459
|
-
$lookup: {
|
|
1460
|
-
from: fieldPathCollectionName,
|
|
1461
|
-
foreignField: pathLocalFieldName,
|
|
1462
|
-
localField: `${currentPath}._id`,
|
|
1463
|
-
as: aliasPath,
|
|
1464
|
-
},
|
|
1465
|
-
};
|
|
1466
|
-
} else {
|
|
1467
|
-
lookup = {
|
|
1468
|
-
$lookup: {
|
|
1469
|
-
from: fieldPathCollectionName,
|
|
1470
|
-
foreignField: '_id',
|
|
1471
|
-
localField: `${currentPath}.${pathLocalFieldName}`,
|
|
1472
|
-
as: aliasPath,
|
|
1473
|
-
},
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
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
|
+
}
|
|
1476
1290
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
+
);
|
|
1487
1327
|
}
|
|
1488
|
-
}
|
|
1328
|
+
} else {
|
|
1329
|
+
embeddedPath = embeddedPath === '' ? pathFieldName : `${embeddedPath}.${pathFieldName}`;
|
|
1330
|
+
}
|
|
1489
1331
|
}
|
|
1490
1332
|
});
|
|
1491
|
-
}
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1492
1335
|
return { aggregateClauses, matchesClauses };
|
|
1493
1336
|
};
|
|
1494
1337
|
|
|
1495
|
-
const
|
|
1496
|
-
const aggregateClauses = [];
|
|
1497
|
-
const matchesClauses = { $match: {} };
|
|
1498
|
-
let addMatch = false;
|
|
1499
|
-
let limitClause = { $limit: 100 };
|
|
1500
|
-
let skipClause = { $skip: 0 };
|
|
1501
|
-
let sortClause = {};
|
|
1502
|
-
let addSort = false;
|
|
1503
|
-
const aggregationsIncluded = {};
|
|
1338
|
+
const MAX_FILTER_GROUP_DEPTH = 5;
|
|
1504
1339
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1340
|
+
const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, aggregationsIncluded, depth = 0) => {
|
|
1341
|
+
if (depth > MAX_FILTER_GROUP_DEPTH) {
|
|
1342
|
+
throw new SimfinityError('Filter nesting too deep', 'FILTER_DEPTH_EXCEEDED', 400);
|
|
1343
|
+
}
|
|
1508
1344
|
|
|
1509
|
-
|
|
1345
|
+
const parts = [];
|
|
1346
|
+
const fields = gqltype.getFields();
|
|
1510
1347
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1348
|
+
if (filterGroup.conditions?.length > 0) {
|
|
1349
|
+
for (const condition of filterGroup.conditions) {
|
|
1350
|
+
const qlField = fields[condition.field];
|
|
1351
|
+
if (!qlField) {
|
|
1352
|
+
throw new SimfinityError(`Unknown filter field: ${condition.field}`, 'INVALID_FILTER_FIELD', 400);
|
|
1353
|
+
}
|
|
1517
1354
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1355
|
+
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1356
|
+
const isObject = fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType);
|
|
1357
|
+
|
|
1358
|
+
let filterInput;
|
|
1359
|
+
if (isObject) {
|
|
1360
|
+
if (!condition.path) {
|
|
1361
|
+
throw new SimfinityError(`Filter on object field "${condition.field}" requires a path`, 'MISSING_FILTER_PATH', 400);
|
|
1527
1362
|
}
|
|
1363
|
+
filterInput = {
|
|
1364
|
+
terms: [{ path: condition.path, operator: condition.operator, value: condition.value }],
|
|
1365
|
+
};
|
|
1366
|
+
} else {
|
|
1367
|
+
filterInput = { operator: condition.operator, value: condition.value };
|
|
1528
1368
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1369
|
+
|
|
1370
|
+
const result = await buildQueryTerms(filterInput, qlField, condition.field);
|
|
1371
|
+
if (!result) continue;
|
|
1372
|
+
|
|
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;
|
|
1377
|
+
}
|
|
1534
1378
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
let fixedSortField = sort.field;
|
|
1539
|
-
|
|
1540
|
-
if (sort.field.indexOf('.') >= 0) {
|
|
1541
|
-
const sortParts = sort.field.split('.');
|
|
1542
|
-
|
|
1543
|
-
fixedSortField = sortParts[0];
|
|
1544
|
-
|
|
1545
|
-
for (let i = 1; i < sortParts.length - 1; i++) {
|
|
1546
|
-
fixedSortField += `_${sortParts[i]}`;
|
|
1547
|
-
}
|
|
1548
|
-
fixedSortField += `.${sortParts[sortParts.length - 1]}`;
|
|
1549
|
-
const qlField = gqltype.getFields()[sortParts[0]];
|
|
1550
|
-
const path = sort.field.slice(sort.field.indexOf('.') + 1);
|
|
1551
|
-
const aggreagtionsForSort = buildAggregationsForSort({ terms: [{ path }] }, qlField, sortParts[0]);
|
|
1552
|
-
for (const [prop, aggregate] of Object.entries(aggreagtionsForSort)) {
|
|
1553
|
-
if (!aggregationsIncluded[prop]) {
|
|
1554
|
-
aggregateClauses.push(aggregate.lookup);
|
|
1555
|
-
aggregateClauses.push(aggregate.unwind);
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1379
|
+
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
1380
|
+
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1381
|
+
parts.push({ [matchKey]: match });
|
|
1558
1382
|
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1559
1386
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1387
|
+
if (filterGroup.AND?.length > 0) {
|
|
1388
|
+
for (const subGroup of filterGroup.AND) {
|
|
1389
|
+
const subMatch = await buildFilterGroupMatch(subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1);
|
|
1390
|
+
if (subMatch) parts.push(subMatch);
|
|
1564
1391
|
}
|
|
1565
1392
|
}
|
|
1566
1393
|
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
1394
|
+
if (filterGroup.OR?.length > 0) {
|
|
1395
|
+
const orParts = [];
|
|
1396
|
+
for (const subGroup of filterGroup.OR) {
|
|
1397
|
+
const subMatch = await buildFilterGroupMatch(subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1);
|
|
1398
|
+
if (subMatch) orParts.push(subMatch);
|
|
1399
|
+
}
|
|
1400
|
+
if (orParts.length === 1) {
|
|
1401
|
+
parts.push(orParts[0]);
|
|
1402
|
+
} else if (orParts.length > 1) {
|
|
1403
|
+
parts.push({ $or: orParts });
|
|
1404
|
+
}
|
|
1569
1405
|
}
|
|
1570
1406
|
|
|
1571
|
-
if (
|
|
1572
|
-
|
|
1407
|
+
if (parts.length === 0) return null;
|
|
1408
|
+
if (parts.length === 1) return parts[0];
|
|
1409
|
+
return { $and: parts };
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
const RESERVED_QUERY_KEYS = new Set(['pagination', 'sort', 'AND', 'OR', 'aggregation']);
|
|
1413
|
+
|
|
1414
|
+
const collectFiltersAndLookups = async (input, gqltype, aggregateClauses, aggregationsIncluded) => {
|
|
1415
|
+
const flatMatchConditions = {};
|
|
1416
|
+
let hasFlat = false;
|
|
1417
|
+
const fields = gqltype.getFields();
|
|
1418
|
+
|
|
1419
|
+
for (const [key, filterField] of Object.entries(input)) {
|
|
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;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1573
1435
|
}
|
|
1574
1436
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1437
|
+
const topLevelAndParts = [];
|
|
1438
|
+
if (hasFlat) topLevelAndParts.push(flatMatchConditions);
|
|
1439
|
+
|
|
1440
|
+
if (input.AND?.length > 0) {
|
|
1441
|
+
for (const group of input.AND) {
|
|
1442
|
+
const groupMatch = await buildFilterGroupMatch(group, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1443
|
+
if (groupMatch) topLevelAndParts.push(groupMatch);
|
|
1444
|
+
}
|
|
1578
1445
|
}
|
|
1579
1446
|
|
|
1447
|
+
if (input.OR?.length > 0) {
|
|
1448
|
+
const orParts = [];
|
|
1449
|
+
for (const group of input.OR) {
|
|
1450
|
+
const groupMatch = await buildFilterGroupMatch(group, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1451
|
+
if (groupMatch) orParts.push(groupMatch);
|
|
1452
|
+
}
|
|
1453
|
+
if (orParts.length === 1) {
|
|
1454
|
+
topLevelAndParts.push(orParts[0]);
|
|
1455
|
+
} else if (orParts.length > 1) {
|
|
1456
|
+
topLevelAndParts.push({ $or: orParts });
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (topLevelAndParts.length === 1) return { $match: topLevelAndParts[0] };
|
|
1461
|
+
if (topLevelAndParts.length > 1) return { $match: { $and: topLevelAndParts } };
|
|
1462
|
+
return null;
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
const buildSortClause = (sortTerms, gqltype, aggregateClauses, aggregationsIncluded) => {
|
|
1466
|
+
const sortExpressions = {};
|
|
1467
|
+
const fields = gqltype.getFields();
|
|
1468
|
+
|
|
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);
|
|
1499
|
+
|
|
1580
1500
|
if (isCount) {
|
|
1581
1501
|
aggregateClauses.push({ $count: 'size' });
|
|
1502
|
+
return aggregateClauses;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
if (input.sort) {
|
|
1506
|
+
aggregateClauses.push(buildSortClause(input.sort.terms, gqltype, aggregateClauses, aggregationsIncluded));
|
|
1582
1507
|
}
|
|
1583
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
|
+
|
|
1584
1518
|
return aggregateClauses;
|
|
1585
1519
|
};
|
|
1586
1520
|
|
|
1587
1521
|
const buildFieldPath = (gqltype, fieldPath) => {
|
|
1588
|
-
// This function resolves a field path (e.g., "category" or "country.name")
|
|
1589
|
-
// and returns the MongoDB field path and any necessary lookups
|
|
1590
1522
|
const pathParts = fieldPath.split('.');
|
|
1591
|
-
const
|
|
1523
|
+
const lookupPairs = [];
|
|
1592
1524
|
let currentPath = '';
|
|
1593
1525
|
let currentGQLType = gqltype;
|
|
1594
|
-
|
|
1595
|
-
for (
|
|
1596
|
-
const part = pathParts[i];
|
|
1526
|
+
|
|
1527
|
+
for (const part of pathParts) {
|
|
1597
1528
|
const field = currentGQLType.getFields()[part];
|
|
1598
|
-
|
|
1599
1529
|
if (!field) {
|
|
1600
1530
|
throw new Error(`Field ${part} not found in type ${currentGQLType.name}`);
|
|
1601
1531
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if ((fieldType instanceof GraphQLObjectType) &&
|
|
1610
|
-
field.extensions && field.extensions.relation &&
|
|
1611
|
-
!field.extensions.relation.embedded) {
|
|
1612
|
-
|
|
1613
|
-
const relatedModel = typesDict.types[fieldType.name].model;
|
|
1614
|
-
const collectionName = relatedModel.collection.collectionName;
|
|
1615
|
-
const connectionField = field.extensions.relation.connectionField || part;
|
|
1616
|
-
|
|
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;
|
|
1617
1539
|
const lookupAlias = currentPath ? `${currentPath}_${part}` : part;
|
|
1618
|
-
const localField = currentPath ? `${currentPath}.${
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
localField,
|
|
1625
|
-
as: lookupAlias,
|
|
1626
|
-
},
|
|
1627
|
-
});
|
|
1628
|
-
|
|
1629
|
-
aggregateClauses.push({
|
|
1630
|
-
$unwind: { path: `$${lookupAlias}`, preserveNullAndEmptyArrays: true },
|
|
1631
|
-
});
|
|
1632
|
-
|
|
1540
|
+
const localField = currentPath ? `${currentPath}.${connField}` : connField;
|
|
1541
|
+
|
|
1542
|
+
lookupPairs.push(buildRelationLookup({
|
|
1543
|
+
collectionName, localField, foreignField: '_id', alias: lookupAlias,
|
|
1544
|
+
}));
|
|
1545
|
+
|
|
1633
1546
|
currentPath = lookupAlias;
|
|
1634
1547
|
currentGQLType = fieldType;
|
|
1635
|
-
} else if (fieldType instanceof GraphQLObjectType &&
|
|
1636
|
-
field.extensions && field.extensions.relation &&
|
|
1637
|
-
field.extensions.relation.embedded) {
|
|
1638
|
-
// Embedded object - just append to path
|
|
1548
|
+
} else if (fieldType instanceof GraphQLObjectType && relation?.embedded) {
|
|
1639
1549
|
currentPath = currentPath ? `${currentPath}.${part}` : part;
|
|
1640
1550
|
currentGQLType = fieldType;
|
|
1641
1551
|
} else {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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;
|
|
1648
1566
|
}
|
|
1649
1567
|
}
|
|
1650
|
-
|
|
1651
|
-
return { mongoPath: currentPath, lookups: aggregateClauses };
|
|
1652
1568
|
};
|
|
1653
1569
|
|
|
1654
1570
|
const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
1655
1571
|
const aggregateClauses = [];
|
|
1656
|
-
const matchesClauses = { $match: {} };
|
|
1657
|
-
let addMatch = false;
|
|
1658
1572
|
const aggregationsIncluded = {};
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
// Build filter and lookup clauses (similar to buildQuery)
|
|
1664
|
-
for (const [key, filterField] of Object.entries(input)) {
|
|
1665
|
-
if (Object.prototype.hasOwnProperty.call(input, key) && key !== 'pagination' && key !== 'sort' && key !== 'aggregation') {
|
|
1666
|
-
const qlField = gqltype.getFields()[key];
|
|
1667
|
-
|
|
1668
|
-
const result = await buildQueryTerms(filterField, qlField, key);
|
|
1669
|
-
|
|
1670
|
-
if (result) {
|
|
1671
|
-
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1672
|
-
aggregateClauses.push(aggregate.lookup);
|
|
1673
|
-
aggregateClauses.push(aggregate.unwind);
|
|
1674
|
-
aggregationsIncluded[prop] = true;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
for (const [matchClauseKey, matchClause] of Object.entries(result.matchesClauses)) {
|
|
1678
|
-
if (Object.prototype.hasOwnProperty.call(result.matchesClauses, matchClauseKey)) {
|
|
1679
|
-
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1680
|
-
if (Object.prototype.hasOwnProperty.call(matchClause, matchKey)) {
|
|
1681
|
-
matchesClauses.$match[matchKey] = match;
|
|
1682
|
-
addMatch = true;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
} else if (key === 'sort' && filterField && filterField.terms && filterField.terms.length > 0) {
|
|
1689
|
-
// Extract all sort terms
|
|
1690
|
-
filterField.terms.forEach(sortTerm => {
|
|
1691
|
-
sortTerms.push({
|
|
1692
|
-
field: sortTerm.field || 'groupId',
|
|
1693
|
-
direction: sortTerm.order === 'ASC' ? 1 : -1,
|
|
1694
|
-
});
|
|
1695
|
-
});
|
|
1696
|
-
} else if (key === 'pagination' && filterField) {
|
|
1697
|
-
// Handle pagination (ignore count parameter)
|
|
1698
|
-
if (filterField.page && filterField.size) {
|
|
1699
|
-
const skip = filterField.size * (filterField.page - 1);
|
|
1700
|
-
limitClause = { $limit: filterField.size + skip };
|
|
1701
|
-
skipClause = { $skip: skip };
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
if (addMatch) {
|
|
1707
|
-
aggregateClauses.push(matchesClauses);
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
// Now build the aggregation with $group
|
|
1573
|
+
|
|
1574
|
+
const matchStage = await collectFiltersAndLookups(input, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1575
|
+
if (matchStage) aggregateClauses.push(matchStage);
|
|
1576
|
+
|
|
1711
1577
|
const { groupId, facts } = aggregationExpression;
|
|
1712
|
-
|
|
1713
|
-
// Resolve the groupId field path
|
|
1714
1578
|
const groupIdPath = buildFieldPath(gqltype, groupId);
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
groupIdPath.
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
}
|
|
1727
|
-
aggregationsIncluded[lookupAlias] = true;
|
|
1728
|
-
}
|
|
1729
|
-
});
|
|
1730
|
-
|
|
1731
|
-
// Build the $group stage
|
|
1732
|
-
const groupStage = {
|
|
1733
|
-
$group: {
|
|
1734
|
-
_id: `$${groupIdPath.mongoPath}`,
|
|
1735
|
-
},
|
|
1736
|
-
};
|
|
1737
|
-
|
|
1738
|
-
// Add aggregation operations for each fact
|
|
1739
|
-
facts.forEach(fact => {
|
|
1740
|
-
const { operation, factName, path } = fact;
|
|
1741
|
-
const factPath = buildFieldPath(gqltype, path);
|
|
1742
|
-
|
|
1743
|
-
// Add any lookups needed for the fact field
|
|
1744
|
-
factPath.lookups.forEach(lookup => {
|
|
1745
|
-
const lookupKey = Object.keys(lookup)[0];
|
|
1746
|
-
const lookupAlias = lookup[lookupKey].as;
|
|
1747
|
-
if (!aggregationsIncluded[lookupAlias]) {
|
|
1748
|
-
aggregateClauses.push(lookup);
|
|
1749
|
-
// Check if next item is an unwind for this lookup
|
|
1750
|
-
const unwindItem = factPath.lookups[factPath.lookups.indexOf(lookup) + 1];
|
|
1751
|
-
if (unwindItem && unwindItem.$unwind) {
|
|
1752
|
-
aggregateClauses.push(unwindItem);
|
|
1753
|
-
}
|
|
1754
|
-
aggregationsIncluded[lookupAlias] = true;
|
|
1755
|
-
}
|
|
1756
|
-
});
|
|
1757
|
-
|
|
1758
|
-
// Map GraphQL operations to MongoDB aggregation operators
|
|
1759
|
-
let mongoOperation;
|
|
1760
|
-
switch (operation) {
|
|
1761
|
-
case 'SUM':
|
|
1762
|
-
mongoOperation = { $sum: `$${factPath.mongoPath}` };
|
|
1763
|
-
break;
|
|
1764
|
-
case 'COUNT':
|
|
1765
|
-
mongoOperation = { $sum: 1 };
|
|
1766
|
-
break;
|
|
1767
|
-
case 'AVG':
|
|
1768
|
-
mongoOperation = { $avg: `$${factPath.mongoPath}` };
|
|
1769
|
-
break;
|
|
1770
|
-
case 'MIN':
|
|
1771
|
-
mongoOperation = { $min: `$${factPath.mongoPath}` };
|
|
1772
|
-
break;
|
|
1773
|
-
case 'MAX':
|
|
1774
|
-
mongoOperation = { $max: `$${factPath.mongoPath}` };
|
|
1775
|
-
break;
|
|
1776
|
-
default:
|
|
1777
|
-
throw new Error(`Unknown aggregation operation: ${operation}`);
|
|
1579
|
+
appendLookupPairs(aggregateClauses, aggregationsIncluded, groupIdPath.lookupPairs);
|
|
1580
|
+
|
|
1581
|
+
const groupStage = { $group: { _id: `$${groupIdPath.mongoPath}` } };
|
|
1582
|
+
|
|
1583
|
+
facts.forEach((fact) => {
|
|
1584
|
+
const factPath = buildFieldPath(gqltype, fact.path);
|
|
1585
|
+
appendLookupPairs(aggregateClauses, aggregationsIncluded, factPath.lookupPairs);
|
|
1586
|
+
|
|
1587
|
+
const builder = AGG_OP_TO_MONGO[fact.operation];
|
|
1588
|
+
if (!builder) {
|
|
1589
|
+
throw new Error(`Unknown aggregation operation: ${fact.operation}`);
|
|
1778
1590
|
}
|
|
1779
|
-
|
|
1780
|
-
groupStage.$group[factName] = mongoOperation;
|
|
1591
|
+
groupStage.$group[fact.factName] = builder(factPath.mongoPath);
|
|
1781
1592
|
});
|
|
1782
|
-
|
|
1593
|
+
|
|
1783
1594
|
aggregateClauses.push(groupStage);
|
|
1784
|
-
|
|
1785
|
-
// Add a final projection stage to format the output
|
|
1595
|
+
|
|
1786
1596
|
aggregateClauses.push({
|
|
1787
1597
|
$project: {
|
|
1788
1598
|
_id: 0,
|
|
1789
1599
|
groupId: '$_id',
|
|
1790
|
-
facts: Object.fromEntries(facts.map(fact => [fact.factName, `$${fact.factName}`])),
|
|
1600
|
+
facts: Object.fromEntries(facts.map((fact) => [fact.factName, `$${fact.factName}`])),
|
|
1791
1601
|
},
|
|
1792
1602
|
});
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
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);
|
|
1796
1607
|
const sortObject = {};
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
if (sortTerm.field !== 'groupId') {
|
|
1803
|
-
// Check if the field is one of the fact names
|
|
1804
|
-
if (factNames.includes(sortTerm.field)) {
|
|
1805
|
-
sortFieldPath = `facts.${sortTerm.field}`;
|
|
1806
|
-
}
|
|
1807
|
-
// If not found, default to groupId (already set)
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
sortObject[sortFieldPath] = sortTerm.direction;
|
|
1811
|
-
});
|
|
1812
|
-
|
|
1813
|
-
// Add sort stage with all sort fields
|
|
1814
|
-
aggregateClauses.push({
|
|
1815
|
-
$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;
|
|
1816
1612
|
});
|
|
1613
|
+
aggregateClauses.push({ $sort: sortObject });
|
|
1817
1614
|
} else {
|
|
1818
|
-
|
|
1819
|
-
aggregateClauses.push({
|
|
1820
|
-
$sort: { groupId: 1 },
|
|
1821
|
-
});
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// Add pagination if provided
|
|
1825
|
-
if (limitClause) {
|
|
1826
|
-
aggregateClauses.push(limitClause);
|
|
1615
|
+
aggregateClauses.push({ $sort: { groupId: 1 } });
|
|
1827
1616
|
}
|
|
1828
|
-
|
|
1829
|
-
|
|
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 });
|
|
1830
1621
|
}
|
|
1831
|
-
|
|
1622
|
+
|
|
1832
1623
|
return aggregateClauses;
|
|
1833
1624
|
};
|
|
1834
1625
|
|
|
@@ -1841,7 +1632,6 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1841
1632
|
if (!shouldNotBeIncludedInSchema(includedTypes, type.gqltype)) {
|
|
1842
1633
|
const wasAddedAsNoEnpointType = !type.simpleEntityEndpointName;
|
|
1843
1634
|
if (!wasAddedAsNoEnpointType) {
|
|
1844
|
-
// Fixing resolve method in order to be compliant with Mongo _id field
|
|
1845
1635
|
if (type.gqltype.getFields().id && !type.gqltype.getFields().id.resolve) {
|
|
1846
1636
|
type.gqltype.getFields().id.resolve = (parent) => parent._id;
|
|
1847
1637
|
}
|
|
@@ -1850,54 +1640,26 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1850
1640
|
type: type.gqltype,
|
|
1851
1641
|
args: { id: { type: GraphQLID } },
|
|
1852
1642
|
async resolve(parent, args, context) {
|
|
1853
|
-
/* Here we define how to get data from database source
|
|
1854
|
-
this will return the type with id passed in argument
|
|
1855
|
-
by the user */
|
|
1856
1643
|
const params = {
|
|
1857
|
-
type,
|
|
1858
|
-
args,
|
|
1859
|
-
operation: 'get_by_id',
|
|
1860
|
-
context,
|
|
1644
|
+
type, args, operation: 'get_by_id', context,
|
|
1861
1645
|
};
|
|
1862
1646
|
excecuteMiddleware(params);
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
if (hasScope) {
|
|
1868
|
-
// Build query args with id filter - scope function will modify this
|
|
1869
|
-
const queryArgs = {
|
|
1870
|
-
id: { operator: 'EQ', value: args.id },
|
|
1871
|
-
};
|
|
1872
|
-
|
|
1873
|
-
// Create temporary params with queryArgs for scope function
|
|
1874
|
-
const scopeParams = {
|
|
1875
|
-
type,
|
|
1876
|
-
args: queryArgs,
|
|
1877
|
-
operation: 'get_by_id',
|
|
1878
|
-
context,
|
|
1879
|
-
};
|
|
1880
|
-
|
|
1881
|
-
// Execute scope which will modify queryArgs in place
|
|
1882
|
-
await executeScope(scopeParams);
|
|
1883
|
-
|
|
1884
|
-
// Build aggregation pipeline from the combined filters
|
|
1885
|
-
const aggregateClauses = await buildQuery(queryArgs, type.gqltype);
|
|
1886
|
-
|
|
1887
|
-
// Execute the query and get the first result
|
|
1888
|
-
let result;
|
|
1889
|
-
if (aggregateClauses.length === 0) {
|
|
1890
|
-
result = await type.model.findOne({ _id: args.id });
|
|
1891
|
-
} else {
|
|
1892
|
-
const results = await type.model.aggregate(aggregateClauses);
|
|
1893
|
-
result = results.length > 0 ? results[0] : null;
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
return result;
|
|
1897
|
-
} else {
|
|
1898
|
-
// No scope defined, use the original findById
|
|
1899
|
-
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);
|
|
1900
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 });
|
|
1660
|
+
}
|
|
1661
|
+
const results = await type.model.aggregate(aggregateClauses);
|
|
1662
|
+
return results[0] || null;
|
|
1901
1663
|
},
|
|
1902
1664
|
};
|
|
1903
1665
|
|
|
@@ -1918,23 +1680,23 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1918
1680
|
excecuteMiddleware(params);
|
|
1919
1681
|
await executeScope(params);
|
|
1920
1682
|
const aggregateClauses = await buildQuery(args, type.gqltype);
|
|
1921
|
-
|
|
1922
|
-
const aggregateClausesForCount = await buildQuery(args, type.gqltype, true);
|
|
1923
|
-
const resultCount = await type.model.aggregate(aggregateClausesForCount);
|
|
1924
|
-
context.count = resultCount[0] ? resultCount[0].size : 0;
|
|
1925
|
-
}
|
|
1683
|
+
const wantsCount = !!(args.pagination && args.pagination.count);
|
|
1926
1684
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
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;
|
|
1932
1695
|
}
|
|
1933
1696
|
return result;
|
|
1934
1697
|
},
|
|
1935
1698
|
};
|
|
1936
1699
|
|
|
1937
|
-
// Add aggregate endpoint
|
|
1938
1700
|
const aggregateArgsObject = { ...argsObject };
|
|
1939
1701
|
aggregateArgsObject.aggregation = {
|
|
1940
1702
|
type: new GraphQLNonNull(QLTypeAggregationExpression),
|
|
@@ -1964,33 +1726,24 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1964
1726
|
return new GraphQLObjectType(rootQueryArgs);
|
|
1965
1727
|
};
|
|
1966
1728
|
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
export const createSchema = (includedQueryTypes,
|
|
1970
|
-
includedMutationTypes, includedCustomMutations) => {
|
|
1971
|
-
|
|
1972
|
-
// Generate models for all registered types now that all types are available
|
|
1973
|
-
Object.values(typesDict.types).forEach(typeInfo => {
|
|
1729
|
+
export const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomMutations) => {
|
|
1730
|
+
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
1974
1731
|
if (typeInfo.gqltype && !typeInfo.model) {
|
|
1975
1732
|
if (typeInfo.endpoint) {
|
|
1976
|
-
// Generate model with collection for endpoint types (types registered with connect)
|
|
1977
1733
|
typeInfo.model = generateModel(typeInfo.gqltype, typeInfo.onModelCreated);
|
|
1978
1734
|
} else if (typeInfo.needsModel) {
|
|
1979
|
-
|
|
1980
|
-
typeInfo.model = generateModelWithoutCollection(typeInfo.gqltype, null);
|
|
1735
|
+
typeInfo.model = generateModel(typeInfo.gqltype, null, { createCollection: false });
|
|
1981
1736
|
}
|
|
1982
1737
|
}
|
|
1983
1738
|
});
|
|
1984
1739
|
|
|
1985
|
-
|
|
1986
|
-
Object.keys(typesDict.types).forEach(typeName => {
|
|
1740
|
+
Object.keys(typesDict.types).forEach((typeName) => {
|
|
1987
1741
|
if (typesDictForUpdate.types[typeName]) {
|
|
1988
1742
|
typesDictForUpdate.types[typeName].model = typesDict.types[typeName].model;
|
|
1989
1743
|
}
|
|
1990
1744
|
});
|
|
1991
1745
|
|
|
1992
|
-
|
|
1993
|
-
Object.values(typesDict.types).forEach(typeInfo => {
|
|
1746
|
+
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
1994
1747
|
if (typeInfo.gqltype) {
|
|
1995
1748
|
autoGenerateResolvers(typeInfo.gqltype);
|
|
1996
1749
|
}
|
|
@@ -2008,7 +1761,6 @@ export const getType = (typeName) => {
|
|
|
2008
1761
|
if (typeof typeName === 'string') {
|
|
2009
1762
|
return typesDict.types[typeName]?.gqltype;
|
|
2010
1763
|
}
|
|
2011
|
-
// If it's already a GraphQL type object, get by its name
|
|
2012
1764
|
if (typeName && typeName.name) {
|
|
2013
1765
|
return typesDict.types[typeName.name]?.gqltype;
|
|
2014
1766
|
}
|
|
@@ -2028,68 +1780,41 @@ const autoGenerateResolvers = (gqltype) => {
|
|
|
2028
1780
|
const fields = gqltype.getFields();
|
|
2029
1781
|
|
|
2030
1782
|
for (const [fieldName, fieldEntry] of Object.entries(fields)) {
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
delete argsObject[connectionField];
|
|
2048
|
-
const argsArray = Object.entries(argsObject);
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
const graphqlArgs = formatArgs(argsArray);
|
|
2052
|
-
|
|
2053
|
-
fieldEntry.args = graphqlArgs;
|
|
2054
|
-
|
|
2055
|
-
fieldEntry.resolve = async (parent, args) => {
|
|
2056
|
-
// Lazy lookup of the related model
|
|
2057
|
-
|
|
2058
|
-
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
2059
|
-
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
args[connectionField] = {
|
|
2063
|
-
terms: [{
|
|
2064
|
-
path: 'id',
|
|
2065
|
-
operator: 'EQ',
|
|
2066
|
-
value: parent.id || parent._id,
|
|
2067
|
-
}],
|
|
2068
|
-
};
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
const aggregateClauses = await buildQuery(args, relatedTypeInfo.gqltype);
|
|
2072
|
-
|
|
2073
|
-
return await relatedTypeInfo.model.aggregate(aggregateClauses);
|
|
2074
|
-
};
|
|
2075
|
-
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
2076
|
-
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
|
|
2077
|
-
// Single object field - generate resolve for one-to-one relationship
|
|
2078
|
-
const relatedType = fieldEntry.type instanceof GraphQLNonNull ? fieldEntry.type.ofType : fieldEntry.type;
|
|
2079
|
-
const connectionField = relation.connectionField || fieldName;
|
|
2080
|
-
|
|
2081
|
-
fieldEntry.resolve = async (parent) => {
|
|
2082
|
-
// Lazy lookup of the related model
|
|
2083
|
-
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
2084
|
-
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
2085
|
-
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
2086
|
-
}
|
|
2087
|
-
const relatedId = parent[connectionField] || parent[fieldName];
|
|
2088
|
-
return relatedId ? await relatedTypeInfo.model.findById(relatedId) : null;
|
|
2089
|
-
};
|
|
2090
|
-
}
|
|
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().`);
|
|
2091
1798
|
}
|
|
2092
|
-
|
|
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
|
+
};
|
|
2093
1818
|
}
|
|
2094
1819
|
}
|
|
2095
1820
|
};
|
|
@@ -2101,43 +1826,36 @@ export const connect = (model, gqltype, simpleEntityEndpointName,
|
|
|
2101
1826
|
gqltype,
|
|
2102
1827
|
};
|
|
2103
1828
|
typesDict.types[gqltype.name] = {
|
|
2104
|
-
model
|
|
1829
|
+
model,
|
|
2105
1830
|
gqltype,
|
|
2106
1831
|
simpleEntityEndpointName,
|
|
2107
1832
|
listEntitiesEndpointName,
|
|
2108
1833
|
endpoint: true,
|
|
2109
1834
|
controller,
|
|
2110
1835
|
stateMachine,
|
|
2111
|
-
onModelCreated,
|
|
1836
|
+
onModelCreated,
|
|
2112
1837
|
};
|
|
2113
1838
|
|
|
2114
1839
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
2115
1840
|
};
|
|
2116
1841
|
|
|
2117
1842
|
export const addNoEndpointType = (gqltype) => {
|
|
2118
|
-
waitingInputType[gqltype.name] = {
|
|
2119
|
-
gqltype,
|
|
2120
|
-
};
|
|
1843
|
+
waitingInputType[gqltype.name] = { gqltype };
|
|
2121
1844
|
|
|
2122
|
-
// Check if this type has relationship fields that might need a model
|
|
2123
1845
|
const fields = gqltype.getFields();
|
|
2124
1846
|
let needsModel = false;
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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))) {
|
|
2130
1852
|
needsModel = true;
|
|
2131
1853
|
break;
|
|
2132
1854
|
}
|
|
2133
1855
|
}
|
|
2134
1856
|
|
|
2135
1857
|
typesDict.types[gqltype.name] = {
|
|
2136
|
-
gqltype,
|
|
2137
|
-
endpoint: false,
|
|
2138
|
-
// Model will be generated later in createSchema if needed
|
|
2139
|
-
model: null,
|
|
2140
|
-
needsModel, // Store whether this type needs a model
|
|
1858
|
+
gqltype, endpoint: false, model: null, needsModel,
|
|
2141
1859
|
};
|
|
2142
1860
|
|
|
2143
1861
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
@@ -2150,6 +1868,8 @@ export { default as scalars } from './scalars.js';
|
|
|
2150
1868
|
export { default as plugins } from './plugins.js';
|
|
2151
1869
|
export { default as auth } from './auth/index.js';
|
|
2152
1870
|
|
|
1871
|
+
export { buildQuery, buildFilterGroupMatch };
|
|
1872
|
+
|
|
2153
1873
|
const createArgsForQuery = (argTypes) => {
|
|
2154
1874
|
const argsObject = {};
|
|
2155
1875
|
|
|
@@ -2182,6 +1902,13 @@ const createArgsForQuery = (argTypes) => {
|
|
|
2182
1902
|
|
|
2183
1903
|
argsObject.sort = {};
|
|
2184
1904
|
argsObject.sort.type = QLSortExpression;
|
|
1905
|
+
|
|
1906
|
+
argsObject.AND = {};
|
|
1907
|
+
argsObject.AND.type = new GraphQLList(QLFilterGroup);
|
|
1908
|
+
|
|
1909
|
+
argsObject.OR = {};
|
|
1910
|
+
argsObject.OR.type = new GraphQLList(QLFilterGroup);
|
|
1911
|
+
|
|
2185
1912
|
return argsObject;
|
|
2186
1913
|
};
|
|
2187
1914
|
|