├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── src ├── __tests__ │ ├── __snapshots__ │ │ └── app.spec.ts.snap │ └── app.spec.ts ├── app.ts ├── config.ts ├── debugConsole.ts ├── index.ts └── schema.graphql ├── test └── setupTestFramework.js ├── webpack.config.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/graphql-mock-api/9cf47627db776b91a02327a148258e255b480cb5/.env.example -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | .github 3 | build 4 | data 5 | digital_assets 6 | flow-typed 7 | hard-source-cache 8 | public 9 | __generated__ 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | parser: '@typescript-eslint/parser', 5 | env: { 6 | browser: true, 7 | node: true, 8 | jest: true, 9 | es6: true, 10 | 'cypress/globals': true, 11 | serviceworker: true, 12 | }, 13 | plugins: ['react', 'flowtype', 'import', 'cypress', 'relay', '@typescript-eslint', 'react-hooks'], 14 | parserOptions: { 15 | ecmaVersion: 10, 16 | sourceType: 'module', 17 | ecmaFeatures: { 18 | modules: true, 19 | }, 20 | }, 21 | extends: [ 22 | 'eslint:recommended', 23 | 'plugin:react/recommended', 24 | 'plugin:import/errors', 25 | 'plugin:relay/recommended', 26 | 'plugin:@typescript-eslint/eslint-recommended', 27 | 'plugin:@typescript-eslint/recommended', 28 | 'prettier/@typescript-eslint', 29 | ], 30 | rules: { 31 | 'comma-dangle': [2, 'always-multiline'], 32 | quotes: [2, 'single', { allowTemplateLiterals: true, avoidEscape: true }], 33 | 'jsx-quotes': [2, 'prefer-single'], 34 | 'react/prop-types': 0, 35 | 'no-case-declarations': 0, 36 | 'react/jsx-no-bind': 0, 37 | 'react/display-name': 0, 38 | 'new-cap': 0, 39 | 'no-unexpected-multiline': 0, 40 | 'no-class-assign': 1, 41 | 'no-console': 2, 42 | 'object-curly-spacing': [1, 'always'], 43 | 'import/first': 2, 44 | 'import/default': 0, 45 | 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 46 | 'no-extra-boolean-cast': 0, 47 | 'import/named': 0, 48 | 'import/namespace': [2, { allowComputed: true }], 49 | 'import/no-duplicates': 2, 50 | 'import/order': [2, { 'newlines-between': 'always-and-inside-groups' }], 51 | 'react/no-children-prop': 1, 52 | 'react/no-deprecated': 1, 53 | 'import/no-cycle': 1, 54 | 'import/no-self-import': 1, 55 | 'relay/graphql-syntax': 'error', 56 | 'relay/compat-uses-vars': 'warn', 57 | 'relay/graphql-naming': 'error', 58 | 'relay/generated-flow-types': 'warn', 59 | 'relay/no-future-added-value': 'warn', 60 | 'relay/unused-fields': 0, 61 | indent: 0, 62 | '@typescript-eslint/indent': 0, 63 | '@typescript-eslint/camelcase': 0, 64 | '@typescript-eslint/explicit-function-return-type': 0, 65 | 'interface-over-type-literal': 0, 66 | '@typescript-eslint/consistent-type-definitions': 0, 67 | '@typescript-eslint/prefer-interface': 0, 68 | 'lines-between-class-members': 0, 69 | '@typescript-eslint/explicit-member-accessibility': 0, 70 | '@typescript-eslint/no-non-null-assertion': 0, 71 | '@typescript-eslint/no-unused-vars': [ 72 | 'error', 73 | { 74 | ignoreRestSiblings: true, 75 | }, 76 | ], 77 | '@typescript-eslint/no-var-requires': 1, 78 | 'react-hooks/rules-of-hooks': 'error', 79 | // disable as it can cause more harm than good 80 | //'react-hooks/exhaustive-deps': 'warn', 81 | '@typescript-eslint/no-empty-function': 1, 82 | '@typescript-eslint/interface-name-prefix': 0, 83 | }, 84 | settings: { 85 | 'import/resolver': { 86 | node: true, 87 | 'eslint-import-resolver-typescript': true, 88 | 'eslint-import-resolver-lerna': { 89 | packages: path.resolve(__dirname, 'packages'), 90 | }, 91 | }, 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | schema.json linguist-generated 2 | *.json merge=json 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea/ 5 | 6 | lib-cov 7 | *.seed 8 | *.log 9 | *.csv 10 | *.dat 11 | *.out 12 | *.pid 13 | *.gz 14 | *.map 15 | 16 | pids 17 | logs 18 | results 19 | test-results 20 | 21 | node_modules 22 | npm-debug.log 23 | 24 | dump.rdb 25 | bundle.js 26 | 27 | build 28 | dist 29 | coverage 30 | .nyc_output 31 | .env 32 | 33 | graphql.*.json 34 | junit.xml 35 | 36 | .vs 37 | 38 | test/globalConfig.json 39 | distTs 40 | 41 | # Random things to ignore 42 | ignore/ 43 | package-lock.json 44 | /yarn-offline-cache 45 | .cache 46 | 47 | **/node_modules 48 | .idea 49 | yarn-error.log 50 | .vs/ 51 | 52 | # IDE/Editor GraphQL Extensions 53 | .gqlconfig 54 | 55 | # Relay modern 56 | __generated__/ 57 | 58 | # Flow log 59 | flow.log 60 | 61 | # Random things to ignore 62 | yarn-offline-cache 63 | 64 | hard-source-cache/ 65 | **/hard-source-cache/ 66 | 67 | # Cypress 68 | cypress/videos 69 | cypress/screenshots 70 | 71 | differencify_reports 72 | .parcel-cache 73 | 74 | .vscode 75 | 76 | .vercel -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Mock API 2 | 3 | Send `schema` and `mocks` inside POST body to mock any GraphQL schema 4 | 5 | ```jsx 6 | const schema = ` 7 | type Query { 8 | user: User 9 | } 10 | 11 | type User { 12 | name: String 13 | } 14 | ` 15 | const mocks = { 16 | User: { 17 | name: 'awesome' 18 | }, 19 | } 20 | ``` 21 | 22 | # How to run 23 | ```shell script 24 | yarn start 25 | ``` -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-typescript', 12 | ], 13 | plugins: [ 14 | '@babel/plugin-proposal-object-rest-spread', 15 | '@babel/plugin-proposal-class-properties', 16 | '@babel/plugin-proposal-export-default-from', 17 | '@babel/plugin-proposal-export-namespace-from', 18 | '@babel/plugin-transform-async-to-generator', 19 | '@babel/plugin-proposal-async-generator-functions', 20 | '@babel/plugin-proposal-nullish-coalescing-operator', 21 | '@babel/plugin-proposal-optional-chaining', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const pack = require('./package'); 2 | 3 | module.exports = { 4 | displayName: pack.name, 5 | name: pack.name, 6 | testPathIgnorePatterns: ['/node_modules/', './dist'], 7 | coverageReporters: ['lcov', 'html'], 8 | setupFilesAfterEnv: ['/test/setupTestFramework.js'], 9 | resetModules: false, 10 | transform: { 11 | '^.+\\.(js|ts|tsx)?$': 'babel-jest', 12 | }, 13 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts|tsx)?$', 14 | moduleFileExtensions: ['ts', 'js', 'tsx', 'json'], 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-mock-api", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@babel/polyfill": "^7.12.1", 6 | "@graphql-tools/mock": "^9.0.2", 7 | "@graphql-tools/schema": "^10.0.3", 8 | "@koa/cors": "^5.0.0", 9 | "dotenv-safe": "^9.1.0", 10 | "graphql": "^16.8.1", 11 | "graphql-tools": "^9.0.1", 12 | "kcors": "^2.2.2", 13 | "koa": "^2.15.0", 14 | "koa-bodyparser": "^4.4.1", 15 | "koa-graphql": "^0.12.0", 16 | "koa-logger": "^3.2.1", 17 | "koa-router": "^12.0.1" 18 | }, 19 | "devDependencies": { 20 | "@babel/cli": "7.23.9", 21 | "@babel/core": "7.24.0", 22 | "@babel/node": "7.23.9", 23 | "@babel/plugin-proposal-async-generator-functions": "7.20.7", 24 | "@babel/plugin-proposal-class-properties": "7.18.6", 25 | "@babel/plugin-proposal-export-default-from": "7.23.3", 26 | "@babel/plugin-proposal-export-namespace-from": "7.18.9", 27 | "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", 28 | "@babel/plugin-proposal-object-rest-spread": "7.20.7", 29 | "@babel/plugin-proposal-optional-chaining": "7.21.0", 30 | "@babel/plugin-transform-async-to-generator": "7.23.3", 31 | "@babel/preset-env": "7.24.0", 32 | "@babel/preset-typescript": "7.23.3", 33 | "@types/babel__core": "7.20.5", 34 | "@types/babel__preset-env": "7.9.6", 35 | "@types/dotenv-safe": "^8.1.6", 36 | "@types/eslint": "8.56.5", 37 | "@types/eslint-plugin-prettier": "^3.1.3", 38 | "@types/kcors": "^2.2.8", 39 | "@types/koa": "^2.15.0", 40 | "@types/koa-bodyparser": "^4.3.12", 41 | "@types/koa-graphql": "^0.8.11", 42 | "@types/koa-logger": "^3.1.5", 43 | "@types/koa-router": "^7.4.8", 44 | "@types/start-server-webpack-plugin": "^2.2.5", 45 | "@types/supertest": "^6.0.2", 46 | "@types/webpack": "5.28.5", 47 | "@types/webpack-bundle-analyzer": "4.7.0", 48 | "@types/webpack-merge": "5.0.0", 49 | "@types/webpack-node-externals": "3.0.4", 50 | "@types/webpack-plugin-serve": "1.4.6", 51 | "@typescript-eslint/eslint-plugin": "7.1.1", 52 | "@typescript-eslint/parser": "7.1.1", 53 | "babel-jest": "^29.7.0", 54 | "babel-loader": "^9.1.3", 55 | "esbuild": "^0.20.1", 56 | "esbuild-register": "^3.5.0", 57 | "eslint": "8.57.0", 58 | "eslint-config-airbnb": "19.0.4", 59 | "eslint-config-okonet": "7.0.2", 60 | "eslint-config-prettier": "^9.1.0", 61 | "eslint-config-shellscape": "6.0.1", 62 | "eslint-import-resolver-lerna": "^2.0.0", 63 | "eslint-import-resolver-typescript": "3.6.1", 64 | "eslint-import-resolver-webpack": "0.13.8", 65 | "eslint-plugin-cypress": "2.15.1", 66 | "eslint-plugin-flowtype": "8.0.3", 67 | "eslint-plugin-import": "2.29.1", 68 | "eslint-plugin-jsx-a11y": "6.8.0", 69 | "eslint-plugin-node": "11.1.0", 70 | "eslint-plugin-prettier": "^5.1.3", 71 | "eslint-plugin-react": "7.34.0", 72 | "eslint-plugin-react-hooks": "4.6.0", 73 | "eslint-plugin-relay": "1.8.3", 74 | "eslint-plugin-typescript": "0.14.0", 75 | "jest": "^29.7.0", 76 | "reload-server-webpack-plugin": "1.0.1", 77 | "start-server-webpack-plugin": "^2.2.5", 78 | "supertest": "^6.3.4", 79 | "typescript": "5.3.3", 80 | "webpack": "5.90.3", 81 | "webpack-bundle-analyzer": "4.10.1", 82 | "webpack-cli": "5.1.4", 83 | "webpack-merge": "5.10.0", 84 | "webpack-node-externals": "3.0.0", 85 | "webpack-plugin-serve": "1.6.0" 86 | }, 87 | "license": "MIT", 88 | "main": "index.js", 89 | "scripts": { 90 | "start": "webpack --watch --progress --config webpack.config.js", 91 | "es": "node --no-experimental-fetch -r esbuild-register" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/app.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should return query from schema and mocks 1`] = ` 4 | Object { 5 | "data": Object { 6 | "user": null, 7 | }, 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/__tests__/app.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import app from '../app'; 3 | 4 | it('should return query from schema and mocks', async () => { 5 | const typeDefs = ` 6 | type Query { 7 | user: User 8 | } 9 | 10 | type User { 11 | name: String 12 | } 13 | `; 14 | 15 | const mocks = { 16 | Query: () => ({ 17 | user: { 18 | name: 'awesome', 19 | }, 20 | }), 21 | User: () => ({ 22 | name: 'Sibelius', 23 | }), 24 | } 25 | 26 | // language=GraphQL 27 | const query = ` 28 | query Q { 29 | user { 30 | name 31 | } 32 | } 33 | `; 34 | 35 | const variables = { 36 | }; 37 | 38 | const payload = { 39 | query, 40 | variables, 41 | schema: typeDefs, 42 | mocks, 43 | }; 44 | 45 | const response = await request(app.callback()) 46 | .post('/graphql') 47 | .set({ 48 | Accept: 'application/json', 49 | 'Content-Type': 'application/json', 50 | }) 51 | .send(JSON.stringify(payload)); 52 | 53 | expect(response.status).toBe(200); 54 | expect(response.body).toMatchSnapshot(); 55 | }); -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import Koa from 'koa'; 2 | import logger from 'koa-logger'; 3 | import Router from 'koa-router'; 4 | import cors from '@koa/cors'; 5 | import { graphqlHTTP } from 'koa-graphql'; 6 | import bodyParser from 'koa-bodyparser'; 7 | import { makeExecutableSchema } from '@graphql-tools/schema'; 8 | import { addMocksToSchema } from '@graphql-tools/mock'; 9 | import fs from 'fs'; 10 | import util from 'util'; 11 | import path from 'path'; 12 | import { debugConsole } from './debugConsole'; 13 | 14 | const readFile = util.promisify(fs.readFile); 15 | 16 | const app = new Koa(); 17 | 18 | const router = new Router(); 19 | 20 | app.use(bodyParser()); 21 | app.on('error', err => { 22 | // eslint-disable-next-line 23 | console.log('app error: ', err); 24 | }); 25 | 26 | app.use(logger()); 27 | app.use(cors( { maxAge: 86400, origin: '*' })); 28 | 29 | app.use(async (ctx, next) => { 30 | await next(); 31 | 32 | debugConsole(ctx.body); 33 | }) 34 | 35 | router.get('/', async ctx => { 36 | ctx.body = 'Welcome to GraphQL Mock server'; 37 | }); 38 | 39 | router.all('/api/graphql', graphqlHTTP(async (request: Request, ctx: Response, koaContext: KoaContext) => { 40 | const { mocks = { 41 | DateTime: () => new Date().toISOString(), 42 | } }= koaContext; 43 | 44 | const typeDefs = await readFile(path.join(__dirname, 'schema.graphql'), 'utf8'); 45 | 46 | const schema = addMocksToSchema({ 47 | schema: makeExecutableSchema({ typeDefs }), 48 | mocks 49 | }); 50 | 51 | return { 52 | graphiql: true, 53 | schema, 54 | } 55 | })); 56 | 57 | app.use(router.routes()).use(router.allowedMethods()); 58 | 59 | export default app; -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import dotenvSafe from 'dotenv-safe'; 4 | 5 | const cwd = process.cwd(); 6 | 7 | const root = path.join.bind(cwd); 8 | 9 | dotenvSafe.config({ 10 | path: root('.env'), 11 | sample: root('.env.example'), 12 | }); 13 | 14 | export const config = { 15 | GRAPHQL_PORT: process.env.GRAPHQL_PORT || 3000, 16 | } -------------------------------------------------------------------------------- /src/debugConsole.ts: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | // import prettyFormat from 'pretty-format'; 3 | 4 | export const debugConsole = (obj: Record) => { 5 | // eslint-disable-next-line 6 | console.log( 7 | util.inspect(obj, { 8 | showHidden: false, 9 | depth: null, 10 | colors: true, 11 | showProxy: false, 12 | }), 13 | ); 14 | // eslint-disable-next-line 15 | // console.log(prettyFormat(obj)); 16 | }; 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | import { config } from './config'; 3 | import app from './app'; 4 | import { createServer } from 'http'; 5 | 6 | (async () => { 7 | const server = createServer(app.callback()); 8 | 9 | server.listen(config.GRAPHQL_PORT, () => { 10 | console.log(`GraphQL server at: ${config.GRAPHQL_PORT}`) 11 | }); 12 | })(); -------------------------------------------------------------------------------- /src/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | version: String! 3 | } 4 | 5 | type Mutation { 6 | } -------------------------------------------------------------------------------- /test/setupTestFramework.js: -------------------------------------------------------------------------------- 1 | // this file is ran right after the test framework is setup for some test file. 2 | require('@babel/polyfill'); 3 | 4 | process.env.TZ = 'GMT'; 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const webpack = require('webpack'); 4 | 5 | const WebpackNodeExternals = require('webpack-node-externals'); 6 | const ReloadServerPlugin = require('reload-server-webpack-plugin'); 7 | 8 | const cwd = process.cwd(); 9 | 10 | module.exports = { 11 | mode: 'development', 12 | devtool: 'cheap-eval-source-map', 13 | entry: { 14 | server: [ 15 | // 'webpack/hot/poll?1000', 16 | './src/index.ts', 17 | ], 18 | }, 19 | output: { 20 | path: path.resolve('build'), 21 | filename: 'api.js', 22 | // https://github.com/webpack/webpack/pull/8642 23 | futureEmitAssets: true, 24 | }, 25 | watch: true, 26 | target: 'node', 27 | externals: [ 28 | WebpackNodeExternals({ 29 | whitelist: ['webpack/hot/poll?1000'], 30 | }), 31 | ], 32 | resolve: { 33 | extensions: ['.ts', '.tsx', '.js', '.json', '.mjs'], 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.mjs$/, 39 | include: /node_modules/, 40 | type: 'javascript/auto', 41 | }, 42 | { 43 | test: /\.(js|jsx|ts|tsx)?$/, 44 | use: { 45 | loader: 'babel-loader?cacheDirectory', 46 | }, 47 | exclude: [/node_modules/], 48 | include: [path.join(cwd, 'src'), path.join(cwd, '../')], 49 | }, 50 | ], 51 | }, 52 | plugins: [ 53 | new ReloadServerPlugin({ 54 | script: path.resolve('build', 'api.js'), 55 | }), 56 | new webpack.HotModuleReplacementPlugin(), 57 | new webpack.DefinePlugin({ 58 | 'process.env.NODE_ENV': JSON.stringify('development'), 59 | }), 60 | ], 61 | }; 62 | --------------------------------------------------------------------------------