Skip to content

Commit 608c0c5

Browse files
committed
feat: add deduplication for interpolated fragment definitions in gql helper
1 parent 3308c19 commit 608c0c5

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

src/__tests__/graphql.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,27 @@ describe("BaseHttpClient.graphql()", () => {
101101
);
102102
});
103103

104+
it("dedupes interpolated fragment definitions by fragment name", () => {
105+
const fragment = gql`
106+
fragment UserFields on User {
107+
id
108+
name
109+
}
110+
`;
111+
112+
const document = gql`
113+
${fragment}
114+
${fragment}
115+
query GetUser {
116+
user {
117+
...UserFields
118+
}
119+
}
120+
`;
121+
122+
expect(document.match(/fragment UserFields on User/g)).toHaveLength(1);
123+
});
124+
104125
it("omits variables and operationName from the body when not provided", async () => {
105126
let captured: RequestContext | undefined;
106127

src/graphql/gql.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,78 @@ export function gql(
88
chunks: TemplateStringsArray,
99
...values: unknown[]
1010
): string {
11-
return chunks.reduce(
11+
const source = chunks.reduce(
1212
(source, chunk, index) =>
1313
`${source}${chunk}${index in values ? String(values[index]) : ""}`,
1414
"",
1515
);
16+
17+
return dedupeFragmentDefinitions(source);
18+
}
19+
20+
function dedupeFragmentDefinitions(source: string): string {
21+
const seen = new Set<string>();
22+
const fragmentPattern =
23+
/\bfragment\s+([_A-Za-z][_0-9A-Za-z]*)\s+on\s+[_A-Za-z][_0-9A-Za-z]*/g;
24+
25+
let result = "";
26+
let cursor = 0;
27+
let match = fragmentPattern.exec(source);
28+
29+
while (match) {
30+
const name = match[1];
31+
if (!name) {
32+
match = fragmentPattern.exec(source);
33+
continue;
34+
}
35+
36+
const bodyStart = source.indexOf("{", fragmentPattern.lastIndex);
37+
if (bodyStart === -1) {
38+
match = fragmentPattern.exec(source);
39+
continue;
40+
}
41+
42+
const bodyEnd = findMatchingBrace(source, bodyStart);
43+
if (bodyEnd === -1) {
44+
match = fragmentPattern.exec(source);
45+
continue;
46+
}
47+
48+
const fragmentStart = match.index;
49+
const fragmentEnd = consumeTrailingWhitespace(source, bodyEnd + 1);
50+
51+
if (!seen.has(name)) {
52+
seen.add(name);
53+
result += source.slice(cursor, fragmentEnd);
54+
} else {
55+
result += source.slice(cursor, fragmentStart);
56+
}
57+
58+
cursor = fragmentEnd;
59+
fragmentPattern.lastIndex = fragmentEnd;
60+
match = fragmentPattern.exec(source);
61+
}
62+
63+
return result + source.slice(cursor);
64+
}
65+
66+
function findMatchingBrace(source: string, openBraceIndex: number): number {
67+
let depth = 0;
68+
69+
for (let index = openBraceIndex; index < source.length; index++) {
70+
const char = source[index];
71+
if (char === "{") depth++;
72+
if (char === "}") depth--;
73+
if (depth === 0) return index;
74+
}
75+
76+
return -1;
77+
}
78+
79+
function consumeTrailingWhitespace(source: string, index: number): number {
80+
let next = index;
81+
while (next < source.length && /\s/.test(source[next] ?? "")) {
82+
next++;
83+
}
84+
return next;
1685
}

0 commit comments

Comments
 (0)