@makerx/graphql-core 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -10
- package/context.d.cts +7 -0
- package/context.d.mts +7 -0
- package/context.d.ts +7 -0
- package/context.js +6 -5
- package/context.js.map +1 -1
- package/context.mjs +6 -5
- package/context.mjs.map +1 -1
- package/deprecation.d.cts +81 -0
- package/deprecation.d.mts +81 -0
- package/deprecation.d.ts +81 -0
- package/deprecation.js +240 -0
- package/deprecation.js.map +1 -0
- package/deprecation.mjs +235 -0
- package/deprecation.mjs.map +1 -0
- package/index.d.cts +1 -0
- package/index.d.mts +1 -0
- package/index.d.ts +1 -0
- package/index.js +5 -0
- package/index.js.map +1 -1
- package/index.mjs +1 -0
- package/index.mjs.map +1 -1
- package/logging.d.cts +17 -2
- package/logging.d.mts +17 -2
- package/logging.d.ts +17 -2
- package/logging.js +21 -3
- package/logging.js.map +1 -1
- package/logging.mjs +21 -3
- package/logging.mjs.map +1 -1
- package/package.json +1 -1
- package/subscriptions/context.d.cts +7 -0
- package/subscriptions/context.d.mts +7 -0
- package/subscriptions/context.d.ts +7 -0
- package/subscriptions/context.js +5 -5
- package/subscriptions/context.js.map +1 -1
- package/subscriptions/context.mjs +6 -6
- package/subscriptions/context.mjs.map +1 -1
- package/subscriptions/server.d.cts +13 -2
- package/subscriptions/server.d.mts +13 -2
- package/subscriptions/server.d.ts +13 -2
- package/subscriptions/server.js +12 -6
- package/subscriptions/server.js.map +1 -1
- package/subscriptions/server.mjs +13 -7
- package/subscriptions/server.mjs.map +1 -1
- package/subscriptions/utils.d.cts +2 -0
- package/subscriptions/utils.d.mts +2 -0
- package/subscriptions/utils.d.ts +2 -0
- package/subscriptions/utils.js +2 -0
- package/subscriptions/utils.js.map +1 -1
- package/subscriptions/utils.mjs +2 -1
- package/subscriptions/utils.mjs.map +1 -1
package/deprecation.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var graphql = require('graphql');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Safety valve for the variable walk. Recursion follows the schema's input types, which may be
|
|
7
|
+
* mutually recursive, and variables are walked before graphql-js coerces them — so nothing else
|
|
8
|
+
* bounds how deep a hand-crafted payload can go. Far above any real operation's nesting.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH = 25;
|
|
11
|
+
/**
|
|
12
|
+
* Bounds the *breadth* of the variable walk, which the depth cap does not: a single large array of
|
|
13
|
+
* small input objects is shallow but arbitrarily wide.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_DEPRECATION_MAX_VARIABLE_NODES = 10000;
|
|
16
|
+
/**
|
|
17
|
+
* Caps the collected result so one hostile document cannot blow up a log entry.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_DEPRECATION_MAX_ELEMENTS = 50;
|
|
20
|
+
const UNKNOWN = '<unknown>';
|
|
21
|
+
/**
|
|
22
|
+
* Reports every `@deprecated` schema element an operation uses, across both the document and the
|
|
23
|
+
* supplied variables.
|
|
24
|
+
*
|
|
25
|
+
* Output fields and arguments are named in the document, so an AST walk finds them. Input fields
|
|
26
|
+
* and enum values are data the caller *supplies* rather than selects: they can arrive as a document
|
|
27
|
+
* literal, or inside a variable where the name appears nowhere in the AST, so both routes are
|
|
28
|
+
* walked and the results deduplicated.
|
|
29
|
+
*
|
|
30
|
+
* Results are deduplicated on kind and name — at most one entry per distinct element per request,
|
|
31
|
+
* bounded by the size of the schema rather than the size of the request — and sorted, so records
|
|
32
|
+
* are stable across requests.
|
|
33
|
+
*
|
|
34
|
+
* Returns `truncated` when any limit stopped collection. Absence from `elements` is only evidence
|
|
35
|
+
* that an element went unused when `truncated` is false — which matters, because concluding
|
|
36
|
+
* "nothing uses this" from a truncated result is how a still-used element gets deleted.
|
|
37
|
+
*
|
|
38
|
+
* The variable walk reads untrusted input before coercion, so a payload whose shape contradicts its
|
|
39
|
+
* declared type reaches it. Every shape it depends on is guarded: malformed variables yield no
|
|
40
|
+
* records rather than an error.
|
|
41
|
+
*
|
|
42
|
+
* This is request-side telemetry — it observes values a caller *sends*, never values the server
|
|
43
|
+
* returns. A deprecated element that only ever appears in responses reads as unused.
|
|
44
|
+
*/
|
|
45
|
+
function collectDeprecatedElementUsage({ schema, document, operation, operationName, variables, maxVariableDepth = DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH, maxVariableNodes = DEFAULT_DEPRECATION_MAX_VARIABLE_NODES, maxElements = DEFAULT_DEPRECATION_MAX_ELEMENTS, }) {
|
|
46
|
+
const collected = new Map();
|
|
47
|
+
const walk = { maxVariableDepth, remaining: maxVariableNodes, truncated: false };
|
|
48
|
+
const collect = (usage) => {
|
|
49
|
+
const key = `${usage.kind}:${usage.name}`;
|
|
50
|
+
// Already recorded is deduplication, not truncation — only a rejected *new* element is a loss.
|
|
51
|
+
if (collected.has(key))
|
|
52
|
+
return;
|
|
53
|
+
if (collected.size >= maxElements) {
|
|
54
|
+
walk.truncated = true;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
collected.set(key, usage);
|
|
58
|
+
};
|
|
59
|
+
const resolvedOperation = operation ?? graphql.getOperationAST(document, operationName ?? undefined);
|
|
60
|
+
collectFromDocument(schema, scopeToOperation(document, resolvedOperation), collect);
|
|
61
|
+
collectFromVariables(schema, resolvedOperation?.variableDefinitions, variables, walk, collect);
|
|
62
|
+
return {
|
|
63
|
+
elements: [...collected.values()].sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)),
|
|
64
|
+
truncated: walk.truncated,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A document may carry several operations but only one runs, so anything the others select would be
|
|
69
|
+
* reported as usage that never happened. `separateOperations` keeps each operation with only the
|
|
70
|
+
* fragments it can reach, which is exactly the scope wanted here.
|
|
71
|
+
*/
|
|
72
|
+
function scopeToOperation(document, operation) {
|
|
73
|
+
if (!operation)
|
|
74
|
+
return document;
|
|
75
|
+
let operationCount = 0;
|
|
76
|
+
for (const definition of document.definitions) {
|
|
77
|
+
if (definition.kind === graphql.Kind.OPERATION_DEFINITION)
|
|
78
|
+
operationCount++;
|
|
79
|
+
if (operationCount > 1)
|
|
80
|
+
return graphql.separateOperations(document)[operation.name?.value ?? ''] ?? document;
|
|
81
|
+
}
|
|
82
|
+
return document;
|
|
83
|
+
}
|
|
84
|
+
function collectFromDocument(schema, document, collect) {
|
|
85
|
+
const typeInfo = new graphql.TypeInfo(schema);
|
|
86
|
+
graphql.visit(document, graphql.visitWithTypeInfo(typeInfo, {
|
|
87
|
+
Field(_node, _key, _parent, _path, ancestors) {
|
|
88
|
+
const field = typeInfo.getFieldDef();
|
|
89
|
+
if (field?.deprecationReason == null)
|
|
90
|
+
return;
|
|
91
|
+
collect({
|
|
92
|
+
kind: 'output-field',
|
|
93
|
+
name: `${typeInfo.getParentType()?.name ?? UNKNOWN}.${field.name}`,
|
|
94
|
+
path: getPath(ancestors, field.name),
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
Argument(_node, _key, _parent, _path, ancestors) {
|
|
98
|
+
const argument = typeInfo.getArgument();
|
|
99
|
+
if (argument?.deprecationReason == null)
|
|
100
|
+
return;
|
|
101
|
+
// TypeInfo resolves an argument against the enclosing directive when there is one, so
|
|
102
|
+
// without this branch a directive's argument is reported as an argument of the field the
|
|
103
|
+
// directive is attached to — a field argument that does not exist.
|
|
104
|
+
const directive = typeInfo.getDirective();
|
|
105
|
+
collect({
|
|
106
|
+
kind: directive ? 'directive-argument' : 'argument',
|
|
107
|
+
name: directive
|
|
108
|
+
? `@${directive.name}(${argument.name})`
|
|
109
|
+
: `${typeInfo.getParentType()?.name ?? UNKNOWN}.${typeInfo.getFieldDef()?.name ?? UNKNOWN}(${argument.name})`,
|
|
110
|
+
path: getPath(ancestors),
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
// Input-object fields written as literals in the document, including variable definition
|
|
114
|
+
// default values, which TypeInfo also tracks as input positions.
|
|
115
|
+
ObjectField(node, _key, _parent, _path, ancestors) {
|
|
116
|
+
const parentType = graphql.getNamedType(typeInfo.getParentInputType());
|
|
117
|
+
if (!graphql.isInputObjectType(parentType))
|
|
118
|
+
return;
|
|
119
|
+
const inputField = parentType.getFields()[node.name.value];
|
|
120
|
+
if (inputField?.deprecationReason == null)
|
|
121
|
+
return;
|
|
122
|
+
collect({
|
|
123
|
+
kind: 'input-field',
|
|
124
|
+
name: `${parentType.name}.${inputField.name}`,
|
|
125
|
+
path: getPath(ancestors, inputField.name),
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
EnumValue(node, _key, _parent, _path, ancestors) {
|
|
129
|
+
const enumType = graphql.getNamedType(typeInfo.getInputType());
|
|
130
|
+
if (!graphql.isEnumType(enumType))
|
|
131
|
+
return;
|
|
132
|
+
const enumValue = enumType.getValue(node.value);
|
|
133
|
+
if (enumValue?.deprecationReason == null)
|
|
134
|
+
return;
|
|
135
|
+
collect({
|
|
136
|
+
kind: 'enum-value',
|
|
137
|
+
name: `${enumType.name}.${enumValue.name}`,
|
|
138
|
+
path: getPath(ancestors),
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
function isPathNode(node) {
|
|
144
|
+
return node.kind === graphql.Kind.FIELD || node.kind === graphql.Kind.OBJECT_FIELD;
|
|
145
|
+
}
|
|
146
|
+
function getPath(ancestors, leaf) {
|
|
147
|
+
const segments = ancestors
|
|
148
|
+
.filter((x) => !Array.isArray(x))
|
|
149
|
+
.filter(isPathNode)
|
|
150
|
+
.map((x) => x.name.value);
|
|
151
|
+
if (leaf)
|
|
152
|
+
segments.push(leaf);
|
|
153
|
+
return segments.join('.');
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Walks each supplied variable value against its declared type, reporting every deprecated input
|
|
157
|
+
* field and enum value the client actually sent.
|
|
158
|
+
*
|
|
159
|
+
* Takes the definitions of the *resolved* operation rather than scanning the whole document: the
|
|
160
|
+
* same variable name may be declared with a different type in each operation, so only the executed
|
|
161
|
+
* one's declarations describe the supplied values.
|
|
162
|
+
*/
|
|
163
|
+
function collectFromVariables(schema, variableDefinitions, variables, state, collect) {
|
|
164
|
+
if (variables == null)
|
|
165
|
+
return;
|
|
166
|
+
for (const variableDefinition of variableDefinitions ?? []) {
|
|
167
|
+
const variableName = variableDefinition.variable.name.value;
|
|
168
|
+
if (!Object.hasOwn(variables, variableName))
|
|
169
|
+
continue;
|
|
170
|
+
const type = graphql.typeFromAST(schema, variableDefinition.type);
|
|
171
|
+
if (!graphql.isInputType(type))
|
|
172
|
+
continue;
|
|
173
|
+
walkInputValue(type, variables[variableName], `$${variableName}`, 0, state, collect);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function walkInputValue(type, value, path, depth, state, collect) {
|
|
177
|
+
// A limit stopping the descent is a loss of telemetry, so it is recorded; a null value is simply
|
|
178
|
+
// nothing to walk, and must not be reported as one.
|
|
179
|
+
if (depth > state.maxVariableDepth || state.remaining <= 0) {
|
|
180
|
+
state.truncated = true;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (value == null)
|
|
184
|
+
return;
|
|
185
|
+
state.remaining--;
|
|
186
|
+
// Unwrapped inline rather than by recursing, so a nullability wrapper doesn't spend a node from
|
|
187
|
+
// the budget for a value that has not been descended into yet.
|
|
188
|
+
const unwrapped = graphql.isNonNullType(type) ? type.ofType : type;
|
|
189
|
+
if (graphql.isListType(unwrapped)) {
|
|
190
|
+
// GraphQL accepts a bare value in a list position, so handle both shapes.
|
|
191
|
+
const items = Array.isArray(value) ? value : [value];
|
|
192
|
+
for (const [index, item] of items.entries()) {
|
|
193
|
+
// Checked here rather than relying on the recursive call returning early: the loop itself is
|
|
194
|
+
// the unbounded part, since list length comes from the payload and each step builds a path.
|
|
195
|
+
if (state.remaining <= 0) {
|
|
196
|
+
state.truncated = true;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
walkInputValue(unwrapped.ofType, item, Array.isArray(value) ? `${path}.${index}` : path, depth + 1, state, collect);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (graphql.isEnumType(unwrapped)) {
|
|
204
|
+
// An unknown name is a coercion error graphql-js raises moments later; nothing to record here.
|
|
205
|
+
if (typeof value !== 'string')
|
|
206
|
+
return;
|
|
207
|
+
const enumValue = unwrapped.getValue(value);
|
|
208
|
+
if (enumValue?.deprecationReason != null) {
|
|
209
|
+
collect({ kind: 'enum-value', name: `${unwrapped.name}.${enumValue.name}`, path });
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Scalars have no members that could carry a deprecation.
|
|
214
|
+
if (!graphql.isInputObjectType(unwrapped) || !isRecord(value))
|
|
215
|
+
return;
|
|
216
|
+
for (const field of Object.values(unwrapped.getFields())) {
|
|
217
|
+
if (state.remaining <= 0) {
|
|
218
|
+
state.truncated = true;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!Object.hasOwn(value, field.name))
|
|
222
|
+
continue;
|
|
223
|
+
const fieldPath = `${path}.${field.name}`;
|
|
224
|
+
// Presence is the signal, explicit `null` included: a client still sending the field would
|
|
225
|
+
// break if it were removed from the schema.
|
|
226
|
+
if (field.deprecationReason != null) {
|
|
227
|
+
collect({ kind: 'input-field', name: `${unwrapped.name}.${field.name}`, path: fieldPath });
|
|
228
|
+
}
|
|
229
|
+
walkInputValue(field.type, value[field.name], fieldPath, depth + 1, state, collect);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function isRecord(value) {
|
|
233
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
exports.DEFAULT_DEPRECATION_MAX_ELEMENTS = DEFAULT_DEPRECATION_MAX_ELEMENTS;
|
|
237
|
+
exports.DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH = DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH;
|
|
238
|
+
exports.DEFAULT_DEPRECATION_MAX_VARIABLE_NODES = DEFAULT_DEPRECATION_MAX_VARIABLE_NODES;
|
|
239
|
+
exports.collectDeprecatedElementUsage = collectDeprecatedElementUsage;
|
|
240
|
+
//# sourceMappingURL=deprecation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deprecation.js","sources":["../src/deprecation.ts"],"sourcesContent":["import type {\n ASTNode,\n DocumentNode,\n FieldNode,\n GraphQLInputType,\n GraphQLSchema,\n ObjectFieldNode,\n OperationDefinitionNode,\n VariableDefinitionNode,\n} from 'graphql'\nimport {\n getNamedType,\n getOperationAST,\n isEnumType,\n isInputObjectType,\n isInputType,\n isListType,\n isNonNullType,\n Kind,\n separateOperations,\n typeFromAST,\n TypeInfo,\n visit,\n visitWithTypeInfo,\n} from 'graphql'\n\n/**\n * Safety valve for the variable walk. Recursion follows the schema's input types, which may be\n * mutually recursive, and variables are walked before graphql-js coerces them — so nothing else\n * bounds how deep a hand-crafted payload can go. Far above any real operation's nesting.\n */\nexport const DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH = 25\n\n/**\n * Bounds the *breadth* of the variable walk, which the depth cap does not: a single large array of\n * small input objects is shallow but arbitrarily wide.\n */\nexport const DEFAULT_DEPRECATION_MAX_VARIABLE_NODES = 10_000\n\n/**\n * Caps the collected result so one hostile document cannot blow up a log entry.\n */\nexport const DEFAULT_DEPRECATION_MAX_ELEMENTS = 50\n\nconst UNKNOWN = '<unknown>'\n\nexport type DeprecatedElementKind = 'output-field' | 'argument' | 'directive-argument' | 'input-field' | 'enum-value'\n\nexport interface DeprecatedElementUsage {\n kind: DeprecatedElementKind\n /**\n * Stable key for aggregating usage, formatted per kind:\n * `Type.field`, `Type.field(arg)`, `@directive(arg)`, `InputType.field`, `EnumType.VALUE`.\n */\n name: string\n /**\n * Best-effort location, for debugging a single record — not an aggregation key. A field selected\n * inside a fragment definition has no enclosing field in its ancestry, so its path is relative to\n * the fragment rather than the operation.\n */\n path?: string\n}\n\nexport interface CollectDeprecatedElementUsageOptions {\n schema: GraphQLSchema\n document: DocumentNode\n /**\n * The executed operation. Resolved from `document` and `operationName` when omitted.\n */\n operation?: OperationDefinitionNode | null\n operationName?: string | null\n /**\n * Raw request variables, as supplied by the client and before graphql-js coerces them.\n */\n variables?: Record<string, unknown> | null\n maxVariableDepth?: number\n maxVariableNodes?: number\n maxElements?: number\n}\n\nexport interface CollectDeprecatedElementUsageResult {\n elements: DeprecatedElementUsage[]\n /**\n * True when a limit — `maxElements`, `maxVariableDepth` or `maxVariableNodes` — stopped\n * collection before it finished, so `elements` may be missing elements the operation really used.\n * Absence from a truncated result is not evidence that an element is unused.\n */\n truncated: boolean\n}\n\n/**\n * Reports every `@deprecated` schema element an operation uses, across both the document and the\n * supplied variables.\n *\n * Output fields and arguments are named in the document, so an AST walk finds them. Input fields\n * and enum values are data the caller *supplies* rather than selects: they can arrive as a document\n * literal, or inside a variable where the name appears nowhere in the AST, so both routes are\n * walked and the results deduplicated.\n *\n * Results are deduplicated on kind and name — at most one entry per distinct element per request,\n * bounded by the size of the schema rather than the size of the request — and sorted, so records\n * are stable across requests.\n *\n * Returns `truncated` when any limit stopped collection. Absence from `elements` is only evidence\n * that an element went unused when `truncated` is false — which matters, because concluding\n * \"nothing uses this\" from a truncated result is how a still-used element gets deleted.\n *\n * The variable walk reads untrusted input before coercion, so a payload whose shape contradicts its\n * declared type reaches it. Every shape it depends on is guarded: malformed variables yield no\n * records rather than an error.\n *\n * This is request-side telemetry — it observes values a caller *sends*, never values the server\n * returns. A deprecated element that only ever appears in responses reads as unused.\n */\nexport function collectDeprecatedElementUsage({\n schema,\n document,\n operation,\n operationName,\n variables,\n maxVariableDepth = DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH,\n maxVariableNodes = DEFAULT_DEPRECATION_MAX_VARIABLE_NODES,\n maxElements = DEFAULT_DEPRECATION_MAX_ELEMENTS,\n}: CollectDeprecatedElementUsageOptions): CollectDeprecatedElementUsageResult {\n const collected = new Map<string, DeprecatedElementUsage>()\n const walk: WalkState = { maxVariableDepth, remaining: maxVariableNodes, truncated: false }\n\n const collect = (usage: DeprecatedElementUsage): void => {\n const key = `${usage.kind}:${usage.name}`\n // Already recorded is deduplication, not truncation — only a rejected *new* element is a loss.\n if (collected.has(key)) return\n if (collected.size >= maxElements) {\n walk.truncated = true\n return\n }\n collected.set(key, usage)\n }\n\n const resolvedOperation = operation ?? getOperationAST(document, operationName ?? undefined)\n\n collectFromDocument(schema, scopeToOperation(document, resolvedOperation), collect)\n collectFromVariables(schema, resolvedOperation?.variableDefinitions, variables, walk, collect)\n\n return {\n elements: [...collected.values()].sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)),\n truncated: walk.truncated,\n }\n}\n\ntype Collect = (usage: DeprecatedElementUsage) => void\n\n/**\n * A document may carry several operations but only one runs, so anything the others select would be\n * reported as usage that never happened. `separateOperations` keeps each operation with only the\n * fragments it can reach, which is exactly the scope wanted here.\n */\nfunction scopeToOperation(document: DocumentNode, operation: OperationDefinitionNode | null | undefined): DocumentNode {\n if (!operation) return document\n\n let operationCount = 0\n for (const definition of document.definitions) {\n if (definition.kind === Kind.OPERATION_DEFINITION) operationCount++\n if (operationCount > 1) return separateOperations(document)[operation.name?.value ?? ''] ?? document\n }\n return document\n}\n\nfunction collectFromDocument(schema: GraphQLSchema, document: DocumentNode, collect: Collect): void {\n const typeInfo = new TypeInfo(schema)\n\n visit(\n document,\n visitWithTypeInfo(typeInfo, {\n Field(_node, _key, _parent, _path, ancestors) {\n const field = typeInfo.getFieldDef()\n if (field?.deprecationReason == null) return\n collect({\n kind: 'output-field',\n name: `${typeInfo.getParentType()?.name ?? UNKNOWN}.${field.name}`,\n path: getPath(ancestors, field.name),\n })\n },\n Argument(_node, _key, _parent, _path, ancestors) {\n const argument = typeInfo.getArgument()\n if (argument?.deprecationReason == null) return\n // TypeInfo resolves an argument against the enclosing directive when there is one, so\n // without this branch a directive's argument is reported as an argument of the field the\n // directive is attached to — a field argument that does not exist.\n const directive = typeInfo.getDirective()\n collect({\n kind: directive ? 'directive-argument' : 'argument',\n name: directive\n ? `@${directive.name}(${argument.name})`\n : `${typeInfo.getParentType()?.name ?? UNKNOWN}.${typeInfo.getFieldDef()?.name ?? UNKNOWN}(${argument.name})`,\n path: getPath(ancestors),\n })\n },\n // Input-object fields written as literals in the document, including variable definition\n // default values, which TypeInfo also tracks as input positions.\n ObjectField(node, _key, _parent, _path, ancestors) {\n const parentType = getNamedType(typeInfo.getParentInputType())\n if (!isInputObjectType(parentType)) return\n const inputField = parentType.getFields()[node.name.value]\n if (inputField?.deprecationReason == null) return\n collect({\n kind: 'input-field',\n name: `${parentType.name}.${inputField.name}`,\n path: getPath(ancestors, inputField.name),\n })\n },\n EnumValue(node, _key, _parent, _path, ancestors) {\n const enumType = getNamedType(typeInfo.getInputType())\n if (!isEnumType(enumType)) return\n const enumValue = enumType.getValue(node.value)\n if (enumValue?.deprecationReason == null) return\n collect({\n kind: 'enum-value',\n name: `${enumType.name}.${enumValue.name}`,\n path: getPath(ancestors),\n })\n },\n }),\n )\n}\n\ntype PathNode = FieldNode | ObjectFieldNode\n\nfunction isPathNode(node: ASTNode): node is PathNode {\n return node.kind === Kind.FIELD || node.kind === Kind.OBJECT_FIELD\n}\n\nfunction getPath(ancestors: readonly (ASTNode | readonly ASTNode[])[], leaf?: string): string {\n const segments = ancestors\n .filter((x): x is ASTNode => !Array.isArray(x))\n .filter(isPathNode)\n .map((x) => x.name.value)\n\n if (leaf) segments.push(leaf)\n\n return segments.join('.')\n}\n\ninterface WalkState {\n maxVariableDepth: number\n /** Remaining node budget, shared across every variable of the operation. */\n remaining: number\n truncated: boolean\n}\n\n/**\n * Walks each supplied variable value against its declared type, reporting every deprecated input\n * field and enum value the client actually sent.\n *\n * Takes the definitions of the *resolved* operation rather than scanning the whole document: the\n * same variable name may be declared with a different type in each operation, so only the executed\n * one's declarations describe the supplied values.\n */\nfunction collectFromVariables(\n schema: GraphQLSchema,\n variableDefinitions: readonly VariableDefinitionNode[] | undefined,\n variables: Record<string, unknown> | null | undefined,\n state: WalkState,\n collect: Collect,\n): void {\n if (variables == null) return\n\n for (const variableDefinition of variableDefinitions ?? []) {\n const variableName = variableDefinition.variable.name.value\n if (!Object.hasOwn(variables, variableName)) continue\n\n const type = typeFromAST(schema, variableDefinition.type)\n if (!isInputType(type)) continue\n\n walkInputValue(type, variables[variableName], `$${variableName}`, 0, state, collect)\n }\n}\n\nfunction walkInputValue(type: GraphQLInputType, value: unknown, path: string, depth: number, state: WalkState, collect: Collect): void {\n // A limit stopping the descent is a loss of telemetry, so it is recorded; a null value is simply\n // nothing to walk, and must not be reported as one.\n if (depth > state.maxVariableDepth || state.remaining <= 0) {\n state.truncated = true\n return\n }\n if (value == null) return\n state.remaining--\n\n // Unwrapped inline rather than by recursing, so a nullability wrapper doesn't spend a node from\n // the budget for a value that has not been descended into yet.\n const unwrapped = isNonNullType(type) ? type.ofType : type\n\n if (isListType(unwrapped)) {\n // GraphQL accepts a bare value in a list position, so handle both shapes.\n const items = Array.isArray(value) ? value : [value]\n for (const [index, item] of items.entries()) {\n // Checked here rather than relying on the recursive call returning early: the loop itself is\n // the unbounded part, since list length comes from the payload and each step builds a path.\n if (state.remaining <= 0) {\n state.truncated = true\n return\n }\n walkInputValue(unwrapped.ofType, item, Array.isArray(value) ? `${path}.${index}` : path, depth + 1, state, collect)\n }\n return\n }\n\n if (isEnumType(unwrapped)) {\n // An unknown name is a coercion error graphql-js raises moments later; nothing to record here.\n if (typeof value !== 'string') return\n const enumValue = unwrapped.getValue(value)\n if (enumValue?.deprecationReason != null) {\n collect({ kind: 'enum-value', name: `${unwrapped.name}.${enumValue.name}`, path })\n }\n return\n }\n\n // Scalars have no members that could carry a deprecation.\n if (!isInputObjectType(unwrapped) || !isRecord(value)) return\n\n for (const field of Object.values(unwrapped.getFields())) {\n if (state.remaining <= 0) {\n state.truncated = true\n return\n }\n if (!Object.hasOwn(value, field.name)) continue\n\n const fieldPath = `${path}.${field.name}`\n // Presence is the signal, explicit `null` included: a client still sending the field would\n // break if it were removed from the schema.\n if (field.deprecationReason != null) {\n collect({ kind: 'input-field', name: `${unwrapped.name}.${field.name}`, path: fieldPath })\n }\n\n walkInputValue(field.type, value[field.name], fieldPath, depth + 1, state, collect)\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n"],"names":["getOperationAST","Kind","separateOperations","TypeInfo","visit","visitWithTypeInfo","getNamedType","isInputObjectType","isEnumType","typeFromAST","isInputType","isNonNullType","isListType"],"mappings":";;;;AA0BA;;;;AAIG;AACI,MAAM,sCAAsC,GAAG;AAEtD;;;AAGG;AACI,MAAM,sCAAsC,GAAG;AAEtD;;AAEG;AACI,MAAM,gCAAgC,GAAG;AAEhD,MAAM,OAAO,GAAG,WAAW;AA8C3B;;;;;;;;;;;;;;;;;;;;;;;AAuBG;AACG,SAAU,6BAA6B,CAAC,EAC5C,MAAM,EACN,QAAQ,EACR,SAAS,EACT,aAAa,EACb,SAAS,EACT,gBAAgB,GAAG,sCAAsC,EACzD,gBAAgB,GAAG,sCAAsC,EACzD,WAAW,GAAG,gCAAgC,GACT,EAAA;AACrC,IAAA,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkC;AAC3D,IAAA,MAAM,IAAI,GAAc,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,SAAS,EAAE,KAAK,EAAE;AAE3F,IAAA,MAAM,OAAO,GAAG,CAAC,KAA6B,KAAU;QACtD,MAAM,GAAG,GAAG,CAAA,EAAG,KAAK,CAAC,IAAI,CAAA,CAAA,EAAI,KAAK,CAAC,IAAI,CAAA,CAAE;;AAEzC,QAAA,IAAI,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE;AACxB,QAAA,IAAI,SAAS,CAAC,IAAI,IAAI,WAAW,EAAE;AACjC,YAAA,IAAI,CAAC,SAAS,GAAG,IAAI;YACrB;QACF;AACA,QAAA,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC;AAC3B,IAAA,CAAC;AAED,IAAA,MAAM,iBAAiB,GAAG,SAAS,IAAIA,uBAAe,CAAC,QAAQ,EAAE,aAAa,IAAI,SAAS,CAAC;AAE5F,IAAA,mBAAmB,CAAC,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC;AACnF,IAAA,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC;IAE9F,OAAO;AACL,QAAA,QAAQ,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9G,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B;AACH;AAIA;;;;AAIG;AACH,SAAS,gBAAgB,CAAC,QAAsB,EAAE,SAAqD,EAAA;AACrG,IAAA,IAAI,CAAC,SAAS;AAAE,QAAA,OAAO,QAAQ;IAE/B,IAAI,cAAc,GAAG,CAAC;AACtB,IAAA,KAAK,MAAM,UAAU,IAAI,QAAQ,CAAC,WAAW,EAAE;AAC7C,QAAA,IAAI,UAAU,CAAC,IAAI,KAAKC,YAAI,CAAC,oBAAoB;AAAE,YAAA,cAAc,EAAE;QACnE,IAAI,cAAc,GAAG,CAAC;AAAE,YAAA,OAAOC,0BAAkB,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,QAAQ;IACtG;AACA,IAAA,OAAO,QAAQ;AACjB;AAEA,SAAS,mBAAmB,CAAC,MAAqB,EAAE,QAAsB,EAAE,OAAgB,EAAA;AAC1F,IAAA,MAAM,QAAQ,GAAG,IAAIC,gBAAQ,CAAC,MAAM,CAAC;AAErC,IAAAC,aAAK,CACH,QAAQ,EACRC,yBAAiB,CAAC,QAAQ,EAAE;QAC1B,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAA;AAC1C,YAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE;AACpC,YAAA,IAAI,KAAK,EAAE,iBAAiB,IAAI,IAAI;gBAAE;AACtC,YAAA,OAAO,CAAC;AACN,gBAAA,IAAI,EAAE,cAAc;AACpB,gBAAA,IAAI,EAAE,CAAA,EAAG,QAAQ,CAAC,aAAa,EAAE,EAAE,IAAI,IAAI,OAAO,CAAA,CAAA,EAAI,KAAK,CAAC,IAAI,CAAA,CAAE;gBAClE,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC;AACrC,aAAA,CAAC;QACJ,CAAC;QACD,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAA;AAC7C,YAAA,MAAM,QAAQ,GAAG,QAAQ,CAAC,WAAW,EAAE;AACvC,YAAA,IAAI,QAAQ,EAAE,iBAAiB,IAAI,IAAI;gBAAE;;;;AAIzC,YAAA,MAAM,SAAS,GAAG,QAAQ,CAAC,YAAY,EAAE;AACzC,YAAA,OAAO,CAAC;gBACN,IAAI,EAAE,SAAS,GAAG,oBAAoB,GAAG,UAAU;AACnD,gBAAA,IAAI,EAAE;sBACF,IAAI,SAAS,CAAC,IAAI,CAAA,CAAA,EAAI,QAAQ,CAAC,IAAI,CAAA,CAAA;sBACnC,GAAG,QAAQ,CAAC,aAAa,EAAE,EAAE,IAAI,IAAI,OAAO,CAAA,CAAA,EAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,IAAI,IAAI,OAAO,CAAA,CAAA,EAAI,QAAQ,CAAC,IAAI,CAAA,CAAA,CAAG;AAC/G,gBAAA,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC;AACzB,aAAA,CAAC;QACJ,CAAC;;;QAGD,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAA;YAC/C,MAAM,UAAU,GAAGC,oBAAY,CAAC,QAAQ,CAAC,kBAAkB,EAAE,CAAC;AAC9D,YAAA,IAAI,CAACC,yBAAiB,CAAC,UAAU,CAAC;gBAAE;AACpC,YAAA,MAAM,UAAU,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;AAC1D,YAAA,IAAI,UAAU,EAAE,iBAAiB,IAAI,IAAI;gBAAE;AAC3C,YAAA,OAAO,CAAC;AACN,gBAAA,IAAI,EAAE,aAAa;gBACnB,IAAI,EAAE,GAAG,UAAU,CAAC,IAAI,CAAA,CAAA,EAAI,UAAU,CAAC,IAAI,CAAA,CAAE;gBAC7C,IAAI,EAAE,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC;AAC1C,aAAA,CAAC;QACJ,CAAC;QACD,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAA;YAC7C,MAAM,QAAQ,GAAGD,oBAAY,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC;AACtD,YAAA,IAAI,CAACE,kBAAU,CAAC,QAAQ,CAAC;gBAAE;YAC3B,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;AAC/C,YAAA,IAAI,SAAS,EAAE,iBAAiB,IAAI,IAAI;gBAAE;AAC1C,YAAA,OAAO,CAAC;AACN,gBAAA,IAAI,EAAE,YAAY;gBAClB,IAAI,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAA,CAAA,EAAI,SAAS,CAAC,IAAI,CAAA,CAAE;AAC1C,gBAAA,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC;AACzB,aAAA,CAAC;QACJ,CAAC;AACF,KAAA,CAAC,CACH;AACH;AAIA,SAAS,UAAU,CAAC,IAAa,EAAA;AAC/B,IAAA,OAAO,IAAI,CAAC,IAAI,KAAKP,YAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,KAAKA,YAAI,CAAC,YAAY;AACpE;AAEA,SAAS,OAAO,CAAC,SAAoD,EAAE,IAAa,EAAA;IAClF,MAAM,QAAQ,GAAG;AACd,SAAA,MAAM,CAAC,CAAC,CAAC,KAAmB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;SAC7C,MAAM,CAAC,UAAU;AACjB,SAAA,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;AAE3B,IAAA,IAAI,IAAI;AAAE,QAAA,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;AAE7B,IAAA,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;AAC3B;AASA;;;;;;;AAOG;AACH,SAAS,oBAAoB,CAC3B,MAAqB,EACrB,mBAAkE,EAClE,SAAqD,EACrD,KAAgB,EAChB,OAAgB,EAAA;IAEhB,IAAI,SAAS,IAAI,IAAI;QAAE;AAEvB,IAAA,KAAK,MAAM,kBAAkB,IAAI,mBAAmB,IAAI,EAAE,EAAE;QAC1D,MAAM,YAAY,GAAG,kBAAkB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK;QAC3D,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC;YAAE;QAE7C,MAAM,IAAI,GAAGQ,mBAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,IAAI,CAAC;AACzD,QAAA,IAAI,CAACC,mBAAW,CAAC,IAAI,CAAC;YAAE;AAExB,QAAA,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC,YAAY,CAAC,EAAE,CAAA,CAAA,EAAI,YAAY,CAAA,CAAE,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC;IACtF;AACF;AAEA,SAAS,cAAc,CAAC,IAAsB,EAAE,KAAc,EAAE,IAAY,EAAE,KAAa,EAAE,KAAgB,EAAE,OAAgB,EAAA;;;AAG7H,IAAA,IAAI,KAAK,GAAG,KAAK,CAAC,gBAAgB,IAAI,KAAK,CAAC,SAAS,IAAI,CAAC,EAAE;AAC1D,QAAA,KAAK,CAAC,SAAS,GAAG,IAAI;QACtB;IACF;IACA,IAAI,KAAK,IAAI,IAAI;QAAE;IACnB,KAAK,CAAC,SAAS,EAAE;;;AAIjB,IAAA,MAAM,SAAS,GAAGC,qBAAa,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI;AAE1D,IAAA,IAAIC,kBAAU,CAAC,SAAS,CAAC,EAAE;;AAEzB,QAAA,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,CAAC,KAAK,CAAC;AACpD,QAAA,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE;;;AAG3C,YAAA,IAAI,KAAK,CAAC,SAAS,IAAI,CAAC,EAAE;AACxB,gBAAA,KAAK,CAAC,SAAS,GAAG,IAAI;gBACtB;YACF;AACA,YAAA,cAAc,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAE,GAAG,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC;QACrH;QACA;IACF;AAEA,IAAA,IAAIJ,kBAAU,CAAC,SAAS,CAAC,EAAE;;QAEzB,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE;QAC/B,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC;AAC3C,QAAA,IAAI,SAAS,EAAE,iBAAiB,IAAI,IAAI,EAAE;YACxC,OAAO,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAA,EAAG,SAAS,CAAC,IAAI,CAAA,CAAA,EAAI,SAAS,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC;QACpF;QACA;IACF;;IAGA,IAAI,CAACD,yBAAiB,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE;AAEvD,IAAA,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC,EAAE;AACxD,QAAA,IAAI,KAAK,CAAC,SAAS,IAAI,CAAC,EAAE;AACxB,YAAA,KAAK,CAAC,SAAS,GAAG,IAAI;YACtB;QACF;QACA,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC;YAAE;QAEvC,MAAM,SAAS,GAAG,CAAA,EAAG,IAAI,IAAI,KAAK,CAAC,IAAI,CAAA,CAAE;;;AAGzC,QAAA,IAAI,KAAK,CAAC,iBAAiB,IAAI,IAAI,EAAE;YACnC,OAAO,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAA,EAAG,SAAS,CAAC,IAAI,CAAA,CAAA,EAAI,KAAK,CAAC,IAAI,CAAA,CAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAC5F;QAEA,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC;IACrF;AACF;AAEA,SAAS,QAAQ,CAAC,KAAc,EAAA;AAC9B,IAAA,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;AAC7E;;;;;;;"}
|
package/deprecation.mjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { getOperationAST, TypeInfo, visit, visitWithTypeInfo, getNamedType, isEnumType, isInputObjectType, separateOperations, typeFromAST, isInputType, isNonNullType, isListType, Kind } from 'graphql';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safety valve for the variable walk. Recursion follows the schema's input types, which may be
|
|
5
|
+
* mutually recursive, and variables are walked before graphql-js coerces them — so nothing else
|
|
6
|
+
* bounds how deep a hand-crafted payload can go. Far above any real operation's nesting.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH = 25;
|
|
9
|
+
/**
|
|
10
|
+
* Bounds the *breadth* of the variable walk, which the depth cap does not: a single large array of
|
|
11
|
+
* small input objects is shallow but arbitrarily wide.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_DEPRECATION_MAX_VARIABLE_NODES = 10000;
|
|
14
|
+
/**
|
|
15
|
+
* Caps the collected result so one hostile document cannot blow up a log entry.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_DEPRECATION_MAX_ELEMENTS = 50;
|
|
18
|
+
const UNKNOWN = '<unknown>';
|
|
19
|
+
/**
|
|
20
|
+
* Reports every `@deprecated` schema element an operation uses, across both the document and the
|
|
21
|
+
* supplied variables.
|
|
22
|
+
*
|
|
23
|
+
* Output fields and arguments are named in the document, so an AST walk finds them. Input fields
|
|
24
|
+
* and enum values are data the caller *supplies* rather than selects: they can arrive as a document
|
|
25
|
+
* literal, or inside a variable where the name appears nowhere in the AST, so both routes are
|
|
26
|
+
* walked and the results deduplicated.
|
|
27
|
+
*
|
|
28
|
+
* Results are deduplicated on kind and name — at most one entry per distinct element per request,
|
|
29
|
+
* bounded by the size of the schema rather than the size of the request — and sorted, so records
|
|
30
|
+
* are stable across requests.
|
|
31
|
+
*
|
|
32
|
+
* Returns `truncated` when any limit stopped collection. Absence from `elements` is only evidence
|
|
33
|
+
* that an element went unused when `truncated` is false — which matters, because concluding
|
|
34
|
+
* "nothing uses this" from a truncated result is how a still-used element gets deleted.
|
|
35
|
+
*
|
|
36
|
+
* The variable walk reads untrusted input before coercion, so a payload whose shape contradicts its
|
|
37
|
+
* declared type reaches it. Every shape it depends on is guarded: malformed variables yield no
|
|
38
|
+
* records rather than an error.
|
|
39
|
+
*
|
|
40
|
+
* This is request-side telemetry — it observes values a caller *sends*, never values the server
|
|
41
|
+
* returns. A deprecated element that only ever appears in responses reads as unused.
|
|
42
|
+
*/
|
|
43
|
+
function collectDeprecatedElementUsage({ schema, document, operation, operationName, variables, maxVariableDepth = DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH, maxVariableNodes = DEFAULT_DEPRECATION_MAX_VARIABLE_NODES, maxElements = DEFAULT_DEPRECATION_MAX_ELEMENTS, }) {
|
|
44
|
+
const collected = new Map();
|
|
45
|
+
const walk = { maxVariableDepth, remaining: maxVariableNodes, truncated: false };
|
|
46
|
+
const collect = (usage) => {
|
|
47
|
+
const key = `${usage.kind}:${usage.name}`;
|
|
48
|
+
// Already recorded is deduplication, not truncation — only a rejected *new* element is a loss.
|
|
49
|
+
if (collected.has(key))
|
|
50
|
+
return;
|
|
51
|
+
if (collected.size >= maxElements) {
|
|
52
|
+
walk.truncated = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
collected.set(key, usage);
|
|
56
|
+
};
|
|
57
|
+
const resolvedOperation = operation ?? getOperationAST(document, operationName ?? undefined);
|
|
58
|
+
collectFromDocument(schema, scopeToOperation(document, resolvedOperation), collect);
|
|
59
|
+
collectFromVariables(schema, resolvedOperation?.variableDefinitions, variables, walk, collect);
|
|
60
|
+
return {
|
|
61
|
+
elements: [...collected.values()].sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)),
|
|
62
|
+
truncated: walk.truncated,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A document may carry several operations but only one runs, so anything the others select would be
|
|
67
|
+
* reported as usage that never happened. `separateOperations` keeps each operation with only the
|
|
68
|
+
* fragments it can reach, which is exactly the scope wanted here.
|
|
69
|
+
*/
|
|
70
|
+
function scopeToOperation(document, operation) {
|
|
71
|
+
if (!operation)
|
|
72
|
+
return document;
|
|
73
|
+
let operationCount = 0;
|
|
74
|
+
for (const definition of document.definitions) {
|
|
75
|
+
if (definition.kind === Kind.OPERATION_DEFINITION)
|
|
76
|
+
operationCount++;
|
|
77
|
+
if (operationCount > 1)
|
|
78
|
+
return separateOperations(document)[operation.name?.value ?? ''] ?? document;
|
|
79
|
+
}
|
|
80
|
+
return document;
|
|
81
|
+
}
|
|
82
|
+
function collectFromDocument(schema, document, collect) {
|
|
83
|
+
const typeInfo = new TypeInfo(schema);
|
|
84
|
+
visit(document, visitWithTypeInfo(typeInfo, {
|
|
85
|
+
Field(_node, _key, _parent, _path, ancestors) {
|
|
86
|
+
const field = typeInfo.getFieldDef();
|
|
87
|
+
if (field?.deprecationReason == null)
|
|
88
|
+
return;
|
|
89
|
+
collect({
|
|
90
|
+
kind: 'output-field',
|
|
91
|
+
name: `${typeInfo.getParentType()?.name ?? UNKNOWN}.${field.name}`,
|
|
92
|
+
path: getPath(ancestors, field.name),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
Argument(_node, _key, _parent, _path, ancestors) {
|
|
96
|
+
const argument = typeInfo.getArgument();
|
|
97
|
+
if (argument?.deprecationReason == null)
|
|
98
|
+
return;
|
|
99
|
+
// TypeInfo resolves an argument against the enclosing directive when there is one, so
|
|
100
|
+
// without this branch a directive's argument is reported as an argument of the field the
|
|
101
|
+
// directive is attached to — a field argument that does not exist.
|
|
102
|
+
const directive = typeInfo.getDirective();
|
|
103
|
+
collect({
|
|
104
|
+
kind: directive ? 'directive-argument' : 'argument',
|
|
105
|
+
name: directive
|
|
106
|
+
? `@${directive.name}(${argument.name})`
|
|
107
|
+
: `${typeInfo.getParentType()?.name ?? UNKNOWN}.${typeInfo.getFieldDef()?.name ?? UNKNOWN}(${argument.name})`,
|
|
108
|
+
path: getPath(ancestors),
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
// Input-object fields written as literals in the document, including variable definition
|
|
112
|
+
// default values, which TypeInfo also tracks as input positions.
|
|
113
|
+
ObjectField(node, _key, _parent, _path, ancestors) {
|
|
114
|
+
const parentType = getNamedType(typeInfo.getParentInputType());
|
|
115
|
+
if (!isInputObjectType(parentType))
|
|
116
|
+
return;
|
|
117
|
+
const inputField = parentType.getFields()[node.name.value];
|
|
118
|
+
if (inputField?.deprecationReason == null)
|
|
119
|
+
return;
|
|
120
|
+
collect({
|
|
121
|
+
kind: 'input-field',
|
|
122
|
+
name: `${parentType.name}.${inputField.name}`,
|
|
123
|
+
path: getPath(ancestors, inputField.name),
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
EnumValue(node, _key, _parent, _path, ancestors) {
|
|
127
|
+
const enumType = getNamedType(typeInfo.getInputType());
|
|
128
|
+
if (!isEnumType(enumType))
|
|
129
|
+
return;
|
|
130
|
+
const enumValue = enumType.getValue(node.value);
|
|
131
|
+
if (enumValue?.deprecationReason == null)
|
|
132
|
+
return;
|
|
133
|
+
collect({
|
|
134
|
+
kind: 'enum-value',
|
|
135
|
+
name: `${enumType.name}.${enumValue.name}`,
|
|
136
|
+
path: getPath(ancestors),
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
function isPathNode(node) {
|
|
142
|
+
return node.kind === Kind.FIELD || node.kind === Kind.OBJECT_FIELD;
|
|
143
|
+
}
|
|
144
|
+
function getPath(ancestors, leaf) {
|
|
145
|
+
const segments = ancestors
|
|
146
|
+
.filter((x) => !Array.isArray(x))
|
|
147
|
+
.filter(isPathNode)
|
|
148
|
+
.map((x) => x.name.value);
|
|
149
|
+
if (leaf)
|
|
150
|
+
segments.push(leaf);
|
|
151
|
+
return segments.join('.');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Walks each supplied variable value against its declared type, reporting every deprecated input
|
|
155
|
+
* field and enum value the client actually sent.
|
|
156
|
+
*
|
|
157
|
+
* Takes the definitions of the *resolved* operation rather than scanning the whole document: the
|
|
158
|
+
* same variable name may be declared with a different type in each operation, so only the executed
|
|
159
|
+
* one's declarations describe the supplied values.
|
|
160
|
+
*/
|
|
161
|
+
function collectFromVariables(schema, variableDefinitions, variables, state, collect) {
|
|
162
|
+
if (variables == null)
|
|
163
|
+
return;
|
|
164
|
+
for (const variableDefinition of variableDefinitions ?? []) {
|
|
165
|
+
const variableName = variableDefinition.variable.name.value;
|
|
166
|
+
if (!Object.hasOwn(variables, variableName))
|
|
167
|
+
continue;
|
|
168
|
+
const type = typeFromAST(schema, variableDefinition.type);
|
|
169
|
+
if (!isInputType(type))
|
|
170
|
+
continue;
|
|
171
|
+
walkInputValue(type, variables[variableName], `$${variableName}`, 0, state, collect);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function walkInputValue(type, value, path, depth, state, collect) {
|
|
175
|
+
// A limit stopping the descent is a loss of telemetry, so it is recorded; a null value is simply
|
|
176
|
+
// nothing to walk, and must not be reported as one.
|
|
177
|
+
if (depth > state.maxVariableDepth || state.remaining <= 0) {
|
|
178
|
+
state.truncated = true;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (value == null)
|
|
182
|
+
return;
|
|
183
|
+
state.remaining--;
|
|
184
|
+
// Unwrapped inline rather than by recursing, so a nullability wrapper doesn't spend a node from
|
|
185
|
+
// the budget for a value that has not been descended into yet.
|
|
186
|
+
const unwrapped = isNonNullType(type) ? type.ofType : type;
|
|
187
|
+
if (isListType(unwrapped)) {
|
|
188
|
+
// GraphQL accepts a bare value in a list position, so handle both shapes.
|
|
189
|
+
const items = Array.isArray(value) ? value : [value];
|
|
190
|
+
for (const [index, item] of items.entries()) {
|
|
191
|
+
// Checked here rather than relying on the recursive call returning early: the loop itself is
|
|
192
|
+
// the unbounded part, since list length comes from the payload and each step builds a path.
|
|
193
|
+
if (state.remaining <= 0) {
|
|
194
|
+
state.truncated = true;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
walkInputValue(unwrapped.ofType, item, Array.isArray(value) ? `${path}.${index}` : path, depth + 1, state, collect);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (isEnumType(unwrapped)) {
|
|
202
|
+
// An unknown name is a coercion error graphql-js raises moments later; nothing to record here.
|
|
203
|
+
if (typeof value !== 'string')
|
|
204
|
+
return;
|
|
205
|
+
const enumValue = unwrapped.getValue(value);
|
|
206
|
+
if (enumValue?.deprecationReason != null) {
|
|
207
|
+
collect({ kind: 'enum-value', name: `${unwrapped.name}.${enumValue.name}`, path });
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Scalars have no members that could carry a deprecation.
|
|
212
|
+
if (!isInputObjectType(unwrapped) || !isRecord(value))
|
|
213
|
+
return;
|
|
214
|
+
for (const field of Object.values(unwrapped.getFields())) {
|
|
215
|
+
if (state.remaining <= 0) {
|
|
216
|
+
state.truncated = true;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!Object.hasOwn(value, field.name))
|
|
220
|
+
continue;
|
|
221
|
+
const fieldPath = `${path}.${field.name}`;
|
|
222
|
+
// Presence is the signal, explicit `null` included: a client still sending the field would
|
|
223
|
+
// break if it were removed from the schema.
|
|
224
|
+
if (field.deprecationReason != null) {
|
|
225
|
+
collect({ kind: 'input-field', name: `${unwrapped.name}.${field.name}`, path: fieldPath });
|
|
226
|
+
}
|
|
227
|
+
walkInputValue(field.type, value[field.name], fieldPath, depth + 1, state, collect);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function isRecord(value) {
|
|
231
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { DEFAULT_DEPRECATION_MAX_ELEMENTS, DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH, DEFAULT_DEPRECATION_MAX_VARIABLE_NODES, collectDeprecatedElementUsage };
|
|
235
|
+
//# sourceMappingURL=deprecation.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deprecation.mjs","sources":["../src/deprecation.ts"],"sourcesContent":["import type {\n ASTNode,\n DocumentNode,\n FieldNode,\n GraphQLInputType,\n GraphQLSchema,\n ObjectFieldNode,\n OperationDefinitionNode,\n VariableDefinitionNode,\n} from 'graphql'\nimport {\n getNamedType,\n getOperationAST,\n isEnumType,\n isInputObjectType,\n isInputType,\n isListType,\n isNonNullType,\n Kind,\n separateOperations,\n typeFromAST,\n TypeInfo,\n visit,\n visitWithTypeInfo,\n} from 'graphql'\n\n/**\n * Safety valve for the variable walk. Recursion follows the schema's input types, which may be\n * mutually recursive, and variables are walked before graphql-js coerces them — so nothing else\n * bounds how deep a hand-crafted payload can go. Far above any real operation's nesting.\n */\nexport const DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH = 25\n\n/**\n * Bounds the *breadth* of the variable walk, which the depth cap does not: a single large array of\n * small input objects is shallow but arbitrarily wide.\n */\nexport const DEFAULT_DEPRECATION_MAX_VARIABLE_NODES = 10_000\n\n/**\n * Caps the collected result so one hostile document cannot blow up a log entry.\n */\nexport const DEFAULT_DEPRECATION_MAX_ELEMENTS = 50\n\nconst UNKNOWN = '<unknown>'\n\nexport type DeprecatedElementKind = 'output-field' | 'argument' | 'directive-argument' | 'input-field' | 'enum-value'\n\nexport interface DeprecatedElementUsage {\n kind: DeprecatedElementKind\n /**\n * Stable key for aggregating usage, formatted per kind:\n * `Type.field`, `Type.field(arg)`, `@directive(arg)`, `InputType.field`, `EnumType.VALUE`.\n */\n name: string\n /**\n * Best-effort location, for debugging a single record — not an aggregation key. A field selected\n * inside a fragment definition has no enclosing field in its ancestry, so its path is relative to\n * the fragment rather than the operation.\n */\n path?: string\n}\n\nexport interface CollectDeprecatedElementUsageOptions {\n schema: GraphQLSchema\n document: DocumentNode\n /**\n * The executed operation. Resolved from `document` and `operationName` when omitted.\n */\n operation?: OperationDefinitionNode | null\n operationName?: string | null\n /**\n * Raw request variables, as supplied by the client and before graphql-js coerces them.\n */\n variables?: Record<string, unknown> | null\n maxVariableDepth?: number\n maxVariableNodes?: number\n maxElements?: number\n}\n\nexport interface CollectDeprecatedElementUsageResult {\n elements: DeprecatedElementUsage[]\n /**\n * True when a limit — `maxElements`, `maxVariableDepth` or `maxVariableNodes` — stopped\n * collection before it finished, so `elements` may be missing elements the operation really used.\n * Absence from a truncated result is not evidence that an element is unused.\n */\n truncated: boolean\n}\n\n/**\n * Reports every `@deprecated` schema element an operation uses, across both the document and the\n * supplied variables.\n *\n * Output fields and arguments are named in the document, so an AST walk finds them. Input fields\n * and enum values are data the caller *supplies* rather than selects: they can arrive as a document\n * literal, or inside a variable where the name appears nowhere in the AST, so both routes are\n * walked and the results deduplicated.\n *\n * Results are deduplicated on kind and name — at most one entry per distinct element per request,\n * bounded by the size of the schema rather than the size of the request — and sorted, so records\n * are stable across requests.\n *\n * Returns `truncated` when any limit stopped collection. Absence from `elements` is only evidence\n * that an element went unused when `truncated` is false — which matters, because concluding\n * \"nothing uses this\" from a truncated result is how a still-used element gets deleted.\n *\n * The variable walk reads untrusted input before coercion, so a payload whose shape contradicts its\n * declared type reaches it. Every shape it depends on is guarded: malformed variables yield no\n * records rather than an error.\n *\n * This is request-side telemetry — it observes values a caller *sends*, never values the server\n * returns. A deprecated element that only ever appears in responses reads as unused.\n */\nexport function collectDeprecatedElementUsage({\n schema,\n document,\n operation,\n operationName,\n variables,\n maxVariableDepth = DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH,\n maxVariableNodes = DEFAULT_DEPRECATION_MAX_VARIABLE_NODES,\n maxElements = DEFAULT_DEPRECATION_MAX_ELEMENTS,\n}: CollectDeprecatedElementUsageOptions): CollectDeprecatedElementUsageResult {\n const collected = new Map<string, DeprecatedElementUsage>()\n const walk: WalkState = { maxVariableDepth, remaining: maxVariableNodes, truncated: false }\n\n const collect = (usage: DeprecatedElementUsage): void => {\n const key = `${usage.kind}:${usage.name}`\n // Already recorded is deduplication, not truncation — only a rejected *new* element is a loss.\n if (collected.has(key)) return\n if (collected.size >= maxElements) {\n walk.truncated = true\n return\n }\n collected.set(key, usage)\n }\n\n const resolvedOperation = operation ?? getOperationAST(document, operationName ?? undefined)\n\n collectFromDocument(schema, scopeToOperation(document, resolvedOperation), collect)\n collectFromVariables(schema, resolvedOperation?.variableDefinitions, variables, walk, collect)\n\n return {\n elements: [...collected.values()].sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)),\n truncated: walk.truncated,\n }\n}\n\ntype Collect = (usage: DeprecatedElementUsage) => void\n\n/**\n * A document may carry several operations but only one runs, so anything the others select would be\n * reported as usage that never happened. `separateOperations` keeps each operation with only the\n * fragments it can reach, which is exactly the scope wanted here.\n */\nfunction scopeToOperation(document: DocumentNode, operation: OperationDefinitionNode | null | undefined): DocumentNode {\n if (!operation) return document\n\n let operationCount = 0\n for (const definition of document.definitions) {\n if (definition.kind === Kind.OPERATION_DEFINITION) operationCount++\n if (operationCount > 1) return separateOperations(document)[operation.name?.value ?? ''] ?? document\n }\n return document\n}\n\nfunction collectFromDocument(schema: GraphQLSchema, document: DocumentNode, collect: Collect): void {\n const typeInfo = new TypeInfo(schema)\n\n visit(\n document,\n visitWithTypeInfo(typeInfo, {\n Field(_node, _key, _parent, _path, ancestors) {\n const field = typeInfo.getFieldDef()\n if (field?.deprecationReason == null) return\n collect({\n kind: 'output-field',\n name: `${typeInfo.getParentType()?.name ?? UNKNOWN}.${field.name}`,\n path: getPath(ancestors, field.name),\n })\n },\n Argument(_node, _key, _parent, _path, ancestors) {\n const argument = typeInfo.getArgument()\n if (argument?.deprecationReason == null) return\n // TypeInfo resolves an argument against the enclosing directive when there is one, so\n // without this branch a directive's argument is reported as an argument of the field the\n // directive is attached to — a field argument that does not exist.\n const directive = typeInfo.getDirective()\n collect({\n kind: directive ? 'directive-argument' : 'argument',\n name: directive\n ? `@${directive.name}(${argument.name})`\n : `${typeInfo.getParentType()?.name ?? UNKNOWN}.${typeInfo.getFieldDef()?.name ?? UNKNOWN}(${argument.name})`,\n path: getPath(ancestors),\n })\n },\n // Input-object fields written as literals in the document, including variable definition\n // default values, which TypeInfo also tracks as input positions.\n ObjectField(node, _key, _parent, _path, ancestors) {\n const parentType = getNamedType(typeInfo.getParentInputType())\n if (!isInputObjectType(parentType)) return\n const inputField = parentType.getFields()[node.name.value]\n if (inputField?.deprecationReason == null) return\n collect({\n kind: 'input-field',\n name: `${parentType.name}.${inputField.name}`,\n path: getPath(ancestors, inputField.name),\n })\n },\n EnumValue(node, _key, _parent, _path, ancestors) {\n const enumType = getNamedType(typeInfo.getInputType())\n if (!isEnumType(enumType)) return\n const enumValue = enumType.getValue(node.value)\n if (enumValue?.deprecationReason == null) return\n collect({\n kind: 'enum-value',\n name: `${enumType.name}.${enumValue.name}`,\n path: getPath(ancestors),\n })\n },\n }),\n )\n}\n\ntype PathNode = FieldNode | ObjectFieldNode\n\nfunction isPathNode(node: ASTNode): node is PathNode {\n return node.kind === Kind.FIELD || node.kind === Kind.OBJECT_FIELD\n}\n\nfunction getPath(ancestors: readonly (ASTNode | readonly ASTNode[])[], leaf?: string): string {\n const segments = ancestors\n .filter((x): x is ASTNode => !Array.isArray(x))\n .filter(isPathNode)\n .map((x) => x.name.value)\n\n if (leaf) segments.push(leaf)\n\n return segments.join('.')\n}\n\ninterface WalkState {\n maxVariableDepth: number\n /** Remaining node budget, shared across every variable of the operation. */\n remaining: number\n truncated: boolean\n}\n\n/**\n * Walks each supplied variable value against its declared type, reporting every deprecated input\n * field and enum value the client actually sent.\n *\n * Takes the definitions of the *resolved* operation rather than scanning the whole document: the\n * same variable name may be declared with a different type in each operation, so only the executed\n * one's declarations describe the supplied values.\n */\nfunction collectFromVariables(\n schema: GraphQLSchema,\n variableDefinitions: readonly VariableDefinitionNode[] | undefined,\n variables: Record<string, unknown> | null | undefined,\n state: WalkState,\n collect: Collect,\n): void {\n if (variables == null) return\n\n for (const variableDefinition of variableDefinitions ?? []) {\n const variableName = variableDefinition.variable.name.value\n if (!Object.hasOwn(variables, variableName)) continue\n\n const type = typeFromAST(schema, variableDefinition.type)\n if (!isInputType(type)) continue\n\n walkInputValue(type, variables[variableName], `$${variableName}`, 0, state, collect)\n }\n}\n\nfunction walkInputValue(type: GraphQLInputType, value: unknown, path: string, depth: number, state: WalkState, collect: Collect): void {\n // A limit stopping the descent is a loss of telemetry, so it is recorded; a null value is simply\n // nothing to walk, and must not be reported as one.\n if (depth > state.maxVariableDepth || state.remaining <= 0) {\n state.truncated = true\n return\n }\n if (value == null) return\n state.remaining--\n\n // Unwrapped inline rather than by recursing, so a nullability wrapper doesn't spend a node from\n // the budget for a value that has not been descended into yet.\n const unwrapped = isNonNullType(type) ? type.ofType : type\n\n if (isListType(unwrapped)) {\n // GraphQL accepts a bare value in a list position, so handle both shapes.\n const items = Array.isArray(value) ? value : [value]\n for (const [index, item] of items.entries()) {\n // Checked here rather than relying on the recursive call returning early: the loop itself is\n // the unbounded part, since list length comes from the payload and each step builds a path.\n if (state.remaining <= 0) {\n state.truncated = true\n return\n }\n walkInputValue(unwrapped.ofType, item, Array.isArray(value) ? `${path}.${index}` : path, depth + 1, state, collect)\n }\n return\n }\n\n if (isEnumType(unwrapped)) {\n // An unknown name is a coercion error graphql-js raises moments later; nothing to record here.\n if (typeof value !== 'string') return\n const enumValue = unwrapped.getValue(value)\n if (enumValue?.deprecationReason != null) {\n collect({ kind: 'enum-value', name: `${unwrapped.name}.${enumValue.name}`, path })\n }\n return\n }\n\n // Scalars have no members that could carry a deprecation.\n if (!isInputObjectType(unwrapped) || !isRecord(value)) return\n\n for (const field of Object.values(unwrapped.getFields())) {\n if (state.remaining <= 0) {\n state.truncated = true\n return\n }\n if (!Object.hasOwn(value, field.name)) continue\n\n const fieldPath = `${path}.${field.name}`\n // Presence is the signal, explicit `null` included: a client still sending the field would\n // break if it were removed from the schema.\n if (field.deprecationReason != null) {\n collect({ kind: 'input-field', name: `${unwrapped.name}.${field.name}`, path: fieldPath })\n }\n\n walkInputValue(field.type, value[field.name], fieldPath, depth + 1, state, collect)\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n"],"names":[],"mappings":"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"}
|
package/index.d.cts
CHANGED
package/index.d.mts
CHANGED
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var context = require('./context.js');
|
|
4
|
+
var deprecation = require('./deprecation.js');
|
|
4
5
|
var logging = require('./logging.js');
|
|
5
6
|
var operation = require('./operation.js');
|
|
6
7
|
var requestInfo = require('./request-info.js');
|
|
@@ -13,6 +14,10 @@ var utils = require('./utils.js');
|
|
|
13
14
|
|
|
14
15
|
exports.createContextFactory = context.createContextFactory;
|
|
15
16
|
exports.defaultCreateUser = context.defaultCreateUser;
|
|
17
|
+
exports.DEFAULT_DEPRECATION_MAX_ELEMENTS = deprecation.DEFAULT_DEPRECATION_MAX_ELEMENTS;
|
|
18
|
+
exports.DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH = deprecation.DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH;
|
|
19
|
+
exports.DEFAULT_DEPRECATION_MAX_VARIABLE_NODES = deprecation.DEFAULT_DEPRECATION_MAX_VARIABLE_NODES;
|
|
20
|
+
exports.collectDeprecatedElementUsage = deprecation.collectDeprecatedElementUsage;
|
|
16
21
|
exports.logGraphQLOperation = logging.logGraphQLOperation;
|
|
17
22
|
exports.logSubscriptionOperation = logging.logSubscriptionOperation;
|
|
18
23
|
exports.extractAnonymousOperationName = operation.extractAnonymousOperationName;
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createContextFactory, defaultCreateUser } from './context.mjs';
|
|
2
|
+
export { DEFAULT_DEPRECATION_MAX_ELEMENTS, DEFAULT_DEPRECATION_MAX_VARIABLE_DEPTH, DEFAULT_DEPRECATION_MAX_VARIABLE_NODES, collectDeprecatedElementUsage } from './deprecation.mjs';
|
|
2
3
|
export { logGraphQLOperation, logSubscriptionOperation } from './logging.mjs';
|
|
3
4
|
export { extractAnonymousOperationName, isIntrospectionQuery } from './operation.mjs';
|
|
4
5
|
export { buildBaseRequestInfo, buildConnectRequestInfo, connectRequestBaseUrl, requestBaseUrl } from './request-info.mjs';
|
package/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|