@contismo/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/release.yml +38 -0
- package/CODE_OF_CONDUCT.md +28 -0
- package/CONTRIBUTING.md +34 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/clients/typescript/docs/codegen.md +107 -0
- package/clients/typescript/eslint.config.js +18 -0
- package/clients/typescript/package.json +87 -0
- package/clients/typescript/src/api-keys.ts +13 -0
- package/clients/typescript/src/cli/codegen-typescript-config.ts +36 -0
- package/clients/typescript/src/cli/generate-models.ts +94 -0
- package/clients/typescript/src/cli/generate.ts +44 -0
- package/clients/typescript/src/cli/import-from-sdk.ts +12 -0
- package/clients/typescript/src/cli/load-config.ts +1 -0
- package/clients/typescript/src/cli/main.ts +39 -0
- package/clients/typescript/src/cli/prettify-generated-types.ts +129 -0
- package/clients/typescript/src/cli/run-codegen.ts +58 -0
- package/clients/typescript/src/client.ts +152 -0
- package/clients/typescript/src/config.ts +41 -0
- package/clients/typescript/src/errors.ts +82 -0
- package/clients/typescript/src/execute.ts +104 -0
- package/clients/typescript/src/fetch-query.ts +123 -0
- package/clients/typescript/src/index.ts +41 -0
- package/clients/typescript/src/introspection.ts +165 -0
- package/clients/typescript/src/load-client.ts +47 -0
- package/clients/typescript/src/load-config.ts +64 -0
- package/clients/typescript/src/load-models.ts +35 -0
- package/clients/typescript/src/models.ts +16 -0
- package/clients/typescript/src/request-payload.ts +16 -0
- package/clients/typescript/src/resolve-codegen-paths.ts +31 -0
- package/clients/typescript/src/select.ts +22 -0
- package/clients/typescript/src/types.ts +55 -0
- package/clients/typescript/src/write-introspection.ts +13 -0
- package/clients/typescript/tests/api-keys.test.ts +14 -0
- package/clients/typescript/tests/client.test.ts +221 -0
- package/clients/typescript/tests/load-client.test.ts +126 -0
- package/clients/typescript/tests/load-config.test.ts +32 -0
- package/clients/typescript/tests/prettify-generated-types.test.ts +43 -0
- package/clients/typescript/tests/resolve-codegen-paths.test.ts +57 -0
- package/clients/typescript/tests/select.test.ts +53 -0
- package/clients/typescript/tests/write-introspection.test.ts +30 -0
- package/clients/typescript/tsconfig.json +16 -0
- package/clients/typescript/tsup.config.ts +32 -0
- package/clients/typescript/vitest.config.ts +8 -0
- package/package.json +17 -0
- package/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, it, vi, type Mock } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ContismoClient,
|
|
4
|
+
ContismoErrorCodes,
|
|
5
|
+
ContismoGraphQLError,
|
|
6
|
+
ContismoHttpError,
|
|
7
|
+
isContismoError,
|
|
8
|
+
} from "../src/index";
|
|
9
|
+
|
|
10
|
+
const config = {
|
|
11
|
+
apiKey: "gql_test_key",
|
|
12
|
+
endpoint: "https://graphql.example.com",
|
|
13
|
+
project: "proj_abc123",
|
|
14
|
+
environment: "production",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function mockFetch(response: Response): Mock<typeof fetch> {
|
|
18
|
+
return vi.fn<typeof fetch>().mockResolvedValue(response);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
22
|
+
return new Response(JSON.stringify(body), {
|
|
23
|
+
status,
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("ContismoClient", () => {
|
|
29
|
+
it("sends correct headers on query", async () => {
|
|
30
|
+
const fetch = mockFetch(
|
|
31
|
+
jsonResponse({ data: { blogPostCollection: { items: [] } } }),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
35
|
+
await client.query(`query { blogPostCollection { items { _id } } }`);
|
|
36
|
+
|
|
37
|
+
expect(fetch).toHaveBeenCalledOnce();
|
|
38
|
+
const call = fetch.mock.calls[0];
|
|
39
|
+
const init = call?.[1];
|
|
40
|
+
if (!init) throw new Error("expected fetch to be called");
|
|
41
|
+
const headers = new Headers(init.headers);
|
|
42
|
+
|
|
43
|
+
expect(headers.get("Authorization")).toBe("Bearer gql_test_key");
|
|
44
|
+
expect(headers.get("X-Project-Id")).toBe("proj_abc123");
|
|
45
|
+
expect(headers.get("X-Environment")).toBe("production");
|
|
46
|
+
expect(headers.get("Content-Type")).toBe("application/json");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns query data", async () => {
|
|
50
|
+
const fetch = mockFetch(
|
|
51
|
+
jsonResponse({
|
|
52
|
+
data: { blogPost: { _id: "1", title: "Hello" } },
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
57
|
+
const data = await client.query<{ blogPost: { _id: string; title: string } }>(
|
|
58
|
+
`query { blogPost(id: "1") { _id title } }`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(data.blogPost).toEqual({ _id: "1", title: "Hello" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws ContismoGraphQLError on GraphQL errors", async () => {
|
|
65
|
+
const fetch = mockFetch(
|
|
66
|
+
jsonResponse({
|
|
67
|
+
errors: [{ message: "Not found", extensions: { code: "NOT_FOUND" } }],
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
72
|
+
|
|
73
|
+
await expect(client.query(`query { blogPost(id: "x") { _id } }`)).rejects.toMatchObject({
|
|
74
|
+
name: "ContismoGraphQLError",
|
|
75
|
+
code: "NOT_FOUND",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("parses entry edit lock extensions", async () => {
|
|
80
|
+
const fetch = mockFetch(
|
|
81
|
+
jsonResponse({
|
|
82
|
+
errors: [
|
|
83
|
+
{
|
|
84
|
+
message: "Entry is locked",
|
|
85
|
+
extensions: {
|
|
86
|
+
code: "ENTRY_EDIT_LOCK_CONFLICT",
|
|
87
|
+
lockedByUserId: "user-1",
|
|
88
|
+
lockedByName: "Alex",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await client.query(`mutation { updateBlogPost { _id } }`);
|
|
99
|
+
expect.fail("should throw");
|
|
100
|
+
} catch (error) {
|
|
101
|
+
expect(error).toBeInstanceOf(ContismoGraphQLError);
|
|
102
|
+
const gqlError = error as ContismoGraphQLError;
|
|
103
|
+
expect(gqlError.code).toBe("ENTRY_EDIT_LOCK_CONFLICT");
|
|
104
|
+
expect(gqlError.lockedByUserId).toBe("user-1");
|
|
105
|
+
expect(gqlError.lockedByName).toBe("Alex");
|
|
106
|
+
expect(isContismoError(error)).toBe(true);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("throws ContismoHttpError on HTTP 429 without GraphQL body", async () => {
|
|
111
|
+
const fetch = mockFetch(jsonResponse({ message: "rate limited" }, 429));
|
|
112
|
+
|
|
113
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
114
|
+
|
|
115
|
+
await expect(client.query(`query { blogPostCollection { items { _id } } }`)).rejects.toBeInstanceOf(
|
|
116
|
+
ContismoHttpError,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("throws on rate limit GraphQL error shape", async () => {
|
|
121
|
+
const fetch = mockFetch(
|
|
122
|
+
jsonResponse(
|
|
123
|
+
{
|
|
124
|
+
errors: [
|
|
125
|
+
{
|
|
126
|
+
message: "Monthly API call limit exceeded",
|
|
127
|
+
extensions: { code: "RATE_LIMIT_EXCEEDED" },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
429,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
136
|
+
|
|
137
|
+
await expect(client.query(`query { blogPostCollection { items { _id } } }`)).rejects.toMatchObject({
|
|
138
|
+
code: ContismoErrorCodes.RATE_LIMIT_EXCEEDED,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("blocks mutate for read-only keys before fetch", async () => {
|
|
143
|
+
const fetch = mockFetch(jsonResponse({ data: { ok: true } }));
|
|
144
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await client.mutate(`mutation { createBlogPost(input: {}) { _id } }`);
|
|
148
|
+
expect.fail("should throw");
|
|
149
|
+
} catch (error) {
|
|
150
|
+
expect(error).toBeInstanceOf(ContismoGraphQLError);
|
|
151
|
+
expect((error as ContismoGraphQLError).code).toBe(ContismoErrorCodes.FORBIDDEN);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("allows mutate for read-write keys", async () => {
|
|
158
|
+
const fetch = mockFetch(
|
|
159
|
+
jsonResponse({ data: { createBlogPost: { _id: "new" } } }),
|
|
160
|
+
);
|
|
161
|
+
const client = new ContismoClient({
|
|
162
|
+
...config,
|
|
163
|
+
apiKey: "gqlw_test_key",
|
|
164
|
+
fetch,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const data = await client.mutate<{ createBlogPost: { _id: string } }>(
|
|
168
|
+
`mutation { createBlogPost(input: {}) { _id } }`,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(data.createBlogPost._id).toBe("new");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("runs introspection with operation name", async () => {
|
|
175
|
+
const fetch = mockFetch(
|
|
176
|
+
jsonResponse({
|
|
177
|
+
data: {
|
|
178
|
+
__schema: {
|
|
179
|
+
queryType: { name: "Query" },
|
|
180
|
+
mutationType: { name: "Mutation" },
|
|
181
|
+
subscriptionType: null,
|
|
182
|
+
types: [],
|
|
183
|
+
directives: [],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
190
|
+
const result = await client.introspect();
|
|
191
|
+
|
|
192
|
+
expect(result.__schema.queryType?.name).toBe("Query");
|
|
193
|
+
|
|
194
|
+
const call = fetch.mock.calls[0];
|
|
195
|
+
const init = call?.[1];
|
|
196
|
+
if (!init?.body) throw new Error("expected fetch to be called");
|
|
197
|
+
const body = JSON.parse(init.body as string) as {
|
|
198
|
+
operationName: string;
|
|
199
|
+
query: string;
|
|
200
|
+
};
|
|
201
|
+
expect(body.operationName).toBe("IntrospectionQuery");
|
|
202
|
+
expect(body.query).toContain("query IntrospectionQuery");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("forwards AbortSignal", async () => {
|
|
206
|
+
const fetch = mockFetch(
|
|
207
|
+
jsonResponse({ data: { blogPostCollection: { items: [] } } }),
|
|
208
|
+
);
|
|
209
|
+
const client = new ContismoClient({ ...config, fetch });
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
|
|
212
|
+
await client.query(`query { blogPostCollection { items { _id } } }`, undefined, {
|
|
213
|
+
signal: controller.signal,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const call = fetch.mock.calls[0];
|
|
217
|
+
const init = call?.[1];
|
|
218
|
+
if (!init) throw new Error("expected fetch to be called");
|
|
219
|
+
expect(init.signal).toBe(controller.signal);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { rm } from "node:fs/promises";
|
|
5
|
+
import { describe, expect, it, vi, type Mock } from "vitest";
|
|
6
|
+
import { ContismoClient } from "../src/client";
|
|
7
|
+
|
|
8
|
+
function mockFetch(response: Response): Mock<typeof fetch> {
|
|
9
|
+
return vi.fn<typeof fetch>().mockResolvedValue(response);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
13
|
+
return new Response(JSON.stringify(body), {
|
|
14
|
+
status,
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("ContismoClient config discovery", () => {
|
|
20
|
+
it("loads config and model registry from default locations", async () => {
|
|
21
|
+
const dir = join(tmpdir(), `contismo-load-${Date.now()}`);
|
|
22
|
+
await mkdir(join(dir, "src/generated"), { recursive: true });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await writeFile(
|
|
26
|
+
join(dir, "contismo.config.json"),
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
client: {
|
|
29
|
+
apiKey: "gql_test",
|
|
30
|
+
endpoint: "https://graphql.contismo.com",
|
|
31
|
+
project: "proj_1",
|
|
32
|
+
environment: "production",
|
|
33
|
+
},
|
|
34
|
+
codegen: {
|
|
35
|
+
outputDir: "./src/generated",
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
await writeFile(
|
|
41
|
+
join(dir, "src/generated/contismo-models.ts"),
|
|
42
|
+
`export const contismoModels = {
|
|
43
|
+
Test: {
|
|
44
|
+
apiId: "Test",
|
|
45
|
+
singular: "test",
|
|
46
|
+
plural: "tests",
|
|
47
|
+
graphqlType: "Entry_Test",
|
|
48
|
+
},
|
|
49
|
+
};`,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const fetch = mockFetch(jsonResponse({ data: { tests: [{ _id: "1" }] } }));
|
|
53
|
+
const client = new ContismoClient({ cwd: dir, fetch });
|
|
54
|
+
const rows = await client.fetch("Test", { select: { _id: true } });
|
|
55
|
+
|
|
56
|
+
expect(rows).toEqual([{ _id: "1" }]);
|
|
57
|
+
expect(fetch).toHaveBeenCalledOnce();
|
|
58
|
+
} finally {
|
|
59
|
+
await rm(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("loads config from a custom path", async () => {
|
|
64
|
+
const dir = join(tmpdir(), `contismo-load-custom-${Date.now()}`);
|
|
65
|
+
await mkdir(join(dir, "cfg/generated"), { recursive: true });
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await writeFile(
|
|
69
|
+
join(dir, "cfg/app.config.json"),
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
client: {
|
|
72
|
+
apiKey: "gql_test",
|
|
73
|
+
endpoint: "https://graphql.contismo.com",
|
|
74
|
+
project: "proj_2",
|
|
75
|
+
environment: "staging",
|
|
76
|
+
},
|
|
77
|
+
codegen: {
|
|
78
|
+
output: "./cfg/generated/types.ts",
|
|
79
|
+
models: "./cfg/generated/registry.mjs",
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await writeFile(
|
|
85
|
+
join(dir, "cfg/generated/registry.mjs"),
|
|
86
|
+
`export const contismoModels = {
|
|
87
|
+
Blog: { apiId: "Blog", singular: "blog", plural: "blogs", graphqlType: "Entry_Blog" },
|
|
88
|
+
};`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const fetch = mockFetch(jsonResponse({ data: { blogs: [] } }));
|
|
92
|
+
const client = new ContismoClient({
|
|
93
|
+
cwd: dir,
|
|
94
|
+
config: "./cfg/app.config.json",
|
|
95
|
+
fetch,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await expect(client.fetch("Blog", { select: { _id: true } })).resolves.toEqual([]);
|
|
99
|
+
} finally {
|
|
100
|
+
await rm(dir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("throws when the model registry file is missing", async () => {
|
|
105
|
+
const dir = join(tmpdir(), `contismo-load-missing-${Date.now()}`);
|
|
106
|
+
await mkdir(dir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await writeFile(
|
|
110
|
+
join(dir, "contismo.config.json"),
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
client: {
|
|
113
|
+
apiKey: "gql_test",
|
|
114
|
+
endpoint: "https://graphql.contismo.com",
|
|
115
|
+
project: "proj_1",
|
|
116
|
+
environment: "production",
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(() => new ContismoClient({ cwd: dir })).toThrow(/Model registry not found/);
|
|
122
|
+
} finally {
|
|
123
|
+
await rm(dir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { rm } from "node:fs/promises";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { loadContismoConfig } from "../src/load-config";
|
|
7
|
+
|
|
8
|
+
describe("loadContismoConfig", () => {
|
|
9
|
+
it("loads contismo.config.json", async () => {
|
|
10
|
+
const dir = join(tmpdir(), `contismo-config-${Date.now()}`);
|
|
11
|
+
await mkdir(dir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await writeFile(
|
|
15
|
+
join(dir, "contismo.config.json"),
|
|
16
|
+
JSON.stringify({
|
|
17
|
+
client: {
|
|
18
|
+
apiKey: "gql_test",
|
|
19
|
+
endpoint: "https://graphql.contismo.com",
|
|
20
|
+
project: "proj_1",
|
|
21
|
+
environment: "production",
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const config = loadContismoConfig(dir);
|
|
27
|
+
expect(config.client.project).toBe("proj_1");
|
|
28
|
+
} finally {
|
|
29
|
+
await rm(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { prettifyGeneratedTypes } from "../src/cli/prettify-generated-types";
|
|
3
|
+
|
|
4
|
+
describe("prettifyGeneratedTypes", () => {
|
|
5
|
+
it("inlines output scalars and Maybe wrappers without breaking utility types", () => {
|
|
6
|
+
const input = `
|
|
7
|
+
export type Maybe<T> = T | null;
|
|
8
|
+
export type InputMaybe<T> = T | null;
|
|
9
|
+
export type Scalars = {
|
|
10
|
+
DateTime: { input: string; output: string; }
|
|
11
|
+
String: { input: string; output: string; }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type Entry_Test = {
|
|
15
|
+
_id: Scalars['ID']['output'];
|
|
16
|
+
tit?: Maybe<Scalars['String']['output']>;
|
|
17
|
+
_updatedAt?: Maybe<Scalars['DateTime']['output']>;
|
|
18
|
+
content?: Maybe<RichText>;
|
|
19
|
+
};
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const out = prettifyGeneratedTypes(input);
|
|
23
|
+
|
|
24
|
+
expect(out).toContain("_id: string;");
|
|
25
|
+
expect(out).toContain("tit?: string | null;");
|
|
26
|
+
expect(out).toContain("_updatedAt?: string | null;");
|
|
27
|
+
expect(out).toContain("content?: RichText | null;");
|
|
28
|
+
expect(out).not.toContain("Scalars[");
|
|
29
|
+
expect(out).not.toContain("Maybe<");
|
|
30
|
+
expect(out).not.toContain("export type T | null");
|
|
31
|
+
expect(out).not.toContain("export type MakeT");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("leaves export type alias lines untouched before stripping boilerplate", () => {
|
|
35
|
+
const input = `
|
|
36
|
+
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
|
37
|
+
export type Entry_Test = { tit?: Maybe<Scalars['String']['output']>; };
|
|
38
|
+
`;
|
|
39
|
+
const out = prettifyGeneratedTypes(input);
|
|
40
|
+
expect(out).toContain("tit?: string | null;");
|
|
41
|
+
expect(out).not.toContain("MakeMaybe");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { resolveCodegenPaths } from "../src/resolve-codegen-paths";
|
|
4
|
+
|
|
5
|
+
describe("resolveCodegenPaths", () => {
|
|
6
|
+
const cwd = "/app";
|
|
7
|
+
|
|
8
|
+
it("uses defaults", () => {
|
|
9
|
+
expect(resolveCodegenPaths(cwd)).toEqual({
|
|
10
|
+
schemaPath: join(cwd, "contismo/schema.json"),
|
|
11
|
+
outputPath: join(cwd, "src/generated/contismo.ts"),
|
|
12
|
+
modelsPath: join(cwd, "src/generated/contismo-models.ts"),
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("uses outputDir and schemaDir", () => {
|
|
17
|
+
expect(
|
|
18
|
+
resolveCodegenPaths(cwd, {
|
|
19
|
+
outputDir: "./lib/contismo",
|
|
20
|
+
outputFile: "types.ts",
|
|
21
|
+
schemaDir: "./.contismo",
|
|
22
|
+
schemaFile: "introspection.json",
|
|
23
|
+
}),
|
|
24
|
+
).toEqual({
|
|
25
|
+
schemaPath: join(cwd, ".contismo/introspection.json"),
|
|
26
|
+
outputPath: join(cwd, "lib/contismo/types.ts"),
|
|
27
|
+
modelsPath: join(cwd, "lib/contismo/contismo-models.ts"),
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("prefers full paths when set", () => {
|
|
32
|
+
expect(
|
|
33
|
+
resolveCodegenPaths(cwd, {
|
|
34
|
+
outputDir: "./ignored",
|
|
35
|
+
output: "./custom/out.ts",
|
|
36
|
+
schemaDir: "./ignored",
|
|
37
|
+
schema: "./custom/schema.json",
|
|
38
|
+
}),
|
|
39
|
+
).toEqual({
|
|
40
|
+
schemaPath: join(cwd, "custom/schema.json"),
|
|
41
|
+
outputPath: join(cwd, "custom/out.ts"),
|
|
42
|
+
modelsPath: join(cwd, "custom/contismo-models.ts"),
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("places models next to a custom output file", () => {
|
|
47
|
+
expect(
|
|
48
|
+
resolveCodegenPaths(cwd, {
|
|
49
|
+
output: "./custom/out.ts",
|
|
50
|
+
}),
|
|
51
|
+
).toEqual({
|
|
52
|
+
schemaPath: join(cwd, "contismo/schema.json"),
|
|
53
|
+
outputPath: join(cwd, "custom/out.ts"),
|
|
54
|
+
modelsPath: join(cwd, "custom/contismo-models.ts"),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildSelectionSet } from "../src/select";
|
|
3
|
+
import { buildFetchListQuery, buildFetchOneQuery } from "../src/fetch-query";
|
|
4
|
+
import type { ModelRegistryEntry } from "../src/models";
|
|
5
|
+
|
|
6
|
+
const testModel: ModelRegistryEntry = {
|
|
7
|
+
apiId: "Test",
|
|
8
|
+
singular: "test",
|
|
9
|
+
plural: "tests",
|
|
10
|
+
graphqlType: "Entry_Test",
|
|
11
|
+
whereInput: "Entry_Test_WhereInput",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("buildSelectionSet", () => {
|
|
15
|
+
it("builds flat and nested fields", () => {
|
|
16
|
+
expect(
|
|
17
|
+
buildSelectionSet({
|
|
18
|
+
_id: true,
|
|
19
|
+
tit: true,
|
|
20
|
+
content: { asHtml: true },
|
|
21
|
+
}),
|
|
22
|
+
).toBe("_id tit content { asHtml }");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("buildFetchListQuery", () => {
|
|
27
|
+
it("builds a collection query with variables", () => {
|
|
28
|
+
const built = buildFetchListQuery(testModel, {
|
|
29
|
+
select: { _id: true, tit: true },
|
|
30
|
+
limit: 10,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(built.operationName).toBe("FetchTestList");
|
|
34
|
+
expect(built.resultKey).toBe("tests");
|
|
35
|
+
expect(built.query).toContain("tests(limit: $limit)");
|
|
36
|
+
expect(built.query).toContain("_id tit");
|
|
37
|
+
expect(built.variables).toEqual({ limit: 10 });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("buildFetchOneQuery", () => {
|
|
42
|
+
it("builds a singular query", () => {
|
|
43
|
+
const built = buildFetchOneQuery(testModel, {
|
|
44
|
+
select: { _id: true },
|
|
45
|
+
id: "entry-1",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(built.operationName).toBe("FetchTestOne");
|
|
49
|
+
expect(built.resultKey).toBe("test");
|
|
50
|
+
expect(built.query).toContain("test(id: $id)");
|
|
51
|
+
expect(built.variables).toEqual({ id: "entry-1" });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { writeIntrospectionResult } from "../src/write-introspection";
|
|
6
|
+
import type { IntrospectionQueryResult } from "../src/introspection";
|
|
7
|
+
|
|
8
|
+
describe("writeIntrospectionResult", () => {
|
|
9
|
+
it("writes schema JSON for codegen", async () => {
|
|
10
|
+
const filePath = join(tmpdir(), `contismo-introspection-${Date.now()}.json`);
|
|
11
|
+
const result: IntrospectionQueryResult = {
|
|
12
|
+
__schema: {
|
|
13
|
+
queryType: { name: "Query" },
|
|
14
|
+
mutationType: null,
|
|
15
|
+
subscriptionType: null,
|
|
16
|
+
types: [],
|
|
17
|
+
directives: [],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await writeIntrospectionResult(result, filePath);
|
|
23
|
+
const raw = await readFile(filePath, "utf8");
|
|
24
|
+
const parsed = JSON.parse(raw) as { data: IntrospectionQueryResult };
|
|
25
|
+
expect(parsed.data.__schema.queryType?.name).toBe("Query");
|
|
26
|
+
} finally {
|
|
27
|
+
await rm(filePath, { force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true,
|
|
13
|
+
"exactOptionalPropertyTypes": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src", "tests"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
const shared = {
|
|
4
|
+
sourcemap: true,
|
|
5
|
+
clean: true,
|
|
6
|
+
splitting: false,
|
|
7
|
+
treeshake: true,
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export default defineConfig([
|
|
11
|
+
{
|
|
12
|
+
...shared,
|
|
13
|
+
entry: {
|
|
14
|
+
index: "src/index.ts",
|
|
15
|
+
config: "src/config.ts",
|
|
16
|
+
},
|
|
17
|
+
format: ["esm", "cjs"],
|
|
18
|
+
dts: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
...shared,
|
|
22
|
+
clean: false,
|
|
23
|
+
entry: {
|
|
24
|
+
"cli/main": "src/cli/main.ts",
|
|
25
|
+
},
|
|
26
|
+
format: ["esm"],
|
|
27
|
+
dts: false,
|
|
28
|
+
banner: {
|
|
29
|
+
js: "#!/usr/bin/env node",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
]);
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contismo/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"packageManager": "pnpm@9.0.0",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "pnpm --filter @contismo/ts-client build",
|
|
13
|
+
"test": "pnpm --filter @contismo/ts-client test",
|
|
14
|
+
"lint": "pnpm --filter @contismo/ts-client lint",
|
|
15
|
+
"typecheck": "pnpm --filter @contismo/ts-client typecheck"
|
|
16
|
+
}
|
|
17
|
+
}
|