@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.
Files changed (49) hide show
  1. package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +23 -0
  2. package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
  3. package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
  4. package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
  5. package/.claude/worktrees/agitated-kepler/README.md +3941 -0
  6. package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
  7. package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
  8. package/.claude/worktrees/agitated-kepler/package.json +41 -0
  9. package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
  10. package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
  11. package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
  12. package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
  13. package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
  14. package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
  15. package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
  16. package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
  17. package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
  18. package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
  19. package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
  20. package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
  21. package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
  22. package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
  23. package/.claude/worktrees/distracted-swirles-4601fe/.claude/settings.local.json +10 -0
  24. package/.claude/worktrees/distracted-swirles-4601fe/AGGREGATION_CHANGES_SUMMARY.md +235 -0
  25. package/.claude/worktrees/distracted-swirles-4601fe/AGGREGATION_EXAMPLE.md +567 -0
  26. package/.claude/worktrees/distracted-swirles-4601fe/LICENSE +201 -0
  27. package/.claude/worktrees/distracted-swirles-4601fe/README.md +3941 -0
  28. package/.claude/worktrees/distracted-swirles-4601fe/eslint.config.mjs +71 -0
  29. package/.claude/worktrees/distracted-swirles-4601fe/package-lock.json +4740 -0
  30. package/.claude/worktrees/distracted-swirles-4601fe/package.json +41 -0
  31. package/.claude/worktrees/distracted-swirles-4601fe/src/auth/errors.js +44 -0
  32. package/.claude/worktrees/distracted-swirles-4601fe/src/auth/expressions.js +273 -0
  33. package/.claude/worktrees/distracted-swirles-4601fe/src/auth/index.js +391 -0
  34. package/.claude/worktrees/distracted-swirles-4601fe/src/auth/rules.js +274 -0
  35. package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLOperator.js +39 -0
  36. package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLSort.js +28 -0
  37. package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLValue.js +39 -0
  38. package/.claude/worktrees/distracted-swirles-4601fe/src/errors/internal-server.error.js +11 -0
  39. package/.claude/worktrees/distracted-swirles-4601fe/src/errors/simfinity.error.js +15 -0
  40. package/.claude/worktrees/distracted-swirles-4601fe/src/index.js +1925 -0
  41. package/.claude/worktrees/distracted-swirles-4601fe/src/plugins.js +53 -0
  42. package/.claude/worktrees/distracted-swirles-4601fe/src/scalars.js +188 -0
  43. package/.claude/worktrees/distracted-swirles-4601fe/src/validators.js +250 -0
  44. package/.claude/worktrees/distracted-swirles-4601fe/yarn.lock +1154 -0
  45. package/.cursor/rules/simfinity-core-functions.mdc +3 -1
  46. package/README.md +202 -0
  47. package/package.json +1 -1
  48. package/src/index.js +623 -896
  49. 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, // IDs are represented as strings in AST
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
- const fieldToExclude = connectionField;
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 executeRegisteredMutation = async (args, callback, session) => {
668
+ const MAX_TRANSIENT_RETRIES = 5;
669
+
670
+ const withTransaction = async (session, body) => {
601
671
  const mySession = session || await mongoose.startSession();
602
- await mySession.startTransaction();
603
- try {
604
- const newObject = await callback(args, mySession);
605
- await mySession.commitTransaction();
606
- mySession.endSession();
607
- return newObject;
608
- } catch (error) {
609
- await mySession.abortTransaction();
610
- if (error.errorLabels && error.errorLabels.includes('TransientTransactionError')) {
611
- return executeRegisteredMutation(args, callback, mySession);
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 onDeleteObject = async (Model, gqltype, controller, args, session, context) => {
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
- Object.entries(argTypes).forEach(([fieldEntryName, fieldEntry]) => {
668
- if (fieldEntry.extensions && fieldEntry.extensions.relation
669
- && fieldEntry.extensions.relation.embedded) {
670
- const oldObjectData = currentObject[fieldEntryName];
671
- const newObjectData = materializedModel.modelArgs[fieldEntryName];
672
- if (newObjectData) {
673
- if (Array.isArray(oldObjectData) && Array.isArray(newObjectData)) {
674
- materializedModel.modelArgs[fieldEntryName] = newObjectData;
675
- } else {
676
- materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
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
- if (args[fieldEntryName] === null
682
- && !(fieldEntry.type instanceof GraphQLNonNull)) {
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 = async (Model, gqltype, controller,
765
- args, operation, actionField, session, context) => {
766
- const mySession = session || await mongoose.startSession();
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
- newObject = await onSaveObject(Model, gqltype, controller, args, mySession, null, context);
773
- break;
848
+ return onSaveObject(Model, gqltype, controller, args, mySession, null, context);
774
849
  case operations.UPDATE:
775
- newObject = await onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
776
- break;
850
+ return onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
777
851
  case operations.DELETE:
778
- newObject = await onDeleteObject(Model, gqltype, controller, args, mySession, context);
779
- break;
852
+ return onDelete(Model, controller, args, mySession, context);
780
853
  case operations.STATE_CHANGED:
781
- newObject = await onStateChanged(Model, gqltype, controller, args, mySession, actionField, context);
782
- break;
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
- mySession.endSession();
793
- throw error;
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 onDeleteSubject(typesDict.types[collectionGQLType.name].model,
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
- // Call the scope function with the same params as middleware
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
- // Helper function to get the base scalar type for custom validated scalars
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 (fieldEntry.type === GraphQLID || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLID)) {
1081
+ if (matchesScalar(type, GraphQLID)) {
1018
1082
  schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
1019
- } else if (fieldEntry.type === GraphQLString
1020
- || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLString)
1021
- || (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLString)
1022
- || (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
1023
- if (fieldEntry.extensions && fieldEntry.extensions.unique) {
1024
- schemaArg[fieldEntryName] = { type: String, unique: true };
1025
- } else {
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 (fieldEntry.type instanceof GraphQLObjectType
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
- schemaArg[fieldEntry.extensions.relation.connectionField ? fieldEntry.extensions.relation.connectionField : fieldEntry.name] = mongoose
1063
- .Schema.Types.ObjectId;
1094
+ const key = fieldEntry.extensions.relation.connectionField || fieldEntry.name;
1095
+ schemaArg[key] = mongoose.Schema.Types.ObjectId;
1064
1096
  } else {
1065
- let entryType = fieldEntry.type;
1066
- if (entryType instanceof GraphQLNonNull) {
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 (fieldEntry.type instanceof GraphQLList) {
1104
+ } else if (type instanceof GraphQLList) {
1077
1105
  if (fieldEntry.extensions && fieldEntry.extensions.relation) {
1078
1106
  if (fieldEntry.extensions.relation.embedded) {
1079
- const entryType = fieldEntry.type.ofType;
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 (fieldEntry.type.ofType === GraphQLString
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 (fieldEntry.type.ofType === GraphQLBoolean
1091
- || (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
1114
+ } else if (listItemMatchesScalar(type, GraphQLBoolean)) {
1092
1115
  schemaArg[fieldEntryName] = [Boolean];
1093
- } else if (fieldEntry.type.ofType === GraphQLInt || fieldEntry.type.ofType === GraphQLFloat
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(fieldEntry.type.ofType))) {
1118
+ } else if (isGraphQLisoDate(getEffectiveTypeName(type.ofType))) {
1097
1119
  schemaArg[fieldEntryName] = [Date];
1098
1120
  }
1099
- } else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type))
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
- // Array of embedded objects - recursively check for ObjectId fields
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 (typeof fieldDefinition === 'object' && !fieldDefinition.type) {
1130
- // Embedded object - recursively check for ObjectId fields
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 generateModelWithoutCollection = (gqlType, onModelCreated) => {
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 matches = {};
1180
- if (operator === QLOperator.getValue('EQ').value || !operator) {
1181
- let fixedValue = value;
1182
- if (fieldname.endsWith('_id')) {
1183
- fixedValue = new mongoose.Types.ObjectId(value);
1184
- }
1185
- matches[fieldname] = fixedValue;
1186
- } else if (operator === QLOperator.getValue('LT').value) {
1187
- matches[fieldname] = { $lt: value };
1188
- } else if (operator === QLOperator.getValue('GT').value) {
1189
- matches[fieldname] = { $gt: value };
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
- return matches;
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
- let fieldType = qlField.type;
1227
- if (qlField.type instanceof GraphQLList) {
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
- aggregateClauses[fieldName] = {
1265
- lookup,
1266
- unwind: { $unwind: { path: `$${fieldName}`, preserveNullAndEmptyArrays: true } },
1267
- };
1268
- }
1269
- }
1226
+ const resolvedFieldType = unwrapNonNull(fieldType);
1270
1227
 
1271
- let currentGQLPathFieldType = qlField.type;
1272
- if (currentGQLPathFieldType instanceof GraphQLList
1273
- || currentGQLPathFieldType instanceof GraphQLNonNull) {
1274
- currentGQLPathFieldType = currentGQLPathFieldType.ofType;
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
- aggregateClauses[aliasPath] = {
1323
- lookup,
1324
- unwind: { $unwind: { path: `$${aliasPath}`, preserveNullAndEmptyArrays: true } },
1325
- };
1326
- }
1327
- } else if (embeddedPath === '') {
1328
- embeddedPath += pathFieldName;
1329
- } else {
1330
- embeddedPath += `.${pathFieldName}`;
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
- const fieldTypeName = fieldType instanceof GraphQLNonNull ? getEffectiveTypeName(fieldType.ofType) : getEffectiveTypeName(fieldType);
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
- } else if (fieldType instanceof GraphQLObjectType
1361
- || isNonNullOfType(fieldType, GraphQLObjectType)) {
1362
- if (fieldType instanceof GraphQLNonNull) {
1363
- fieldType = qlField.type.ofType;
1364
- }
1276
+ return { aggregateClauses, matchesClauses };
1277
+ }
1365
1278
 
1366
- filterField.terms.forEach((term) => {
1367
- if (qlField.extensions && qlField.extensions.relation
1368
- && !qlField.extensions.relation.embedded) {
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
- aggregateClauses[fieldName] = {
1396
- lookup,
1397
- unwind: { $unwind: { path: `$${fieldName}`, preserveNullAndEmptyArrays: true } },
1398
- };
1399
- }
1400
- }
1283
+ const resolvedFieldType = unwrapNonNull(fieldType);
1401
1284
 
1402
- if (term.path.indexOf('.') < 0) {
1403
- const { type } = fieldType.getFields()[term.path];
1404
- const typeName = type instanceof GraphQLNonNull ? getEffectiveTypeName(type.ofType) : getEffectiveTypeName(type);
1405
- if (isGraphQLisoDate(typeName)) {
1406
- if (Array.isArray(term.value)) {
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
- aggregateClauses[aliasPath] = {
1478
- lookup,
1479
- unwind: { $unwind: { path: `$${aliasPath}`, preserveNullAndEmptyArrays: true } },
1480
- };
1481
- }
1482
- } else if (embeddedPath === '') {
1483
- embeddedPath += pathFieldName;
1484
- } else {
1485
- embeddedPath += `.${pathFieldName}`;
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 buildQuery = async (input, gqltype, isCount) => {
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
- for (const [key, filterField] of Object.entries(input)) {
1506
- if (Object.prototype.hasOwnProperty.call(input, key) && key !== 'pagination' && key !== 'sort') {
1507
- const qlField = gqltype.getFields()[key];
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
- const result = await buildQueryTerms(filterField, qlField, key);
1345
+ const parts = [];
1346
+ const fields = gqltype.getFields();
1510
1347
 
1511
- if (result) {
1512
- for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
1513
- aggregateClauses.push(aggregate.lookup);
1514
- aggregateClauses.push(aggregate.unwind);
1515
- aggregationsIncluded[prop] = true;
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
- for (const [matchClauseKey, matchClause] of Object.entries(result.matchesClauses)) {
1519
- if (Object.prototype.hasOwnProperty.call(result.matchesClauses, matchClauseKey)) {
1520
- for (const [matchKey, match] of Object.entries(matchClause)) {
1521
- if (Object.prototype.hasOwnProperty.call(matchClause, matchKey)) {
1522
- matchesClauses.$match[matchKey] = match;
1523
- addMatch = true;
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
- } else if (key === 'pagination') {
1530
- if (filterField.page && filterField.size) {
1531
- const skip = filterField.size * (filterField.page - 1);
1532
- limitClause = { $limit: filterField.size + skip };
1533
- skipClause = { $skip: skip };
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
- } else if (key === 'sort') {
1536
- const sortExpressions = {};
1537
- filterField.terms.forEach((sort) => {
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
- sortExpressions[fixedSortField] = sort.order === 'ASC' ? 1 : -1;
1561
- });
1562
- sortClause = { $sort: sortExpressions };
1563
- addSort = true;
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 (addMatch) {
1568
- aggregateClauses.push(matchesClauses);
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 (addSort && !isCount) {
1572
- aggregateClauses.push(sortClause);
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
- if (!isCount) {
1576
- aggregateClauses.push(limitClause);
1577
- aggregateClauses.push(skipClause);
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 aggregateClauses = [];
1523
+ const lookupPairs = [];
1592
1524
  let currentPath = '';
1593
1525
  let currentGQLType = gqltype;
1594
-
1595
- for (let i = 0; i < pathParts.length; i++) {
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
- let fieldType = field.type;
1604
- if (fieldType instanceof GraphQLNonNull || fieldType instanceof GraphQLList) {
1605
- fieldType = fieldType.ofType;
1606
- }
1607
-
1608
- // If it's an object type with non-embedded relation, we need a lookup
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}.${connectionField}` : connectionField;
1619
-
1620
- aggregateClauses.push({
1621
- $lookup: {
1622
- from: collectionName,
1623
- foreignField: '_id',
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
- // Scalar field - final part of path
1643
- if (part === 'id') {
1644
- currentPath = currentPath ? `${currentPath}._id` : '_id';
1645
- } else {
1646
- currentPath = currentPath ? `${currentPath}.${part}` : part;
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
- const sortTerms = []; // Store multiple sort terms
1660
- let limitClause = null;
1661
- let skipClause = null;
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
- // Add any lookups needed for the groupId field
1717
- groupIdPath.lookups.forEach(lookup => {
1718
- const lookupKey = Object.keys(lookup)[0];
1719
- const lookupAlias = lookup[lookupKey].as;
1720
- if (!aggregationsIncluded[lookupAlias]) {
1721
- aggregateClauses.push(lookup);
1722
- // Check if next item is an unwind for this lookup
1723
- const unwindItem = groupIdPath.lookups[groupIdPath.lookups.indexOf(lookup) + 1];
1724
- if (unwindItem && unwindItem.$unwind) {
1725
- aggregateClauses.push(unwindItem);
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
- // Build sort object from multiple sort terms
1795
- if (sortTerms.length > 0) {
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
- const factNames = facts.map(fact => fact.factName);
1798
-
1799
- sortTerms.forEach(sortTerm => {
1800
- let sortFieldPath = 'groupId';
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
- // Default sort by groupId ascending if no sort terms provided
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
- if (skipClause) {
1829
- aggregateClauses.push(skipClause);
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
- // Check if scope is defined for get_by_id
1865
- const hasScope = type.gqltype.extensions && type.gqltype.extensions.scope && type.gqltype.extensions.scope.get_by_id;
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
- if (args.pagination && args.pagination.count) {
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
- let result;
1928
- if (aggregateClauses.length === 0) {
1929
- result = await type.model.find({});
1930
- } else {
1931
- result = await type.model.aggregate(aggregateClauses);
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
- /* Creating a new GraphQL Schema, with options query which defines query
1968
- we will allow users to use when they are making request. */
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
- // Generate model without collection for no-endpoint types that need models (addNoEndpointType)
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
- // Also update the typesDictForUpdate with the generated models
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
- // Auto-generate resolvers for all registered types now that all types are available
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
- // Skip if resolve method already exists
2032
- if (!fieldEntry.resolve) {
2033
- // Check if field has relation extension
2034
- if (fieldEntry.extensions && fieldEntry.extensions.relation) {
2035
- const { relation } = fieldEntry.extensions;
2036
-
2037
- // Only generate resolvers for non-embedded relationships
2038
- if (!relation.embedded) {
2039
- if (fieldEntry.type instanceof GraphQLList) {
2040
- // Collection field - generate resolve for one-to-many relationship
2041
- //This is a one-to-many resolver that will return a list of related objects. Also this one allows to filter the related objects as is in the find endpoint.
2042
- const relatedType = fieldEntry.type.ofType;
2043
- const connectionField = relation.connectionField || fieldName;
2044
- const relatedTypeInfo = typesDict.types[relatedType.name];
2045
- const argsObject = createArgsForQuery(relatedTypeInfo.gqltype.getFields());
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: model, // Will be generated later in createSchema if not provided
1829
+ model,
2105
1830
  gqltype,
2106
1831
  simpleEntityEndpointName,
2107
1832
  listEntitiesEndpointName,
2108
1833
  endpoint: true,
2109
1834
  controller,
2110
1835
  stateMachine,
2111
- onModelCreated, // Store the callback for later use
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
- for (const [, fieldEntry] of Object.entries(fields)) {
2127
- if (fieldEntry.extensions && fieldEntry.extensions.relation
2128
- && (fieldEntry.type instanceof GraphQLObjectType || fieldEntry.type instanceof GraphQLList
2129
- || (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType))) {
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