Skip to content

Commit 7e1adfe

Browse files
committed
Add tests, handle toString() call internally
1 parent 75dff97 commit 7e1adfe

File tree

3 files changed

+152
-17
lines changed

3 files changed

+152
-17
lines changed

src/with-defaults.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,6 @@ import type {
77
import { graphql } from "./graphql.js";
88
import type { DocumentTypeDecoration } from "@graphql-typed-document-node/core";
99

10-
/**
11-
* Allows discarding the DocumentTypeDecoration information from the external
12-
* interface, and casting the String part to string. This avoids the external
13-
* API interface spreading to the internal types, while keeping type-safety for
14-
* future extensions/changes.
15-
*/
16-
type DiscardTypeDecoration<T> = T extends String &
17-
DocumentTypeDecoration<unknown, unknown>
18-
? string
19-
: T;
20-
2110
export function withDefaults(
2211
request: typeof Request,
2312
newDefaults: RequestParameters,
@@ -30,11 +19,20 @@ export function withDefaults(
3019
| RequestParameters,
3120
options?: RequestParameters,
3221
) => {
33-
return graphql<ResponseData>(
34-
newRequest,
35-
query as DiscardTypeDecoration<typeof query>,
36-
options,
37-
);
22+
const innerQuery =
23+
typeof query === "string"
24+
? query
25+
: // Allows casting String & DocumentTypeDecoration<unknown, unknown> to
26+
// string. This could be replaced with an instanceof check if we had
27+
// access to a shared TypedDocumentString. Alternatively, we could use
28+
// string & TypedDocumentDecoration<unknown, unknown> as the external
29+
// interface, and push `.toString()` onto the caller, which might not
30+
// be the worst idea.
31+
String.prototype.isPrototypeOf(query)
32+
? query.toString()
33+
: (query as RequestParameters);
34+
35+
return graphql<ResponseData>(newRequest, innerQuery, options);
3836
};
3937

4038
return Object.assign(newApi, {

test/graphql.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,25 @@ import type * as OctokitTypes from "@octokit/types";
55
import { graphql } from "../src";
66
import { VERSION } from "../src/version";
77
import type { RequestParameters } from "../src/types";
8+
import type { DocumentTypeDecoration } from "@graphql-typed-document-node/core";
89

910
const userAgent = `octokit-graphql.js/${VERSION} ${getUserAgent()}`;
1011

12+
class TypedDocumentString<TResult, TVariables>
13+
extends String
14+
implements DocumentTypeDecoration<TResult, TVariables>
15+
{
16+
__apiType?: DocumentTypeDecoration<TResult, TVariables>["__apiType"];
17+
18+
constructor(private value: string) {
19+
super(value);
20+
}
21+
22+
toString(): string & DocumentTypeDecoration<TResult, TVariables> {
23+
return this.value;
24+
}
25+
}
26+
1127
describe("graphql()", () => {
1228
it("is a function", () => {
1329
expect(graphql).toBeInstanceOf(Function);
@@ -75,6 +91,72 @@ describe("graphql()", () => {
7591
});
7692
});
7793

94+
it("README TypedDocumentString example", () => {
95+
const mockData = {
96+
repository: {
97+
issues: {
98+
edges: [
99+
{
100+
node: {
101+
title: "Foo",
102+
},
103+
},
104+
{
105+
node: {
106+
title: "Bar",
107+
},
108+
},
109+
{
110+
node: {
111+
title: "Baz",
112+
},
113+
},
114+
],
115+
},
116+
},
117+
};
118+
119+
const RepositoryDocument = new TypedDocumentString<
120+
{
121+
repository: { issues: { edges: Array<{ node: { title: string } }> } };
122+
},
123+
Record<string, never>
124+
>(/* GraphQL */ `
125+
{
126+
repository(owner: "octokit", name: "graphql.js") {
127+
issues(last: 3) {
128+
edges {
129+
node {
130+
title
131+
}
132+
}
133+
}
134+
}
135+
}
136+
`);
137+
138+
return graphql(RepositoryDocument, {
139+
headers: {
140+
authorization: `token secret123`,
141+
},
142+
request: {
143+
fetch: fetchMock.sandbox().post(
144+
"https://api.github.com/graphql",
145+
{ data: mockData },
146+
{
147+
headers: {
148+
accept: "application/vnd.github.v3+json",
149+
authorization: "token secret123",
150+
"user-agent": userAgent,
151+
},
152+
},
153+
),
154+
},
155+
}).then((result) => {
156+
expect(JSON.stringify(result)).toStrictEqual(JSON.stringify(mockData));
157+
});
158+
});
159+
78160
it("Variables", () => {
79161
const query = `query lastIssues($owner: String!, $repo: String!, $num: Int = 3) {
80162
repository(owner:$owner, name:$repo) {
@@ -114,6 +196,54 @@ describe("graphql()", () => {
114196
});
115197
});
116198

199+
it("Variables with TypedDocumentString", () => {
200+
const query = new TypedDocumentString<
201+
{
202+
repository: { issues: { edges: Array<{ node: { title: string } }> } };
203+
},
204+
{
205+
owner: string;
206+
repo: string;
207+
num?: number;
208+
}
209+
>(`query lastIssues($owner: String!, $repo: String!, $num: Int = 3) {
210+
repository(owner:$owner, name:$repo) {
211+
issues(last:$num) {
212+
edges {
213+
node {
214+
title
215+
}
216+
}
217+
}
218+
}
219+
}`);
220+
221+
return graphql(query, {
222+
headers: {
223+
authorization: `token secret123`,
224+
},
225+
owner: "octokit",
226+
repo: "graphql.js",
227+
request: {
228+
fetch: fetchMock
229+
.sandbox()
230+
.post(
231+
"https://api.github.com/graphql",
232+
(_url, options: OctokitTypes.RequestOptions) => {
233+
const body = JSON.parse(options.body);
234+
expect(body.query).toEqual(query.toString());
235+
expect(body.variables).toStrictEqual({
236+
owner: "octokit",
237+
repo: "graphql.js",
238+
});
239+
240+
return { data: {} };
241+
},
242+
),
243+
},
244+
});
245+
});
246+
117247
it("Pass headers together with variables as 2nd argument", () => {
118248
const query = `query lastIssues($owner: String!, $repo: String!, $num: Int = 3) {
119249
repository(owner:$owner, name:$repo) {

test/tsconfig.test.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
"compilerOptions": {
44
"emitDeclarationOnly": false,
55
"noEmit": true,
6-
"allowImportingTsExtensions": true
6+
"allowImportingTsExtensions": true,
7+
// TODO: This setting is not compatible with the default definition of
8+
// DocumentTypeDecoration from '@graphql-codegen-types/core'. That
9+
// definition includes `{__apiType?: ...}` as an optional, so assigning it
10+
// to an explicit `undefined` is impossible under
11+
// exactOptionalPropertyTypes. We only need to work around this in tests,
12+
// since that is where we use concrete examples of DocumentTypeDecoration.
13+
"exactOptionalPropertyTypes": false
714
},
815
"include": ["src/**/*"]
916
}

0 commit comments

Comments
 (0)