@simtlix/simfinity-core 3.5.6 → 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 +378 -140
- package/src/scalars/factory.js +6 -0
- 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';
|
|
@@ -24,6 +25,34 @@ import './introspection.js';
|
|
|
24
25
|
// created is application code, such as a masking resolver, recorded once per type so later
|
|
25
26
|
// in-place wrapping (for example by the auth plugin) does not change the snapshot.
|
|
26
27
|
const generatedResolvers = new WeakSet();
|
|
28
|
+
// A caller that runs one operation per call, such as simfinity-mcp, can collect the pagination
|
|
29
|
+
// count through the root value instead of the shared context: a function stored under this key
|
|
30
|
+
// receives the count. The key is not exported; simfinity-mcp defines the same Symbol.for key.
|
|
31
|
+
const COUNT_SINK = Symbol.for('simfinity.countSink');
|
|
32
|
+
const countSinkOf = (value) => {
|
|
33
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return undefined;
|
|
34
|
+
const sink = value[COUNT_SINK];
|
|
35
|
+
return typeof sink === 'function' ? sink : undefined;
|
|
36
|
+
};
|
|
37
|
+
// The root value arrives as parent, which resolver wrappers that forward only (parent, args,
|
|
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.
|
|
46
|
+
const reportCount = (parent, info, context, count) => {
|
|
47
|
+
const sink = countSinkFor(parent, info);
|
|
48
|
+
if (sink) {
|
|
49
|
+
sink(count);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
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);
|
|
55
|
+
};
|
|
27
56
|
const applicationResolvedFields = new WeakMap();
|
|
28
57
|
const markGenerated = (resolve) => {
|
|
29
58
|
generatedResolvers.add(resolve);
|
|
@@ -181,6 +210,31 @@ const GraphQLJSON = new GraphQLScalarType({
|
|
|
181
210
|
},
|
|
182
211
|
});
|
|
183
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
|
+
|
|
184
238
|
// graphql-js wraps a value thrown while resolving a field (locatedError): the wrapper has the field
|
|
185
239
|
// path and repeats the thrown error's message. A thrown non-Error becomes a NonErrorThrown whose
|
|
186
240
|
// message prints the value.
|
|
@@ -318,6 +372,14 @@ export const createRuntime = (adapter) => {
|
|
|
318
372
|
const typesDict = { types: {} };
|
|
319
373
|
const waitingInputType = {};
|
|
320
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();
|
|
321
383
|
const registeredMutations = {};
|
|
322
384
|
const middlewares = [];
|
|
323
385
|
|
|
@@ -499,13 +561,24 @@ const QLTypeAggregationExpression = new GraphQLInputObjectType({
|
|
|
499
561
|
}),
|
|
500
562
|
});
|
|
501
563
|
|
|
502
|
-
|
|
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({
|
|
503
567
|
name: 'QLTypeAggregationResult',
|
|
504
568
|
fields: () => ({
|
|
505
|
-
groupId: { type:
|
|
506
|
-
facts: { type:
|
|
569
|
+
groupId: { type: valueType },
|
|
570
|
+
facts: { type: valueType },
|
|
507
571
|
}),
|
|
508
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
|
+
};
|
|
509
582
|
|
|
510
583
|
const isNonNullOfType = (fieldEntryType, graphQLType) => {
|
|
511
584
|
let isOfType = false;
|
|
@@ -516,6 +589,7 @@ const isNonNullOfType = (fieldEntryType, graphQLType) => {
|
|
|
516
589
|
};
|
|
517
590
|
|
|
518
591
|
const unwrapNonNull = (type) => (type instanceof GraphQLNonNull ? type.ofType : type);
|
|
592
|
+
const isAbstractOutputType = (type) => type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType;
|
|
519
593
|
|
|
520
594
|
const unwrapListAndNonNull = (type) => {
|
|
521
595
|
let unwrapped = type;
|
|
@@ -533,59 +607,69 @@ const wrapListInputType = (itemType, listShape, preserveOuterNonNull) => {
|
|
|
533
607
|
: listType;
|
|
534
608
|
};
|
|
535
609
|
|
|
610
|
+
const capitalize = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
611
|
+
|
|
536
612
|
/**
|
|
537
|
-
*
|
|
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.
|
|
538
618
|
* @param {string} inputNamePrefix - The prefix for the input type name.
|
|
539
|
-
* @param {
|
|
619
|
+
* @param {GraphQLObjectType} itemType - The collection's item type.
|
|
540
620
|
* @param {string} fieldToExclude - The name of the field to exclude.
|
|
541
|
-
* @
|
|
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.
|
|
542
624
|
*/
|
|
543
|
-
const createTypeWithExcludedField = (inputNamePrefix,
|
|
544
|
-
connectionFieldName = fieldToExclude) => {
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
),
|
|
559
646
|
});
|
|
647
|
+
if (!shared) excludedFieldInputTypes.set(name, { itemType, fieldToExclude, optionalFields, type });
|
|
648
|
+
return type;
|
|
560
649
|
};
|
|
561
650
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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(
|
|
568
657
|
inputNamePrefix,
|
|
569
|
-
|
|
658
|
+
itemType,
|
|
570
659
|
connection.graphqlFieldName || connection.declaredFieldName,
|
|
571
660
|
connection.declaredFieldName,
|
|
572
|
-
|
|
573
|
-
|
|
661
|
+
optionalFields,
|
|
662
|
+
)
|
|
663
|
+
: null;
|
|
664
|
+
const listOf = (type) => new GraphQLList(itemNonNull ? new GraphQLNonNull(type) : type);
|
|
574
665
|
|
|
575
666
|
return new GraphQLInputObjectType({
|
|
576
|
-
name
|
|
667
|
+
name,
|
|
577
668
|
fields: () => ({
|
|
578
|
-
added: {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
},
|
|
582
|
-
updated: {
|
|
583
|
-
type: new GraphQLList(itemNonNull
|
|
584
|
-
? new GraphQLNonNull(updateInputType) : updateInputType),
|
|
585
|
-
},
|
|
586
|
-
deleted: {
|
|
587
|
-
type: new GraphQLList(itemNonNull ? new GraphQLNonNull(GraphQLID) : GraphQLID),
|
|
588
|
-
},
|
|
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) },
|
|
589
673
|
}),
|
|
590
674
|
});
|
|
591
675
|
};
|
|
@@ -596,26 +680,19 @@ const graphQLListInputType = (dict, fieldEntry, fieldEntryName,
|
|
|
596
680
|
if (!listShape) return null;
|
|
597
681
|
const { itemType } = listShape;
|
|
598
682
|
|
|
599
|
-
if (itemType instanceof GraphQLObjectType
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const oneToMany = createOneToManyInputType(inputNamePrefix
|
|
604
|
-
|
|
605
|
-
typesDictForUpdate.types[itemType.name].inputType,
|
|
606
|
-
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),
|
|
607
689
|
listShape.itemNonNull);
|
|
608
690
|
return preserveOuterNonNull && listShape.outerNonNull
|
|
609
691
|
? new GraphQLNonNull(oneToMany)
|
|
610
692
|
: oneToMany;
|
|
611
693
|
}
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
return wrapListInputType(
|
|
615
|
-
dict.types[itemType.name].inputType,
|
|
616
|
-
listShape,
|
|
617
|
-
preserveOuterNonNull,
|
|
618
|
-
);
|
|
694
|
+
if (registration.inputType) {
|
|
695
|
+
return wrapListInputType(registration.inputType, listShape, preserveOuterNonNull);
|
|
619
696
|
}
|
|
620
697
|
} else if (itemType instanceof GraphQLScalarType || itemType instanceof GraphQLEnumType) {
|
|
621
698
|
return wrapListInputType(itemType, listShape, preserveOuterNonNull);
|
|
@@ -623,6 +700,38 @@ const graphQLListInputType = (dict, fieldEntry, fieldEntryName,
|
|
|
623
700
|
return null;
|
|
624
701
|
};
|
|
625
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.
|
|
626
735
|
const buildInputType = (gqltype) => {
|
|
627
736
|
const argTypes = gqltype.getFields();
|
|
628
737
|
|
|
@@ -630,6 +739,7 @@ const buildInputType = (gqltype) => {
|
|
|
630
739
|
const fieldsArgForUpdate = {};
|
|
631
740
|
|
|
632
741
|
const selfReferenceCollections = {};
|
|
742
|
+
const unresolvedFields = [];
|
|
633
743
|
|
|
634
744
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
635
745
|
const fieldArg = {};
|
|
@@ -667,11 +777,19 @@ const buildInputType = (gqltype) => {
|
|
|
667
777
|
fieldArg.type = typesDict.types[fieldEntryNameValue].inputType;
|
|
668
778
|
fieldArgForUpdate.type = typesDictForUpdate.types[fieldEntryNameValue].inputType;
|
|
669
779
|
} else {
|
|
670
|
-
|
|
780
|
+
unresolvedFields.push({ field: fieldEntryName, waitsFor: fieldEntryNameValue });
|
|
671
781
|
}
|
|
672
|
-
} 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.
|
|
673
785
|
console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
|
|
674
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.');
|
|
675
793
|
} else if (getListShape(fieldEntry.type)) {
|
|
676
794
|
const listShape = getListShape(fieldEntry.type);
|
|
677
795
|
if (listShape.itemType === gqltype) {
|
|
@@ -687,7 +805,11 @@ const buildInputType = (gqltype) => {
|
|
|
687
805
|
fieldArg.type = listInputTypeForAdd;
|
|
688
806
|
fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
689
807
|
} else {
|
|
690
|
-
|
|
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 });
|
|
691
813
|
}
|
|
692
814
|
}
|
|
693
815
|
}
|
|
@@ -709,94 +831,165 @@ const buildInputType = (gqltype) => {
|
|
|
709
831
|
}
|
|
710
832
|
}
|
|
711
833
|
|
|
712
|
-
|
|
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({
|
|
713
851
|
name: `${gqltype.name}Input`,
|
|
714
852
|
description: gqltype.description,
|
|
715
853
|
fields: fieldsArgs,
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
const inputTypeBodyForUpdate = {
|
|
854
|
+
});
|
|
855
|
+
const inputTypeForUpdate = new GraphQLInputObjectType({
|
|
719
856
|
name: `${gqltype.name}InputForUpdate`,
|
|
720
857
|
description: gqltype.description,
|
|
721
858
|
fields: fieldsArgForUpdate,
|
|
722
|
-
};
|
|
723
|
-
|
|
724
|
-
const inputTypeForAdd = new GraphQLInputObjectType(inputTypeBody);
|
|
725
|
-
const inputTypeForUpdate = new GraphQLInputObjectType(inputTypeBodyForUpdate);
|
|
726
|
-
|
|
727
|
-
const inputTypeForAddFields = inputTypeForAdd._fields();
|
|
728
|
-
|
|
729
|
-
Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
|
|
730
|
-
if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
|
|
731
|
-
const fieldEntry = selfReferenceCollections[fieldEntryName];
|
|
732
|
-
const listShape = getListShape(fieldEntry.type);
|
|
733
|
-
const oneToMany = createOneToManyInputType('A', fieldEntryName,
|
|
734
|
-
inputTypeForAdd, inputTypeForUpdate,
|
|
735
|
-
normalizeConnectionField(
|
|
736
|
-
gqltype,
|
|
737
|
-
fieldEntry.extensions?.relation?.connectionField,
|
|
738
|
-
), listShape.itemNonNull);
|
|
739
|
-
inputTypeForAddFields[fieldEntryName] = {
|
|
740
|
-
type: listShape.outerNonNull ? new GraphQLNonNull(oneToMany) : oneToMany,
|
|
741
|
-
name: fieldEntryName,
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
859
|
});
|
|
745
860
|
|
|
746
|
-
inputTypeForAdd._fields = () => inputTypeForAddFields;
|
|
747
|
-
|
|
748
|
-
const inputTypeForUpdateFields = inputTypeForUpdate._fields();
|
|
749
|
-
|
|
750
|
-
Object.keys(selfReferenceCollections).forEach((fieldEntryName) => {
|
|
751
|
-
if (Object.prototype.hasOwnProperty.call(selfReferenceCollections, fieldEntryName)) {
|
|
752
|
-
const fieldEntry = selfReferenceCollections[fieldEntryName];
|
|
753
|
-
const listShape = getListShape(fieldEntry.type);
|
|
754
|
-
inputTypeForUpdateFields[fieldEntryName] = {
|
|
755
|
-
type: createOneToManyInputType('U', fieldEntryName,
|
|
756
|
-
inputTypeForAdd, inputTypeForUpdate,
|
|
757
|
-
normalizeConnectionField(
|
|
758
|
-
gqltype,
|
|
759
|
-
fieldEntry.extensions?.relation?.connectionField,
|
|
760
|
-
), listShape.itemNonNull),
|
|
761
|
-
name: fieldEntryName,
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
inputTypeForUpdate._fields = () => inputTypeForUpdateFields;
|
|
767
|
-
|
|
768
861
|
return { inputTypeBody: inputTypeForAdd, inputTypeBodyForUpdate: inputTypeForUpdate };
|
|
769
862
|
};
|
|
770
863
|
|
|
771
864
|
const getInputType = (type) => typesDict.types[type.name].inputType;
|
|
772
865
|
|
|
773
|
-
|
|
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);
|
|
774
967
|
let pending = waitingForInputType;
|
|
775
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 = {};
|
|
776
971
|
|
|
777
972
|
while (Object.keys(pending).length > 0) {
|
|
778
973
|
const currentCount = Object.keys(pending).length;
|
|
779
974
|
if (currentCount >= previousPendingCount) {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
`Could not build input types for: ${unresolved}. Check for circular or misconfigured relations.`,
|
|
783
|
-
'INPUT_TYPE_UNRESOLVED',
|
|
784
|
-
500,
|
|
785
|
-
);
|
|
975
|
+
throw new SimfinityError(unresolvedInputTypesMessage(Object.keys(pending), unresolvedFields),
|
|
976
|
+
'INPUT_TYPE_UNRESOLVED', 500);
|
|
786
977
|
}
|
|
787
978
|
previousPendingCount = currentCount;
|
|
788
979
|
|
|
789
980
|
const stillWaiting = {};
|
|
981
|
+
unresolvedFields = {};
|
|
790
982
|
for (const [key, value] of Object.entries(pending)) {
|
|
791
983
|
const { gqltype } = value;
|
|
792
984
|
if (typesDict.types[gqltype.name].inputType) continue;
|
|
793
985
|
|
|
794
986
|
const result = buildInputType(gqltype);
|
|
795
|
-
if (result
|
|
987
|
+
if (result.inputTypeBody && result.inputTypeBodyForUpdate) {
|
|
796
988
|
typesDict.types[gqltype.name].inputType = result.inputTypeBody;
|
|
797
989
|
typesDictForUpdate.types[gqltype.name].inputType = result.inputTypeBodyForUpdate;
|
|
798
990
|
} else {
|
|
799
991
|
stillWaiting[key] = value;
|
|
992
|
+
unresolvedFields[gqltype.name] = result.unresolvedFields;
|
|
800
993
|
}
|
|
801
994
|
}
|
|
802
995
|
pending = stillWaiting;
|
|
@@ -1050,7 +1243,11 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
1050
1243
|
if (!result) throw new SimfinityError(`${gqltype.name} ${objectId} is not valid`, 'NOT_VALID_ID', 404);
|
|
1051
1244
|
|
|
1052
1245
|
if (materializedModel.collectionFields) {
|
|
1053
|
-
|
|
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);
|
|
1054
1251
|
}
|
|
1055
1252
|
|
|
1056
1253
|
if (controller && controller.onUpdated) {
|
|
@@ -1212,7 +1409,9 @@ const executeOperation = (Model, gqltype, controller, args, operation, actionFie
|
|
|
1212
1409
|
);
|
|
1213
1410
|
};
|
|
1214
1411
|
|
|
1215
|
-
|
|
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,
|
|
1216
1415
|
collectionFieldsList, operationType, context) => {
|
|
1217
1416
|
const argTypes = gqltype.getFields();
|
|
1218
1417
|
const collectionGQLType = unwrapListAndNonNull(argTypes[collectionField].type);
|
|
@@ -1226,7 +1425,7 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
1226
1425
|
const type = operationType === operations.UPDATE
|
|
1227
1426
|
? typesDictForUpdate.types[collectionGQLType.name] : typesDict.types[collectionGQLType.name];
|
|
1228
1427
|
const linkToParent = (item) => {
|
|
1229
|
-
item[connection.storageFieldName] =
|
|
1428
|
+
item[connection.storageFieldName] = parentKey;
|
|
1230
1429
|
if (item.$unset) delete item.$unset[connection.storageFieldName];
|
|
1231
1430
|
if (item.$set) delete item.$set[connection.storageFieldName];
|
|
1232
1431
|
};
|
|
@@ -1242,8 +1441,8 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
1242
1441
|
if (!child) {
|
|
1243
1442
|
throw new SimfinityError(`${collectionGQLType.name} ${id} is not valid`, 'NOT_VALID_ID', 404);
|
|
1244
1443
|
}
|
|
1245
|
-
if (String(child[connection.storageFieldName]) !== String(
|
|
1246
|
-
throw new
|
|
1444
|
+
if (String(child[connection.storageFieldName]) !== String(parentKey)) {
|
|
1445
|
+
throw new ForbiddenError('Child does not belong to this parent');
|
|
1247
1446
|
}
|
|
1248
1447
|
}
|
|
1249
1448
|
|
|
@@ -1508,12 +1707,15 @@ const resolveById = async (type, args, context, requiredId) => {
|
|
|
1508
1707
|
return results[0] || null;
|
|
1509
1708
|
};
|
|
1510
1709
|
|
|
1710
|
+
// Every generated mutation field carries extensions.simfinityMutation with its operation (and, for
|
|
1711
|
+
// a state-machine action, the action and its from/to states), so tools such as the MCP package can
|
|
1712
|
+
// classify it without guessing from its name. Extensions do not appear in SDL or introspection.
|
|
1511
1713
|
const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
|
|
1512
1714
|
const rootQueryArgs = {};
|
|
1513
1715
|
rootQueryArgs.name = name;
|
|
1514
1716
|
rootQueryArgs.fields = {};
|
|
1515
1717
|
|
|
1516
|
-
buildPendingInputTypes(waitingInputType);
|
|
1718
|
+
buildPendingInputTypes(waitingInputType, includedMutationTypes);
|
|
1517
1719
|
|
|
1518
1720
|
for (const type of Object.values(typesDict.types)) {
|
|
1519
1721
|
if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
@@ -1524,6 +1726,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1524
1726
|
type: type.gqltype,
|
|
1525
1727
|
description: 'add',
|
|
1526
1728
|
args: argsObject,
|
|
1729
|
+
extensions: { simfinityMutation: { typeName: type.gqltype.name, operation: operations.SAVE } },
|
|
1527
1730
|
async resolve(parent, args, context) {
|
|
1528
1731
|
const params = {
|
|
1529
1732
|
type,
|
|
@@ -1541,6 +1744,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1541
1744
|
type: type.gqltype,
|
|
1542
1745
|
description: 'delete',
|
|
1543
1746
|
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
|
|
1747
|
+
extensions: { simfinityMutation: { typeName: type.gqltype.name, operation: operations.DELETE } },
|
|
1544
1748
|
async resolve(parent, args, context) {
|
|
1545
1749
|
const params = {
|
|
1546
1750
|
type,
|
|
@@ -1566,6 +1770,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1566
1770
|
type: type.gqltype,
|
|
1567
1771
|
description: 'update',
|
|
1568
1772
|
args: argsObject,
|
|
1773
|
+
extensions: { simfinityMutation: { typeName: type.gqltype.name, operation: operations.UPDATE } },
|
|
1569
1774
|
async resolve(parent, args, context) {
|
|
1570
1775
|
const params = {
|
|
1571
1776
|
type,
|
|
@@ -1586,6 +1791,15 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1586
1791
|
type: type.gqltype,
|
|
1587
1792
|
description: actionField.description,
|
|
1588
1793
|
args: argsObject,
|
|
1794
|
+
extensions: {
|
|
1795
|
+
simfinityMutation: {
|
|
1796
|
+
typeName: type.gqltype.name,
|
|
1797
|
+
operation: operations.STATE_CHANGED,
|
|
1798
|
+
action: actionName,
|
|
1799
|
+
from: actionField.from && actionField.from.name,
|
|
1800
|
+
to: actionField.to && actionField.to.name,
|
|
1801
|
+
},
|
|
1802
|
+
},
|
|
1589
1803
|
async resolve(parent, args, context) {
|
|
1590
1804
|
const params = {
|
|
1591
1805
|
type,
|
|
@@ -1617,6 +1831,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1617
1831
|
type: registeredMutation.outputModel,
|
|
1618
1832
|
description: registeredMutation.description,
|
|
1619
1833
|
args: argsObject,
|
|
1834
|
+
extensions: { simfinityMutation: { operation: operations.CUSTOM_MUTATION } },
|
|
1620
1835
|
async resolve(parent, args, context) {
|
|
1621
1836
|
const params = {
|
|
1622
1837
|
args,
|
|
@@ -1635,7 +1850,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1635
1850
|
return new GraphQLObjectType(rootQueryArgs);
|
|
1636
1851
|
};
|
|
1637
1852
|
|
|
1638
|
-
const buildRootQuery = (name, includedTypes) => {
|
|
1853
|
+
const buildRootQuery = (name, includedTypes, aggregationResultType = QLTypeAggregationResult) => {
|
|
1639
1854
|
const rootQueryArgs = {};
|
|
1640
1855
|
rootQueryArgs.name = name;
|
|
1641
1856
|
rootQueryArgs.fields = {};
|
|
@@ -1657,8 +1872,10 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1657
1872
|
rootQueryArgs.fields[type.listEntitiesEndpointName] = {
|
|
1658
1873
|
type: new GraphQLList(type.gqltype),
|
|
1659
1874
|
args: argsObject,
|
|
1660
|
-
|
|
1661
|
-
|
|
1875
|
+
// countSink: this resolver reports pagination counts through reportCount, so a caller
|
|
1876
|
+
// that supplies a root-value sink can pass its context unchanged.
|
|
1877
|
+
extensions: { simfinityQuery: { typeName: type.gqltype.name, operation: 'find', countSink: true } },
|
|
1878
|
+
async resolve(parent, args, context, info) {
|
|
1662
1879
|
const joinedScopes = inspectClientPaths(type.gqltype, args, 'find');
|
|
1663
1880
|
const params = {
|
|
1664
1881
|
type,
|
|
@@ -1670,7 +1887,8 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1670
1887
|
await executeScope(params);
|
|
1671
1888
|
await applyJoinedScopes(joinedScopes, params);
|
|
1672
1889
|
const queryArgs = params.args;
|
|
1673
|
-
const wantsCount = !!(queryArgs.pagination && queryArgs.pagination.count)
|
|
1890
|
+
const wantsCount = !!(queryArgs.pagination && queryArgs.pagination.count)
|
|
1891
|
+
&& canReportCount(parent, info, context);
|
|
1674
1892
|
|
|
1675
1893
|
const dataPromise = adapter.find(type.model, type.gqltype, queryArgs, null);
|
|
1676
1894
|
const countPromise = wantsCount
|
|
@@ -1678,8 +1896,10 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1678
1896
|
: null;
|
|
1679
1897
|
|
|
1680
1898
|
const [result, resultCount] = await Promise.all([dataPromise, countPromise]);
|
|
1681
|
-
if (wantsCount) {
|
|
1682
|
-
context
|
|
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.');
|
|
1683
1903
|
}
|
|
1684
1904
|
return result;
|
|
1685
1905
|
},
|
|
@@ -1696,7 +1916,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1696
1916
|
};
|
|
1697
1917
|
|
|
1698
1918
|
rootQueryArgs.fields[`${type.listEntitiesEndpointName}_aggregate`] = {
|
|
1699
|
-
type: new GraphQLList(
|
|
1919
|
+
type: new GraphQLList(aggregationResultType),
|
|
1700
1920
|
args: aggregateArgsObject,
|
|
1701
1921
|
extensions: { simfinityQuery: { typeName: type.gqltype.name, operation: 'aggregate' } },
|
|
1702
1922
|
async resolve(parent, args, context) {
|
|
@@ -1876,17 +2096,21 @@ const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomM
|
|
|
1876
2096
|
const objectMemberTypes = new Set();
|
|
1877
2097
|
Object.values(typesDict.types).forEach(({ gqltype }) => installObjectMemberResolvers(gqltype, objectMemberTypes));
|
|
1878
2098
|
|
|
1879
|
-
|
|
2099
|
+
let query = buildRootQuery('RootQueryType', includedQueryTypes);
|
|
1880
2100
|
Object.values(typesDict.types).forEach(({ gqltype }) => {
|
|
1881
2101
|
if (gqltype && !applicationResolvedFields.has(gqltype)) {
|
|
1882
2102
|
applicationResolvedFields.set(gqltype, listApplicationResolvedFields(gqltype));
|
|
1883
2103
|
}
|
|
1884
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
|
+
}
|
|
1885
2112
|
|
|
1886
|
-
const schema = new GraphQLSchema({
|
|
1887
|
-
query,
|
|
1888
|
-
mutation: buildMutation('Mutation', includedMutationTypes, includedCustomMutations),
|
|
1889
|
-
});
|
|
2113
|
+
const schema = new GraphQLSchema({ query, mutation });
|
|
1890
2114
|
// Reserve reachable types whose relation fields still have no resolver, such as an unregistered
|
|
1891
2115
|
// custom mutation result, so another runtime cannot later generate resolvers this schema executes.
|
|
1892
2116
|
schemaTypes.forEach((type) => {
|
|
@@ -2009,6 +2233,14 @@ const autoGenerateResolvers = (gqltype) => {
|
|
|
2009
2233
|
}
|
|
2010
2234
|
};
|
|
2011
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
|
+
|
|
2012
2244
|
const connect = (model, gqltype, simpleEntityEndpointName,
|
|
2013
2245
|
listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
|
|
2014
2246
|
const registration = {
|
|
@@ -2025,6 +2257,7 @@ const connect = (model, gqltype, simpleEntityEndpointName,
|
|
|
2025
2257
|
validateScope(gqltype);
|
|
2026
2258
|
if (adapter.validateRegistration) adapter.validateRegistration(registration);
|
|
2027
2259
|
|
|
2260
|
+
invalidateCollectionInputs(gqltype);
|
|
2028
2261
|
waitingInputType[gqltype.name] = {
|
|
2029
2262
|
model,
|
|
2030
2263
|
gqltype,
|
|
@@ -2054,6 +2287,7 @@ const addNoEndpointType = (gqltype) => {
|
|
|
2054
2287
|
validateScope(gqltype);
|
|
2055
2288
|
if (adapter.validateRegistration) adapter.validateRegistration(registration);
|
|
2056
2289
|
|
|
2290
|
+
invalidateCollectionInputs(gqltype);
|
|
2057
2291
|
waitingInputType[gqltype.name] = { gqltype };
|
|
2058
2292
|
typesDict.types[gqltype.name] = registration;
|
|
2059
2293
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
@@ -2077,27 +2311,31 @@ const createArgsForQuery = (gqltype, omit = null) => {
|
|
|
2077
2311
|
|
|
2078
2312
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
2079
2313
|
if (fieldEntryName === omit) continue;
|
|
2080
|
-
|
|
2314
|
+
let filterType = null;
|
|
2081
2315
|
|
|
2082
2316
|
if (fieldEntry.type instanceof GraphQLScalarType
|
|
2083
2317
|
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
2084
2318
|
|| fieldEntry.type instanceof GraphQLEnumType
|
|
2085
2319
|
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
2086
|
-
|
|
2320
|
+
filterType = QLFilter;
|
|
2087
2321
|
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
2088
2322
|
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
2089
|
-
|
|
2323
|
+
filterType = QLTypeFilterExpression;
|
|
2090
2324
|
} else if (getListShape(fieldEntry.type)) {
|
|
2091
2325
|
const listOfType = getListShape(fieldEntry.type).itemType;
|
|
2092
2326
|
if (listOfType instanceof GraphQLScalarType
|
|
2093
2327
|
|| isNonNullOfType(listOfType, GraphQLScalarType)
|
|
2094
2328
|
|| listOfType instanceof GraphQLEnumType
|
|
2095
2329
|
|| isNonNullOfType(listOfType, GraphQLEnumType)) {
|
|
2096
|
-
|
|
2330
|
+
filterType = QLFilter;
|
|
2097
2331
|
} else {
|
|
2098
|
-
|
|
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;
|
|
2099
2335
|
}
|
|
2100
2336
|
}
|
|
2337
|
+
// A single interface or union field has no generated filter, so it gets no argument.
|
|
2338
|
+
if (filterType) argsObject[fieldEntryName] = { type: filterType };
|
|
2101
2339
|
}
|
|
2102
2340
|
|
|
2103
2341
|
argsObject.pagination = {};
|