@newmo/graphql-codegen-fake-server-client 0.19.0 → 0.20.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
CHANGED
|
@@ -17,21 +17,21 @@ GraphQL Code Generator configuration:
|
|
|
17
17
|
import type { CodegenConfig } from "@graphql-codegen/cli";
|
|
18
18
|
|
|
19
19
|
const config: CodegenConfig = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
27
|
-
"./generated/fake-client.ts": {
|
|
28
|
-
plugins: ["@newmo/graphql-codegen-fake-server-client"],
|
|
29
|
-
config: {
|
|
30
|
-
// Required: path to the generated client's graphql file
|
|
31
|
-
typesFile: "./graphql"
|
|
32
|
-
},
|
|
33
|
-
},
|
|
20
|
+
overwrite: true,
|
|
21
|
+
schema: "./api/graphql/api.graphqls",
|
|
22
|
+
documents: "./api/graphql/query.graphql",
|
|
23
|
+
generates: {
|
|
24
|
+
"./generated/": {
|
|
25
|
+
preset: "client",
|
|
34
26
|
},
|
|
27
|
+
"./generated/fake-client.ts": {
|
|
28
|
+
plugins: ["@newmo/graphql-codegen-fake-server-client"],
|
|
29
|
+
config: {
|
|
30
|
+
// Required: path to the generated client's graphql file
|
|
31
|
+
typesFile: "./graphql",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export default config;
|
|
@@ -39,34 +39,39 @@ export default config;
|
|
|
39
39
|
|
|
40
40
|
You can use `./generated/fake-client.ts` to register the fake to the fake server.
|
|
41
41
|
|
|
42
|
+
### Basic Usage
|
|
43
|
+
|
|
42
44
|
```ts
|
|
43
45
|
import { it, expect } from "vitest";
|
|
44
46
|
import { createFakeClient } from "./generated/fake-client";
|
|
45
47
|
|
|
46
48
|
const fakeClient = createFakeClient({
|
|
47
|
-
fakeServerEndpoint: "http://localhost:4000"
|
|
48
|
-
})
|
|
49
|
+
fakeServerEndpoint: "http://localhost:4000",
|
|
50
|
+
});
|
|
49
51
|
it("register fake response for query", async () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
expect(resRegister).toMatchInlineSnapshot(`"{"ok":true}"`);
|
|
61
|
-
// request to server
|
|
62
|
-
const client = new GraphQLClient(`${fakeServerUrl}/graphql`, {
|
|
63
|
-
headers: {
|
|
64
|
-
"sequence-id": sequenceId,
|
|
52
|
+
const sequenceId = crypto.randomUUID();
|
|
53
|
+
// register fake response for GetBooks query
|
|
54
|
+
const resRegister = await fakeClient.registerGetBooksQueryResponse(
|
|
55
|
+
sequenceId,
|
|
56
|
+
{
|
|
57
|
+
books: [
|
|
58
|
+
{
|
|
59
|
+
id: "new id",
|
|
60
|
+
title: "new title",
|
|
65
61
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
expect(resRegister).toMatchInlineSnapshot(`"{"ok":true}"`);
|
|
66
|
+
// request to server
|
|
67
|
+
const client = new GraphQLClient(`${fakeServerUrl}/graphql`, {
|
|
68
|
+
headers: {
|
|
69
|
+
"sequence-id": sequenceId,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
// Got fake response
|
|
73
|
+
const response = await client.request(GetBooksDocument);
|
|
74
|
+
expect(response).toMatchInlineSnapshot(`
|
|
70
75
|
{
|
|
71
76
|
"books": [
|
|
72
77
|
{
|
|
@@ -76,13 +81,90 @@ it("register fake response for query", async () => {
|
|
|
76
81
|
],
|
|
77
82
|
}
|
|
78
83
|
`);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
// Get actual request and response for testing
|
|
85
|
+
const calledResults = await fakeClient.calledGetBooksDocumentQuery(
|
|
86
|
+
sequenceId
|
|
87
|
+
);
|
|
88
|
+
console.log(calledResults[0].request);
|
|
89
|
+
console.log(calledResults[0].response);
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Conditional Fake Responses
|
|
94
|
+
|
|
95
|
+
You can register conditional fake responses that return different results based on call count or variables. **The variables are now fully type-safe** based on your GraphQL operations:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { it, expect } from "vitest";
|
|
99
|
+
import { createFakeClient } from "./generated/fake-client";
|
|
100
|
+
|
|
101
|
+
const fakeClient = createFakeClient({
|
|
102
|
+
fakeServerEndpoint: "http://localhost:4000",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("register conditional fake responses", async () => {
|
|
106
|
+
const sequenceId = crypto.randomUUID();
|
|
107
|
+
|
|
108
|
+
// Return different response on first call
|
|
109
|
+
await fakeClient.registerGetBooksQueryResponse(
|
|
110
|
+
sequenceId,
|
|
111
|
+
{
|
|
112
|
+
books: [{ id: "1", title: "First Call" }],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
requestCondition: { type: "count", value: 1 },
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Return different response on second call
|
|
120
|
+
await fakeClient.registerGetBooksQueryResponse(
|
|
121
|
+
sequenceId,
|
|
122
|
+
{
|
|
123
|
+
books: [{ id: "2", title: "Second Call" }],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
requestCondition: { type: "count", value: 2 },
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Type-safe variables condition!
|
|
131
|
+
// TypeScript will enforce the correct variables shape for this operation
|
|
132
|
+
await fakeClient.registerListDestinationCandidatesQueryResponse(
|
|
133
|
+
sequenceId,
|
|
134
|
+
{
|
|
135
|
+
destinationCandidates: [{ id: "3", name: "Tokyo Station" }],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
requestCondition: {
|
|
139
|
+
type: "variables",
|
|
140
|
+
value: { text: "tokyo" }, // ✅ Type-safe! Must match ListDestinationCandidatesQueryVariables
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Another type-safe variables condition example
|
|
146
|
+
await fakeClient.registerCreateUrlRideHistoryMutationResponse(
|
|
147
|
+
sequenceId,
|
|
148
|
+
{
|
|
149
|
+
createURLRideHistory: { id: "4", name: "Shibuya" },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
requestCondition: {
|
|
153
|
+
type: "variables",
|
|
154
|
+
value: { desinationName: "Shibuya" }, // ✅ Type-safe for this mutation!
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
);
|
|
83
158
|
});
|
|
84
159
|
```
|
|
85
160
|
|
|
161
|
+
#### Type Safety Benefits
|
|
162
|
+
|
|
163
|
+
- **Variables validation**: TypeScript will ensure variables in conditions match the exact shape expected by your GraphQL operation
|
|
164
|
+
- **Autocomplete**: Your IDE will provide autocomplete for available variable fields
|
|
165
|
+
- **Compile-time errors**: Typos or wrong variable types will be caught at compile time
|
|
166
|
+
- **Refactoring safety**: If you change your GraphQL schema, TypeScript will help you update the affected conditions
|
|
167
|
+
|
|
86
168
|
## Options
|
|
87
169
|
|
|
88
170
|
- `typesFile` (required): Path to the generated client's graphql file.
|
|
@@ -3,10 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const config_1 = require("./config");
|
|
4
4
|
const convertName_1 = require("./convertName");
|
|
5
5
|
const plugin = {
|
|
6
|
-
plugin(
|
|
6
|
+
plugin(_schema, documents, rawConfig, _info) {
|
|
7
7
|
const config = (0, config_1.normalizeConfig)(rawConfig);
|
|
8
|
-
const
|
|
9
|
-
const registerOperationResponseType = "{ ok: true } | { ok: false; errors: string[] }";
|
|
8
|
+
const _fakeEndpoint = config.fakeServerEndpoint;
|
|
9
|
+
const registerOperationResponseType = "{ ok: true } | { ok: false; errors: string[] }"; // Conditional fake types with generic Variables
|
|
10
|
+
const conditionRuleTypes = `
|
|
11
|
+
export type FakeClientCountConditionRule = { type: "count"; value: number };
|
|
12
|
+
export type FakeClientVariablesConditionRule<TVariables = Record<string, any>> = { type: "variables"; value: TVariables };
|
|
13
|
+
export type FakeClientConditionRule<TVariables = Record<string, any>> = FakeClientCountConditionRule | FakeClientVariablesConditionRule<TVariables>;
|
|
14
|
+
export type FakeClientRegisterSequenceOptions<TVariables = Record<string, any>> = { requestCondition?: FakeClientConditionRule<TVariables> };`;
|
|
10
15
|
const indentEachLine = (indent, text) => {
|
|
11
16
|
return text
|
|
12
17
|
.split("\n")
|
|
@@ -51,7 +56,8 @@ ${exportsFunctions
|
|
|
51
56
|
}`;
|
|
52
57
|
};
|
|
53
58
|
const generateRegisterOperationMethod = (name, fakeEndpointVariableName) => {
|
|
54
|
-
|
|
59
|
+
const variablesType = `${(0, convertName_1.convertName)(name, config)}QueryVariables`;
|
|
60
|
+
return `async register${name}QueryResponse(sequenceId:string, queryResponse: ${name}Query, sequenceOptions?: FakeClientRegisterSequenceOptions<${variablesType}>): Promise<${registerOperationResponseType}> {
|
|
55
61
|
return await fetch(${fakeEndpointVariableName}, {
|
|
56
62
|
method: 'POST',
|
|
57
63
|
headers: {
|
|
@@ -61,7 +67,8 @@ ${exportsFunctions
|
|
|
61
67
|
body: JSON.stringify({
|
|
62
68
|
type: "operation",
|
|
63
69
|
operationName: "${name}",
|
|
64
|
-
data: queryResponse
|
|
70
|
+
data: queryResponse,
|
|
71
|
+
...(sequenceOptions?.requestCondition && { requestCondition: sequenceOptions.requestCondition })
|
|
65
72
|
}),
|
|
66
73
|
}).then((res) => res.json()) as ${registerOperationResponseType};
|
|
67
74
|
}`;
|
|
@@ -84,7 +91,8 @@ ${exportsFunctions
|
|
|
84
91
|
}`;
|
|
85
92
|
};
|
|
86
93
|
const generateRegisterMutationMethod = (name, fakeEndpointVariableName) => {
|
|
87
|
-
|
|
94
|
+
const variablesType = `${(0, convertName_1.convertName)(name, config)}MutationVariables`;
|
|
95
|
+
return `async register${name}MutationResponse(sequenceId:string, mutationResponse: ${name}Mutation, sequenceOptions?: FakeClientRegisterSequenceOptions<${variablesType}>): Promise<${registerOperationResponseType}> {
|
|
88
96
|
return await fetch(${fakeEndpointVariableName}, {
|
|
89
97
|
method: 'POST',
|
|
90
98
|
headers: {
|
|
@@ -94,7 +102,8 @@ ${exportsFunctions
|
|
|
94
102
|
body: JSON.stringify({
|
|
95
103
|
type: "operation",
|
|
96
104
|
operationName: "${name}",
|
|
97
|
-
data: mutationResponse
|
|
105
|
+
data: mutationResponse,
|
|
106
|
+
...(sequenceOptions?.requestCondition && { requestCondition: sequenceOptions.requestCondition })
|
|
98
107
|
}),
|
|
99
108
|
}).then((res) => res.json()) as ${registerOperationResponseType};
|
|
100
109
|
}`;
|
|
@@ -215,7 +224,7 @@ ${exportsFunctions
|
|
|
215
224
|
}`;
|
|
216
225
|
};
|
|
217
226
|
const importQueryIdentifierName = (documentName) => {
|
|
218
|
-
return `import type { ${(0, convertName_1.convertName)(documentName, config)}Query } from '${config.typesFile}';`;
|
|
227
|
+
return `import type { ${(0, convertName_1.convertName)(documentName, config)}Query, ${(0, convertName_1.convertName)(documentName, config)}QueryVariables } from '${config.typesFile}';`;
|
|
219
228
|
};
|
|
220
229
|
const importMutationIdentifierName = (documentName) => {
|
|
221
230
|
return `import type { ${(0, convertName_1.convertName)(documentName, config)}Mutation, ${(0, convertName_1.convertName)(documentName, config)}MutationVariables } from '${config.typesFile}';`;
|
|
@@ -240,6 +249,7 @@ ${documents
|
|
|
240
249
|
});
|
|
241
250
|
})
|
|
242
251
|
.join("\n")}
|
|
252
|
+
${conditionRuleTypes}
|
|
243
253
|
${generateFakeClient(documents.flatMap((document) => {
|
|
244
254
|
const flatMap = document.document?.definitions?.flatMap((definition) => {
|
|
245
255
|
if (definition.kind === "OperationDefinition" &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newmo/graphql-codegen-fake-server-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "GraphQL Codegen plugin for generating a fake server client",
|
|
6
6
|
"keywords": [
|
|
@@ -37,19 +37,19 @@
|
|
|
37
37
|
"codegen": "graphql-codegen --config graphql-codegen.ts"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@graphql-codegen/plugin-helpers": "^5.1.
|
|
41
|
-
"@graphql-codegen/visitor-plugin-common": "^5.
|
|
40
|
+
"@graphql-codegen/plugin-helpers": "^5.1.1",
|
|
41
|
+
"@graphql-codegen/visitor-plugin-common": "^5.8.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@graphql-codegen/cli": "^5.0.
|
|
45
|
-
"@graphql-codegen/typescript": "^4.1.
|
|
44
|
+
"@graphql-codegen/cli": "^5.0.7",
|
|
45
|
+
"@graphql-codegen/typescript": "^4.1.6",
|
|
46
46
|
"@graphql-typed-document-node/core": "^3.2.0",
|
|
47
47
|
"@tsconfig/node18": "^18.2.0",
|
|
48
48
|
"@tsconfig/strictest": "^2.0.1",
|
|
49
49
|
"@types/eslint": "^9.6.1",
|
|
50
|
-
"@types/node": "^
|
|
51
|
-
"graphql": "^16.
|
|
52
|
-
"typescript": "^5.
|
|
50
|
+
"@types/node": "^24.0.3",
|
|
51
|
+
"graphql": "^16.11.0",
|
|
52
|
+
"typescript": "^5.8.3"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"graphql": "^16.8.1"
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
"access": "public",
|
|
62
62
|
"registry": "https://registry.npmjs.org/"
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "e7fb0401bb3d6d989adfc98cc15ec4c936bc0cde"
|
|
65
65
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { CodegenPlugin } from "@graphql-codegen/plugin-helpers";
|
|
2
|
-
import { type RawPluginConfig
|
|
2
|
+
import { normalizeConfig, type RawPluginConfig } from "./config";
|
|
3
3
|
import { convertName } from "./convertName";
|
|
4
4
|
|
|
5
5
|
const plugin: CodegenPlugin<RawPluginConfig> = {
|
|
6
|
-
plugin(
|
|
6
|
+
plugin(_schema, documents, rawConfig, _info) {
|
|
7
7
|
const config = normalizeConfig(rawConfig);
|
|
8
|
-
const
|
|
9
|
-
const registerOperationResponseType = "{ ok: true } | { ok: false; errors: string[] }";
|
|
8
|
+
const _fakeEndpoint = config.fakeServerEndpoint;
|
|
9
|
+
const registerOperationResponseType = "{ ok: true } | { ok: false; errors: string[] }"; // Conditional fake types with generic Variables
|
|
10
|
+
const conditionRuleTypes = `
|
|
11
|
+
export type FakeClientCountConditionRule = { type: "count"; value: number };
|
|
12
|
+
export type FakeClientVariablesConditionRule<TVariables = Record<string, any>> = { type: "variables"; value: TVariables };
|
|
13
|
+
export type FakeClientConditionRule<TVariables = Record<string, any>> = FakeClientCountConditionRule | FakeClientVariablesConditionRule<TVariables>;
|
|
14
|
+
export type FakeClientRegisterSequenceOptions<TVariables = Record<string, any>> = { requestCondition?: FakeClientConditionRule<TVariables> };`;
|
|
10
15
|
type GenerateFakeFunction =
|
|
11
16
|
| {
|
|
12
17
|
type: "query";
|
|
@@ -81,7 +86,8 @@ ${exportsFunctions
|
|
|
81
86
|
name: string,
|
|
82
87
|
fakeEndpointVariableName: string,
|
|
83
88
|
) => {
|
|
84
|
-
|
|
89
|
+
const variablesType = `${convertName(name, config)}QueryVariables`;
|
|
90
|
+
return `async register${name}QueryResponse(sequenceId:string, queryResponse: ${name}Query, sequenceOptions?: FakeClientRegisterSequenceOptions<${variablesType}>): Promise<${registerOperationResponseType}> {
|
|
85
91
|
return await fetch(${fakeEndpointVariableName}, {
|
|
86
92
|
method: 'POST',
|
|
87
93
|
headers: {
|
|
@@ -91,7 +97,8 @@ ${exportsFunctions
|
|
|
91
97
|
body: JSON.stringify({
|
|
92
98
|
type: "operation",
|
|
93
99
|
operationName: "${name}",
|
|
94
|
-
data: queryResponse
|
|
100
|
+
data: queryResponse,
|
|
101
|
+
...(sequenceOptions?.requestCondition && { requestCondition: sequenceOptions.requestCondition })
|
|
95
102
|
}),
|
|
96
103
|
}).then((res) => res.json()) as ${registerOperationResponseType};
|
|
97
104
|
}`;
|
|
@@ -117,7 +124,8 @@ ${exportsFunctions
|
|
|
117
124
|
}`;
|
|
118
125
|
};
|
|
119
126
|
const generateRegisterMutationMethod = (name: string, fakeEndpointVariableName: string) => {
|
|
120
|
-
|
|
127
|
+
const variablesType = `${convertName(name, config)}MutationVariables`;
|
|
128
|
+
return `async register${name}MutationResponse(sequenceId:string, mutationResponse: ${name}Mutation, sequenceOptions?: FakeClientRegisterSequenceOptions<${variablesType}>): Promise<${registerOperationResponseType}> {
|
|
121
129
|
return await fetch(${fakeEndpointVariableName}, {
|
|
122
130
|
method: 'POST',
|
|
123
131
|
headers: {
|
|
@@ -127,7 +135,8 @@ ${exportsFunctions
|
|
|
127
135
|
body: JSON.stringify({
|
|
128
136
|
type: "operation",
|
|
129
137
|
operationName: "${name}",
|
|
130
|
-
data: mutationResponse
|
|
138
|
+
data: mutationResponse,
|
|
139
|
+
...(sequenceOptions?.requestCondition && { requestCondition: sequenceOptions.requestCondition })
|
|
131
140
|
}),
|
|
132
141
|
}).then((res) => res.json()) as ${registerOperationResponseType};
|
|
133
142
|
}`;
|
|
@@ -253,9 +262,10 @@ ${exportsFunctions
|
|
|
253
262
|
};
|
|
254
263
|
|
|
255
264
|
const importQueryIdentifierName = (documentName: string) => {
|
|
256
|
-
return `import type { ${convertName(
|
|
257
|
-
|
|
258
|
-
|
|
265
|
+
return `import type { ${convertName(
|
|
266
|
+
documentName,
|
|
267
|
+
config,
|
|
268
|
+
)}Query, ${convertName(documentName, config)}QueryVariables } from '${config.typesFile}';`;
|
|
259
269
|
};
|
|
260
270
|
const importMutationIdentifierName = (documentName: string) => {
|
|
261
271
|
return `import type { ${convertName(documentName, config)}Mutation, ${convertName(
|
|
@@ -287,6 +297,7 @@ ${documents
|
|
|
287
297
|
});
|
|
288
298
|
})
|
|
289
299
|
.join("\n")}
|
|
300
|
+
${conditionRuleTypes}
|
|
290
301
|
${generateFakeClient(
|
|
291
302
|
documents.flatMap((document) => {
|
|
292
303
|
const flatMap =
|