@simtlix/simfinity-js 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -7,12 +7,12 @@ const QLOperator = require('./const/QLOperator');
7
7
  const QLValue = require('./const/QLValue');
8
8
  const QLSort = require('./const/QLSort');
9
9
 
10
- mongoose.set('useFindAndModify', false);
10
+ mongoose.set('strictQuery', false);
11
11
 
12
12
  const {
13
13
  GraphQLObjectType, GraphQLString, GraphQLID, GraphQLSchema, GraphQLList,
14
14
  GraphQLNonNull, GraphQLInputObjectType, GraphQLScalarType, __Field,
15
- GraphQLInt, GraphQLEnumType, GraphQLBoolean, GraphQLFloat,
15
+ GraphQLInt, GraphQLEnumType, GraphQLBoolean, GraphQLFloat, Kind,
16
16
  } = graphql;
17
17
 
18
18
  // Adding 'extensions' field into instronspection query
@@ -93,6 +93,12 @@ module.exports.SimfinityError = SimfinityError;
93
93
 
94
94
  module.exports.InternalServerError = InternalServerError;
95
95
 
96
+ let preventCollectionCreation = false;
97
+
98
+ module.exports.preventCreatingCollection = (prevent) => {
99
+ preventCollectionCreation = !!prevent;
100
+ };
101
+
96
102
  /* Schema defines data on the Graph like object types(book type), relation between
97
103
  these object types and describes how it can reach into the graph to interact with
98
104
  the data to retrieve or mutate the data */
@@ -159,26 +165,123 @@ const isNonNullOfTypeForNotScalar = (fieldEntryType, graphQLType) => {
159
165
  return isOfType;
160
166
  };
161
167
 
168
+ const getEffectiveTypeName = (type) => {
169
+ if (type instanceof GraphQLScalarType && type.baseScalarType) {
170
+ return type.baseScalarType.name;
171
+ }
172
+ return type.name;
173
+ };
174
+
162
175
  const isGraphQLisoDate = (typeName) => typeName === 'DateTime' || typeName === 'Date' || typeName === 'Time';
163
176
 
177
+ function createValidatedScalar(name, description, baseScalarType, validate) {
178
+ if (!baseScalarType) {
179
+ throw new Error('baseScalarType is required');
180
+ }
181
+
182
+ // Validate that baseScalarType is a valid GraphQL scalar type
183
+ if (!(baseScalarType instanceof GraphQLScalarType)) {
184
+ throw new Error('baseScalarType must be a valid GraphQL scalar type');
185
+ }
186
+
187
+ // Check if it's one of the standard GraphQL scalar types
188
+ const validScalarTypes = [GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID];
189
+ const isValidStandardType = validScalarTypes.some((type) => baseScalarType === type);
190
+
191
+ if (!isValidStandardType && !baseScalarType.name) {
192
+ throw new Error('baseScalarType must be a standard GraphQL scalar type or a custom scalar with a valid name');
193
+ }
194
+
195
+ const kindMap = {
196
+ String: Kind.STRING,
197
+ Int: Kind.INT,
198
+ Float: Kind.FLOAT,
199
+ Boolean: Kind.BOOLEAN,
200
+ ID: Kind.STRING, // IDs are represented as strings in AST
201
+ };
202
+
203
+ // Try to infer the kind from the baseScalarType name
204
+ const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
205
+
206
+ const scalar = new GraphQLScalarType({
207
+ name,
208
+ description,
209
+ serialize(value) {
210
+ validate(value);
211
+ return baseScalarType.serialize(value);
212
+ },
213
+ parseValue(value) {
214
+ validate(value);
215
+ return baseScalarType.parseValue(value);
216
+ },
217
+ parseLiteral(ast, variables) {
218
+ if (ast.kind !== baseKind) {
219
+ throw new Error(`${name} must be a ${baseScalarType.name}`);
220
+ }
221
+ const value = baseScalarType.parseLiteral(ast, variables);
222
+ validate(value);
223
+ return value;
224
+ },
225
+ });
226
+
227
+ scalar.baseScalarType = baseScalarType;
228
+ return scalar;
229
+ }
230
+
231
+ /**
232
+ * Creates a new GraphQLInputObjectType with a field excluded.
233
+ * @param {string} inputNamePrefix - The prefix for the input type name.
234
+ * @param {GraphQLInputObjectType} originalType - The original input type.
235
+ * @param {string} fieldToExclude - The name of the field to exclude.
236
+ * @returns {GraphQLInputObjectType} A new input type without the specified field.
237
+ */
238
+ const createTypeWithExcludedField = (inputNamePrefix, originalType, fieldToExclude) => {
239
+ const originalFields = originalType.getFields();
240
+ const newFields = Object.fromEntries(
241
+ Object.entries(originalFields).filter(([fieldName]) => fieldName !== fieldToExclude),
242
+ );
243
+
244
+ return new GraphQLInputObjectType({
245
+ name: `${inputNamePrefix}${originalType.name}For${fieldToExclude.charAt(0).toUpperCase() + fieldToExclude.slice(1)}`,
246
+ fields: newFields,
247
+ });
248
+ };
249
+
164
250
  const createOneToManyInputType = (inputNamePrefix, fieldEntryName,
165
- inputType, updateInputType) => new GraphQLInputObjectType({
166
- name: `OneToMany${inputNamePrefix}${fieldEntryName}`,
167
- fields: () => ({
168
- added: { type: new GraphQLList(inputType) },
169
- updated: { type: new GraphQLList(updateInputType) },
170
- deleted: { type: new GraphQLList(GraphQLID) },
171
- }),
172
- });
251
+ inputType, updateInputType, connectionField) => {
252
+ let inputTypeForAdd = inputType;
253
+
254
+ // If a gqltype is provided, create a new input type for 'added'
255
+ // that excludes the field named after the gqltype.
256
+ if (connectionField) {
257
+ const fieldToExclude = connectionField;
258
+ inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, fieldToExclude);
259
+ }
173
260
 
174
- const graphQLListInputType = (dict, fieldEntry, fieldEntryName, inputNamePrefix) => {
261
+ return new GraphQLInputObjectType({
262
+ name: `OneToMany${inputNamePrefix}${fieldEntryName}`,
263
+ fields: () => ({
264
+ added: {
265
+ type: new GraphQLList(inputTypeForAdd),
266
+ },
267
+ updated: {
268
+ type: new GraphQLList(updateInputType),
269
+ },
270
+ deleted: {
271
+ type: new GraphQLList(GraphQLID),
272
+ },
273
+ }),
274
+ });
275
+ };
276
+
277
+ const graphQLListInputType = (dict, fieldEntry, fieldEntryName, inputNamePrefix, connectionField) => {
175
278
  const { ofType } = fieldEntry.type;
176
279
 
177
280
  if (ofType instanceof GraphQLObjectType && dict.types[ofType.name].inputType) {
178
281
  if (!fieldEntry.extensions || !fieldEntry.extensions.relation
179
282
  || !fieldEntry.extensions.relation.embedded) {
180
283
  const oneToMany = createOneToManyInputType(inputNamePrefix, fieldEntryName,
181
- typesDict.types[ofType.name].inputType, typesDictForUpdate.types[ofType.name].inputType);
284
+ typesDict.types[ofType.name].inputType, typesDictForUpdate.types[ofType.name].inputType, connectionField);
182
285
  return oneToMany;
183
286
  }
184
287
  if (fieldEntry.extensions && fieldEntry.extensions.relation
@@ -211,8 +314,7 @@ const buildInputType = (gqltype) => {
211
314
  if (fieldEntry.type instanceof GraphQLScalarType
212
315
  || fieldEntry.type instanceof GraphQLEnumType
213
316
  || isNonNullOfType(fieldEntry.type, GraphQLScalarType)
214
- || isNonNullOfType(fieldEntry.type, GraphQLEnumType)
215
- ) {
317
+ || isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
216
318
  if (fieldEntryName !== 'id') {
217
319
  fieldArg.type = fieldEntry.type;
218
320
  }
@@ -244,8 +346,8 @@ const buildInputType = (gqltype) => {
244
346
  if (fieldEntry.type.ofType === gqltype) {
245
347
  selfReferenceCollections[fieldEntryName] = fieldEntry;
246
348
  } else {
247
- const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, 'A');
248
- const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, 'U');
349
+ const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, 'A', fieldEntry.extensions?.relation?.connectionField);
350
+ const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, 'U', fieldEntry.extensions?.relation?.connectionField);
249
351
  if (listInputTypeForAdd && listInputTypeForUpdate) {
250
352
  fieldArg.type = listInputTypeForAdd;
251
353
  fieldArgForUpdate.type = listInputTypeForUpdate;
@@ -288,7 +390,7 @@ const buildInputType = (gqltype) => {
288
390
  Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
289
391
  if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
290
392
  inputTypeForAddFields[fieldEntryName] = {
291
- type: createOneToManyInputType('A', fieldEntryName, inputTypeForAdd, inputTypeForUpdate),
393
+ type: createOneToManyInputType('A', fieldEntryName, inputTypeForAdd, inputTypeForUpdate, selfReferenceCollections[fieldEntryName].extensions?.relation?.connectionField),
292
394
  name: fieldEntryName,
293
395
  };
294
396
  }
@@ -301,7 +403,7 @@ const buildInputType = (gqltype) => {
301
403
  Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
302
404
  if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
303
405
  inputTypeForUpdateFields[fieldEntryName] = {
304
- type: createOneToManyInputType('U', fieldEntryName, inputTypeForAdd, inputTypeForUpdate),
406
+ type: createOneToManyInputType('U', fieldEntryName, inputTypeForAdd, inputTypeForUpdate, selfReferenceCollections[fieldEntryName].extensions?.relation?.connectionField),
305
407
  name: fieldEntryName,
306
408
  };
307
409
  }
@@ -382,6 +484,8 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
382
484
  null, operation, session)).modelArgs;
383
485
  }
384
486
  } else {
487
+ modelArgs[fieldEntry.name] = new mongoose.Types
488
+ .ObjectId(args[fieldEntryName].id);
385
489
  console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
386
490
  }
387
491
  } else if (fieldEntry.type instanceof GraphQLList) {
@@ -462,15 +566,24 @@ const iterateonCollectionFields = async (materializedModel, gqltype, objectId, s
462
566
  }
463
567
  };
464
568
 
465
- const onDeleteObject = async (Model, gqltype, controller, args, session, linkToParent) => {
466
- const result = await materializeModel(args, gqltype, linkToParent, 'DELETE', session);
467
- const deletedObject = new Model(result.modelArgs);
569
+ const onDeleteObject = async (Model, gqltype, controller, args, session) => {
570
+ const deletedObject = await Model.findById({ _id: args }).session(session).lean();
468
571
 
469
572
  if (controller && controller.onDelete) {
470
573
  await controller.onDelete(deletedObject, session);
471
574
  }
472
575
 
473
- return Model.findByIdAndDelete(args, deletedObject.modelArgs).session(session);
576
+ return Model.findByIdAndDelete({ _id: args }).session(session);
577
+ };
578
+
579
+ const onDeleteSubject = async (Model, controller, id, session) => {
580
+ const currentObject = await Model.findById({ _id: id }).session(session).lean();
581
+
582
+ if (controller && controller.onDelete) {
583
+ await controller.onDelete(currentObject, session);
584
+ }
585
+
586
+ return Model.findByIdAndDelete({ _id: id }).session(session);
474
587
  };
475
588
 
476
589
  const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent) => {
@@ -481,7 +594,6 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
481
594
  await iterateonCollectionFields(materializedModel, gqltype, objectId, session);
482
595
  }
483
596
 
484
- let modifiedObject = materializedModel.modelArgs;
485
597
  const currentObject = await Model.findById({ _id: objectId }).lean();
486
598
 
487
599
  const argTypes = gqltype.getFields();
@@ -490,32 +602,32 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
490
602
  if (fieldEntry.extensions && fieldEntry.extensions.relation
491
603
  && fieldEntry.extensions.relation.embedded) {
492
604
  const oldObjectData = currentObject[fieldEntryName];
493
- const newObjectData = modifiedObject[fieldEntryName];
605
+ const newObjectData = materializedModel.modelArgs[fieldEntryName];
494
606
  if (newObjectData) {
495
607
  if (Array.isArray(oldObjectData) && Array.isArray(newObjectData)) {
496
- modifiedObject[fieldEntryName] = newObjectData;
608
+ materializedModel.modelArgs[fieldEntryName] = newObjectData;
497
609
  } else {
498
- modifiedObject[fieldEntryName] = { ...oldObjectData, ...newObjectData };
610
+ materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
499
611
  }
500
612
  }
501
613
  }
502
614
 
503
615
  if (args[fieldEntryName] === null
504
616
  && !(fieldEntry.type instanceof GraphQLNonNull)) {
505
- modifiedObject = { ...modifiedObject, $unset: { [fieldEntryName]: '' } };
617
+ materializedModel.modelArgs = { ...materializedModel.modelArgs, $unset: { [fieldEntryName]: '' } };
506
618
  }
507
619
  });
508
620
 
509
621
  if (controller && controller.onUpdating) {
510
- await controller.onUpdating(objectId, modifiedObject, args, session);
622
+ await controller.onUpdating(objectId, materializedModel.modelArgs, session);
511
623
  }
512
624
 
513
625
  const result = Model.findByIdAndUpdate(
514
- objectId, modifiedObject, { new: true },
515
- );
626
+ objectId, materializedModel.modelArgs, { new: true },
627
+ ).session(session);
516
628
 
517
629
  if (controller && controller.onUpdated) {
518
- await controller.onUpdated(result, args, session);
630
+ await controller.onUpdated(result, session);
519
631
  }
520
632
 
521
633
  return result;
@@ -633,7 +745,10 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
633
745
  };
634
746
  break;
635
747
  case operations.DELETE:
636
- // TODO: implement
748
+ operationFunction = async (collectionItem) => {
749
+ await onDeleteSubject(typesDict.types[collectionGQLType.name].model,
750
+ typesDict.types[collectionGQLType.name].controller, collectionItem, session);
751
+ };
637
752
  }
638
753
 
639
754
  for (const element of collectionFieldsList) {
@@ -793,10 +908,18 @@ const generateSchemaDefinition = (gqlType) => {
793
908
  const schemaArg = {};
794
909
 
795
910
  for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
911
+ // Helper function to get the base scalar type for custom validated scalars
912
+ const getBaseScalarType = (scalarType) => scalarType.baseScalarType || scalarType;
913
+
914
+ // Helper function to check if a type is a custom validated scalar
915
+ const isCustomValidatedScalar = (type) => type instanceof GraphQLScalarType && type.baseScalarType;
916
+
796
917
  if (fieldEntry.type === GraphQLID || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLID)) {
797
918
  schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
798
919
  } else if (fieldEntry.type === GraphQLString
799
- || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLString)) {
920
+ || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLString)
921
+ || (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLString)
922
+ || (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
800
923
  if (fieldEntry.extensions && fieldEntry.extensions.unique) {
801
924
  schemaArg[fieldEntryName] = { type: String, unique: true };
802
925
  } else {
@@ -810,27 +933,33 @@ const generateSchemaDefinition = (gqlType) => {
810
933
  schemaArg[fieldEntryName] = String;
811
934
  }
812
935
  } else if (fieldEntry.type === GraphQLInt
813
- || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLInt)) {
936
+ || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLInt)
937
+ || (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLInt)
938
+ || (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt)) {
814
939
  if (fieldEntry.extensions && fieldEntry.extensions.unique) {
815
940
  schemaArg[fieldEntryName] = { type: Number, unique: true };
816
941
  } else {
817
942
  schemaArg[fieldEntryName] = Number;
818
943
  }
819
944
  } else if (fieldEntry.type === GraphQLFloat
820
- || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLFloat)) {
945
+ || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLFloat)
946
+ || (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLFloat)
947
+ || (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat)) {
821
948
  if (fieldEntry.extensions && fieldEntry.extensions.unique) {
822
949
  schemaArg[fieldEntryName] = { type: Number, unique: true };
823
950
  } else {
824
951
  schemaArg[fieldEntryName] = Number;
825
952
  }
826
953
  } else if (fieldEntry.type === GraphQLBoolean
827
- || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLBoolean)) {
954
+ || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLBoolean)
955
+ || (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLBoolean)
956
+ || (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
828
957
  schemaArg[fieldEntryName] = Boolean;
829
958
  } else if (fieldEntry.type instanceof GraphQLObjectType
830
959
  || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
831
960
  if (fieldEntry.extensions && fieldEntry.extensions.relation) {
832
961
  if (!fieldEntry.extensions.relation.embedded) {
833
- schemaArg[fieldEntry.extensions.relation.connectionField] = mongoose
962
+ schemaArg[fieldEntry.extensions.relation.connectionField ? fieldEntry.extensions.relation.connectionField : fieldEntry.name] = mongoose
834
963
  .Schema.Types.ObjectId;
835
964
  } else {
836
965
  let entryType = fieldEntry.type;
@@ -855,17 +984,20 @@ const generateSchemaDefinition = (gqlType) => {
855
984
  }
856
985
  }
857
986
  } else if (fieldEntry.type.ofType === GraphQLString
858
- || fieldEntry.type.ofType instanceof GraphQLEnumType) {
987
+ || fieldEntry.type.ofType instanceof GraphQLEnumType
988
+ || (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
859
989
  schemaArg[fieldEntryName] = [String];
860
- } else if (fieldEntry.type.ofType === GraphQLBoolean) {
990
+ } else if (fieldEntry.type.ofType === GraphQLBoolean
991
+ || (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
861
992
  schemaArg[fieldEntryName] = [Boolean];
862
- } else if (fieldEntry.type.ofType === GraphQLInt || fieldEntry.type.ofType === GraphQLFloat) {
993
+ } else if (fieldEntry.type.ofType === GraphQLInt || fieldEntry.type.ofType === GraphQLFloat
994
+ || (isCustomValidatedScalar(fieldEntry.type.ofType) && (getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt || getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat))) {
863
995
  schemaArg[fieldEntryName] = [Number];
864
- } else if (isGraphQLisoDate(fieldEntry.type.ofType.name)) {
996
+ } else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType))) {
865
997
  schemaArg[fieldEntryName] = [Date];
866
998
  }
867
- } else if (isGraphQLisoDate(fieldEntry.type.name)
868
- || (fieldEntry.type instanceof GraphQLNonNull && isGraphQLisoDate(fieldEntry.type.ofType.name))) {
999
+ } else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type))
1000
+ || (fieldEntry.type instanceof GraphQLNonNull && isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType)))) {
869
1001
  schemaArg[fieldEntryName] = Date;
870
1002
  }
871
1003
  }
@@ -878,7 +1010,18 @@ const generateModel = (gqlType, onModelCreated) => {
878
1010
  if (onModelCreated) {
879
1011
  onModelCreated(model);
880
1012
  }
881
- model.createCollection();
1013
+ if (!preventCollectionCreation) {
1014
+ model.createCollection();
1015
+ }
1016
+ return model;
1017
+ };
1018
+
1019
+ const generateModelWithoutCollection = (gqlType, onModelCreated) => {
1020
+ const model = mongoose.model(gqlType.name, generateSchemaDefinition(gqlType), gqlType.name);
1021
+ if (onModelCreated) {
1022
+ onModelCreated(model);
1023
+ }
1024
+ // Never create collection for no-endpoint types
882
1025
  return model;
883
1026
  };
884
1027
 
@@ -939,13 +1082,12 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
939
1082
  if (fieldType instanceof GraphQLNonNull) {
940
1083
  fieldType = qlField.type.ofType;
941
1084
  }
942
-
943
1085
  filterField.terms.forEach((term) => {
944
1086
  if (qlField.extensions && qlField.extensions.relation
945
1087
  && !qlField.extensions.relation.embedded) {
946
1088
  const { model } = typesDict.types[fieldType.name];
947
1089
  const { collectionName } = model.collection;
948
- const localFieldName = qlField.extensions.relation.connectionField;
1090
+ const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
949
1091
  if (!aggregateClauses[fieldName]) {
950
1092
  let lookup = {};
951
1093
 
@@ -1003,7 +1145,7 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
1003
1145
 
1004
1146
  const pathModel = typesDict.types[pathFieldType.name].model;
1005
1147
  const fieldPathCollectionName = pathModel.collection.collectionName;
1006
- const pathLocalFieldName = pathField.extensions.relation.connectionField;
1148
+ const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
1007
1149
 
1008
1150
  if (!aggregateClauses[aliasPath]) {
1009
1151
  let lookup = {};
@@ -1056,7 +1198,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1056
1198
  || isNonNullOfType(fieldType, GraphQLScalarType)
1057
1199
  || fieldType instanceof GraphQLEnumType
1058
1200
  || isNonNullOfType(fieldType, GraphQLEnumType)) {
1059
- const fieldTypeName = fieldType instanceof GraphQLNonNull ? fieldType.ofType.name : fieldType.name;
1201
+ const fieldTypeName = fieldType instanceof GraphQLNonNull ? getEffectiveTypeName(fieldType.ofType) : getEffectiveTypeName(fieldType);
1060
1202
  if (isGraphQLisoDate(fieldTypeName)) {
1061
1203
  if (Array.isArray(filterField.value)) {
1062
1204
  filterField.value = filterField.value.map((value) => value && new Date(value));
@@ -1076,7 +1218,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1076
1218
  && !qlField.extensions.relation.embedded) {
1077
1219
  const { model } = typesDict.types[fieldType.name];
1078
1220
  const { collectionName } = model.collection;
1079
- const localFieldName = qlField.extensions.relation.connectionField;
1221
+ const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
1080
1222
  if (!aggregateClauses[fieldName]) {
1081
1223
  let lookup = {};
1082
1224
 
@@ -1109,7 +1251,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1109
1251
 
1110
1252
  if (term.path.indexOf('.') < 0) {
1111
1253
  const { type } = fieldType.getFields()[term.path];
1112
- const typeName = type instanceof GraphQLNonNull ? type.ofType.name : type.name;
1254
+ const typeName = type instanceof GraphQLNonNull ? getEffectiveTypeName(type.ofType) : getEffectiveTypeName(type);
1113
1255
  if (isGraphQLisoDate(typeName)) {
1114
1256
  if (Array.isArray(term.value)) {
1115
1257
  term.value = term.value.map((value) => value && new Date(value));
@@ -1131,7 +1273,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1131
1273
  const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
1132
1274
  if (pathField.type instanceof GraphQLScalarType
1133
1275
  || isNonNullOfType(pathField.type, GraphQLScalarType)) {
1134
- const typeName = pathField.type instanceof GraphQLNonNull ? pathField.type.ofType.name : pathField.type.name;
1276
+ const typeName = pathField.type instanceof GraphQLNonNull ? getEffectiveTypeName(pathField.type.ofType) : getEffectiveTypeName(pathField.type);
1135
1277
  if (isGraphQLisoDate(typeName)) {
1136
1278
  if (Array.isArray(term.value)) {
1137
1279
  term.value = term.value.map((value) => value && new Date(value));
@@ -1158,7 +1300,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1158
1300
 
1159
1301
  const pathModel = typesDict.types[pathFieldType.name].model;
1160
1302
  const fieldPathCollectionName = pathModel.collection.collectionName;
1161
- const pathLocalFieldName = pathField.extensions.relation.connectionField;
1303
+ const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
1162
1304
 
1163
1305
  if (!aggregateClauses[aliasPath]) {
1164
1306
  let lookup = {};
@@ -1411,6 +1553,52 @@ module.exports.registerMutation = (name, description, inputModel, outputModel, c
1411
1553
  };
1412
1554
  };
1413
1555
 
1556
+ const autoGenerateResolvers = (gqltype) => {
1557
+ const fields = gqltype.getFields();
1558
+
1559
+ for (const [fieldName, fieldEntry] of Object.entries(fields)) {
1560
+ // Skip if resolve method already exists
1561
+ if (!fieldEntry.resolve) {
1562
+ // Check if field has relation extension
1563
+ if (fieldEntry.extensions && fieldEntry.extensions.relation) {
1564
+ const { relation } = fieldEntry.extensions;
1565
+
1566
+ if (fieldEntry.type instanceof GraphQLList) {
1567
+ // Collection field - generate resolve for one-to-many relationship
1568
+ const relatedType = fieldEntry.type.ofType;
1569
+ const connectionField = relation.connectionField || fieldName;
1570
+
1571
+ fieldEntry.resolve = (parent) => {
1572
+ // Lazy lookup of the related model
1573
+ const relatedTypeInfo = typesDict.types[relatedType.name];
1574
+ if (!relatedTypeInfo || !relatedTypeInfo.model) {
1575
+ throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
1576
+ }
1577
+ const query = {};
1578
+ query[connectionField] = parent.id || parent._id;
1579
+ return relatedTypeInfo.model.find(query);
1580
+ };
1581
+ } else if (fieldEntry.type instanceof GraphQLObjectType
1582
+ || (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
1583
+ // Single object field - generate resolve for one-to-one relationship
1584
+ const relatedType = fieldEntry.type instanceof GraphQLNonNull ? fieldEntry.type.ofType : fieldEntry.type;
1585
+ const connectionField = relation.connectionField || fieldName;
1586
+
1587
+ fieldEntry.resolve = (parent) => {
1588
+ // Lazy lookup of the related model
1589
+ const relatedTypeInfo = typesDict.types[relatedType.name];
1590
+ if (!relatedTypeInfo || !relatedTypeInfo.model) {
1591
+ throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
1592
+ }
1593
+ const relatedId = parent[connectionField] || parent[fieldName];
1594
+ return relatedId ? relatedTypeInfo.model.findById(relatedId) : null;
1595
+ };
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1600
+ };
1601
+
1414
1602
  module.exports.connect = (model, gqltype, simpleEntityEndpointName,
1415
1603
  listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
1416
1604
  waitingInputType[gqltype.name] = {
@@ -1428,6 +1616,9 @@ module.exports.connect = (model, gqltype, simpleEntityEndpointName,
1428
1616
  };
1429
1617
 
1430
1618
  typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
1619
+
1620
+ // Auto-generate resolve methods for relationship fields if not already defined
1621
+ autoGenerateResolvers(gqltype);
1431
1622
  };
1432
1623
 
1433
1624
  module.exports.addNoEndpointType = (gqltype) => {
@@ -1435,10 +1626,30 @@ module.exports.addNoEndpointType = (gqltype) => {
1435
1626
  gqltype,
1436
1627
  };
1437
1628
 
1629
+ // Check if this type has relationship fields that might need a model
1630
+ const fields = gqltype.getFields();
1631
+ let needsModel = false;
1632
+
1633
+ for (const [, fieldEntry] of Object.entries(fields)) {
1634
+ if (fieldEntry.extensions && fieldEntry.extensions.relation
1635
+ && (fieldEntry.type instanceof GraphQLObjectType || fieldEntry.type instanceof GraphQLList
1636
+ || (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType))) {
1637
+ needsModel = true;
1638
+ break;
1639
+ }
1640
+ }
1641
+
1438
1642
  typesDict.types[gqltype.name] = {
1439
1643
  gqltype,
1440
1644
  endpoint: false,
1645
+ // Generate model if needed for relationships, but don't create collection
1646
+ model: needsModel ? generateModelWithoutCollection(gqltype, null) : null,
1441
1647
  };
1442
1648
 
1443
1649
  typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
1650
+
1651
+ // Auto-generate resolve methods for relationship fields if not already defined
1652
+ autoGenerateResolvers(gqltype);
1444
1653
  };
1654
+
1655
+ module.exports.createValidatedScalar = createValidatedScalar;
@@ -0,0 +1,63 @@
1
+ const mongoose = require('mongoose');
2
+ const { GraphQLObjectType, GraphQLString, GraphQLID } = require('graphql');
3
+ const simfinity = require('../src/index');
4
+
5
+ describe('preventCreatingCollection option', () => {
6
+ let createCollectionSpy;
7
+
8
+ beforeEach(() => {
9
+ // Reset modules to have a clean state for each test
10
+ jest.resetModules();
11
+ // Spy on the createCollection method of the mongoose model prototype
12
+ createCollectionSpy = jest.spyOn(mongoose.Model, 'createCollection').mockImplementation(() => Promise.resolve());
13
+ });
14
+
15
+ afterEach(() => {
16
+ // Restore the original implementation
17
+ createCollectionSpy.mockRestore();
18
+ });
19
+
20
+ test('should create collection by default', () => {
21
+ const TestType = new GraphQLObjectType({
22
+ name: 'TestTypeDefault',
23
+ fields: () => ({
24
+ id: { type: GraphQLID },
25
+ name: { type: GraphQLString },
26
+ }),
27
+ });
28
+
29
+ simfinity.connect(null, TestType, 'testTypeDefault', 'testTypesDefault');
30
+ expect(createCollectionSpy).toHaveBeenCalledTimes(1);
31
+ });
32
+
33
+ test('should NOT create collection when preventCreatingCollection is true', () => {
34
+ simfinity.preventCreatingCollection(true);
35
+
36
+ const TestType = new GraphQLObjectType({
37
+ name: 'TestTypePrevent',
38
+ fields: () => ({
39
+ id: { type: GraphQLID },
40
+ name: { type: GraphQLString },
41
+ }),
42
+ });
43
+
44
+ simfinity.connect(null, TestType, 'testTypePrevent', 'testTypesPrevent');
45
+ expect(createCollectionSpy).not.toHaveBeenCalled();
46
+ });
47
+
48
+ test('should create collection when preventCreatingCollection is set back to false', () => {
49
+ simfinity.preventCreatingCollection(true); // first prevent
50
+ simfinity.preventCreatingCollection(false); // then allow
51
+
52
+ const TestType = new GraphQLObjectType({
53
+ name: 'TestTypeAllow',
54
+ fields: () => ({
55
+ id: { type: GraphQLID },
56
+ name: { type: GraphQLString },
57
+ }),
58
+ });
59
+
60
+ simfinity.connect(null, TestType, 'testTypeAllow', 'testTypesAllow');
61
+ expect(createCollectionSpy).toHaveBeenCalledTimes(1);
62
+ });
63
+ });