@simtlix/simfinity-js 1.2.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.
@@ -0,0 +1,63 @@
1
+ import globals from "globals";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import js from "@eslint/js";
5
+ import { FlatCompat } from "@eslint/eslintrc";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const compat = new FlatCompat({
10
+ baseDirectory: __dirname,
11
+ recommendedConfig: js.configs.recommended,
12
+ allConfig: js.configs.all
13
+ });
14
+
15
+ export default [
16
+ {
17
+ ignores: ["node_modules/*", "data/*", "eslint.config.mjs"],
18
+ },
19
+ ...compat.extends("airbnb-base"),
20
+ {
21
+ files: ["**/*.js"],
22
+ languageOptions: {
23
+ globals: {
24
+ ...globals.commonjs,
25
+ ...globals.node,
26
+ ...globals.jest,
27
+ },
28
+
29
+ ecmaVersion: 2024,
30
+ sourceType: "commonjs",
31
+ },
32
+
33
+ rules: {
34
+ "default-case": "off",
35
+ "max-len": 0,
36
+ "no-console": "off",
37
+ "no-underscore-dangle": "off",
38
+ "no-await-in-loop": "off",
39
+
40
+ "no-param-reassign": ["error", {
41
+ props: false,
42
+ }],
43
+
44
+ "function-paren-newline": "off",
45
+ "function-call-argument-newline": "off",
46
+
47
+ "no-restricted-syntax": ["error", {
48
+ selector: "ForInStatement",
49
+ message: "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.",
50
+ }, {
51
+ selector: "LabeledStatement",
52
+ message: "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.",
53
+ }, {
54
+ selector: "WithStatement",
55
+ message: "`with` is disallowed in strict mode because it makes code impossible to predict and optimize.",
56
+ }],
57
+
58
+ "import/no-unresolved": ["error", {
59
+ ignore: ["graphql", "mongoose"],
60
+ }],
61
+ },
62
+ },
63
+ ];
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
- "test": "echo \"No test specified\"",
7
+ "test": "jest",
8
+ "test:watch": "jest --watch",
9
+ "test:coverage": "jest --coverage",
8
10
  "lint": "eslint '**/*.js'",
9
11
  "lint-fix": "eslint --fix '**/*.js'"
10
12
  },
13
+ "engines": {
14
+ "node": ">=18.18.0"
15
+ },
11
16
  "author": "Simtlix",
12
17
  "license": "Apache-2.0",
13
18
  "repository": {
@@ -19,10 +24,15 @@
19
24
  "mongoose": "^8.15.1"
20
25
  },
21
26
  "devDependencies": {
22
- "eslint": "^7.7.0",
23
- "eslint-config-airbnb-base": "^14.2.0",
24
- "eslint-plugin-import": "^2.22.0",
25
- "ghooks": "^2.0.4"
27
+ "@eslint/compat": "^1.2.0",
28
+ "@eslint/eslintrc": "^3.1.0",
29
+ "@eslint/js": "^9.30.1",
30
+ "eslint": "^9.30.1",
31
+ "eslint-config-airbnb-base": "^15.0.0",
32
+ "eslint-plugin-import": "^2.32.0",
33
+ "ghooks": "^2.0.4",
34
+ "globals": "^16.3.0",
35
+ "jest": "^29.0.0"
26
36
  },
27
37
  "config": {
28
38
  "ghooks": {
@@ -33,5 +43,15 @@
33
43
  "optionalDependencies": {
34
44
  "graphql": "^14.7.0",
35
45
  "mongoose": "^8.15.1"
46
+ },
47
+ "jest": {
48
+ "testEnvironment": "node",
49
+ "testMatch": [
50
+ "**/tests/**/*.test.js"
51
+ ],
52
+ "collectCoverageFrom": [
53
+ "src/**/*.js",
54
+ "!src/index.js"
55
+ ]
36
56
  }
37
57
  }
package/src/index.js CHANGED
@@ -12,7 +12,7 @@ mongoose.set('strictQuery', false);
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
+ }
260
+
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
+ };
173
276
 
174
- const graphQLListInputType = (dict, fieldEntry, fieldEntryName, inputNamePrefix) => {
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,25 +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({ _id: args.id }).session(session);
576
+ return Model.findByIdAndDelete({ _id: args }).session(session);
474
577
  };
475
578
 
476
579
  const onDeleteSubject = async (Model, controller, id, session) => {
477
- const currentObject = await Model.findById({ _id: id }).lean();
580
+ const currentObject = await Model.findById({ _id: id }).session(session).lean();
478
581
 
479
582
  if (controller && controller.onDelete) {
480
583
  await controller.onDelete(currentObject, session);
481
584
  }
482
585
 
483
- return Model.findByIdAndDelete(id, { session });
586
+ return Model.findByIdAndDelete({ _id: id }).session(session);
484
587
  };
485
588
 
486
589
  const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent) => {
@@ -805,10 +908,18 @@ const generateSchemaDefinition = (gqlType) => {
805
908
  const schemaArg = {};
806
909
 
807
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
+
808
917
  if (fieldEntry.type === GraphQLID || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLID)) {
809
918
  schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
810
919
  } else if (fieldEntry.type === GraphQLString
811
- || 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)) {
812
923
  if (fieldEntry.extensions && fieldEntry.extensions.unique) {
813
924
  schemaArg[fieldEntryName] = { type: String, unique: true };
814
925
  } else {
@@ -822,27 +933,33 @@ const generateSchemaDefinition = (gqlType) => {
822
933
  schemaArg[fieldEntryName] = String;
823
934
  }
824
935
  } else if (fieldEntry.type === GraphQLInt
825
- || 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)) {
826
939
  if (fieldEntry.extensions && fieldEntry.extensions.unique) {
827
940
  schemaArg[fieldEntryName] = { type: Number, unique: true };
828
941
  } else {
829
942
  schemaArg[fieldEntryName] = Number;
830
943
  }
831
944
  } else if (fieldEntry.type === GraphQLFloat
832
- || 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)) {
833
948
  if (fieldEntry.extensions && fieldEntry.extensions.unique) {
834
949
  schemaArg[fieldEntryName] = { type: Number, unique: true };
835
950
  } else {
836
951
  schemaArg[fieldEntryName] = Number;
837
952
  }
838
953
  } else if (fieldEntry.type === GraphQLBoolean
839
- || 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)) {
840
957
  schemaArg[fieldEntryName] = Boolean;
841
958
  } else if (fieldEntry.type instanceof GraphQLObjectType
842
959
  || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
843
960
  if (fieldEntry.extensions && fieldEntry.extensions.relation) {
844
961
  if (!fieldEntry.extensions.relation.embedded) {
845
- schemaArg[fieldEntry.extensions.relation.connectionField] = mongoose
962
+ schemaArg[fieldEntry.extensions.relation.connectionField ? fieldEntry.extensions.relation.connectionField : fieldEntry.name] = mongoose
846
963
  .Schema.Types.ObjectId;
847
964
  } else {
848
965
  let entryType = fieldEntry.type;
@@ -867,17 +984,20 @@ const generateSchemaDefinition = (gqlType) => {
867
984
  }
868
985
  }
869
986
  } else if (fieldEntry.type.ofType === GraphQLString
870
- || fieldEntry.type.ofType instanceof GraphQLEnumType) {
987
+ || fieldEntry.type.ofType instanceof GraphQLEnumType
988
+ || (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
871
989
  schemaArg[fieldEntryName] = [String];
872
- } else if (fieldEntry.type.ofType === GraphQLBoolean) {
990
+ } else if (fieldEntry.type.ofType === GraphQLBoolean
991
+ || (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
873
992
  schemaArg[fieldEntryName] = [Boolean];
874
- } 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))) {
875
995
  schemaArg[fieldEntryName] = [Number];
876
- } else if (isGraphQLisoDate(fieldEntry.type.ofType.name)) {
996
+ } else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType))) {
877
997
  schemaArg[fieldEntryName] = [Date];
878
998
  }
879
- } else if (isGraphQLisoDate(fieldEntry.type.name)
880
- || (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)))) {
881
1001
  schemaArg[fieldEntryName] = Date;
882
1002
  }
883
1003
  }
@@ -890,7 +1010,18 @@ const generateModel = (gqlType, onModelCreated) => {
890
1010
  if (onModelCreated) {
891
1011
  onModelCreated(model);
892
1012
  }
893
- 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
894
1025
  return model;
895
1026
  };
896
1027
 
@@ -951,13 +1082,12 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
951
1082
  if (fieldType instanceof GraphQLNonNull) {
952
1083
  fieldType = qlField.type.ofType;
953
1084
  }
954
-
955
1085
  filterField.terms.forEach((term) => {
956
1086
  if (qlField.extensions && qlField.extensions.relation
957
1087
  && !qlField.extensions.relation.embedded) {
958
1088
  const { model } = typesDict.types[fieldType.name];
959
1089
  const { collectionName } = model.collection;
960
- const localFieldName = qlField.extensions.relation.connectionField;
1090
+ const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
961
1091
  if (!aggregateClauses[fieldName]) {
962
1092
  let lookup = {};
963
1093
 
@@ -1015,7 +1145,7 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
1015
1145
 
1016
1146
  const pathModel = typesDict.types[pathFieldType.name].model;
1017
1147
  const fieldPathCollectionName = pathModel.collection.collectionName;
1018
- const pathLocalFieldName = pathField.extensions.relation.connectionField;
1148
+ const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
1019
1149
 
1020
1150
  if (!aggregateClauses[aliasPath]) {
1021
1151
  let lookup = {};
@@ -1068,7 +1198,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1068
1198
  || isNonNullOfType(fieldType, GraphQLScalarType)
1069
1199
  || fieldType instanceof GraphQLEnumType
1070
1200
  || isNonNullOfType(fieldType, GraphQLEnumType)) {
1071
- const fieldTypeName = fieldType instanceof GraphQLNonNull ? fieldType.ofType.name : fieldType.name;
1201
+ const fieldTypeName = fieldType instanceof GraphQLNonNull ? getEffectiveTypeName(fieldType.ofType) : getEffectiveTypeName(fieldType);
1072
1202
  if (isGraphQLisoDate(fieldTypeName)) {
1073
1203
  if (Array.isArray(filterField.value)) {
1074
1204
  filterField.value = filterField.value.map((value) => value && new Date(value));
@@ -1088,7 +1218,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1088
1218
  && !qlField.extensions.relation.embedded) {
1089
1219
  const { model } = typesDict.types[fieldType.name];
1090
1220
  const { collectionName } = model.collection;
1091
- const localFieldName = qlField.extensions.relation.connectionField;
1221
+ const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
1092
1222
  if (!aggregateClauses[fieldName]) {
1093
1223
  let lookup = {};
1094
1224
 
@@ -1121,7 +1251,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1121
1251
 
1122
1252
  if (term.path.indexOf('.') < 0) {
1123
1253
  const { type } = fieldType.getFields()[term.path];
1124
- const typeName = type instanceof GraphQLNonNull ? type.ofType.name : type.name;
1254
+ const typeName = type instanceof GraphQLNonNull ? getEffectiveTypeName(type.ofType) : getEffectiveTypeName(type);
1125
1255
  if (isGraphQLisoDate(typeName)) {
1126
1256
  if (Array.isArray(term.value)) {
1127
1257
  term.value = term.value.map((value) => value && new Date(value));
@@ -1143,7 +1273,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1143
1273
  const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
1144
1274
  if (pathField.type instanceof GraphQLScalarType
1145
1275
  || isNonNullOfType(pathField.type, GraphQLScalarType)) {
1146
- 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);
1147
1277
  if (isGraphQLisoDate(typeName)) {
1148
1278
  if (Array.isArray(term.value)) {
1149
1279
  term.value = term.value.map((value) => value && new Date(value));
@@ -1170,7 +1300,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1170
1300
 
1171
1301
  const pathModel = typesDict.types[pathFieldType.name].model;
1172
1302
  const fieldPathCollectionName = pathModel.collection.collectionName;
1173
- const pathLocalFieldName = pathField.extensions.relation.connectionField;
1303
+ const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
1174
1304
 
1175
1305
  if (!aggregateClauses[aliasPath]) {
1176
1306
  let lookup = {};
@@ -1423,6 +1553,52 @@ module.exports.registerMutation = (name, description, inputModel, outputModel, c
1423
1553
  };
1424
1554
  };
1425
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
+
1426
1602
  module.exports.connect = (model, gqltype, simpleEntityEndpointName,
1427
1603
  listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
1428
1604
  waitingInputType[gqltype.name] = {
@@ -1440,6 +1616,9 @@ module.exports.connect = (model, gqltype, simpleEntityEndpointName,
1440
1616
  };
1441
1617
 
1442
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);
1443
1622
  };
1444
1623
 
1445
1624
  module.exports.addNoEndpointType = (gqltype) => {
@@ -1447,10 +1626,30 @@ module.exports.addNoEndpointType = (gqltype) => {
1447
1626
  gqltype,
1448
1627
  };
1449
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
+
1450
1642
  typesDict.types[gqltype.name] = {
1451
1643
  gqltype,
1452
1644
  endpoint: false,
1645
+ // Generate model if needed for relationships, but don't create collection
1646
+ model: needsModel ? generateModelWithoutCollection(gqltype, null) : null,
1453
1647
  };
1454
1648
 
1455
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);
1456
1653
  };
1654
+
1655
+ module.exports.createValidatedScalar = createValidatedScalar;