@simtlix/simfinity-js 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +765 -442
- package/eslint.config.mjs +63 -0
- package/package.json +26 -6
- package/src/index.js +242 -43
- package/tests/prevent-collection-creation.test.js +63 -0
- package/tests/validated-scalar.test.js +167 -0
- package/.eslintignore +0 -2
- package/.eslintrc.json +0 -38
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import js from "@eslint/js";
|
|
5
|
+
import { FlatCompat } from "@eslint/eslintrc";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const compat = new FlatCompat({
|
|
10
|
+
baseDirectory: __dirname,
|
|
11
|
+
recommendedConfig: js.configs.recommended,
|
|
12
|
+
allConfig: js.configs.all
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export default [
|
|
16
|
+
{
|
|
17
|
+
ignores: ["node_modules/*", "data/*", "eslint.config.mjs"],
|
|
18
|
+
},
|
|
19
|
+
...compat.extends("airbnb-base"),
|
|
20
|
+
{
|
|
21
|
+
files: ["**/*.js"],
|
|
22
|
+
languageOptions: {
|
|
23
|
+
globals: {
|
|
24
|
+
...globals.commonjs,
|
|
25
|
+
...globals.node,
|
|
26
|
+
...globals.jest,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
ecmaVersion: 2024,
|
|
30
|
+
sourceType: "commonjs",
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
rules: {
|
|
34
|
+
"default-case": "off",
|
|
35
|
+
"max-len": 0,
|
|
36
|
+
"no-console": "off",
|
|
37
|
+
"no-underscore-dangle": "off",
|
|
38
|
+
"no-await-in-loop": "off",
|
|
39
|
+
|
|
40
|
+
"no-param-reassign": ["error", {
|
|
41
|
+
props: false,
|
|
42
|
+
}],
|
|
43
|
+
|
|
44
|
+
"function-paren-newline": "off",
|
|
45
|
+
"function-call-argument-newline": "off",
|
|
46
|
+
|
|
47
|
+
"no-restricted-syntax": ["error", {
|
|
48
|
+
selector: "ForInStatement",
|
|
49
|
+
message: "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.",
|
|
50
|
+
}, {
|
|
51
|
+
selector: "LabeledStatement",
|
|
52
|
+
message: "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.",
|
|
53
|
+
}, {
|
|
54
|
+
selector: "WithStatement",
|
|
55
|
+
message: "`with` is disallowed in strict mode because it makes code impossible to predict and optimize.",
|
|
56
|
+
}],
|
|
57
|
+
|
|
58
|
+
"import/no-unresolved": ["error", {
|
|
59
|
+
ignore: ["graphql", "mongoose"],
|
|
60
|
+
}],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:watch": "jest --watch",
|
|
9
|
+
"test:coverage": "jest --coverage",
|
|
8
10
|
"lint": "eslint '**/*.js'",
|
|
9
11
|
"lint-fix": "eslint --fix '**/*.js'"
|
|
10
12
|
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.18.0"
|
|
15
|
+
},
|
|
11
16
|
"author": "Simtlix",
|
|
12
17
|
"license": "Apache-2.0",
|
|
13
18
|
"repository": {
|
|
@@ -19,10 +24,15 @@
|
|
|
19
24
|
"mongoose": "^8.15.1"
|
|
20
25
|
},
|
|
21
26
|
"devDependencies": {
|
|
22
|
-
"eslint": "^
|
|
23
|
-
"eslint
|
|
24
|
-
"eslint
|
|
25
|
-
"
|
|
27
|
+
"@eslint/compat": "^1.2.0",
|
|
28
|
+
"@eslint/eslintrc": "^3.1.0",
|
|
29
|
+
"@eslint/js": "^9.30.1",
|
|
30
|
+
"eslint": "^9.30.1",
|
|
31
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
32
|
+
"eslint-plugin-import": "^2.32.0",
|
|
33
|
+
"ghooks": "^2.0.4",
|
|
34
|
+
"globals": "^16.3.0",
|
|
35
|
+
"jest": "^29.0.0"
|
|
26
36
|
},
|
|
27
37
|
"config": {
|
|
28
38
|
"ghooks": {
|
|
@@ -33,5 +43,15 @@
|
|
|
33
43
|
"optionalDependencies": {
|
|
34
44
|
"graphql": "^14.7.0",
|
|
35
45
|
"mongoose": "^8.15.1"
|
|
46
|
+
},
|
|
47
|
+
"jest": {
|
|
48
|
+
"testEnvironment": "node",
|
|
49
|
+
"testMatch": [
|
|
50
|
+
"**/tests/**/*.test.js"
|
|
51
|
+
],
|
|
52
|
+
"collectCoverageFrom": [
|
|
53
|
+
"src/**/*.js",
|
|
54
|
+
"!src/index.js"
|
|
55
|
+
]
|
|
36
56
|
}
|
|
37
57
|
}
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,7 @@ mongoose.set('strictQuery', false);
|
|
|
12
12
|
const {
|
|
13
13
|
GraphQLObjectType, GraphQLString, GraphQLID, GraphQLSchema, GraphQLList,
|
|
14
14
|
GraphQLNonNull, GraphQLInputObjectType, GraphQLScalarType, __Field,
|
|
15
|
-
GraphQLInt, GraphQLEnumType, GraphQLBoolean, GraphQLFloat,
|
|
15
|
+
GraphQLInt, GraphQLEnumType, GraphQLBoolean, GraphQLFloat, Kind,
|
|
16
16
|
} = graphql;
|
|
17
17
|
|
|
18
18
|
// Adding 'extensions' field into instronspection query
|
|
@@ -93,6 +93,12 @@ module.exports.SimfinityError = SimfinityError;
|
|
|
93
93
|
|
|
94
94
|
module.exports.InternalServerError = InternalServerError;
|
|
95
95
|
|
|
96
|
+
let preventCollectionCreation = false;
|
|
97
|
+
|
|
98
|
+
module.exports.preventCreatingCollection = (prevent) => {
|
|
99
|
+
preventCollectionCreation = !!prevent;
|
|
100
|
+
};
|
|
101
|
+
|
|
96
102
|
/* Schema defines data on the Graph like object types(book type), relation between
|
|
97
103
|
these object types and describes how it can reach into the graph to interact with
|
|
98
104
|
the data to retrieve or mutate the data */
|
|
@@ -159,26 +165,123 @@ const isNonNullOfTypeForNotScalar = (fieldEntryType, graphQLType) => {
|
|
|
159
165
|
return isOfType;
|
|
160
166
|
};
|
|
161
167
|
|
|
168
|
+
const getEffectiveTypeName = (type) => {
|
|
169
|
+
if (type instanceof GraphQLScalarType && type.baseScalarType) {
|
|
170
|
+
return type.baseScalarType.name;
|
|
171
|
+
}
|
|
172
|
+
return type.name;
|
|
173
|
+
};
|
|
174
|
+
|
|
162
175
|
const isGraphQLisoDate = (typeName) => typeName === 'DateTime' || typeName === 'Date' || typeName === 'Time';
|
|
163
176
|
|
|
177
|
+
function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
178
|
+
if (!baseScalarType) {
|
|
179
|
+
throw new Error('baseScalarType is required');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate that baseScalarType is a valid GraphQL scalar type
|
|
183
|
+
if (!(baseScalarType instanceof GraphQLScalarType)) {
|
|
184
|
+
throw new Error('baseScalarType must be a valid GraphQL scalar type');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if it's one of the standard GraphQL scalar types
|
|
188
|
+
const validScalarTypes = [GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID];
|
|
189
|
+
const isValidStandardType = validScalarTypes.some((type) => baseScalarType === type);
|
|
190
|
+
|
|
191
|
+
if (!isValidStandardType && !baseScalarType.name) {
|
|
192
|
+
throw new Error('baseScalarType must be a standard GraphQL scalar type or a custom scalar with a valid name');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const kindMap = {
|
|
196
|
+
String: Kind.STRING,
|
|
197
|
+
Int: Kind.INT,
|
|
198
|
+
Float: Kind.FLOAT,
|
|
199
|
+
Boolean: Kind.BOOLEAN,
|
|
200
|
+
ID: Kind.STRING, // IDs are represented as strings in AST
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Try to infer the kind from the baseScalarType name
|
|
204
|
+
const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
|
|
205
|
+
|
|
206
|
+
const scalar = new GraphQLScalarType({
|
|
207
|
+
name,
|
|
208
|
+
description,
|
|
209
|
+
serialize(value) {
|
|
210
|
+
validate(value);
|
|
211
|
+
return baseScalarType.serialize(value);
|
|
212
|
+
},
|
|
213
|
+
parseValue(value) {
|
|
214
|
+
validate(value);
|
|
215
|
+
return baseScalarType.parseValue(value);
|
|
216
|
+
},
|
|
217
|
+
parseLiteral(ast, variables) {
|
|
218
|
+
if (ast.kind !== baseKind) {
|
|
219
|
+
throw new Error(`${name} must be a ${baseScalarType.name}`);
|
|
220
|
+
}
|
|
221
|
+
const value = baseScalarType.parseLiteral(ast, variables);
|
|
222
|
+
validate(value);
|
|
223
|
+
return value;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
scalar.baseScalarType = baseScalarType;
|
|
228
|
+
return scalar;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Creates a new GraphQLInputObjectType with a field excluded.
|
|
233
|
+
* @param {string} inputNamePrefix - The prefix for the input type name.
|
|
234
|
+
* @param {GraphQLInputObjectType} originalType - The original input type.
|
|
235
|
+
* @param {string} fieldToExclude - The name of the field to exclude.
|
|
236
|
+
* @returns {GraphQLInputObjectType} A new input type without the specified field.
|
|
237
|
+
*/
|
|
238
|
+
const createTypeWithExcludedField = (inputNamePrefix, originalType, fieldToExclude) => {
|
|
239
|
+
const originalFields = originalType.getFields();
|
|
240
|
+
const newFields = Object.fromEntries(
|
|
241
|
+
Object.entries(originalFields).filter(([fieldName]) => fieldName !== fieldToExclude),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return new GraphQLInputObjectType({
|
|
245
|
+
name: `${inputNamePrefix}${originalType.name}For${fieldToExclude.charAt(0).toUpperCase() + fieldToExclude.slice(1)}`,
|
|
246
|
+
fields: newFields,
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
164
250
|
const createOneToManyInputType = (inputNamePrefix, fieldEntryName,
|
|
165
|
-
inputType, updateInputType) =>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
251
|
+
inputType, updateInputType, connectionField) => {
|
|
252
|
+
let inputTypeForAdd = inputType;
|
|
253
|
+
|
|
254
|
+
// If a gqltype is provided, create a new input type for 'added'
|
|
255
|
+
// that excludes the field named after the gqltype.
|
|
256
|
+
if (connectionField) {
|
|
257
|
+
const fieldToExclude = connectionField;
|
|
258
|
+
inputTypeForAdd = createTypeWithExcludedField(inputNamePrefix, inputType, fieldToExclude);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return new GraphQLInputObjectType({
|
|
262
|
+
name: `OneToMany${inputNamePrefix}${fieldEntryName}`,
|
|
263
|
+
fields: () => ({
|
|
264
|
+
added: {
|
|
265
|
+
type: new GraphQLList(inputTypeForAdd),
|
|
266
|
+
},
|
|
267
|
+
updated: {
|
|
268
|
+
type: new GraphQLList(updateInputType),
|
|
269
|
+
},
|
|
270
|
+
deleted: {
|
|
271
|
+
type: new GraphQLList(GraphQLID),
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
});
|
|
275
|
+
};
|
|
173
276
|
|
|
174
|
-
const graphQLListInputType = (dict, fieldEntry, fieldEntryName, inputNamePrefix) => {
|
|
277
|
+
const graphQLListInputType = (dict, fieldEntry, fieldEntryName, inputNamePrefix, connectionField) => {
|
|
175
278
|
const { ofType } = fieldEntry.type;
|
|
176
279
|
|
|
177
280
|
if (ofType instanceof GraphQLObjectType && dict.types[ofType.name].inputType) {
|
|
178
281
|
if (!fieldEntry.extensions || !fieldEntry.extensions.relation
|
|
179
282
|
|| !fieldEntry.extensions.relation.embedded) {
|
|
180
283
|
const oneToMany = createOneToManyInputType(inputNamePrefix, fieldEntryName,
|
|
181
|
-
typesDict.types[ofType.name].inputType, typesDictForUpdate.types[ofType.name].inputType);
|
|
284
|
+
typesDict.types[ofType.name].inputType, typesDictForUpdate.types[ofType.name].inputType, connectionField);
|
|
182
285
|
return oneToMany;
|
|
183
286
|
}
|
|
184
287
|
if (fieldEntry.extensions && fieldEntry.extensions.relation
|
|
@@ -211,8 +314,7 @@ const buildInputType = (gqltype) => {
|
|
|
211
314
|
if (fieldEntry.type instanceof GraphQLScalarType
|
|
212
315
|
|| fieldEntry.type instanceof GraphQLEnumType
|
|
213
316
|
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
214
|
-
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)
|
|
215
|
-
) {
|
|
317
|
+
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
216
318
|
if (fieldEntryName !== 'id') {
|
|
217
319
|
fieldArg.type = fieldEntry.type;
|
|
218
320
|
}
|
|
@@ -244,8 +346,8 @@ const buildInputType = (gqltype) => {
|
|
|
244
346
|
if (fieldEntry.type.ofType === gqltype) {
|
|
245
347
|
selfReferenceCollections[fieldEntryName] = fieldEntry;
|
|
246
348
|
} else {
|
|
247
|
-
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, 'A');
|
|
248
|
-
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, 'U');
|
|
349
|
+
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, 'A', fieldEntry.extensions?.relation?.connectionField);
|
|
350
|
+
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, 'U', fieldEntry.extensions?.relation?.connectionField);
|
|
249
351
|
if (listInputTypeForAdd && listInputTypeForUpdate) {
|
|
250
352
|
fieldArg.type = listInputTypeForAdd;
|
|
251
353
|
fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
@@ -288,7 +390,7 @@ const buildInputType = (gqltype) => {
|
|
|
288
390
|
Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
|
|
289
391
|
if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
|
|
290
392
|
inputTypeForAddFields[fieldEntryName] = {
|
|
291
|
-
type: createOneToManyInputType('A', fieldEntryName, inputTypeForAdd, inputTypeForUpdate),
|
|
393
|
+
type: createOneToManyInputType('A', fieldEntryName, inputTypeForAdd, inputTypeForUpdate, selfReferenceCollections[fieldEntryName].extensions?.relation?.connectionField),
|
|
292
394
|
name: fieldEntryName,
|
|
293
395
|
};
|
|
294
396
|
}
|
|
@@ -301,7 +403,7 @@ const buildInputType = (gqltype) => {
|
|
|
301
403
|
Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
|
|
302
404
|
if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
|
|
303
405
|
inputTypeForUpdateFields[fieldEntryName] = {
|
|
304
|
-
type: createOneToManyInputType('U', fieldEntryName, inputTypeForAdd, inputTypeForUpdate),
|
|
406
|
+
type: createOneToManyInputType('U', fieldEntryName, inputTypeForAdd, inputTypeForUpdate, selfReferenceCollections[fieldEntryName].extensions?.relation?.connectionField),
|
|
305
407
|
name: fieldEntryName,
|
|
306
408
|
};
|
|
307
409
|
}
|
|
@@ -382,6 +484,8 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
382
484
|
null, operation, session)).modelArgs;
|
|
383
485
|
}
|
|
384
486
|
} else {
|
|
487
|
+
modelArgs[fieldEntry.name] = new mongoose.Types
|
|
488
|
+
.ObjectId(args[fieldEntryName].id);
|
|
385
489
|
console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
|
|
386
490
|
}
|
|
387
491
|
} else if (fieldEntry.type instanceof GraphQLList) {
|
|
@@ -462,25 +566,24 @@ const iterateonCollectionFields = async (materializedModel, gqltype, objectId, s
|
|
|
462
566
|
}
|
|
463
567
|
};
|
|
464
568
|
|
|
465
|
-
const onDeleteObject = async (Model, gqltype, controller, args, session
|
|
466
|
-
const
|
|
467
|
-
const deletedObject = new Model(result.modelArgs);
|
|
569
|
+
const onDeleteObject = async (Model, gqltype, controller, args, session) => {
|
|
570
|
+
const deletedObject = await Model.findById({ _id: args }).session(session).lean();
|
|
468
571
|
|
|
469
572
|
if (controller && controller.onDelete) {
|
|
470
573
|
await controller.onDelete(deletedObject, session);
|
|
471
574
|
}
|
|
472
575
|
|
|
473
|
-
return Model.findByIdAndDelete({ _id: args
|
|
576
|
+
return Model.findByIdAndDelete({ _id: args }).session(session);
|
|
474
577
|
};
|
|
475
578
|
|
|
476
579
|
const onDeleteSubject = async (Model, controller, id, session) => {
|
|
477
|
-
const currentObject = await Model.findById({ _id: id }).lean();
|
|
580
|
+
const currentObject = await Model.findById({ _id: id }).session(session).lean();
|
|
478
581
|
|
|
479
582
|
if (controller && controller.onDelete) {
|
|
480
583
|
await controller.onDelete(currentObject, session);
|
|
481
584
|
}
|
|
482
585
|
|
|
483
|
-
return Model.findByIdAndDelete(
|
|
586
|
+
return Model.findByIdAndDelete({ _id: id }).session(session);
|
|
484
587
|
};
|
|
485
588
|
|
|
486
589
|
const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent) => {
|
|
@@ -805,10 +908,18 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
805
908
|
const schemaArg = {};
|
|
806
909
|
|
|
807
910
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
911
|
+
// Helper function to get the base scalar type for custom validated scalars
|
|
912
|
+
const getBaseScalarType = (scalarType) => scalarType.baseScalarType || scalarType;
|
|
913
|
+
|
|
914
|
+
// Helper function to check if a type is a custom validated scalar
|
|
915
|
+
const isCustomValidatedScalar = (type) => type instanceof GraphQLScalarType && type.baseScalarType;
|
|
916
|
+
|
|
808
917
|
if (fieldEntry.type === GraphQLID || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLID)) {
|
|
809
918
|
schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
|
|
810
919
|
} else if (fieldEntry.type === GraphQLString
|
|
811
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLString)
|
|
920
|
+
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLString)
|
|
921
|
+
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLString)
|
|
922
|
+
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
|
|
812
923
|
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
813
924
|
schemaArg[fieldEntryName] = { type: String, unique: true };
|
|
814
925
|
} else {
|
|
@@ -822,27 +933,33 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
822
933
|
schemaArg[fieldEntryName] = String;
|
|
823
934
|
}
|
|
824
935
|
} else if (fieldEntry.type === GraphQLInt
|
|
825
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLInt)
|
|
936
|
+
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLInt)
|
|
937
|
+
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLInt)
|
|
938
|
+
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt)) {
|
|
826
939
|
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
827
940
|
schemaArg[fieldEntryName] = { type: Number, unique: true };
|
|
828
941
|
} else {
|
|
829
942
|
schemaArg[fieldEntryName] = Number;
|
|
830
943
|
}
|
|
831
944
|
} else if (fieldEntry.type === GraphQLFloat
|
|
832
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLFloat)
|
|
945
|
+
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLFloat)
|
|
946
|
+
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLFloat)
|
|
947
|
+
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat)) {
|
|
833
948
|
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
834
949
|
schemaArg[fieldEntryName] = { type: Number, unique: true };
|
|
835
950
|
} else {
|
|
836
951
|
schemaArg[fieldEntryName] = Number;
|
|
837
952
|
}
|
|
838
953
|
} else if (fieldEntry.type === GraphQLBoolean
|
|
839
|
-
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLBoolean)
|
|
954
|
+
|| isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLBoolean)
|
|
955
|
+
|| (isCustomValidatedScalar(fieldEntry.type) && getBaseScalarType(fieldEntry.type) === GraphQLBoolean)
|
|
956
|
+
|| (isNonNullOfType(fieldEntry.type, GraphQLScalarType) && isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
|
|
840
957
|
schemaArg[fieldEntryName] = Boolean;
|
|
841
958
|
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
842
959
|
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
843
960
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
844
961
|
if (!fieldEntry.extensions.relation.embedded) {
|
|
845
|
-
schemaArg[fieldEntry.extensions.relation.connectionField] = mongoose
|
|
962
|
+
schemaArg[fieldEntry.extensions.relation.connectionField ? fieldEntry.extensions.relation.connectionField : fieldEntry.name] = mongoose
|
|
846
963
|
.Schema.Types.ObjectId;
|
|
847
964
|
} else {
|
|
848
965
|
let entryType = fieldEntry.type;
|
|
@@ -867,17 +984,20 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
867
984
|
}
|
|
868
985
|
}
|
|
869
986
|
} else if (fieldEntry.type.ofType === GraphQLString
|
|
870
|
-
|| fieldEntry.type.ofType instanceof GraphQLEnumType
|
|
987
|
+
|| fieldEntry.type.ofType instanceof GraphQLEnumType
|
|
988
|
+
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
|
|
871
989
|
schemaArg[fieldEntryName] = [String];
|
|
872
|
-
} else if (fieldEntry.type.ofType === GraphQLBoolean
|
|
990
|
+
} else if (fieldEntry.type.ofType === GraphQLBoolean
|
|
991
|
+
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
|
|
873
992
|
schemaArg[fieldEntryName] = [Boolean];
|
|
874
|
-
} else if (fieldEntry.type.ofType === GraphQLInt || fieldEntry.type.ofType === GraphQLFloat
|
|
993
|
+
} else if (fieldEntry.type.ofType === GraphQLInt || fieldEntry.type.ofType === GraphQLFloat
|
|
994
|
+
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && (getBaseScalarType(fieldEntry.type.ofType) === GraphQLInt || getBaseScalarType(fieldEntry.type.ofType) === GraphQLFloat))) {
|
|
875
995
|
schemaArg[fieldEntryName] = [Number];
|
|
876
|
-
} else if (isGraphQLisoDate(fieldEntry.type.ofType
|
|
996
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType))) {
|
|
877
997
|
schemaArg[fieldEntryName] = [Date];
|
|
878
998
|
}
|
|
879
|
-
} else if (isGraphQLisoDate(fieldEntry.type
|
|
880
|
-
|| (fieldEntry.type instanceof GraphQLNonNull && isGraphQLisoDate(fieldEntry.type.ofType
|
|
999
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type))
|
|
1000
|
+
|| (fieldEntry.type instanceof GraphQLNonNull && isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType)))) {
|
|
881
1001
|
schemaArg[fieldEntryName] = Date;
|
|
882
1002
|
}
|
|
883
1003
|
}
|
|
@@ -890,7 +1010,18 @@ const generateModel = (gqlType, onModelCreated) => {
|
|
|
890
1010
|
if (onModelCreated) {
|
|
891
1011
|
onModelCreated(model);
|
|
892
1012
|
}
|
|
893
|
-
|
|
1013
|
+
if (!preventCollectionCreation) {
|
|
1014
|
+
model.createCollection();
|
|
1015
|
+
}
|
|
1016
|
+
return model;
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
const generateModelWithoutCollection = (gqlType, onModelCreated) => {
|
|
1020
|
+
const model = mongoose.model(gqlType.name, generateSchemaDefinition(gqlType), gqlType.name);
|
|
1021
|
+
if (onModelCreated) {
|
|
1022
|
+
onModelCreated(model);
|
|
1023
|
+
}
|
|
1024
|
+
// Never create collection for no-endpoint types
|
|
894
1025
|
return model;
|
|
895
1026
|
};
|
|
896
1027
|
|
|
@@ -951,13 +1082,12 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
|
951
1082
|
if (fieldType instanceof GraphQLNonNull) {
|
|
952
1083
|
fieldType = qlField.type.ofType;
|
|
953
1084
|
}
|
|
954
|
-
|
|
955
1085
|
filterField.terms.forEach((term) => {
|
|
956
1086
|
if (qlField.extensions && qlField.extensions.relation
|
|
957
1087
|
&& !qlField.extensions.relation.embedded) {
|
|
958
1088
|
const { model } = typesDict.types[fieldType.name];
|
|
959
1089
|
const { collectionName } = model.collection;
|
|
960
|
-
const localFieldName = qlField.extensions
|
|
1090
|
+
const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
|
|
961
1091
|
if (!aggregateClauses[fieldName]) {
|
|
962
1092
|
let lookup = {};
|
|
963
1093
|
|
|
@@ -1015,7 +1145,7 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
|
1015
1145
|
|
|
1016
1146
|
const pathModel = typesDict.types[pathFieldType.name].model;
|
|
1017
1147
|
const fieldPathCollectionName = pathModel.collection.collectionName;
|
|
1018
|
-
const pathLocalFieldName = pathField.extensions
|
|
1148
|
+
const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1019
1149
|
|
|
1020
1150
|
if (!aggregateClauses[aliasPath]) {
|
|
1021
1151
|
let lookup = {};
|
|
@@ -1068,7 +1198,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1068
1198
|
|| isNonNullOfType(fieldType, GraphQLScalarType)
|
|
1069
1199
|
|| fieldType instanceof GraphQLEnumType
|
|
1070
1200
|
|| isNonNullOfType(fieldType, GraphQLEnumType)) {
|
|
1071
|
-
const fieldTypeName = fieldType instanceof GraphQLNonNull ? fieldType.ofType
|
|
1201
|
+
const fieldTypeName = fieldType instanceof GraphQLNonNull ? getEffectiveTypeName(fieldType.ofType) : getEffectiveTypeName(fieldType);
|
|
1072
1202
|
if (isGraphQLisoDate(fieldTypeName)) {
|
|
1073
1203
|
if (Array.isArray(filterField.value)) {
|
|
1074
1204
|
filterField.value = filterField.value.map((value) => value && new Date(value));
|
|
@@ -1088,7 +1218,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1088
1218
|
&& !qlField.extensions.relation.embedded) {
|
|
1089
1219
|
const { model } = typesDict.types[fieldType.name];
|
|
1090
1220
|
const { collectionName } = model.collection;
|
|
1091
|
-
const localFieldName = qlField.extensions
|
|
1221
|
+
const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1092
1222
|
if (!aggregateClauses[fieldName]) {
|
|
1093
1223
|
let lookup = {};
|
|
1094
1224
|
|
|
@@ -1121,7 +1251,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1121
1251
|
|
|
1122
1252
|
if (term.path.indexOf('.') < 0) {
|
|
1123
1253
|
const { type } = fieldType.getFields()[term.path];
|
|
1124
|
-
const typeName = type instanceof GraphQLNonNull ? type.ofType
|
|
1254
|
+
const typeName = type instanceof GraphQLNonNull ? getEffectiveTypeName(type.ofType) : getEffectiveTypeName(type);
|
|
1125
1255
|
if (isGraphQLisoDate(typeName)) {
|
|
1126
1256
|
if (Array.isArray(term.value)) {
|
|
1127
1257
|
term.value = term.value.map((value) => value && new Date(value));
|
|
@@ -1143,7 +1273,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1143
1273
|
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1144
1274
|
if (pathField.type instanceof GraphQLScalarType
|
|
1145
1275
|
|| isNonNullOfType(pathField.type, GraphQLScalarType)) {
|
|
1146
|
-
const typeName = pathField.type instanceof GraphQLNonNull ? pathField.type.ofType
|
|
1276
|
+
const typeName = pathField.type instanceof GraphQLNonNull ? getEffectiveTypeName(pathField.type.ofType) : getEffectiveTypeName(pathField.type);
|
|
1147
1277
|
if (isGraphQLisoDate(typeName)) {
|
|
1148
1278
|
if (Array.isArray(term.value)) {
|
|
1149
1279
|
term.value = term.value.map((value) => value && new Date(value));
|
|
@@ -1170,7 +1300,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1170
1300
|
|
|
1171
1301
|
const pathModel = typesDict.types[pathFieldType.name].model;
|
|
1172
1302
|
const fieldPathCollectionName = pathModel.collection.collectionName;
|
|
1173
|
-
const pathLocalFieldName = pathField.extensions
|
|
1303
|
+
const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1174
1304
|
|
|
1175
1305
|
if (!aggregateClauses[aliasPath]) {
|
|
1176
1306
|
let lookup = {};
|
|
@@ -1423,6 +1553,52 @@ module.exports.registerMutation = (name, description, inputModel, outputModel, c
|
|
|
1423
1553
|
};
|
|
1424
1554
|
};
|
|
1425
1555
|
|
|
1556
|
+
const autoGenerateResolvers = (gqltype) => {
|
|
1557
|
+
const fields = gqltype.getFields();
|
|
1558
|
+
|
|
1559
|
+
for (const [fieldName, fieldEntry] of Object.entries(fields)) {
|
|
1560
|
+
// Skip if resolve method already exists
|
|
1561
|
+
if (!fieldEntry.resolve) {
|
|
1562
|
+
// Check if field has relation extension
|
|
1563
|
+
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
1564
|
+
const { relation } = fieldEntry.extensions;
|
|
1565
|
+
|
|
1566
|
+
if (fieldEntry.type instanceof GraphQLList) {
|
|
1567
|
+
// Collection field - generate resolve for one-to-many relationship
|
|
1568
|
+
const relatedType = fieldEntry.type.ofType;
|
|
1569
|
+
const connectionField = relation.connectionField || fieldName;
|
|
1570
|
+
|
|
1571
|
+
fieldEntry.resolve = (parent) => {
|
|
1572
|
+
// Lazy lookup of the related model
|
|
1573
|
+
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
1574
|
+
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
1575
|
+
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
1576
|
+
}
|
|
1577
|
+
const query = {};
|
|
1578
|
+
query[connectionField] = parent.id || parent._id;
|
|
1579
|
+
return relatedTypeInfo.model.find(query);
|
|
1580
|
+
};
|
|
1581
|
+
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
1582
|
+
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
|
|
1583
|
+
// Single object field - generate resolve for one-to-one relationship
|
|
1584
|
+
const relatedType = fieldEntry.type instanceof GraphQLNonNull ? fieldEntry.type.ofType : fieldEntry.type;
|
|
1585
|
+
const connectionField = relation.connectionField || fieldName;
|
|
1586
|
+
|
|
1587
|
+
fieldEntry.resolve = (parent) => {
|
|
1588
|
+
// Lazy lookup of the related model
|
|
1589
|
+
const relatedTypeInfo = typesDict.types[relatedType.name];
|
|
1590
|
+
if (!relatedTypeInfo || !relatedTypeInfo.model) {
|
|
1591
|
+
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
|
|
1592
|
+
}
|
|
1593
|
+
const relatedId = parent[connectionField] || parent[fieldName];
|
|
1594
|
+
return relatedId ? relatedTypeInfo.model.findById(relatedId) : null;
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1426
1602
|
module.exports.connect = (model, gqltype, simpleEntityEndpointName,
|
|
1427
1603
|
listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
|
|
1428
1604
|
waitingInputType[gqltype.name] = {
|
|
@@ -1440,6 +1616,9 @@ module.exports.connect = (model, gqltype, simpleEntityEndpointName,
|
|
|
1440
1616
|
};
|
|
1441
1617
|
|
|
1442
1618
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
1619
|
+
|
|
1620
|
+
// Auto-generate resolve methods for relationship fields if not already defined
|
|
1621
|
+
autoGenerateResolvers(gqltype);
|
|
1443
1622
|
};
|
|
1444
1623
|
|
|
1445
1624
|
module.exports.addNoEndpointType = (gqltype) => {
|
|
@@ -1447,10 +1626,30 @@ module.exports.addNoEndpointType = (gqltype) => {
|
|
|
1447
1626
|
gqltype,
|
|
1448
1627
|
};
|
|
1449
1628
|
|
|
1629
|
+
// Check if this type has relationship fields that might need a model
|
|
1630
|
+
const fields = gqltype.getFields();
|
|
1631
|
+
let needsModel = false;
|
|
1632
|
+
|
|
1633
|
+
for (const [, fieldEntry] of Object.entries(fields)) {
|
|
1634
|
+
if (fieldEntry.extensions && fieldEntry.extensions.relation
|
|
1635
|
+
&& (fieldEntry.type instanceof GraphQLObjectType || fieldEntry.type instanceof GraphQLList
|
|
1636
|
+
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType))) {
|
|
1637
|
+
needsModel = true;
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1450
1642
|
typesDict.types[gqltype.name] = {
|
|
1451
1643
|
gqltype,
|
|
1452
1644
|
endpoint: false,
|
|
1645
|
+
// Generate model if needed for relationships, but don't create collection
|
|
1646
|
+
model: needsModel ? generateModelWithoutCollection(gqltype, null) : null,
|
|
1453
1647
|
};
|
|
1454
1648
|
|
|
1455
1649
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
1650
|
+
|
|
1651
|
+
// Auto-generate resolve methods for relationship fields if not already defined
|
|
1652
|
+
autoGenerateResolvers(gqltype);
|
|
1456
1653
|
};
|
|
1654
|
+
|
|
1655
|
+
module.exports.createValidatedScalar = createValidatedScalar;
|