Skip to content

Commit

Permalink
Add initial masked fragment interpolation
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten committed Jan 10, 2024
1 parent 0f131b0 commit 1a89dff
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 138 deletions.
32 changes: 0 additions & 32 deletions src/__tests__/fragments.test-d.ts

This file was deleted.

7 changes: 4 additions & 3 deletions src/__tests__/parser.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<kitchensinkQuery>;

expectTypeOf<parseDocument<kitchensinkQuery>>().toEqualTypeOf<kitchensinkDocument>();
expectTypeOf<parseDocument<kitchensinkQuery>>().toMatchTypeOf<kitchensinkDocument>();
expectTypeOf<parseDocument<kitchensinkQuery>>().toMatchTypeOf<DocumentNode>();
expectTypeOf<actual>().toEqualTypeOf<kitchensinkDocument>();
expectTypeOf<actual>().toMatchTypeOf<kitchensinkDocument>();
expectTypeOf<actual>().toMatchTypeOf<DocumentNode>();
});
});
43 changes: 31 additions & 12 deletions src/__tests__/selection.test-d.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -114,23 +114,20 @@ test('infers fragment spreads', () => {

test('infers fragment spreads for fragment refs', () => {
type fragment = parseDocument</* GraphQL */ `
fragment Fields on Todo { id text __typename }
`>['definitions'][0] & {
[$tada.fragmentName]: 'Fields';
};
fragment Fields on Query { __typename }
`>;

type query = parseDocument</* GraphQL */ `
query { todos { ...Fields } }
query { ...Fields }
`>;

type actual = getDocumentType<query, schema, { Fields: fragment }>;
type extraFragments = getFragmentsOfDocumentsRec<[decorateDocument<fragment>]>;
type actual = getDocumentType<query, schema, extraFragments>;

type expected = {
todos: Array<{
[$tada.fragmentRefs]: {
Fields: fragment;
};
} | null> | null;
[$tada.fragmentRefs]?: {
Fields: extraFragments['Fields'][$tada.fragmentId];
};
};

expectTypeOf<expected>().toEqualTypeOf<actual>();
Expand Down Expand Up @@ -275,3 +272,25 @@ test('infers queries from GitHub introspection schema', () => {

expectTypeOf<expected>().toEqualTypeOf<actual>();
});

test('creates a type for a given fragment', () => {
type fragment = parseDocument<`
fragment Fields on Todo {
id
text
complete
__typename
}
`>;

type actual = getDocumentType<fragment, schema>;

type expected = {
__typename: 'Todo';
id: string | number;
text: string;
complete: boolean | null;
};

expectTypeOf<expected>().toEqualTypeOf<actual>();
});
57 changes: 54 additions & 3 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,60 @@
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<setupSchema['introspection'], setupSchema['scalars']>;

function parse<const In extends stringLiteral<In>>(input: In): parseDocument<In> {
return _parse(input) as any;
}

export { parse };
type getDocumentNode<
In extends string,
Introspection extends IntrospectionLikeType,
Fragments extends { [name: string]: any } = {},
> = parseDocument<In> extends infer Document extends DocumentNodeLike
? getDocumentType<Document, Introspection, Fragments> extends infer Result
? Result extends never
? never
: TypedDocumentNode<Result, getVariablesType<Document, Introspection>> &
decorateDocument<Document>
: never
: never;

function graphql<
const In extends stringLiteral<In>,
const Fragments extends readonly [...FragmentDocumentNode[]],
>(
input: In,
_fragments?: Fragments
): getDocumentNode<In, Schema, getFragmentsOfDocumentsRec<Fragments>> {
return _parse(input) as any;
}

export { parse, graphql };
export type { setupSchema };
16 changes: 0 additions & 16 deletions src/fragments.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/introspection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { obj } from './utils';

interface IntrospectionQuery {
export interface IntrospectionQuery {
readonly __schema: IntrospectionSchema;
}

Expand Down
49 changes: 46 additions & 3 deletions src/namespace.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { Kind } from '@0no-co/graphql.web';
import type { DocumentNodeLike } from './parser';

/** Private namespace holding our symbols for markers.
*
* @remarks
Expand All @@ -10,8 +13,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 extends DocumentNodeLike> = 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> = Documents extends readonly [
infer Document,
...infer Rest,
]
? (Document extends { [$tada.fragmentDef]?: any }
? Exclude<Document[$tada.fragmentDef], undefined> extends infer FragmentDef extends {
kind: Kind.FRAGMENT_DEFINITION;
name: any;
typeCondition: any;
}
? { [Name in FragmentDef['name']['value']]: FragmentDef }
: {}
: {}) &
getFragmentsOfDocumentsRec<Rest>
: {};

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 };
91 changes: 45 additions & 46 deletions src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -81,10 +80,12 @@ type getFragmentSelection<
? getSelection<Node['selectionSet']['selections'], Type, Introspection, Fragments>
: 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,
Expand Down Expand Up @@ -196,55 +197,53 @@ type getFragmentsSelection<
? _getFragmentsSelectionRec<Selections, Type['name'], Type, Introspection, Fragments>
: {};

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<Rest, Introspection, Fragments>
: {};
> = Definition extends {
kind: Kind.OPERATION_DEFINITION;
selectionSet: any;
operation: any;
}
? Introspection['types'][Introspection[Definition['operation']]] extends infer Type extends
ObjectLikeType
? getSelection<Definition['selectionSet']['selections'], Type, Introspection, Fragments>
: {}
: never;

type getDocumentType<
Document extends DocumentNodeLike,
type getFragmentSelectionType<
Definition,
Introspection extends IntrospectionLikeType,
Fragments extends { [name: string]: any } = {},
> = getDefinitionSelectionRec<
Document['definitions'],
Introspection,
getFragmentMap<Document> & 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<Definition['selectionSet']['selections'], Type, Introspection, Fragments>
: {}
: 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<Document> & Fragments
>
> = Document['definitions'] extends readonly [infer Definition, ...infer Rest]
? Definition extends { kind: Kind.OPERATION_DEFINITION }
? getOperationSelectionType<Definition, Introspection, getFragmentMapRec<Rest> & Fragments>
: Definition extends { kind: Kind.FRAGMENT_DEFINITION }
? getFragmentSelectionType<Definition, Introspection, getFragmentMapRec<Rest> & Fragments>
: never
: never
: never;

export type { getDocumentType, getFragmentType };
type getFragmentMapRec<Definitions> = Definitions extends readonly [infer Definition, ...infer Rest]
? (Definition extends { kind: Kind.FRAGMENT_DEFINITION; name: any }
? { [Name in Definition['name']['value']]: Definition }
: {}) &
getFragmentMapRec<Rest>
: {};

export type { getDocumentType, getFragmentMapRec };
Loading

0 comments on commit 1a89dff

Please sign in to comment.