@simtlix/simfinity-core 3.5.7 → 3.5.8
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 +3 -1
- package/package.json +54 -8
- package/src/const/QLValue.js +11 -3
- package/src/errors/internal-server.error.js +5 -1
- package/src/runtime.js +343 -143
- package/src/scalars.js +53 -33
- package/src/validators.js +159 -47
- package/types/auth/errors.d.ts +16 -0
- package/types/auth/expressions.d.ts +25 -0
- package/types/auth/index.d.ts +36 -0
- package/types/auth/rules.d.ts +30 -0
- package/types/index.d.ts +55 -12
- package/types/plugins.d.ts +12 -0
- package/types/scalars.d.ts +16 -0
- package/types/validators.d.ts +20 -0
package/src/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
import SimfinityError from './errors/simfinity.error.js';
|
|
11
11
|
import InternalServerError from './errors/internal-server.error.js';
|
|
12
|
+
import { ForbiddenError } from './auth/errors.js';
|
|
12
13
|
import QLOperator from './const/QLOperator.js';
|
|
13
14
|
import QLValue from './const/QLValue.js';
|
|
14
15
|
import QLSort from './const/QLSort.js';
|
|
@@ -34,15 +35,23 @@ const countSinkOf = (value) => {
|
|
|
34
35
|
return typeof sink === 'function' ? sink : undefined;
|
|
35
36
|
};
|
|
36
37
|
// The root value arrives as parent, which resolver wrappers that forward only (parent, args,
|
|
37
|
-
// context) still pass on; info.rootValue is the fallback.
|
|
38
|
-
|
|
38
|
+
// context) still pass on; info.rootValue is the fallback.
|
|
39
|
+
const countSinkFor = (parent, info) => countSinkOf(parent) || (info ? countSinkOf(info.rootValue) : undefined);
|
|
40
|
+
// Without a sink the count goes to context.count, where Envelop/Apollo/Yoga count plugins read it.
|
|
41
|
+
// With neither, such as a graphql() call without contextValue, nothing could read a count, so the
|
|
42
|
+
// count query is skipped.
|
|
43
|
+
const canReportCount = (parent, info, context) => countSinkFor(parent, info) !== undefined
|
|
44
|
+
|| (context !== null && (typeof context === 'object' || typeof context === 'function'));
|
|
45
|
+
// Returns whether the count was stored.
|
|
39
46
|
const reportCount = (parent, info, context, count) => {
|
|
40
|
-
const sink =
|
|
47
|
+
const sink = countSinkFor(parent, info);
|
|
41
48
|
if (sink) {
|
|
42
49
|
sink(count);
|
|
43
|
-
return;
|
|
50
|
+
return true;
|
|
44
51
|
}
|
|
45
|
-
|
|
52
|
+
// Reflect.set returns false for a frozen, sealed or read-only context, where an assignment would
|
|
53
|
+
// throw after both queries ran.
|
|
54
|
+
return Reflect.set(context, 'count', count);
|
|
46
55
|
};
|
|
47
56
|
const applicationResolvedFields = new WeakMap();
|
|
48
57
|
const markGenerated = (resolve) => {
|
|
@@ -201,6 +210,31 @@ const GraphQLJSON = new GraphQLScalarType({
|
|
|
201
210
|
},
|
|
202
211
|
});
|
|
203
212
|
|
|
213
|
+
// The type called `name` that a schema built from these roots would contain, other than through
|
|
214
|
+
// `skip`, following the references GraphQLSchema collects: fields, arguments, interfaces, union
|
|
215
|
+
// members and input fields.
|
|
216
|
+
const findSchemaType = (roots, name, skip) => {
|
|
217
|
+
const visited = new Set();
|
|
218
|
+
const pending = [...roots];
|
|
219
|
+
while (pending.length) {
|
|
220
|
+
const type = getNamedType(pending.pop());
|
|
221
|
+
if (!type || type === skip || visited.has(type)) continue;
|
|
222
|
+
visited.add(type);
|
|
223
|
+
if (type.name === name) return type;
|
|
224
|
+
if (type instanceof GraphQLUnionType) pending.push(...type.getTypes());
|
|
225
|
+
if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) {
|
|
226
|
+
pending.push(...type.getInterfaces());
|
|
227
|
+
for (const field of Object.values(type.getFields())) {
|
|
228
|
+
pending.push(field.type, ...field.args.map((arg) => arg.type));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (type instanceof GraphQLInputObjectType) {
|
|
232
|
+
for (const field of Object.values(type.getFields())) pending.push(field.type);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
};
|
|
237
|
+
|
|
204
238
|
// graphql-js wraps a value thrown while resolving a field (locatedError): the wrapper has the field
|
|
205
239
|
// path and repeats the thrown error's message. A thrown non-Error becomes a NonErrorThrown whose
|
|
206
240
|
// message prints the value.
|
|
@@ -338,6 +372,14 @@ export const createRuntime = (adapter) => {
|
|
|
338
372
|
const typesDict = { types: {} };
|
|
339
373
|
const waitingInputType = {};
|
|
340
374
|
const typesDictForUpdate = { types: {} };
|
|
375
|
+
// `added` item inputs of referenced collections by generated name, so collections that reach the
|
|
376
|
+
// same child through the same connectionField share one input type.
|
|
377
|
+
const excludedFieldInputTypes = new Map();
|
|
378
|
+
// Self-referencing collection field name -> the types whose collection inputs were built with the
|
|
379
|
+
// unqualified names OneToManyA<field> and OneToManyU<field>; type -> its self-referencing
|
|
380
|
+
// collections whose inputs are named after the type instead (see claimSelfCollectionNames).
|
|
381
|
+
const selfCollectionNameHolders = new Map();
|
|
382
|
+
const qualifiedSelfCollections = new Map();
|
|
341
383
|
const registeredMutations = {};
|
|
342
384
|
const middlewares = [];
|
|
343
385
|
|
|
@@ -519,13 +561,24 @@ const QLTypeAggregationExpression = new GraphQLInputObjectType({
|
|
|
519
561
|
}),
|
|
520
562
|
});
|
|
521
563
|
|
|
522
|
-
|
|
564
|
+
// Aggregation results use Simfinity's JSON scalar, or the application's own scalar named JSON when
|
|
565
|
+
// the schema has one, such as GraphQLJSON from graphql-scalars, so the two do not collide.
|
|
566
|
+
const createAggregationResultType = (valueType) => new GraphQLObjectType({
|
|
523
567
|
name: 'QLTypeAggregationResult',
|
|
524
568
|
fields: () => ({
|
|
525
|
-
groupId: { type:
|
|
526
|
-
facts: { type:
|
|
569
|
+
groupId: { type: valueType },
|
|
570
|
+
facts: { type: valueType },
|
|
527
571
|
}),
|
|
528
572
|
});
|
|
573
|
+
const QLTypeAggregationResult = createAggregationResultType(GraphQLJSON);
|
|
574
|
+
const applicationJSONResultTypes = new WeakMap();
|
|
575
|
+
const aggregationResultTypeFor = (valueType) => {
|
|
576
|
+
if (valueType === GraphQLJSON) return QLTypeAggregationResult;
|
|
577
|
+
if (!applicationJSONResultTypes.has(valueType)) {
|
|
578
|
+
applicationJSONResultTypes.set(valueType, createAggregationResultType(valueType));
|
|
579
|
+
}
|
|
580
|
+
return applicationJSONResultTypes.get(valueType);
|
|
581
|
+
};
|
|
529
582
|
|
|
530
583
|
const isNonNullOfType = (fieldEntryType, graphQLType) => {
|
|
531
584
|
let isOfType = false;
|
|
@@ -536,6 +589,7 @@ const isNonNullOfType = (fieldEntryType, graphQLType) => {
|
|
|
536
589
|
};
|
|
537
590
|
|
|
538
591
|
const unwrapNonNull = (type) => (type instanceof GraphQLNonNull ? type.ofType : type);
|
|
592
|
+
const isAbstractOutputType = (type) => type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType;
|
|
539
593
|
|
|
540
594
|
const unwrapListAndNonNull = (type) => {
|
|
541
595
|
let unwrapped = type;
|
|
@@ -553,59 +607,69 @@ const wrapListInputType = (itemType, listShape, preserveOuterNonNull) => {
|
|
|
553
607
|
: listType;
|
|
554
608
|
};
|
|
555
609
|
|
|
610
|
+
const capitalize = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
611
|
+
|
|
556
612
|
/**
|
|
557
|
-
*
|
|
613
|
+
* The `added` item input of a referenced collection: the item type's create input without the
|
|
614
|
+
* back-reference that the parent supplies. Its fields are read from the item's create input when
|
|
615
|
+
* the schema is built, so the item's input may be built after this type, as in cycles of
|
|
616
|
+
* collections. `optionalFields` lose their outer non-null wrapper. Collections that reach the same
|
|
617
|
+
* item through the same connectionField with the same prefix share one type.
|
|
558
618
|
* @param {string} inputNamePrefix - The prefix for the input type name.
|
|
559
|
-
* @param {
|
|
619
|
+
* @param {GraphQLObjectType} itemType - The collection's item type.
|
|
560
620
|
* @param {string} fieldToExclude - The name of the field to exclude.
|
|
561
|
-
* @
|
|
621
|
+
* @param {string} [connectionFieldName] - The declared connectionField, which names the type.
|
|
622
|
+
* @param {string[]} [optionalFields] - Fields of the item's create input that become optional.
|
|
623
|
+
* @returns {GraphQLInputObjectType} An input type without the specified field.
|
|
562
624
|
*/
|
|
563
|
-
const createTypeWithExcludedField = (inputNamePrefix,
|
|
564
|
-
connectionFieldName = fieldToExclude) => {
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
625
|
+
const createTypeWithExcludedField = (inputNamePrefix, itemType, fieldToExclude,
|
|
626
|
+
connectionFieldName = fieldToExclude, optionalFields = []) => {
|
|
627
|
+
// The item's create input is `${itemType.name}Input`, so the name is the one it always had.
|
|
628
|
+
const name = `${inputNamePrefix}${itemType.name}InputFor${capitalize(connectionFieldName)}`;
|
|
629
|
+
const shared = excludedFieldInputTypes.get(name);
|
|
630
|
+
if (shared && shared.itemType === itemType && shared.fieldToExclude === fieldToExclude
|
|
631
|
+
&& shared.optionalFields.join() === optionalFields.join()) {
|
|
632
|
+
return shared.type;
|
|
633
|
+
}
|
|
634
|
+
// Any other type with this name is a new one, so a real name clash still fails as before.
|
|
635
|
+
const type = new GraphQLInputObjectType({
|
|
636
|
+
name,
|
|
637
|
+
fields: () => Object.fromEntries(
|
|
638
|
+
Object.entries(getInputType(itemType).getFields())
|
|
639
|
+
.filter(([fieldName]) => fieldName !== fieldToExclude)
|
|
640
|
+
.map(([fieldName, field]) => [fieldName, {
|
|
641
|
+
type: optionalFields.includes(fieldName) ? unwrapNonNull(field.type) : field.type,
|
|
642
|
+
description: field.description,
|
|
643
|
+
defaultValue: field.defaultValue,
|
|
644
|
+
}]),
|
|
645
|
+
),
|
|
579
646
|
});
|
|
647
|
+
if (!shared) excludedFieldInputTypes.set(name, { itemType, fieldToExclude, optionalFields, type });
|
|
648
|
+
return type;
|
|
580
649
|
};
|
|
581
650
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
651
|
+
// A referenced collection's operation input. The item inputs are read when the schema is built, so
|
|
652
|
+
// the item type's inputs need not exist yet.
|
|
653
|
+
const createOneToManyInputType = (name, inputNamePrefix, itemType, connection,
|
|
654
|
+
itemNonNull = false, optionalFields = []) => {
|
|
655
|
+
const inputTypeForAdd = connection.declaredFieldName
|
|
656
|
+
? createTypeWithExcludedField(
|
|
588
657
|
inputNamePrefix,
|
|
589
|
-
|
|
658
|
+
itemType,
|
|
590
659
|
connection.graphqlFieldName || connection.declaredFieldName,
|
|
591
660
|
connection.declaredFieldName,
|
|
592
|
-
|
|
593
|
-
|
|
661
|
+
optionalFields,
|
|
662
|
+
)
|
|
663
|
+
: null;
|
|
664
|
+
const listOf = (type) => new GraphQLList(itemNonNull ? new GraphQLNonNull(type) : type);
|
|
594
665
|
|
|
595
666
|
return new GraphQLInputObjectType({
|
|
596
|
-
name
|
|
667
|
+
name,
|
|
597
668
|
fields: () => ({
|
|
598
|
-
added: {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
},
|
|
602
|
-
updated: {
|
|
603
|
-
type: new GraphQLList(itemNonNull
|
|
604
|
-
? new GraphQLNonNull(updateInputType) : updateInputType),
|
|
605
|
-
},
|
|
606
|
-
deleted: {
|
|
607
|
-
type: new GraphQLList(itemNonNull ? new GraphQLNonNull(GraphQLID) : GraphQLID),
|
|
608
|
-
},
|
|
669
|
+
added: { type: listOf(inputTypeForAdd || getInputType(itemType)) },
|
|
670
|
+
updated: { type: listOf(typesDictForUpdate.types[itemType.name].inputType) },
|
|
671
|
+
// Null IDs are skipped, so `deleted` accepts them whatever the collection's item nullability.
|
|
672
|
+
deleted: { type: new GraphQLList(GraphQLID) },
|
|
609
673
|
}),
|
|
610
674
|
});
|
|
611
675
|
};
|
|
@@ -616,26 +680,19 @@ const graphQLListInputType = (dict, fieldEntry, fieldEntryName,
|
|
|
616
680
|
if (!listShape) return null;
|
|
617
681
|
const { itemType } = listShape;
|
|
618
682
|
|
|
619
|
-
if (itemType instanceof GraphQLObjectType
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const oneToMany = createOneToManyInputType(inputNamePrefix
|
|
624
|
-
|
|
625
|
-
typesDictForUpdate.types[itemType.name].inputType,
|
|
626
|
-
normalizeConnectionField(itemType, connectionField),
|
|
683
|
+
if (itemType instanceof GraphQLObjectType) {
|
|
684
|
+
const registration = requireRegisteredType(dict, itemType, fieldEntryName, ownerName);
|
|
685
|
+
// A referenced collection reads its item inputs lazily, so it never waits for them.
|
|
686
|
+
if (!fieldEntry.extensions?.relation?.embedded) {
|
|
687
|
+
const oneToMany = createOneToManyInputType(`OneToMany${inputNamePrefix}${fieldEntryName}`,
|
|
688
|
+
inputNamePrefix, itemType, normalizeConnectionField(itemType, connectionField),
|
|
627
689
|
listShape.itemNonNull);
|
|
628
690
|
return preserveOuterNonNull && listShape.outerNonNull
|
|
629
691
|
? new GraphQLNonNull(oneToMany)
|
|
630
692
|
: oneToMany;
|
|
631
693
|
}
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
return wrapListInputType(
|
|
635
|
-
dict.types[itemType.name].inputType,
|
|
636
|
-
listShape,
|
|
637
|
-
preserveOuterNonNull,
|
|
638
|
-
);
|
|
694
|
+
if (registration.inputType) {
|
|
695
|
+
return wrapListInputType(registration.inputType, listShape, preserveOuterNonNull);
|
|
639
696
|
}
|
|
640
697
|
} else if (itemType instanceof GraphQLScalarType || itemType instanceof GraphQLEnumType) {
|
|
641
698
|
return wrapListInputType(itemType, listShape, preserveOuterNonNull);
|
|
@@ -643,6 +700,38 @@ const graphQLListInputType = (dict, fieldEntry, fieldEntryName,
|
|
|
643
700
|
return null;
|
|
644
701
|
};
|
|
645
702
|
|
|
703
|
+
// Writable list fields whose items are the type itself, as buildInputType selects them. Their
|
|
704
|
+
// collection inputs are named OneToManyA<field> and OneToManyU<field> (see claimSelfCollectionNames).
|
|
705
|
+
const selfCollectionFieldNames = (gqltype) => {
|
|
706
|
+
const hasStateMachine = !!typesDict.types[gqltype.name]?.stateMachine;
|
|
707
|
+
return Object.entries(gqltype.getFields())
|
|
708
|
+
.filter(([fieldName, field]) => !field.extensions?.readOnly
|
|
709
|
+
&& !(fieldName === 'state' && hasStateMachine)
|
|
710
|
+
&& getListShape(field.type)?.itemType === gqltype)
|
|
711
|
+
.map(([fieldName]) => fieldName);
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const selfCollectionInputType = (gqltype, fieldEntryName, fieldEntry, forUpdate, selfFields) => {
|
|
715
|
+
const listShape = getListShape(fieldEntry.type);
|
|
716
|
+
const qualifier = qualifiedSelfCollections.get(gqltype)?.has(fieldEntryName) ? gqltype.name : '';
|
|
717
|
+
const kind = forUpdate ? 'U' : 'A';
|
|
718
|
+
const oneToMany = createOneToManyInputType(
|
|
719
|
+
`OneToMany${qualifier}${kind}${fieldEntryName}`,
|
|
720
|
+
kind,
|
|
721
|
+
gqltype,
|
|
722
|
+
normalizeConnectionField(gqltype, fieldEntry.extensions?.relation?.connectionField),
|
|
723
|
+
listShape.itemNonNull,
|
|
724
|
+
// Items an add mutation creates may carry their own self-referencing collections. These stay
|
|
725
|
+
// optional there, as such items had no such fields before; items added by an update keep them
|
|
726
|
+
// as the type declares them.
|
|
727
|
+
forUpdate ? [] : selfFields,
|
|
728
|
+
);
|
|
729
|
+
return !forUpdate && listShape.outerNonNull ? new GraphQLNonNull(oneToMany) : oneToMany;
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// Builds a type's create and update inputs, or returns every field whose input cannot be built yet:
|
|
733
|
+
// `{ unresolvedFields: [{ field, waitsFor }] }`, where `waitsFor` names the embedded type whose
|
|
734
|
+
// inputs the field waits for, and is absent when the field can have no generated input.
|
|
646
735
|
const buildInputType = (gqltype) => {
|
|
647
736
|
const argTypes = gqltype.getFields();
|
|
648
737
|
|
|
@@ -650,6 +739,7 @@ const buildInputType = (gqltype) => {
|
|
|
650
739
|
const fieldsArgForUpdate = {};
|
|
651
740
|
|
|
652
741
|
const selfReferenceCollections = {};
|
|
742
|
+
const unresolvedFields = [];
|
|
653
743
|
|
|
654
744
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
655
745
|
const fieldArg = {};
|
|
@@ -687,11 +777,19 @@ const buildInputType = (gqltype) => {
|
|
|
687
777
|
fieldArg.type = typesDict.types[fieldEntryNameValue].inputType;
|
|
688
778
|
fieldArgForUpdate.type = typesDictForUpdate.types[fieldEntryNameValue].inputType;
|
|
689
779
|
} else {
|
|
690
|
-
|
|
780
|
+
unresolvedFields.push({ field: fieldEntryName, waitsFor: fieldEntryNameValue });
|
|
691
781
|
}
|
|
692
|
-
} else {
|
|
782
|
+
} else if (unresolvedFields.length === 0) {
|
|
783
|
+
// Fields after one that cannot be built yet are only checked, so this logs as many times
|
|
784
|
+
// as when the build stopped at that field.
|
|
693
785
|
console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
|
|
694
786
|
}
|
|
787
|
+
} else if (isAbstractOutputType(unwrapListAndNonNull(fieldEntry.type))) {
|
|
788
|
+
// Generated inputs cannot write an interface or union value, single or in a list, so the
|
|
789
|
+
// field is output-only. readOnly fields never get here.
|
|
790
|
+
warnOnce(gqltype, `abstract:${fieldEntryName}`, `Configuration issue: ${gqltype.name}.${fieldEntryName} `
|
|
791
|
+
+ 'has an interface or union type, so generated inputs leave it out and generated mutations cannot '
|
|
792
|
+
+ 'set it; resolve it yourself, and mark it readOnly to state that it is output-only.');
|
|
695
793
|
} else if (getListShape(fieldEntry.type)) {
|
|
696
794
|
const listShape = getListShape(fieldEntry.type);
|
|
697
795
|
if (listShape.itemType === gqltype) {
|
|
@@ -707,7 +805,11 @@ const buildInputType = (gqltype) => {
|
|
|
707
805
|
fieldArg.type = listInputTypeForAdd;
|
|
708
806
|
fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
709
807
|
} else {
|
|
710
|
-
|
|
808
|
+
// An object item here is embedded and its inputs are not built yet; any other item,
|
|
809
|
+
// such as a list, has no generated input.
|
|
810
|
+
unresolvedFields.push(listShape.itemType instanceof GraphQLObjectType
|
|
811
|
+
? { field: fieldEntryName, waitsFor: listShape.itemType.name }
|
|
812
|
+
: { field: fieldEntryName });
|
|
711
813
|
}
|
|
712
814
|
}
|
|
713
815
|
}
|
|
@@ -729,94 +831,165 @@ const buildInputType = (gqltype) => {
|
|
|
729
831
|
}
|
|
730
832
|
}
|
|
731
833
|
|
|
732
|
-
|
|
834
|
+
if (unresolvedFields.length > 0) return { unresolvedFields };
|
|
835
|
+
|
|
836
|
+
// Self-referencing collections follow the other fields, as they always have. Their item inputs
|
|
837
|
+
// are read when the schema is built, after these input types exist.
|
|
838
|
+
const selfFields = Object.keys(selfReferenceCollections);
|
|
839
|
+
for (const [fieldEntryName, fieldEntry] of Object.entries(selfReferenceCollections)) {
|
|
840
|
+
fieldsArgs[fieldEntryName] = {
|
|
841
|
+
type: selfCollectionInputType(gqltype, fieldEntryName, fieldEntry, false, selfFields),
|
|
842
|
+
description: fieldEntry.description,
|
|
843
|
+
};
|
|
844
|
+
fieldsArgForUpdate[fieldEntryName] = {
|
|
845
|
+
type: selfCollectionInputType(gqltype, fieldEntryName, fieldEntry, true, selfFields),
|
|
846
|
+
description: fieldEntry.description,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const inputTypeForAdd = new GraphQLInputObjectType({
|
|
733
851
|
name: `${gqltype.name}Input`,
|
|
734
852
|
description: gqltype.description,
|
|
735
853
|
fields: fieldsArgs,
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
const inputTypeBodyForUpdate = {
|
|
854
|
+
});
|
|
855
|
+
const inputTypeForUpdate = new GraphQLInputObjectType({
|
|
739
856
|
name: `${gqltype.name}InputForUpdate`,
|
|
740
857
|
description: gqltype.description,
|
|
741
858
|
fields: fieldsArgForUpdate,
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
const inputTypeForAdd = new GraphQLInputObjectType(inputTypeBody);
|
|
745
|
-
const inputTypeForUpdate = new GraphQLInputObjectType(inputTypeBodyForUpdate);
|
|
746
|
-
|
|
747
|
-
const inputTypeForAddFields = inputTypeForAdd._fields();
|
|
748
|
-
|
|
749
|
-
Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
|
|
750
|
-
if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
|
|
751
|
-
const fieldEntry = selfReferenceCollections[fieldEntryName];
|
|
752
|
-
const listShape = getListShape(fieldEntry.type);
|
|
753
|
-
const oneToMany = createOneToManyInputType('A', fieldEntryName,
|
|
754
|
-
inputTypeForAdd, inputTypeForUpdate,
|
|
755
|
-
normalizeConnectionField(
|
|
756
|
-
gqltype,
|
|
757
|
-
fieldEntry.extensions?.relation?.connectionField,
|
|
758
|
-
), listShape.itemNonNull);
|
|
759
|
-
inputTypeForAddFields[fieldEntryName] = {
|
|
760
|
-
type: listShape.outerNonNull ? new GraphQLNonNull(oneToMany) : oneToMany,
|
|
761
|
-
name: fieldEntryName,
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
inputTypeForAdd._fields = () => inputTypeForAddFields;
|
|
767
|
-
|
|
768
|
-
const inputTypeForUpdateFields = inputTypeForUpdate._fields();
|
|
769
|
-
|
|
770
|
-
Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
|
|
771
|
-
if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
|
|
772
|
-
const fieldEntry = selfReferenceCollections[fieldEntryName];
|
|
773
|
-
const listShape = getListShape(fieldEntry.type);
|
|
774
|
-
inputTypeForUpdateFields[fieldEntryName] = {
|
|
775
|
-
type: createOneToManyInputType('U', fieldEntryName,
|
|
776
|
-
inputTypeForAdd, inputTypeForUpdate,
|
|
777
|
-
normalizeConnectionField(
|
|
778
|
-
gqltype,
|
|
779
|
-
fieldEntry.extensions?.relation?.connectionField,
|
|
780
|
-
), listShape.itemNonNull),
|
|
781
|
-
name: fieldEntryName,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
859
|
});
|
|
785
860
|
|
|
786
|
-
inputTypeForUpdate._fields = () => inputTypeForUpdateFields;
|
|
787
|
-
|
|
788
861
|
return { inputTypeBody: inputTypeForAdd, inputTypeBodyForUpdate: inputTypeForUpdate };
|
|
789
862
|
};
|
|
790
863
|
|
|
791
864
|
const getInputType = (type) => typesDict.types[type.name].inputType;
|
|
792
865
|
|
|
793
|
-
|
|
866
|
+
// Registered types whose generated inputs the generated mutations of this schema can reach: the
|
|
867
|
+
// included endpoint types and, through their writable fields, embedded types and the items of
|
|
868
|
+
// collections.
|
|
869
|
+
const typesWithReachableInputs = (includedMutationTypes) => {
|
|
870
|
+
const reached = new Set();
|
|
871
|
+
const visit = (gqltype) => {
|
|
872
|
+
if (reached.has(gqltype)) return;
|
|
873
|
+
reached.add(gqltype);
|
|
874
|
+
for (const field of Object.values(gqltype.getFields())) {
|
|
875
|
+
if (field.extensions?.readOnly) continue;
|
|
876
|
+
const listShape = getListShape(field.type);
|
|
877
|
+
const target = listShape ? listShape.itemType : unwrapNonNull(field.type);
|
|
878
|
+
// A single reference is an IdInputType, so it reaches no input of its target.
|
|
879
|
+
if (target instanceof GraphQLObjectType && typesDict.types[target.name]
|
|
880
|
+
&& (listShape || field.extensions?.relation?.embedded)) {
|
|
881
|
+
visit(typesDict.types[target.name].gqltype);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
for (const type of Object.values(typesDict.types)) {
|
|
886
|
+
if (type.endpoint && !shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
887
|
+
visit(type.gqltype);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return reached;
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
// A self-referencing collection's inputs are OneToManyA<field> and OneToManyU<field>. When several
|
|
894
|
+
// types that the generated mutations reach have one with the same field name, the first registered
|
|
895
|
+
// keeps those names and the others get OneToMany<Type>A<field> and OneToMany<Type>U<field>, as other
|
|
896
|
+
// collections do. Types the mutations cannot reach keep the unqualified names, as such inputs never
|
|
897
|
+
// collided. Names are fixed when a type's inputs are built, at its first schema.
|
|
898
|
+
// A type is qualified only when this schema also reaches an earlier type that holds the unqualified
|
|
899
|
+
// names and is still the type registered under its name, since both inputs then meet in one schema,
|
|
900
|
+
// which failed with a duplicate type name before such names were qualified. Otherwise, as with a
|
|
901
|
+
// type registered again under the same name or after an earlier createSchema(), it keeps the
|
|
902
|
+
// unqualified names.
|
|
903
|
+
const claimSelfCollectionNames = (pending, includedMutationTypes) => {
|
|
904
|
+
const reachable = typesWithReachableInputs(includedMutationTypes);
|
|
905
|
+
const meets = (type) => typesDict.types[type.name]?.gqltype === type && reachable.has(type);
|
|
906
|
+
for (const { gqltype } of Object.values(pending)) {
|
|
907
|
+
if (typesDict.types[gqltype.name].inputType) continue;
|
|
908
|
+
for (const fieldName of selfCollectionFieldNames(gqltype)) {
|
|
909
|
+
if (!selfCollectionNameHolders.has(fieldName)) selfCollectionNameHolders.set(fieldName, []);
|
|
910
|
+
const holders = selfCollectionNameHolders.get(fieldName);
|
|
911
|
+
const owner = reachable.has(gqltype) && holders.find((type) => type !== gqltype && meets(type));
|
|
912
|
+
if (!owner) {
|
|
913
|
+
if (!holders.includes(gqltype)) holders.push(gqltype);
|
|
914
|
+
} else {
|
|
915
|
+
if (!qualifiedSelfCollections.has(gqltype)) qualifiedSelfCollections.set(gqltype, new Set());
|
|
916
|
+
qualifiedSelfCollections.get(gqltype).add(fieldName);
|
|
917
|
+
warnOnce(gqltype, `selfCollectionName:${fieldName}`, `Configuration issue: ${gqltype.name}.${fieldName} `
|
|
918
|
+
+ `and ${owner.name}.${fieldName} are self-referencing collections with the same name, so the inputs `
|
|
919
|
+
+ `of ${gqltype.name}.${fieldName} are named OneToMany${gqltype.name}A${fieldName} and `
|
|
920
|
+
+ `OneToMany${gqltype.name}U${fieldName}, while ${owner.name}.${fieldName} keeps OneToManyA${fieldName} `
|
|
921
|
+
+ `and OneToManyU${fieldName}. To choose which type keeps those names, register it first, or make `
|
|
922
|
+
+ 'the other type unreachable from the generated mutations.');
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// The INPUT_TYPE_UNRESOLVED message. It names every field whose input could not be built, by type:
|
|
929
|
+
// the causes (lists whose items have no generated input, and embedded fields whose types contain
|
|
930
|
+
// each other) and, apart from them, fields that only wait for another listed type.
|
|
931
|
+
const unresolvedInputTypesMessage = (pendingTypeNames, unresolvedByType) => {
|
|
932
|
+
// Whether the inputs of type `from` wait, through embedded fields, for those of type `to`.
|
|
933
|
+
const waitsOn = (from, to, seen = new Set()) => {
|
|
934
|
+
if (from === to) return true;
|
|
935
|
+
if (seen.has(from)) return false;
|
|
936
|
+
seen.add(from);
|
|
937
|
+
return (unresolvedByType[from] || [])
|
|
938
|
+
.some(({ waitsFor }) => waitsFor !== undefined && waitsOn(waitsFor, to, seen));
|
|
939
|
+
};
|
|
940
|
+
const lists = [];
|
|
941
|
+
const cycles = [];
|
|
942
|
+
const waiting = [];
|
|
943
|
+
for (const [typeName, fields] of Object.entries(unresolvedByType)) {
|
|
944
|
+
for (const { field, waitsFor } of fields) {
|
|
945
|
+
const name = `${typeName}.${field}`;
|
|
946
|
+
if (waitsFor === undefined) lists.push(name);
|
|
947
|
+
else if (waitsOn(waitsFor, typeName)) cycles.push(name);
|
|
948
|
+
else waiting.push(`${name} waits for ${waitsFor}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const causes = [
|
|
952
|
+
lists.length > 0 && `${lists.join(', ')} (writable lists whose items have no generated input, such as lists of lists)`,
|
|
953
|
+
cycles.length > 0 && `${cycles.join(', ')} (embedded types that contain each other)`,
|
|
954
|
+
].filter(Boolean);
|
|
955
|
+
return `Could not build input types for: ${pendingTypeNames.join(', ')}. `
|
|
956
|
+
+ 'Check for circular or misconfigured relations'
|
|
957
|
+
+ (causes.length > 0
|
|
958
|
+
? `: no input can be generated for ${causes.join(' or for ')}; mark such a field readOnly.`
|
|
959
|
+
: '.')
|
|
960
|
+
+ (waiting.length > 0
|
|
961
|
+
? ` These fields only wait for another listed type and build once it does: ${waiting.join(', ')}.`
|
|
962
|
+
: '');
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const buildPendingInputTypes = (waitingForInputType, includedMutationTypes) => {
|
|
966
|
+
claimSelfCollectionNames(waitingForInputType, includedMutationTypes);
|
|
794
967
|
let pending = waitingForInputType;
|
|
795
968
|
let previousPendingCount = Object.keys(pending).length + 1;
|
|
969
|
+
// Type name -> the fields whose input could not be built in the last pass.
|
|
970
|
+
let unresolvedFields = {};
|
|
796
971
|
|
|
797
972
|
while (Object.keys(pending).length > 0) {
|
|
798
973
|
const currentCount = Object.keys(pending).length;
|
|
799
974
|
if (currentCount >= previousPendingCount) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
`Could not build input types for: ${unresolved}. Check for circular or misconfigured relations.`,
|
|
803
|
-
'INPUT_TYPE_UNRESOLVED',
|
|
804
|
-
500,
|
|
805
|
-
);
|
|
975
|
+
throw new SimfinityError(unresolvedInputTypesMessage(Object.keys(pending), unresolvedFields),
|
|
976
|
+
'INPUT_TYPE_UNRESOLVED', 500);
|
|
806
977
|
}
|
|
807
978
|
previousPendingCount = currentCount;
|
|
808
979
|
|
|
809
980
|
const stillWaiting = {};
|
|
981
|
+
unresolvedFields = {};
|
|
810
982
|
for (const [key, value] of Object.entries(pending)) {
|
|
811
983
|
const { gqltype } = value;
|
|
812
984
|
if (typesDict.types[gqltype.name].inputType) continue;
|
|
813
985
|
|
|
814
986
|
const result = buildInputType(gqltype);
|
|
815
|
-
if (result
|
|
987
|
+
if (result.inputTypeBody && result.inputTypeBodyForUpdate) {
|
|
816
988
|
typesDict.types[gqltype.name].inputType = result.inputTypeBody;
|
|
817
989
|
typesDictForUpdate.types[gqltype.name].inputType = result.inputTypeBodyForUpdate;
|
|
818
990
|
} else {
|
|
819
991
|
stillWaiting[key] = value;
|
|
992
|
+
unresolvedFields[gqltype.name] = result.unresolvedFields;
|
|
820
993
|
}
|
|
821
994
|
}
|
|
822
995
|
pending = stillWaiting;
|
|
@@ -1070,7 +1243,11 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
1070
1243
|
if (!result) throw new SimfinityError(`${gqltype.name} ${objectId} is not valid`, 'NOT_VALID_ID', 404);
|
|
1071
1244
|
|
|
1072
1245
|
if (materializedModel.collectionFields) {
|
|
1073
|
-
|
|
1246
|
+
// Children link to, and are owned through, the parent's stored key, as when a new parent is
|
|
1247
|
+
// saved: the client may send a spelling of the ID that the adapter accepts but does not store,
|
|
1248
|
+
// such as an uppercase ObjectId or UUID. It is the key the generated collection field reads
|
|
1249
|
+
// children by (`_id`, else `id`); an adapter whose update result has neither keeps the requested ID.
|
|
1250
|
+
await iterateOnCollectionFields(materializedModel, gqltype, result._id ?? result.id ?? objectId, session, context);
|
|
1074
1251
|
}
|
|
1075
1252
|
|
|
1076
1253
|
if (controller && controller.onUpdated) {
|
|
@@ -1232,7 +1409,9 @@ const executeOperation = (Model, gqltype, controller, args, operation, actionFie
|
|
|
1232
1409
|
);
|
|
1233
1410
|
};
|
|
1234
1411
|
|
|
1235
|
-
|
|
1412
|
+
// parentKey is the parent's stored key: children are linked to it, and an updated or deleted child
|
|
1413
|
+
// must already store it in its connectionField.
|
|
1414
|
+
const executeItemFunction = async (gqltype, collectionField, parentKey, session,
|
|
1236
1415
|
collectionFieldsList, operationType, context) => {
|
|
1237
1416
|
const argTypes = gqltype.getFields();
|
|
1238
1417
|
const collectionGQLType = unwrapListAndNonNull(argTypes[collectionField].type);
|
|
@@ -1246,7 +1425,7 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
1246
1425
|
const type = operationType === operations.UPDATE
|
|
1247
1426
|
? typesDictForUpdate.types[collectionGQLType.name] : typesDict.types[collectionGQLType.name];
|
|
1248
1427
|
const linkToParent = (item) => {
|
|
1249
|
-
item[connection.storageFieldName] =
|
|
1428
|
+
item[connection.storageFieldName] = parentKey;
|
|
1250
1429
|
if (item.$unset) delete item.$unset[connection.storageFieldName];
|
|
1251
1430
|
if (item.$set) delete item.$set[connection.storageFieldName];
|
|
1252
1431
|
};
|
|
@@ -1262,8 +1441,8 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
1262
1441
|
if (!child) {
|
|
1263
1442
|
throw new SimfinityError(`${collectionGQLType.name} ${id} is not valid`, 'NOT_VALID_ID', 404);
|
|
1264
1443
|
}
|
|
1265
|
-
if (String(child[connection.storageFieldName]) !== String(
|
|
1266
|
-
throw new
|
|
1444
|
+
if (String(child[connection.storageFieldName]) !== String(parentKey)) {
|
|
1445
|
+
throw new ForbiddenError('Child does not belong to this parent');
|
|
1267
1446
|
}
|
|
1268
1447
|
}
|
|
1269
1448
|
|
|
@@ -1536,7 +1715,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1536
1715
|
rootQueryArgs.name = name;
|
|
1537
1716
|
rootQueryArgs.fields = {};
|
|
1538
1717
|
|
|
1539
|
-
buildPendingInputTypes(waitingInputType);
|
|
1718
|
+
buildPendingInputTypes(waitingInputType, includedMutationTypes);
|
|
1540
1719
|
|
|
1541
1720
|
for (const type of Object.values(typesDict.types)) {
|
|
1542
1721
|
if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
@@ -1671,7 +1850,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1671
1850
|
return new GraphQLObjectType(rootQueryArgs);
|
|
1672
1851
|
};
|
|
1673
1852
|
|
|
1674
|
-
const buildRootQuery = (name, includedTypes) => {
|
|
1853
|
+
const buildRootQuery = (name, includedTypes, aggregationResultType = QLTypeAggregationResult) => {
|
|
1675
1854
|
const rootQueryArgs = {};
|
|
1676
1855
|
rootQueryArgs.name = name;
|
|
1677
1856
|
rootQueryArgs.fields = {};
|
|
@@ -1708,7 +1887,8 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1708
1887
|
await executeScope(params);
|
|
1709
1888
|
await applyJoinedScopes(joinedScopes, params);
|
|
1710
1889
|
const queryArgs = params.args;
|
|
1711
|
-
const wantsCount = !!(queryArgs.pagination && queryArgs.pagination.count)
|
|
1890
|
+
const wantsCount = !!(queryArgs.pagination && queryArgs.pagination.count)
|
|
1891
|
+
&& canReportCount(parent, info, context);
|
|
1712
1892
|
|
|
1713
1893
|
const dataPromise = adapter.find(type.model, type.gqltype, queryArgs, null);
|
|
1714
1894
|
const countPromise = wantsCount
|
|
@@ -1716,8 +1896,10 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1716
1896
|
: null;
|
|
1717
1897
|
|
|
1718
1898
|
const [result, resultCount] = await Promise.all([dataPromise, countPromise]);
|
|
1719
|
-
if (wantsCount) {
|
|
1720
|
-
|
|
1899
|
+
if (wantsCount && !reportCount(parent, info, context, resultCount)) {
|
|
1900
|
+
warnOnce(runtimeIdentity, 'count-context', 'Configuration issue: the GraphQL context does not accept '
|
|
1901
|
+
+ 'a count property (it is frozen, sealed or has a read-only count), so pagination counts are not '
|
|
1902
|
+
+ 'reported; pass a fresh mutable context object for each request.');
|
|
1721
1903
|
}
|
|
1722
1904
|
return result;
|
|
1723
1905
|
},
|
|
@@ -1734,7 +1916,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1734
1916
|
};
|
|
1735
1917
|
|
|
1736
1918
|
rootQueryArgs.fields[`${type.listEntitiesEndpointName}_aggregate`] = {
|
|
1737
|
-
type: new GraphQLList(
|
|
1919
|
+
type: new GraphQLList(aggregationResultType),
|
|
1738
1920
|
args: aggregateArgsObject,
|
|
1739
1921
|
extensions: { simfinityQuery: { typeName: type.gqltype.name, operation: 'aggregate' } },
|
|
1740
1922
|
async resolve(parent, args, context) {
|
|
@@ -1914,17 +2096,21 @@ const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomM
|
|
|
1914
2096
|
const objectMemberTypes = new Set();
|
|
1915
2097
|
Object.values(typesDict.types).forEach(({ gqltype }) => installObjectMemberResolvers(gqltype, objectMemberTypes));
|
|
1916
2098
|
|
|
1917
|
-
|
|
2099
|
+
let query = buildRootQuery('RootQueryType', includedQueryTypes);
|
|
1918
2100
|
Object.values(typesDict.types).forEach(({ gqltype }) => {
|
|
1919
2101
|
if (gqltype && !applicationResolvedFields.has(gqltype)) {
|
|
1920
2102
|
applicationResolvedFields.set(gqltype, listApplicationResolvedFields(gqltype));
|
|
1921
2103
|
}
|
|
1922
2104
|
});
|
|
2105
|
+
const mutation = buildMutation('Mutation', includedMutationTypes, includedCustomMutations);
|
|
2106
|
+
// An application scalar named JSON that this schema contains also types aggregation results. Any
|
|
2107
|
+
// other type named JSON still collides, and GraphQLSchema reports the duplicate name.
|
|
2108
|
+
const applicationJSON = findSchemaType([query, mutation], 'JSON', QLTypeAggregationResult);
|
|
2109
|
+
if (applicationJSON instanceof GraphQLScalarType) {
|
|
2110
|
+
query = buildRootQuery('RootQueryType', includedQueryTypes, aggregationResultTypeFor(applicationJSON));
|
|
2111
|
+
}
|
|
1923
2112
|
|
|
1924
|
-
const schema = new GraphQLSchema({
|
|
1925
|
-
query,
|
|
1926
|
-
mutation: buildMutation('Mutation', includedMutationTypes, includedCustomMutations),
|
|
1927
|
-
});
|
|
2113
|
+
const schema = new GraphQLSchema({ query, mutation });
|
|
1928
2114
|
// Reserve reachable types whose relation fields still have no resolver, such as an unregistered
|
|
1929
2115
|
// custom mutation result, so another runtime cannot later generate resolvers this schema executes.
|
|
1930
2116
|
schemaTypes.forEach((type) => {
|
|
@@ -2047,6 +2233,14 @@ const autoGenerateResolvers = (gqltype) => {
|
|
|
2047
2233
|
}
|
|
2048
2234
|
};
|
|
2049
2235
|
|
|
2236
|
+
// A replaced registration builds fresh inputs. Its cached collection projections may already
|
|
2237
|
+
// contain fields from the previous input graph; keep those alive only in earlier schemas.
|
|
2238
|
+
const invalidateCollectionInputs = (gqltype) => {
|
|
2239
|
+
for (const [name, cached] of excludedFieldInputTypes) {
|
|
2240
|
+
if (cached.itemType.name === gqltype.name) excludedFieldInputTypes.delete(name);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
|
|
2050
2244
|
const connect = (model, gqltype, simpleEntityEndpointName,
|
|
2051
2245
|
listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
|
|
2052
2246
|
const registration = {
|
|
@@ -2063,6 +2257,7 @@ const connect = (model, gqltype, simpleEntityEndpointName,
|
|
|
2063
2257
|
validateScope(gqltype);
|
|
2064
2258
|
if (adapter.validateRegistration) adapter.validateRegistration(registration);
|
|
2065
2259
|
|
|
2260
|
+
invalidateCollectionInputs(gqltype);
|
|
2066
2261
|
waitingInputType[gqltype.name] = {
|
|
2067
2262
|
model,
|
|
2068
2263
|
gqltype,
|
|
@@ -2092,6 +2287,7 @@ const addNoEndpointType = (gqltype) => {
|
|
|
2092
2287
|
validateScope(gqltype);
|
|
2093
2288
|
if (adapter.validateRegistration) adapter.validateRegistration(registration);
|
|
2094
2289
|
|
|
2290
|
+
invalidateCollectionInputs(gqltype);
|
|
2095
2291
|
waitingInputType[gqltype.name] = { gqltype };
|
|
2096
2292
|
typesDict.types[gqltype.name] = registration;
|
|
2097
2293
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
@@ -2115,27 +2311,31 @@ const createArgsForQuery = (gqltype, omit = null) => {
|
|
|
2115
2311
|
|
|
2116
2312
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
2117
2313
|
if (fieldEntryName === omit) continue;
|
|
2118
|
-
|
|
2314
|
+
let filterType = null;
|
|
2119
2315
|
|
|
2120
2316
|
if (fieldEntry.type instanceof GraphQLScalarType
|
|
2121
2317
|
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
2122
2318
|
|| fieldEntry.type instanceof GraphQLEnumType
|
|
2123
2319
|
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
2124
|
-
|
|
2320
|
+
filterType = QLFilter;
|
|
2125
2321
|
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
2126
2322
|
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
2127
|
-
|
|
2323
|
+
filterType = QLTypeFilterExpression;
|
|
2128
2324
|
} else if (getListShape(fieldEntry.type)) {
|
|
2129
2325
|
const listOfType = getListShape(fieldEntry.type).itemType;
|
|
2130
2326
|
if (listOfType instanceof GraphQLScalarType
|
|
2131
2327
|
|| isNonNullOfType(listOfType, GraphQLScalarType)
|
|
2132
2328
|
|| listOfType instanceof GraphQLEnumType
|
|
2133
2329
|
|| isNonNullOfType(listOfType, GraphQLEnumType)) {
|
|
2134
|
-
|
|
2330
|
+
filterType = QLFilter;
|
|
2135
2331
|
} else {
|
|
2136
|
-
|
|
2332
|
+
// Includes lists of interfaces and unions, which have no generated filter either. Their
|
|
2333
|
+
// argument has always been generated and is kept, so requests that send it as null stay valid.
|
|
2334
|
+
filterType = QLTypeFilterExpression;
|
|
2137
2335
|
}
|
|
2138
2336
|
}
|
|
2337
|
+
// A single interface or union field has no generated filter, so it gets no argument.
|
|
2338
|
+
if (filterType) argsObject[fieldEntryName] = { type: filterType };
|
|
2139
2339
|
}
|
|
2140
2340
|
|
|
2141
2341
|
argsObject.pagination = {};
|