@simtlix/simfinity-js 2.5.0 → 2.6.1

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