@simtlix/simfinity-js 2.4.6 → 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.
- package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +23 -0
- package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
- package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
- package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
- package/.claude/worktrees/agitated-kepler/README.md +3941 -0
- package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
- package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
- package/.claude/worktrees/agitated-kepler/package.json +41 -0
- package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
- package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
- package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
- package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
- package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
- package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
- package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
- package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
- package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
- package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
- package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
- package/.cursor/rules/simfinity-core-functions.mdc +3 -1
- package/README.md +202 -0
- package/package.json +1 -1
- package/src/index.js +235 -21
|
@@ -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
|
+
}
|