UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

562 lines 24.6 kB
import type EventEmitter from "eventemitter3"; import type { Middleware } from "graphile-config"; import type { ASTNode, ExecutionArgs, FragmentDefinitionNode, GraphQLArgs, GraphQLArgument, GraphQLArgumentConfig, GraphQLEnumValue, GraphQLField, GraphQLFieldConfig, GraphQLInputField, GraphQLInputFieldConfig, GraphQLInputObjectType, GraphQLInputType, GraphQLInterfaceType, GraphQLObjectType, GraphQLOutputType, GraphQLScalarType, GraphQLSchema, GraphQLUnionType, OperationDefinitionNode, Source, ValueNode, VariableNode } from "graphql"; import type { ObjMap } from "graphql/jsutils/ObjMap.js"; import type { $$streamMore, $$timeout, $$ts, ExecutionEntryFlags } from "./constants.js"; import type { Constraint } from "./constraints.js"; import type { LayerPlanReasonListItemStream } from "./engine/LayerPlan.js"; import type { OperationPlan } from "./engine/OperationPlan.js"; import type { FlaggedValue, SafeError } from "./error.js"; import type { GrafastOperationOptions } from "./prepare.js"; import type { Step } from "./step.js"; import type { __InputDefaultStep } from "./steps/__inputDefault.js"; import type { __InputDynamicScalarStep } from "./steps/__inputDynamicScalar.js"; import type { ApplyableStep } from "./steps/applyInput.js"; import type { __InputListStep, __InputObjectStepWithDollars, __InputStaticLeafStep, __TrackedValueStepWithDollars, ConstantStep, ObjectStep } from "./steps/index.js"; export type { ExecutionEntryFlags }; export interface GrafastTimeouts { /** * How many milliseconds should we allow for planning. Remember: planning is * synchronous, so whilst it is happening the event loop is blocked. */ planning?: number; /** * How many milliseconds should we allow for execution. We will only check * this immediately before triggering the execution of an asynchronous step, * and if it is exceeded it will only prevent the execution of asynchronous * steps, not synchronous ones. * * IMPORTANT: since we only check this _before_ an asynchronous step * executes, there's nothing to stop an asynchronous step from continuing to * execute long after the timeout has expired - therefore it's the * responsibility of each step to abort itself if it goes over the allocated * time budget (which is detailed in `ExecutionExtra.stopTime`). */ execution?: number; } export type Fragments = { [key: string]: FragmentDefinitionNode; }; export interface IEstablishOperationPlanResult { variableValuesConstraints: Constraint[]; contextConstraints: Constraint[]; rootValueConstraints: Constraint[]; errorBehavior: ErrorBehavior; } export interface EstablishOperationPlanResultSuccess extends IEstablishOperationPlanResult { error?: never; operationPlan: OperationPlan; } export interface EstablishOperationPlanResultError extends IEstablishOperationPlanResult { error: Error | SafeError<{ [$$timeout]: number; [$$ts]: number; } | { [$$timeout]?: undefined; [$$ts]?: undefined; } | undefined>; operationPlan?: never; } export type EstablishOperationPlanResult = EstablishOperationPlanResultSuccess | EstablishOperationPlanResultError; /** * This represents the list of possible operationPlans for a specific document. * * @remarks * * It also includes the fragments for validation, but generally we trust that * if the OperationDefinitionNode is the same then the request is equivalent. */ export interface CacheByOperationEntry { /** * Implemented as a linked list so the hot operationPlans can be kept at the top of the * list, and if the list grows beyond a maximum size we can drop the last * element. */ possibleOperationPlans: LinkedList<EstablishOperationPlanResult> | null; fragments: Fragments; } export interface LinkedList<T> { value: T; next: LinkedList<T> | null; } export interface IndexByListItemStepId { [listItemStepId: number]: number; } export type GrafastValuesList<TData> = ReadonlyArray<TData>; export type PromiseOrDirect<T> = PromiseLike<T> | T; export type ExecutionResultValue<T> = T | FlaggedValue<Error> | FlaggedValue<null>; export type GrafastResultsList<TData> = ReadonlyArray<PromiseOrDirect<ExecutionResultValue<TData>>>; export type GrafastResultStreamList<TStreamItem> = ReadonlyArray<PromiseOrDirect<AsyncIterable<PromiseOrDirect<ExecutionResultValue<TStreamItem>>>> | PromiseLike<never>>; export type AwaitedExecutionResults<TData> = ReadonlyArray<PromiseOrDirect<ExecutionResultValue<TData> | AsyncIterable<PromiseOrDirect<ExecutionResultValue<TData extends ReadonlyArray<infer UStreamItem> ? UStreamItem : never>>>>>; export type ExecutionResults<TData> = PromiseOrDirect<AwaitedExecutionResults<TData>> | PromiseLike<never>; export type BaseGraphQLRootValue = any; export interface BaseGraphQLVariables { [key: string]: unknown; } export interface BaseGraphQLArguments { [key: string]: unknown; } export type FieldArgs<TObj extends BaseGraphQLArguments = any> = { /** @deprecated Use bakedInput() step instead. */ get?: never; getRaw<TKey extends keyof TObj & string>(path: TKey): Step<TObj[TKey]>; getRaw(path?: ReadonlyArray<string | number>): AnyInputStep | ObjectStep<{ [argName: string]: AnyInputStep; }>; getBaked<TKey extends keyof TObj & string>(path: TKey): Step; getBaked(path: ReadonlyArray<string | number>): Step; typeAt(path: keyof TObj & string): GraphQLInputType; typeAt(path: ReadonlyArray<string | number>): GraphQLInputType; /** This also works (without path) to apply each list entry against $target */ apply<TArg extends object>($target: ApplyableStep<TArg>, path: keyof TObj & string, getTargetFromParent?: (parent: TArg, inputValue: any) => object | undefined): void; apply<TArg extends object>($target: ApplyableStep<TArg>, path?: ReadonlyArray<string | number>, getTargetFromParent?: (parent: TArg, inputValue: any) => object | undefined): void; apply<TArg extends object>($target: ApplyableStep<TArg>, getTargetFromParent: (parent: TArg, inputValue: any) => object | undefined, justTargetFromParent?: never): void; /** * Applies the arguments to $target if they haven't already been applied. * Called automatically by the system once the `plan()` returns, but may be * called manually (e.g. by a plan wrapper) to ensure args are applied at any * point. */ autoApply($target: Step): void; } & { [key in keyof TObj & string as `$${key}`]: Step<TObj[key]> & ([unknown] extends [TObj[key]] ? { [subkey in string as `$${subkey}`]: Step<any>; } : TObj[key] extends Record<string, any> ? { [subkey in keyof TObj[key] & string as `$${subkey}`]: Step<TObj[key][subkey]>; } : unknown); }; export type FieldArg<TData = any> = { /** @deprecated Use bakedInput() step instead. */ get?: never; getRaw<TKey extends keyof TData & string>(path: TKey): Step<TData[TKey]>; getRaw(path?: string | ReadonlyArray<string | number>): AnyInputStep; typeAt(path: string | ReadonlyArray<string | number>): GraphQLInputType; /** This also works (without path) to apply each list entry against $target */ apply<TArg extends object>($target: ApplyableStep<TArg>, path?: ReadonlyArray<string | number>, getTargetFromParent?: (parent: TArg, inputValue: any) => object | undefined): void; apply<TArg extends object>($target: ApplyableStep<TArg>, getTargetFromParent: (parent: TArg, inputValue: any) => object | undefined, justTargetFromParent?: never): void; }; export type AnyInputStep = __TrackedValueStepWithDollars<any, GraphQLInputType> | __InputListStep | __InputStaticLeafStep | __InputDynamicScalarStep | __InputObjectStepWithDollars<GraphQLInputObjectType> | __InputDefaultStep | ConstantStep<any>; export type AnyInputStepWithDollars = AnyInputStep & AnyInputStepDollars; /** * Lies to make it easier to write TypeScript code like * `{ $input: { $user: { $username } } }` without having to pass loads of * generics. */ export type AnyInputStepDollars = { [key in string as `$${key}`]: AnyInputStepWithDollars; }; export interface FieldInfo { fieldName: string; field: GraphQLField<any, any, any>; schema: GraphQLSchema; } /** * Step resolvers are like regular resolvers except they're called beforehand, * they return plans rather than values, and they only run once for lists * rather than for each item in the list. * * The idea is that the plan resolver returns a plan object which later will * process the data and feed that into the actual resolver functions * (preferably using the default resolver function?). * * They are stored onto `<field>.extensions.grafast.plan` * * @returns a plan for this field. * * @remarks * We're using `TrackedObject<...>` so we can later consider caching these * executions. */ export type FieldPlanResolver<TSourceStep extends Step = Step, TArgs extends BaseGraphQLArguments = any, TResultStep extends Step = Step> = ($source: TSourceStep, fieldArgs: FieldArgs<TArgs>, info: FieldInfo) => TResultStep | null; export type InputObjectFieldApplyResolver<TParent = any, TData = any, TScope = any> = (target: TParent, input: TData, // Don't use unknown here, otherwise users can't easily cast it info: { schema: GraphQLSchema; fieldName: string; field: GraphQLInputField; scope: TScope; }) => any; export type InputObjectTypeBakedInfo = { schema: GraphQLSchema; type: GraphQLInputObjectType; applyChildren(val: any): void; }; export type InputObjectTypeBakedResolver = (input: Record<string, any>, info: InputObjectTypeBakedInfo) => any; export type ArgumentApplyPlanResolver<TSource extends Step = any, TFieldStep extends Step = any, TData = any> = ($parentPlan: TSource, $fieldPlan: TFieldStep, input: FieldArg<TData>, info: { schema: GraphQLSchema; arg: GraphQLArgument; argName: string; }) => void; /** * GraphQLScalarTypes can have plans, these are passed the field plan and must * return an executable plan. */ export type ScalarPlanResolver<TSourceStep extends Step = Step, TResultStep extends Step = Step> = ($source: TSourceStep, info: { schema: GraphQLSchema; }) => TResultStep; /** * GraphQLScalarTypes can have plans, these are passed the field plan and must * return an executable plan. */ export type ScalarInputPlanResolver<TResultStep extends Step = Step> = ($inputValue: AnyInputStep, info: { schema: GraphQLSchema; type: GraphQLScalarType; }) => TResultStep; /** * EXPERIMENTAL! * * NOTE: this is an `any` because we want to allow users to specify * subclasses of ExecutableStep but TypeScript only wants to allow * superclasses. * * @experimental */ export type EnumValueApplyResolver<TParent = any, TScope = any> = (parent: TParent, info: { value: GraphQLEnumValue; scope: TScope; }) => void; /** * Basically GraphQLFieldConfig but with an easy to access `plan` method. */ export type GrafastFieldConfig<TSourceStep extends Step, TArgs extends BaseGraphQLArguments = any, TFieldStep extends Step = any> = Omit<GraphQLFieldConfig<any, any>, "args" | "type"> & { type: GraphQLOutputType; plan?: FieldPlanResolver<TSourceStep, TArgs, TFieldStep>; subscribePlan?: FieldPlanResolver<TSourceStep, TArgs, TFieldStep>; args?: GrafastFieldConfigArgumentMap; }; /** * Basically GraphQLFieldConfigArgumentMap but allowing for args to have plans. */ export type GrafastFieldConfigArgumentMap = { [argName: string]: GrafastArgumentConfig; }; /** * Basically GraphQLArgumentConfig but allowing for a plan. */ export type GrafastArgumentConfig<TSource extends Step = any, TFieldStep extends Step = any, TData = any> = Omit<GraphQLArgumentConfig, "type"> & { type: GraphQLInputType; applyPlan?: ArgumentApplyPlanResolver<TSource, TFieldStep, TData>; applySubscribePlan?: ArgumentApplyPlanResolver<TSource, TFieldStep, TData>; inputPlan?: never; autoApplyAfterParentPlan?: never; autoApplyAfterParentSubscribePlan?: never; }; /** * Basically GraphQLInputFieldConfig but allowing for the field to have a plan. */ export type GrafastInputFieldConfig<TParent = any, TData = any> = Omit<GraphQLInputFieldConfig, "type"> & { type: GraphQLInputType; apply?: InputObjectFieldApplyResolver<TParent, TData>; inputPlan?: never; applyPlan?: never; autoApplyAfterParentInputPlan?: never; autoApplyAfterParentApplyPlan?: never; }; /** * The args passed to a field plan resolver, the values are plans. */ export type TrackedArguments<TArgs extends BaseGraphQLArguments = BaseGraphQLArguments> = { [TKey in keyof TArgs & string]: AnyInputStep; }; /** * `@stream` directive meta. */ export interface StepStreamOptions extends LayerPlanReasonListItemStream { } /** * Additional details about the planning for a field; currently only relates to * the `@stream` directive. */ export interface StepOptions { /** * Details for the `@stream` directive. * * object - `@stream` details * * true - no stream directive, but is inside a subscription field * * null - no stream directive */ stream: StepStreamOptions | true | null; /** * Should we walk an iterable if presented. This is important because we * don't want to walk things like Map/Set except if we're doing it as part of * a list step. */ walkIterable: boolean; } /** * Options passed to the `optimize` method of a plan to give more context. */ export interface StepOptimizeOptions { /** * If null, this step will not stream. If non-null, this step _might_ stream, * but it's not guaranteed - it may be dependent on user variables, e.g. the * `if` parameter. */ stream: null | {}; meta: Record<string, unknown> | undefined; } /** * A subscriber provides realtime data, a SubscribeStep can subscribe to a * given topic (string) and will receive an AsyncIterableIterator with messages * published to that topic (standard pub/sub semantics). */ export type GrafastSubscriber<TTopics extends { [key: string]: any; } = { [key: string]: any; }> = { subscribe<TTopic extends keyof TTopics = keyof TTopics>(topic: TTopic): PromiseOrDirect<AsyncIterableIterator<TTopics[TTopic]>>; release?(): PromiseOrDirect<void>; }; /** * Specifically relates to the stringification of NodeIDs, e.g. `["User", 1]` * to/from `WyJVc2VyIiwgMV0=` */ export interface NodeIdCodec<T = any> { name: string; encode(value: T): string | null; decode(value: string): T; } /** * Determines if a NodeID relates to a given object type, and also relates to * encoding the NodeID for that type. */ export type NodeIdHandler<TIdentifiers extends readonly any[] = readonly any[], TCodec extends NodeIdCodec<any> = NodeIdCodec<any>, TNodeStep extends Step = Step, TSpec = any> = { /** * The name of the object type this handler is for. */ typeName: string; /** * Which codec are we using to encode/decode the NodeID string? */ codec: TCodec; /** * Returns true if the given decoded Node ID value represents this type. */ match(specifier: TCodec extends NodeIdCodec<infer U> ? U : any): boolean; /** * Returns the underlying identifiers extracted from the decoded NodeID * value. */ getIdentifiers(value: TCodec extends NodeIdCodec<infer U> ? U : any): TIdentifiers; /** * Returns a plan that returns the value ready to be encoded. When the result * of this plan is fed into `match`, it should return `true`. */ plan($thing: TNodeStep): Step<TCodec extends NodeIdCodec<infer U> ? U : any>; /** * Returns a specification based on the Node ID, this can be in any format * you like. It is intended to then be fed into `get` or handled in your own * code as you see fit. (When used directly, it's primarily useful for * referencing a node without actually fetching it - e.g. allowing you to * delete a node by its ID without first fetching it.) */ getSpec(plan: Step<TCodec extends NodeIdCodec<infer U> ? U : any>): TSpec; /** * Combined with `getSpec`, this forms the recprocal of `plan`; i.e. * `get(getSpec( plan(node) ))` should return a plan that results in the * original node. */ get(spec: TSpec): TNodeStep; deprecationReason?: string; }; export type BaseEventMap = Record<string, any>; export type EventMapKey<TEventMap extends BaseEventMap> = string & keyof TEventMap; export type EventCallback<TPayload> = (params: TPayload) => void; export interface TypedEventEmitter<TEventMap extends BaseEventMap> extends EventEmitter<any, any> { addListener<TEventName extends EventMapKey<TEventMap>>(eventName: TEventName, callback: EventCallback<TEventMap[TEventName]>): this; on<TEventName extends EventMapKey<TEventMap>>(eventName: TEventName, callback: EventCallback<TEventMap[TEventName]>): this; once<TEventName extends EventMapKey<TEventMap>>(eventName: TEventName, callback: EventCallback<TEventMap[TEventName]>): this; removeListener<TEventName extends EventMapKey<TEventMap>>(eventName: TEventName, callback: EventCallback<TEventMap[TEventName]>): this; off<TEventName extends EventMapKey<TEventMap>>(eventName: TEventName, callback: EventCallback<TEventMap[TEventName]>): this; emit<TEventName extends EventMapKey<TEventMap>>(eventName: TEventName, params: TEventMap[TEventName]): boolean; } export type ExecutionEventMap = { /** * Something that can be added to the * ExecutionResult.extensions.explain.operations list. */ explainOperation: { operation: Record<string, any> & { type: string; title: string; }; }; }; export type ExecutionEventEmitter = TypedEventEmitter<ExecutionEventMap>; export interface ExecutionExtraBase { /** The `performance.now()` at which your step should stop executing */ stopTime: number | null; /** If you have set a `metaKey` on your step, the relevant meta object which you can write into (e.g. for caching) */ meta: Record<string, unknown> | undefined; eventEmitter: ExecutionEventEmitter | undefined; } export interface ExecutionExtra extends ExecutionExtraBase { } export interface UnbatchedExecutionExtra extends ExecutionExtraBase { stream: ExecutionDetailsStream | null; } export type ExecutionValue<TData = any> = BatchExecutionValue<TData> | UnaryExecutionValue<TData>; interface ExecutionValueBase<TData = any> { at(i: number): TData; isBatch: boolean; /** Returns this.value for a unary execution value; throws if non-unary */ unaryValue(): TData; } export interface BatchExecutionValue<TData = any> extends ExecutionValueBase<TData> { isBatch: true; entries: ReadonlyArray<TData>; /** Always throws, since this should only be called on unary execution values */ unaryValue(): never; } export interface UnaryExecutionValue<TData = any> extends ExecutionValueBase<TData> { isBatch: false; value: TData; /** Same as getting .value */ unaryValue(): TData; } export type IndexMap = <T>(callback: (i: number) => T) => ReadonlyArray<T>; export type IndexForEach = (callback: (i: number) => any) => void; export interface ExecutionDetailsStream { initialCount: number; } export interface ExecutionDetails<TDeps extends readonly [...any[]] = readonly [...any[]]> { count: number; indexMap: IndexMap; indexForEach: IndexForEach; values: { [DepIdx in keyof TDeps]: ExecutionValue<TDeps[DepIdx]>; } & { length: TDeps["length"]; map: ReadonlyArray<ExecutionValue<TDeps[number]>>["map"]; }; extra: ExecutionExtra; stream: ExecutionDetailsStream | null; } export interface LocationDetails { node: ASTNode | readonly ASTNode[]; /** This should only be null for the root selection */ parentTypeName: string | null; /** This should only be null for the root selection */ fieldName: string | null; } export type UnwrapPlanTuple</* const */ TIn extends readonly Step[]> = { [Index in keyof TIn]: DataFromStep<TIn[Index]>; }; export type NotVariableValueNode = Exclude<ValueNode, VariableNode>; export type StreamMaybeMoreableArray<T = any> = Array<T> & { [$$streamMore]?: AsyncIterator<any, any, any> | Iterator<any, any, any>; }; export type StreamMoreableArray<T = any> = Array<T> & { [$$streamMore]: AsyncIterator<any, any, any> | Iterator<any, any, any>; }; export interface GrafastArgs extends GraphQLArgs { onError?: ErrorBehavior; extensions?: Record<string, unknown>; resolvedPreset?: GraphileConfig.ResolvedPreset; requestContext?: Partial<Grafast.RequestContext>; middleware?: Middleware<GraphileConfig.GrafastMiddleware> | null; } export type Maybe<T> = T | null | undefined; export type * from "./planJSONInterfaces.js"; export interface BaseDependencyOptions<TStep extends Step = Step> { step: TStep; skipDeduplication?: boolean; /** @defaultValue `FLAG_NULL` */ acceptFlags?: ExecutionEntryFlags; onReject?: Maybe<Error>; } export interface AddDependencyOptions<TStep extends Step = Step> extends BaseDependencyOptions<TStep> { nonUnaryMessage?: never; dataOnly?: boolean; } export interface AddUnaryDependencyOptions<TStep extends Step = Step> extends BaseDependencyOptions<TStep> { nonUnaryMessage?: ($dependent: Step, $dependency: Step) => string; dataOnly?: never; } export interface DependencyOptions<TStep extends Step = Step> { step: TStep; acceptFlags: ExecutionEntryFlags; onReject: Maybe<Error>; dataOnly: boolean; } export type DataFromStep<TStep extends Step> = TStep extends Step<infer TData> ? TData : never; export interface GrafastExecutionArgs extends ExecutionArgs { onError?: ErrorBehavior; extensions?: Record<string, unknown>; resolvedPreset?: GraphileConfig.ResolvedPreset; middleware?: Middleware<GraphileConfig.GrafastMiddleware> | null; requestContext?: Partial<Grafast.RequestContext>; outputDataAsString?: boolean; } export interface ValidateSchemaEvent { resolvedPreset: GraphileConfig.ResolvedPreset; schema: GraphQLSchema; } export interface ParseAndValidateEvent { resolvedPreset: GraphileConfig.ResolvedPreset; schema: GraphQLSchema; source: string | Source; } export interface PrepareArgsEvent { args: Grafast.ExecutionArgs; } export interface ExecuteEvent { args: GrafastExecutionArgs; } export interface SubscribeEvent { args: GrafastExecutionArgs; } export interface EstablishOperationPlanEvent { schema: GraphQLSchema; operation: OperationDefinitionNode; fragments: ObjMap<FragmentDefinitionNode>; variableValues: Record<string, any>; context: any; rootValue: any; onError: ErrorBehavior; args: GrafastExecutionArgs; options: GrafastOperationOptions; } export interface ExecuteStepEvent { args: GrafastExecutionArgs; step: Step; executeDetails: ExecutionDetails; } export interface PlanTypeInfo<TOriginalStep extends Step = Step> { abstractType: GraphQLUnionType | GraphQLInterfaceType; /** * If this polymorphic position was represented by exactly one source step, * this will be that step and you may use it to implement a more optimal * planType. If more than one step was combined as input to this * polymorphism, this will be null. */ $original: TOriginalStep | null; } /** * When planning an abstract type, an interface or union, it should have a * `extensions.grafast.planType` method which accepts an incoming step * representing the polymorphic data (we call this the `$specifier`) and will * return a AbstractTypePlanner object. This object has a key `$__typename` * whose value must be a step that represents the GraphQL type name to use for * the given $specifier, and a method `planForType` that should return the step * to use for a specific object type within the interface/union. */ export interface AbstractTypePlanner { /** * Must be a step representing the name of the object type associated with * the given `$specifier`, or `null` if no such type could be determined. */ $__typename: Step<string | null>; /** * If not provided, will call `t.planType($specifier)` */ planForType?(t: GraphQLObjectType): Step | null; } export type Thunk<T> = T | (() => T); /** * GraphQL error behavior, as per https://github.com/graphql/graphql-spec/pull/1163 */ export type ErrorBehavior = "PROPAGATE" | "NULL" | "HALT"; //# sourceMappingURL=interfaces.d.ts.map