graphql-data-generator 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/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # graphql-data-generator
2
+
3
+ A tool to generate objects and operation mocks from a GraphQL schema. Allows
4
+ defining _transforms_ to simplify common variations.
5
+
6
+ ## Example
7
+
8
+ First generate types and type lists:
9
+
10
+ ```sh
11
+ npx graphql-data-generator --schema src/graphql/schema.graphql --outfile src/util/test/types.ts
12
+ ```
13
+
14
+ Then consume these types and initialize a builder:
15
+
16
+ ```ts
17
+ import { readFileSync } from "node:fs";
18
+ import { init, Patch } from "npm:graphql-data-generator";
19
+ import {
20
+ Inputs,
21
+ inputs,
22
+ Mutation,
23
+ mutations,
24
+ queries,
25
+ Query,
26
+ Subscription,
27
+ subscriptions,
28
+ Types,
29
+ types,
30
+ } from "./types.ts";
31
+
32
+ const schema = readFileSync("graphql/schema.graphql", "utf-8");
33
+
34
+ const scalars = {
35
+ ID: (typename) => `${typename.toLowerCase()}-0`,
36
+ String: "",
37
+ };
38
+
39
+ export const build = init<Query, Mutation, Subscription, Types, Inputs>(
40
+ schema,
41
+ queries,
42
+ mutations,
43
+ subscriptions,
44
+ types,
45
+ inputs,
46
+ scalars,
47
+ )(() => ({
48
+ // Can define transforms for objects
49
+ User: {
50
+ // `default` automatically applies to all User objects
51
+ default: { profilePicture: (u) => `https://example.com/${u.id}.png` },
52
+ // Can invoke with `build.User.withPost()` or `build.User().withPost()`
53
+ withPost: (_p, post: Patch<Types["Post"]> = {}) => ({
54
+ posts: { next: post },
55
+ }),
56
+ },
57
+ // Can define transforms for operations
58
+ CreatePost: {
59
+ withAuthorId: (_, authorId: string) => ({
60
+ variables: { input: { authorId } },
61
+ data: { createPost: { author: { id: authorId } } },
62
+ }),
63
+ },
64
+ }));
65
+ ```
66
+
67
+ After which you can build objects and operations in your tests:
68
+
69
+ ```ts
70
+ import { build } from "util/tests/build.ts";
71
+
72
+ const user1 = build.User().withPost();
73
+ // Can override properties
74
+ const user2 = build.User({ id: "user-2", email: (u) => u.email + "2" });
75
+ // `patch` is a built-in transform while `withPost` was defined above
76
+ const user3 = user1.patch({
77
+ id: "user-3",
78
+ // `last` is special property for arrays to modify the last element in the array. If one does not exist it is created
79
+ // `next` is a special property for arrays to append a new item to the array
80
+ posts: { last: { author: { id: "user-3" } }, next: {} },
81
+ });
82
+
83
+ const createPost = build.CreatePost({ data: { createPost: { id: "post-id" } } })
84
+ .withAuthorId("user-3");
85
+ ```
86
+
87
+ ## Patches
88
+
89
+ A `patch` is similar to a `DeepPartial` with a few extensions. First, functions
90
+ can be passed instead of literal properties. These functions will be invoked
91
+ during instantiation and will receieve the previous host value as a property:
92
+
93
+ ```ts
94
+ type Thing = { foo: string };
95
+ type ThingPatch = Patch<Thing>;
96
+ // Can exclude properties
97
+ const patch1: ThingPatch = {};
98
+ // Can specify them
99
+ const patch2: ThingPatch = { foo: "ok" };
100
+ // undefined will be ignored
101
+ const patch3: ThingPatch = { foo: undefined };
102
+ // Can use a function for more dynamic values
103
+ const patch4: ThingPatch = { foo: (prev: Thing) => `${prev.foo}2` };
104
+ ```
105
+
106
+ `Patch` also has added semantics for arrays, including an object notation:
107
+
108
+ ```ts
109
+ type Container = { values: string[] };
110
+ type ContainerPatch = Patch<Container>;
111
+ // Directly set index 1
112
+ const patch1: ContainerPatch = { values: { 1: "ok" } };
113
+ // `last` will modify the last element in the array. If the array is empty,
114
+ // instantiates a new element.
115
+ const patch2: ContainerPatch = { values: { last: "ok" } };
116
+ // `next` instantiates a new element and appends it to the array.
117
+ const patch3: ContainerPatch = { values: { next: "ok" } };
118
+ // `length` can be used to truncate or instantiate new elements
119
+ const patch4: ContainerPatch = { values: { length: 0 } };
120
+ // An array can be directly used. Will truncate extra elements.
121
+ const patch5: ContainerPatch = { values: ["ok"] };
122
+ ```
123
+
124
+ ## Transforms
125
+
126
+ `graphql-data-generator` creates objects and operations by applying **patches**
127
+ in sequence. A **patch** is similar to a `DeepPartial`, but supports functions
128
+ for each property and has . Transforms are a mechanism to define shorthands for
129
+ common patches for particular objects or operations. There are several built in
130
+ transforms:
131
+
132
+ Objects:
133
+
134
+ - `default`: A special transform that is automatically called for all
135
+ instantations.
136
+ - `patch`: Accepts a list of patches
137
+
138
+ Operations:
139
+
140
+ - `patch`: Accepts a list of patches
141
+ - `variables`: Accepts an operation variable patch
142
+ - `data`: Accepts an operation data patch
143
+
144
+ When defining custom transforms, the `default` transform has special meaning: it
145
+ will be automatically applied as the first aptch to all instances.
146
+
147
+ ## Extra
148
+
149
+ The `init` function supports a 6th optional generic parameter, Extra, which
150
+ allows defining extra properties for operation mocks, passable in operation
151
+ patches. This is helpful to support extra Apollo-related properties or custom
152
+ logic. Extra properties will always be optional in patches and the final object
153
+ and will not be patched in but simply merged, such as by `Object.assign`.
154
+
155
+ ### Example: Adding an extra optional property to bypass an assertion a mock is used
156
+
157
+ ```ts
158
+ const build = init<
159
+ Query,
160
+ Mutation,
161
+ Subscription,
162
+ Types,
163
+ Inputs,
164
+ { optional: boolean }
165
+ >(
166
+ schema,
167
+ queries,
168
+ mutations,
169
+ subscriptions,
170
+ types,
171
+ inputs,
172
+ scalars,
173
+ )(() => ({}));
174
+
175
+ build.CreatePost({ optional: true }).optional; // true
176
+ ```
177
+
178
+ ## finalizeOperation
179
+
180
+ The `init`'s final parmaeter, `options`, supports a `finalizeOperation` key.
181
+ This is used as final step when building an operation and acts as a generic
182
+ transform on the final mock itself, which can be useful to attach spies or when
183
+ building interactivity with other GQL tools.
184
+
185
+ ### Exmaple: Enforcing a mock is used in Apollo & Jest
186
+
187
+ ```ts
188
+ const build = init<Query, Mutation, Subscription, Types, Inputs>(
189
+ schema,
190
+ queries,
191
+ mutations,
192
+ subscriptions,
193
+ types,
194
+ inputs,
195
+ scalars,
196
+ {
197
+ finalizeOperation: (op) => {
198
+ const fn = Object.assign(
199
+ jest.fn(() => op.result),
200
+ op.result,
201
+ ) as typeof op["result"];
202
+ op.result = fn;
203
+ return op;
204
+ },
205
+ },
206
+ )(() => ({}));
207
+
208
+ const createPost = build.CreatePost();
209
+ // ...
210
+ expect(createPost.result).toHaveBeenCalled();
211
+ ```
@@ -0,0 +1,5 @@
1
+ import { Deno } from "@deno/shim-deno";
2
+ export { Deno } from "@deno/shim-deno";
3
+ export declare const dntGlobalThis: Omit<typeof globalThis, "Deno"> & {
4
+ Deno: typeof Deno;
5
+ };
@@ -0,0 +1,61 @@
1
+ import { Deno } from "@deno/shim-deno";
2
+ export { Deno } from "@deno/shim-deno";
3
+ const dntGlobals = {
4
+ Deno,
5
+ };
6
+ export const dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
7
+ function createMergeProxy(baseObj, extObj) {
8
+ return new Proxy(baseObj, {
9
+ get(_target, prop, _receiver) {
10
+ if (prop in extObj) {
11
+ return extObj[prop];
12
+ }
13
+ else {
14
+ return baseObj[prop];
15
+ }
16
+ },
17
+ set(_target, prop, value) {
18
+ if (prop in extObj) {
19
+ delete extObj[prop];
20
+ }
21
+ baseObj[prop] = value;
22
+ return true;
23
+ },
24
+ deleteProperty(_target, prop) {
25
+ let success = false;
26
+ if (prop in extObj) {
27
+ delete extObj[prop];
28
+ success = true;
29
+ }
30
+ if (prop in baseObj) {
31
+ delete baseObj[prop];
32
+ success = true;
33
+ }
34
+ return success;
35
+ },
36
+ ownKeys(_target) {
37
+ const baseKeys = Reflect.ownKeys(baseObj);
38
+ const extKeys = Reflect.ownKeys(extObj);
39
+ const extKeysSet = new Set(extKeys);
40
+ return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
41
+ },
42
+ defineProperty(_target, prop, desc) {
43
+ if (prop in extObj) {
44
+ delete extObj[prop];
45
+ }
46
+ Reflect.defineProperty(baseObj, prop, desc);
47
+ return true;
48
+ },
49
+ getOwnPropertyDescriptor(_target, prop) {
50
+ if (prop in extObj) {
51
+ return Reflect.getOwnPropertyDescriptor(extObj, prop);
52
+ }
53
+ else {
54
+ return Reflect.getOwnPropertyDescriptor(baseObj, prop);
55
+ }
56
+ },
57
+ has(_target, prop) {
58
+ return prop in extObj || prop in baseObj;
59
+ },
60
+ });
61
+ }
package/esm/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/esm/cli.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ import * as dntShim from "./_dnt.shims.js";
3
+ import { readFile } from "node:fs";
4
+ import fg from "fast-glob";
5
+ import { parseArgs } from "node:util";
6
+ import { codegen, loadFiles } from "./codegen.js";
7
+ import { formatCode } from "./util.js";
8
+ import process from "node:process";
9
+ const args = parseArgs({
10
+ args: process.argv.slice(2),
11
+ options: {
12
+ schema: { type: "string" },
13
+ scalars: { type: "string" },
14
+ operations: { type: "string", multiple: true },
15
+ scalar: { type: "string", multiple: true },
16
+ outfile: { type: "string" },
17
+ useEnums: { type: "boolean", default: false },
18
+ includeTypenames: { type: "boolean", default: true },
19
+ },
20
+ }).values;
21
+ const findFirst = async (path) => {
22
+ for await (const file of fg.stream(path)) {
23
+ return file.toString();
24
+ }
25
+ };
26
+ const schemaPath = args.schema
27
+ ? await findFirst(args.schema)
28
+ : (await findFirst("**/schema.graphql") ??
29
+ await findFirst("**/schema.gql") ??
30
+ await findFirst("**/schema.graphqls"));
31
+ const fail = (reason, code = 1) => {
32
+ process.stderr.write(reason + "\n");
33
+ process.exit(code);
34
+ };
35
+ if (!schemaPath) {
36
+ fail(`Could not locate schema${args.schema ? ` "${args.schema}"` : ", try passing --schema"}`);
37
+ throw ""; // TS being dumb?
38
+ }
39
+ const operationDirs = args.operations?.map((v) => `${v}`) ?? ["."];
40
+ const [schema, operations] = await loadFiles(schemaPath, operationDirs);
41
+ const defaultScalars = {
42
+ Int: "number",
43
+ Float: "number",
44
+ String: "string",
45
+ Boolean: "boolean",
46
+ ID: "string",
47
+ };
48
+ const scalars = { ...defaultScalars };
49
+ if (args.scalars) {
50
+ readFile;
51
+ Object.assign(scalars, JSON.parse(await dntShim.Deno.readTextFile(args.scalars)));
52
+ }
53
+ if (args.scalar) {
54
+ for (const kv of args.scalar) {
55
+ const [key, value] = `${kv}`.split(":");
56
+ if (!value) {
57
+ fail("Invalid --scalar argument. Pass as a key-value pair like --scalar=key:value");
58
+ }
59
+ scalars[key] = value;
60
+ }
61
+ }
62
+ try {
63
+ const file = await formatCode(codegen(schema, operations, {
64
+ useEnums: args.useEnums,
65
+ includeTypenames: args.includeTypenames,
66
+ scalars,
67
+ }));
68
+ if (args.outfile)
69
+ await dntShim.Deno.writeTextFile(args.outfile, file);
70
+ else
71
+ console.log(file);
72
+ }
73
+ catch (err) {
74
+ const message = err instanceof Error ? err.message : `${err}`;
75
+ if (message.startsWith("Could not find scalar")) {
76
+ const scalar = message.match(/'([^']*)'/)?.[1];
77
+ fail(`${message}. Try passing --scalars=scalars.json or --scalar=${scalar ?? "scalar"}:string, replacing string with the TypeScript type of the scalar.`);
78
+ }
79
+ fail(err instanceof Error ? err.message : err);
80
+ }
@@ -0,0 +1,12 @@
1
+ export declare const codegen: (schema: string, files: {
2
+ path: string;
3
+ content: string;
4
+ }[], { useEnums, scalars, includeTypenames }?: {
5
+ useEnums?: boolean;
6
+ scalars?: Record<string, string | undefined>;
7
+ includeTypenames?: boolean;
8
+ }) => string;
9
+ export declare const loadFiles: (schemaPath: string, operationDirs: string[]) => Promise<[schema: string, operations: {
10
+ path: string;
11
+ content: string;
12
+ }[]]>;