Skip to content

Commit

Permalink
feat(operation-ids): choose between inline, attach and skip fragments
Browse files Browse the repository at this point in the history
  • Loading branch information
pmelab committed Oct 23, 2024
1 parent 20c93c8 commit 7209255
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 13 deletions.
140 changes: 132 additions & 8 deletions packages/npm/@amazeelabs/codegen-operation-ids/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { buildSchema, parse } from 'graphql';
import { describe, expect, it } from 'vitest';

import { plugin } from './';
import { inlineFragments } from './inline';

const schema = buildSchema(`
type Query {
Expand All @@ -19,8 +20,16 @@ const schema = buildSchema(`
`);

describe('mode: map', () => {
function runPlugin(documents: Array<Types.DocumentFile>) {
return plugin(schema, documents, {}, { outputFile: 'map.json' });
function runPlugin(
documents: Array<Types.DocumentFile>,
mode: 'inline' | 'attach' | undefined = 'inline',
) {
return plugin(
schema,
documents,
{ fragments: mode },
{ outputFile: 'map.json' },
);
}

it('creates a map entry for a single operation', async () => {
Expand Down Expand Up @@ -123,7 +132,30 @@ describe('mode: map', () => {
`);
});

it('adds used fragments', async () => {
it('adds used fragments inline', async () => {
const result = await runPlugin([
{
location: 'a.gql',
document: parse(`
fragment Page on Page { title }
query Home { loadPage(path: "/") { ...Page } }
`),
},
]);
expect(JSON.parse(result)).toMatchInlineSnapshot(`
{
"HomeQuery:dc086da112964a8f85ae0520dab3fa68a84e067ee6aa8b0e06305f4cb5e9898a": "query Home {
loadPage(path: "/") {
... on Page {
title
}
}
}",
}
`);
});

it('attaches used fragments', async () => {
const result = await runPlugin([
{
location: 'a.gql',
Expand All @@ -135,7 +167,7 @@ describe('mode: map', () => {
]);
expect(JSON.parse(result)).toMatchInlineSnapshot(`
{
"HomeQuery:37d40553a898c4026ba372c8f42af3df9c3451953b65695b823a8e1e7b5fd90d": "query Home {
"HomeQuery:dc086da112964a8f85ae0520dab3fa68a84e067ee6aa8b0e06305f4cb5e9898a": "query Home {
loadPage(path: "/") {
... on Page {
title
Expand All @@ -145,6 +177,7 @@ describe('mode: map', () => {
}
`);
});

it('inlines multiple invocations', async () => {
const result = await runPlugin([
{
Expand All @@ -166,7 +199,7 @@ describe('mode: map', () => {
]);
expect(JSON.parse(result)).toMatchInlineSnapshot(`
{
"HomeQuery:e8b5953fe0f339244ebb14102eddc5d0e23259606de6f697574f69bfe468ac53": "query Home {
"HomeQuery:9e615243c0c61d86531091aa395544df99db822d94d50a761c2809d61caf3164": "query Home {
loadPage(path: "/") {
... on Page {
title
Expand All @@ -190,6 +223,59 @@ describe('mode: map', () => {
}
`);
});

it('attaches multiple invocations', async () => {
const result = await runPlugin(
[
{
location: 'a.gql',
document: parse(`
fragment Page on Page { title, related { path } }
fragment Teaser on Page { path }
query Home {
loadPage(path: "/") {
...Page,
related {
...Page
...Teaser
}
}
}
`),
},
],
'attach',
);
expect(JSON.parse(result)).toMatchInlineSnapshot(`
{
"HomeQuery:c25b93055bb9dce4d474a8e9031df3842a686ad4ad1b3ce2806ef528eb5b1c47": "query Home {
loadPage(path: "/") {
...Page
related {
...Page
...Teaser
}
}
}
fragment Page on Page {
title
related {
path
}
}
fragment Page on Page {
title
related {
path
}
}
fragment Teaser on Page {
path
}",
}
`);
});

it('adds nested fragments', async () => {
const result = await runPlugin([
{
Expand All @@ -207,7 +293,7 @@ describe('mode: map', () => {
]);
expect(JSON.parse(result)).toMatchInlineSnapshot(`
{
"HomeQuery:37d40553a898c4026ba372c8f42af3df9c3451953b65695b823a8e1e7b5fd90d": "query Home {
"HomeQuery:7f81601e1df0c179be7354f3f834201b9615d0ea191d4ba5f83bb45b0bfe052d": "query Home {
loadPage(path: "/") {
... on Page {
title
Expand All @@ -223,6 +309,44 @@ describe('mode: map', () => {
`);
});

it('attaches nested fragments', async () => {
const result = await runPlugin(
[
{
location: 'a.gql',
document: parse(`
fragment RelatedPage on Page { title }
fragment Page on Page { title, related { ...RelatedPage } }
query Home {
loadPage(path: "/") {
...Page,
}
}
`),
},
],
'attach',
);
expect(JSON.parse(result)).toMatchInlineSnapshot(`
{
"HomeQuery:fe7f24087d5deae03464a1e58e1c5a50cd6dbbbf4b4e68c41e5f6b9f2f947d3c": "query Home {
loadPage(path: "/") {
...Page
}
}
fragment Page on Page {
title
related {
...RelatedPage
}
}
fragment RelatedPage on Page {
title
}",
}
`);
});

it('adds fragments from different documents', async () => {
const result = await runPlugin([
{
Expand All @@ -244,7 +368,7 @@ describe('mode: map', () => {
]);
expect(JSON.parse(result)).toMatchInlineSnapshot(`
{
"HomeQuery:37d40553a898c4026ba372c8f42af3df9c3451953b65695b823a8e1e7b5fd90d": "query Home {
"HomeQuery:dc086da112964a8f85ae0520dab3fa68a84e067ee6aa8b0e06305f4cb5e9898a": "query Home {
loadPage(path: "/") {
... on Page {
title
Expand Down Expand Up @@ -282,7 +406,7 @@ describe('mode: map', () => {
]);
expect(JSON.parse(result)).toMatchInlineSnapshot(`
{
"HomeQuery:37d40553a898c4026ba372c8f42af3df9c3451953b65695b823a8e1e7b5fd90d": "query Home {
"HomeQuery:7f81601e1df0c179be7354f3f834201b9615d0ea191d4ba5f83bb45b0bfe052d": "query Home {
loadPage(path: "/") {
... on Page {
title
Expand Down
27 changes: 22 additions & 5 deletions packages/npm/@amazeelabs/codegen-operation-ids/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from 'graphql';

import { inlineFragments } from './inline';
import { scanFragments } from './scan';

class OperationIdVisitor extends ClientSideBaseVisitor {
_extractFragments() {
Expand All @@ -30,16 +31,17 @@ class OperationIdVisitor extends ClientSideBaseVisitor {

return `export const ${operationResultType} = "${queryId(
node,
print(node),
)}" as OperationId<${operationResultType},${operationVariablesTypes}${
hasRequiredVariables ? '' : ' | undefined'
}>;`;
}
}

function queryId(node: OperationDefinitionNode) {
function queryId(node: OperationDefinitionNode, content: string) {
return `${node.name?.value ?? 'anonymous'}${pascalCase(
node.operation,
)}:${crypto.createHash('sha256').update(print(node)).digest('hex')}`;
)}:${crypto.createHash('sha256').update(content).digest('hex')}`;
}

export const plugin: PluginFunction<any, string> = async (
Expand Down Expand Up @@ -69,9 +71,24 @@ export const plugin: PluginFunction<any, string> = async (
const idMap = new Map<string, string>();
visit(allAst, {
OperationDefinition(node) {
const query = [print(inlineFragments(node, fragmentMap))];
const id = queryId(node);
operationMap.set(id, query.join('\n'));
const query = [
print(
config.fragments === 'inline'
? inlineFragments(node, fragmentMap)
: node,
),
];
if (config.fragments === 'attach') {
scanFragments(node, fragmentMap).forEach((name) => {
const fragment = fragmentMap.get(name);
if (fragment) {
query.push(print(fragment));
}
});
}
const queryString = query.join('\n');
const id = queryId(node, queryString);
operationMap.set(id, queryString);
if (node.name) {
idMap.set(node.name.value, id);
}
Expand Down
98 changes: 98 additions & 0 deletions packages/npm/@amazeelabs/codegen-operation-ids/src/scan.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
DefinitionNode,
FragmentDefinitionNode,
Kind,
OperationDefinitionNode,
parse,
print,
} from 'graphql';
import { describe, expect, it } from 'vitest';

import { scanFragments } from './scan';

function isFragmentDefinitionNode(
def: DefinitionNode,
): def is FragmentDefinitionNode {
return def.kind === Kind.FRAGMENT_DEFINITION;
}

function isOperationDefinitionNode(
def: DefinitionNode,
): def is OperationDefinitionNode {
return def.kind === Kind.OPERATION_DEFINITION;
}

describe('scanFragments', () => {
it('detects a fragment on a query', () => {
const doc = parse(`
query {
...A
}
fragment A on Query {
myprop
}
`);
const [query] = doc.definitions.filter(isOperationDefinitionNode);
const [A] = doc.definitions.filter(isFragmentDefinitionNode);
const fragments = scanFragments(query, new Map(Object.entries({ A })));
expect(fragments).toEqual(['A']);
});

it('detects a fragment on a field', () => {
const doc = parse(`
query {
a {
...A
}
}
fragment A on A {
myprop
}
`);
const [query] = doc.definitions.filter(isOperationDefinitionNode);
const [A] = doc.definitions.filter(isFragmentDefinitionNode);
const fragments = scanFragments(query, new Map(Object.entries({ A })));
expect(fragments).toEqual(['A']);
});

it('detects nested fragments', () => {
const doc = parse(`query {
a {
propA
...A
}
}
fragment A on A {
propA
propB {
...B
}
}
fragment B on B {
propC
}`);
const [query] = doc.definitions.filter(isOperationDefinitionNode);
const [A, B] = doc.definitions.filter(isFragmentDefinitionNode);
const fragments = scanFragments(query, new Map(Object.entries({ A, B })));
expect(fragments).toEqual(['A', 'B']);
});
it('detects mixed fragments', () => {
const doc = parse(`query {
a {
propA
... on A {
propB {
...B
}
}
}
}
fragment B on B {
propC
}`);
const [query] = doc.definitions.filter(isOperationDefinitionNode);
const [B] = doc.definitions.filter(isFragmentDefinitionNode);
const fragments = scanFragments(query, new Map(Object.entries({ B })));
expect(fragments).toEqual(['B']);
});
});
Loading

0 comments on commit 7209255

Please sign in to comment.