@simtlix/simfinity-js 1.1.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/.github/workflows/master.yml +1 -1
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/release.yml +1 -1
- package/README.md +966 -72
- package/eslint.config.mjs +63 -0
- package/package.json +31 -7
- package/src/index.js +263 -52
- 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 -37
package/src/index.js
CHANGED
|
@@ -7,12 +7,12 @@ const QLOperator = require('./const/QLOperator');
|
|
|
7
7
|
const QLValue = require('./const/QLValue');
|
|
8
8
|
const QLSort = require('./const/QLSort');
|
|
9
9
|
|
|
10
|
-
mongoose.set('
|
|
10
|
+
mongoose.set('strictQuery', false);
|
|
11
11
|
|
|
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
|
+
}
|
|
173
260
|
|
|
174
|
-
|
|
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
|
+
};
|
|
276
|
+
|
|
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,15 +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(args
|
|
576
|
+
return Model.findByIdAndDelete({ _id: args }).session(session);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const onDeleteSubject = async (Model, controller, id, session) => {
|
|
580
|
+
const currentObject = await Model.findById({ _id: id }).session(session).lean();
|
|
581
|
+
|
|
582
|
+
if (controller && controller.onDelete) {
|
|
583
|
+
await controller.onDelete(currentObject, session);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return Model.findByIdAndDelete({ _id: id }).session(session);
|
|
474
587
|
};
|
|
475
588
|
|
|
476
589
|
const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent) => {
|
|
@@ -481,7 +594,6 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
481
594
|
await iterateonCollectionFields(materializedModel, gqltype, objectId, session);
|
|
482
595
|
}
|
|
483
596
|
|
|
484
|
-
let modifiedObject = materializedModel.modelArgs;
|
|
485
597
|
const currentObject = await Model.findById({ _id: objectId }).lean();
|
|
486
598
|
|
|
487
599
|
const argTypes = gqltype.getFields();
|
|
@@ -490,32 +602,32 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
490
602
|
if (fieldEntry.extensions && fieldEntry.extensions.relation
|
|
491
603
|
&& fieldEntry.extensions.relation.embedded) {
|
|
492
604
|
const oldObjectData = currentObject[fieldEntryName];
|
|
493
|
-
const newObjectData =
|
|
605
|
+
const newObjectData = materializedModel.modelArgs[fieldEntryName];
|
|
494
606
|
if (newObjectData) {
|
|
495
607
|
if (Array.isArray(oldObjectData) && Array.isArray(newObjectData)) {
|
|
496
|
-
|
|
608
|
+
materializedModel.modelArgs[fieldEntryName] = newObjectData;
|
|
497
609
|
} else {
|
|
498
|
-
|
|
610
|
+
materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
|
|
499
611
|
}
|
|
500
612
|
}
|
|
501
613
|
}
|
|
502
614
|
|
|
503
615
|
if (args[fieldEntryName] === null
|
|
504
616
|
&& !(fieldEntry.type instanceof GraphQLNonNull)) {
|
|
505
|
-
|
|
617
|
+
materializedModel.modelArgs = { ...materializedModel.modelArgs, $unset: { [fieldEntryName]: '' } };
|
|
506
618
|
}
|
|
507
619
|
});
|
|
508
620
|
|
|
509
621
|
if (controller && controller.onUpdating) {
|
|
510
|
-
await controller.onUpdating(objectId,
|
|
622
|
+
await controller.onUpdating(objectId, materializedModel.modelArgs, session);
|
|
511
623
|
}
|
|
512
624
|
|
|
513
625
|
const result = Model.findByIdAndUpdate(
|
|
514
|
-
objectId,
|
|
515
|
-
);
|
|
626
|
+
objectId, materializedModel.modelArgs, { new: true },
|
|
627
|
+
).session(session);
|
|
516
628
|
|
|
517
629
|
if (controller && controller.onUpdated) {
|
|
518
|
-
await controller.onUpdated(result,
|
|
630
|
+
await controller.onUpdated(result, session);
|
|
519
631
|
}
|
|
520
632
|
|
|
521
633
|
return result;
|
|
@@ -633,7 +745,10 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
633
745
|
};
|
|
634
746
|
break;
|
|
635
747
|
case operations.DELETE:
|
|
636
|
-
|
|
748
|
+
operationFunction = async (collectionItem) => {
|
|
749
|
+
await onDeleteSubject(typesDict.types[collectionGQLType.name].model,
|
|
750
|
+
typesDict.types[collectionGQLType.name].controller, collectionItem, session);
|
|
751
|
+
};
|
|
637
752
|
}
|
|
638
753
|
|
|
639
754
|
for (const element of collectionFieldsList) {
|
|
@@ -793,10 +908,18 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
793
908
|
const schemaArg = {};
|
|
794
909
|
|
|
795
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
|
+
|
|
796
917
|
if (fieldEntry.type === GraphQLID || isNonNullOfTypeForNotScalar(fieldEntry.type, GraphQLID)) {
|
|
797
918
|
schemaArg[fieldEntryName] = mongoose.Schema.Types.ObjectId;
|
|
798
919
|
} else if (fieldEntry.type === GraphQLString
|
|
799
|
-
|| 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)) {
|
|
800
923
|
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
801
924
|
schemaArg[fieldEntryName] = { type: String, unique: true };
|
|
802
925
|
} else {
|
|
@@ -810,27 +933,33 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
810
933
|
schemaArg[fieldEntryName] = String;
|
|
811
934
|
}
|
|
812
935
|
} else if (fieldEntry.type === GraphQLInt
|
|
813
|
-
|| 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)) {
|
|
814
939
|
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
815
940
|
schemaArg[fieldEntryName] = { type: Number, unique: true };
|
|
816
941
|
} else {
|
|
817
942
|
schemaArg[fieldEntryName] = Number;
|
|
818
943
|
}
|
|
819
944
|
} else if (fieldEntry.type === GraphQLFloat
|
|
820
|
-
|| 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)) {
|
|
821
948
|
if (fieldEntry.extensions && fieldEntry.extensions.unique) {
|
|
822
949
|
schemaArg[fieldEntryName] = { type: Number, unique: true };
|
|
823
950
|
} else {
|
|
824
951
|
schemaArg[fieldEntryName] = Number;
|
|
825
952
|
}
|
|
826
953
|
} else if (fieldEntry.type === GraphQLBoolean
|
|
827
|
-
|| 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)) {
|
|
828
957
|
schemaArg[fieldEntryName] = Boolean;
|
|
829
958
|
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
830
959
|
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
831
960
|
if (fieldEntry.extensions && fieldEntry.extensions.relation) {
|
|
832
961
|
if (!fieldEntry.extensions.relation.embedded) {
|
|
833
|
-
schemaArg[fieldEntry.extensions.relation.connectionField] = mongoose
|
|
962
|
+
schemaArg[fieldEntry.extensions.relation.connectionField ? fieldEntry.extensions.relation.connectionField : fieldEntry.name] = mongoose
|
|
834
963
|
.Schema.Types.ObjectId;
|
|
835
964
|
} else {
|
|
836
965
|
let entryType = fieldEntry.type;
|
|
@@ -855,17 +984,20 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
855
984
|
}
|
|
856
985
|
}
|
|
857
986
|
} else if (fieldEntry.type.ofType === GraphQLString
|
|
858
|
-
|| fieldEntry.type.ofType instanceof GraphQLEnumType
|
|
987
|
+
|| fieldEntry.type.ofType instanceof GraphQLEnumType
|
|
988
|
+
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLString)) {
|
|
859
989
|
schemaArg[fieldEntryName] = [String];
|
|
860
|
-
} else if (fieldEntry.type.ofType === GraphQLBoolean
|
|
990
|
+
} else if (fieldEntry.type.ofType === GraphQLBoolean
|
|
991
|
+
|| (isCustomValidatedScalar(fieldEntry.type.ofType) && getBaseScalarType(fieldEntry.type.ofType) === GraphQLBoolean)) {
|
|
861
992
|
schemaArg[fieldEntryName] = [Boolean];
|
|
862
|
-
} 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))) {
|
|
863
995
|
schemaArg[fieldEntryName] = [Number];
|
|
864
|
-
} else if (isGraphQLisoDate(fieldEntry.type.ofType
|
|
996
|
+
} else if (isGraphQLisoDate(getEffectiveTypeName(fieldEntry.type.ofType))) {
|
|
865
997
|
schemaArg[fieldEntryName] = [Date];
|
|
866
998
|
}
|
|
867
|
-
} else if (isGraphQLisoDate(fieldEntry.type
|
|
868
|
-
|| (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)))) {
|
|
869
1001
|
schemaArg[fieldEntryName] = Date;
|
|
870
1002
|
}
|
|
871
1003
|
}
|
|
@@ -878,7 +1010,18 @@ const generateModel = (gqlType, onModelCreated) => {
|
|
|
878
1010
|
if (onModelCreated) {
|
|
879
1011
|
onModelCreated(model);
|
|
880
1012
|
}
|
|
881
|
-
|
|
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
|
|
882
1025
|
return model;
|
|
883
1026
|
};
|
|
884
1027
|
|
|
@@ -939,13 +1082,12 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
|
939
1082
|
if (fieldType instanceof GraphQLNonNull) {
|
|
940
1083
|
fieldType = qlField.type.ofType;
|
|
941
1084
|
}
|
|
942
|
-
|
|
943
1085
|
filterField.terms.forEach((term) => {
|
|
944
1086
|
if (qlField.extensions && qlField.extensions.relation
|
|
945
1087
|
&& !qlField.extensions.relation.embedded) {
|
|
946
1088
|
const { model } = typesDict.types[fieldType.name];
|
|
947
1089
|
const { collectionName } = model.collection;
|
|
948
|
-
const localFieldName = qlField.extensions
|
|
1090
|
+
const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
|
|
949
1091
|
if (!aggregateClauses[fieldName]) {
|
|
950
1092
|
let lookup = {};
|
|
951
1093
|
|
|
@@ -1003,7 +1145,7 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
|
1003
1145
|
|
|
1004
1146
|
const pathModel = typesDict.types[pathFieldType.name].model;
|
|
1005
1147
|
const fieldPathCollectionName = pathModel.collection.collectionName;
|
|
1006
|
-
const pathLocalFieldName = pathField.extensions
|
|
1148
|
+
const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1007
1149
|
|
|
1008
1150
|
if (!aggregateClauses[aliasPath]) {
|
|
1009
1151
|
let lookup = {};
|
|
@@ -1056,7 +1198,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1056
1198
|
|| isNonNullOfType(fieldType, GraphQLScalarType)
|
|
1057
1199
|
|| fieldType instanceof GraphQLEnumType
|
|
1058
1200
|
|| isNonNullOfType(fieldType, GraphQLEnumType)) {
|
|
1059
|
-
const fieldTypeName = fieldType instanceof GraphQLNonNull ? fieldType.ofType
|
|
1201
|
+
const fieldTypeName = fieldType instanceof GraphQLNonNull ? getEffectiveTypeName(fieldType.ofType) : getEffectiveTypeName(fieldType);
|
|
1060
1202
|
if (isGraphQLisoDate(fieldTypeName)) {
|
|
1061
1203
|
if (Array.isArray(filterField.value)) {
|
|
1062
1204
|
filterField.value = filterField.value.map((value) => value && new Date(value));
|
|
@@ -1076,7 +1218,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1076
1218
|
&& !qlField.extensions.relation.embedded) {
|
|
1077
1219
|
const { model } = typesDict.types[fieldType.name];
|
|
1078
1220
|
const { collectionName } = model.collection;
|
|
1079
|
-
const localFieldName = qlField.extensions
|
|
1221
|
+
const localFieldName = qlField.extensions?.relation?.connectionField || fieldName;
|
|
1080
1222
|
if (!aggregateClauses[fieldName]) {
|
|
1081
1223
|
let lookup = {};
|
|
1082
1224
|
|
|
@@ -1109,7 +1251,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1109
1251
|
|
|
1110
1252
|
if (term.path.indexOf('.') < 0) {
|
|
1111
1253
|
const { type } = fieldType.getFields()[term.path];
|
|
1112
|
-
const typeName = type instanceof GraphQLNonNull ? type.ofType
|
|
1254
|
+
const typeName = type instanceof GraphQLNonNull ? getEffectiveTypeName(type.ofType) : getEffectiveTypeName(type);
|
|
1113
1255
|
if (isGraphQLisoDate(typeName)) {
|
|
1114
1256
|
if (Array.isArray(term.value)) {
|
|
1115
1257
|
term.value = term.value.map((value) => value && new Date(value));
|
|
@@ -1131,7 +1273,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1131
1273
|
const pathField = currentGQLPathFieldType.getFields()[pathFieldName];
|
|
1132
1274
|
if (pathField.type instanceof GraphQLScalarType
|
|
1133
1275
|
|| isNonNullOfType(pathField.type, GraphQLScalarType)) {
|
|
1134
|
-
const typeName = pathField.type instanceof GraphQLNonNull ? pathField.type.ofType
|
|
1276
|
+
const typeName = pathField.type instanceof GraphQLNonNull ? getEffectiveTypeName(pathField.type.ofType) : getEffectiveTypeName(pathField.type);
|
|
1135
1277
|
if (isGraphQLisoDate(typeName)) {
|
|
1136
1278
|
if (Array.isArray(term.value)) {
|
|
1137
1279
|
term.value = term.value.map((value) => value && new Date(value));
|
|
@@ -1158,7 +1300,7 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1158
1300
|
|
|
1159
1301
|
const pathModel = typesDict.types[pathFieldType.name].model;
|
|
1160
1302
|
const fieldPathCollectionName = pathModel.collection.collectionName;
|
|
1161
|
-
const pathLocalFieldName = pathField.extensions
|
|
1303
|
+
const pathLocalFieldName = pathField.extensions?.relation?.connectionField || pathFieldName;
|
|
1162
1304
|
|
|
1163
1305
|
if (!aggregateClauses[aliasPath]) {
|
|
1164
1306
|
let lookup = {};
|
|
@@ -1411,6 +1553,52 @@ module.exports.registerMutation = (name, description, inputModel, outputModel, c
|
|
|
1411
1553
|
};
|
|
1412
1554
|
};
|
|
1413
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
|
+
|
|
1414
1602
|
module.exports.connect = (model, gqltype, simpleEntityEndpointName,
|
|
1415
1603
|
listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
|
|
1416
1604
|
waitingInputType[gqltype.name] = {
|
|
@@ -1428,6 +1616,9 @@ module.exports.connect = (model, gqltype, simpleEntityEndpointName,
|
|
|
1428
1616
|
};
|
|
1429
1617
|
|
|
1430
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);
|
|
1431
1622
|
};
|
|
1432
1623
|
|
|
1433
1624
|
module.exports.addNoEndpointType = (gqltype) => {
|
|
@@ -1435,10 +1626,30 @@ module.exports.addNoEndpointType = (gqltype) => {
|
|
|
1435
1626
|
gqltype,
|
|
1436
1627
|
};
|
|
1437
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
|
+
|
|
1438
1642
|
typesDict.types[gqltype.name] = {
|
|
1439
1643
|
gqltype,
|
|
1440
1644
|
endpoint: false,
|
|
1645
|
+
// Generate model if needed for relationships, but don't create collection
|
|
1646
|
+
model: needsModel ? generateModelWithoutCollection(gqltype, null) : null,
|
|
1441
1647
|
};
|
|
1442
1648
|
|
|
1443
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);
|
|
1444
1653
|
};
|
|
1654
|
+
|
|
1655
|
+
module.exports.createValidatedScalar = createValidatedScalar;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
const { GraphQLObjectType, GraphQLString, GraphQLID } = require('graphql');
|
|
3
|
+
const simfinity = require('../src/index');
|
|
4
|
+
|
|
5
|
+
describe('preventCreatingCollection option', () => {
|
|
6
|
+
let createCollectionSpy;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Reset modules to have a clean state for each test
|
|
10
|
+
jest.resetModules();
|
|
11
|
+
// Spy on the createCollection method of the mongoose model prototype
|
|
12
|
+
createCollectionSpy = jest.spyOn(mongoose.Model, 'createCollection').mockImplementation(() => Promise.resolve());
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
// Restore the original implementation
|
|
17
|
+
createCollectionSpy.mockRestore();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should create collection by default', () => {
|
|
21
|
+
const TestType = new GraphQLObjectType({
|
|
22
|
+
name: 'TestTypeDefault',
|
|
23
|
+
fields: () => ({
|
|
24
|
+
id: { type: GraphQLID },
|
|
25
|
+
name: { type: GraphQLString },
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
simfinity.connect(null, TestType, 'testTypeDefault', 'testTypesDefault');
|
|
30
|
+
expect(createCollectionSpy).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should NOT create collection when preventCreatingCollection is true', () => {
|
|
34
|
+
simfinity.preventCreatingCollection(true);
|
|
35
|
+
|
|
36
|
+
const TestType = new GraphQLObjectType({
|
|
37
|
+
name: 'TestTypePrevent',
|
|
38
|
+
fields: () => ({
|
|
39
|
+
id: { type: GraphQLID },
|
|
40
|
+
name: { type: GraphQLString },
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
simfinity.connect(null, TestType, 'testTypePrevent', 'testTypesPrevent');
|
|
45
|
+
expect(createCollectionSpy).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should create collection when preventCreatingCollection is set back to false', () => {
|
|
49
|
+
simfinity.preventCreatingCollection(true); // first prevent
|
|
50
|
+
simfinity.preventCreatingCollection(false); // then allow
|
|
51
|
+
|
|
52
|
+
const TestType = new GraphQLObjectType({
|
|
53
|
+
name: 'TestTypeAllow',
|
|
54
|
+
fields: () => ({
|
|
55
|
+
id: { type: GraphQLID },
|
|
56
|
+
name: { type: GraphQLString },
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
simfinity.connect(null, TestType, 'testTypeAllow', 'testTypesAllow');
|
|
61
|
+
expect(createCollectionSpy).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
});
|