@simtlix/simfinity-core 3.5.7 → 3.5.9
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 +5 -3
- package/package.json +54 -8
- package/src/const/QLValue.js +11 -3
- package/src/errors/internal-server.error.js +5 -1
- package/src/index.js +3 -0
- package/src/introspection.js +42 -8
- package/src/runtime.js +691 -177
- package/src/scalars.js +53 -33
- package/src/validators.js +159 -47
- package/types/auth/errors.d.ts +12 -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 +108 -15
- package/types/plugins.d.ts +14 -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';
|
|
@@ -18,7 +19,9 @@ import { isThenable } from './auth/thenable.js';
|
|
|
18
19
|
import {
|
|
19
20
|
getFieldStorageName, getListShape, markStoredIdentity, normalizeConnectionField,
|
|
20
21
|
} from './relation-storage.js';
|
|
21
|
-
import
|
|
22
|
+
import {
|
|
23
|
+
fieldMetadataNamesFixed, fieldMetadataTypes, fixFieldMetadataNames, renameFieldMetadataTypes,
|
|
24
|
+
} from './introspection.js';
|
|
22
25
|
|
|
23
26
|
// Resolvers Simfinity generates. Any other resolver on a registered type when its schema is first
|
|
24
27
|
// created is application code, such as a masking resolver, recorded once per type so later
|
|
@@ -34,15 +37,23 @@ const countSinkOf = (value) => {
|
|
|
34
37
|
return typeof sink === 'function' ? sink : undefined;
|
|
35
38
|
};
|
|
36
39
|
// 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
|
-
|
|
40
|
+
// context) still pass on; info.rootValue is the fallback.
|
|
41
|
+
const countSinkFor = (parent, info) => countSinkOf(parent) || (info ? countSinkOf(info.rootValue) : undefined);
|
|
42
|
+
// Without a sink the count goes to context.count, where Envelop/Apollo/Yoga count plugins read it.
|
|
43
|
+
// With neither, such as a graphql() call without contextValue, nothing could read a count, so the
|
|
44
|
+
// count query is skipped.
|
|
45
|
+
const canReportCount = (parent, info, context) => countSinkFor(parent, info) !== undefined
|
|
46
|
+
|| (context !== null && (typeof context === 'object' || typeof context === 'function'));
|
|
47
|
+
// Returns whether the count was stored.
|
|
39
48
|
const reportCount = (parent, info, context, count) => {
|
|
40
|
-
const sink =
|
|
49
|
+
const sink = countSinkFor(parent, info);
|
|
41
50
|
if (sink) {
|
|
42
51
|
sink(count);
|
|
43
|
-
return;
|
|
52
|
+
return true;
|
|
44
53
|
}
|
|
45
|
-
|
|
54
|
+
// Reflect.set returns false for a frozen, sealed or read-only context, where an assignment would
|
|
55
|
+
// throw after both queries ran.
|
|
56
|
+
return Reflect.set(context, 'count', count);
|
|
46
57
|
};
|
|
47
58
|
const applicationResolvedFields = new WeakMap();
|
|
48
59
|
const markGenerated = (resolve) => {
|
|
@@ -201,6 +212,94 @@ const GraphQLJSON = new GraphQLScalarType({
|
|
|
201
212
|
},
|
|
202
213
|
});
|
|
203
214
|
|
|
215
|
+
// The type called `name` that a schema built from these roots would contain, other than through
|
|
216
|
+
// `skip`, following the references GraphQLSchema collects: fields, arguments, interfaces, union
|
|
217
|
+
// members and input fields. With `inputFields` false, input objects are matched by their own name
|
|
218
|
+
// only, and their fields are never read.
|
|
219
|
+
const findSchemaType = (roots, name, skip, { inputFields = true } = {}) => {
|
|
220
|
+
const visited = new Set();
|
|
221
|
+
const pending = [...roots];
|
|
222
|
+
while (pending.length) {
|
|
223
|
+
const type = getNamedType(pending.pop());
|
|
224
|
+
if (!type || type === skip || visited.has(type)) continue;
|
|
225
|
+
visited.add(type);
|
|
226
|
+
if (type.name === name) return type;
|
|
227
|
+
if (type instanceof GraphQLUnionType) pending.push(...type.getTypes());
|
|
228
|
+
if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) {
|
|
229
|
+
pending.push(...type.getInterfaces());
|
|
230
|
+
for (const field of Object.values(type.getFields())) {
|
|
231
|
+
pending.push(field.type, ...field.args.map((arg) => arg.type));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (inputFields && type instanceof GraphQLInputObjectType) {
|
|
235
|
+
for (const field of Object.values(type.getFields())) pending.push(field.type);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Every schema contains Simfinity's field metadata types, the types of `__Field.extensions`, named
|
|
242
|
+
// FieldExtensionsType and RelationType unless a clash renamed them. Returns the type of a schema built
|
|
243
|
+
// from these roots that has the name of one of them, or null (#162).
|
|
244
|
+
const fieldMetadataClash = (roots, options) => fieldMetadataTypes()
|
|
245
|
+
.map((own) => findSchemaType(roots, own.name, own, options)).find(Boolean) || null;
|
|
246
|
+
|
|
247
|
+
// The first schema that contains the metadata types fixes their names for the process, so a later
|
|
248
|
+
// schema with a type of the same name is rejected.
|
|
249
|
+
const reservedTypeNameError = (clash) => new SimfinityError(
|
|
250
|
+
`Type ${clash.name} has the name of a type that Simfinity adds to every schema for __Field.extensions, `
|
|
251
|
+
+ 'and a schema created earlier in this process already contains that type under this name. Rename '
|
|
252
|
+
+ `${clash.name}, or create this schema first, so that Simfinity gives its types other names, such as `
|
|
253
|
+
+ 'SimfinityFieldExtensionsType and SimfinityRelationType',
|
|
254
|
+
'RESERVED_TYPE_NAME',
|
|
255
|
+
500,
|
|
256
|
+
);
|
|
257
|
+
// The check before anything is built reads only what the output type check before it reads: output
|
|
258
|
+
// types, their fields, arguments, interfaces and union members. Input objects, a registered
|
|
259
|
+
// mutation's input among them, are checked by their own name, because their fields function may read
|
|
260
|
+
// generated inputs (getInputType) that do not exist yet, and graphql-js keeps what it returns. A clash
|
|
261
|
+
// inside an input's fields is found when the schema is constructed.
|
|
262
|
+
const assertFieldMetadataNamesFree = (roots) => {
|
|
263
|
+
const clash = fieldMetadataNamesFixed() && fieldMetadataClash(roots, { inputFields: false });
|
|
264
|
+
if (clash) throw reservedTypeNameError(clash);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Constructs the schema of these roots. When one of its types has the name of a field metadata type,
|
|
268
|
+
// those are renamed SimfinityFieldExtensionsType and SimfinityRelationType, with a number appended
|
|
269
|
+
// when the schema also has those names, in every schema the process constructs from then on. A
|
|
270
|
+
// construction that fails puts the previous metadata types back, so only a schema that exists fixes
|
|
271
|
+
// the names.
|
|
272
|
+
const constructSchema = (query, mutation) => {
|
|
273
|
+
const roots = [query, mutation];
|
|
274
|
+
const clash = fieldMetadataClash(roots);
|
|
275
|
+
if (clash && fieldMetadataNamesFixed()) throw reservedTypeNameError(clash);
|
|
276
|
+
let names = null;
|
|
277
|
+
if (clash) {
|
|
278
|
+
const taken = (name) => findSchemaType(roots, name) !== null;
|
|
279
|
+
let suffix = '';
|
|
280
|
+
for (let n = 2; taken(`SimfinityFieldExtensionsType${suffix}`) || taken(`SimfinityRelationType${suffix}`); n += 1) {
|
|
281
|
+
suffix = String(n);
|
|
282
|
+
}
|
|
283
|
+
names = [`SimfinityFieldExtensionsType${suffix}`, `SimfinityRelationType${suffix}`];
|
|
284
|
+
}
|
|
285
|
+
const restore = names ? renameFieldMetadataTypes(...names) : null;
|
|
286
|
+
let schema;
|
|
287
|
+
try {
|
|
288
|
+
schema = new GraphQLSchema({ query, mutation });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (restore) restore();
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
fixFieldMetadataNames();
|
|
294
|
+
if (names) {
|
|
295
|
+
console.warn(`Configuration issue: the schema has a type named ${clash.name}, the name of a type that `
|
|
296
|
+
+ `Simfinity adds to every schema for __Field.extensions, so Simfinity names its types ${names[0]} and `
|
|
297
|
+
+ `${names[1]} in every schema this process creates from now on. Use these names in named fragments `
|
|
298
|
+
+ 'and permission maps.');
|
|
299
|
+
}
|
|
300
|
+
return schema;
|
|
301
|
+
};
|
|
302
|
+
|
|
204
303
|
// graphql-js wraps a value thrown while resolving a field (locatedError): the wrapper has the field
|
|
205
304
|
// path and repeats the thrown error's message. A thrown non-Error becomes a NonErrorThrown whose
|
|
206
305
|
// message prints the value.
|
|
@@ -338,6 +437,14 @@ export const createRuntime = (adapter) => {
|
|
|
338
437
|
const typesDict = { types: {} };
|
|
339
438
|
const waitingInputType = {};
|
|
340
439
|
const typesDictForUpdate = { types: {} };
|
|
440
|
+
// `added` item inputs of referenced collections by generated name, so collections that reach the
|
|
441
|
+
// same child through the same connectionField share one input type.
|
|
442
|
+
const excludedFieldInputTypes = new Map();
|
|
443
|
+
// Self-referencing collection field name -> the types whose collection inputs were built with the
|
|
444
|
+
// unqualified names OneToManyA<field> and OneToManyU<field>; type -> its self-referencing
|
|
445
|
+
// collections whose inputs are named after the type instead (see claimSelfCollectionNames).
|
|
446
|
+
const selfCollectionNameHolders = new Map();
|
|
447
|
+
const qualifiedSelfCollections = new Map();
|
|
341
448
|
const registeredMutations = {};
|
|
342
449
|
const middlewares = [];
|
|
343
450
|
|
|
@@ -519,13 +626,24 @@ const QLTypeAggregationExpression = new GraphQLInputObjectType({
|
|
|
519
626
|
}),
|
|
520
627
|
});
|
|
521
628
|
|
|
522
|
-
|
|
629
|
+
// Aggregation results use Simfinity's JSON scalar, or the application's own scalar named JSON when
|
|
630
|
+
// the schema has one, such as GraphQLJSON from graphql-scalars, so the two do not collide.
|
|
631
|
+
const createAggregationResultType = (valueType) => new GraphQLObjectType({
|
|
523
632
|
name: 'QLTypeAggregationResult',
|
|
524
633
|
fields: () => ({
|
|
525
|
-
groupId: { type:
|
|
526
|
-
facts: { type:
|
|
634
|
+
groupId: { type: valueType },
|
|
635
|
+
facts: { type: valueType },
|
|
527
636
|
}),
|
|
528
637
|
});
|
|
638
|
+
const QLTypeAggregationResult = createAggregationResultType(GraphQLJSON);
|
|
639
|
+
const applicationJSONResultTypes = new WeakMap();
|
|
640
|
+
const aggregationResultTypeFor = (valueType) => {
|
|
641
|
+
if (valueType === GraphQLJSON) return QLTypeAggregationResult;
|
|
642
|
+
if (!applicationJSONResultTypes.has(valueType)) {
|
|
643
|
+
applicationJSONResultTypes.set(valueType, createAggregationResultType(valueType));
|
|
644
|
+
}
|
|
645
|
+
return applicationJSONResultTypes.get(valueType);
|
|
646
|
+
};
|
|
529
647
|
|
|
530
648
|
const isNonNullOfType = (fieldEntryType, graphQLType) => {
|
|
531
649
|
let isOfType = false;
|
|
@@ -536,6 +654,7 @@ const isNonNullOfType = (fieldEntryType, graphQLType) => {
|
|
|
536
654
|
};
|
|
537
655
|
|
|
538
656
|
const unwrapNonNull = (type) => (type instanceof GraphQLNonNull ? type.ofType : type);
|
|
657
|
+
const isAbstractOutputType = (type) => type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType;
|
|
539
658
|
|
|
540
659
|
const unwrapListAndNonNull = (type) => {
|
|
541
660
|
let unwrapped = type;
|
|
@@ -553,89 +672,123 @@ const wrapListInputType = (itemType, listShape, preserveOuterNonNull) => {
|
|
|
553
672
|
: listType;
|
|
554
673
|
};
|
|
555
674
|
|
|
675
|
+
const capitalize = (name) => name.charAt(0).toUpperCase() + name.slice(1);
|
|
676
|
+
|
|
677
|
+
// A GraphQL input object needs at least one field, so a generated input that would have none is
|
|
678
|
+
// left out, together with the operations, fields and mutations that would take it (#161).
|
|
679
|
+
const hasInputFields = (inputType) => Object.keys(inputType.getFields()).length > 0;
|
|
680
|
+
|
|
556
681
|
/**
|
|
557
|
-
*
|
|
682
|
+
* The `added` item input of a referenced collection: the item type's create input without the
|
|
683
|
+
* back-reference that the parent supplies. Its fields are read from the item's create input when
|
|
684
|
+
* the schema is built, so the item's input may be built after this type, as in cycles of
|
|
685
|
+
* collections. `optionalFields` lose their outer non-null wrapper. Collections that reach the same
|
|
686
|
+
* item through the same connectionField with the same prefix share one type.
|
|
558
687
|
* @param {string} inputNamePrefix - The prefix for the input type name.
|
|
559
|
-
* @param {
|
|
688
|
+
* @param {GraphQLObjectType} itemType - The collection's item type.
|
|
560
689
|
* @param {string} fieldToExclude - The name of the field to exclude.
|
|
561
|
-
* @
|
|
690
|
+
* @param {string} [connectionFieldName] - The declared connectionField, which names the type.
|
|
691
|
+
* @param {string[]} [optionalFields] - Fields of the item's create input that become optional.
|
|
692
|
+
* @returns {GraphQLInputObjectType} An input type without the specified field.
|
|
562
693
|
*/
|
|
563
|
-
const createTypeWithExcludedField = (inputNamePrefix,
|
|
564
|
-
connectionFieldName = fieldToExclude) => {
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
694
|
+
const createTypeWithExcludedField = (inputNamePrefix, itemType, fieldToExclude,
|
|
695
|
+
connectionFieldName = fieldToExclude, optionalFields = []) => {
|
|
696
|
+
// The item's create input is `${itemType.name}Input`, so the name is the one it always had.
|
|
697
|
+
const name = `${inputNamePrefix}${itemType.name}InputFor${capitalize(connectionFieldName)}`;
|
|
698
|
+
const shared = excludedFieldInputTypes.get(name);
|
|
699
|
+
if (shared && shared.itemType === itemType && shared.fieldToExclude === fieldToExclude
|
|
700
|
+
&& shared.optionalFields.join() === optionalFields.join()) {
|
|
701
|
+
return shared.type;
|
|
702
|
+
}
|
|
703
|
+
// Any other type with this name is a new one, so a real name clash still fails as before.
|
|
704
|
+
const type = new GraphQLInputObjectType({
|
|
705
|
+
name,
|
|
706
|
+
fields: () => Object.fromEntries(
|
|
707
|
+
Object.entries(getInputType(itemType).getFields())
|
|
708
|
+
.filter(([fieldName]) => fieldName !== fieldToExclude)
|
|
709
|
+
.map(([fieldName, field]) => [fieldName, {
|
|
710
|
+
type: optionalFields.includes(fieldName) ? unwrapNonNull(field.type) : field.type,
|
|
711
|
+
description: field.description,
|
|
712
|
+
defaultValue: field.defaultValue,
|
|
713
|
+
}]),
|
|
714
|
+
),
|
|
579
715
|
});
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
716
|
+
if (!shared) excludedFieldInputTypes.set(name, { itemType, fieldToExclude, optionalFields, type });
|
|
717
|
+
return type;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// A referenced collection's operation input. The item inputs are read when the schema is built, so
|
|
721
|
+
// the item type's inputs need not exist yet. `added` or `updated` is left out when its item input
|
|
722
|
+
// would have no fields, such as `added` for a child whose only writable field is the back-reference;
|
|
723
|
+
// `deleted` always remains. `owner` names the collection, as `Type.field`, in the warning.
|
|
724
|
+
const createOneToManyInputType = (name, inputNamePrefix, itemType, connection,
|
|
725
|
+
itemNonNull = false, optionalFields = [], owner = name) => {
|
|
726
|
+
const inputTypeForAdd = connection.declaredFieldName
|
|
727
|
+
? createTypeWithExcludedField(
|
|
588
728
|
inputNamePrefix,
|
|
589
|
-
|
|
729
|
+
itemType,
|
|
590
730
|
connection.graphqlFieldName || connection.declaredFieldName,
|
|
591
731
|
connection.declaredFieldName,
|
|
592
|
-
|
|
593
|
-
|
|
732
|
+
optionalFields,
|
|
733
|
+
)
|
|
734
|
+
: null;
|
|
735
|
+
const listOf = (type) => new GraphQLList(itemNonNull ? new GraphQLNonNull(type) : type);
|
|
736
|
+
|
|
737
|
+
const omittedOperation = {
|
|
738
|
+
added: () => `because ${itemType.name} has no writable fields`
|
|
739
|
+
+ `${connection.declaredFieldName ? ` besides its connectionField ${connection.declaredFieldName}` : ''}; `
|
|
740
|
+
+ `create ${itemType.name} records with its own mutations or a registered mutation.`,
|
|
741
|
+
updated: () => `because ${itemType.name} has no writable fields and no writable id; update `
|
|
742
|
+
+ `${itemType.name} records with a registered mutation.`,
|
|
743
|
+
};
|
|
594
744
|
|
|
595
745
|
return new GraphQLInputObjectType({
|
|
596
|
-
name
|
|
597
|
-
fields: () =>
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
746
|
+
name,
|
|
747
|
+
fields: () => {
|
|
748
|
+
const fields = {};
|
|
749
|
+
const itemInputs = {
|
|
750
|
+
added: inputTypeForAdd || getInputType(itemType),
|
|
751
|
+
updated: typesDictForUpdate.types[itemType.name].inputType,
|
|
752
|
+
};
|
|
753
|
+
for (const [operation, itemInput] of Object.entries(itemInputs)) {
|
|
754
|
+
if (hasInputFields(itemInput)) {
|
|
755
|
+
fields[operation] = { type: listOf(itemInput) };
|
|
756
|
+
} else {
|
|
757
|
+
warnOnce(itemType, `emptyItemInput:${owner}:${operation}`, `Configuration issue: ${owner} has no `
|
|
758
|
+
+ `\`${operation}\` operation in generated inputs, ${omittedOperation[operation]()}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Null IDs are skipped, so `deleted` accepts them whatever the collection's item nullability.
|
|
762
|
+
fields.deleted = { type: new GraphQLList(GraphQLID) };
|
|
763
|
+
return fields;
|
|
764
|
+
},
|
|
610
765
|
});
|
|
611
766
|
};
|
|
612
767
|
|
|
768
|
+
// What graphQLListInputType returns for a list of an embedded type whose input has no fields.
|
|
769
|
+
const NO_INPUT_FIELDS = Symbol('no input fields');
|
|
770
|
+
|
|
613
771
|
const graphQLListInputType = (dict, fieldEntry, fieldEntryName,
|
|
614
772
|
inputNamePrefix, connectionField, preserveOuterNonNull, ownerName) => {
|
|
615
773
|
const listShape = getListShape(fieldEntry.type);
|
|
616
774
|
if (!listShape) return null;
|
|
617
775
|
const { itemType } = listShape;
|
|
618
776
|
|
|
619
|
-
if (itemType instanceof GraphQLObjectType
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const oneToMany = createOneToManyInputType(inputNamePrefix
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
normalizeConnectionField(itemType, connectionField),
|
|
627
|
-
listShape.itemNonNull);
|
|
777
|
+
if (itemType instanceof GraphQLObjectType) {
|
|
778
|
+
const registration = requireRegisteredType(dict, itemType, fieldEntryName, ownerName);
|
|
779
|
+
// A referenced collection reads its item inputs lazily, so it never waits for them.
|
|
780
|
+
if (!fieldEntry.extensions?.relation?.embedded) {
|
|
781
|
+
const oneToMany = createOneToManyInputType(`OneToMany${inputNamePrefix}${fieldEntryName}`,
|
|
782
|
+
inputNamePrefix, itemType, normalizeConnectionField(itemType, connectionField),
|
|
783
|
+
listShape.itemNonNull, [], `${ownerName}.${fieldEntryName}`);
|
|
628
784
|
return preserveOuterNonNull && listShape.outerNonNull
|
|
629
785
|
? new GraphQLNonNull(oneToMany)
|
|
630
786
|
: oneToMany;
|
|
631
787
|
}
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
listShape,
|
|
637
|
-
preserveOuterNonNull,
|
|
638
|
-
);
|
|
788
|
+
if (registration.inputType) {
|
|
789
|
+
return hasInputFields(registration.inputType)
|
|
790
|
+
? wrapListInputType(registration.inputType, listShape, preserveOuterNonNull)
|
|
791
|
+
: NO_INPUT_FIELDS;
|
|
639
792
|
}
|
|
640
793
|
} else if (itemType instanceof GraphQLScalarType || itemType instanceof GraphQLEnumType) {
|
|
641
794
|
return wrapListInputType(itemType, listShape, preserveOuterNonNull);
|
|
@@ -643,13 +796,86 @@ const graphQLListInputType = (dict, fieldEntry, fieldEntryName,
|
|
|
643
796
|
return null;
|
|
644
797
|
};
|
|
645
798
|
|
|
646
|
-
|
|
799
|
+
// Writable list fields whose items are the type itself, as buildInputType selects them. Their
|
|
800
|
+
// collection inputs are named OneToManyA<field> and OneToManyU<field> (see claimSelfCollectionNames).
|
|
801
|
+
const selfCollectionFieldNames = (gqltype) => {
|
|
802
|
+
const hasStateMachine = !!typesDict.types[gqltype.name]?.stateMachine;
|
|
803
|
+
return Object.entries(gqltype.getFields())
|
|
804
|
+
.filter(([fieldName, field]) => !field.extensions?.readOnly
|
|
805
|
+
&& !(fieldName === 'state' && hasStateMachine)
|
|
806
|
+
&& getListShape(field.type)?.itemType === gqltype)
|
|
807
|
+
.map(([fieldName]) => fieldName);
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
const selfCollectionInputType = (gqltype, fieldEntryName, fieldEntry, forUpdate, selfFields) => {
|
|
811
|
+
const listShape = getListShape(fieldEntry.type);
|
|
812
|
+
const qualifier = qualifiedSelfCollections.get(gqltype)?.has(fieldEntryName) ? gqltype.name : '';
|
|
813
|
+
const kind = forUpdate ? 'U' : 'A';
|
|
814
|
+
const oneToMany = createOneToManyInputType(
|
|
815
|
+
`OneToMany${qualifier}${kind}${fieldEntryName}`,
|
|
816
|
+
kind,
|
|
817
|
+
gqltype,
|
|
818
|
+
normalizeConnectionField(gqltype, fieldEntry.extensions?.relation?.connectionField),
|
|
819
|
+
listShape.itemNonNull,
|
|
820
|
+
// Items an add mutation creates may carry their own self-referencing collections. These stay
|
|
821
|
+
// optional there, as such items had no such fields before; items added by an update keep them
|
|
822
|
+
// as the type declares them.
|
|
823
|
+
forUpdate ? [] : selfFields,
|
|
824
|
+
`${gqltype.name}.${fieldEntryName}`,
|
|
825
|
+
);
|
|
826
|
+
return !forUpdate && listShape.outerNonNull ? new GraphQLNonNull(oneToMany) : oneToMany;
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// An embedded field, single or list, whose embedded type's create or update input has no fields is
|
|
830
|
+
// left out of that input of its owner. Only the create input is empty when the type's writable fields
|
|
831
|
+
// hold only ids, which create inputs leave out; generated updates still set the field then.
|
|
832
|
+
const warnEmptyEmbeddedInputs = (gqltype, fieldName, embeddedTypeName, noCreate, noUpdate) => {
|
|
833
|
+
if (!noCreate && !noUpdate) return;
|
|
834
|
+
const field = `Configuration issue: ${gqltype.name}.${fieldName} `;
|
|
835
|
+
if (!noUpdate) {
|
|
836
|
+
warnOnce(gqltype, `emptyEmbeddedInput:${fieldName}`, `${field}is left out of generated create inputs, because `
|
|
837
|
+
+ `the create input of its embedded type ${embeddedTypeName} would have no fields (create inputs leave out `
|
|
838
|
+
+ 'ids), so generated creates cannot set it, while generated updates still can; to set it on create, use a '
|
|
839
|
+
+ 'controller or a registered mutation.');
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const inputs = [noCreate && 'create', 'update'].filter(Boolean).join(' and ');
|
|
843
|
+
warnOnce(gqltype, `emptyEmbeddedInput:${fieldName}`, `${field}is left out of generated ${inputs} inputs, because `
|
|
844
|
+
+ `its embedded type ${embeddedTypeName} has no writable fields, so generated mutations cannot set it; set it `
|
|
845
|
+
+ 'in a controller or a registered mutation, and mark it readOnly to state that it is output-only.');
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// A non-null embedded field, single or list, that generated create inputs cannot set. Generated
|
|
849
|
+
// mutations that reach the owner's create input would store records without it, so such a schema is
|
|
850
|
+
// rejected; one whose generated mutations cannot reach that input only leaves the field out.
|
|
851
|
+
const requiredEmbeddedWithoutInputError = (gqltype, { field, embeddedTypeName }) => new SimfinityError(
|
|
852
|
+
`${gqltype.name}.${field} is non-null but its embedded type ${embeddedTypeName} has no writable fields, so `
|
|
853
|
+
+ 'generated create inputs cannot set it; mark it readOnly and set it in a controller, or make it nullable',
|
|
854
|
+
'INVALID_MODEL',
|
|
855
|
+
400,
|
|
856
|
+
);
|
|
857
|
+
// Registration -> the non-null embedded fields its create input left out.
|
|
858
|
+
const requiredFieldsWithoutCreateInput = new WeakMap();
|
|
859
|
+
|
|
860
|
+
// Builds a type's create and update inputs, or returns every field whose input cannot be built yet:
|
|
861
|
+
// `{ unresolvedFields: [{ field, waitsFor }] }`, where `waitsFor` names the embedded type whose
|
|
862
|
+
// inputs the field waits for, and is absent when the field can have no generated input. `reachable`
|
|
863
|
+
// holds the types whose inputs the generated mutations of the schema reach.
|
|
864
|
+
const buildInputType = (gqltype, reachable) => {
|
|
647
865
|
const argTypes = gqltype.getFields();
|
|
648
866
|
|
|
649
867
|
const fieldsArgs = {};
|
|
650
868
|
const fieldsArgForUpdate = {};
|
|
651
869
|
|
|
652
870
|
const selfReferenceCollections = {};
|
|
871
|
+
const unresolvedFields = [];
|
|
872
|
+
const requiredWithoutCreateInput = [];
|
|
873
|
+
// Leaves a non-null embedded field without a create input out, or rejects the schema.
|
|
874
|
+
const omitRequiredEmbedded = (field, embeddedTypeName) => {
|
|
875
|
+
const omitted = { field, embeddedTypeName };
|
|
876
|
+
if (reachable.has(gqltype)) throw requiredEmbeddedWithoutInputError(gqltype, omitted);
|
|
877
|
+
requiredWithoutCreateInput.push(omitted);
|
|
878
|
+
};
|
|
653
879
|
|
|
654
880
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
655
881
|
const fieldArg = {};
|
|
@@ -684,14 +910,31 @@ const buildInputType = (gqltype) => {
|
|
|
684
910
|
} else if (requireRegisteredType(typesDict, unwrapNonNull(fieldEntry.type), fieldEntryName,
|
|
685
911
|
gqltype.name).inputType
|
|
686
912
|
&& typesDictForUpdate.types[fieldEntryNameValue].inputType) {
|
|
687
|
-
|
|
688
|
-
|
|
913
|
+
// Create and update inputs are decided apart: an embedded type whose only writable
|
|
914
|
+
// field is `id` has one only for updates.
|
|
915
|
+
const embeddedCreate = typesDict.types[fieldEntryNameValue].inputType;
|
|
916
|
+
const embeddedUpdate = typesDictForUpdate.types[fieldEntryNameValue].inputType;
|
|
917
|
+
if (hasInputFields(embeddedCreate)) fieldArg.type = embeddedCreate;
|
|
918
|
+
else if (fieldEntry.type instanceof GraphQLNonNull) {
|
|
919
|
+
omitRequiredEmbedded(fieldEntryName, fieldEntryNameValue);
|
|
920
|
+
}
|
|
921
|
+
if (hasInputFields(embeddedUpdate)) fieldArgForUpdate.type = embeddedUpdate;
|
|
922
|
+
warnEmptyEmbeddedInputs(gqltype, fieldEntryName, fieldEntryNameValue,
|
|
923
|
+
!fieldArg.type, !fieldArgForUpdate.type);
|
|
689
924
|
} else {
|
|
690
|
-
|
|
925
|
+
unresolvedFields.push({ field: fieldEntryName, waitsFor: fieldEntryNameValue });
|
|
691
926
|
}
|
|
692
|
-
} else {
|
|
927
|
+
} else if (unresolvedFields.length === 0) {
|
|
928
|
+
// Fields after one that cannot be built yet are only checked, so this logs as many times
|
|
929
|
+
// as when the build stopped at that field.
|
|
693
930
|
console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
|
|
694
931
|
}
|
|
932
|
+
} else if (isAbstractOutputType(unwrapListAndNonNull(fieldEntry.type))) {
|
|
933
|
+
// Generated inputs cannot write an interface or union value, single or in a list, so the
|
|
934
|
+
// field is output-only. readOnly fields never get here.
|
|
935
|
+
warnOnce(gqltype, `abstract:${fieldEntryName}`, `Configuration issue: ${gqltype.name}.${fieldEntryName} `
|
|
936
|
+
+ 'has an interface or union type, so generated inputs leave it out and generated mutations cannot '
|
|
937
|
+
+ 'set it; resolve it yourself, and mark it readOnly to state that it is output-only.');
|
|
695
938
|
} else if (getListShape(fieldEntry.type)) {
|
|
696
939
|
const listShape = getListShape(fieldEntry.type);
|
|
697
940
|
if (listShape.itemType === gqltype) {
|
|
@@ -704,10 +947,17 @@ const buildInputType = (gqltype) => {
|
|
|
704
947
|
fieldEntryName, `${gqltype.name}U`,
|
|
705
948
|
fieldEntry.extensions?.relation?.connectionField, false, gqltype.name);
|
|
706
949
|
if (listInputTypeForAdd && listInputTypeForUpdate) {
|
|
707
|
-
fieldArg.type = listInputTypeForAdd;
|
|
708
|
-
|
|
950
|
+
if (listInputTypeForAdd !== NO_INPUT_FIELDS) fieldArg.type = listInputTypeForAdd;
|
|
951
|
+
else if (listShape.outerNonNull) omitRequiredEmbedded(fieldEntryName, listShape.itemType.name);
|
|
952
|
+
if (listInputTypeForUpdate !== NO_INPUT_FIELDS) fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
953
|
+
warnEmptyEmbeddedInputs(gqltype, fieldEntryName, listShape.itemType.name,
|
|
954
|
+
listInputTypeForAdd === NO_INPUT_FIELDS, listInputTypeForUpdate === NO_INPUT_FIELDS);
|
|
709
955
|
} else {
|
|
710
|
-
|
|
956
|
+
// An object item here is embedded and its inputs are not built yet; any other item,
|
|
957
|
+
// such as a list, has no generated input.
|
|
958
|
+
unresolvedFields.push(listShape.itemType instanceof GraphQLObjectType
|
|
959
|
+
? { field: fieldEntryName, waitsFor: listShape.itemType.name }
|
|
960
|
+
: { field: fieldEntryName });
|
|
711
961
|
}
|
|
712
962
|
}
|
|
713
963
|
}
|
|
@@ -729,94 +979,176 @@ const buildInputType = (gqltype) => {
|
|
|
729
979
|
}
|
|
730
980
|
}
|
|
731
981
|
|
|
732
|
-
|
|
982
|
+
if (unresolvedFields.length > 0) return { unresolvedFields };
|
|
983
|
+
|
|
984
|
+
// Self-referencing collections follow the other fields, as they always have. Their item inputs
|
|
985
|
+
// are read when the schema is built, after these input types exist.
|
|
986
|
+
const selfFields = Object.keys(selfReferenceCollections);
|
|
987
|
+
for (const [fieldEntryName, fieldEntry] of Object.entries(selfReferenceCollections)) {
|
|
988
|
+
fieldsArgs[fieldEntryName] = {
|
|
989
|
+
type: selfCollectionInputType(gqltype, fieldEntryName, fieldEntry, false, selfFields),
|
|
990
|
+
description: fieldEntry.description,
|
|
991
|
+
};
|
|
992
|
+
fieldsArgForUpdate[fieldEntryName] = {
|
|
993
|
+
type: selfCollectionInputType(gqltype, fieldEntryName, fieldEntry, true, selfFields),
|
|
994
|
+
description: fieldEntry.description,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const inputTypeForAdd = new GraphQLInputObjectType({
|
|
733
999
|
name: `${gqltype.name}Input`,
|
|
734
1000
|
description: gqltype.description,
|
|
735
1001
|
fields: fieldsArgs,
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
const inputTypeBodyForUpdate = {
|
|
1002
|
+
});
|
|
1003
|
+
const inputTypeForUpdate = new GraphQLInputObjectType({
|
|
739
1004
|
name: `${gqltype.name}InputForUpdate`,
|
|
740
1005
|
description: gqltype.description,
|
|
741
1006
|
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
1007
|
});
|
|
785
1008
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1009
|
+
return {
|
|
1010
|
+
inputTypeBody: inputTypeForAdd, inputTypeBodyForUpdate: inputTypeForUpdate, requiredWithoutCreateInput,
|
|
1011
|
+
};
|
|
789
1012
|
};
|
|
790
1013
|
|
|
791
1014
|
const getInputType = (type) => typesDict.types[type.name].inputType;
|
|
792
1015
|
|
|
793
|
-
|
|
1016
|
+
// Registered types whose generated inputs the generated mutations of this schema can reach: the
|
|
1017
|
+
// included endpoint types and, through their writable fields, embedded types and the items of
|
|
1018
|
+
// collections.
|
|
1019
|
+
const typesWithReachableInputs = (includedMutationTypes) => {
|
|
1020
|
+
const reached = new Set();
|
|
1021
|
+
const visit = (gqltype) => {
|
|
1022
|
+
if (reached.has(gqltype)) return;
|
|
1023
|
+
reached.add(gqltype);
|
|
1024
|
+
for (const field of Object.values(gqltype.getFields())) {
|
|
1025
|
+
if (field.extensions?.readOnly) continue;
|
|
1026
|
+
const listShape = getListShape(field.type);
|
|
1027
|
+
const target = listShape ? listShape.itemType : unwrapNonNull(field.type);
|
|
1028
|
+
// A single reference is an IdInputType, so it reaches no input of its target.
|
|
1029
|
+
if (target instanceof GraphQLObjectType && typesDict.types[target.name]
|
|
1030
|
+
&& (listShape || field.extensions?.relation?.embedded)) {
|
|
1031
|
+
visit(typesDict.types[target.name].gqltype);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
for (const type of Object.values(typesDict.types)) {
|
|
1036
|
+
if (type.endpoint && !shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
1037
|
+
visit(type.gqltype);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return reached;
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
// A self-referencing collection's inputs are OneToManyA<field> and OneToManyU<field>. When several
|
|
1044
|
+
// types that the generated mutations reach have one with the same field name, the first registered
|
|
1045
|
+
// keeps those names and the others get OneToMany<Type>A<field> and OneToMany<Type>U<field>, as other
|
|
1046
|
+
// collections do. Types the mutations cannot reach keep the unqualified names, as such inputs never
|
|
1047
|
+
// collided. Names are fixed when a type's inputs are built, at its first schema.
|
|
1048
|
+
// A type is qualified only when this schema also reaches an earlier type that holds the unqualified
|
|
1049
|
+
// names and is still the type registered under its name, since both inputs then meet in one schema,
|
|
1050
|
+
// which failed with a duplicate type name before such names were qualified. Otherwise, as with a
|
|
1051
|
+
// type registered again under the same name or after an earlier createSchema(), it keeps the
|
|
1052
|
+
// unqualified names.
|
|
1053
|
+
const claimSelfCollectionNames = (pending, includedMutationTypes) => {
|
|
1054
|
+
const reachable = typesWithReachableInputs(includedMutationTypes);
|
|
1055
|
+
const meets = (type) => typesDict.types[type.name]?.gqltype === type && reachable.has(type);
|
|
1056
|
+
for (const { gqltype } of Object.values(pending)) {
|
|
1057
|
+
if (typesDict.types[gqltype.name].inputType) continue;
|
|
1058
|
+
for (const fieldName of selfCollectionFieldNames(gqltype)) {
|
|
1059
|
+
if (!selfCollectionNameHolders.has(fieldName)) selfCollectionNameHolders.set(fieldName, []);
|
|
1060
|
+
const holders = selfCollectionNameHolders.get(fieldName);
|
|
1061
|
+
const owner = reachable.has(gqltype) && holders.find((type) => type !== gqltype && meets(type));
|
|
1062
|
+
if (!owner) {
|
|
1063
|
+
if (!holders.includes(gqltype)) holders.push(gqltype);
|
|
1064
|
+
} else {
|
|
1065
|
+
if (!qualifiedSelfCollections.has(gqltype)) qualifiedSelfCollections.set(gqltype, new Set());
|
|
1066
|
+
qualifiedSelfCollections.get(gqltype).add(fieldName);
|
|
1067
|
+
warnOnce(gqltype, `selfCollectionName:${fieldName}`, `Configuration issue: ${gqltype.name}.${fieldName} `
|
|
1068
|
+
+ `and ${owner.name}.${fieldName} are self-referencing collections with the same name, so the inputs `
|
|
1069
|
+
+ `of ${gqltype.name}.${fieldName} are named OneToMany${gqltype.name}A${fieldName} and `
|
|
1070
|
+
+ `OneToMany${gqltype.name}U${fieldName}, while ${owner.name}.${fieldName} keeps OneToManyA${fieldName} `
|
|
1071
|
+
+ `and OneToManyU${fieldName}. To choose which type keeps those names, register it first, or make `
|
|
1072
|
+
+ 'the other type unreachable from the generated mutations.');
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// The INPUT_TYPE_UNRESOLVED message. It names every field whose input could not be built, by type:
|
|
1079
|
+
// the causes (lists whose items have no generated input, and embedded fields whose types contain
|
|
1080
|
+
// each other) and, apart from them, fields that only wait for another listed type.
|
|
1081
|
+
const unresolvedInputTypesMessage = (pendingTypeNames, unresolvedByType) => {
|
|
1082
|
+
// Whether the inputs of type `from` wait, through embedded fields, for those of type `to`.
|
|
1083
|
+
const waitsOn = (from, to, seen = new Set()) => {
|
|
1084
|
+
if (from === to) return true;
|
|
1085
|
+
if (seen.has(from)) return false;
|
|
1086
|
+
seen.add(from);
|
|
1087
|
+
return (unresolvedByType[from] || [])
|
|
1088
|
+
.some(({ waitsFor }) => waitsFor !== undefined && waitsOn(waitsFor, to, seen));
|
|
1089
|
+
};
|
|
1090
|
+
const lists = [];
|
|
1091
|
+
const cycles = [];
|
|
1092
|
+
const waiting = [];
|
|
1093
|
+
for (const [typeName, fields] of Object.entries(unresolvedByType)) {
|
|
1094
|
+
for (const { field, waitsFor } of fields) {
|
|
1095
|
+
const name = `${typeName}.${field}`;
|
|
1096
|
+
if (waitsFor === undefined) lists.push(name);
|
|
1097
|
+
else if (waitsOn(waitsFor, typeName)) cycles.push(name);
|
|
1098
|
+
else waiting.push(`${name} waits for ${waitsFor}`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const causes = [
|
|
1102
|
+
lists.length > 0 && `${lists.join(', ')} (writable lists whose items have no generated input, such as lists of lists)`,
|
|
1103
|
+
cycles.length > 0 && `${cycles.join(', ')} (embedded types that contain each other)`,
|
|
1104
|
+
].filter(Boolean);
|
|
1105
|
+
return `Could not build input types for: ${pendingTypeNames.join(', ')}. `
|
|
1106
|
+
+ 'Check for circular or misconfigured relations'
|
|
1107
|
+
+ (causes.length > 0
|
|
1108
|
+
? `: no input can be generated for ${causes.join(' or for ')}; mark such a field readOnly.`
|
|
1109
|
+
: '.')
|
|
1110
|
+
+ (waiting.length > 0
|
|
1111
|
+
? ` These fields only wait for another listed type and build once it does: ${waiting.join(', ')}.`
|
|
1112
|
+
: '');
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
const buildPendingInputTypes = (waitingForInputType, includedMutationTypes) => {
|
|
1116
|
+
claimSelfCollectionNames(waitingForInputType, includedMutationTypes);
|
|
1117
|
+
// Inputs built for an earlier schema, which did not reach them, may leave out a non-null embedded
|
|
1118
|
+
// field that this schema's generated mutations would need; inputs built now are checked as built.
|
|
1119
|
+
const reachable = typesWithReachableInputs(includedMutationTypes);
|
|
1120
|
+
for (const gqltype of reachable) {
|
|
1121
|
+
const registration = typesDict.types[gqltype.name];
|
|
1122
|
+
const [omitted] = (registration.inputType && requiredFieldsWithoutCreateInput.get(registration)) || [];
|
|
1123
|
+
if (omitted) throw requiredEmbeddedWithoutInputError(gqltype, omitted);
|
|
1124
|
+
}
|
|
794
1125
|
let pending = waitingForInputType;
|
|
795
1126
|
let previousPendingCount = Object.keys(pending).length + 1;
|
|
1127
|
+
// Type name -> the fields whose input could not be built in the last pass.
|
|
1128
|
+
let unresolvedFields = {};
|
|
796
1129
|
|
|
797
1130
|
while (Object.keys(pending).length > 0) {
|
|
798
1131
|
const currentCount = Object.keys(pending).length;
|
|
799
1132
|
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
|
-
);
|
|
1133
|
+
throw new SimfinityError(unresolvedInputTypesMessage(Object.keys(pending), unresolvedFields),
|
|
1134
|
+
'INPUT_TYPE_UNRESOLVED', 500);
|
|
806
1135
|
}
|
|
807
1136
|
previousPendingCount = currentCount;
|
|
808
1137
|
|
|
809
1138
|
const stillWaiting = {};
|
|
1139
|
+
unresolvedFields = {};
|
|
810
1140
|
for (const [key, value] of Object.entries(pending)) {
|
|
811
1141
|
const { gqltype } = value;
|
|
812
1142
|
if (typesDict.types[gqltype.name].inputType) continue;
|
|
813
1143
|
|
|
814
|
-
const result = buildInputType(gqltype);
|
|
815
|
-
if (result
|
|
1144
|
+
const result = buildInputType(gqltype, reachable);
|
|
1145
|
+
if (result.inputTypeBody && result.inputTypeBodyForUpdate) {
|
|
816
1146
|
typesDict.types[gqltype.name].inputType = result.inputTypeBody;
|
|
817
1147
|
typesDictForUpdate.types[gqltype.name].inputType = result.inputTypeBodyForUpdate;
|
|
1148
|
+
requiredFieldsWithoutCreateInput.set(typesDict.types[gqltype.name], result.requiredWithoutCreateInput);
|
|
818
1149
|
} else {
|
|
819
1150
|
stillWaiting[key] = value;
|
|
1151
|
+
unresolvedFields[gqltype.name] = result.unresolvedFields;
|
|
820
1152
|
}
|
|
821
1153
|
}
|
|
822
1154
|
pending = stillWaiting;
|
|
@@ -1070,7 +1402,11 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
1070
1402
|
if (!result) throw new SimfinityError(`${gqltype.name} ${objectId} is not valid`, 'NOT_VALID_ID', 404);
|
|
1071
1403
|
|
|
1072
1404
|
if (materializedModel.collectionFields) {
|
|
1073
|
-
|
|
1405
|
+
// Children link to, and are owned through, the parent's stored key, as when a new parent is
|
|
1406
|
+
// saved: the client may send a spelling of the ID that the adapter accepts but does not store,
|
|
1407
|
+
// such as an uppercase ObjectId or UUID. It is the key the generated collection field reads
|
|
1408
|
+
// children by (`_id`, else `id`); an adapter whose update result has neither keeps the requested ID.
|
|
1409
|
+
await iterateOnCollectionFields(materializedModel, gqltype, result._id ?? result.id ?? objectId, session, context);
|
|
1074
1410
|
}
|
|
1075
1411
|
|
|
1076
1412
|
if (controller && controller.onUpdated) {
|
|
@@ -1232,7 +1568,9 @@ const executeOperation = (Model, gqltype, controller, args, operation, actionFie
|
|
|
1232
1568
|
);
|
|
1233
1569
|
};
|
|
1234
1570
|
|
|
1235
|
-
|
|
1571
|
+
// parentKey is the parent's stored key: children are linked to it, and an updated or deleted child
|
|
1572
|
+
// must already store it in its connectionField.
|
|
1573
|
+
const executeItemFunction = async (gqltype, collectionField, parentKey, session,
|
|
1236
1574
|
collectionFieldsList, operationType, context) => {
|
|
1237
1575
|
const argTypes = gqltype.getFields();
|
|
1238
1576
|
const collectionGQLType = unwrapListAndNonNull(argTypes[collectionField].type);
|
|
@@ -1246,7 +1584,7 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
1246
1584
|
const type = operationType === operations.UPDATE
|
|
1247
1585
|
? typesDictForUpdate.types[collectionGQLType.name] : typesDict.types[collectionGQLType.name];
|
|
1248
1586
|
const linkToParent = (item) => {
|
|
1249
|
-
item[connection.storageFieldName] =
|
|
1587
|
+
item[connection.storageFieldName] = parentKey;
|
|
1250
1588
|
if (item.$unset) delete item.$unset[connection.storageFieldName];
|
|
1251
1589
|
if (item.$set) delete item.$set[connection.storageFieldName];
|
|
1252
1590
|
};
|
|
@@ -1262,8 +1600,8 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
1262
1600
|
if (!child) {
|
|
1263
1601
|
throw new SimfinityError(`${collectionGQLType.name} ${id} is not valid`, 'NOT_VALID_ID', 404);
|
|
1264
1602
|
}
|
|
1265
|
-
if (String(child[connection.storageFieldName]) !== String(
|
|
1266
|
-
throw new
|
|
1603
|
+
if (String(child[connection.storageFieldName]) !== String(parentKey)) {
|
|
1604
|
+
throw new ForbiddenError('Child does not belong to this parent');
|
|
1267
1605
|
}
|
|
1268
1606
|
}
|
|
1269
1607
|
|
|
@@ -1528,39 +1866,61 @@ const resolveById = async (type, args, context, requiredId) => {
|
|
|
1528
1866
|
return results[0] || null;
|
|
1529
1867
|
};
|
|
1530
1868
|
|
|
1869
|
+
// Why a type's generated create or update input has no fields, for the warnings of buildMutation. An
|
|
1870
|
+
// update input also needs a writable id.
|
|
1871
|
+
const withoutWritableFields = (type, { noWritableId = false } = {}) => {
|
|
1872
|
+
const stateField = type.gqltype.getFields().state;
|
|
1873
|
+
const managedState = type.stateMachine && stateField && !stateField.extensions?.readOnly;
|
|
1874
|
+
const state = managedState ? ' besides its state, which its state machine manages' : '';
|
|
1875
|
+
const id = noWritableId ? `${state ? ',' : ''} and no writable id` : '';
|
|
1876
|
+
return `${type.gqltype.name} has no writable fields${state}${id}`;
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1531
1879
|
// Every generated mutation field carries extensions.simfinityMutation with its operation (and, for
|
|
1532
1880
|
// a state-machine action, the action and its from/to states), so tools such as the MCP package can
|
|
1533
1881
|
// classify it without guessing from its name. Extensions do not appear in SDL or introspection.
|
|
1882
|
+
// An input object needs a field, so a type whose create input has none gets no add mutation, and
|
|
1883
|
+
// one whose update input has none, which only a type without a writable id can have, gets no update
|
|
1884
|
+
// mutation or state machine actions either (#161).
|
|
1534
1885
|
const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
|
|
1535
1886
|
const rootQueryArgs = {};
|
|
1536
1887
|
rootQueryArgs.name = name;
|
|
1537
1888
|
rootQueryArgs.fields = {};
|
|
1538
1889
|
|
|
1539
|
-
buildPendingInputTypes(waitingInputType);
|
|
1890
|
+
buildPendingInputTypes(waitingInputType, includedMutationTypes);
|
|
1540
1891
|
|
|
1541
1892
|
for (const type of Object.values(typesDict.types)) {
|
|
1542
1893
|
if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
1543
1894
|
if (type.endpoint) {
|
|
1544
1895
|
const argsObject = { input: { type: new GraphQLNonNull(type.inputType) } };
|
|
1545
1896
|
|
|
1546
|
-
|
|
1547
|
-
type
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1897
|
+
if (hasInputFields(type.inputType)) {
|
|
1898
|
+
rootQueryArgs.fields[`add${type.simpleEntityEndpointName}`] = {
|
|
1899
|
+
type: type.gqltype,
|
|
1900
|
+
description: 'add',
|
|
1901
|
+
args: argsObject,
|
|
1902
|
+
extensions: { simfinityMutation: { typeName: type.gqltype.name, operation: operations.SAVE } },
|
|
1903
|
+
async resolve(parent, args, context) {
|
|
1904
|
+
const params = {
|
|
1905
|
+
type,
|
|
1906
|
+
args,
|
|
1907
|
+
operation: operations.SAVE,
|
|
1908
|
+
context,
|
|
1909
|
+
};
|
|
1558
1910
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1911
|
+
await executeMiddleware(params);
|
|
1912
|
+
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1913
|
+
args.input, operations.SAVE, null, null, context, type.inputType);
|
|
1914
|
+
},
|
|
1915
|
+
};
|
|
1916
|
+
} else {
|
|
1917
|
+
warnOnce(type.gqltype, 'emptyCreateInput', `Configuration issue: ${withoutWritableFields(type)}, so the `
|
|
1918
|
+
+ `schema has no add${type.simpleEntityEndpointName} mutation; create its records with a registered `
|
|
1919
|
+
+ (type.stateMachine
|
|
1920
|
+
? 'mutation, which must set the initial state (saveObject() sets it).'
|
|
1921
|
+
: `mutation that takes no input or an input of its own, since getInputType(${type.gqltype.name}) `
|
|
1922
|
+
+ 'has no fields.'));
|
|
1923
|
+
}
|
|
1564
1924
|
rootQueryArgs.fields[`delete${type.simpleEntityEndpointName}`] = {
|
|
1565
1925
|
type: type.gqltype,
|
|
1566
1926
|
description: 'delete',
|
|
@@ -1585,7 +1945,12 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1585
1945
|
|
|
1586
1946
|
for (const type of Object.values(typesDictForUpdate.types)) {
|
|
1587
1947
|
if (!shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)) {
|
|
1588
|
-
if (type.endpoint) {
|
|
1948
|
+
if (type.endpoint && !hasInputFields(type.inputType)) {
|
|
1949
|
+
const reason = withoutWritableFields(type, { noWritableId: true });
|
|
1950
|
+
warnOnce(type.gqltype, 'emptyUpdateInput', `Configuration issue: ${reason}, so its generated update input `
|
|
1951
|
+
+ `would have no fields and the schema has no update${type.simpleEntityEndpointName} mutation`
|
|
1952
|
+
+ `${type.stateMachine ? ' and no state machine actions' : ''}.`);
|
|
1953
|
+
} else if (type.endpoint) {
|
|
1589
1954
|
const argsObject = { input: { type: new GraphQLNonNull(type.inputType) } };
|
|
1590
1955
|
rootQueryArgs.fields[`update${type.simpleEntityEndpointName}`] = {
|
|
1591
1956
|
type: type.gqltype,
|
|
@@ -1671,7 +2036,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1671
2036
|
return new GraphQLObjectType(rootQueryArgs);
|
|
1672
2037
|
};
|
|
1673
2038
|
|
|
1674
|
-
const buildRootQuery = (name, includedTypes) => {
|
|
2039
|
+
const buildRootQuery = (name, includedTypes, aggregationResultType = QLTypeAggregationResult) => {
|
|
1675
2040
|
const rootQueryArgs = {};
|
|
1676
2041
|
rootQueryArgs.name = name;
|
|
1677
2042
|
rootQueryArgs.fields = {};
|
|
@@ -1708,7 +2073,8 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1708
2073
|
await executeScope(params);
|
|
1709
2074
|
await applyJoinedScopes(joinedScopes, params);
|
|
1710
2075
|
const queryArgs = params.args;
|
|
1711
|
-
const wantsCount = !!(queryArgs.pagination && queryArgs.pagination.count)
|
|
2076
|
+
const wantsCount = !!(queryArgs.pagination && queryArgs.pagination.count)
|
|
2077
|
+
&& canReportCount(parent, info, context);
|
|
1712
2078
|
|
|
1713
2079
|
const dataPromise = adapter.find(type.model, type.gqltype, queryArgs, null);
|
|
1714
2080
|
const countPromise = wantsCount
|
|
@@ -1716,8 +2082,10 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1716
2082
|
: null;
|
|
1717
2083
|
|
|
1718
2084
|
const [result, resultCount] = await Promise.all([dataPromise, countPromise]);
|
|
1719
|
-
if (wantsCount) {
|
|
1720
|
-
|
|
2085
|
+
if (wantsCount && !reportCount(parent, info, context, resultCount)) {
|
|
2086
|
+
warnOnce(runtimeIdentity, 'count-context', 'Configuration issue: the GraphQL context does not accept '
|
|
2087
|
+
+ 'a count property (it is frozen, sealed or has a read-only count), so pagination counts are not '
|
|
2088
|
+
+ 'reported; pass a fresh mutable context object for each request.');
|
|
1721
2089
|
}
|
|
1722
2090
|
return result;
|
|
1723
2091
|
},
|
|
@@ -1734,7 +2102,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1734
2102
|
};
|
|
1735
2103
|
|
|
1736
2104
|
rootQueryArgs.fields[`${type.listEntitiesEndpointName}_aggregate`] = {
|
|
1737
|
-
type: new GraphQLList(
|
|
2105
|
+
type: new GraphQLList(aggregationResultType),
|
|
1738
2106
|
args: aggregateArgsObject,
|
|
1739
2107
|
extensions: { simfinityQuery: { typeName: type.gqltype.name, operation: 'aggregate' } },
|
|
1740
2108
|
async resolve(parent, args, context) {
|
|
@@ -1785,6 +2153,102 @@ const installIdResolver = (gqltype) => {
|
|
|
1785
2153
|
if (idField && !idField.resolve) idField.resolve = markGenerated((parent) => parent._id ?? parent.id);
|
|
1786
2154
|
};
|
|
1787
2155
|
|
|
2156
|
+
// A rendered embedded value that may hold no data: a plain object, or a hydrated record, which
|
|
2157
|
+
// converts itself with toObject() (MongoDB documents and their nested paths do). Promises, arrays,
|
|
2158
|
+
// functions and other objects, such as dates or identifiers, are values.
|
|
2159
|
+
const isRecordObject = (value) => {
|
|
2160
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || isThenable(value)) return false;
|
|
2161
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2162
|
+
return prototype === Object.prototype || prototype === null || typeof value.toObject === 'function';
|
|
2163
|
+
};
|
|
2164
|
+
const isPlainObject = (value) => {
|
|
2165
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2166
|
+
return prototype === Object.prototype || prototype === null;
|
|
2167
|
+
};
|
|
2168
|
+
|
|
2169
|
+
// The members an embedded value stores. The rule below reads them instead of the value, so no getter
|
|
2170
|
+
// or virtual of the application runs, whatever the selection. An adapter that hydrates records
|
|
2171
|
+
// returns their stored data through rawEmbeddedValue (MongoDB: a nested path's raw value, a
|
|
2172
|
+
// subdocument's _doc); an absent stored value has no members. A plain object is read as it is.
|
|
2173
|
+
// Returns null for anything else, such as a record the adapter does not convert, which counts as data.
|
|
2174
|
+
const storedMembersOf = (value) => {
|
|
2175
|
+
if (!isRecordObject(value)) return null;
|
|
2176
|
+
const stored = typeof adapter.rawEmbeddedValue === 'function' ? adapter.rawEmbeddedValue(value) : value;
|
|
2177
|
+
if (stored === null || stored === undefined) return {};
|
|
2178
|
+
return typeof stored === 'object' && !Array.isArray(stored) && !isThenable(stored) && isPlainObject(stored)
|
|
2179
|
+
? stored : null;
|
|
2180
|
+
};
|
|
2181
|
+
|
|
2182
|
+
// A stored member: an own data property, read without calling accessors. An accessor is present,
|
|
2183
|
+
// whatever it would return.
|
|
2184
|
+
const PRESENT = Symbol('present');
|
|
2185
|
+
const ownStoredValue = (stored, key) => {
|
|
2186
|
+
const descriptor = Object.getOwnPropertyDescriptor(stored, key);
|
|
2187
|
+
if (!descriptor) return undefined;
|
|
2188
|
+
if (!('value' in descriptor)) return PRESENT;
|
|
2189
|
+
return isObjectMember(key, descriptor.value) ? undefined : descriptor.value;
|
|
2190
|
+
};
|
|
2191
|
+
// A referenced collection stores nothing in the object. `id` reads the stored identity, `_id` before
|
|
2192
|
+
// `id`, as the generated entity resolver (installIdResolver) and Mongoose's id virtual do. Every other
|
|
2193
|
+
// member is stored under its storage name.
|
|
2194
|
+
const isReferencedCollection = (member) => {
|
|
2195
|
+
const relation = member.extensions?.relation;
|
|
2196
|
+
return Boolean(relation && !relation.embedded && getListShape(member.type));
|
|
2197
|
+
};
|
|
2198
|
+
const storedMemberValue = (stored, memberName, member) => {
|
|
2199
|
+
if (isReferencedCollection(member)) return undefined;
|
|
2200
|
+
const identity = memberName === 'id' ? ownStoredValue(stored, '_id') : undefined;
|
|
2201
|
+
return identity ?? ownStoredValue(stored, getFieldStorageName(memberName, member));
|
|
2202
|
+
};
|
|
2203
|
+
|
|
2204
|
+
// Whether stored members hold no data: every member is null or undefined, an empty list, or a
|
|
2205
|
+
// singular embedded object that holds no data. Any other value, a promise or an accessor included,
|
|
2206
|
+
// is data. An object that contains itself, which only a custom result can, holds data.
|
|
2207
|
+
const holdsNoData = (stored, type, ancestors = []) => !ancestors.includes(stored)
|
|
2208
|
+
&& Object.entries(type.getFields()).every(([memberName, member]) => {
|
|
2209
|
+
const value = storedMemberValue(stored, memberName, member);
|
|
2210
|
+
if (value === null || value === undefined) return true;
|
|
2211
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
2212
|
+
const memberType = unwrapNonNull(member.type);
|
|
2213
|
+
if (member.extensions?.relation?.embedded !== true || !(memberType instanceof GraphQLObjectType)) return false;
|
|
2214
|
+
const nested = storedMembersOf(value);
|
|
2215
|
+
return nested !== null && holdsNoData(nested, memberType, [...ancestors, stored]);
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
// The members that count: non-null members that generated inputs accept. As on writes
|
|
2219
|
+
// (completeEmbeddedValue), `id` and readOnly members do not; nor do interface and union members,
|
|
2220
|
+
// which buildInputType leaves out; nor do referenced collections or members with an application
|
|
2221
|
+
// resolver, which may compute a value.
|
|
2222
|
+
const countsAsRequired = (type, memberName, member) => member.type instanceof GraphQLNonNull
|
|
2223
|
+
&& !isReferencedCollection(member) && memberName !== 'id' && !member.extensions?.readOnly
|
|
2224
|
+
&& !isAbstractOutputType(unwrapListAndNonNull(member.type)) && !hasApplicationResolver(type, memberName);
|
|
2225
|
+
const hasRequiredMember = (type) => Object.entries(type.getFields())
|
|
2226
|
+
.some(([memberName, member]) => countsAsRequired(type, memberName, member));
|
|
2227
|
+
|
|
2228
|
+
// Whether rendering such an object fails: a member that counts is null or undefined, or is a singular
|
|
2229
|
+
// embedded object that misses one of its own. A promise or an accessor counts as present.
|
|
2230
|
+
const missesRequiredMember = (stored, type) => Object.entries(type.getFields()).some(([memberName, member]) => {
|
|
2231
|
+
if (!countsAsRequired(type, memberName, member)) return false;
|
|
2232
|
+
const value = storedMemberValue(stored, memberName, member);
|
|
2233
|
+
if (value === null || value === undefined) return true;
|
|
2234
|
+
if (member.extensions?.relation?.embedded !== true || !(member.type.ofType instanceof GraphQLObjectType)) return false;
|
|
2235
|
+
const nested = storedMembersOf(value);
|
|
2236
|
+
return nested !== null && missesRequiredMember(nested, member.type.ofType);
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
// A type without a member that counts never reads as null, so its values are returned untouched,
|
|
2240
|
+
// with no member read. Reading stored members never changes the result otherwise: a value whose
|
|
2241
|
+
// members cannot be read, because an adapter hook or a property read throws, is returned as it is.
|
|
2242
|
+
const readEmbeddedObject = (value, type) => {
|
|
2243
|
+
if (!(type instanceof GraphQLObjectType) || !hasRequiredMember(type)) return value;
|
|
2244
|
+
try {
|
|
2245
|
+
const stored = storedMembersOf(value);
|
|
2246
|
+
return stored !== null && holdsNoData(stored, type) && missesRequiredMember(stored, type) ? null : value;
|
|
2247
|
+
} catch {
|
|
2248
|
+
return value;
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
|
|
1788
2252
|
// An adapter that hydrates stored records may render an explicitly null singular embedded object as
|
|
1789
2253
|
// an object (MongoDB nested paths do); readEmbeddedValue returns null for it. Nullable singular
|
|
1790
2254
|
// embedded fields of registered types, and of the embedded types they reach, read through it. The
|
|
@@ -1793,11 +2257,24 @@ const installIdResolver = (gqltype) => {
|
|
|
1793
2257
|
// The value is read as graphql's default resolver reads it, so a method on the parent, such as one a
|
|
1794
2258
|
// custom mutation result supplies, is still called; a promise it returns is read once it settles.
|
|
1795
2259
|
// Only a field named like an Object.prototype member that reads as that member reads as null.
|
|
2260
|
+
// Such an adapter may also render an absent object, or store an omitted one, as an object that holds
|
|
2261
|
+
// nothing but its list defaults (MongoDB nested paths do). An object that holds no data and misses a
|
|
2262
|
+
// required member (see missesRequiredMember) cannot be rendered, so it reads as null, as an absent
|
|
2263
|
+
// object does on other read paths, whatever the selection. The rule reads the stored members
|
|
2264
|
+
// (storedMembersOf), never running a getter or virtual, and depends only on the value and its type,
|
|
2265
|
+
// so runtimes that share the type, with or without the hook, read the same. An object a
|
|
2266
|
+
// generated create or update writes does not read as null, since those writes require the members
|
|
2267
|
+
// counted, with one exception: Mongoose minimizes empty objects when it saves a new document, so a
|
|
2268
|
+
// create that writes an empty object for a required embedded member (`geo: {}`) stores that member
|
|
2269
|
+
// as absent. An object that holds nothing else then reads as null, as an omitted one does, except
|
|
2270
|
+
// where Mongoose hydrates the document (by-ID and reference reads, update responses) and the member is
|
|
2271
|
+
// a nested path, which it renders as `{}`. Generated models declare every embedded member as a nested
|
|
2272
|
+
// path except one named `type`, a single nested subdocument. PostgreSQL rejects such a create
|
|
2273
|
+
// (REQUIRED_VALUE).
|
|
1796
2274
|
const readEmbeddedField = markGenerated((parent, args, context, info) => {
|
|
1797
2275
|
const value = readOwnField(parent, args, context, info);
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
: adapter.readEmbeddedValue(value);
|
|
2276
|
+
const read = (resolved) => readEmbeddedObject(adapter.readEmbeddedValue(resolved), getNamedType(info.returnType));
|
|
2277
|
+
return isThenable(value) ? value.then(read) : read(value);
|
|
1801
2278
|
});
|
|
1802
2279
|
|
|
1803
2280
|
const installEmbeddedValueResolvers = (gqltype, visited) => {
|
|
@@ -1860,6 +2337,20 @@ const assertCollectionConnections = () => {
|
|
|
1860
2337
|
|
|
1861
2338
|
const getRegistrations = () => Object.values(typesDict.types);
|
|
1862
2339
|
|
|
2340
|
+
// Application types that a schema with these allowlists contains, whatever else it reaches: the
|
|
2341
|
+
// endpoint types of its queries and mutations, and the input and output types of its registered
|
|
2342
|
+
// mutations.
|
|
2343
|
+
const includedApplicationTypes = (includedQueryTypes, includedMutationTypes, includedCustomMutations) => [
|
|
2344
|
+
...Object.values(typesDict.types)
|
|
2345
|
+
.filter((type) => type.endpoint
|
|
2346
|
+
&& ((type.simpleEntityEndpointName && !shouldNotBeIncludedInSchema(includedQueryTypes, type.gqltype))
|
|
2347
|
+
|| !shouldNotBeIncludedInSchema(includedMutationTypes, type.gqltype)))
|
|
2348
|
+
.map((type) => type.gqltype),
|
|
2349
|
+
...Object.entries(registeredMutations)
|
|
2350
|
+
.filter(([entry]) => !shouldNotBeIncludedInSchema(includedCustomMutations, entry))
|
|
2351
|
+
.flatMap(([, { inputModel, outputModel }]) => [inputModel, outputModel].filter(Boolean)),
|
|
2352
|
+
];
|
|
2353
|
+
|
|
1863
2354
|
const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomMutations) => {
|
|
1864
2355
|
// Reject before models, resolvers or roots are built, so a rejected schema changes nothing.
|
|
1865
2356
|
Object.values(typesDict.types).forEach(({ gqltype }) => validateScope(gqltype));
|
|
@@ -1867,6 +2358,11 @@ const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomM
|
|
|
1867
2358
|
schemaTypes.forEach(assertNotBoundElsewhere);
|
|
1868
2359
|
schemaTypes.forEach(assertFieldsNotBoundElsewhere);
|
|
1869
2360
|
assertCollectionConnections();
|
|
2361
|
+
// An application type with the name of a field metadata type that an earlier schema fixed; one that
|
|
2362
|
+
// only generated types or the fields of an input object reach is found when the schema is
|
|
2363
|
+
// constructed.
|
|
2364
|
+
assertFieldMetadataNamesFree(includedApplicationTypes(includedQueryTypes, includedMutationTypes,
|
|
2365
|
+
includedCustomMutations));
|
|
1870
2366
|
const referencedTypes = markReferencedTypesForModelGeneration();
|
|
1871
2367
|
// Entities, which keep their own stored identity: connect() types and relation targets.
|
|
1872
2368
|
Object.values(typesDict.types).forEach((typeInfo) => {
|
|
@@ -1914,17 +2410,21 @@ const createSchema = (includedQueryTypes, includedMutationTypes, includedCustomM
|
|
|
1914
2410
|
const objectMemberTypes = new Set();
|
|
1915
2411
|
Object.values(typesDict.types).forEach(({ gqltype }) => installObjectMemberResolvers(gqltype, objectMemberTypes));
|
|
1916
2412
|
|
|
1917
|
-
|
|
2413
|
+
let query = buildRootQuery('RootQueryType', includedQueryTypes);
|
|
1918
2414
|
Object.values(typesDict.types).forEach(({ gqltype }) => {
|
|
1919
2415
|
if (gqltype && !applicationResolvedFields.has(gqltype)) {
|
|
1920
2416
|
applicationResolvedFields.set(gqltype, listApplicationResolvedFields(gqltype));
|
|
1921
2417
|
}
|
|
1922
2418
|
});
|
|
2419
|
+
const mutation = buildMutation('Mutation', includedMutationTypes, includedCustomMutations);
|
|
2420
|
+
// An application scalar named JSON that this schema contains also types aggregation results. Any
|
|
2421
|
+
// other type named JSON still collides, and GraphQLSchema reports the duplicate name.
|
|
2422
|
+
const applicationJSON = findSchemaType([query, mutation], 'JSON', QLTypeAggregationResult);
|
|
2423
|
+
if (applicationJSON instanceof GraphQLScalarType) {
|
|
2424
|
+
query = buildRootQuery('RootQueryType', includedQueryTypes, aggregationResultTypeFor(applicationJSON));
|
|
2425
|
+
}
|
|
1923
2426
|
|
|
1924
|
-
const schema =
|
|
1925
|
-
query,
|
|
1926
|
-
mutation: buildMutation('Mutation', includedMutationTypes, includedCustomMutations),
|
|
1927
|
-
});
|
|
2427
|
+
const schema = constructSchema(query, mutation);
|
|
1928
2428
|
// Reserve reachable types whose relation fields still have no resolver, such as an unregistered
|
|
1929
2429
|
// custom mutation result, so another runtime cannot later generate resolvers this schema executes.
|
|
1930
2430
|
schemaTypes.forEach((type) => {
|
|
@@ -2047,6 +2547,14 @@ const autoGenerateResolvers = (gqltype) => {
|
|
|
2047
2547
|
}
|
|
2048
2548
|
};
|
|
2049
2549
|
|
|
2550
|
+
// A replaced registration builds fresh inputs. Its cached collection projections may already
|
|
2551
|
+
// contain fields from the previous input graph; keep those alive only in earlier schemas.
|
|
2552
|
+
const invalidateCollectionInputs = (gqltype) => {
|
|
2553
|
+
for (const [name, cached] of excludedFieldInputTypes) {
|
|
2554
|
+
if (cached.itemType.name === gqltype.name) excludedFieldInputTypes.delete(name);
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2050
2558
|
const connect = (model, gqltype, simpleEntityEndpointName,
|
|
2051
2559
|
listEntitiesEndpointName, controller, onModelCreated, stateMachine) => {
|
|
2052
2560
|
const registration = {
|
|
@@ -2063,6 +2571,7 @@ const connect = (model, gqltype, simpleEntityEndpointName,
|
|
|
2063
2571
|
validateScope(gqltype);
|
|
2064
2572
|
if (adapter.validateRegistration) adapter.validateRegistration(registration);
|
|
2065
2573
|
|
|
2574
|
+
invalidateCollectionInputs(gqltype);
|
|
2066
2575
|
waitingInputType[gqltype.name] = {
|
|
2067
2576
|
model,
|
|
2068
2577
|
gqltype,
|
|
@@ -2092,6 +2601,7 @@ const addNoEndpointType = (gqltype) => {
|
|
|
2092
2601
|
validateScope(gqltype);
|
|
2093
2602
|
if (adapter.validateRegistration) adapter.validateRegistration(registration);
|
|
2094
2603
|
|
|
2604
|
+
invalidateCollectionInputs(gqltype);
|
|
2095
2605
|
waitingInputType[gqltype.name] = { gqltype };
|
|
2096
2606
|
typesDict.types[gqltype.name] = registration;
|
|
2097
2607
|
typesDictForUpdate.types[gqltype.name] = { ...typesDict.types[gqltype.name] };
|
|
@@ -2115,27 +2625,31 @@ const createArgsForQuery = (gqltype, omit = null) => {
|
|
|
2115
2625
|
|
|
2116
2626
|
for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
|
|
2117
2627
|
if (fieldEntryName === omit) continue;
|
|
2118
|
-
|
|
2628
|
+
let filterType = null;
|
|
2119
2629
|
|
|
2120
2630
|
if (fieldEntry.type instanceof GraphQLScalarType
|
|
2121
2631
|
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
2122
2632
|
|| fieldEntry.type instanceof GraphQLEnumType
|
|
2123
2633
|
|| isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
|
|
2124
|
-
|
|
2634
|
+
filterType = QLFilter;
|
|
2125
2635
|
} else if (fieldEntry.type instanceof GraphQLObjectType
|
|
2126
2636
|
|| isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
|
|
2127
|
-
|
|
2637
|
+
filterType = QLTypeFilterExpression;
|
|
2128
2638
|
} else if (getListShape(fieldEntry.type)) {
|
|
2129
2639
|
const listOfType = getListShape(fieldEntry.type).itemType;
|
|
2130
2640
|
if (listOfType instanceof GraphQLScalarType
|
|
2131
2641
|
|| isNonNullOfType(listOfType, GraphQLScalarType)
|
|
2132
2642
|
|| listOfType instanceof GraphQLEnumType
|
|
2133
2643
|
|| isNonNullOfType(listOfType, GraphQLEnumType)) {
|
|
2134
|
-
|
|
2644
|
+
filterType = QLFilter;
|
|
2135
2645
|
} else {
|
|
2136
|
-
|
|
2646
|
+
// Includes lists of interfaces and unions, which have no generated filter either. Their
|
|
2647
|
+
// argument has always been generated and is kept, so requests that send it as null stay valid.
|
|
2648
|
+
filterType = QLTypeFilterExpression;
|
|
2137
2649
|
}
|
|
2138
2650
|
}
|
|
2651
|
+
// A single interface or union field has no generated filter, so it gets no argument.
|
|
2652
|
+
if (filterType) argsObject[fieldEntryName] = { type: filterType };
|
|
2139
2653
|
}
|
|
2140
2654
|
|
|
2141
2655
|
argsObject.pagination = {};
|