diff --git a/src/__tests__/api.test.ts b/src/__tests__/api.test.ts new file mode 100644 index 00000000..51a8c380 --- /dev/null +++ b/src/__tests__/api.test.ts @@ -0,0 +1,13 @@ +import { simpleIntrospection } from './fixtures/simpleIntrospection'; +import { $tada } from '../namespace'; +import { graphql } from '../api'; + +declare module '../api' { + interface setupSchema { + introspection: typeof simpleIntrospection; + } +} + +const fragment = graphql('fragment Test on Query { __typename }'); + +const document = graphql('{ ...Test }', [fragment]); diff --git a/src/__tests__/fragments.test-d.ts b/src/__tests__/fragments.test-d.ts deleted file mode 100644 index 0d1a4f50..00000000 --- a/src/__tests__/fragments.test-d.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { assertType, test } from 'vitest'; -import { simpleIntrospection } from './fixtures/simpleIntrospection'; -import { parseDocument } from '../parser'; -import { mapIntrospection } from '../introspection'; -import { getFragmentType } from '../selection'; - -type introspection = mapIntrospection; - -const any = {} as any; - -test('creates a type for a given fragment', () => { - const unionQuery = ` - fragment Fields on Todo { - id - text - complete - __typename - } -`; - - type doc = parseDocument; - type typedDoc = getFragmentType; - - const actual = any as typedDoc; - - assertType<{ - id: string | number; - text: string; - complete: boolean | null; - __typename: 'Todo'; - }>(actual); -}); diff --git a/src/__tests__/parser.test-d.ts b/src/__tests__/parser.test-d.ts index d0bb96fe..d3073e35 100644 --- a/src/__tests__/parser.test-d.ts +++ b/src/__tests__/parser.test-d.ts @@ -854,9 +854,10 @@ describe('parseDocument', () => { it('parses kitchen sink query', () => { type kitchensinkQuery = typeof import('./fixtures/kitchensinkQuery').kitchensinkQuery; type kitchensinkDocument = import('./fixtures/kitchensinkQuery').kitchensinkDocument; + type actual = parseDocument; - expectTypeOf>().toEqualTypeOf(); - expectTypeOf>().toMatchTypeOf(); - expectTypeOf>().toMatchTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); }); }); diff --git a/src/__tests__/selection.test-d.ts b/src/__tests__/selection.test-d.ts index ab34ce46..6dd6c045 100644 --- a/src/__tests__/selection.test-d.ts +++ b/src/__tests__/selection.test-d.ts @@ -1,6 +1,6 @@ import { expectTypeOf, test } from 'vitest'; import { simpleSchema } from './fixtures/simpleSchema'; -import { $tada } from '../namespace'; +import { $tada, decorateDocument, getFragmentsOfDocumentsRec } from '../namespace'; import { parseDocument } from '../parser'; import { mapIntrospection } from '../introspection'; import { getDocumentType } from '../selection'; @@ -114,23 +114,20 @@ test('infers fragment spreads', () => { test('infers fragment spreads for fragment refs', () => { type fragment = parseDocument['definitions'][0] & { - [$tada.fragmentName]: 'Fields'; - }; + fragment Fields on Query { __typename } + `>; type query = parseDocument; - type actual = getDocumentType; + type extraFragments = getFragmentsOfDocumentsRec<[decorateDocument]>; + type actual = getDocumentType; type expected = { - todos: Array<{ - [$tada.fragmentRefs]: { - Fields: fragment; - }; - } | null> | null; + [$tada.fragmentRefs]?: { + Fields: extraFragments['Fields'][$tada.fragmentId]; + }; }; expectTypeOf().toEqualTypeOf(); @@ -275,3 +272,25 @@ test('infers queries from GitHub introspection schema', () => { expectTypeOf().toEqualTypeOf(); }); + +test('creates a type for a given fragment', () => { + type fragment = parseDocument<` + fragment Fields on Todo { + id + text + complete + __typename + } + `>; + + type actual = getDocumentType; + + type expected = { + __typename: 'Todo'; + id: string | number; + text: string; + complete: boolean | null; + }; + + expectTypeOf().toEqualTypeOf(); +}); diff --git a/src/api.ts b/src/api.ts index 54c24c5e..de4bbb63 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,9 +1,57 @@ import { parse as _parse } from '@0no-co/graphql.web'; -import type { stringLiteral } from './utils'; -import type { parseDocument } from './parser'; +import type { + IntrospectionQuery, + ScalarsLike, + IntrospectionLikeType, + mapIntrospection, +} from './introspection'; +import type { + decorateDocument, + getFragmentsOfDocumentsRec, + FragmentDocumentNode, +} from './namespace'; +import type { getDocumentType } from './selection'; +import type { getVariablesType } from './variables'; +import type { parseDocument, DocumentNodeLike } from './parser'; +import type { stringLiteral, TypedDocumentNode } from './utils'; + +interface AbstractSetupSchema { + introspection: IntrospectionQuery; + scalars: ScalarsLike; +} + +interface setupSchema extends AbstractSetupSchema { + /*empty*/ +} + +type Schema = mapIntrospection; function parse>(input: In): parseDocument { return _parse(input) as any; } -export { parse }; +type getDocumentNode< + In extends string, + Introspection extends IntrospectionLikeType, + Fragments extends { [name: string]: any } = {}, +> = parseDocument extends infer Document extends DocumentNodeLike + ? getDocumentType extends infer Result + ? Result extends never + ? never + : TypedDocumentNode> & + decorateDocument + : never + : never; + +function graphql< + const In extends stringLiteral, + const Fragments extends readonly [...FragmentDocumentNode[]], +>( + input: In, + _fragments?: Fragments +): getDocumentNode> { + return _parse(input) as any; +} + +export { parse, graphql }; +export type { setupSchema }; diff --git a/src/fragments.ts b/src/fragments.ts deleted file mode 100644 index 1a1aa3f8..00000000 --- a/src/fragments.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Kind } from '@0no-co/graphql.web'; -import type { DocumentNodeLike } from './parser'; - -type _getFragmentMapRec = Definitions extends readonly [ - infer Definition, - ...infer Rest, -] - ? (Definition extends { kind: Kind.FRAGMENT_DEFINITION; name: any } - ? { [Name in Definition['name']['value']]: Definition } - : {}) & - _getFragmentMapRec - : {}; - -export type getFragmentMap = _getFragmentMapRec< - Document['definitions'] ->; diff --git a/src/introspection.ts b/src/introspection.ts index 0758fb17..9b7951a8 100644 --- a/src/introspection.ts +++ b/src/introspection.ts @@ -1,6 +1,6 @@ import type { obj } from './utils'; -interface IntrospectionQuery { +export interface IntrospectionQuery { readonly __schema: IntrospectionSchema; } diff --git a/src/namespace.ts b/src/namespace.ts index 321598fd..c65c0e8d 100644 --- a/src/namespace.ts +++ b/src/namespace.ts @@ -1,3 +1,7 @@ +import type { Kind } from '@0no-co/graphql.web'; +import type { DocumentNodeLike } from './parser'; +import type { TypedDocumentNode } from './utils'; + /** Private namespace holding our symbols for markers. * * @remarks @@ -10,8 +14,48 @@ declare namespace $tada { const fragmentRefs: unique symbol; export type fragmentRefs = typeof fragmentRefs; - const fragmentName: unique symbol; - export type fragmentName = typeof fragmentName; + const fragmentDef: unique symbol; + export type fragmentDef = typeof fragmentDef; + + const fragmentId: unique symbol; + export type fragmentId = typeof fragmentId; +} + +type decorateDocument = Document['definitions'][0] extends { + kind: Kind.FRAGMENT_DEFINITION; + name: any; + typeCondition: any; } + ? { + [$tada.fragmentDef]?: Document['definitions'][0] & { + readonly [$tada.fragmentId]: unique symbol; + }; + } + : {}; + +type getFragmentsOfDocumentsRec = Documents extends readonly [ + infer Document, + ...infer Rest, +] + ? (Document extends { [$tada.fragmentDef]?: any } + ? Exclude extends infer FragmentDef extends { + kind: Kind.FRAGMENT_DEFINITION; + name: any; + typeCondition: any; + } + ? { [Name in FragmentDef['name']['value']]: FragmentDef } + : {} + : {}) & + getFragmentsOfDocumentsRec + : {}; + +type FragmentDocumentNode = { + [$tada.fragmentDef]?: { + readonly [$tada.fragmentId]: symbol; + kind: Kind.FRAGMENT_DEFINITION; + name: any; + typeCondition: any; + }; +}; -export type { $tada }; +export type { $tada, decorateDocument, getFragmentsOfDocumentsRec, FragmentDocumentNode }; diff --git a/src/selection.ts b/src/selection.ts index 655a1226..af7f0d1a 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -8,7 +8,6 @@ import type { import type { $tada } from './namespace'; import type { obj, objValues } from './utils'; -import type { getFragmentMap } from './fragments'; import type { DocumentNodeLike } from './parser'; import type { @@ -81,10 +80,12 @@ type getFragmentSelection< ? getSelection : Node extends { kind: Kind.FRAGMENT_SPREAD; name: any } ? Node['name']['value'] extends keyof Fragments - ? Fragments[Node['name']['value']] extends infer Fragment extends { - [$tada.fragmentName]: string; - } - ? { [$tada.fragmentRefs]: { [Name in Fragment[$tada.fragmentName]]: Fragment } } + ? Fragments[Node['name']['value']] extends { readonly [$tada.fragmentId]: symbol } + ? { + [$tada.fragmentRefs]?: { + [Name in Node['name']['value']]: Fragments[Node['name']['value']][$tada.fragmentId]; + }; + } : getSelection< Fragments[Node['name']['value']]['selectionSet']['selections'], Type, @@ -196,55 +197,53 @@ type getFragmentsSelection< ? _getFragmentsSelectionRec : {}; -type getDefinitionSelectionRec< - Definitions extends any[], +type getOperationSelectionType< + Definition, Introspection extends IntrospectionLikeType, Fragments extends { [name: string]: any }, -> = Definitions extends readonly [infer Definition, ...infer Rest] - ? Definition extends { - kind: Kind.OPERATION_DEFINITION; - selectionSet: { kind: Kind.SELECTION_SET; selections: [...infer Selections] }; - operation: any; - } - ? Introspection['types'][Introspection[Definition['operation']]] extends ObjectLikeType - ? getSelection< - Selections, - Introspection['types'][Introspection[Definition['operation']]], - Introspection, - Fragments - > - : {} - : getDefinitionSelectionRec - : {}; +> = Definition extends { + kind: Kind.OPERATION_DEFINITION; + selectionSet: any; + operation: any; +} + ? Introspection['types'][Introspection[Definition['operation']]] extends infer Type extends + ObjectLikeType + ? getSelection + : {} + : never; -type getDocumentType< - Document extends DocumentNodeLike, +type getFragmentSelectionType< + Definition, Introspection extends IntrospectionLikeType, - Fragments extends { [name: string]: any } = {}, -> = getDefinitionSelectionRec< - Document['definitions'], - Introspection, - getFragmentMap & Fragments ->; + Fragments extends { [name: string]: any }, +> = Definition extends { + kind: Kind.FRAGMENT_DEFINITION; + selectionSet: any; + typeCondition: any; +} + ? Introspection['types'][Definition['typeCondition']['name']['value']] extends infer Type extends + ObjectLikeType + ? getSelection + : {} + : never; -type getFragmentType< +type getDocumentType< Document extends DocumentNodeLike, Introspection extends IntrospectionLikeType, Fragments extends { [name: string]: any } = {}, -> = Document['definitions'][0] extends { - kind: Kind.FRAGMENT_DEFINITION; - typeCondition: { name: { value: infer TypeName } }; -} - ? TypeName extends keyof Introspection['types'] - ? Introspection['types'][TypeName] extends ObjectLikeType - ? getSelection< - Document['definitions'][0]['selectionSet']['selections'], - Introspection['types'][TypeName], - Introspection, - getFragmentMap & Fragments - > +> = Document['definitions'] extends readonly [infer Definition, ...infer Rest] + ? Definition extends { kind: Kind.OPERATION_DEFINITION } + ? getOperationSelectionType & Fragments> + : Definition extends { kind: Kind.FRAGMENT_DEFINITION } + ? getFragmentSelectionType & Fragments> : never - : never : never; -export type { getDocumentType, getFragmentType }; +type getFragmentMapRec = Definitions extends readonly [infer Definition, ...infer Rest] + ? (Definition extends { kind: Kind.FRAGMENT_DEFINITION; name: any } + ? { [Name in Definition['name']['value']]: Definition } + : {}) & + getFragmentMapRec + : {}; + +export type { getDocumentType, getFragmentMapRec }; diff --git a/src/utils.ts b/src/utils.ts index db8b550b..034384e8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,7 +22,7 @@ export type objValues = T[keyof T] extends infer U : U : never; -/** A GraphQL `DocumentNode` with attached generics for its result data and variables. +/** Annotations for GraphQL’s `DocumentNode` with attached generics for its result data and variables types. * * @remarks * A GraphQL {@link DocumentNode} defines both the variables it accepts on request and the `data` @@ -41,10 +41,10 @@ export type objValues = T[keyof T] extends infer U * * @see {@link https://github.com/dotansimha/graphql-typed-document-node} for more information. */ -export type TypedDocumentNode< +export interface DocumentDecoration< Result = { [key: string]: any }, Variables = { [key: string]: any }, -> = DocumentNode & { +> { /** Type to support `@graphql-typed-document-node/core` * @internal */ @@ -53,4 +53,29 @@ export type TypedDocumentNode< * @internal */ __ensureTypesOfVariablesAndResultMatching?: (variables: Variables) => Result; -}; +} + +/** A GraphQL `DocumentNode` with attached generics for its result data and variables. + * + * @remarks + * A GraphQL {@link DocumentNode} defines both the variables it accepts on request and the `data` + * shape it delivers on a response in the GraphQL query language. + * + * To bridge the gap to TypeScript, tools may be used to generate TypeScript types that define the shape + * of `data` and `variables` ahead of time. These types are then attached to GraphQL documents using this + * `TypedDocumentNode` type. + * + * Using a `DocumentNode` that is typed like this will cause any `urql` API to type its input `variables` + * and resulting `data` using the types provided. + * + * @privateRemarks + * For compatibility reasons this type has been copied and internalized from: + * https://github.com/dotansimha/graphql-typed-document-node/blob/3711b12/packages/core/src/index.ts#L3-L10 + * + * @see {@link https://github.com/dotansimha/graphql-typed-document-node} for more information. + */ +export interface TypedDocumentNode< + Result = { [key: string]: any }, + Variables = { [key: string]: any }, +> extends DocumentNode, + DocumentDecoration {} diff --git a/src/variables.ts b/src/variables.ts index 54b3a6bc..46ecac73 100644 --- a/src/variables.ts +++ b/src/variables.ts @@ -1,5 +1,6 @@ import type { Kind, TypeNode } from '@0no-co/graphql.web'; import type { IntrospectionLikeType } from './introspection'; +import type { DocumentNodeLike } from './parser'; import type { obj } from './utils'; type getInputObjectTypeRec< @@ -86,26 +87,14 @@ type getVariablesRec< getVariablesRec : {}; -type getDefinitionVariablesRec< - Definitions extends readonly unknown[], +type getVariablesType< + Document extends DocumentNodeLike, Introspection extends IntrospectionLikeType, -> = (Definitions[0] extends { +> = Document['definitions'][0] extends { kind: Kind.OPERATION_DEFINITION; - variableDefinitions: infer VarDefs; + variableDefinitions: any; } - ? VarDefs extends Array<{ kind: Kind.VARIABLE_DEFINITION }> - ? getVariablesRec - : never - : never) & - (Definitions extends readonly [any, ...infer Rest] - ? Rest extends readonly [] - ? {} - : getDefinitionVariablesRec - : never); - -type getVariablesType< - D extends { kind: Kind.DOCUMENT; definitions: any[] }, - I extends IntrospectionLikeType, -> = obj>; + ? obj> + : {}; export type { getVariablesType };