Skip to content

Commit 0b1aaec

Browse files
committed
chore(package): initial package
0 parents  commit 0b1aaec

File tree

12 files changed

+5451
-0
lines changed

12 files changed

+5451
-0
lines changed

.eslintrc.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
extends: ['landr'],
3+
plugins: ['jest'],
4+
rules: {
5+
'@typescript-eslint/explicit-function-return-type': ['off'],
6+
},
7+
};

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.idea
2+
dist

.prettierrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('prettier-config-landr');

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019-present LANDR Audio Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# graphql-codegen-typescript-mock-data
2+
3+
## Description
4+
5+
[GraphQL Codegen Plugin](https://github.com/dotansimha/graphql-code-generator) for building mock data based on the schema.
6+
7+
## Installation
8+
9+
`yarn add -D graphql-codegen-typescript-mock-data`
10+
11+
## Configuration
12+
13+
### typesFile (`string`, defaultValue: `null`)
14+
15+
Defines the file path containing all GraphQL types. This file can also be generated through graphql-codgen
16+
17+
## Example of usage
18+
19+
**codegen.yml**
20+
21+
```yaml
22+
overwrite: true
23+
schema: schema.graphql
24+
generates:
25+
src/generated-types.ts:
26+
plugins:
27+
- 'typescript'
28+
src/mocks/generated-mocks.ts:
29+
plugins:
30+
- 'graphql-codegen-typescript-mock-data':
31+
typesFile: '../generated-types.ts'
32+
```

jest.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
roots: ['<rootDir>/tests'],
3+
transform: {
4+
'^.+\\.tsx?$': 'ts-jest',
5+
},
6+
};

package.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"name": "graphql-codegen-typescript-mock-data",
3+
"version": "0.0.1",
4+
"description": "GraphQL Codegen plugin for building mock data",
5+
"main": "dist/commonjs/index.js",
6+
"module": "dist/esnext/index.js",
7+
"typings": "dist/esnext/index.d.ts",
8+
"repository": "http://github.com/LandrAudio/graphql-codegen-typescript-mocks",
9+
"author": {
10+
"name": "Corentin Ardeois",
11+
"email": "[email protected]",
12+
"url": "https://github.com/ardeois"
13+
},
14+
"license": "MIT",
15+
"keywords": [
16+
"graphql",
17+
"codegen",
18+
"graphql-codegen",
19+
"plugin",
20+
"typescript",
21+
"mocks",
22+
"fakes"
23+
],
24+
"dependencies": {
25+
"@graphql-codegen/plugin-helpers": "^1.8.1",
26+
"faker": "Marak/faker.js",
27+
"graphql": "^14.5.8"
28+
},
29+
"devDependencies": {
30+
"@graphql-codegen/testing": "^1.8.2",
31+
"@types/faker": "^4.1.6",
32+
"@types/jest": "^24.0.19",
33+
"@typescript-eslint/eslint-plugin": "^1.4.2",
34+
"@typescript-eslint/parser": "^1.4.2",
35+
"eslint": "6.0.0",
36+
"eslint-config-landr": "0.1.0",
37+
"eslint-config-prettier": "^4.1.0",
38+
"eslint-plugin-import": "^2.17.2",
39+
"eslint-plugin-jest": "^22.20.0",
40+
"eslint-plugin-prettier": "^3.0.1",
41+
"graphql-toolkit": "^0.5.19-beta.0",
42+
"jest": "^24.9.0",
43+
"prettier": "^1.18.2",
44+
"prettier-config-landr": "^0.0.6",
45+
"ts-jest": "^24.1.0",
46+
"typescript": "^3.3.3333"
47+
},
48+
"sideEffects": false,
49+
"scripts": {
50+
"build": "tsc -m esnext --outDir dist/esnext && tsc -m commonjs --outDir dist/commonjs",
51+
"test": "jest"
52+
},
53+
"files": [
54+
"dist/**/*",
55+
"LICENCE",
56+
"README.md"
57+
]
58+
}

src/index.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { printSchema, parse, visit, ASTKindToNode, NamedTypeNode, TypeNode, VisitFn } from 'graphql';
2+
import faker from 'faker';
3+
import { toPascalCase, PluginFunction } from '@graphql-codegen/plugin-helpers';
4+
5+
const mockBuilderHelper = `
6+
function lowerFirst(value: string) {
7+
return value.replace(/^\\w/, c => c.toLowerCase());
8+
}
9+
10+
/**
11+
* Creates a new mock object for use on mock providers, or unit tests.
12+
* @param {T} obj Initial object from which mock will be created
13+
* @returns {T} Object with auto-generated methods \`withPropertyName\`
14+
* for each property \`propertyName\` of the initial object.
15+
* @note Will failed to create accessors for optional TypeScript properties
16+
* like \`propertyName?: boolean\` that will result in no property created in JavaScript.
17+
*/
18+
export function mockBuilder<T extends Object>(obj: T): T & any {
19+
let proxy = new Proxy(obj, {
20+
get: (target: T, key: keyof T) => {
21+
if (typeof key === 'string' && key.startsWith('with') && !(key in target)) {
22+
// It's a builder property \`withXxxx\` we dynamically create the setter function
23+
let property = key.substring(4);
24+
property = property in target ? property : lowerFirst(property);
25+
if (property in target) {
26+
return function(value: any) {
27+
// Property is a simple value
28+
// @ts-ignore
29+
target[property] = value;
30+
return proxy;
31+
};
32+
} else {
33+
throw \`Property '\${property}' doesn't exist for object \${target.constructor.name ||
34+
typeof target}\`;
35+
}
36+
}
37+
return target[key];
38+
}
39+
});
40+
41+
return proxy;
42+
}`;
43+
44+
const toMockName = (name: string) => {
45+
const isVowel = name.match(/^[AEIO]/);
46+
return isVowel ? `an${name}` : `a${name}`;
47+
};
48+
49+
const capitalize = (s: unknown) => {
50+
if (typeof s !== 'string') {
51+
return '';
52+
}
53+
return s.charAt(0).toUpperCase() + s.slice(1);
54+
};
55+
56+
const hashedString = (value: string) => {
57+
let hash = 0;
58+
if (value.length === 0) {
59+
return hash;
60+
}
61+
for (let i = 0; i < value.length; i++) {
62+
let char = value.charCodeAt(i);
63+
// eslint-disable-next-line no-bitwise
64+
hash = (hash << 5) - hash + char;
65+
// eslint-disable-next-line no-bitwise
66+
hash = hash & hash; // Convert to 32bit integer
67+
}
68+
return hash;
69+
};
70+
71+
const getNamedType = (
72+
typeName: string,
73+
fieldName: string,
74+
types: TypeItem[],
75+
namedType?: NamedTypeNode,
76+
): string | number | boolean => {
77+
if (!namedType) {
78+
return '';
79+
}
80+
81+
faker.seed(hashedString(typeName + fieldName));
82+
const name = namedType.name.value;
83+
switch (name) {
84+
case 'String':
85+
return `'${faker.lorem.word()}'`;
86+
case 'Float':
87+
return faker.random.number({ min: 0, max: 10, precision: 0.01 });
88+
case 'ID':
89+
return `'${faker.random.uuid()}'`;
90+
case 'Boolean':
91+
return faker.random.boolean();
92+
case 'Int':
93+
return faker.random.number({ min: 0, max: 9999 });
94+
case 'Date':
95+
return `'${faker.date.past().toISOString()}'`;
96+
default:
97+
const foundType = types.find(enumType => enumType.name === name);
98+
if (foundType) {
99+
switch (foundType.type) {
100+
case 'enum':
101+
// It's an enum
102+
const value = foundType.values ? foundType.values[0] : '';
103+
return `${foundType.name}.${toPascalCase(value)}`;
104+
case 'union':
105+
// Return the first union type node.
106+
return getNamedType(typeName, fieldName, types, foundType.types && foundType.types[0]);
107+
default:
108+
throw `foundType is unknown: ${foundType.name}: ${foundType.type}`;
109+
}
110+
}
111+
return `${toMockName(name)}()`;
112+
}
113+
};
114+
115+
const generateMockValue = (
116+
typeName: string,
117+
fieldName: string,
118+
types: TypeItem[],
119+
currentType: TypeNode,
120+
): string | number | boolean => {
121+
switch (currentType.kind) {
122+
case 'NamedType':
123+
return getNamedType(typeName, fieldName, types, currentType as NamedTypeNode);
124+
case 'NonNullType':
125+
return generateMockValue(typeName, fieldName, types, currentType.type);
126+
case 'ListType':
127+
const value = generateMockValue(typeName, fieldName, types, currentType.type);
128+
return `[${value}]`;
129+
}
130+
};
131+
132+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
133+
export interface TypescriptMocksPluginConfig {
134+
typesFile: string;
135+
}
136+
137+
interface TypeItem {
138+
name: string;
139+
type: string;
140+
values?: string[];
141+
types?: ReadonlyArray<NamedTypeNode>;
142+
}
143+
144+
type VisitorType = { [K in keyof ASTKindToNode]?: VisitFn<ASTKindToNode[keyof ASTKindToNode], ASTKindToNode[K]> };
145+
146+
// This plugin was generated with the help of ast explorer.
147+
// https://astexplorer.net
148+
// Paste your graphql schema in it, and you'll be able to see what the `astNode` will look like
149+
export const plugin: PluginFunction<TypescriptMocksPluginConfig> = (schema, documents, config) => {
150+
const printedSchema = printSchema(schema); // Returns a string representation of the schema
151+
const astNode = parse(printedSchema); // Transforms the string into ASTNode
152+
// List of types that are enums
153+
const types: TypeItem[] = [];
154+
const visitor: VisitorType = {
155+
EnumTypeDefinition: node => {
156+
const name = node.name.value;
157+
if (!types.find((enumType: TypeItem) => enumType.name === name)) {
158+
types.push({
159+
name,
160+
type: 'enum',
161+
values: node.values ? node.values.map(node => node.name.value) : [],
162+
});
163+
}
164+
},
165+
UnionTypeDefinition: node => {
166+
const name = node.name.value;
167+
if (!types.find(enumType => enumType.name === name)) {
168+
types.push({
169+
name,
170+
type: 'union',
171+
types: node.types,
172+
});
173+
}
174+
},
175+
FieldDefinition: node => {
176+
const fieldName = node.name.value;
177+
178+
return {
179+
name: fieldName,
180+
mockFn: (typeName: string) => {
181+
const value = generateMockValue(typeName, fieldName, types, node.type);
182+
183+
return ` ${fieldName}: ${value},`;
184+
},
185+
};
186+
},
187+
ObjectTypeDefinition: node => {
188+
// This function triggered per each type
189+
const typeName = node.name.value;
190+
191+
if (typeName === 'Query') {
192+
return null;
193+
}
194+
195+
const { fields } = node;
196+
return {
197+
typeName,
198+
mockFn: () => {
199+
const mockFields = fields ? fields.map(({ mockFn }: any) => mockFn(typeName)).join('\n') : '';
200+
const fieldNames = fields ? fields.map(({ name }) => name) : [];
201+
202+
const mockInterface = `interface ${typeName}Mock extends ${typeName} {
203+
${fieldNames
204+
.map(name => `with${capitalize(name)}: (value: ${typeName}['${name}']) => ${typeName}Mock;`)
205+
.join('\n ')}
206+
}`;
207+
208+
return `
209+
${mockInterface}
210+
211+
export const ${toMockName(typeName)} = (): ${typeName}Mock => {
212+
return mockBuilder<${typeName}>({
213+
${mockFields}
214+
});
215+
};`;
216+
},
217+
};
218+
},
219+
};
220+
221+
const result: any = visit(astNode, { leave: visitor });
222+
const definitions = result.definitions.filter((definition: any) => !!definition);
223+
const typesFile = config.typesFile.replace(/\.[\w]+$/, '');
224+
const typeImports = definitions
225+
.map(({ typeName }: { typeName: string }) => typeName)
226+
.filter((typeName: string) => !!typeName);
227+
typeImports.push(...types.map(({ name }) => name));
228+
// List of function that will generate the mock.
229+
// We generate it after having visited because we need to distinct types from enums
230+
const mockFns = definitions.map(({ mockFn }: any) => mockFn).filter((mockFn: Function) => !!mockFn);
231+
const typesFileImport = typesFile
232+
? `/* eslint-disable @typescript-eslint/no-use-before-define,@typescript-eslint/no-unused-vars */
233+
import { ${typeImports.join(', ')} } from '${typesFile}';\n`
234+
: '';
235+
236+
return `${typesFileImport}
237+
${mockBuilderHelper}
238+
${mockFns.map((mockFn: Function) => mockFn()).join('\n')}
239+
`;
240+
};

0 commit comments

Comments
 (0)