@simtlix/simfinity-js 3.0.1 → 3.2.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/README.md +150 -83
- package/package.json +17 -4
- package/src/auth/errors.js +2 -44
- package/src/auth/expressions.js +2 -273
- package/src/auth/index.js +3 -390
- package/src/auth/register-mongo-object-id.js +7 -0
- package/src/auth/rules.js +3 -273
- package/src/const/QLOperator.js +1 -39
- package/src/const/QLSort.js +1 -28
- package/src/const/QLValue.js +1 -39
- package/src/errors/internal-server.error.js +1 -11
- package/src/errors/simfinity.error.js +1 -15
- package/src/index.js +34 -1991
- package/src/mcp.js +2 -1377
- package/src/mongo/adapter.js +111 -0
- package/src/mongo/models.js +160 -0
- package/src/mongo/queries.js +570 -0
- package/src/mongo/transactions.js +81 -0
- package/src/plugins.js +2 -53
- package/src/scalars.js +2 -188
- package/src/validators.js +2 -250
- package/types/index.d.ts +65 -480
package/src/index.js
CHANGED
|
@@ -1,1942 +1,41 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
InternalServerError,
|
|
3
|
+
SimfinityError,
|
|
4
|
+
buildErrorFormatter,
|
|
5
|
+
createRuntime,
|
|
6
|
+
createValidatedScalar,
|
|
7
|
+
} from '@simtlix/simfinity-core';
|
|
8
|
+
|
|
9
|
+
import { createMongoAdapter } from './mongo/adapter.js';
|
|
10
|
+
|
|
11
|
+
const mongoAdapter = createMongoAdapter();
|
|
12
|
+
const runtime = createRuntime(mongoAdapter);
|
|
13
|
+
|
|
14
|
+
export const configureQueryLimits = runtime.configureQueryLimits;
|
|
15
|
+
export const connect = runtime.connect;
|
|
16
|
+
export const addNoEndpointType = runtime.addNoEndpointType;
|
|
17
|
+
export const createSchema = runtime.createSchema;
|
|
18
|
+
export const getModel = runtime.getModel;
|
|
19
|
+
export const getType = runtime.getType;
|
|
20
|
+
export const getInputType = runtime.getInputType;
|
|
21
|
+
export const getRegistrations = runtime.getRegistrations;
|
|
22
|
+
export const use = runtime.use;
|
|
23
|
+
export const registerMutation = runtime.registerMutation;
|
|
24
|
+
export const saveObject = runtime.saveObject;
|
|
25
|
+
export const preventCreatingCollection = runtime.preventCreatingCollection;
|
|
26
|
+
|
|
27
|
+
export const buildQuery = (...args) => mongoAdapter.buildQuery(...args);
|
|
28
|
+
export const buildFilterGroupMatch = (...args) => mongoAdapter.buildFilterGroupMatch(...args);
|
|
7
29
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const GraphQLJSON = new GraphQLScalarType({
|
|
17
|
-
name: 'JSON',
|
|
18
|
-
description: 'The `JSON` scalar type represents JSON values as specified by ECMA-404',
|
|
19
|
-
serialize(value) {
|
|
20
|
-
return value;
|
|
21
|
-
},
|
|
22
|
-
parseValue(value) {
|
|
23
|
-
return value;
|
|
24
|
-
},
|
|
25
|
-
parseLiteral(ast) {
|
|
26
|
-
switch (ast.kind) {
|
|
27
|
-
case Kind.STRING:
|
|
28
|
-
case Kind.BOOLEAN:
|
|
29
|
-
return ast.value;
|
|
30
|
-
case Kind.INT:
|
|
31
|
-
return parseInt(ast.value, 10);
|
|
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
|
-
const RelationType = new GraphQLObjectType({
|
|
52
|
-
name: 'RelationType',
|
|
53
|
-
fields: () => ({
|
|
54
|
-
embedded: { type: GraphQLBoolean },
|
|
55
|
-
connectionField: { type: GraphQLString },
|
|
56
|
-
displayField: { type: GraphQLString },
|
|
57
|
-
}),
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const FieldExtensionsType = new GraphQLObjectType({
|
|
61
|
-
name: 'FieldExtensionsType',
|
|
62
|
-
fields: () => ({
|
|
63
|
-
relation: { type: RelationType },
|
|
64
|
-
stateMachine: { type: GraphQLBoolean },
|
|
65
|
-
readOnly: { type: GraphQLBoolean },
|
|
66
|
-
}),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const fieldTypeDefinitions = __Field._fields;
|
|
70
|
-
|
|
71
|
-
const fixedFieldsWithExtensions = () => {
|
|
72
|
-
const originalFields = fieldTypeDefinitions();
|
|
73
|
-
originalFields.extensions = {
|
|
74
|
-
type: FieldExtensionsType,
|
|
75
|
-
name: 'extensions',
|
|
76
|
-
resolve: (obj) => obj.extensions,
|
|
77
|
-
args: [],
|
|
78
|
-
isDeprecated: false,
|
|
79
|
-
};
|
|
80
|
-
return originalFields;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
__Field._fields = fixedFieldsWithExtensions;
|
|
84
|
-
|
|
85
|
-
const typesDict = { types: {} };
|
|
86
|
-
const waitingInputType = {};
|
|
87
|
-
const typesDictForUpdate = { types: {} };
|
|
88
|
-
const registeredMutations = {};
|
|
89
|
-
|
|
90
|
-
const operations = {
|
|
91
|
-
SAVE: 'save',
|
|
92
|
-
UPDATE: 'update',
|
|
93
|
-
DELETE: 'delete',
|
|
94
|
-
STATE_CHANGED: 'state_changed',
|
|
95
|
-
CUSTOM_MUTATION: 'custom_mutation',
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const buildErrorFormatter = (callback) => {
|
|
99
|
-
const formatError = (err) => {
|
|
100
|
-
let result = null;
|
|
101
|
-
if (err instanceof SimfinityError) {
|
|
102
|
-
result = err;
|
|
103
|
-
} else {
|
|
104
|
-
result = new InternalServerError(err.message, err);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (callback) {
|
|
108
|
-
const formattedError = callback(result);
|
|
109
|
-
return formattedError || result;
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
};
|
|
113
|
-
return formatError;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const middlewares = [];
|
|
117
|
-
|
|
118
|
-
export const use = (middleware) => {
|
|
119
|
-
middlewares.push(middleware);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export { buildErrorFormatter };
|
|
123
|
-
|
|
124
|
-
export { SimfinityError };
|
|
125
|
-
|
|
126
|
-
export { InternalServerError };
|
|
127
|
-
|
|
128
|
-
let preventCollectionCreation = false;
|
|
129
|
-
|
|
130
|
-
export const preventCreatingCollection = (prevent) => {
|
|
131
|
-
preventCollectionCreation = !!prevent;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const QLFilter = new GraphQLInputObjectType({
|
|
135
|
-
name: 'QLFilter',
|
|
136
|
-
fields: () => ({
|
|
137
|
-
operator: { type: QLOperator },
|
|
138
|
-
value: { type: QLValue },
|
|
139
|
-
}),
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const QLTypeFilter = new GraphQLInputObjectType({
|
|
143
|
-
name: 'QLTypeFilter',
|
|
144
|
-
fields: () => ({
|
|
145
|
-
operator: { type: QLOperator },
|
|
146
|
-
value: { type: QLValue },
|
|
147
|
-
path: { type: GraphQLString },
|
|
148
|
-
}),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const IdInputType = new GraphQLInputObjectType({
|
|
152
|
-
name: 'IdInputType',
|
|
153
|
-
fields: () => ({
|
|
154
|
-
id: { type: new GraphQLNonNull(GraphQLString) },
|
|
155
|
-
}),
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const QLTypeFilterExpression = new GraphQLInputObjectType({
|
|
159
|
-
name: 'QLTypeFilterExpression',
|
|
160
|
-
fields: () => ({
|
|
161
|
-
terms: { type: new GraphQLList(QLTypeFilter) },
|
|
162
|
-
}),
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const QLFilterCondition = new GraphQLInputObjectType({
|
|
166
|
-
name: 'QLFilterCondition',
|
|
167
|
-
fields: () => ({
|
|
168
|
-
field: { type: new GraphQLNonNull(GraphQLString) },
|
|
169
|
-
operator: { type: QLOperator },
|
|
170
|
-
value: { type: QLValue },
|
|
171
|
-
path: { type: GraphQLString },
|
|
172
|
-
}),
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const QLFilterGroup = new GraphQLInputObjectType({
|
|
176
|
-
name: 'QLFilterGroup',
|
|
177
|
-
fields: () => ({
|
|
178
|
-
AND: { type: new GraphQLList(QLFilterGroup) },
|
|
179
|
-
OR: { type: new GraphQLList(QLFilterGroup) },
|
|
180
|
-
conditions: { type: new GraphQLList(QLFilterCondition) },
|
|
181
|
-
}),
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const QLPagination = new GraphQLInputObjectType({
|
|
185
|
-
name: 'QLPagination',
|
|
186
|
-
fields: () => ({
|
|
187
|
-
page: { type: new GraphQLNonNull(GraphQLInt) },
|
|
188
|
-
size: { type: new GraphQLNonNull(GraphQLInt) },
|
|
189
|
-
count: { type: GraphQLBoolean },
|
|
190
|
-
}),
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
const QLSortExpression = new GraphQLInputObjectType({
|
|
194
|
-
name: 'QLSortExpression',
|
|
195
|
-
fields: () => ({
|
|
196
|
-
terms: { type: new GraphQLList(QLSort) },
|
|
197
|
-
}),
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const QLAggregationOperation = new GraphQLEnumType({
|
|
201
|
-
name: 'QLAggregationOperation',
|
|
202
|
-
values: {
|
|
203
|
-
SUM: { value: 'SUM' },
|
|
204
|
-
COUNT: { value: 'COUNT' },
|
|
205
|
-
AVG: { value: 'AVG' },
|
|
206
|
-
MIN: { value: 'MIN' },
|
|
207
|
-
MAX: { value: 'MAX' },
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const QLTypeAggregationFact = new GraphQLInputObjectType({
|
|
212
|
-
name: 'QLTypeAggregationFact',
|
|
213
|
-
fields: () => ({
|
|
214
|
-
operation: { type: new GraphQLNonNull(QLAggregationOperation) },
|
|
215
|
-
factName: { type: new GraphQLNonNull(GraphQLString) },
|
|
216
|
-
path: { type: new GraphQLNonNull(GraphQLString) },
|
|
217
|
-
}),
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const QLTypeAggregationExpression = new GraphQLInputObjectType({
|
|
221
|
-
name: 'QLTypeAggregationExpression',
|
|
222
|
-
fields: () => ({
|
|
223
|
-
groupId: { type: new GraphQLNonNull(GraphQLString) },
|
|
224
|
-
facts: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(QLTypeAggregationFact))) },
|
|
225
|
-
}),
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const QLTypeAggregationResult = new GraphQLObjectType({
|
|
229
|
-
name: 'QLTypeAggregationResult',
|
|
230
|
-
fields: () => ({
|
|
231
|
-
groupId: { type: GraphQLJSON },
|
|
232
|
-
facts: { type: GraphQLJSON },
|
|
233
|
-
}),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const isNonNullOfType = (fieldEntryType, graphQLType) => {
|
|
237
|
-
let isOfType = false;
|
|
238
|
-
if (fieldEntryType instanceof GraphQLNonNull) {
|
|
239
|
-
isOfType = fieldEntryType.ofType instanceof graphQLType;
|
|
240
|
-
}
|
|
241
|
-
return isOfType;
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const isNonNullOfTypeForNotScalar = (fieldEntryType, graphQLType) => {
|
|
245
|
-
let isOfType = false;
|
|
246
|
-
if (fieldEntryType instanceof GraphQLNonNull) {
|
|
247
|
-
isOfType = fieldEntryType.ofType === graphQLType;
|
|
248
|
-
}
|
|
249
|
-
return isOfType;
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const getEffectiveTypeName = (type) => {
|
|
253
|
-
if (type instanceof GraphQLScalarType && type.baseScalarType) {
|
|
254
|
-
return type.baseScalarType.name;
|
|
255
|
-
}
|
|
256
|
-
return type.name;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const isGraphQLisoDate = (typeName) => typeName === 'DateTime' || typeName === 'Date' || typeName === 'Time';
|
|
260
|
-
|
|
261
|
-
const unwrapNonNull = (type) => (type instanceof GraphQLNonNull ? type.ofType : type);
|
|
262
|
-
|
|
263
|
-
const unwrapListAndNonNull = (type) => {
|
|
264
|
-
if (type instanceof GraphQLList || type instanceof GraphQLNonNull) {
|
|
265
|
-
return type.ofType;
|
|
266
|
-
}
|
|
267
|
-
return type;
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const coerceDateValue = (fieldType, holder, key) => {
|
|
271
|
-
const typeName = getEffectiveTypeName(unwrapNonNull(fieldType));
|
|
272
|
-
if (!isGraphQLisoDate(typeName)) return;
|
|
273
|
-
const raw = holder[key];
|
|
274
|
-
if (Array.isArray(raw)) {
|
|
275
|
-
holder[key] = raw.map((v) => v && new Date(v));
|
|
276
|
-
} else {
|
|
277
|
-
holder[key] = raw && new Date(raw);
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const isCustomValidatedScalar = (type) => type instanceof GraphQLScalarType && type.baseScalarType;
|
|
282
|
-
|
|
283
|
-
const matchesScalar = (fieldType, target) => {
|
|
284
|
-
if (fieldType === target) return true;
|
|
285
|
-
if (isNonNullOfTypeForNotScalar(fieldType, target)) return true;
|
|
286
|
-
if (isCustomValidatedScalar(fieldType) && fieldType.baseScalarType === target) return true;
|
|
287
|
-
if (isNonNullOfType(fieldType, GraphQLScalarType)
|
|
288
|
-
&& isCustomValidatedScalar(fieldType.ofType)
|
|
289
|
-
&& fieldType.ofType.baseScalarType === target) return true;
|
|
290
|
-
return false;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const buildRelationLookup = ({
|
|
294
|
-
collectionName, localField, foreignField, alias,
|
|
295
|
-
}) => ({
|
|
296
|
-
lookup: {
|
|
297
|
-
$lookup: {
|
|
298
|
-
from: collectionName, foreignField, localField, as: alias,
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
unwind: { $unwind: { path: `$${alias}`, preserveNullAndEmptyArrays: true } },
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
const escapeRegex = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
305
|
-
|
|
306
|
-
const PATH_SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
307
|
-
|
|
308
|
-
const assertValidFilterPath = (path) => {
|
|
309
|
-
if (typeof path !== 'string' || path.length === 0) {
|
|
310
|
-
throw new SimfinityError('Filter path must be a non-empty string', 'INVALID_FILTER_PATH', 400);
|
|
311
|
-
}
|
|
312
|
-
for (const segment of path.split('.')) {
|
|
313
|
-
if (!PATH_SEGMENT_RE.test(segment)) {
|
|
314
|
-
throw new SimfinityError(`Invalid filter path segment: "${segment}"`, 'INVALID_FILTER_PATH', 400);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const normalizeFilterCondition = (condition) => {
|
|
320
|
-
const { field } = condition;
|
|
321
|
-
if (typeof field !== 'string') return condition;
|
|
322
|
-
const dotIdx = field.indexOf('.');
|
|
323
|
-
if (dotIdx < 0) return condition;
|
|
324
|
-
if (condition.path != null && condition.path !== '') {
|
|
325
|
-
throw new SimfinityError(
|
|
326
|
-
'Filter condition cannot use both dotted field syntax and a separate path',
|
|
327
|
-
'AMBIGUOUS_FILTER_FIELD',
|
|
328
|
-
400,
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
return {
|
|
332
|
-
...condition,
|
|
333
|
-
field: field.slice(0, dotIdx),
|
|
334
|
-
path: field.slice(dotIdx + 1),
|
|
335
|
-
};
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const OP_TO_MONGO = {
|
|
339
|
-
LT: (v) => ({ $lt: v }),
|
|
340
|
-
GT: (v) => ({ $gt: v }),
|
|
341
|
-
LTE: (v) => ({ $lte: v }),
|
|
342
|
-
GTE: (v) => ({ $gte: v }),
|
|
343
|
-
NE: (v) => ({ $ne: v }),
|
|
344
|
-
BTW: (v) => ({ $gte: v[0], $lte: v[1] }),
|
|
345
|
-
LIKE: (v) => ({ $regex: `.*${escapeRegex(v)}.*` }),
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
const AGG_OP_TO_MONGO = {
|
|
349
|
-
SUM: (path) => ({ $sum: `$${path}` }),
|
|
350
|
-
COUNT: () => ({ $sum: 1 }),
|
|
351
|
-
AVG: (path) => ({ $avg: `$${path}` }),
|
|
352
|
-
MIN: (path) => ({ $min: `$${path}` }),
|
|
353
|
-
MAX: (path) => ({ $max: `$${path}` }),
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
357
|
-
if (!baseScalarType) {
|
|
358
|
-
throw new Error('baseScalarType is required');
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (!(baseScalarType instanceof GraphQLScalarType)) {
|
|
362
|
-
throw new Error('baseScalarType must be a valid GraphQL scalar type');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const validScalarTypes = [GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID];
|
|
366
|
-
const isValidStandardType = validScalarTypes.some((type) => baseScalarType === type);
|
|
367
|
-
|
|
368
|
-
if (!isValidStandardType && !baseScalarType.name) {
|
|
369
|
-
throw new Error('baseScalarType must be a standard GraphQL scalar type or a custom scalar with a valid name');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const kindMap = {
|
|
373
|
-
String: Kind.STRING,
|
|
374
|
-
Int: Kind.INT,
|
|
375
|
-
Float: Kind.FLOAT,
|
|
376
|
-
Boolean: Kind.BOOLEAN,
|
|
377
|
-
ID: Kind.STRING,
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
|
|
381
|
-
|
|
382
|
-
const scalar = new GraphQLScalarType({
|
|
383
|
-
name: `${name}_${baseScalarType.name}`,
|
|
384
|
-
description,
|
|
385
|
-
serialize(value) {
|
|
386
|
-
validate(value);
|
|
387
|
-
return baseScalarType.serialize(value);
|
|
388
|
-
},
|
|
389
|
-
parseValue(value) {
|
|
390
|
-
validate(value);
|
|
391
|
-
return baseScalarType.parseValue(value);
|
|
392
|
-
},
|
|
393
|
-
parseLiteral(ast, variables) {
|
|
394
|
-
if (ast.kind !== baseKind) {
|
|
395
|
-
throw new Error(`${name}_${baseScalarType.name} must be a ${baseScalarType.name}`);
|
|
396
|
-
}
|
|
397
|
-
const value = baseScalarType.parseLiteral(ast, variables);
|
|
398
|
-
validate(value);
|
|
399
|
-
return value;
|
|
400
|
-
},
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
scalar.baseScalarType = baseScalarType;
|
|
404
|
-
return scalar;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Creates a new GraphQLInputObjectType with a field excluded.
|
|
409
|
-
* @param {string} inputNamePrefix - The prefix for the input type name.
|
|
410
|
-
* @param {GraphQLInputObjectType} originalType - The original input type.
|
|
411
|
-
* @param {string} fieldToExclude - The name of the field to exclude.
|
|
412
|
-
* @returns {GraphQLInputObjectType} A new input type without the specified field.
|
|
413
|
-
*/
|
|
414
|
-
const createTypeWithExcludedField = (inputNamePrefix, originalType, fieldToExclude) => {
|
|
415
|
-
const originalFields = originalType.getFields();
|
|
416
|
-
const newFields = Object.fromEntries(
|
|
417
|
-
Object.entries(originalFields)
|
|
418
|
-
.filter(([fieldName]) => fieldName !== fieldToExclude)
|
|
419
|
-
.map(([fieldName, field]) => [fieldName, {
|
|
420
|
-
type: field.type,
|
|
421
|
-
description: field.description,
|
|
422
|
-
defaultValue: field.defaultValue,
|
|
423
|
-
}]),
|
|
424
|
-
);
|
|
425
|
-
|
|
426
|
-
return new GraphQLInputObjectType({
|
|
427
|
-
name: `${inputNamePrefix}${originalType.name}For${fieldToExclude.charAt(0).toUpperCase() + fieldToExclude.slice(1)}`,
|
|
428
|
-
fields: newFields,
|
|
429
|
-
});
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
const createOneToManyInputType = (inputNamePrefix, fieldEntryName,
|
|
433
|
-
inputType, updateInputType, connectionField) => {
|
|
434
|
-
let inputTypeForAdd = inputType;
|
|
435
|
-
|
|
436
|
-
if (connectionField) {
|
|
437
|
-
inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, connectionField);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return new GraphQLInputObjectType({
|
|
441
|
-
name: `OneToMany${inputNamePrefix}${fieldEntryName}`,
|
|
442
|
-
fields: () => ({
|
|
443
|
-
added: {
|
|
444
|
-
type: new GraphQLList(inputTypeForAdd),
|
|
445
|
-
},
|
|
446
|
-
updated: {
|
|
447
|
-
type: new GraphQLList(updateInputType),
|
|
448
|
-
},
|
|
449
|
-
deleted: {
|
|
450
|
-
type: new GraphQLList(GraphQLID),
|
|
451
|
-
},
|
|
452
|
-
}),
|
|
453
|
-
});
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const graphQLListInputType = (dict, fieldEntry, fieldEntryName, inputNamePrefix, connectionField) => {
|
|
457
|
-
const { ofType } = fieldEntry.type;
|
|
458
|
-
|
|
459
|
-
if (ofType instanceof GraphQLObjectType && dict.types[ofType.name].inputType) {
|
|
460
|
-
if (!fieldEntry.extensions || !fieldEntry.extensions.relation
|
|
461
|
-
|| !fieldEntry.extensions.relation.embedded) {
|
|
462
|
-
const oneToMany = createOneToManyInputType(inputNamePrefix, fieldEntryName,
|
|
463
|
-
typesDict.types[ofType.name].inputType, typesDictForUpdate.types[ofType.name].inputType, connectionField);
|
|
464
|
-
return oneToMany;
|
|
465
|
-
}
|
|
466
|
-
if (fieldEntry.extensions && fieldEntry.extensions.relation
|
|
467
|
-
&& fieldEntry.extensions.relation.embedded) {
|
|
468
|
-
return new GraphQLList(dict.types[ofType.name].inputType);
|
|
469
|
-
}
|
|
470
|
-
} else if (ofType instanceof GraphQLScalarType || ofType instanceof GraphQLEnumType) {
|
|
471
|
-
return new GraphQLList(ofType);
|
|
472
|
-
}
|
|
473
|
-
return null;
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
const buildInputType = (gqltype) => {
|
|
477
|
-
const argTypes = gqltype.getFields();
|
|
478
|
-
|
|
479
|
-
const fieldsArgs = {};
|
|
480
|
-
const fieldsArgForUpdate = {};
|
|
481
|
-
|
|
482
|
-
const selfReferenceCollections = {};
|
|
483
|
-
|
|
484
|
-
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
485
|
-
const fieldArg = {};
|
|
486
|
-
const fieldArgForUpdate = {};
|
|
487
|
-
|
|
488
|
-
if (!fieldEntry.extensions || !fieldEntry.extensions.readOnly) {
|
|
489
|
-
const hasStateMachine = !!typesDict.types[gqltype.name].stateMachine;
|
|
490
|
-
const stateFieldManagedByStateMachine = !!(fieldEntryName === 'state' && hasStateMachine);
|
|
491
|
-
|
|
492
|
-
if (!stateFieldManagedByStateMachine) {
|
|
493
|
-
if (fieldEntry.type instanceof GraphQLScalarType
|
|
494
|
-
|| fieldEntry.type instanceof GraphQLEnumType
|
|
495
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
496
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
497
|
-
if (fieldEntryName !== 'id') {
|
|
498
|
-
fieldArg.type = fieldEntry.type;
|
|
499
|
-
}
|
|
500
|
-
fieldArgForUpdate.type = fieldEntry.type instanceof GraphQLNonNull
|
|
501
|
-
? fieldEntry.type.ofType : fieldEntry.type;
|
|
502
|
-
if (fieldEntry.type === GraphQLID) {
|
|
503
|
-
fieldArgForUpdate.type = new GraphQLNonNull(GraphQLID);
|
|
504
|
-
}
|
|
505
|
-
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
506
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
507
|
-
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
508
|
-
const fieldEntryNameValue = fieldEntry.type instanceof GraphQLNonNull
|
|
509
|
-
? fieldEntry.type.ofType.name : fieldEntry.type.name;
|
|
510
|
-
if (!fieldEntry.extensions.relation.embedded) {
|
|
511
|
-
fieldArg.type = fieldEntry.type instanceof GraphQLNonNull
|
|
512
|
-
? new GraphQLNonNull(IdInputType) : IdInputType;
|
|
513
|
-
fieldArgForUpdate.type = IdInputType;
|
|
514
|
-
} else if (typesDict.types[fieldEntryNameValue].inputType
|
|
515
|
-
&& typesDictForUpdate.types[fieldEntryNameValue].inputType) {
|
|
516
|
-
fieldArg.type = typesDict.types[fieldEntryNameValue].inputType;
|
|
517
|
-
fieldArgForUpdate.type = typesDictForUpdate.types[fieldEntryNameValue].inputType;
|
|
518
|
-
} else {
|
|
519
|
-
return null;
|
|
520
|
-
}
|
|
521
|
-
} else {
|
|
522
|
-
console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
|
|
523
|
-
}
|
|
524
|
-
} else if (fieldEntry.type instanceof GraphQLList) {
|
|
525
|
-
if (fieldEntry.type.ofType === gqltype) {
|
|
526
|
-
selfReferenceCollections[fieldEntryName] = fieldEntry;
|
|
527
|
-
} else {
|
|
528
|
-
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, `${gqltype.name}A`, fieldEntry.extensions?.relation?.connectionField);
|
|
529
|
-
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, `${gqltype.name}U`, fieldEntry.extensions?.relation?.connectionField);
|
|
530
|
-
if (listInputTypeForAdd && listInputTypeForUpdate) {
|
|
531
|
-
fieldArg.type = listInputTypeForAdd;
|
|
532
|
-
fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
533
|
-
} else {
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
fieldArg.description = fieldEntry.description;
|
|
539
|
-
fieldArgForUpdate.description = fieldEntry.description;
|
|
540
|
-
|
|
541
|
-
if (fieldArg.type) {
|
|
542
|
-
fieldsArgs[fieldEntryName] = fieldArg;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (fieldArgForUpdate.type) {
|
|
546
|
-
fieldsArgForUpdate[fieldEntryName] = fieldArgForUpdate;
|
|
547
|
-
}
|
|
548
|
-
} else {
|
|
549
|
-
fieldEntry.extensions = { ...fieldEntry.extensions, stateMachine: true };
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const inputTypeBody = {
|
|
555
|
-
name: `${gqltype.name}Input`,
|
|
556
|
-
description: gqltype.description,
|
|
557
|
-
fields: fieldsArgs,
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const inputTypeBodyForUpdate = {
|
|
561
|
-
name: `${gqltype.name}InputForUpdate`,
|
|
562
|
-
description: gqltype.description,
|
|
563
|
-
fields: fieldsArgForUpdate,
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
const inputTypeForAdd = new GraphQLInputObjectType(inputTypeBody);
|
|
567
|
-
const inputTypeForUpdate = new GraphQLInputObjectType(inputTypeBodyForUpdate);
|
|
568
|
-
|
|
569
|
-
const inputTypeForAddFields = inputTypeForAdd._fields();
|
|
570
|
-
|
|
571
|
-
Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
|
|
572
|
-
if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
|
|
573
|
-
inputTypeForAddFields[fieldEntryName] = {
|
|
574
|
-
type: createOneToManyInputType('A', fieldEntryName, inputTypeForAdd, inputTypeForUpdate, selfReferenceCollections[fieldEntryName].extensions?.relation?.connectionField),
|
|
575
|
-
name: fieldEntryName,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
inputTypeForAdd._fields = () => inputTypeForAddFields;
|
|
581
|
-
|
|
582
|
-
const inputTypeForUpdateFields = inputTypeForUpdate._fields();
|
|
583
|
-
|
|
584
|
-
Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
|
|
585
|
-
if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
|
|
586
|
-
inputTypeForUpdateFields[fieldEntryName] = {
|
|
587
|
-
type: createOneToManyInputType('U', fieldEntryName, inputTypeForAdd, inputTypeForUpdate, selfReferenceCollections[fieldEntryName].extensions?.relation?.connectionField),
|
|
588
|
-
name: fieldEntryName,
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
inputTypeForUpdate._fields = () => inputTypeForUpdateFields;
|
|
594
|
-
|
|
595
|
-
return { inputTypeBody: inputTypeForAdd, inputTypeBodyForUpdate: inputTypeForUpdate };
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
const getInputType = (type) => typesDict.types[type.name].inputType;
|
|
599
|
-
|
|
600
|
-
export { getInputType };
|
|
601
|
-
|
|
602
|
-
const buildPendingInputTypes = (waitingForInputType) => {
|
|
603
|
-
let pending = waitingForInputType;
|
|
604
|
-
let previousPendingCount = Object.keys(pending).length + 1;
|
|
605
|
-
|
|
606
|
-
while (Object.keys(pending).length > 0) {
|
|
607
|
-
const currentCount = Object.keys(pending).length;
|
|
608
|
-
if (currentCount >= previousPendingCount) {
|
|
609
|
-
const unresolved = Object.keys(pending).join(', ');
|
|
610
|
-
throw new SimfinityError(
|
|
611
|
-
`Could not build input types for: ${unresolved}. Check for circular or misconfigured relations.`,
|
|
612
|
-
'INPUT_TYPE_UNRESOLVED',
|
|
613
|
-
500,
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
previousPendingCount = currentCount;
|
|
617
|
-
|
|
618
|
-
const stillWaiting = {};
|
|
619
|
-
for (const [key, value] of Object.entries(pending)) {
|
|
620
|
-
const { gqltype } = value;
|
|
621
|
-
if (typesDict.types[gqltype.name].inputType) continue;
|
|
622
|
-
|
|
623
|
-
const result = buildInputType(gqltype);
|
|
624
|
-
if (result && result.inputTypeBody && result.inputTypeBodyForUpdate) {
|
|
625
|
-
typesDict.types[gqltype.name].inputType = result.inputTypeBody;
|
|
626
|
-
typesDictForUpdate.types[gqltype.name].inputType = result.inputTypeBodyForUpdate;
|
|
627
|
-
} else {
|
|
628
|
-
stillWaiting[key] = value;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
pending = stillWaiting;
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
const isEmpty = (value) => !value && value !== false && value !== 0;
|
|
636
|
-
|
|
637
|
-
const materializeModel = async (args, gqltype, linkToParent, operation, session) => {
|
|
638
|
-
if (!args) {
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const argTypes = gqltype.getFields();
|
|
643
|
-
|
|
644
|
-
const modelArgs = {};
|
|
645
|
-
const collectionFields = {};
|
|
646
|
-
|
|
647
|
-
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
648
|
-
if (fieldEntry.extensions && fieldEntry.extensions.validations
|
|
649
|
-
&& fieldEntry.extensions.validations[operation]) {
|
|
650
|
-
for (const validator of fieldEntry.extensions.validations[operation]) {
|
|
651
|
-
await validator.validate(gqltype.name, fieldEntryName, args[fieldEntryName], session);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (!isEmpty(args[fieldEntryName])) {
|
|
656
|
-
if (fieldEntry.type instanceof GraphQLScalarType
|
|
657
|
-
|| fieldEntry.type instanceof GraphQLEnumType
|
|
658
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
659
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
660
|
-
modelArgs[fieldEntryName] = args[fieldEntryName];
|
|
661
|
-
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
662
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
663
|
-
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
664
|
-
if (!fieldEntry.extensions.relation.embedded) {
|
|
665
|
-
modelArgs[fieldEntry.extensions.relation.connectionField] = new mongoose.Types
|
|
666
|
-
.ObjectId(args[fieldEntryName].id);
|
|
667
|
-
} else {
|
|
668
|
-
const fieldType = fieldEntry.type instanceof GraphQLNonNull
|
|
669
|
-
? fieldEntry.type.ofType : fieldEntry.type;
|
|
670
|
-
modelArgs[fieldEntryName] = (await materializeModel(args[fieldEntryName], fieldType,
|
|
671
|
-
null, operation, session)).modelArgs;
|
|
672
|
-
}
|
|
673
|
-
} else {
|
|
674
|
-
throw new SimfinityError(
|
|
675
|
-
`Field ${gqltype.name}.${fieldEntryName} is an object type but does not define extensions.relation`,
|
|
676
|
-
'MISSING_RELATION_EXTENSION',
|
|
677
|
-
500,
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
} else if (fieldEntry.type instanceof GraphQLList) {
|
|
681
|
-
const { ofType } = fieldEntry.type;
|
|
682
|
-
if (ofType instanceof GraphQLObjectType && fieldEntry.extensions
|
|
683
|
-
&& fieldEntry.extensions.relation) {
|
|
684
|
-
if (!fieldEntry.extensions.relation.embedded) {
|
|
685
|
-
collectionFields[fieldEntryName] = args[fieldEntryName];
|
|
686
|
-
} else if (fieldEntry.extensions.relation.embedded) {
|
|
687
|
-
const collectionEntries = [];
|
|
688
|
-
|
|
689
|
-
for (const element of args[fieldEntryName]) {
|
|
690
|
-
const collectionEntry = (await materializeModel(element, ofType,
|
|
691
|
-
null, operation, session)).modelArgs;
|
|
692
|
-
if (collectionEntry) {
|
|
693
|
-
collectionEntries.push(collectionEntry);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
modelArgs[fieldEntryName] = collectionEntries;
|
|
697
|
-
}
|
|
698
|
-
} else if (ofType instanceof GraphQLScalarType || ofType instanceof GraphQLEnumType) {
|
|
699
|
-
modelArgs[fieldEntryName] = args[fieldEntryName];
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (linkToParent) {
|
|
706
|
-
linkToParent(modelArgs);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
if (gqltype.extensions && gqltype.extensions.validations
|
|
710
|
-
&& gqltype.extensions.validations[operation]) {
|
|
711
|
-
for (const validator of gqltype.extensions.validations[operation]) {
|
|
712
|
-
await validator.validate(gqltype.name, args, modelArgs, session);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
return { modelArgs, collectionFields };
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
const MAX_TRANSIENT_RETRIES = 5;
|
|
720
|
-
|
|
721
|
-
const withTransaction = async (session, body) => {
|
|
722
|
-
const ownsSession = !session;
|
|
723
|
-
const mySession = session || await mongoose.startSession();
|
|
724
|
-
try {
|
|
725
|
-
for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
|
|
726
|
-
await mySession.startTransaction();
|
|
727
|
-
try {
|
|
728
|
-
const result = await body(mySession);
|
|
729
|
-
await mySession.commitTransaction();
|
|
730
|
-
return result;
|
|
731
|
-
} catch (error) {
|
|
732
|
-
await mySession.abortTransaction();
|
|
733
|
-
const isTransient = error.errorLabels?.includes('TransientTransactionError');
|
|
734
|
-
if (isTransient && attempt < MAX_TRANSIENT_RETRIES) {
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
throw error;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
throw new SimfinityError('Transaction exceeded retry limit', 'TRANSACTION_RETRY_EXCEEDED', 500);
|
|
741
|
-
} finally {
|
|
742
|
-
if (ownsSession) {
|
|
743
|
-
mySession.endSession();
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
const executeRegisteredMutation = (args, callback, context, session) => withTransaction(
|
|
749
|
-
session,
|
|
750
|
-
(mySession) => callback(args, mySession, context),
|
|
751
|
-
);
|
|
752
|
-
|
|
753
|
-
const iterateOnCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
|
|
754
|
-
for (const [collectionFieldKey, collectionField] of Object.entries(materializedModel.collectionFields)) {
|
|
755
|
-
if (collectionField.added) {
|
|
756
|
-
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
757
|
-
collectionField.added, operations.SAVE, context);
|
|
758
|
-
}
|
|
759
|
-
if (collectionField.updated) {
|
|
760
|
-
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
761
|
-
collectionField.updated, operations.UPDATE, context);
|
|
762
|
-
}
|
|
763
|
-
if (collectionField.deleted) {
|
|
764
|
-
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
765
|
-
collectionField.deleted, operations.DELETE, context);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
const onDelete = async (Model, controller, id, session, context) => {
|
|
771
|
-
const currentObject = await Model.findById({ _id: id }).session(session).lean();
|
|
772
|
-
|
|
773
|
-
if (controller && controller.onDelete) {
|
|
774
|
-
await controller.onDelete(currentObject, session, context);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return Model.findByIdAndDelete({ _id: id }).session(session);
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
const getEmbeddedFieldNames = (gqltype) => {
|
|
781
|
-
const cached = typesDict.types[gqltype.name];
|
|
782
|
-
if (cached && cached.embeddedFieldNames) return cached.embeddedFieldNames;
|
|
783
|
-
const names = [];
|
|
784
|
-
for (const [fieldName, fieldEntry] of Object.entries(gqltype.getFields())) {
|
|
785
|
-
if (fieldEntry.extensions?.relation?.embedded) names.push(fieldName);
|
|
786
|
-
}
|
|
787
|
-
if (cached) cached.embeddedFieldNames = names;
|
|
788
|
-
return names;
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
|
|
792
|
-
const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
|
|
793
|
-
const objectId = args.id;
|
|
794
|
-
const argTypes = gqltype.getFields();
|
|
795
|
-
const embeddedFieldNames = getEmbeddedFieldNames(gqltype);
|
|
796
|
-
|
|
797
|
-
if (embeddedFieldNames.length > 0) {
|
|
798
|
-
const projection = Object.fromEntries(embeddedFieldNames.map((name) => [name, 1]));
|
|
799
|
-
const currentObject = await Model.findById(objectId, projection).session(session).lean();
|
|
800
|
-
if (currentObject) {
|
|
801
|
-
for (const fieldEntryName of embeddedFieldNames) {
|
|
802
|
-
const oldObjectData = currentObject[fieldEntryName];
|
|
803
|
-
const newObjectData = materializedModel.modelArgs[fieldEntryName];
|
|
804
|
-
if (newObjectData) {
|
|
805
|
-
if (Array.isArray(oldObjectData) && Array.isArray(newObjectData)) {
|
|
806
|
-
materializedModel.modelArgs[fieldEntryName] = newObjectData;
|
|
807
|
-
} else {
|
|
808
|
-
materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
816
|
-
if (args[fieldEntryName] === null && !(fieldEntry.type instanceof GraphQLNonNull)) {
|
|
817
|
-
materializedModel.modelArgs = {
|
|
818
|
-
...materializedModel.modelArgs,
|
|
819
|
-
$unset: { ...materializedModel.modelArgs.$unset, [fieldEntryName]: '' },
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if (controller && controller.onUpdating) {
|
|
825
|
-
await controller.onUpdating(objectId, materializedModel.modelArgs, session, context);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
const result = Model.findByIdAndUpdate(
|
|
829
|
-
objectId, materializedModel.modelArgs, { new: true },
|
|
830
|
-
).session(session);
|
|
831
|
-
|
|
832
|
-
if (materializedModel.collectionFields) {
|
|
833
|
-
await iterateOnCollectionFields(materializedModel, gqltype, objectId, session, context);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
if (controller && controller.onUpdated) {
|
|
837
|
-
await controller.onUpdated(result, session, context);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
return result;
|
|
841
|
-
};
|
|
842
|
-
|
|
843
|
-
const onStateChanged = async (Model, gqltype, controller, args, session, actionField, context) => {
|
|
844
|
-
const storedModel = await Model.findById(args.id).session(session);
|
|
845
|
-
if (!storedModel) {
|
|
846
|
-
throw new SimfinityError(`${gqltype.name} ${args.id} is not valid`, 'NOT_VALID_ID', 404);
|
|
847
|
-
}
|
|
848
|
-
if (storedModel.state === actionField.from.name) {
|
|
849
|
-
if (actionField.action) {
|
|
850
|
-
await actionField.action(args, session);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
args.state = actionField.to.name;
|
|
854
|
-
let result = await onUpdateSubject(Model, gqltype, controller, args, session, null, context);
|
|
855
|
-
result = result.toObject();
|
|
856
|
-
result.state = actionField.to.value;
|
|
857
|
-
return result;
|
|
858
|
-
}
|
|
859
|
-
throw new SimfinityError(`Action is not allowed from state ${storedModel.state}`, 'BAD_REQUEST', 400);
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
const onSaveObject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
|
|
863
|
-
const materializedModel = await materializeModel(args, gqltype, linkToParent, 'CREATE', session);
|
|
864
|
-
if (typesDict.types[gqltype.name].stateMachine) {
|
|
865
|
-
materializedModel.modelArgs.state = typesDict.types[gqltype.name]
|
|
866
|
-
.stateMachine.initialState.name;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const newObject = new Model(materializedModel.modelArgs);
|
|
870
|
-
newObject.$session(session);
|
|
871
|
-
|
|
872
|
-
if (controller && controller.onSaving) {
|
|
873
|
-
await controller.onSaving(newObject, args, session, context);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
let result = await newObject.save();
|
|
877
|
-
result = result.toObject();
|
|
878
|
-
|
|
879
|
-
if (materializedModel.collectionFields) {
|
|
880
|
-
await iterateOnCollectionFields(materializedModel, gqltype, newObject._id, session, context);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
if (controller && controller.onSaved) {
|
|
884
|
-
await controller.onSaved(result, args, session, context);
|
|
885
|
-
}
|
|
886
|
-
if (typesDict.types[gqltype.name].stateMachine) {
|
|
887
|
-
result.state = typesDict.types[gqltype.name].stateMachine.initialState.value;
|
|
888
|
-
}
|
|
889
|
-
return result;
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
export const saveObject = async (typeName, args, session, context) => {
|
|
893
|
-
const type = typesDict.types[typeName];
|
|
894
|
-
return onSaveObject(type.model, type.gqltype, type.controller, args, session, null, context);
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
const executeOperation = (Model, gqltype, controller, args, operation, actionField, session, context) => withTransaction(
|
|
898
|
-
session,
|
|
899
|
-
async (mySession) => {
|
|
900
|
-
switch (operation) {
|
|
901
|
-
case operations.SAVE:
|
|
902
|
-
return onSaveObject(Model, gqltype, controller, args, mySession, null, context);
|
|
903
|
-
case operations.UPDATE:
|
|
904
|
-
return onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
|
|
905
|
-
case operations.DELETE:
|
|
906
|
-
return onDelete(Model, controller, args, mySession, context);
|
|
907
|
-
case operations.STATE_CHANGED:
|
|
908
|
-
return onStateChanged(Model, gqltype, controller, args, mySession, actionField, context);
|
|
909
|
-
default:
|
|
910
|
-
return null;
|
|
911
|
-
}
|
|
912
|
-
},
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
916
|
-
collectionFieldsList, operationType, context) => {
|
|
917
|
-
const argTypes = gqltype.getFields();
|
|
918
|
-
const collectionGQLType = argTypes[collectionField].type.ofType;
|
|
919
|
-
const { connectionField } = argTypes[collectionField].extensions.relation;
|
|
920
|
-
|
|
921
|
-
let operationFunction = async () => { };
|
|
922
|
-
|
|
923
|
-
switch (operationType) {
|
|
924
|
-
case operations.SAVE:
|
|
925
|
-
operationFunction = async (collectionItem) => {
|
|
926
|
-
await onSaveObject(typesDict.types[collectionGQLType.name].model, collectionGQLType,
|
|
927
|
-
typesDict.types[collectionGQLType.name].controller, collectionItem, session, (item) => {
|
|
928
|
-
item[connectionField] = objectId;
|
|
929
|
-
}, context);
|
|
930
|
-
};
|
|
931
|
-
break;
|
|
932
|
-
case operations.UPDATE:
|
|
933
|
-
operationFunction = async (collectionItem) => {
|
|
934
|
-
await onUpdateSubject(typesDict.types[collectionGQLType.name].model, collectionGQLType,
|
|
935
|
-
typesDict.types[collectionGQLType.name].controller, collectionItem, session, (item) => {
|
|
936
|
-
item[connectionField] = objectId;
|
|
937
|
-
}, context);
|
|
938
|
-
};
|
|
939
|
-
break;
|
|
940
|
-
case operations.DELETE:
|
|
941
|
-
operationFunction = async (collectionItem) => {
|
|
942
|
-
await onDelete(typesDict.types[collectionGQLType.name].model,
|
|
943
|
-
typesDict.types[collectionGQLType.name].controller, collectionItem, session, context);
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
for (const element of collectionFieldsList) {
|
|
948
|
-
await operationFunction(element);
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
|
|
952
|
-
const shouldNotBeIncludedInSchema = (includedTypes,
|
|
953
|
-
type) => includedTypes && !includedTypes.includes(type);
|
|
954
|
-
|
|
955
|
-
const executeMiddleware = async (context) => {
|
|
956
|
-
const buildNext = (middlewaresParam) => {
|
|
957
|
-
if (!middlewaresParam) {
|
|
958
|
-
return async () => {};
|
|
959
|
-
}
|
|
960
|
-
return async () => {
|
|
961
|
-
const middleware = middlewaresParam[0];
|
|
962
|
-
if (middleware) {
|
|
963
|
-
await middleware(context, buildNext(middlewaresParam.slice(1)));
|
|
964
|
-
}
|
|
965
|
-
};
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
await buildNext(middlewares)();
|
|
969
|
-
};
|
|
970
|
-
|
|
971
|
-
const executeScope = async (params) => {
|
|
972
|
-
const { type, args, operation, context } = params;
|
|
973
|
-
|
|
974
|
-
if (!type || !type.gqltype || !type.gqltype.extensions) {
|
|
975
|
-
return null;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
const extensions = type.gqltype.extensions;
|
|
979
|
-
if (!extensions.scope || !extensions.scope[operation]) {
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
const scopeFunction = extensions.scope[operation];
|
|
984
|
-
if (typeof scopeFunction !== 'function') {
|
|
985
|
-
return null;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return scopeFunction({ type, args, operation, context });
|
|
989
|
-
};
|
|
990
|
-
|
|
991
|
-
const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
|
|
992
|
-
const rootQueryArgs = {};
|
|
993
|
-
rootQueryArgs.name = name;
|
|
994
|
-
rootQueryArgs.fields = {};
|
|
995
|
-
|
|
996
|
-
buildPendingInputTypes(waitingInputType);
|
|
997
|
-
|
|
998
|
-
for (const type of Object.values(typesDict.types)) {
|
|
999
|
-
if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
1000
|
-
if (type.endpoint) {
|
|
1001
|
-
const argsObject = { input: { type: new GraphQLNonNull(type.inputType) } };
|
|
1002
|
-
|
|
1003
|
-
rootQueryArgs.fields[`add${type.simpleEntityEndpointName}`] = {
|
|
1004
|
-
type: type.gqltype,
|
|
1005
|
-
description: 'add',
|
|
1006
|
-
args: argsObject,
|
|
1007
|
-
async resolve(parent, args, context) {
|
|
1008
|
-
const params = {
|
|
1009
|
-
type,
|
|
1010
|
-
args,
|
|
1011
|
-
operation: operations.SAVE,
|
|
1012
|
-
context,
|
|
1013
|
-
};
|
|
1014
|
-
|
|
1015
|
-
await executeMiddleware(params);
|
|
1016
|
-
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1017
|
-
args.input, operations.SAVE, null, null, context);
|
|
1018
|
-
},
|
|
1019
|
-
};
|
|
1020
|
-
rootQueryArgs.fields[`delete${type.simpleEntityEndpointName}`] = {
|
|
1021
|
-
type: type.gqltype,
|
|
1022
|
-
description: 'delete',
|
|
1023
|
-
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
|
|
1024
|
-
async resolve(parent, args, context) {
|
|
1025
|
-
const params = {
|
|
1026
|
-
type,
|
|
1027
|
-
args,
|
|
1028
|
-
operation: operations.DELETE,
|
|
1029
|
-
context,
|
|
1030
|
-
};
|
|
1031
|
-
|
|
1032
|
-
await executeMiddleware(params);
|
|
1033
|
-
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1034
|
-
args.id, operations.DELETE, null, null, context);
|
|
1035
|
-
},
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
for (const type of Object.values(typesDictForUpdate.types)) {
|
|
1042
|
-
if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
1043
|
-
if (type.endpoint) {
|
|
1044
|
-
const argsObject = { input: { type: new GraphQLNonNull(type.inputType) } };
|
|
1045
|
-
rootQueryArgs.fields[`update${type.simpleEntityEndpointName}`] = {
|
|
1046
|
-
type: type.gqltype,
|
|
1047
|
-
description: 'update',
|
|
1048
|
-
args: argsObject,
|
|
1049
|
-
async resolve(parent, args, context) {
|
|
1050
|
-
const params = {
|
|
1051
|
-
type,
|
|
1052
|
-
args,
|
|
1053
|
-
operation: operations.UPDATE,
|
|
1054
|
-
context,
|
|
1055
|
-
};
|
|
1056
|
-
|
|
1057
|
-
await executeMiddleware(params);
|
|
1058
|
-
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1059
|
-
args.input, operations.UPDATE, null, null, context);
|
|
1060
|
-
},
|
|
1061
|
-
};
|
|
1062
|
-
if (type.stateMachine) {
|
|
1063
|
-
for (const [actionName, actionField] of Object.entries(type.stateMachine.actions)) {
|
|
1064
|
-
if ({}.hasOwnProperty.call(type.stateMachine.actions, actionName)) {
|
|
1065
|
-
rootQueryArgs.fields[`${actionName}_${type.simpleEntityEndpointName}`] = {
|
|
1066
|
-
type: type.gqltype,
|
|
1067
|
-
description: actionField.description,
|
|
1068
|
-
args: argsObject,
|
|
1069
|
-
async resolve(parent, args, context) {
|
|
1070
|
-
const params = {
|
|
1071
|
-
type,
|
|
1072
|
-
args,
|
|
1073
|
-
operation: operations.STATE_CHANGED,
|
|
1074
|
-
actionName,
|
|
1075
|
-
actionField,
|
|
1076
|
-
context,
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
await executeMiddleware(params);
|
|
1080
|
-
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1081
|
-
args.input, operations.STATE_CHANGED, actionField, null, context);
|
|
1082
|
-
},
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
for (const [entry, registeredMutation] of Object.entries(registeredMutations)) {
|
|
1092
|
-
if (!shouldNotBeIncludedInSchema(includedCustomMutations, entry)) {
|
|
1093
|
-
const argsObject = registeredMutation.inputModel
|
|
1094
|
-
? { input: { type: new GraphQLNonNull(registeredMutation.inputModel) } } : null;
|
|
1095
|
-
rootQueryArgs.fields[entry] = {
|
|
1096
|
-
type: registeredMutation.outputModel,
|
|
1097
|
-
description: registeredMutation.description,
|
|
1098
|
-
args: argsObject,
|
|
1099
|
-
async resolve(parent, args, context) {
|
|
1100
|
-
const params = {
|
|
1101
|
-
args,
|
|
1102
|
-
operation: operations.CUSTOM_MUTATION,
|
|
1103
|
-
entry,
|
|
1104
|
-
context,
|
|
1105
|
-
};
|
|
1106
|
-
await executeMiddleware(params);
|
|
1107
|
-
return executeRegisteredMutation(args.input, registeredMutation.callback, context);
|
|
1108
|
-
},
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
return new GraphQLObjectType(rootQueryArgs);
|
|
1114
|
-
};
|
|
1115
|
-
|
|
1116
|
-
const listItemMatchesScalar = (listType, target) => {
|
|
1117
|
-
const ofType = listType.ofType;
|
|
1118
|
-
return ofType === target
|
|
1119
|
-
|| (isCustomValidatedScalar(ofType) && ofType.baseScalarType === target);
|
|
1120
|
-
};
|
|
1121
|
-
|
|
1122
|
-
const withUnique = (fieldEntry, mongoType) => (fieldEntry.extensions && fieldEntry.extensions.unique
|
|
1123
|
-
? { type: mongoType, unique: true }
|
|
1124
|
-
: mongoType);
|
|
1125
|
-
|
|
1126
|
-
const generateSchemaDefinition = (gqlType) => {
|
|
1127
|
-
const argTypes = gqlType.getFields();
|
|
1128
|
-
const schemaArg = {};
|
|
1129
|
-
|
|
1130
|
-
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
1131
|
-
const { type } = fieldEntry;
|
|
1132
|
-
|
|
1133
|
-
if (matchesScalar(type, GraphQLID)) {
|
|
1134
|
-
schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
|
|
1135
|
-
} else if (matchesScalar(type, GraphQLString)
|
|
1136
|
-
|| type instanceof GraphQLEnumType
|
|
1137
|
-
|| isNonNullOfType(type, GraphQLEnumType)) {
|
|
1138
|
-
schemaArg[fieldEntryName] = withUnique(fieldEntry, String);
|
|
1139
|
-
} else if (matchesScalar(type, GraphQLInt) || matchesScalar(type, GraphQLFloat)) {
|
|
1140
|
-
schemaArg[fieldEntryName] = withUnique(fieldEntry, Number);
|
|
1141
|
-
} else if (matchesScalar(type, GraphQLBoolean)) {
|
|
1142
|
-
schemaArg[fieldEntryName] = Boolean;
|
|
1143
|
-
} else if (type instanceof GraphQLObjectType || isNonNullOfType(type, GraphQLObjectType)) {
|
|
1144
|
-
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1145
|
-
if (!fieldEntry.extensions.relation.embedded) {
|
|
1146
|
-
const key = fieldEntry.extensions.relation.connectionField || fieldEntry.name;
|
|
1147
|
-
schemaArg[key] = mongoose.Schema.Types.ObjectId;
|
|
1148
|
-
} else {
|
|
1149
|
-
const entryType = unwrapNonNull(type);
|
|
1150
|
-
if (entryType === gqlType) {
|
|
1151
|
-
throw new Error('A type cannot have a field of its same type and embedded');
|
|
1152
|
-
}
|
|
1153
|
-
schemaArg[fieldEntryName] = generateSchemaDefinition(entryType);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
} else if (type instanceof GraphQLList) {
|
|
1157
|
-
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1158
|
-
if (fieldEntry.extensions.relation.embedded) {
|
|
1159
|
-
if (type.ofType === gqlType) {
|
|
1160
|
-
throw new Error('A type cannot have a field of its same type and embedded');
|
|
1161
|
-
}
|
|
1162
|
-
schemaArg[fieldEntryName] = [generateSchemaDefinition(type.ofType)];
|
|
1163
|
-
}
|
|
1164
|
-
} else if (listItemMatchesScalar(type, GraphQLString) || type.ofType instanceof GraphQLEnumType) {
|
|
1165
|
-
schemaArg[fieldEntryName] = [String];
|
|
1166
|
-
} else if (listItemMatchesScalar(type, GraphQLBoolean)) {
|
|
1167
|
-
schemaArg[fieldEntryName] = [Boolean];
|
|
1168
|
-
} else if (listItemMatchesScalar(type, GraphQLInt) || listItemMatchesScalar(type, GraphQLFloat)) {
|
|
1169
|
-
schemaArg[fieldEntryName] = [Number];
|
|
1170
|
-
} else if (isGraphQLisoDate(getEffectiveTypeName(type.ofType))) {
|
|
1171
|
-
schemaArg[fieldEntryName] = [Date];
|
|
1172
|
-
}
|
|
1173
|
-
} else if (isGraphQLisoDate(getEffectiveTypeName(unwrapNonNull(type)))) {
|
|
1174
|
-
schemaArg[fieldEntryName] = Date;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
return schemaArg;
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
1182
|
-
const objectIdFields = [];
|
|
1183
|
-
|
|
1184
|
-
for (const [fieldName, fieldDefinition] of Object.entries(schemaDefinition)) {
|
|
1185
|
-
const currentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
1186
|
-
|
|
1187
|
-
if (fieldDefinition === mongoose.Schema.Types.ObjectId) {
|
|
1188
|
-
objectIdFields.push(currentPath);
|
|
1189
|
-
} else if (typeof fieldDefinition === 'object' && fieldDefinition !== null) {
|
|
1190
|
-
if (Array.isArray(fieldDefinition)) {
|
|
1191
|
-
const arrayElement = fieldDefinition[0];
|
|
1192
|
-
if (typeof arrayElement === 'object' && arrayElement !== null) {
|
|
1193
|
-
objectIdFields.push(...findObjectIdFields(arrayElement, currentPath));
|
|
1194
|
-
}
|
|
1195
|
-
} else if (fieldDefinition.type === mongoose.Schema.Types.ObjectId) {
|
|
1196
|
-
objectIdFields.push(currentPath);
|
|
1197
|
-
} else if (!fieldDefinition.type) {
|
|
1198
|
-
objectIdFields.push(...findObjectIdFields(fieldDefinition, currentPath));
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
return objectIdFields;
|
|
1204
|
-
};
|
|
1205
|
-
|
|
1206
|
-
const createSchemaWithIndexes = (schemaDefinition) => {
|
|
1207
|
-
const schema = new mongoose.Schema(schemaDefinition);
|
|
1208
|
-
findObjectIdFields(schemaDefinition).forEach((fieldPath) => {
|
|
1209
|
-
schema.index({ [fieldPath]: 1 });
|
|
1210
|
-
});
|
|
1211
|
-
return schema;
|
|
1212
|
-
};
|
|
1213
|
-
|
|
1214
|
-
const generateModel = (gqlType, onModelCreated, { createCollection = true } = {}) => {
|
|
1215
|
-
const schemaDefinition = generateSchemaDefinition(gqlType);
|
|
1216
|
-
const schema = createSchemaWithIndexes(schemaDefinition);
|
|
1217
|
-
const model = mongoose.model(gqlType.name, schema, gqlType.name);
|
|
1218
|
-
if (onModelCreated) {
|
|
1219
|
-
onModelCreated(model);
|
|
1220
|
-
}
|
|
1221
|
-
if (createCollection && !preventCollectionCreation) {
|
|
1222
|
-
model.createCollection();
|
|
1223
|
-
}
|
|
1224
|
-
return model;
|
|
1225
|
-
};
|
|
1226
|
-
|
|
1227
|
-
const coerceIdArray = (value) => value.map((element) => {
|
|
1228
|
-
if (element === null || element === undefined) {
|
|
1229
|
-
throw new SimfinityError('ID value cannot be null in collection filter', 'INVALID_FILTER_VALUE', 400);
|
|
1230
|
-
}
|
|
1231
|
-
return new mongoose.Types.ObjectId(element);
|
|
1232
|
-
});
|
|
1233
|
-
|
|
1234
|
-
const buildMatchesClause = (fieldname, operator, value) => {
|
|
1235
|
-
const isIdField = fieldname.endsWith('_id');
|
|
1236
|
-
const op = operator || 'EQ';
|
|
1237
|
-
|
|
1238
|
-
if (op === 'EQ') {
|
|
1239
|
-
return { [fieldname]: isIdField ? new mongoose.Types.ObjectId(value) : value };
|
|
1240
|
-
}
|
|
1241
|
-
if (op === 'IN' || op === 'NIN') {
|
|
1242
|
-
if (!Array.isArray(value)) {
|
|
1243
|
-
throw new SimfinityError(`${op} requires an array value for ${fieldname}`, 'INVALID_FILTER_VALUE', 400);
|
|
1244
|
-
}
|
|
1245
|
-
const coerced = isIdField ? coerceIdArray(value) : value;
|
|
1246
|
-
return { [fieldname]: { [op === 'IN' ? '$in' : '$nin']: coerced } };
|
|
1247
|
-
}
|
|
1248
|
-
const builder = OP_TO_MONGO[op];
|
|
1249
|
-
return builder ? { [fieldname]: builder(value) } : {};
|
|
1250
|
-
};
|
|
1251
|
-
|
|
1252
|
-
const topLevelRelationLookup = (qlField, fieldType, fieldName) => {
|
|
1253
|
-
const { collectionName } = typesDict.types[fieldType.name].model.collection;
|
|
1254
|
-
const connField = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1255
|
-
const isList = qlField.type instanceof GraphQLList;
|
|
1256
|
-
return buildRelationLookup({
|
|
1257
|
-
collectionName,
|
|
1258
|
-
localField: isList ? '_id' : connField,
|
|
1259
|
-
foreignField: isList ? connField : '_id',
|
|
1260
|
-
alias: fieldName,
|
|
1261
|
-
});
|
|
1262
|
-
};
|
|
1263
|
-
|
|
1264
|
-
const nestedRelationLookup = (pathField, pathFieldType, currentPath, aliasPath, pathFieldName) => {
|
|
1265
|
-
const { collectionName } = typesDict.types[pathFieldType.name].model.collection;
|
|
1266
|
-
const connField = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1267
|
-
const isList = pathField.type instanceof GraphQLList;
|
|
1268
|
-
return buildRelationLookup({
|
|
1269
|
-
collectionName,
|
|
1270
|
-
localField: isList ? `${currentPath}._id` : `${currentPath}.${connField}`,
|
|
1271
|
-
foreignField: isList ? connField : '_id',
|
|
1272
|
-
alias: aliasPath,
|
|
1273
|
-
});
|
|
1274
|
-
};
|
|
1275
|
-
|
|
1276
|
-
const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
1277
|
-
const aggregateClauses = {};
|
|
1278
|
-
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1279
|
-
|
|
1280
|
-
if (!(fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType))) {
|
|
1281
|
-
return aggregateClauses;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
if (!Array.isArray(filterField?.terms) || filterField.terms.length === 0) {
|
|
1285
|
-
return aggregateClauses;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1289
|
-
|
|
1290
|
-
filterField.terms.forEach((term) => {
|
|
1291
|
-
assertValidFilterPath(term.path);
|
|
1292
|
-
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1293
|
-
&& !aggregateClauses[fieldName]) {
|
|
1294
|
-
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
let currentGQLPathFieldType = unwrapListAndNonNull(qlField.type);
|
|
1298
|
-
let aliasPath = fieldName;
|
|
1299
|
-
let embeddedPath = '';
|
|
1300
|
-
|
|
1301
|
-
term.path.split('.').forEach((pathFieldName) => {
|
|
1302
|
-
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1303
|
-
if (pathField.type instanceof GraphQLObjectType
|
|
1304
|
-
|| pathField.type instanceof GraphQLList
|
|
1305
|
-
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1306
|
-
const pathFieldType = unwrapListAndNonNull(pathField.type);
|
|
1307
|
-
currentGQLPathFieldType = pathFieldType;
|
|
1308
|
-
|
|
1309
|
-
if (pathField.extensions?.relation && !pathField.extensions.relation.embedded) {
|
|
1310
|
-
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1311
|
-
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1312
|
-
embeddedPath = '';
|
|
1313
|
-
|
|
1314
|
-
if (!aggregateClauses[aliasPath]) {
|
|
1315
|
-
aggregateClauses[aliasPath] = nestedRelationLookup(
|
|
1316
|
-
pathField, pathFieldType, currentPath, aliasPath, pathFieldName,
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
} else {
|
|
1320
|
-
embeddedPath = embeddedPath === '' ? pathFieldName : `${embeddedPath}.${pathFieldName}`;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
});
|
|
1324
|
-
});
|
|
1325
|
-
return aggregateClauses;
|
|
1326
|
-
};
|
|
1327
|
-
|
|
1328
|
-
const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
1329
|
-
const aggregateClauses = {};
|
|
1330
|
-
const matchesClauses = {};
|
|
1331
|
-
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1332
|
-
|
|
1333
|
-
if (filterField == null) {
|
|
1334
|
-
return { aggregateClauses, matchesClauses };
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
if (fieldType instanceof GraphQLScalarType
|
|
1338
|
-
|| isNonNullOfType(fieldType, GraphQLScalarType)
|
|
1339
|
-
|| fieldType instanceof GraphQLEnumType
|
|
1340
|
-
|| isNonNullOfType(fieldType, GraphQLEnumType)) {
|
|
1341
|
-
coerceDateValue(fieldType, filterField, 'value');
|
|
1342
|
-
matchesClauses[fieldName] = buildMatchesClause(fieldName === 'id' ? '_id' : fieldName, filterField.operator, filterField.value);
|
|
1343
|
-
return { aggregateClauses, matchesClauses };
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
if (!(fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType))) {
|
|
1347
|
-
return { aggregateClauses, matchesClauses };
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
if (!Array.isArray(filterField.terms) || filterField.terms.length === 0) {
|
|
1351
|
-
return { aggregateClauses, matchesClauses };
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1355
|
-
|
|
1356
|
-
filterField.terms.forEach((term) => {
|
|
1357
|
-
assertValidFilterPath(term.path);
|
|
1358
|
-
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1359
|
-
&& !aggregateClauses[fieldName]) {
|
|
1360
|
-
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
if (term.path.indexOf('.') < 0) {
|
|
1364
|
-
const { type: leafType } = resolvedFieldType.getFields()[term.path];
|
|
1365
|
-
coerceDateValue(leafType, term, 'value');
|
|
1366
|
-
const leafName = resolvedFieldType.getFields()[term.path].name === 'id' ? '_id' : term.path;
|
|
1367
|
-
matchesClauses[fieldName] = buildMatchesClause(`${fieldName}.${leafName}`, term.operator, term.value);
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
let currentGQLPathFieldType = unwrapListAndNonNull(qlField.type);
|
|
1372
|
-
let aliasPath = fieldName;
|
|
1373
|
-
let embeddedPath = '';
|
|
1374
|
-
|
|
1375
|
-
term.path.split('.').forEach((pathFieldName) => {
|
|
1376
|
-
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1377
|
-
if (pathField.type instanceof GraphQLScalarType
|
|
1378
|
-
|| isNonNullOfType(pathField.type, GraphQLScalarType)) {
|
|
1379
|
-
coerceDateValue(pathField.type, term, 'value');
|
|
1380
|
-
const leafName = pathFieldName === 'id' ? '_id' : pathFieldName;
|
|
1381
|
-
const mongoPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}.` : '.') + leafName;
|
|
1382
|
-
matchesClauses[`${aliasPath}_${pathFieldName}`] = buildMatchesClause(mongoPath, term.operator, term.value);
|
|
1383
|
-
embeddedPath = '';
|
|
1384
|
-
} else if (pathField.type instanceof GraphQLObjectType
|
|
1385
|
-
|| pathField.type instanceof GraphQLList
|
|
1386
|
-
|| isNonNullOfType(pathField.type, GraphQLObjectType)) {
|
|
1387
|
-
const pathFieldType = unwrapListAndNonNull(pathField.type);
|
|
1388
|
-
currentGQLPathFieldType = pathFieldType;
|
|
1389
|
-
|
|
1390
|
-
if (pathField.extensions?.relation && !pathField.extensions.relation.embedded) {
|
|
1391
|
-
const currentPath = aliasPath + (embeddedPath !== '' ? `.${embeddedPath}` : '');
|
|
1392
|
-
aliasPath += (embeddedPath !== '' ? `_${embeddedPath}_` : '_') + pathFieldName;
|
|
1393
|
-
embeddedPath = '';
|
|
1394
|
-
|
|
1395
|
-
if (!aggregateClauses[aliasPath]) {
|
|
1396
|
-
aggregateClauses[aliasPath] = nestedRelationLookup(
|
|
1397
|
-
pathField, pathFieldType, currentPath, aliasPath, pathFieldName,
|
|
1398
|
-
);
|
|
1399
|
-
}
|
|
1400
|
-
} else {
|
|
1401
|
-
embeddedPath = embeddedPath === '' ? pathFieldName : `${embeddedPath}.${pathFieldName}`;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
});
|
|
1405
|
-
});
|
|
1406
|
-
|
|
1407
|
-
return { aggregateClauses, matchesClauses };
|
|
1408
|
-
};
|
|
1409
|
-
|
|
1410
|
-
const MAX_FILTER_GROUP_DEPTH = 5;
|
|
1411
|
-
|
|
1412
|
-
const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, aggregationsIncluded, depth = 0) => {
|
|
1413
|
-
if (depth > MAX_FILTER_GROUP_DEPTH) {
|
|
1414
|
-
throw new SimfinityError('Filter nesting too deep', 'FILTER_DEPTH_EXCEEDED', 400);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
const parts = [];
|
|
1418
|
-
const fields = gqltype.getFields();
|
|
1419
|
-
|
|
1420
|
-
if (filterGroup.conditions?.length > 0) {
|
|
1421
|
-
for (const rawCondition of filterGroup.conditions) {
|
|
1422
|
-
const condition = normalizeFilterCondition(rawCondition);
|
|
1423
|
-
const qlField = fields[condition.field];
|
|
1424
|
-
if (!qlField) {
|
|
1425
|
-
throw new SimfinityError(`Unknown filter field: ${condition.field}`, 'INVALID_FILTER_FIELD', 400);
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1429
|
-
const isObject = fieldType instanceof GraphQLObjectType || isNonNullOfType(fieldType, GraphQLObjectType);
|
|
1430
|
-
|
|
1431
|
-
let filterInput;
|
|
1432
|
-
if (isObject) {
|
|
1433
|
-
if (!condition.path) {
|
|
1434
|
-
throw new SimfinityError(`Filter on object field "${condition.field}" requires a path`, 'MISSING_FILTER_PATH', 400);
|
|
1435
|
-
}
|
|
1436
|
-
filterInput = {
|
|
1437
|
-
terms: [{ path: condition.path, operator: condition.operator, value: condition.value }],
|
|
1438
|
-
};
|
|
1439
|
-
} else {
|
|
1440
|
-
filterInput = { operator: condition.operator, value: condition.value };
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
const result = await buildQueryTerms(filterInput, qlField, condition.field);
|
|
1444
|
-
if (!result) continue;
|
|
1445
|
-
|
|
1446
|
-
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1447
|
-
if (!aggregationsIncluded[prop]) {
|
|
1448
|
-
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1449
|
-
aggregationsIncluded[prop] = true;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
1453
|
-
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1454
|
-
parts.push({ [matchKey]: match });
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
if (filterGroup.AND?.length > 0) {
|
|
1461
|
-
for (const subGroup of filterGroup.AND) {
|
|
1462
|
-
const subMatch = await buildFilterGroupMatch(subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1);
|
|
1463
|
-
if (subMatch) parts.push(subMatch);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
if (filterGroup.OR?.length > 0) {
|
|
1468
|
-
const orParts = [];
|
|
1469
|
-
for (const subGroup of filterGroup.OR) {
|
|
1470
|
-
const subMatch = await buildFilterGroupMatch(subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1);
|
|
1471
|
-
if (subMatch) orParts.push(subMatch);
|
|
1472
|
-
}
|
|
1473
|
-
if (orParts.length === 1) {
|
|
1474
|
-
parts.push(orParts[0]);
|
|
1475
|
-
} else if (orParts.length > 1) {
|
|
1476
|
-
parts.push({ $or: orParts });
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
if (parts.length === 0) return null;
|
|
1481
|
-
if (parts.length === 1) return parts[0];
|
|
1482
|
-
return { $and: parts };
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
const RESERVED_QUERY_KEYS = new Set(['pagination', 'sort', 'AND', 'OR', 'aggregation']);
|
|
1486
|
-
|
|
1487
|
-
const collectFiltersAndLookups = async (input, gqltype, aggregateClauses, aggregationsIncluded) => {
|
|
1488
|
-
const flatMatchConditions = {};
|
|
1489
|
-
let hasFlat = false;
|
|
1490
|
-
const fields = gqltype.getFields();
|
|
1491
|
-
|
|
1492
|
-
for (const [key, filterField] of Object.entries(input)) {
|
|
1493
|
-
if (RESERVED_QUERY_KEYS.has(key)) continue;
|
|
1494
|
-
const qlField = fields[key];
|
|
1495
|
-
const result = await buildQueryTerms(filterField, qlField, key);
|
|
1496
|
-
if (!result) continue;
|
|
1497
|
-
|
|
1498
|
-
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1499
|
-
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1500
|
-
aggregationsIncluded[prop] = true;
|
|
1501
|
-
}
|
|
1502
|
-
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
1503
|
-
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1504
|
-
flatMatchConditions[matchKey] = match;
|
|
1505
|
-
hasFlat = true;
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
const topLevelAndParts = [];
|
|
1511
|
-
if (hasFlat) topLevelAndParts.push(flatMatchConditions);
|
|
1512
|
-
|
|
1513
|
-
if (input.AND?.length > 0) {
|
|
1514
|
-
for (const group of input.AND) {
|
|
1515
|
-
const groupMatch = await buildFilterGroupMatch(group, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1516
|
-
if (groupMatch) topLevelAndParts.push(groupMatch);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
if (input.OR?.length > 0) {
|
|
1521
|
-
const orParts = [];
|
|
1522
|
-
for (const group of input.OR) {
|
|
1523
|
-
const groupMatch = await buildFilterGroupMatch(group, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1524
|
-
if (groupMatch) orParts.push(groupMatch);
|
|
1525
|
-
}
|
|
1526
|
-
if (orParts.length === 1) {
|
|
1527
|
-
topLevelAndParts.push(orParts[0]);
|
|
1528
|
-
} else if (orParts.length > 1) {
|
|
1529
|
-
topLevelAndParts.push({ $or: orParts });
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
if (topLevelAndParts.length === 1) return { $match: topLevelAndParts[0] };
|
|
1534
|
-
if (topLevelAndParts.length > 1) return { $match: { $and: topLevelAndParts } };
|
|
1535
|
-
return null;
|
|
1536
|
-
};
|
|
1537
|
-
|
|
1538
|
-
const buildSortClause = (sortTerms, gqltype, aggregateClauses, aggregationsIncluded) => {
|
|
1539
|
-
const sortExpressions = {};
|
|
1540
|
-
const fields = gqltype.getFields();
|
|
1541
|
-
|
|
1542
|
-
sortTerms.forEach((sort) => {
|
|
1543
|
-
assertValidFilterPath(sort.field);
|
|
1544
|
-
let fixedSortField = sort.field;
|
|
1545
|
-
if (sort.field.indexOf('.') >= 0) {
|
|
1546
|
-
const sortParts = sort.field.split('.');
|
|
1547
|
-
fixedSortField = sortParts[0];
|
|
1548
|
-
for (let i = 1; i < sortParts.length - 1; i++) {
|
|
1549
|
-
fixedSortField += `_${sortParts[i]}`;
|
|
1550
|
-
}
|
|
1551
|
-
fixedSortField += `.${sortParts[sortParts.length - 1]}`;
|
|
1552
|
-
const qlField = fields[sortParts[0]];
|
|
1553
|
-
const path = sort.field.slice(sort.field.indexOf('.') + 1);
|
|
1554
|
-
const sortAggregations = buildAggregationsForSort({ terms: [{ path }] }, qlField, sortParts[0]);
|
|
1555
|
-
for (const [prop, aggregate] of Object.entries(sortAggregations)) {
|
|
1556
|
-
if (!aggregationsIncluded[prop]) {
|
|
1557
|
-
aggregateClauses.push(aggregate.lookup, aggregate.unwind);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
sortExpressions[fixedSortField] = sort.order === 'ASC' ? 1 : -1;
|
|
1562
|
-
});
|
|
1563
|
-
|
|
1564
|
-
return { $sort: sortExpressions };
|
|
1565
|
-
};
|
|
1566
|
-
|
|
1567
|
-
const buildQuery = async (input, gqltype, isCount) => {
|
|
1568
|
-
const aggregateClauses = [];
|
|
1569
|
-
const aggregationsIncluded = {};
|
|
1570
|
-
|
|
1571
|
-
const matchStage = await collectFiltersAndLookups(input, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1572
|
-
if (matchStage) aggregateClauses.push(matchStage);
|
|
1573
|
-
|
|
1574
|
-
if (isCount) {
|
|
1575
|
-
aggregateClauses.push({ $count: 'size' });
|
|
1576
|
-
return aggregateClauses;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
if (input.sort) {
|
|
1580
|
-
aggregateClauses.push(buildSortClause(input.sort.terms, gqltype, aggregateClauses, aggregationsIncluded));
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
let skipClause = { $skip: 0 };
|
|
1584
|
-
let limitClause = { $limit: 100 };
|
|
1585
|
-
if (input.pagination?.page && input.pagination?.size) {
|
|
1586
|
-
skipClause = { $skip: input.pagination.size * (input.pagination.page - 1) };
|
|
1587
|
-
limitClause = { $limit: input.pagination.size };
|
|
1588
|
-
}
|
|
1589
|
-
aggregateClauses.push(skipClause, limitClause);
|
|
1590
|
-
|
|
1591
|
-
return aggregateClauses;
|
|
1592
|
-
};
|
|
1593
|
-
|
|
1594
|
-
const buildFieldPath = (gqltype, fieldPath) => {
|
|
1595
|
-
assertValidFilterPath(fieldPath);
|
|
1596
|
-
const pathParts = fieldPath.split('.');
|
|
1597
|
-
const lookupPairs = [];
|
|
1598
|
-
let currentPath = '';
|
|
1599
|
-
let currentGQLType = gqltype;
|
|
1600
|
-
|
|
1601
|
-
for (const part of pathParts) {
|
|
1602
|
-
const field = currentGQLType.getFields()[part];
|
|
1603
|
-
if (!field) {
|
|
1604
|
-
throw new Error(`Field ${part} not found in type ${currentGQLType.name}`);
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
const fieldType = unwrapListAndNonNull(field.type);
|
|
1608
|
-
const relation = field.extensions?.relation;
|
|
1609
|
-
|
|
1610
|
-
if (fieldType instanceof GraphQLObjectType && relation && !relation.embedded) {
|
|
1611
|
-
const { collectionName } = typesDict.types[fieldType.name].model.collection;
|
|
1612
|
-
const connField = relation.connectionField || part;
|
|
1613
|
-
const lookupAlias = currentPath ? `${currentPath}_${part}` : part;
|
|
1614
|
-
const localField = currentPath ? `${currentPath}.${connField}` : connField;
|
|
1615
|
-
|
|
1616
|
-
lookupPairs.push(buildRelationLookup({
|
|
1617
|
-
collectionName, localField, foreignField: '_id', alias: lookupAlias,
|
|
1618
|
-
}));
|
|
1619
|
-
|
|
1620
|
-
currentPath = lookupAlias;
|
|
1621
|
-
currentGQLType = fieldType;
|
|
1622
|
-
} else if (fieldType instanceof GraphQLObjectType && relation?.embedded) {
|
|
1623
|
-
currentPath = currentPath ? `${currentPath}.${part}` : part;
|
|
1624
|
-
currentGQLType = fieldType;
|
|
1625
|
-
} else {
|
|
1626
|
-
const leaf = part === 'id' ? '_id' : part;
|
|
1627
|
-
currentPath = currentPath ? `${currentPath}.${leaf}` : leaf;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
return { mongoPath: currentPath, lookupPairs };
|
|
1632
|
-
};
|
|
1633
|
-
|
|
1634
|
-
const appendLookupPairs = (aggregateClauses, aggregationsIncluded, lookupPairs) => {
|
|
1635
|
-
for (const { lookup, unwind } of lookupPairs) {
|
|
1636
|
-
const alias = lookup.$lookup.as;
|
|
1637
|
-
if (!aggregationsIncluded[alias]) {
|
|
1638
|
-
aggregateClauses.push(lookup, unwind);
|
|
1639
|
-
aggregationsIncluded[alias] = true;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
};
|
|
1643
|
-
|
|
1644
|
-
const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
1645
|
-
const aggregateClauses = [];
|
|
1646
|
-
const aggregationsIncluded = {};
|
|
1647
|
-
|
|
1648
|
-
const matchStage = await collectFiltersAndLookups(input, gqltype, aggregateClauses, aggregationsIncluded);
|
|
1649
|
-
if (matchStage) aggregateClauses.push(matchStage);
|
|
1650
|
-
|
|
1651
|
-
const { groupId, facts } = aggregationExpression;
|
|
1652
|
-
const groupIdPath = buildFieldPath(gqltype, groupId);
|
|
1653
|
-
appendLookupPairs(aggregateClauses, aggregationsIncluded, groupIdPath.lookupPairs);
|
|
1654
|
-
|
|
1655
|
-
const groupStage = { $group: { _id: `$${groupIdPath.mongoPath}` } };
|
|
1656
|
-
|
|
1657
|
-
facts.forEach((fact) => {
|
|
1658
|
-
const factPath = buildFieldPath(gqltype, fact.path);
|
|
1659
|
-
appendLookupPairs(aggregateClauses, aggregationsIncluded, factPath.lookupPairs);
|
|
1660
|
-
|
|
1661
|
-
const builder = AGG_OP_TO_MONGO[fact.operation];
|
|
1662
|
-
if (!builder) {
|
|
1663
|
-
throw new Error(`Unknown aggregation operation: ${fact.operation}`);
|
|
1664
|
-
}
|
|
1665
|
-
groupStage.$group[fact.factName] = builder(factPath.mongoPath);
|
|
1666
|
-
});
|
|
1667
|
-
|
|
1668
|
-
aggregateClauses.push(groupStage);
|
|
1669
|
-
|
|
1670
|
-
aggregateClauses.push({
|
|
1671
|
-
$project: {
|
|
1672
|
-
_id: 0,
|
|
1673
|
-
groupId: '$_id',
|
|
1674
|
-
facts: Object.fromEntries(facts.map((fact) => [fact.factName, `$${fact.factName}`])),
|
|
1675
|
-
},
|
|
1676
|
-
});
|
|
1677
|
-
|
|
1678
|
-
const sortTerms = input.sort?.terms?.length > 0 ? input.sort.terms : null;
|
|
1679
|
-
if (sortTerms) {
|
|
1680
|
-
const factNames = facts.map((fact) => fact.factName);
|
|
1681
|
-
const sortObject = {};
|
|
1682
|
-
sortTerms.forEach((sortTerm) => {
|
|
1683
|
-
const field = sortTerm.field || 'groupId';
|
|
1684
|
-
const sortFieldPath = factNames.includes(field) ? `facts.${field}` : 'groupId';
|
|
1685
|
-
sortObject[sortFieldPath] = sortTerm.order === 'ASC' ? 1 : -1;
|
|
1686
|
-
});
|
|
1687
|
-
aggregateClauses.push({ $sort: sortObject });
|
|
1688
|
-
} else {
|
|
1689
|
-
aggregateClauses.push({ $sort: { groupId: 1 } });
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
if (input.pagination?.page && input.pagination?.size) {
|
|
1693
|
-
const skip = input.pagination.size * (input.pagination.page - 1);
|
|
1694
|
-
aggregateClauses.push({ $skip: skip }, { $limit: input.pagination.size });
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
return aggregateClauses;
|
|
1698
|
-
};
|
|
1699
|
-
|
|
1700
|
-
const buildRootQuery = (name, includedTypes) => {
|
|
1701
|
-
const rootQueryArgs = {};
|
|
1702
|
-
rootQueryArgs.name = name;
|
|
1703
|
-
rootQueryArgs.fields = {};
|
|
1704
|
-
|
|
1705
|
-
for (const type of Object.values(typesDict.types)) {
|
|
1706
|
-
if (!shouldNotBeIncludedInSchema(includedTypes, type.gqltype)) {
|
|
1707
|
-
const wasAddedAsNoEnpointType = !type.simpleEntityEndpointName;
|
|
1708
|
-
if (!wasAddedAsNoEnpointType) {
|
|
1709
|
-
if (type.gqltype.getFields().id && !type.gqltype.getFields().id.resolve) {
|
|
1710
|
-
type.gqltype.getFields().id.resolve = (parent) => parent._id;
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
rootQueryArgs.fields[type.simpleEntityEndpointName] = {
|
|
1714
|
-
type: type.gqltype,
|
|
1715
|
-
args: { id: { type: GraphQLID } },
|
|
1716
|
-
async resolve(parent, args, context) {
|
|
1717
|
-
const params = {
|
|
1718
|
-
type, args, operation: 'get_by_id', context,
|
|
1719
|
-
};
|
|
1720
|
-
await executeMiddleware(params);
|
|
1721
|
-
|
|
1722
|
-
const hasScope = type.gqltype.extensions?.scope?.get_by_id;
|
|
1723
|
-
if (!hasScope) {
|
|
1724
|
-
return type.model.findById(args.id);
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
const queryArgs = { id: { operator: 'EQ', value: args.id } };
|
|
1728
|
-
await executeScope({
|
|
1729
|
-
type, args: queryArgs, operation: 'get_by_id', context,
|
|
1730
|
-
});
|
|
1731
|
-
const aggregateClauses = await buildQuery(queryArgs, type.gqltype);
|
|
1732
|
-
if (aggregateClauses.length === 0) {
|
|
1733
|
-
return type.model.findOne({ _id: args.id });
|
|
1734
|
-
}
|
|
1735
|
-
const results = await type.model.aggregate(aggregateClauses);
|
|
1736
|
-
return results[0] || null;
|
|
1737
|
-
},
|
|
1738
|
-
};
|
|
1739
|
-
|
|
1740
|
-
const argTypes = type.gqltype.getFields();
|
|
1741
|
-
|
|
1742
|
-
const argsObject = createArgsForQuery(argTypes);
|
|
1743
|
-
|
|
1744
|
-
rootQueryArgs.fields[type.listEntitiesEndpointName] = {
|
|
1745
|
-
type: new GraphQLList(type.gqltype),
|
|
1746
|
-
args: argsObject,
|
|
1747
|
-
async resolve(parent, args, context) {
|
|
1748
|
-
const params = {
|
|
1749
|
-
type,
|
|
1750
|
-
args,
|
|
1751
|
-
operation: 'find',
|
|
1752
|
-
context,
|
|
1753
|
-
};
|
|
1754
|
-
await executeMiddleware(params);
|
|
1755
|
-
await executeScope(params);
|
|
1756
|
-
const aggregateClauses = await buildQuery(args, type.gqltype);
|
|
1757
|
-
const wantsCount = !!(args.pagination && args.pagination.count);
|
|
1758
|
-
|
|
1759
|
-
const dataPromise = aggregateClauses.length === 0
|
|
1760
|
-
? type.model.find({})
|
|
1761
|
-
: type.model.aggregate(aggregateClauses);
|
|
1762
|
-
const countPromise = wantsCount
|
|
1763
|
-
? buildQuery(args, type.gqltype, true).then((p) => type.model.aggregate(p))
|
|
1764
|
-
: null;
|
|
1765
|
-
|
|
1766
|
-
const [result, resultCount] = await Promise.all([dataPromise, countPromise]);
|
|
1767
|
-
if (wantsCount) {
|
|
1768
|
-
context.count = resultCount[0] ? resultCount[0].size : 0;
|
|
1769
|
-
}
|
|
1770
|
-
return result;
|
|
1771
|
-
},
|
|
1772
|
-
};
|
|
1773
|
-
|
|
1774
|
-
const aggregateArgsObject = { ...argsObject };
|
|
1775
|
-
aggregateArgsObject.aggregation = {
|
|
1776
|
-
type: new GraphQLNonNull(QLTypeAggregationExpression),
|
|
1777
|
-
};
|
|
1778
|
-
|
|
1779
|
-
rootQueryArgs.fields[`${type.listEntitiesEndpointName}_aggregate`] = {
|
|
1780
|
-
type: new GraphQLList(QLTypeAggregationResult),
|
|
1781
|
-
args: aggregateArgsObject,
|
|
1782
|
-
async resolve(parent, args, context) {
|
|
1783
|
-
const params = {
|
|
1784
|
-
type,
|
|
1785
|
-
args,
|
|
1786
|
-
operation: 'aggregate',
|
|
1787
|
-
context,
|
|
1788
|
-
};
|
|
1789
|
-
await executeMiddleware(params);
|
|
1790
|
-
await executeScope(params);
|
|
1791
|
-
const aggregateClauses = await buildAggregationQuery(args, type.gqltype, args.aggregation);
|
|
1792
|
-
const result = await type.model.aggregate(aggregateClauses);
|
|
1793
|
-
return result;
|
|
1794
|
-
},
|
|
1795
|
-
};
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
return new GraphQLObjectType(rootQueryArgs);
|
|
1801
|
-
};
|
|
1802
|
-
|
|
1803
|
-
export const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomMutations) => {
|
|
1804
|
-
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
1805
|
-
if (typeInfo.gqltype && !typeInfo.model) {
|
|
1806
|
-
if (typeInfo.endpoint) {
|
|
1807
|
-
typeInfo.model = generateModel(typeInfo.gqltype, typeInfo.onModelCreated);
|
|
1808
|
-
} else if (typeInfo.needsModel) {
|
|
1809
|
-
typeInfo.model = generateModel(typeInfo.gqltype, null, { createCollection: false });
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
});
|
|
1813
|
-
|
|
1814
|
-
Object.keys(typesDict.types).forEach((typeName) => {
|
|
1815
|
-
if (typesDictForUpdate.types[typeName]) {
|
|
1816
|
-
typesDictForUpdate.types[typeName].model = typesDict.types[typeName].model;
|
|
1817
|
-
}
|
|
1818
|
-
});
|
|
1819
|
-
|
|
1820
|
-
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
1821
|
-
if (typeInfo.gqltype) {
|
|
1822
|
-
autoGenerateResolvers(typeInfo.gqltype);
|
|
1823
|
-
}
|
|
1824
|
-
});
|
|
1825
|
-
|
|
1826
|
-
return new GraphQLSchema({
|
|
1827
|
-
query: buildRootQuery('RootQueryType', includedQueryTypes),
|
|
1828
|
-
mutation: buildMutation('Mutation', includedMutationTypes, includedCustomMutations),
|
|
1829
|
-
});
|
|
1830
|
-
};
|
|
1831
|
-
|
|
1832
|
-
export const getModel = (gqltype) => typesDict.types[gqltype.name].model;
|
|
1833
|
-
|
|
1834
|
-
export const getType = (typeName) => {
|
|
1835
|
-
if (typeof typeName === 'string') {
|
|
1836
|
-
return typesDict.types[typeName]?.gqltype;
|
|
1837
|
-
}
|
|
1838
|
-
if (typeName && typeName.name) {
|
|
1839
|
-
return typesDict.types[typeName.name]?.gqltype;
|
|
1840
|
-
}
|
|
1841
|
-
return null;
|
|
1842
|
-
};
|
|
1843
|
-
|
|
1844
|
-
export const registerMutation = (name, description, inputModel, outputModel, callback) => {
|
|
1845
|
-
registeredMutations[name] = {
|
|
1846
|
-
description,
|
|
1847
|
-
inputModel,
|
|
1848
|
-
outputModel,
|
|
1849
|
-
callback,
|
|
1850
|
-
};
|
|
1851
|
-
};
|
|
1852
|
-
|
|
1853
|
-
const autoGenerateResolvers = (gqltype) => {
|
|
1854
|
-
const fields = gqltype.getFields();
|
|
1855
|
-
|
|
1856
|
-
for (const [fieldName, fieldEntry] of Object.entries(fields)) {
|
|
1857
|
-
if (fieldEntry.resolve) continue;
|
|
1858
|
-
const relation = fieldEntry.extensions?.relation;
|
|
1859
|
-
if (!relation || relation.embedded) continue;
|
|
1860
|
-
|
|
1861
|
-
if (fieldEntry.type instanceof GraphQLList) {
|
|
1862
|
-
const relatedType = fieldEntry.type.ofType;
|
|
1863
|
-
const connectionField = relation.connectionField || fieldName;
|
|
1864
|
-
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
1865
|
-
const argsObject = createArgsForQuery(relatedTypeInfo.gqltype.getFields());
|
|
1866
|
-
delete argsObject[connectionField];
|
|
1867
|
-
|
|
1868
|
-
fieldEntry.args = formatArgs(Object.entries(argsObject));
|
|
1869
|
-
fieldEntry.resolve = async (parent, args) => {
|
|
1870
|
-
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
1871
|
-
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
1872
|
-
}
|
|
1873
|
-
args[connectionField] = {
|
|
1874
|
-
terms: [{ path: 'id', operator: 'EQ', value: parent.id || parent._id }],
|
|
1875
|
-
};
|
|
1876
|
-
const aggregateClauses = await buildQuery(args, relatedTypeInfo.gqltype);
|
|
1877
|
-
return relatedTypeInfo.model.aggregate(aggregateClauses);
|
|
1878
|
-
};
|
|
1879
|
-
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
1880
|
-
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
|
|
1881
|
-
const relatedType = unwrapNonNull(fieldEntry.type);
|
|
1882
|
-
const connectionField = relation.connectionField || fieldName;
|
|
1883
|
-
|
|
1884
|
-
fieldEntry.resolve = async (parent) => {
|
|
1885
|
-
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
1886
|
-
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
1887
|
-
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
1888
|
-
}
|
|
1889
|
-
const relatedId = parent[connectionField] || parent[fieldName];
|
|
1890
|
-
return relatedId ? relatedTypeInfo.model.findById(relatedId) : null;
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
};
|
|
1895
|
-
|
|
1896
|
-
export const connect = (model, gqltype, simpleEntityEndpointName,
|
|
1897
|
-
listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
|
|
1898
|
-
waitingInputType[gqltype.name] = {
|
|
1899
|
-
model,
|
|
1900
|
-
gqltype,
|
|
1901
|
-
};
|
|
1902
|
-
typesDict.types[gqltype.name] = {
|
|
1903
|
-
model,
|
|
1904
|
-
gqltype,
|
|
1905
|
-
simpleEntityEndpointName,
|
|
1906
|
-
listEntitiesEndpointName,
|
|
1907
|
-
endpoint: true,
|
|
1908
|
-
controller,
|
|
1909
|
-
stateMachine,
|
|
1910
|
-
onModelCreated,
|
|
1911
|
-
};
|
|
1912
|
-
|
|
1913
|
-
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
1914
|
-
};
|
|
1915
|
-
|
|
1916
|
-
export const addNoEndpointType = (gqltype) => {
|
|
1917
|
-
waitingInputType[gqltype.name] = { gqltype };
|
|
1918
|
-
|
|
1919
|
-
const fields = gqltype.getFields();
|
|
1920
|
-
let needsModel = false;
|
|
1921
|
-
for (const fieldEntry of Object.values(fields)) {
|
|
1922
|
-
if (fieldEntry.extensions?.relation
|
|
1923
|
-
&& (fieldEntry.type instanceof GraphQLObjectType
|
|
1924
|
-
|| fieldEntry.type instanceof GraphQLList
|
|
1925
|
-
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType))) {
|
|
1926
|
-
needsModel = true;
|
|
1927
|
-
break;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
typesDict.types[gqltype.name] = {
|
|
1932
|
-
gqltype, endpoint: false, model: null, needsModel,
|
|
1933
|
-
};
|
|
1934
|
-
|
|
1935
|
-
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
30
|
+
export {
|
|
31
|
+
buildErrorFormatter,
|
|
32
|
+
createMongoAdapter,
|
|
33
|
+
createRuntime,
|
|
34
|
+
createValidatedScalar,
|
|
35
|
+
InternalServerError,
|
|
36
|
+
SimfinityError,
|
|
1936
37
|
};
|
|
1937
38
|
|
|
1938
|
-
export { createValidatedScalar };
|
|
1939
|
-
|
|
1940
39
|
export { default as validators } from './validators.js';
|
|
1941
40
|
export { default as scalars } from './scalars.js';
|
|
1942
41
|
export { default as plugins } from './plugins.js';
|
|
@@ -1949,59 +48,3 @@ export {
|
|
|
1949
48
|
startStdioMCPServer,
|
|
1950
49
|
createHTTPMCPHandler,
|
|
1951
50
|
} from './mcp.js';
|
|
1952
|
-
|
|
1953
|
-
export { buildQuery, buildFilterGroupMatch };
|
|
1954
|
-
|
|
1955
|
-
const createArgsForQuery = (argTypes) => {
|
|
1956
|
-
const argsObject = {};
|
|
1957
|
-
|
|
1958
|
-
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
1959
|
-
argsObject[fieldEntryName] = {};
|
|
1960
|
-
|
|
1961
|
-
if (fieldEntry.type instanceof GraphQLScalarType
|
|
1962
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
1963
|
-
|| fieldEntry.type instanceof GraphQLEnumType
|
|
1964
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
1965
|
-
argsObject[fieldEntryName].type = QLFilter;
|
|
1966
|
-
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
1967
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
1968
|
-
argsObject[fieldEntryName].type = QLTypeFilterExpression;
|
|
1969
|
-
} else if (fieldEntry.type instanceof GraphQLList) {
|
|
1970
|
-
const listOfType = fieldEntry.type.ofType;
|
|
1971
|
-
if (listOfType instanceof GraphQLScalarType
|
|
1972
|
-
|| isNonNullOfType(listOfType, GraphQLScalarType)
|
|
1973
|
-
|| listOfType instanceof GraphQLEnumType
|
|
1974
|
-
|| isNonNullOfType(listOfType, GraphQLEnumType)) {
|
|
1975
|
-
argsObject[fieldEntryName].type = QLFilter;
|
|
1976
|
-
} else {
|
|
1977
|
-
argsObject[fieldEntryName].type = QLTypeFilterExpression;
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
argsObject.pagination = {};
|
|
1983
|
-
argsObject.pagination.type = QLPagination;
|
|
1984
|
-
|
|
1985
|
-
argsObject.sort = {};
|
|
1986
|
-
argsObject.sort.type = QLSortExpression;
|
|
1987
|
-
|
|
1988
|
-
argsObject.AND = {};
|
|
1989
|
-
argsObject.AND.type = new GraphQLList(QLFilterGroup);
|
|
1990
|
-
|
|
1991
|
-
argsObject.OR = {};
|
|
1992
|
-
argsObject.OR.type = new GraphQLList(QLFilterGroup);
|
|
1993
|
-
|
|
1994
|
-
return argsObject;
|
|
1995
|
-
};
|
|
1996
|
-
|
|
1997
|
-
function formatArgs(argsArray) {
|
|
1998
|
-
const graphqlArgs = [];
|
|
1999
|
-
for (const [key, value] of argsArray) {
|
|
2000
|
-
const item = {
|
|
2001
|
-
name: key,
|
|
2002
|
-
type: value.type,
|
|
2003
|
-
};
|
|
2004
|
-
graphqlArgs.push(item);
|
|
2005
|
-
}
|
|
2006
|
-
return graphqlArgs;
|
|
2007
|
-
}
|