| /** @category Validation Rules */ |
| import { GraphQLError } from '../../error/GraphQLError.mjs'; |
| import { Kind } from '../../language/kinds.mjs'; |
| import { collectFields } from '../../execution/collectFields.mjs'; |
| |
| /** |
| * Subscriptions must only include a non-introspection field. |
| * |
| * A GraphQL subscription is valid only if it contains a single root field and |
| * that root field is not an introspection field. |
| * |
| * See https://spec.graphql.org/draft/#sec-Single-root-field |
| * @param context - The validation context used while checking the document. |
| * @returns A visitor that reports validation errors for this rule. |
| * @example |
| * ```ts |
| * import { buildSchema, parse, validate } from 'graphql'; |
| * import { SingleFieldSubscriptionsRule } from 'graphql/validation'; |
| * |
| * const schema = buildSchema(` |
| * type Query { |
| * name: String |
| * } |
| * |
| * type Subscription { |
| * a: String |
| * b: String |
| * } |
| * `); |
| * |
| * const invalidDocument = parse(` |
| * subscription { a b } |
| * `); |
| * const invalidErrors = validate(schema, invalidDocument, [SingleFieldSubscriptionsRule]); |
| * |
| * invalidErrors.length; // => 1 |
| * |
| * const validDocument = parse(` |
| * subscription { a } |
| * `); |
| * const validErrors = validate(schema, validDocument, [SingleFieldSubscriptionsRule]); |
| * |
| * validErrors; // => [] |
| * ``` |
| */ |
| export function SingleFieldSubscriptionsRule(context) { |
| return { |
| OperationDefinition(node) { |
| if (node.operation === 'subscription') { |
| const schema = context.getSchema(); |
| const subscriptionType = schema.getSubscriptionType(); |
| |
| if (subscriptionType) { |
| const operationName = node.name ? node.name.value : null; |
| const variableValues = Object.create(null); |
| const document = context.getDocument(); |
| const fragments = Object.create(null); |
| |
| for (const definition of document.definitions) { |
| if (definition.kind === Kind.FRAGMENT_DEFINITION) { |
| fragments[definition.name.value] = definition; |
| } |
| } |
| |
| const fields = collectFields( |
| schema, |
| fragments, |
| variableValues, |
| subscriptionType, |
| node.selectionSet, |
| ); |
| |
| if (fields.size > 1) { |
| const fieldSelectionLists = [...fields.values()]; |
| const extraFieldSelectionLists = fieldSelectionLists.slice(1); |
| const extraFieldSelections = extraFieldSelectionLists.flat(); |
| context.reportError( |
| new GraphQLError( |
| operationName != null |
| ? `Subscription "${operationName}" must select only one top level field.` |
| : 'Anonymous Subscription must select only one top level field.', |
| { |
| nodes: extraFieldSelections, |
| }, |
| ), |
| ); |
| } |
| |
| for (const fieldNodes of fields.values()) { |
| const field = fieldNodes[0]; |
| const fieldName = field.name.value; |
| |
| if (fieldName.startsWith('__')) { |
| context.reportError( |
| new GraphQLError( |
| operationName != null |
| ? `Subscription "${operationName}" must not select an introspection top level field.` |
| : 'Anonymous Subscription must not select an introspection top level field.', |
| { |
| nodes: fieldNodes, |
| }, |
| ), |
| ); |
| } |
| } |
| } |
| } |
| }, |
| }; |
| } |