Skip to content

Commit 22003e1

Browse files
committed
Add tests, handle toString() call internally
1 parent eee2a7d commit 22003e1

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 { getUserAgent } from "universal-user-agent";
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);
@@ -77,6 +93,72 @@ describe("graphql()", () => {
7793
});
7894
});
7995

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

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