Skip to content

Commit

Permalink
feat: Implement @_unmask directives to unmask fragment types (#31)
Browse files Browse the repository at this point in the history
Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>
  • Loading branch information
kitten and JoviDeCroock committed Jan 22, 2024
1 parent e714a8f commit e064743
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 102 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-knives-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gql.tada': patch
---

Format `TadaDocumentNode` output’s third generic differently. The output of fragment definitions will now be more readable (e.g. `{ fragment: 'Name', on: 'Type', masked: true }`)
5 changes: 5 additions & 0 deletions .changeset/little-dragons-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gql.tada': minor
---

Add support for `@_unmask` directive on fragments causing the fragment type to not be masked. `FragmentOf<>` will return the full result type of fragments when they’re annotated with `@_unmask` and spreading these unmasked fragments into parent documents will use their full type.
108 changes: 106 additions & 2 deletions src/__tests__/api.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,106 @@
import { describe, it, expectTypeOf } from 'vitest';

import type { simpleSchema } from './fixtures/simpleSchema';
import type { simpleIntrospection } from './fixtures/simpleIntrospection';

import type { parseDocument } from '../parser';
import type { ResultOf, FragmentOf, mirrorFragmentTypeRec, getDocumentNode } from '../api';
import { readFragment } from '../api';
import type { $tada } from '../namespace';
import { readFragment, initGraphQLTada } from '../api';

import type {
ResultOf,
VariablesOf,
FragmentOf,
mirrorFragmentTypeRec,
getDocumentNode,
} from '../api';

type schema = simpleSchema;
type value = { __value: true };
type data = { __data: true };

describe('Public API', () => {
const graphql = initGraphQLTada<{ introspection: typeof simpleIntrospection }>();

it('should create a fragment mask on masked fragments', () => {
const fragment = graphql(`
fragment Fields on Todo {
id
text
}
`);

const query = graphql(
`
query Test($limit: Int) {
todos(limit: $limit) {
...Fields
}
}
`,
[fragment]
);

expectTypeOf<FragmentOf<typeof fragment>>().toEqualTypeOf<{
[$tada.fragmentRefs]: {
Fields: $tada.ref;
};
}>();

expectTypeOf<ResultOf<typeof query>>().toEqualTypeOf<{
todos:
| ({
[$tada.fragmentRefs]: {
Fields: $tada.ref;
};
} | null)[]
| null;
}>();

expectTypeOf<VariablesOf<typeof query>>().toEqualTypeOf<{
limit?: number | null;
}>();
});

it('should create a fragment type on unmasked fragments', () => {
const fragment = graphql(`
fragment Fields on Todo @_unmask {
id
text
}
`);

const query = graphql(
`
query Test($limit: Int) {
todos(limit: $limit) {
...Fields
}
}
`,
[fragment]
);

expectTypeOf<FragmentOf<typeof fragment>>().toEqualTypeOf<{
id: string | number;
text: string;
}>();

expectTypeOf<ResultOf<typeof query>>().toEqualTypeOf<{
todos:
| ({
id: string | number;
text: string;
} | null)[]
| null;
}>();

expectTypeOf<VariablesOf<typeof query>>().toEqualTypeOf<{
limit?: number | null;
}>();
});
});

describe('mirrorFragmentTypeRec', () => {
it('mirrors null and undefined', () => {
expectTypeOf<mirrorFragmentTypeRec<value, data>>().toEqualTypeOf<data>();
Expand Down Expand Up @@ -108,4 +200,16 @@ describe('readFragment', () => {
const result = readFragment({} as document, {} as FragmentOf<document>);
expectTypeOf<typeof result>().toEqualTypeOf<ResultOf<document>>();
});

it('should behave correctly on unmasked fragments', () => {
type fragment = parseDocument<`
fragment Fields on Todo @_unmask {
id
}
`>;

type document = getDocumentNode<fragment, schema>;
const result = readFragment({} as document, {} as FragmentOf<document>);
expectTypeOf<typeof result>().toEqualTypeOf<ResultOf<document>>();
});
});
102 changes: 97 additions & 5 deletions src/__tests__/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import { describe, test } from 'vitest';
import { Kind } from '@0no-co/graphql.web';
import { describe, it, test, expect } from 'vitest';
import * as ts from './tsHarness';
import type { simpleIntrospection } from './fixtures/simpleIntrospection';
import { initGraphQLTada } from '../api';

const testTypeHost = test.each([
{ strictNullChecks: false, noImplicitAny: false },
{ strictNullChecks: true },
]);

describe('graphql function', () => {
it('should strip @_unmask from fragment documents', () => {
const graphql = initGraphQLTada<{ introspection: typeof simpleIntrospection }>();

const todoFragment = graphql(`
fragment TodoData on Todo @_unmask {
id
text
}
`);

expect(todoFragment).toMatchObject({
definitions: [
{
kind: Kind.FRAGMENT_DEFINITION,
directives: [],
},
],
});
});
});

describe('declare setupSchema configuration', () => {
testTypeHost('creates simple documents (%o)', (options) => {
const virtualHost = ts.createVirtualHost({
Expand Down Expand Up @@ -46,16 +71,19 @@ describe('declare setupSchema configuration', () => {
const result: ResultOf<typeof todoQuery> = {} as any;
expectTypeOf<typeof result>().toMatchTypeOf<{
expectTypeOf<typeof result>().toEqualTypeOf<{
todos: ({
id: string | number;
complete: boolean | null;
[$tada.fragmentRefs]: {
TodoData: $tada.ref;
};
} | null)[] | null;
}>();
const todo = readFragment(todoFragment, result?.todos?.[0]);
expectTypeOf<typeof todo>().toMatchTypeOf<{
expectTypeOf<typeof todo>().toEqualTypeOf<{
id: string | number;
text: string;
} | undefined | null>();
Expand Down Expand Up @@ -108,16 +136,80 @@ describe('initGraphQLTada configuration', () => {
const result: ResultOf<typeof todoQuery> = {} as any;
expectTypeOf<typeof result>().toMatchTypeOf<{
expectTypeOf<typeof result>().toEqualTypeOf<{
todos: ({
id: string | number;
complete: boolean | null;
[$tada.fragmentRefs]: {
TodoData: $tada.ref;
};
} | null)[] | null;
}>();
const todo = readFragment(todoFragment, result?.todos?.[0]);
expectTypeOf<typeof todo>().toEqualTypeOf<{
id: string | number;
text: string;
} | undefined | null>();
`,
});

ts.runDiagnostics(
ts.createTypeHost({
...options,
rootNames: ['index.ts'],
host: virtualHost,
})
);
});

testTypeHost('creates simple documents with unmasked fragments (%o)', (options) => {
const virtualHost = ts.createVirtualHost({
...ts.readVirtualModule('expect-type'),
...ts.readVirtualModule('@0no-co/graphql.web'),
...ts.readSourceFolders(['.']),
'simpleIntrospection.ts': ts.readFileFromRoot(
'src/__tests__/fixtures/simpleIntrospection.ts'
),
'index.ts': `
import { expectTypeOf } from 'expect-type';
import { simpleIntrospection } from './simpleIntrospection';
import { ResultOf, FragmentOf, initGraphQLTada, readFragment } from './api';
import { $tada } from './namespace';
const graphql = initGraphQLTada<{ introspection: typeof simpleIntrospection; }>();
const todoFragment = graphql(\`
fragment TodoData on Todo @_unmask {
id
text
}
\`);
const todoQuery = graphql(\`
query {
todos {
id
complete
...TodoData
}
}
\`, [todoFragment]);
const result: ResultOf<typeof todoQuery> = {} as any;
expectTypeOf<typeof result>().toEqualTypeOf<{
todos: ({
id: string | number;
complete: boolean | null;
text: string;
} | null)[] | null;
}>();
const todo = readFragment(todoFragment, result?.todos?.[0]);
expectTypeOf<typeof todo>().toMatchTypeOf<{
expectTypeOf<typeof todo>().toEqualTypeOf<{
id: string | number;
text: string;
} | undefined | null>();
Expand Down
55 changes: 27 additions & 28 deletions src/__tests__/namespace.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,29 @@ describe('decorateFragmentDef', () => {
};

type actual = decorateFragmentDef<input>;
type expected = {

expectTypeOf<actual>().toMatchTypeOf<{
fragment: 'TodoFragment';
on: 'Todo';
}>();
});
});

describe('getFragmentsOfDocumentsRec', () => {
type actual = getFragmentsOfDocumentsRec<
[
{
[$tada.definition]?: {
fragment: 'TodoFragment';
on: 'Todo';
masked: true;
};
},
]
>;

type expected = {
TodoFragment: {
kind: Kind.FRAGMENT_DEFINITION;
name: {
kind: Kind.NAME;
Expand All @@ -41,36 +63,13 @@ describe('decorateFragmentDef', () => {
value: 'Todo';
};
};
readonly [$tada.fragmentId]: symbol;
};

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

describe('getFragmentsOfDocumentsRec', () => {
type inputFragmentDef = {
kind: Kind.FRAGMENT_DEFINITION;
name: {
kind: Kind.NAME;
value: 'TodoFragment';
};
typeCondition: {
kind: Kind.NAMED_TYPE;
name: {
kind: Kind.NAME;
value: 'Todo';
[$tada.ref]: {
[$tada.fragmentRefs]: {
TodoFragment: $tada.ref;
};
};
};
readonly [$tada.fragmentId]: unique symbol;
};

type input = {
[$tada.fragmentDef]?: inputFragmentDef;
};

type actual = getFragmentsOfDocumentsRec<[input]>;
type expected = { TodoFragment: inputFragmentDef };

expectTypeOf<actual>().toMatchTypeOf<expected>();
});
13 changes: 4 additions & 9 deletions src/__tests__/selection.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
$tada,
decorateFragmentDef,
getFragmentsOfDocumentsRec,
makeFragmentDefDecoration,
makeDefinitionDecoration,
} from '../namespace';

type schema = simpleSchema;
Expand Down Expand Up @@ -203,7 +203,7 @@ test('infers fragment spreads', () => {
expectTypeOf<expected>().toEqualTypeOf<actual>();
});

test('infers fragment spreads for fragment refs', () => {
test('infers fragment spreads for masked fragment refs', () => {
type fragment = parseDocument</* GraphQL */ `
fragment Fields on Query { __typename }
`>;
Expand All @@ -213,16 +213,11 @@ test('infers fragment spreads for fragment refs', () => {
`>;

type extraFragments = getFragmentsOfDocumentsRec<
[makeFragmentDefDecoration<decorateFragmentDef<fragment>>]
[makeDefinitionDecoration<decorateFragmentDef<fragment>>]
>;

type actual = getDocumentType<query, schema, extraFragments>;

type expected = {
[$tada.fragmentRefs]: {
Fields: extraFragments['Fields'][$tada.fragmentId];
};
};
type expected = extraFragments['Fields'][$tada.ref];

expectTypeOf<expected>().toEqualTypeOf<actual>();
});
Expand Down
Loading

0 comments on commit e064743

Please sign in to comment.