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