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 +211 -0
- package/esm/_dnt.shims.d.ts +5 -0
- package/esm/_dnt.shims.js +61 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +80 -0
- package/esm/codegen.d.ts +12 -0
- package/esm/codegen.js +474 -0
- package/esm/extendedTypes.d.ts +65 -0
- package/esm/extendedTypes.js +1 -0
- package/esm/index.d.ts +5 -0
- package/esm/index.js +4 -0
- package/esm/init.d.ts +16 -0
- package/esm/init.js +133 -0
- package/esm/package.json +3 -0
- package/esm/proxy.d.ts +8 -0
- package/esm/proxy.js +589 -0
- package/esm/types.d.ts +37 -0
- package/esm/types.js +1 -0
- package/esm/util.d.ts +11 -0
- package/esm/util.js +47 -0
- package/package.json +30 -0
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,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
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
|
+
}
|
package/esm/codegen.d.ts
ADDED
|
@@ -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
|
+
}[]]>;
|