@newmo/graphql-fake-core 0.3.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.
@@ -0,0 +1,61 @@
1
+ export const EXAMPLE_DIRECTIVE = `
2
+ """
3
+ @exampleID directive specifies an example value for a ID field.
4
+ This example value is used in the fake data.
5
+ ID value will be unique between all ID fake data.
6
+ """
7
+ directive @exampleID(
8
+ """
9
+ The value of the ID field.
10
+ @exampleID(value: "id")
11
+ """
12
+ value: ID!
13
+ ) on FIELD_DEFINITION
14
+ """
15
+ @exampleString directive specifies an example value for a String field.
16
+ This example value is used in the fake data.
17
+ """
18
+ directive @exampleString(
19
+ """
20
+ The value of the String field.
21
+ @exampleString(value: "example")
22
+ """
23
+ value: String!
24
+ ) on FIELD_DEFINITION
25
+ """
26
+ @exampleInt directive specifies an example value for a Inf field.
27
+ This example value is used in the fake data.
28
+ """
29
+ directive @exampleInt(
30
+ """
31
+ The value of the Int field.
32
+ @exampleInt(value: 1)
33
+ """
34
+ value: Int!
35
+ ) on FIELD_DEFINITION
36
+ """
37
+ @exampleFloat directive specifies an example value for a Float field.
38
+ This example value is used in the fake data.
39
+ """
40
+ directive @exampleFloat(
41
+ """
42
+ The value of the Float field.
43
+ @exampleFloat(value: 1.0)
44
+ """
45
+ value: Float!
46
+ ) on FIELD_DEFINITION
47
+ """
48
+ @exampleBoolean directive specifies an example value for a Boolean field.
49
+ This example value is used in the fake data.
50
+ """
51
+ directive @exampleBoolean(
52
+ """
53
+ The value of the Boolean field.
54
+ @exampleBoolean(value: true)
55
+ """
56
+ value: Boolean!
57
+ ) on FIELD_DEFINITION
58
+ `;
59
+ export const extendSchema = (schema: string) => {
60
+ return EXAMPLE_DIRECTIVE + schema;
61
+ };
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { generateCode } from "./code-generator.js";
2
+ export type { ConfigWithOutput } from "./code-generator.js";
3
+ export { normalizeConfig, validateConfig } from "./config.js";
4
+ export type { Config, RawConfig } from "./config.js";
5
+ export { extendSchema, EXAMPLE_DIRECTIVE } from "./extend-schema.js";
6
+ export { getTypeInfos } from "./schema-scanner.js";
7
+ export type {
8
+ TypeInfo,
9
+ AbstractTypeInfo,
10
+ ObjectTypeInfo,
11
+ ExampleDirective,
12
+ ExampleDirectiveValue,
13
+ ExampleDirectionExpression,
14
+ } from "./schema-scanner.js";
@@ -0,0 +1,454 @@
1
+ import { convertFactory, transformComment } from "@graphql-codegen/visitor-plugin-common";
2
+ import {
3
+ type ASTNode,
4
+ type ConstValueNode,
5
+ type FieldDefinitionNode,
6
+ type GraphQLSchema,
7
+ type InputObjectTypeDefinitionNode,
8
+ type InputValueDefinitionNode,
9
+ type InterfaceTypeDefinitionNode,
10
+ Kind,
11
+ type ListTypeNode,
12
+ type NamedTypeNode,
13
+ type NonNullTypeNode,
14
+ type ObjectTypeDefinitionNode,
15
+ type TypeNode,
16
+ type UnionTypeDefinitionNode,
17
+ } from "graphql";
18
+ import { generateCreateReferenceCode } from "./code-generator.js";
19
+ import type { Config } from "./config.js";
20
+
21
+ function convertName(node: ASTNode | string, config: Config): string {
22
+ const convert = config.namingConvention
23
+ ? convertFactory({ namingConvention: config.namingConvention })
24
+ : convertFactory({});
25
+ let convertedName = "";
26
+ convertedName += config.typesPrefix;
27
+ convertedName += convert(node);
28
+ convertedName += config.typesSuffix;
29
+ return convertedName;
30
+ }
31
+
32
+ const createIDFactory = () => {
33
+ return (name: string, key: string) => {
34
+ return `__id({ name: "${name}", key:"${key}", depth })`;
35
+ };
36
+ };
37
+
38
+ const parseTypeNodeStructure = (node: TypeNode): string => {
39
+ if (node.kind === Kind.NON_NULL_TYPE) {
40
+ return parseTypeNodeStructure(node.type);
41
+ }
42
+ if (node.kind === Kind.LIST_TYPE) {
43
+ return "array";
44
+ }
45
+ // string, number, boolean, null
46
+ if (node.name.value === "String") {
47
+ return "string";
48
+ }
49
+ if (node.name.value === "Int") {
50
+ return "number";
51
+ }
52
+ if (node.name.value === "Float") {
53
+ return "number";
54
+ }
55
+ if (node.name.value === "Boolean") {
56
+ return "boolean";
57
+ }
58
+ if (node.name.value === "ID") {
59
+ return "string";
60
+ }
61
+ return "object";
62
+ };
63
+ type ValuePrimitive = string | number | boolean | null;
64
+ type ValueArray = ValuePrimitive[];
65
+ type ValueObject = Record<string, ValuePrimitive | ValueArray>;
66
+ export type ExampleDirectiveValue = {
67
+ // value is serialized value
68
+ value: ValuePrimitive | ValueArray | ValueObject;
69
+ };
70
+ export type ExampleDirectionExpression = {
71
+ expression: string;
72
+ };
73
+ export type ExampleDirective = ExampleDirectiveValue | ExampleDirectionExpression;
74
+
75
+ function valueOfNode(value: ConstValueNode): ValuePrimitive | ValueArray | ValueObject {
76
+ // object
77
+ if (value.kind === Kind.OBJECT) {
78
+ return value.fields.reduce((acc, field) => {
79
+ // @ts-expect-error TODO: nesting type
80
+ acc[field.name.value] = valueOfNode(field.value);
81
+ return acc;
82
+ }, {} as ValueObject);
83
+ }
84
+ // list
85
+ if (value.kind === Kind.LIST) {
86
+ return value.values.map((v) => {
87
+ return valueOfNode(v);
88
+ }) as ValueArray;
89
+ }
90
+ // null
91
+ if (value.kind === Kind.NULL) {
92
+ return null;
93
+ }
94
+ // string
95
+ if (value.kind === Kind.STRING) {
96
+ return value.value;
97
+ }
98
+ // enum
99
+ if (value.kind === Kind.ENUM) {
100
+ return value.value;
101
+ }
102
+ // int
103
+ if (value.kind === Kind.INT) {
104
+ return Number.parseInt(value.value, 10);
105
+ }
106
+ // float
107
+ if (value.kind === Kind.FLOAT) {
108
+ return Number.parseFloat(value.value);
109
+ }
110
+ // boolean
111
+ if (value.kind === Kind.BOOLEAN) {
112
+ return value.value;
113
+ }
114
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
115
+ throw new Error(`Unknown kind of value ${value satisfies never}`);
116
+ }
117
+
118
+ const typeToFunction = ({
119
+ convertedTypeName,
120
+ fieldName,
121
+ type,
122
+ config,
123
+ idFactory,
124
+ }: {
125
+ convertedTypeName: string;
126
+ fieldName: string;
127
+ type: string;
128
+ config: Config;
129
+ idFactory: ReturnType<typeof createIDFactory>;
130
+ }): string => {
131
+ switch (type) {
132
+ case "String":
133
+ return `"${config.defaultValues.String}"`;
134
+ case "Int":
135
+ return `${config.defaultValues.Int}`;
136
+ case "Float":
137
+ return `${config.defaultValues.Float}`;
138
+ case "Boolean":
139
+ return `${config.defaultValues.Boolean ? "true" : "false"}`;
140
+ case "ID": {
141
+ const pathOfField = `${convertedTypeName}.${fieldName}`;
142
+ return `${idFactory(config.defaultValues.ID, pathOfField)}`;
143
+ }
144
+ default:
145
+ // reference to the object
146
+ return `${generateCreateReferenceCode({ fieldName, typeName: type, config: config })}`;
147
+ }
148
+ };
149
+ const typeToFunctionWithArray = ({
150
+ convertedTypeName,
151
+ fieldName,
152
+ type,
153
+ config,
154
+ idFactory,
155
+ }: {
156
+ convertedTypeName: string;
157
+ fieldName: string;
158
+ type: string;
159
+ config: Config;
160
+ idFactory: ReturnType<typeof createIDFactory>;
161
+ }): string => {
162
+ // Avoid [null, null, null]
163
+ // Mock server can't handle null values in the array
164
+ return `(depth < ${config.maxFieldRecursionDepth}) ? Array.from({ length: ${
165
+ config.defaultValues.listLength
166
+ } }).map(() => ${typeToFunction({
167
+ convertedTypeName,
168
+ fieldName: fieldName,
169
+ type: type,
170
+ config: config,
171
+ idFactory: idFactory,
172
+ })}) : []`;
173
+ };
174
+ // NamedType/ListType handling
175
+ const nodeToExpression = ({
176
+ convertedTypeName,
177
+ fieldName,
178
+ currentNode,
179
+ isArray = false,
180
+ config,
181
+ idFactory,
182
+ }: {
183
+ convertedTypeName: string;
184
+ fieldName: string;
185
+ currentNode: NonNullTypeNode | NamedTypeNode | ListTypeNode;
186
+ config: Config;
187
+ isArray?: boolean;
188
+ idFactory: ReturnType<typeof createIDFactory>;
189
+ }): ExampleDirectionExpression => {
190
+ if (currentNode.kind === "NonNullType") {
191
+ return nodeToExpression({
192
+ convertedTypeName,
193
+ fieldName,
194
+ currentNode: currentNode.type,
195
+ isArray,
196
+ config,
197
+ idFactory,
198
+ });
199
+ }
200
+ if (currentNode.kind === "NamedType") {
201
+ if (isArray) {
202
+ return {
203
+ expression: typeToFunctionWithArray({
204
+ convertedTypeName,
205
+ fieldName: fieldName,
206
+ type: currentNode.name.value,
207
+ config: config,
208
+ idFactory: idFactory,
209
+ }),
210
+ };
211
+ }
212
+ return {
213
+ expression: typeToFunction({
214
+ convertedTypeName,
215
+ fieldName,
216
+ type: currentNode.name.value,
217
+ config: config,
218
+ idFactory: idFactory,
219
+ }),
220
+ };
221
+ }
222
+ if (currentNode.kind === "ListType") {
223
+ return nodeToExpression({
224
+ convertedTypeName,
225
+ fieldName,
226
+ currentNode: currentNode.type,
227
+ isArray: true,
228
+ config,
229
+ idFactory,
230
+ });
231
+ }
232
+ throw new Error("Unknown node kind");
233
+ };
234
+
235
+ const SUPPORTED_EXAMPLE_DIRECTIVES = [
236
+ "exampleID",
237
+ "exampleString",
238
+ "exampleInt",
239
+ "exampleFloat",
240
+ "exampleBoolean",
241
+ ];
242
+ const isIdType = (node: NonNullTypeNode | NamedTypeNode | ListTypeNode): boolean => {
243
+ if (node.kind === "NonNullType") {
244
+ return isIdType(node.type);
245
+ }
246
+ if (node.kind === "NamedType") {
247
+ return node.name.value === "ID";
248
+ }
249
+ if (node.kind === "ListType") {
250
+ return false;
251
+ }
252
+ return false;
253
+ };
254
+
255
+ function parseFieldOrInputValueDefinition({
256
+ node,
257
+ convertedTypeName,
258
+ config,
259
+ idFactory,
260
+ }: {
261
+ node: FieldDefinitionNode | InputValueDefinitionNode;
262
+ convertedTypeName: string;
263
+ config: Config;
264
+ idFactory: ReturnType<typeof createIDFactory>;
265
+ }): { comment?: string | undefined; example?: ExampleDirective | undefined } {
266
+ const fieldName = node.name.value;
267
+ const comment = node.description ? transformComment(node.description) : undefined;
268
+ const exampleDirective = node.directives?.find((d) => {
269
+ return SUPPORTED_EXAMPLE_DIRECTIVES.includes(d.name.value);
270
+ });
271
+ // @example* directive is not found, return random value for the scalar type
272
+ if (!exampleDirective) {
273
+ return {
274
+ comment,
275
+ example: nodeToExpression({
276
+ convertedTypeName,
277
+ fieldName,
278
+ currentNode: node.type,
279
+ config,
280
+ idFactory,
281
+ }),
282
+ };
283
+ }
284
+ if (!exampleDirective.arguments) {
285
+ throw new Error(
286
+ `@${exampleDirective.name.value} directive must have arguments. @${exampleDirective.name.value}(value: ...)`,
287
+ );
288
+ }
289
+ /**
290
+ * @exampleID(value: "id")
291
+ * -> { value: "id1" }
292
+ * @exampleString(value: "value")
293
+ * -> { value: "value" }
294
+ * @exampleInt(value: 1)
295
+ * -> { value: 1 }
296
+ * @exampleFloat(value: 1.1)
297
+ * -> { value: 1.1 }
298
+ * @exampleBoolean(value: true)
299
+ * -> { value: true }
300
+ */
301
+ const value = exampleDirective.arguments.find((a) => a.name.value === "value");
302
+ if (!value) {
303
+ throw new Error(
304
+ `@${exampleDirective.name.value} directive must have value argument. @${exampleDirective.name.value}(value: ...)`,
305
+ );
306
+ }
307
+ const rawValue = valueOfNode(value.value);
308
+ // if node type is not equal to the value type, throw an error
309
+ const nodeType = parseTypeNodeStructure(node.type);
310
+ // array, object, string, number, boolean, null
311
+ const rawValueType = Object.prototype.toString.call(rawValue).slice(8, -1).toLowerCase();
312
+ if (nodeType !== rawValueType) {
313
+ throw new Error(
314
+ `${convertedTypeName}.${fieldName}: @${exampleDirective.name.value} directive value type must be ${nodeType}. Got ${rawValueType}`,
315
+ );
316
+ }
317
+ // if ID type, add idFactory() to the value
318
+ // e.g. @exampleID(value: "id") -> { expression: __id("id") }
319
+ const isExampleIdDirective = exampleDirective.name.value === "exampleID";
320
+ if (isExampleIdDirective && typeof rawValue === "string") {
321
+ const pathOfField = `${convertedTypeName}.${fieldName}.${rawValue}`;
322
+ return { comment, example: { expression: idFactory(rawValue, pathOfField) } };
323
+ }
324
+ return { comment, example: { value: rawValue } };
325
+ }
326
+
327
+ function parseObjectTypeOrInputObjectTypeDefinition({
328
+ node,
329
+ config,
330
+ idFactory,
331
+ }: {
332
+ node: ObjectTypeDefinitionNode | InputObjectTypeDefinitionNode;
333
+ config: Config;
334
+ idFactory: ReturnType<typeof createIDFactory>;
335
+ }): ObjectTypeInfo {
336
+ const originalTypeName = node.name.value;
337
+ const convertedTypeName = convertName(originalTypeName, config);
338
+ return {
339
+ type: "object",
340
+ name: originalTypeName,
341
+ fields: [
342
+ ...(node.fields ?? []).map((field) => ({
343
+ name: field.name.value,
344
+ ...parseFieldOrInputValueDefinition({
345
+ node: field,
346
+ convertedTypeName,
347
+ config,
348
+ idFactory,
349
+ }),
350
+ })),
351
+ ],
352
+ };
353
+ }
354
+
355
+ type FieldInfo = {
356
+ name: string;
357
+ example?: ExampleDirective | undefined;
358
+ };
359
+ export type ObjectTypeInfo = {
360
+ type: "object";
361
+ name: string;
362
+ fields: FieldInfo[];
363
+ };
364
+ export type AbstractTypeInfo = {
365
+ type: "abstract";
366
+ name: string;
367
+ possibleTypes: string[];
368
+ comment?: string | undefined;
369
+ example?: ExampleDirective | undefined;
370
+ };
371
+ export type TypeInfo = ObjectTypeInfo | AbstractTypeInfo;
372
+
373
+ export function getTypeInfos(config: Config, schema: GraphQLSchema): TypeInfo[] {
374
+ const types = Object.values(schema.getTypeMap());
375
+
376
+ const idFactory = createIDFactory();
377
+ const userDefinedTypeDefinitions = types
378
+ .map((type) => type.astNode)
379
+ .filter(
380
+ (
381
+ node,
382
+ ): node is
383
+ | ObjectTypeDefinitionNode
384
+ | InputObjectTypeDefinitionNode
385
+ | InterfaceTypeDefinitionNode
386
+ | UnionTypeDefinitionNode => {
387
+ if (!node) return false;
388
+ return (
389
+ node.kind === Kind.OBJECT_TYPE_DEFINITION ||
390
+ node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION ||
391
+ node.kind === Kind.INTERFACE_TYPE_DEFINITION ||
392
+ node.kind === Kind.UNION_TYPE_DEFINITION
393
+ );
394
+ },
395
+ );
396
+ const objectTypeDefinitions = userDefinedTypeDefinitions.filter(
397
+ (node): node is ObjectTypeDefinitionNode => {
398
+ if (!node) return false;
399
+ return node.kind === Kind.OBJECT_TYPE_DEFINITION;
400
+ },
401
+ );
402
+
403
+ return types
404
+ .map((type) => type.astNode)
405
+ .filter(
406
+ (
407
+ node,
408
+ ): node is
409
+ | ObjectTypeDefinitionNode
410
+ | InputObjectTypeDefinitionNode
411
+ | InterfaceTypeDefinitionNode
412
+ | UnionTypeDefinitionNode => {
413
+ if (!node) return false;
414
+ return (
415
+ node.kind === Kind.OBJECT_TYPE_DEFINITION ||
416
+ node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION ||
417
+ node.kind === Kind.INTERFACE_TYPE_DEFINITION ||
418
+ node.kind === Kind.UNION_TYPE_DEFINITION
419
+ );
420
+ },
421
+ )
422
+ .map((node) => {
423
+ if (
424
+ node?.kind === Kind.OBJECT_TYPE_DEFINITION ||
425
+ node?.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION
426
+ ) {
427
+ return parseObjectTypeOrInputObjectTypeDefinition({ node, config, idFactory });
428
+ }
429
+ if (node?.kind === Kind.INTERFACE_TYPE_DEFINITION) {
430
+ return {
431
+ type: "abstract",
432
+ name: convertName(node.name.value, config),
433
+ possibleTypes: objectTypeDefinitions
434
+ .filter((objectTypeDefinitionNode) =>
435
+ (objectTypeDefinitionNode.interfaces ?? []).some(
436
+ (i) => i.name.value === node.name.value,
437
+ ),
438
+ )
439
+ .map((objectTypeDefinitionNode) =>
440
+ convertName(objectTypeDefinitionNode.name.value, config),
441
+ ),
442
+ comment: node.description ? transformComment(node.description) : undefined,
443
+ };
444
+ }
445
+ return {
446
+ type: "abstract",
447
+ name: convertName(node.name.value, config),
448
+ possibleTypes: (node.types ?? []).map((type) =>
449
+ convertName(type.name.value, config),
450
+ ),
451
+ comment: node.description ? transformComment(node.description) : undefined,
452
+ };
453
+ });
454
+ }