1import { assert, describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3
4import { aliasedGraphQlDocument, readGraphQlPages } from "./githubGraphQl.ts";
5
6describe("aliasedGraphQlDocument", () => {
7 it("keeps every value out of the document, each under its alias's own variable", () => {
8 const document = aliasedGraphQlDocument({
9 operation: "query",
10 name: "Lookups",
11 alias: "s",
12 items: ['acme") { x } #', "web"],
13 shared: { owner: ["String!", "acme"] },
14 variables: (repo) => ({ name: ["String!", repo] }),
15 field: ({ name }) => `repository(owner: $owner, name: ${name}) { id }`,
16 });
17 assert.isNotNull(document);
18 expect(document!.query).toBe(
19 "query Lookups($owner: String!, $s0_name: String!, $s1_name: String!) {\n" +
20 " s0: repository(owner: $owner, name: $s0_name) { id }\n" +
21 " s1: repository(owner: $owner, name: $s1_name) { id }\n}",
22 );
23 expect(document!.variables).toEqual({
24 owner: "acme",
25 s0_name: 'acme") { x } #',
26 s1_name: "web",
27 });
28 });
29
30 it("aliases by key and nests inside a shared parent when asked", () => {
31 const document = aliasedGraphQlDocument({
32 operation: "query",
33 alias: "pr",
34 key: (number) => number,
35 items: [2, 3],
36 variables: (number) => ({ number: ["Int!", number] }),
37 field: ({ number }) => `pullRequest(number: ${number}) { id }`,
38 within: (fields) => `repository(owner: "a", name: "b") {\n${fields}\n}`,
39 });
40 expect(document?.query).toContain(" pr2: pullRequest(number: $pr2_number) { id }");
41 expect(document?.query).toContain(" pr3: pullRequest(number: $pr3_number) { id }");
42 expect(document?.variables).toEqual({ pr2_number: 2, pr3_number: 3 });
43 });
44
45 it("asks for nothing when there is nothing to ask", () => {
46 expect(
47 aliasedGraphQlDocument({
48 operation: "mutation",
49 alias: "f",
50 items: [],
51 variables: () => ({}),
52 field: () => "x",
53 }),
54 ).toBeNull();
55 });
56});
57
58describe("readGraphQlPages", () => {
59 const pages = (cursors: ReadonlyArray<string | null>) => {
60 const asked: Array<string | null> = [];
61 const read = (after: string | null) =>
62 Effect.sync(() => {
63 asked.push(after);
64 return { next: cursors[asked.length - 1] ?? null };
65 });
66 return { asked, read };
67 };
68
69 it.effect("follows the cursor until GitHub has no more", () =>
70 Effect.gen(function* () {
71 const { asked, read } = pages(["a", "b", null]);
72 const result = yield* readGraphQlPages(read, { nextCursor: (page) => page.next });
73 expect(asked).toEqual([null, "a", "b"]);
74 expect(result.truncated).toBe(false);
75 }),
76 );
77
78 it.effect("stops as truncated at the page limit, at `until`, or on a repeated cursor", () =>
79 Effect.gen(function* () {
80 const limited = pages(["a", "b", "c"]);
81 const atLimit = yield* readGraphQlPages(limited.read, {
82 nextCursor: (page) => page.next,
83 maxPages: 2,
84 });
85 expect(limited.asked).toEqual([null, "a"]);
86 expect(atLimit.truncated).toBe(true);
87
88 const enough = pages(["a", "b", "c"]);
89 const satisfied = yield* readGraphQlPages(enough.read, {
90 nextCursor: (page) => page.next,
91 until: (read) => read.length === 1,
92 });
93 expect(enough.asked).toEqual([null]);
94 expect(satisfied.truncated).toBe(true);
95
96 const stuck = pages(["a", "a", "a"]);
97 const repeated = yield* readGraphQlPages(stuck.read, {
98 nextCursor: (page) => page.next,
99 from: "start",
100 });
101 expect(stuck.asked).toEqual(["start", "a"]);
102 expect(repeated.truncated).toBe(true);
103 }),
104 );
105});