@simtlix/simfinity-js 2.4.4 → 2.5.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.
Files changed (27) hide show
  1. package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +23 -0
  2. package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
  3. package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
  4. package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
  5. package/.claude/worktrees/agitated-kepler/README.md +3941 -0
  6. package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
  7. package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
  8. package/.claude/worktrees/agitated-kepler/package.json +41 -0
  9. package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
  10. package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
  11. package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
  12. package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
  13. package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
  14. package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
  15. package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
  16. package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
  17. package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
  18. package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
  19. package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
  20. package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
  21. package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
  22. package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
  23. package/.cursor/rules/simfinity-core-functions.mdc +3 -1
  24. package/README.md +202 -0
  25. package/git-report.js +224 -0
  26. package/package.json +1 -1
  27. package/src/index.js +237 -23
@@ -0,0 +1,2412 @@
1
+ import {
2
+ GraphQLObjectType, GraphQLString, GraphQLID, GraphQLSchema, GraphQLList,
3
+ GraphQLNonNull, GraphQLInputObjectType, GraphQLScalarType, __Field,
4
+ GraphQLInt, GraphQLEnumType, GraphQLBoolean, GraphQLFloat, Kind,
5
+ } from 'graphql';
6
+ import mongoose from 'mongoose';
7
+
8
+ import SimfinityError from './errors/simfinity.error.js';
9
+ import InternalServerError from './errors/internal-server.error.js';
10
+ import QLOperator from './const/QLOperator.js';
11
+ import QLValue from './const/QLValue.js';
12
+ import QLSort from './const/QLSort.js';
13
+
14
+ mongoose.set('strictQuery', false);
15
+
16
+ // Custom JSON scalar type for aggregation results
17
+ const GraphQLJSON = new GraphQLScalarType({
18
+ name: 'JSON',
19
+ description: 'The `JSON` scalar type represents JSON values as specified by ECMA-404',
20
+ serialize(value) {
21
+ return value;
22
+ },
23
+ parseValue(value) {
24
+ return value;
25
+ },
26
+ parseLiteral(ast) {
27
+ switch (ast.kind) {
28
+ case Kind.STRING:
29
+ case Kind.BOOLEAN:
30
+ return ast.value;
31
+ case Kind.INT:
32
+ case Kind.FLOAT:
33
+ return parseFloat(ast.value);
34
+ case Kind.OBJECT: {
35
+ const value = Object.create(null);
36
+ ast.fields.forEach((field) => {
37
+ value[field.name.value] = GraphQLJSON.parseLiteral(field.value);
38
+ });
39
+ return value;
40
+ }
41
+ case Kind.LIST:
42
+ return ast.values.map((n) => GraphQLJSON.parseLiteral(n));
43
+ case Kind.NULL:
44
+ return null;
45
+ default:
46
+ return undefined;
47
+ }
48
+ },
49
+ });
50
+
51
+ // Adding 'extensions' field into instronspection query
52
+ const RelationType = new GraphQLObjectType({
53
+ name: 'RelationType',
54
+ fields: () => ({
55
+ embedded: { type: GraphQLBoolean },
56
+ connectionField: { type: GraphQLString },
57
+ displayField: { type: GraphQLString },
58
+ }),
59
+ });
60
+
61
+ const FieldExtensionsType = new GraphQLObjectType({
62
+ name: 'FieldExtensionsType',
63
+ fields: () => ({
64
+ relation: { type: RelationType },
65
+ stateMachine: { type: GraphQLBoolean },
66
+ readOnly: { type: GraphQLBoolean },
67
+ }),
68
+ });
69
+
70
+ const fieldTypeDefinitions = __Field._fields;
71
+
72
+ const fixedFieldsWithExtensions = () => {
73
+ const originalFields = fieldTypeDefinitions();
74
+ originalFields.extensions = {
75
+ type: FieldExtensionsType,
76
+ name: 'extensions',
77
+ resolve: (obj) => obj.extensions,
78
+ args: [],
79
+ isDeprecated: false,
80
+ };
81
+ return originalFields;
82
+ };
83
+
84
+ __Field._fields = fixedFieldsWithExtensions;
85
+ // End of adding 'extensions' field to instrospection query
86
+
87
+ const typesDict = { types: {} };
88
+ const waitingInputType = {};
89
+ const typesDictForUpdate = { types: {} };
90
+ const registeredMutations = {};
91
+
92
+ const operations = {
93
+ SAVE: 'save',
94
+ UPDATE: 'update',
95
+ DELETE: 'delete',
96
+ STATE_CHANGED: 'state_changed',
97
+ CUSTOM_MUTATION: 'custom_mutation',
98
+ };
99
+
100
+ const buildErrorFormatter = (callback) => {
101
+ const formatError = (err) => {
102
+ let result = null;
103
+ if (err instanceof SimfinityError) {
104
+ result = err;
105
+ } else {
106
+ result = new InternalServerError(err.message, err);
107
+ }
108
+
109
+ if (callback) {
110
+ const formattedError = callback(result);
111
+ return formattedError || result;
112
+ }
113
+ return result;
114
+ };
115
+ return formatError;
116
+ };
117
+
118
+ const middlewares = [];
119
+
120
+ export const use = (middleware) => {
121
+ middlewares.push(middleware);
122
+ };
123
+
124
+ export { buildErrorFormatter };
125
+
126
+ export { SimfinityError };
127
+
128
+ export { InternalServerError };
129
+
130
+ let preventCollectionCreation = false;
131
+
132
+ export const preventCreatingCollection = (prevent) => {
133
+ preventCollectionCreation = !!prevent;
134
+ };
135
+
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
+ const QLFilter = new GraphQLInputObjectType({
140
+ name: 'QLFilter',
141
+ fields: () => ({
142
+ operator: { type: QLOperator },
143
+ value: { type: QLValue },
144
+ }),
145
+ });
146
+
147
+ const QLTypeFilter = new GraphQLInputObjectType({
148
+ name: 'QLTypeFilter',
149
+ fields: () => ({
150
+ operator: { type: QLOperator },
151
+ value: { type: QLValue },
152
+ path: { type: GraphQLString },
153
+ }),
154
+ });
155
+
156
+ const IdInputType = new GraphQLInputObjectType({
157
+ name: 'IdInputType',
158
+ fields: () => ({
159
+ id: { type: new GraphQLNonNull(GraphQLString) },
160
+ }),
161
+ });
162
+
163
+ const QLTypeFilterExpression = new GraphQLInputObjectType({
164
+ name: 'QLTypeFilterExpression',
165
+ fields: () => ({
166
+ terms: { type: new GraphQLList(QLTypeFilter) },
167
+ }),
168
+ });
169
+
170
+ const QLFilterCondition = new GraphQLInputObjectType({
171
+ name: 'QLFilterCondition',
172
+ fields: () => ({
173
+ field: { type: new GraphQLNonNull(GraphQLString) },
174
+ operator: { type: QLOperator },
175
+ value: { type: QLValue },
176
+ path: { type: GraphQLString },
177
+ }),
178
+ });
179
+
180
+ const QLFilterGroup = new GraphQLInputObjectType({
181
+ name: 'QLFilterGroup',
182
+ fields: () => ({
183
+ AND: { type: new GraphQLList(QLFilterGroup) },
184
+ OR: { type: new GraphQLList(QLFilterGroup) },
185
+ conditions: { type: new GraphQLList(QLFilterCondition) },
186
+ }),
187
+ });
188
+
189
+ const QLPagination = new GraphQLInputObjectType({
190
+ name: 'QLPagination',
191
+ fields: () => ({
192
+ page: { type: new GraphQLNonNull(GraphQLInt) },
193
+ size: { type: new GraphQLNonNull(GraphQLInt) },
194
+ count: { type: GraphQLBoolean },
195
+ }),
196
+ });
197
+
198
+ const QLSortExpression = new GraphQLInputObjectType({
199
+ name: 'QLSortExpression',
200
+ fields: () => ({
201
+ terms: { type: new GraphQLList(QLSort) },
202
+ }),
203
+ });
204
+
205
+ const QLAggregationOperation = new GraphQLEnumType({
206
+ name: 'QLAggregationOperation',
207
+ values: {
208
+ SUM: { value: 'SUM' },
209
+ COUNT: { value: 'COUNT' },
210
+ AVG: { value: 'AVG' },
211
+ MIN: { value: 'MIN' },
212
+ MAX: { value: 'MAX' },
213
+ },
214
+ });
215
+
216
+ const QLTypeAggregationFact = new GraphQLInputObjectType({
217
+ name: 'QLTypeAggregationFact',
218
+ fields: () => ({
219
+ operation: { type: new GraphQLNonNull(QLAggregationOperation) },
220
+ factName: { type: new GraphQLNonNull(GraphQLString) },
221
+ path: { type: new GraphQLNonNull(GraphQLString) },
222
+ }),
223
+ });
224
+
225
+ const QLTypeAggregationExpression = new GraphQLInputObjectType({
226
+ name: 'QLTypeAggregationExpression',
227
+ fields: () => ({
228
+ groupId: { type: new GraphQLNonNull(GraphQLString) },
229
+ facts: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(QLTypeAggregationFact))) },
230
+ }),
231
+ });
232
+
233
+ const QLTypeAggregationResult = new GraphQLObjectType({
234
+ name: 'QLTypeAggregationResult',
235
+ fields: () => ({
236
+ groupId: { type: GraphQLJSON },
237
+ facts: { type: GraphQLJSON },
238
+ }),
239
+ });
240
+
241
+ const isNonNullOfType = (fieldEntryType, graphQLType) => {
242
+ let isOfType = false;
243
+ if (fieldEntryType instanceof GraphQLNonNull) {
244
+ isOfType = fieldEntryType.ofType instanceof graphQLType;
245
+ }
246
+ return isOfType;
247
+ };
248
+
249
+ const isNonNullOfTypeForNotScalar = (fieldEntryType, graphQLType) => {
250
+ let isOfType = false;
251
+ if (fieldEntryType instanceof GraphQLNonNull) {
252
+ isOfType = fieldEntryType.ofType === graphQLType;
253
+ }
254
+ return isOfType;
255
+ };
256
+
257
+ const getEffectiveTypeName = (type) => {
258
+ if (type instanceof GraphQLScalarType && type.baseScalarType) {
259
+ return type.baseScalarType.name;
260
+ }
261
+ return type.name;
262
+ };
263
+
264
+ const isGraphQLisoDate = (typeName) => typeName === 'DateTime' || typeName === 'Date' || typeName === 'Time';
265
+
266
+ function createValidatedScalar(name, description, baseScalarType, validate) {
267
+ if (!baseScalarType) {
268
+ throw new Error('baseScalarType is required');
269
+ }
270
+
271
+ // Validate that baseScalarType is a valid GraphQL scalar type
272
+ if (!(baseScalarType instanceof GraphQLScalarType)) {
273
+ throw new Error('baseScalarType must be a valid GraphQL scalar type');
274
+ }
275
+
276
+ // Check if it's one of the standard GraphQL scalar types
277
+ const validScalarTypes = [GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID];
278
+ const isValidStandardType = validScalarTypes.some((type) => baseScalarType === type);
279
+
280
+ if (!isValidStandardType && !baseScalarType.name) {
281
+ throw new Error('baseScalarType must be a standard GraphQL scalar type or a custom scalar with a valid name');
282
+ }
283
+
284
+ const kindMap = {
285
+ String: Kind.STRING,
286
+ Int: Kind.INT,
287
+ Float: Kind.FLOAT,
288
+ Boolean: Kind.BOOLEAN,
289
+ ID: Kind.STRING, // IDs are represented as strings in AST
290
+ };
291
+
292
+ // Try to infer the kind from the baseScalarType name
293
+ const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
294
+
295
+ const scalar = new GraphQLScalarType({
296
+ name: `${name}_${baseScalarType.name}`,
297
+ description,
298
+ serialize(value) {
299
+ validate(value);
300
+ return baseScalarType.serialize(value);
301
+ },
302
+ parseValue(value) {
303
+ validate(value);
304
+ return baseScalarType.parseValue(value);
305
+ },
306
+ parseLiteral(ast, variables) {
307
+ if (ast.kind !== baseKind) {
308
+ throw new Error(`${name}_${baseScalarType.name} must be a ${baseScalarType.name}`);
309
+ }
310
+ const value = baseScalarType.parseLiteral(ast, variables);
311
+ validate(value);
312
+ return value;
313
+ },
314
+ });
315
+
316
+ scalar.baseScalarType = baseScalarType;
317
+ return scalar;
318
+ }
319
+
320
+ /**
321
+ * Creates a new GraphQLInputObjectType with a field excluded.
322
+ * @param {string} inputNamePrefix - The prefix for the input type name.
323
+ * @param {GraphQLInputObjectType} originalType - The original input type.
324
+ * @param {string} fieldToExclude - The name of the field to exclude.
325
+ * @returns {GraphQLInputObjectType} A new input type without the specified field.
326
+ */
327
+ const createTypeWithExcludedField = (inputNamePrefix, originalType, fieldToExclude) => {
328
+ const originalFields = originalType.getFields();
329
+ const newFields = Object.fromEntries(
330
+ Object.entries(originalFields).filter(([fieldName]) => fieldName !== fieldToExclude),
331
+ );
332
+
333
+ return new GraphQLInputObjectType({
334
+ name: `${inputNamePrefix}${originalType.name}For${fieldToExclude.charAt(0).toUpperCase() + fieldToExclude.slice(1)}`,
335
+ fields: newFields,
336
+ });
337
+ };
338
+
339
+ const createOneToManyInputType = (inputNamePrefix, fieldEntryName,
340
+ inputType, updateInputType, connectionField) => {
341
+ let inputTypeForAdd = inputType;
342
+
343
+ // If a gqltype is provided, create a new input type for 'added'
344
+ // that excludes the field named after the gqltype.
345
+ if (connectionField) {
346
+ const fieldToExclude = connectionField;
347
+ inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, fieldToExclude);
348
+ }
349
+
350
+ return new GraphQLInputObjectType({
351
+ name: `OneToMany${inputNamePrefix}${fieldEntryName}`,
352
+ fields: () => ({
353
+ added: {
354
+ type: new GraphQLList(inputTypeForAdd),
355
+ },
356
+ updated: {
357
+ type: new GraphQLList(updateInputType),
358
+ },
359
+ deleted: {
360
+ type: new GraphQLList(GraphQLID),
361
+ },
362
+ }),
363
+ });
364
+ };
365
+
366
+ const graphQLListInputType = (dict, fieldEntry, fieldEntryName, inputNamePrefix, connectionField) => {
367
+ const { ofType } = fieldEntry.type;
368
+
369
+ if (ofType instanceof GraphQLObjectType && dict.types[ofType.name].inputType) {
370
+ if (!fieldEntry.extensions || !fieldEntry.extensions.relation
371
+ || !fieldEntry.extensions.relation.embedded) {
372
+ const oneToMany = createOneToManyInputType(inputNamePrefix, fieldEntryName,
373
+ typesDict.types[ofType.name].inputType, typesDictForUpdate.types[ofType.name].inputType, connectionField);
374
+ return oneToMany;
375
+ }
376
+ if (fieldEntry.extensions && fieldEntry.extensions.relation
377
+ && fieldEntry.extensions.relation.embedded) {
378
+ return new GraphQLList(dict.types[ofType.name].inputType);
379
+ }
380
+ } else if (ofType instanceof GraphQLScalarType || ofType instanceof GraphQLEnumType) {
381
+ return new GraphQLList(ofType);
382
+ }
383
+ return null;
384
+ };
385
+
386
+ const buildInputType = (gqltype) => {
387
+ const argTypes = gqltype.getFields();
388
+
389
+ const fieldsArgs = {};
390
+ const fieldsArgForUpdate = {};
391
+
392
+ const selfReferenceCollections = {};
393
+
394
+ for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
395
+ const fieldArg = {};
396
+ const fieldArgForUpdate = {};
397
+
398
+ if (!fieldEntry.extensions || !fieldEntry.extensions.readOnly) {
399
+ const hasStateMachine = !!typesDict.types[gqltype.name].stateMachine;
400
+ const doesEstateFieldExistButIsManagedByStateMachine = !!(fieldEntryName === 'state' && hasStateMachine);
401
+
402
+ if (!doesEstateFieldExistButIsManagedByStateMachine) {
403
+ if (fieldEntry.type instanceof GraphQLScalarType
404
+ || fieldEntry.type instanceof GraphQLEnumType
405
+ || isNonNullOfType(fieldEntry.type, GraphQLScalarType)
406
+ || isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
407
+ if (fieldEntryName !== 'id') {
408
+ fieldArg.type = fieldEntry.type;
409
+ }
410
+ fieldArgForUpdate.type = fieldEntry.type instanceof GraphQLNonNull
411
+ ? fieldEntry.type.ofType : fieldEntry.type;
412
+ if (fieldEntry.type === GraphQLID) {
413
+ fieldArgForUpdate.type = new GraphQLNonNull(GraphQLID);
414
+ }
415
+ } else if (fieldEntry.type instanceof GraphQLObjectType
416
+ || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
417
+ if (fieldEntry.extensions && fieldEntry.extensions.relation) {
418
+ const fieldEntryNameValue = fieldEntry.type instanceof GraphQLNonNull
419
+ ? fieldEntry.type.ofType.name : fieldEntry.type.name;
420
+ if (!fieldEntry.extensions.relation.embedded) {
421
+ fieldArg.type = fieldEntry.type instanceof GraphQLNonNull
422
+ ? new GraphQLNonNull(IdInputType) : IdInputType;
423
+ fieldArgForUpdate.type = IdInputType;
424
+ } else if (typesDict.types[fieldEntryNameValue].inputType
425
+ && typesDictForUpdate.types[fieldEntryNameValue].inputType) {
426
+ fieldArg.type = typesDict.types[fieldEntryNameValue].inputType;
427
+ fieldArgForUpdate.type = typesDictForUpdate.types[fieldEntryNameValue].inputType;
428
+ } else {
429
+ return null;
430
+ }
431
+ } else {
432
+ console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
433
+ }
434
+ } else if (fieldEntry.type instanceof GraphQLList) {
435
+ if (fieldEntry.type.ofType === gqltype) {
436
+ selfReferenceCollections[fieldEntryName] = fieldEntry;
437
+ } else {
438
+ const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, gqltype.name + 'A', fieldEntry.extensions?.relation?.connectionField);
439
+ const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, gqltype.name +'U', fieldEntry.extensions?.relation?.connectionField);
440
+ if (listInputTypeForAdd && listInputTypeForUpdate) {
441
+ fieldArg.type = listInputTypeForAdd;
442
+ fieldArgForUpdate.type = listInputTypeForUpdate;
443
+ } else {
444
+ return null;
445
+ }
446
+ }
447
+ }
448
+ fieldArg.description = fieldEntry.description;
449
+ fieldArgForUpdate.description = fieldEntry.description;
450
+
451
+ if (fieldArg.type) {
452
+ fieldsArgs[fieldEntryName] = fieldArg;
453
+ }
454
+
455
+ if (fieldArgForUpdate.type) {
456
+ fieldsArgForUpdate[fieldEntryName] = fieldArgForUpdate;
457
+ }
458
+ } else {
459
+ fieldEntry.extensions = { ...fieldEntry.extensions, stateMachine: true };
460
+ }
461
+ }
462
+ }
463
+
464
+ const inputTypeBody = {
465
+ name: `${gqltype.name}Input`,
466
+ fields: fieldsArgs,
467
+ };
468
+
469
+ const inputTypeBodyForUpdate = {
470
+ name: `${gqltype.name}InputForUpdate`,
471
+ fields: fieldsArgForUpdate,
472
+ };
473
+
474
+ const inputTypeForAdd = new GraphQLInputObjectType(inputTypeBody);
475
+ const inputTypeForUpdate = new GraphQLInputObjectType(inputTypeBodyForUpdate);
476
+
477
+ const inputTypeForAddFields = inputTypeForAdd._fields();
478
+
479
+ Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
480
+ if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
481
+ inputTypeForAddFields[fieldEntryName] = {
482
+ type: createOneToManyInputType('A', fieldEntryName, inputTypeForAdd, inputTypeForUpdate, selfReferenceCollections[fieldEntryName].extensions?.relation?.connectionField),
483
+ name: fieldEntryName,
484
+ };
485
+ }
486
+ });
487
+
488
+ inputTypeForAdd._fields = () => inputTypeForAddFields;
489
+
490
+ const inputTypeForUpdateFields = inputTypeForUpdate._fields();
491
+
492
+ Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
493
+ if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
494
+ inputTypeForUpdateFields[fieldEntryName] = {
495
+ type: createOneToManyInputType('U', fieldEntryName, inputTypeForAdd, inputTypeForUpdate, selfReferenceCollections[fieldEntryName].extensions?.relation?.connectionField),
496
+ name: fieldEntryName,
497
+ };
498
+ }
499
+ });
500
+
501
+ inputTypeForUpdate._fields = () => inputTypeForUpdateFields;
502
+
503
+ return { inputTypeBody: inputTypeForAdd, inputTypeBodyForUpdate: inputTypeForUpdate };
504
+ };
505
+
506
+ const getInputType = (type) => typesDict.types[type.name].inputType;
507
+
508
+ export { getInputType };
509
+
510
+ const buildPendingInputTypes = (waitingForInputType) => {
511
+ const stillWaitingInputType = {};
512
+ let isThereAtLeastOneWaiting = false;
513
+
514
+ Object.entries(waitingForInputType).forEach(([key, value]) => {
515
+ const { gqltype } = value;
516
+
517
+ if (!typesDict.types[gqltype.name].inputType) {
518
+ const buildInputTypeResult = buildInputType(gqltype);
519
+
520
+ if (buildInputTypeResult && buildInputTypeResult.inputTypeBody
521
+ && buildInputTypeResult.inputTypeBodyForUpdate) {
522
+ typesDict.types[gqltype.name].inputType = buildInputTypeResult.inputTypeBody;
523
+ typesDictForUpdate.types[gqltype.name].inputType = buildInputTypeResult
524
+ .inputTypeBodyForUpdate;
525
+ } else {
526
+ stillWaitingInputType[key] = value;
527
+ isThereAtLeastOneWaiting = true;
528
+ }
529
+ }
530
+ });
531
+
532
+ if (isThereAtLeastOneWaiting) {
533
+ buildPendingInputTypes(stillWaitingInputType);
534
+ }
535
+ };
536
+
537
+ const isEmpty = (value) => !value && value !== false && value !== 0;
538
+
539
+ const materializeModel = async (args, gqltype, linkToParent, operation, session) => {
540
+ if (!args) {
541
+ return null;
542
+ }
543
+
544
+ const argTypes = gqltype.getFields();
545
+
546
+ const modelArgs = {};
547
+ const collectionFields = {};
548
+
549
+ for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
550
+ if (fieldEntry.extensions && fieldEntry.extensions.validations
551
+ && fieldEntry.extensions.validations[operation]) {
552
+ for (const validator of fieldEntry.extensions.validations[operation]) {
553
+ await validator.validate(gqltype.name, fieldEntryName, args[fieldEntryName], session);
554
+ }
555
+ }
556
+
557
+ if (!isEmpty(args[fieldEntryName])) {
558
+ if (fieldEntry.type instanceof GraphQLScalarType
559
+ || fieldEntry.type instanceof GraphQLEnumType
560
+ || isNonNullOfType(fieldEntry.type, GraphQLScalarType)
561
+ || isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
562
+ modelArgs[fieldEntryName] = args[fieldEntryName];
563
+ } else if (fieldEntry.type instanceof GraphQLObjectType
564
+ || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
565
+ if (fieldEntry.extensions && fieldEntry.extensions.relation) {
566
+ if (!fieldEntry.extensions.relation.embedded) {
567
+ modelArgs[fieldEntry.extensions.relation.connectionField] = new mongoose.Types
568
+ .ObjectId(args[fieldEntryName].id);
569
+ } else {
570
+ const fieldType = fieldEntry.type instanceof GraphQLNonNull
571
+ ? fieldEntry.type.ofType : fieldEntry.type;
572
+ modelArgs[fieldEntryName] = (await materializeModel(args[fieldEntryName], fieldType,
573
+ null, operation, session)).modelArgs;
574
+ }
575
+ } else {
576
+ modelArgs[fieldEntry.name] = new mongoose.Types
577
+ .ObjectId(args[fieldEntryName].id);
578
+ console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
579
+ }
580
+ } else if (fieldEntry.type instanceof GraphQLList) {
581
+ const { ofType } = fieldEntry.type;
582
+ if (ofType instanceof GraphQLObjectType && fieldEntry.extensions
583
+ && fieldEntry.extensions.relation) {
584
+ if (!fieldEntry.extensions.relation.embedded) {
585
+ collectionFields[fieldEntryName] = args[fieldEntryName];
586
+ } else if (fieldEntry.extensions.relation.embedded) {
587
+ const collectionEntries = [];
588
+
589
+ for (const element of args[fieldEntryName]) {
590
+ const collectionEntry = (await materializeModel(element, ofType,
591
+ null, operation, session)).modelArgs;
592
+ if (collectionEntry) {
593
+ collectionEntries.push(collectionEntry);
594
+ }
595
+ }
596
+ modelArgs[fieldEntryName] = collectionEntries;
597
+ }
598
+ } else if (ofType instanceof GraphQLScalarType || ofType instanceof GraphQLEnumType) {
599
+ modelArgs[fieldEntryName] = args[fieldEntryName];
600
+ }
601
+ }
602
+ }
603
+ }
604
+
605
+ if (linkToParent) {
606
+ linkToParent(modelArgs);
607
+ }
608
+
609
+ if (gqltype.extensions && gqltype.extensions.validations
610
+ && gqltype.extensions.validations[operation]) {
611
+ for (const validator of gqltype.extensions.validations[operation]) {
612
+ await validator.validate(gqltype.name, args, modelArgs, session);
613
+ }
614
+ }
615
+
616
+ return { modelArgs, collectionFields };
617
+ };
618
+
619
+ const executeRegisteredMutation = async (args, callback, session) => {
620
+ 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);
631
+ }
632
+ mySession.endSession();
633
+ throw error;
634
+ }
635
+ };
636
+
637
+ const iterateonCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
638
+ for (const [collectionFieldKey, collectionField] of
639
+ Object.entries(materializedModel.collectionFields)) {
640
+ if (collectionField.added) {
641
+
642
+ await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
643
+ collectionField.added, operations.SAVE, context);
644
+ }
645
+ if (collectionField.updated) {
646
+
647
+ await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
648
+ collectionField.updated, operations.UPDATE, context);
649
+ }
650
+ if (collectionField.deleted) {
651
+
652
+ await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
653
+ collectionField.deleted, operations.DELETE, context);
654
+ }
655
+ }
656
+ };
657
+
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) => {
669
+ const currentObject = await Model.findById({ _id: id }).session(session).lean();
670
+
671
+ if (controller && controller.onDelete) {
672
+ await controller.onDelete(currentObject, session, context);
673
+ }
674
+
675
+ return Model.findByIdAndDelete({ _id: id }).session(session);
676
+ };
677
+
678
+ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
679
+ const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
680
+ const objectId = args.id;
681
+
682
+ const currentObject = await Model.findById({ _id: objectId }).lean();
683
+
684
+ 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 };
696
+ }
697
+ }
698
+ }
699
+
700
+ if (args[fieldEntryName] === null
701
+ && !(fieldEntry.type instanceof GraphQLNonNull)) {
702
+ materializedModel.modelArgs = {
703
+ ...materializedModel.modelArgs,
704
+ $unset: { ...materializedModel.modelArgs.$unset, [fieldEntryName]: '' },
705
+ };
706
+ }
707
+ });
708
+
709
+ if (controller && controller.onUpdating) {
710
+ await controller.onUpdating(objectId, materializedModel.modelArgs, session, context);
711
+ }
712
+
713
+ const result = Model.findByIdAndUpdate(
714
+ objectId, materializedModel.modelArgs, { new: true },
715
+ ).session(session);
716
+
717
+ if (materializedModel.collectionFields) {
718
+ await iterateonCollectionFields(materializedModel, gqltype, objectId, session, context);
719
+ }
720
+
721
+ if (controller && controller.onUpdated) {
722
+ await controller.onUpdated(result, session, context);
723
+ }
724
+
725
+ return result;
726
+ };
727
+
728
+ const onStateChanged = async (Model, gqltype, controller, args, session, actionField, context) => {
729
+ const storedModel = await Model.findById(args.id);
730
+ if (!storedModel) {
731
+ throw new SimfinityError(`${gqltype.name} ${args.id} is not valid`, 'NOT_VALID_ID', 404);
732
+ }
733
+ if (storedModel.state === actionField.from.name) {
734
+ if (actionField.action) {
735
+ await actionField.action(args, session);
736
+ }
737
+
738
+ args.state = actionField.to.name;
739
+ let result = await onUpdateSubject(Model, gqltype, controller, args, session, null, context);
740
+ result = result.toObject();
741
+ result.state = actionField.to.value;
742
+ return result;
743
+ }
744
+ throw new SimfinityError(`Action is not allowed from state ${storedModel.state}`, 'BAD_REQUEST', 400);
745
+ };
746
+
747
+ const onSaveObject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
748
+ const materializedModel = await materializeModel(args, gqltype, linkToParent, 'CREATE', session);
749
+ if (typesDict.types[gqltype.name].stateMachine) {
750
+ materializedModel.modelArgs.state = typesDict.types[gqltype.name]
751
+ .stateMachine.initialState.name;
752
+ }
753
+
754
+ const newObject = new Model(materializedModel.modelArgs);
755
+ newObject.$session(session);
756
+
757
+ if (controller && controller.onSaving) {
758
+ await controller.onSaving(newObject, args, session, context);
759
+ }
760
+
761
+ let result = await newObject.save();
762
+ result = result.toObject();
763
+
764
+ if (materializedModel.collectionFields) {
765
+ await iterateonCollectionFields(materializedModel, gqltype, newObject._id, session, context);
766
+ }
767
+
768
+
769
+ if (controller && controller.onSaved) {
770
+ await controller.onSaved(result, args, session, context);
771
+ }
772
+ if (typesDict.types[gqltype.name].stateMachine) {
773
+ result.state = typesDict.types[gqltype.name].stateMachine.initialState.value;
774
+ }
775
+ return result;
776
+ };
777
+
778
+ export const saveObject = async (typeName, args, session, context) => {
779
+ const type = typesDict.types[typeName];
780
+ return onSaveObject(type.model, type.gqltype, type.controller, args, session, null, context);
781
+ };
782
+
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;
789
+ switch (operation) {
790
+ case operations.SAVE:
791
+ newObject = await onSaveObject(Model, gqltype, controller, args, mySession, null, context);
792
+ break;
793
+ case operations.UPDATE:
794
+ newObject = await onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
795
+ break;
796
+ case operations.DELETE:
797
+ newObject = await onDeleteObject(Model, gqltype, controller, args, mySession, context);
798
+ break;
799
+ 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);
810
+ }
811
+ mySession.endSession();
812
+ throw error;
813
+ }
814
+ };
815
+
816
+ const executeItemFunction = async (gqltype, collectionField, objectId, session,
817
+ collectionFieldsList, operationType, context) => {
818
+ const argTypes = gqltype.getFields();
819
+ const collectionGQLType = argTypes[collectionField].type.ofType;
820
+ const { connectionField } = argTypes[collectionField].extensions.relation;
821
+
822
+ let operationFunction = async () => { };
823
+
824
+ switch (operationType) {
825
+ case operations.SAVE:
826
+ operationFunction = async (collectionItem) => {
827
+ await onSaveObject(typesDict.types[collectionGQLType.name].model, collectionGQLType,
828
+ typesDict.types[collectionGQLType.name].controller, collectionItem, session, (item) => {
829
+ item[connectionField] = objectId;
830
+ }, context);
831
+ };
832
+ break;
833
+ case operations.UPDATE:
834
+ operationFunction = async (collectionItem) => {
835
+ await onUpdateSubject(typesDict.types[collectionGQLType.name].model, collectionGQLType,
836
+ typesDict.types[collectionGQLType.name].controller, collectionItem, session, (item) => {
837
+ item[connectionField] = objectId;
838
+ }, context);
839
+ };
840
+ break;
841
+ case operations.DELETE:
842
+ operationFunction = async (collectionItem) => {
843
+ await onDeleteSubject(typesDict.types[collectionGQLType.name].model,
844
+ typesDict.types[collectionGQLType.name].controller, collectionItem, session, context);
845
+ };
846
+ }
847
+
848
+ for (const element of collectionFieldsList) {
849
+ await operationFunction(element);
850
+ }
851
+ };
852
+
853
+ const shouldNotBeIncludedInSchema = (includedTypes,
854
+ type) => includedTypes && !includedTypes.includes(type);
855
+
856
+ const excecuteMiddleware = (context) => {
857
+ const buildNext = (middlewaresParam) => {
858
+ if (!middlewaresParam) {
859
+ return () => {};
860
+ }
861
+ const next = () => {
862
+ const middleware = middlewaresParam[0];
863
+ if (middleware) {
864
+ middleware(context, buildNext(middlewaresParam.slice(1)));
865
+ }
866
+ };
867
+ return next;
868
+ };
869
+
870
+ const middleware = buildNext(middlewares);
871
+ middleware();
872
+ };
873
+
874
+ const executeScope = async (params) => {
875
+ const { type, args, operation, context } = params;
876
+
877
+ if (!type || !type.gqltype || !type.gqltype.extensions) {
878
+ return null;
879
+ }
880
+
881
+ const extensions = type.gqltype.extensions;
882
+ if (!extensions.scope || !extensions.scope[operation]) {
883
+ return null;
884
+ }
885
+
886
+ const scopeFunction = extensions.scope[operation];
887
+ if (typeof scopeFunction !== 'function') {
888
+ return null;
889
+ }
890
+
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;
897
+ };
898
+
899
+ const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
900
+ const rootQueryArgs = {};
901
+ rootQueryArgs.name = name;
902
+ rootQueryArgs.fields = {};
903
+
904
+ buildPendingInputTypes(waitingInputType);
905
+
906
+ for (const type of Object.values(typesDict.types)) {
907
+ if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
908
+ if (type.endpoint) {
909
+ const argsObject = { input: { type: new GraphQLNonNull(type.inputType) } };
910
+
911
+ rootQueryArgs.fields[`add${type.simpleEntityEndpointName}`] = {
912
+ type: type.gqltype,
913
+ description: 'add',
914
+ args: argsObject,
915
+ async resolve(parent, args, context) {
916
+ const params = {
917
+ type,
918
+ args,
919
+ operation: operations.SAVE,
920
+ context,
921
+ };
922
+
923
+ excecuteMiddleware(params);
924
+ return executeOperation(type.model, type.gqltype, type.controller,
925
+ args.input, operations.SAVE, null, null, context);
926
+ },
927
+ };
928
+ rootQueryArgs.fields[`delete${type.simpleEntityEndpointName}`] = {
929
+ type: type.gqltype,
930
+ description: 'delete',
931
+ args: { id: { type: new GraphQLNonNull(GraphQLID) } },
932
+ async resolve(parent, args, context) {
933
+ const params = {
934
+ type,
935
+ args,
936
+ operation: operations.DELETE,
937
+ context,
938
+ };
939
+
940
+ excecuteMiddleware(params);
941
+ return executeOperation(type.model, type.gqltype, type.controller,
942
+ args.id, operations.DELETE, null, null, context);
943
+ },
944
+ };
945
+ }
946
+ }
947
+ }
948
+
949
+ for (const type of Object.values(typesDictForUpdate.types)) {
950
+ if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
951
+ if (type.endpoint) {
952
+ const argsObject = { input: { type: new GraphQLNonNull(type.inputType) } };
953
+ rootQueryArgs.fields[`update${type.simpleEntityEndpointName}`] = {
954
+ type: type.gqltype,
955
+ description: 'update',
956
+ args: argsObject,
957
+ async resolve(parent, args, context) {
958
+ const params = {
959
+ type,
960
+ args,
961
+ operation: operations.UPDATE,
962
+ context,
963
+ };
964
+
965
+ excecuteMiddleware(params);
966
+ return executeOperation(type.model, type.gqltype, type.controller,
967
+ args.input, operations.UPDATE, null, null, context);
968
+ },
969
+ };
970
+ if (type.stateMachine) {
971
+ for (const [actionName, actionField] of Object.entries(type.stateMachine.actions)) {
972
+ if ({}.hasOwnProperty.call(type.stateMachine.actions, actionName)) {
973
+ rootQueryArgs.fields[`${actionName}_${type.simpleEntityEndpointName}`] = {
974
+ type: type.gqltype,
975
+ description: actionField.description,
976
+ args: argsObject,
977
+ async resolve(parent, args, context) {
978
+ const params = {
979
+ type,
980
+ args,
981
+ operation: operations.STATE_CHANGED,
982
+ actionName,
983
+ actionField,
984
+ context,
985
+ };
986
+
987
+ excecuteMiddleware(params);
988
+ return executeOperation(type.model, type.gqltype, type.controller,
989
+ args.input, operations.STATE_CHANGED, actionField, null, context);
990
+ },
991
+ };
992
+ }
993
+ }
994
+ }
995
+ }
996
+ }
997
+ }
998
+
999
+ for (const [entry, registeredMutation] of Object.entries(registeredMutations)) {
1000
+ if (!shouldNotBeIncludedInSchema(includedCustomMutations, entry)) {
1001
+ const argsObject = registeredMutation.inputModel
1002
+ ? { input: { type: new GraphQLNonNull(registeredMutation.inputModel) } } : null;
1003
+ rootQueryArgs.fields[entry] = {
1004
+ type: registeredMutation.outputModel,
1005
+ description: registeredMutation.description,
1006
+ args: argsObject,
1007
+ async resolve(parent, args, context) {
1008
+ const params = {
1009
+ args,
1010
+ operation: operations.CUSTOM_MUTATION,
1011
+ entry,
1012
+ context,
1013
+ };
1014
+ excecuteMiddleware(params);
1015
+ return executeRegisteredMutation(args.input, registeredMutation.callback);
1016
+ },
1017
+ };
1018
+ }
1019
+ }
1020
+
1021
+ return new GraphQLObjectType(rootQueryArgs);
1022
+ };
1023
+
1024
+ const generateSchemaDefinition = (gqlType) => {
1025
+ const argTypes = gqlType.getFields();
1026
+
1027
+ const schemaArg = {};
1028
+
1029
+ 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;
1035
+
1036
+ if (fieldEntry.type === GraphQLID || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLID)) {
1037
+ 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)) {
1076
+ schemaArg[fieldEntryName] = Boolean;
1077
+ } else if (fieldEntry.type instanceof GraphQLObjectType
1078
+ || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
1079
+ if (fieldEntry.extensions && fieldEntry.extensions.relation) {
1080
+ if (!fieldEntry.extensions.relation.embedded) {
1081
+ schemaArg[fieldEntry.extensions.relation.connectionField ? fieldEntry.extensions.relation.connectionField : fieldEntry.name] = mongoose
1082
+ .Schema.Types.ObjectId;
1083
+ } 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 {
1091
+ throw new Error('A type cannot have a field of its same type and embedded');
1092
+ }
1093
+ }
1094
+ }
1095
+ } else if (fieldEntry.type instanceof GraphQLList) {
1096
+ if (fieldEntry.extensions && fieldEntry.extensions.relation) {
1097
+ if (fieldEntry.extensions.relation.embedded) {
1098
+ const entryType = fieldEntry.type.ofType;
1099
+ if (entryType !== gqlType) {
1100
+ schemaArg[fieldEntryName] = [generateSchemaDefinition(entryType)];
1101
+ } else {
1102
+ throw new Error('A type cannot have a field of its same type and embedded');
1103
+ }
1104
+ }
1105
+ } else if (fieldEntry.type.ofType === GraphQLString
1106
+ || fieldEntry.type.ofType instanceof GraphQLEnumType
1107
+ || (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
1108
+ schemaArg[fieldEntryName] = [String];
1109
+ } else if (fieldEntry.type.ofType === GraphQLBoolean
1110
+ || (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
1111
+ 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))) {
1114
+ schemaArg[fieldEntryName] = [Number];
1115
+ } else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType))) {
1116
+ schemaArg[fieldEntryName] = [Date];
1117
+ }
1118
+ } else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type))
1119
+ || (fieldEntry.type instanceof GraphQLNonNull && isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType)))) {
1120
+ schemaArg[fieldEntryName] = Date;
1121
+ }
1122
+ }
1123
+
1124
+ return schemaArg;
1125
+ };
1126
+
1127
+ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
1128
+ const objectIdFields = [];
1129
+
1130
+ for (const [fieldName, fieldDefinition] of Object.entries(schemaDefinition)) {
1131
+ const currentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
1132
+
1133
+ if (fieldDefinition === mongoose.Schema.Types.ObjectId) {
1134
+ // Direct ObjectId field
1135
+ objectIdFields.push(currentPath);
1136
+ } else if (typeof fieldDefinition === 'object' && fieldDefinition !== null) {
1137
+ if (Array.isArray(fieldDefinition)) {
1138
+ // Array field - check if it's an array of objects
1139
+ const arrayElement = fieldDefinition[0];
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);
1144
+ }
1145
+ } else if (fieldDefinition.type === mongoose.Schema.Types.ObjectId) {
1146
+ // Object with ObjectId type
1147
+ 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);
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ return objectIdFields;
1157
+ };
1158
+
1159
+ const createSchemaWithIndexes = (schemaDefinition) => {
1160
+ 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 => {
1167
+ schema.index({ [fieldPath]: 1 });
1168
+ });
1169
+
1170
+ return schema;
1171
+ };
1172
+
1173
+ const generateModel = (gqlType, onModelCreated) => {
1174
+ const schemaDefinition = generateSchemaDefinition(gqlType);
1175
+ const schema = createSchemaWithIndexes(schemaDefinition);
1176
+ const model = mongoose.model(gqlType.name, schema, gqlType.name);
1177
+ if (onModelCreated) {
1178
+ onModelCreated(model);
1179
+ }
1180
+ if (!preventCollectionCreation) {
1181
+ model.createCollection();
1182
+ }
1183
+ return model;
1184
+ };
1185
+
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
+ };
1196
+
1197
+ 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}.*` };
1237
+ }
1238
+
1239
+ return matches;
1240
+ };
1241
+
1242
+ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
1243
+ const aggregateClauses = {};
1244
+
1245
+ let fieldType = qlField.type;
1246
+ if (qlField.type instanceof GraphQLList) {
1247
+ fieldType = qlField.type.ofType;
1248
+ }
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
+
1283
+ aggregateClauses[fieldName] = {
1284
+ lookup,
1285
+ unwind: { $unwind: { path: `$${fieldName}`, preserveNullAndEmptyArrays: true } },
1286
+ };
1287
+ }
1288
+ }
1289
+
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
+ }
1340
+
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}`;
1350
+ }
1351
+ }
1352
+ });
1353
+ });
1354
+ }
1355
+ return aggregateClauses;
1356
+ };
1357
+
1358
+ const buildQueryTerms = async (filterField, qlField, fieldName) => {
1359
+ const aggregateClauses = {};
1360
+ const matchesClauses = {};
1361
+
1362
+ let fieldType = qlField.type;
1363
+ if (qlField.type instanceof GraphQLList) {
1364
+ fieldType = qlField.type.ofType;
1365
+ }
1366
+ if (fieldType instanceof GraphQLScalarType
1367
+ || isNonNullOfType(fieldType, GraphQLScalarType)
1368
+ || fieldType instanceof GraphQLEnumType
1369
+ || 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
+ }
1378
+ 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
+ }
1384
+
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
+ }
1413
+
1414
+ aggregateClauses[fieldName] = {
1415
+ lookup,
1416
+ unwind: { $unwind: { path: `$${fieldName}`, preserveNullAndEmptyArrays: true } },
1417
+ };
1418
+ }
1419
+ }
1420
+
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
+ }
1495
+
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
+ }
1506
+ }
1507
+ });
1508
+ }
1509
+ });
1510
+ }
1511
+ return { aggregateClauses, matchesClauses };
1512
+ };
1513
+
1514
+ const MAX_FILTER_GROUP_DEPTH = 5;
1515
+
1516
+ const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, aggregationsIncluded, depth = 0) => {
1517
+ if (depth > MAX_FILTER_GROUP_DEPTH) {
1518
+ throw new SimfinityError('Filter nesting too deep', 'FILTER_DEPTH_EXCEEDED', 400);
1519
+ }
1520
+
1521
+ const parts = [];
1522
+ const fields = gqltype.getFields();
1523
+
1524
+ // Process leaf conditions
1525
+ if (filterGroup.conditions && filterGroup.conditions.length > 0) {
1526
+ for (const condition of filterGroup.conditions) {
1527
+ const qlField = fields[condition.field];
1528
+ if (!qlField) {
1529
+ throw new SimfinityError(
1530
+ `Unknown filter field: ${condition.field}`,
1531
+ 'INVALID_FILTER_FIELD',
1532
+ 400,
1533
+ );
1534
+ }
1535
+
1536
+ let filterInput;
1537
+ let fieldType = qlField.type;
1538
+ if (fieldType instanceof GraphQLList || fieldType instanceof GraphQLNonNull) {
1539
+ fieldType = fieldType.ofType;
1540
+ }
1541
+
1542
+ if (fieldType instanceof GraphQLObjectType
1543
+ || isNonNullOfType(fieldType, GraphQLObjectType)) {
1544
+ // Object/relation field — wrap as QLTypeFilterExpression shape
1545
+ if (!condition.path) {
1546
+ throw new SimfinityError(
1547
+ `Filter on object field "${condition.field}" requires a path`,
1548
+ 'MISSING_FILTER_PATH',
1549
+ 400,
1550
+ );
1551
+ }
1552
+ filterInput = {
1553
+ terms: [{
1554
+ path: condition.path,
1555
+ operator: condition.operator,
1556
+ value: condition.value,
1557
+ }],
1558
+ };
1559
+ } else {
1560
+ // Scalar/enum field
1561
+ filterInput = {
1562
+ operator: condition.operator,
1563
+ value: condition.value,
1564
+ };
1565
+ }
1566
+
1567
+ const result = await buildQueryTerms(filterInput, qlField, condition.field);
1568
+
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
+ }
1577
+ }
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
+ }
1584
+ }
1585
+ }
1586
+ }
1587
+ }
1588
+
1589
+ // Process AND sub-groups
1590
+ if (filterGroup.AND && filterGroup.AND.length > 0) {
1591
+ 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
+ }
1598
+ }
1599
+ }
1600
+
1601
+ // Process OR sub-groups
1602
+ if (filterGroup.OR && filterGroup.OR.length > 0) {
1603
+ const orParts = [];
1604
+ 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
+ }
1611
+ }
1612
+ if (orParts.length === 1) {
1613
+ parts.push(orParts[0]);
1614
+ } else if (orParts.length > 1) {
1615
+ parts.push({ $or: orParts });
1616
+ }
1617
+ }
1618
+
1619
+ if (parts.length === 0) return null;
1620
+ if (parts.length === 1) return parts[0];
1621
+ return { $and: parts };
1622
+ };
1623
+
1624
+ const RESERVED_QUERY_KEYS = new Set(['pagination', 'sort', 'AND', 'OR', 'aggregation']);
1625
+
1626
+ const buildQuery = async (input, gqltype, isCount) => {
1627
+ const aggregateClauses = [];
1628
+ const flatMatchConditions = {};
1629
+ let hasFlat = false;
1630
+ let limitClause = { $limit: 100 };
1631
+ let skipClause = { $skip: 0 };
1632
+ let sortClause = {};
1633
+ let addSort = false;
1634
+ const aggregationsIncluded = {};
1635
+
1636
+ 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 };
1665
+ }
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
+ }
1696
+ }
1697
+
1698
+ // Combine flat conditions with AND/OR groups
1699
+ const topLevelAndParts = [];
1700
+
1701
+ if (hasFlat) {
1702
+ topLevelAndParts.push(flatMatchConditions);
1703
+ }
1704
+
1705
+ if (input.AND && input.AND.length > 0) {
1706
+ 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
+ }
1713
+ }
1714
+ }
1715
+
1716
+ if (input.OR && input.OR.length > 0) {
1717
+ const orParts = [];
1718
+ 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
+ }
1725
+ }
1726
+ if (orParts.length === 1) {
1727
+ topLevelAndParts.push(orParts[0]);
1728
+ } else if (orParts.length > 1) {
1729
+ topLevelAndParts.push({ $or: orParts });
1730
+ }
1731
+ }
1732
+
1733
+ if (topLevelAndParts.length === 1) {
1734
+ aggregateClauses.push({ $match: topLevelAndParts[0] });
1735
+ } else if (topLevelAndParts.length > 1) {
1736
+ aggregateClauses.push({ $match: { $and: topLevelAndParts } });
1737
+ }
1738
+
1739
+ if (addSort && !isCount) {
1740
+ aggregateClauses.push(sortClause);
1741
+ }
1742
+
1743
+ if (!isCount) {
1744
+ aggregateClauses.push(limitClause);
1745
+ aggregateClauses.push(skipClause);
1746
+ }
1747
+
1748
+ if (isCount) {
1749
+ aggregateClauses.push({ $count: 'size' });
1750
+ }
1751
+
1752
+ return aggregateClauses;
1753
+ };
1754
+
1755
+ 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
+ const pathParts = fieldPath.split('.');
1759
+ const aggregateClauses = [];
1760
+ let currentPath = '';
1761
+ let currentGQLType = gqltype;
1762
+
1763
+ for (let i = 0; i < pathParts.length; i++) {
1764
+ const part = pathParts[i];
1765
+ const field = currentGQLType.getFields()[part];
1766
+
1767
+ if (!field) {
1768
+ throw new Error(`Field ${part} not found in type ${currentGQLType.name}`);
1769
+ }
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
+
1785
+ 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
+
1801
+ currentPath = lookupAlias;
1802
+ 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
1807
+ currentPath = currentPath ? `${currentPath}.${part}` : part;
1808
+ currentGQLType = fieldType;
1809
+ } 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
+ }
1816
+ }
1817
+ }
1818
+
1819
+ return { mongoPath: currentPath, lookups: aggregateClauses };
1820
+ };
1821
+
1822
+ const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
1823
+ const aggregateClauses = [];
1824
+ const flatMatchConditions = {};
1825
+ let hasFlat = false;
1826
+ 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
+
1836
+ const result = await buildQueryTerms(filterField, qlField, key);
1837
+
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
+ }
1880
+
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
+ }
1891
+
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
+ }
1908
+
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;
1933
+ }
1934
+ });
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
+
1988
+ aggregateClauses.push(groupStage);
1989
+
1990
+ // Add a final projection stage to format the output
1991
+ aggregateClauses.push({
1992
+ $project: {
1993
+ _id: 0,
1994
+ groupId: '$_id',
1995
+ facts: Object.fromEntries(facts.map(fact => [fact.factName, `$${fact.factName}`])),
1996
+ },
1997
+ });
1998
+
1999
+ // Build sort object from multiple sort terms
2000
+ if (sortTerms.length > 0) {
2001
+ 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,
2021
+ });
2022
+ } 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);
2032
+ }
2033
+ if (skipClause) {
2034
+ aggregateClauses.push(skipClause);
2035
+ }
2036
+
2037
+ return aggregateClauses;
2038
+ };
2039
+
2040
+ const buildRootQuery = (name, includedTypes) => {
2041
+ const rootQueryArgs = {};
2042
+ rootQueryArgs.name = name;
2043
+ rootQueryArgs.fields = {};
2044
+
2045
+ for (const type of Object.values(typesDict.types)) {
2046
+ if (!shouldNotBeIncludedInSchema(includedTypes, type.gqltype)) {
2047
+ const wasAddedAsNoEnpointType = !type.simpleEntityEndpointName;
2048
+ if (!wasAddedAsNoEnpointType) {
2049
+ // Fixing resolve method in order to be compliant with Mongo _id field
2050
+ if (type.gqltype.getFields().id && !type.gqltype.getFields().id.resolve) {
2051
+ type.gqltype.getFields().id.resolve = (parent) => parent._id;
2052
+ }
2053
+
2054
+ rootQueryArgs.fields[type.simpleEntityEndpointName] = {
2055
+ type: type.gqltype,
2056
+ args: { id: { type: GraphQLID } },
2057
+ 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
+ const params = {
2062
+ type,
2063
+ args,
2064
+ operation: 'get_by_id',
2065
+ context,
2066
+ };
2067
+ 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);
2105
+ }
2106
+ },
2107
+ };
2108
+
2109
+ const argTypes = type.gqltype.getFields();
2110
+
2111
+ const argsObject = createArgsForQuery(argTypes);
2112
+
2113
+ rootQueryArgs.fields[type.listEntitiesEndpointName] = {
2114
+ type: new GraphQLList(type.gqltype),
2115
+ args: argsObject,
2116
+ async resolve(parent, args, context) {
2117
+ const params = {
2118
+ type,
2119
+ args,
2120
+ operation: 'find',
2121
+ context,
2122
+ };
2123
+ excecuteMiddleware(params);
2124
+ await executeScope(params);
2125
+ 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
+ }
2131
+
2132
+ let result;
2133
+ if (aggregateClauses.length === 0) {
2134
+ result = await type.model.find({});
2135
+ } else {
2136
+ result = await type.model.aggregate(aggregateClauses);
2137
+ }
2138
+ return result;
2139
+ },
2140
+ };
2141
+
2142
+ // Add aggregate endpoint
2143
+ const aggregateArgsObject = { ...argsObject };
2144
+ aggregateArgsObject.aggregation = {
2145
+ type: new GraphQLNonNull(QLTypeAggregationExpression),
2146
+ };
2147
+
2148
+ rootQueryArgs.fields[`${type.listEntitiesEndpointName}_aggregate`] = {
2149
+ type: new GraphQLList(QLTypeAggregationResult),
2150
+ args: aggregateArgsObject,
2151
+ async resolve(parent, args, context) {
2152
+ const params = {
2153
+ type,
2154
+ args,
2155
+ operation: 'aggregate',
2156
+ context,
2157
+ };
2158
+ excecuteMiddleware(params);
2159
+ await executeScope(params);
2160
+ const aggregateClauses = await buildAggregationQuery(args, type.gqltype, args.aggregation);
2161
+ const result = await type.model.aggregate(aggregateClauses);
2162
+ return result;
2163
+ },
2164
+ };
2165
+ }
2166
+ }
2167
+ }
2168
+
2169
+ return new GraphQLObjectType(rootQueryArgs);
2170
+ };
2171
+
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 => {
2179
+ if (typeInfo.gqltype && !typeInfo.model) {
2180
+ if (typeInfo.endpoint) {
2181
+ // Generate model with collection for endpoint types (types registered with connect)
2182
+ typeInfo.model = generateModel(typeInfo.gqltype, typeInfo.onModelCreated);
2183
+ } else if (typeInfo.needsModel) {
2184
+ // Generate model without collection for no-endpoint types that need models (addNoEndpointType)
2185
+ typeInfo.model = generateModelWithoutCollection(typeInfo.gqltype, null);
2186
+ }
2187
+ }
2188
+ });
2189
+
2190
+ // Also update the typesDictForUpdate with the generated models
2191
+ Object.keys(typesDict.types).forEach(typeName => {
2192
+ if (typesDictForUpdate.types[typeName]) {
2193
+ typesDictForUpdate.types[typeName].model = typesDict.types[typeName].model;
2194
+ }
2195
+ });
2196
+
2197
+ // Auto-generate resolvers for all registered types now that all types are available
2198
+ Object.values(typesDict.types).forEach(typeInfo => {
2199
+ if (typeInfo.gqltype) {
2200
+ autoGenerateResolvers(typeInfo.gqltype);
2201
+ }
2202
+ });
2203
+
2204
+ return new GraphQLSchema({
2205
+ query: buildRootQuery('RootQueryType', includedQueryTypes),
2206
+ mutation: buildMutation('Mutation', includedMutationTypes, includedCustomMutations),
2207
+ });
2208
+ };
2209
+
2210
+ export const getModel = (gqltype) => typesDict.types[gqltype.name].model;
2211
+
2212
+ export const getType = (typeName) => {
2213
+ if (typeof typeName === 'string') {
2214
+ return typesDict.types[typeName]?.gqltype;
2215
+ }
2216
+ // If it's already a GraphQL type object, get by its name
2217
+ if (typeName && typeName.name) {
2218
+ return typesDict.types[typeName.name]?.gqltype;
2219
+ }
2220
+ return null;
2221
+ };
2222
+
2223
+ export const registerMutation = (name, description, inputModel, outputModel, callback) => {
2224
+ registeredMutations[name] = {
2225
+ description,
2226
+ inputModel,
2227
+ outputModel,
2228
+ callback,
2229
+ };
2230
+ };
2231
+
2232
+ const autoGenerateResolvers = (gqltype) => {
2233
+ const fields = gqltype.getFields();
2234
+
2235
+ 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
+ }
2296
+ }
2297
+ }
2298
+ }
2299
+ }
2300
+ };
2301
+
2302
+ export const connect = (model, gqltype, simpleEntityEndpointName,
2303
+ listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
2304
+ waitingInputType[gqltype.name] = {
2305
+ model,
2306
+ gqltype,
2307
+ };
2308
+ typesDict.types[gqltype.name] = {
2309
+ model: model, // Will be generated later in createSchema if not provided
2310
+ gqltype,
2311
+ simpleEntityEndpointName,
2312
+ listEntitiesEndpointName,
2313
+ endpoint: true,
2314
+ controller,
2315
+ stateMachine,
2316
+ onModelCreated, // Store the callback for later use
2317
+ };
2318
+
2319
+ typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
2320
+ };
2321
+
2322
+ export const addNoEndpointType = (gqltype) => {
2323
+ waitingInputType[gqltype.name] = {
2324
+ gqltype,
2325
+ };
2326
+
2327
+ // Check if this type has relationship fields that might need a model
2328
+ const fields = gqltype.getFields();
2329
+ 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))) {
2335
+ needsModel = true;
2336
+ break;
2337
+ }
2338
+ }
2339
+
2340
+ 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
2346
+ };
2347
+
2348
+ typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
2349
+ };
2350
+
2351
+ export { createValidatedScalar };
2352
+
2353
+ export { default as validators } from './validators.js';
2354
+ export { default as scalars } from './scalars.js';
2355
+ export { default as plugins } from './plugins.js';
2356
+ export { default as auth } from './auth/index.js';
2357
+
2358
+ export { buildQuery, buildFilterGroupMatch };
2359
+
2360
+ const createArgsForQuery = (argTypes) => {
2361
+ const argsObject = {};
2362
+
2363
+ for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
2364
+ argsObject[fieldEntryName] = {};
2365
+
2366
+ if (fieldEntry.type instanceof GraphQLScalarType
2367
+ || isNonNullOfType(fieldEntry.type, GraphQLScalarType)
2368
+ || fieldEntry.type instanceof GraphQLEnumType
2369
+ || isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
2370
+ argsObject[fieldEntryName].type = QLFilter;
2371
+ } else if (fieldEntry.type instanceof GraphQLObjectType
2372
+ || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
2373
+ argsObject[fieldEntryName].type = QLTypeFilterExpression;
2374
+ } else if (fieldEntry.type instanceof GraphQLList) {
2375
+ const listOfType = fieldEntry.type.ofType;
2376
+ if (listOfType instanceof GraphQLScalarType
2377
+ || isNonNullOfType(listOfType, GraphQLScalarType)
2378
+ || listOfType instanceof GraphQLEnumType
2379
+ || isNonNullOfType(listOfType, GraphQLEnumType)) {
2380
+ argsObject[fieldEntryName].type = QLFilter;
2381
+ } else {
2382
+ argsObject[fieldEntryName].type = QLTypeFilterExpression;
2383
+ }
2384
+ }
2385
+ }
2386
+
2387
+ argsObject.pagination = {};
2388
+ argsObject.pagination.type = QLPagination;
2389
+
2390
+ argsObject.sort = {};
2391
+ argsObject.sort.type = QLSortExpression;
2392
+
2393
+ argsObject.AND = {};
2394
+ argsObject.AND.type = new GraphQLList(QLFilterGroup);
2395
+
2396
+ argsObject.OR = {};
2397
+ argsObject.OR.type = new GraphQLList(QLFilterGroup);
2398
+
2399
+ return argsObject;
2400
+ };
2401
+
2402
+ function formatArgs(argsArray) {
2403
+ const graphqlArgs = [];
2404
+ for (const [key, value] of argsArray) {
2405
+ const item = {
2406
+ name: key,
2407
+ type: value.type,
2408
+ };
2409
+ graphqlArgs.push(item);
2410
+ }
2411
+ return graphqlArgs;
2412
+ }