├── .eslintignore ├── packages ├── generator │ ├── .npmignore │ ├── .flowconfig │ ├── src │ │ ├── connection │ │ │ ├── templates │ │ │ │ └── Connection.js.template │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── ConnectionGenerator.spec.js.snap │ │ │ │ └── ConnectionGenerator.spec.js │ │ │ └── index.js │ │ ├── graphqlrc.json │ │ ├── paths.js │ │ ├── ejsHelpers.js │ │ ├── form │ │ │ ├── __tests__ │ │ │ │ ├── FormGenerator.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── FormGenerator.spec.js.snap │ │ │ ├── index.js │ │ │ └── templates │ │ │ │ └── Form.js.template │ │ ├── list │ │ │ ├── __tests__ │ │ │ │ ├── ListGenerator.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ListGenerator.spec.js.snap │ │ │ ├── index.js │ │ │ └── templates │ │ │ │ └── List.js.template │ │ ├── view │ │ │ ├── __tests__ │ │ │ │ ├── ViewGenerator.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ViewGenerator.spec.js.snap │ │ │ ├── templates │ │ │ │ └── View.js.template │ │ │ └── index.js │ │ ├── type │ │ │ ├── templates │ │ │ │ ├── Type.js.template │ │ │ │ ├── test │ │ │ │ │ └── Type.js.template │ │ │ │ └── TypeWithSchema.js.template │ │ │ ├── __tests__ │ │ │ │ ├── TypeGenerator.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── TypeGenerator.spec.js.snap │ │ │ └── index.js │ │ ├── add │ │ │ ├── __tests__ │ │ │ │ ├── AddGenerator.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── AddGenerator.spec.js.snap │ │ │ ├── templates │ │ │ │ ├── Add.js.template │ │ │ │ └── AddMutation.js.template │ │ │ └── index.js │ │ ├── edit │ │ │ ├── __tests__ │ │ │ │ ├── EditGenerator.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── EditGenerator.spec.js.snap │ │ │ ├── templates │ │ │ │ ├── EditMutation.js.template │ │ │ │ └── Edit.js.template │ │ │ └── index.js │ │ ├── graphql-logo.js │ │ ├── mutation │ │ │ ├── templates │ │ │ │ ├── test │ │ │ │ │ ├── MutationAdd.js.template │ │ │ │ │ ├── MutationEdit.js.template │ │ │ │ │ ├── MutationAddWithSchema.js.template │ │ │ │ │ └── MutationEditWithSchema.js.template │ │ │ │ ├── MutationAdd.js.template │ │ │ │ ├── MutationEdit.js.template │ │ │ │ ├── MutationAddWithSchema.js.template │ │ │ │ └── MutationEditWithSchema.js.template │ │ │ ├── __tests__ │ │ │ │ ├── MutationGenerator.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── MutationGenerator.spec.js.snap │ │ │ └── index.js │ │ ├── utils.js │ │ ├── loader │ │ │ ├── templates │ │ │ │ ├── Loader.js.template │ │ │ │ └── LoaderWithSchema.js.template │ │ │ ├── index.js │ │ │ └── __tests__ │ │ │ │ ├── LoaderGenerator.spec.js │ │ │ │ └── __snapshots__ │ │ │ │ └── LoaderGenerator.spec.js.snap │ │ ├── config.js │ │ ├── app │ │ │ └── index.js │ │ └── parser │ │ │ ├── graphql.js │ │ │ └── mongoose.js │ ├── .babelrc │ ├── fixtures │ │ ├── AdminUser.js │ │ ├── User.js │ │ ├── Comment.js │ │ └── Post.js │ ├── package.json │ ├── test │ │ └── helpers.js │ └── flow-typed │ │ └── npm │ │ └── yeoman-generator_vx.x.x.js └── create-graphql │ ├── .npmignore │ ├── bin │ └── entria-graphql │ ├── src │ ├── commands │ │ ├── index.js │ │ ├── init.js │ │ ├── generate.js │ │ └── frontend.js │ ├── utils.js │ └── index.js │ ├── .flowconfig │ ├── .babelrc │ ├── package.json │ └── README.md ├── media └── logo.png ├── .flowconfig ├── docs ├── README.md ├── Configuration.md └── Commands.md ├── .gitignore ├── .babelrc ├── appveyor.yml ├── lerna.json ├── .travis.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── .eslintrc ├── jest.json ├── LICENSE ├── README.md ├── CONTRIBUTING.md └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /packages/generator/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | npm-debug.log 3 | coverage 4 | -------------------------------------------------------------------------------- /packages/create-graphql/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | npm-debug.log 3 | coverage 4 | -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/entria/entria-graphql/HEAD/media/logo.png -------------------------------------------------------------------------------- /packages/create-graphql/bin/entria-graphql: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/index.js'); 3 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | > Documentation topics 3 | 4 | - [Commands](Commands.md) 5 | - [Configuration](Configuration.md) 6 | -------------------------------------------------------------------------------- /packages/create-graphql/src/commands/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export init from './init'; 3 | export generate from './generate'; 4 | export frontend from './frontend'; 5 | -------------------------------------------------------------------------------- /packages/generator/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | esproposal.export_star_as=enable 11 | 12 | [strict] 13 | -------------------------------------------------------------------------------- /packages/create-graphql/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | esproposal.export_star_as=enable 11 | 12 | [strict] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # WebStorm 2 | .idea 3 | 4 | # OSX 5 | .DS_Store 6 | 7 | # Lerna 8 | lerna-debug.log 9 | 10 | # npm 11 | node_modules 12 | npm-debug.log 13 | 14 | # Build folders 15 | dist 16 | generators 17 | 18 | # Test 19 | coverage 20 | -------------------------------------------------------------------------------- /packages/create-graphql/src/commands/init.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import spawn from 'cross-spawn'; 3 | 4 | function create(project) { 5 | const spawnOptions = ['@entria/graphql', project]; 6 | 7 | spawn('yo', spawnOptions, { shell: true, stdio: 'inherit' }); 8 | } 9 | 10 | export default create; 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "flow", 4 | ["env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | }] 9 | ], 10 | "plugins": [ 11 | "transform-object-rest-spread", 12 | "transform-class-properties", 13 | "transform-export-extensions", 14 | "transform-async-to-generator" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/generator/src/connection/templates/Connection.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { connectionDefinitions } from '../../graphql/connection/CustomConnectionType'; 3 | 4 | import <%= name %>Type from './<%= name %>Type'; 5 | 6 | export default connectionDefinitions({ 7 | name: '<%= name %>', 8 | nodeType: <%= name %>Type, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/generator/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "flow", 4 | ["env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | }] 9 | ], 10 | "plugins": [ 11 | "transform-object-rest-spread", 12 | "transform-class-properties", 13 | "transform-export-extensions", 14 | "transform-async-to-generator" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/create-graphql/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "flow", 4 | ["env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | }] 9 | ], 10 | "plugins": [ 11 | "transform-object-rest-spread", 12 | "transform-class-properties", 13 | "transform-export-extensions", 14 | "transform-async-to-generator" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/generator/src/graphqlrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "directories": { 3 | "source": "src", 4 | "module": "modules", 5 | "connection": "", 6 | "loader": "", 7 | "model": "", 8 | "mutation": "", 9 | "mutation_test": "", 10 | "type": "", 11 | "type_test": "", 12 | "interface": "../../../graphql/interface" 13 | }, 14 | "files": { 15 | "schema": "schema" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - node_version: "4" 4 | - node_version: "6" 5 | - node_version: "7" 6 | 7 | install: 8 | - ps: Install-Product node $env:node_version 9 | - npm install -g npm@3.x 10 | - npm install 11 | - npm run bootstrap 12 | 13 | build: off 14 | 15 | test_script: 16 | - node --version 17 | - yarn --version 18 | - npm run lint 19 | - npm run test -- --runInBand 20 | -------------------------------------------------------------------------------- /packages/generator/src/paths.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import path from 'path'; 3 | 4 | export const getModulePath = (source: string, moduleName: string) => { 5 | return path.join(source, moduleName); 6 | }; 7 | 8 | export const getTestPath = (source: string) => { 9 | return path.join(source, '__tests__'); 10 | }; 11 | 12 | export const getMutationPath = (source: string) => { 13 | return path.join(source, 'mutation'); 14 | }; 15 | -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | You may customize the folders that the generated files will be created on by using a `.graphqlrc` file on the root folder with the following content: 4 | 5 | ```json 6 | { 7 | "directories": { 8 | "source": "src", 9 | "connection": "graphql/connection", 10 | "loader": "graphql/loader", 11 | "model": "models/models", 12 | "mutation": "graphql/mutation", 13 | "type": "graphql/type" 14 | } 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0-beta.30", 3 | "version": "0.0.16", 4 | "changelog": { 5 | "repo": "entria/create-graphql", 6 | "labels": { 7 | "new feature": ":rocket: New Feature", 8 | "enhancement": ":white_check_mark: Enhancement", 9 | "bug fix": ":bug: Bug Fix", 10 | "polish": ":nail_care: Polish", 11 | "documentation": ":memo: Documentation", 12 | "internal": ":house: Internal" 13 | } 14 | }, 15 | "packages": [ 16 | "packages/*" 17 | ], 18 | "npmClient": "yarn" 19 | } 20 | -------------------------------------------------------------------------------- /packages/generator/fixtures/AdminUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import mongoose from 'mongoose'; 3 | 4 | const Schema = new mongoose.Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | password: { 10 | type: String, 11 | hidden: true, 12 | }, 13 | email: { 14 | type: String, 15 | required: false, 16 | index: true, 17 | }, 18 | active: { 19 | type: Boolean, 20 | default: true, 21 | }, 22 | }, { 23 | collection: 'adminUser', 24 | }); 25 | 26 | export default mongoose.model('AdminUser', Schema); 27 | -------------------------------------------------------------------------------- /packages/generator/fixtures/User.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import mongoose from 'mongoose'; 3 | 4 | const Schema = new mongoose.Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | password: { 10 | type: String, 11 | hidden: true, 12 | }, 13 | email: { 14 | type: String, 15 | required: false, 16 | index: true, 17 | }, 18 | active: { 19 | type: Boolean, 20 | default: true, 21 | }, 22 | lastLoginAt: Date, 23 | }, { 24 | collection: 'user', 25 | }); 26 | 27 | export default mongoose.model('User', Schema); 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | 7 | node_js: 8 | - '4' 9 | - '5' 10 | - '6' 11 | - '7' 12 | 13 | before_install: 14 | - curl -o- -L https://yarnpkg.com/install.sh | bash 15 | - export PATH=$HOME/.yarn/bin:$PATH 16 | 17 | install: 18 | - yarn install 19 | - yarn run bootstrap 20 | 21 | script: 22 | - node --version 23 | - npm --version 24 | - yarn --version 25 | - ./node_modules/.bin/eslint --version 26 | - yarn run lint 27 | - yarn run test -- --runInBand 28 | 29 | after_success: 30 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /packages/generator/src/ejsHelpers.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /** 3 | * Uppercase the first letter of a text 4 | * @param text {string} 5 | * @returns {string} 6 | */ 7 | export const uppercaseFirstLetter = (text: string) => `${text.charAt(0).toUpperCase()}${text.slice(1)}`; 8 | /** 9 | * Camel cases text 10 | * @param text {string} Text to be camel-cased 11 | * @returns {string} Camel-cased text 12 | */ 13 | export const camelCaseText = (text: string) => 14 | text.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, (match, index) => { 15 | if (+match === 0) { 16 | return ''; 17 | } 18 | 19 | return index === 0 ? match.toLowerCase() : match.toUpperCase(); 20 | }); 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please provide enough information so that others can review your pull request: 2 | 3 | Explain the **motivation** for making this change. What existing problem does the pull request solve? 4 | 5 | Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise split it. 6 | 7 | **Test plan (required)** 8 | 9 | Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. 10 | 11 | The code must pass tests and shouldn't add more Flow errors. 12 | 13 | **Code formatting** 14 | 15 | Look around. Match the style of the rest of the codebase. 16 | -------------------------------------------------------------------------------- /packages/generator/src/form/__tests__/FormGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | getFileContent, 7 | } from '../../../test/helpers'; 8 | 9 | import { getConfigDir } from '../../config'; 10 | 11 | const formGenerator = path.join(__dirname, '..'); 12 | 13 | it('generate a form file', async () => { 14 | const folder = await helper.run(formGenerator) 15 | .withArguments('Example') 16 | .toPromise(); 17 | 18 | const destinationDir = getConfigDir('add'); 19 | 20 | assert.file([ 21 | `${destinationDir}/ExampleForm.js`, 22 | ]); 23 | 24 | expect(getFileContent(`${folder}/${destinationDir}/ExampleForm.js`)).toMatchSnapshot(); 25 | }); 26 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "no-console": 0, 6 | "max-len": [1, 120, 2], 7 | "no-param-reassign": [2, { "props": false }], 8 | "no-continue": 0, 9 | "no-underscore-dangle": 0, 10 | "generator-star-spacing": 0, 11 | "class-methods-use-this": 0, 12 | "import/no-extraneous-dependencies": 0, // https://github.com/benmosher/eslint-plugin-import/pull/685 13 | "prefer-destructuring": 1, 14 | "no-use-before-define": 1, 15 | "flowtype/define-flow-type": 1, 16 | "flowtype/use-flow-type": 1, 17 | "no-restricted-syntax": 1 18 | }, 19 | "plugins": [ 20 | "flowtype" 21 | ], 22 | "env": { 23 | "jest": true, 24 | "node": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "testPathIgnorePatterns": [ 4 | "/coverage/", 5 | "/dist/", 6 | "/generators/", 7 | "/node_modules/" 8 | ], 9 | "moduleDirectories": [ 10 | "src", 11 | "node_modules" 12 | ], 13 | "coveragePathIgnorePatterns": [ 14 | "/__tests__/", 15 | "/coverage/", 16 | "/dist/", 17 | "/generators/", 18 | "/node_modules/" 19 | ], 20 | "coverageDirectory": "./coverage/", 21 | "coverageReporters": [ 22 | "lcov", 23 | "html" 24 | ], 25 | "collectCoverage": true, 26 | "collectCoverageFrom": [ 27 | "**/**/*.js" 28 | ], 29 | "roots": [ 30 | "generator/src", 31 | "create-graphql/src" 32 | ], 33 | "rootDir": "./packages/", 34 | "verbose": true 35 | } 36 | -------------------------------------------------------------------------------- /packages/generator/fixtures/Comment.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import mongoose from 'mongoose'; 3 | 4 | import User from './User'; 5 | const { ObjectId } = mongoose.Schema.Types; 6 | 7 | const Schema = new mongoose.Schema({ 8 | author: { 9 | type: ObjectId, 10 | ref: 'User', 11 | description: 'User that created this comment', 12 | required: true, 13 | }, 14 | score: { 15 | type: Number, 16 | description: 'Sum of all upvotes/downvotes this comment has', 17 | required: false, 18 | }, 19 | text: { 20 | type: String, 21 | required: true, 22 | }, 23 | }, { 24 | timestamps: { 25 | createdAt: 'createdAt', 26 | updatedAt: 'updatedAt', 27 | }, 28 | collection: 'comment', 29 | }); 30 | 31 | export default mongoose.model('Comment', Schema); 32 | -------------------------------------------------------------------------------- /packages/generator/src/list/__tests__/ListGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | getFileContent, 7 | } from '../../../test/helpers'; 8 | 9 | import { getConfigDir } from '../../config'; 10 | 11 | const listGenerator = path.join(__dirname, '..'); 12 | 13 | it('generate list files', async () => { 14 | const folder = await helper.run(listGenerator) 15 | .withArguments('Example') 16 | .toPromise(); 17 | 18 | const destinationDir = getConfigDir('list'); 19 | 20 | assert.file([ 21 | `${destinationDir}/ExampleList.js`, 22 | ]); 23 | 24 | const files = { 25 | list: getFileContent(`${folder}/${destinationDir}/ExampleList.js`), 26 | }; 27 | 28 | expect(files).toMatchSnapshot(); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/generator/src/view/__tests__/ViewGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | getFileContent, 7 | } from '../../../test/helpers'; 8 | 9 | import { getConfigDir } from '../../config'; 10 | 11 | const viewGenerator = path.join(__dirname, '..'); 12 | 13 | it('generate view files', async () => { 14 | const folder = await helper.run(viewGenerator) 15 | .withArguments('Example') 16 | .toPromise(); 17 | 18 | const destinationDir = getConfigDir('view'); 19 | 20 | assert.file([ 21 | `${destinationDir}/ExampleView.js`, 22 | ]); 23 | 24 | const files = { 25 | view: getFileContent(`${folder}/${destinationDir}/ExampleView.js`), 26 | }; 27 | 28 | expect(files).toMatchSnapshot(); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/generator/src/connection/__tests__/__snapshots__/ConnectionGenerator.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generate a connection 1`] = ` 4 | Object { 5 | "connection": "// @flow 6 | import { connectionDefinitions } from '../../graphql/connection/CustomConnectionType'; 7 | 8 | import ExampleType from './ExampleType'; 9 | 10 | export default connectionDefinitions({ 11 | name: 'Example', 12 | nodeType: ExampleType, 13 | }); 14 | ", 15 | } 16 | `; 17 | 18 | exports[`generate a connection with schema 1`] = ` 19 | Object { 20 | "connection": "// @flow 21 | import { connectionDefinitions } from '../../graphql/connection/CustomConnectionType'; 22 | 23 | import PostType from './PostType'; 24 | 25 | export default connectionDefinitions({ 26 | name: 'Post', 27 | nodeType: PostType, 28 | }); 29 | ", 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### Current Behavior 6 | 8 | 9 | 10 | ### Expected Behavior 11 | 12 | 13 | 14 | ### Your Environment 15 | 16 | 17 | | software | version 18 | | ---------------- | ------- 19 | | create-graphql | 20 | | node | 21 | | npm or yarn | 22 | -------------------------------------------------------------------------------- /packages/generator/src/type/templates/Type.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | GraphQLObjectType, 4 | GraphQLString, 5 | } from 'graphql'; 6 | import { globalIdField } from 'graphql-relay'; 7 | import type { GraphQLObjectTypeConfig } from 'graphql'; 8 | import type { GraphQLContext } from '../../TypeDefinition'; 9 | 10 | import { NodeInterface } from '<%= directories.interface %>/NodeInterface'; 11 | import <%= name %> from './<%= name %>Loader'; 12 | 13 | export default new GraphQLObjectType( 14 | ({ 15 | name: '<%= name %>', 16 | description: 'Represents <%= name %>', 17 | fields: () => ({ 18 | id: globalIdField('<%= name %>'), 19 | example: { 20 | type: GraphQLString, 21 | description: 'My example field', 22 | resolve: obj => obj.example, 23 | }, 24 | }), 25 | interfaces: () => [NodeInterface], 26 | }: GraphQLObjectTypeConfig<<%= name %>, GraphQLContext>), 27 | ); 28 | -------------------------------------------------------------------------------- /packages/generator/src/add/__tests__/AddGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | getFileContent, 7 | } from '../../../test/helpers'; 8 | 9 | import { getConfigDir } from '../../config'; 10 | 11 | const addGenerator = path.join(__dirname, '..'); 12 | 13 | it('generate add and add mutation files', async () => { 14 | const folder = await helper.run(addGenerator) 15 | .withArguments('Example') 16 | .toPromise(); 17 | 18 | const destinationDir = getConfigDir('add'); 19 | 20 | assert.file([ 21 | `${destinationDir}/ExampleAdd.js`, `${destinationDir}/ExampleAddMutation.js`, 22 | ]); 23 | 24 | const files = { 25 | add: getFileContent(`${folder}/${destinationDir}/ExampleAdd.js`), 26 | addMutation: getFileContent(`${folder}/${destinationDir}/ExampleAddMutation.js`), 27 | }; 28 | 29 | expect(files).toMatchSnapshot(); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/generator/src/edit/__tests__/EditGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | getFileContent, 7 | } from '../../../test/helpers'; 8 | 9 | import { getConfigDir } from '../../config'; 10 | 11 | const editGenerator = path.join(__dirname, '..'); 12 | 13 | it('generate edit and edit mutation files', async () => { 14 | const folder = await helper.run(editGenerator) 15 | .withArguments('Example') 16 | .toPromise(); 17 | 18 | const destinationDir = getConfigDir('edit'); 19 | 20 | assert.file([ 21 | `${destinationDir}/ExampleEdit.js`, `${destinationDir}/ExampleEditMutation.js`, 22 | ]); 23 | 24 | const files = { 25 | edit: getFileContent(`${folder}/${destinationDir}/ExampleEdit.js`), 26 | editMutation: getFileContent(`${folder}/${destinationDir}/ExampleEditMutation.js`), 27 | }; 28 | 29 | expect(files).toMatchSnapshot(); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/create-graphql/src/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import shell from 'shelljs'; 3 | import chalk from 'chalk'; 4 | import ora from 'ora'; 5 | import spawn from 'cross-spawn-promise'; 6 | 7 | const tic = chalk.green('✓'); 8 | const tac = chalk.red('✗'); 9 | 10 | const installYeoman = async () => { 11 | const spinner = ora('Installing Yeoman...'); 12 | 13 | spinner.start(); 14 | 15 | const command = 'npm'; 16 | const args = ['install', '-g', 'yo']; 17 | const options = { 18 | shell: true, 19 | stdio: false, 20 | }; 21 | 22 | try { 23 | await spawn(command, args, options); 24 | 25 | spinner.stop(); 26 | 27 | console.log(`${tic} Yeoman installed!`); 28 | } catch (error) { 29 | spinner.stop(); 30 | 31 | console.error(`${tac} There was an error while trying to install Yeoman:`, error); 32 | } 33 | }; 34 | 35 | export const verifyYeoman = async () => { // eslint-disable-line import/prefer-default-export 36 | if (!shell.which('yo')) { 37 | console.error(`${tac} GraphQL CLI requires Yeoman to be installed.`); 38 | 39 | await installYeoman(); 40 | } 41 | 42 | return true; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/generator/src/type/templates/test/Type.js.template: -------------------------------------------------------------------------------- 1 | import { graphql } from 'graphql'; 2 | import { schema } from '../../../graphql/schema'; 3 | import { 4 | getContext, 5 | connectMongoose, 6 | clearDbAndRestartCounters, 7 | disconnectMongoose, 8 | } from '../../../../test/helper'; 9 | import { 10 | User, 11 | <%= name %>, 12 | } from '../../../models'; 13 | 14 | beforeAll(connectMongoose); 15 | 16 | beforeEach(clearDbAndRestartCounters); 17 | 18 | afterAll(disconnectMongoose); 19 | 20 | it('should retrieve a record', async () => { 21 | const user = await new User({ 22 | name: 'user', 23 | email: 'user@example.com', 24 | }).save(); 25 | 26 | // TODO: query to return a record 27 | const query = ` 28 | query Q { 29 | node(id:"123") { 30 | id 31 | } 32 | } 33 | `; 34 | 35 | const variables = { 36 | 37 | }; 38 | const rootValue = {}; 39 | const context = getContext({ user }); 40 | 41 | const { errors, data } = await graphql(schema, query, rootValue, context, variables); 42 | 43 | expect(data.node).toBe(null); 44 | expect(errors).toBe(undefined); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/generator/fixtures/Post.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | import User from './User'; 4 | import Comment from './Comment'; 5 | const { ObjectId } = mongoose.Schema.Types; 6 | 7 | const Schema = new mongoose.Schema({ 8 | title: { 9 | type: String, 10 | maxlength: 120, 11 | required: true, 12 | }, 13 | author: { 14 | type: ObjectId, 15 | ref: 'User', 16 | description: 'User that created this post', 17 | required: true, 18 | }, 19 | slug: { 20 | type: String, 21 | indexed: true, 22 | description: 'Used for SEO', 23 | }, 24 | tags: [String], 25 | oldSlugs: { 26 | type: [String], 27 | description: 'Old slugs used by this post' 28 | }, 29 | comments: [ 30 | { 31 | type: ObjectId, 32 | ref: 'Comment', 33 | }, 34 | ], 35 | externalComments: { 36 | type: [ObjectId], 37 | ref: 'Comment', 38 | description: 'Comments from external source' 39 | }, 40 | }, { 41 | timestamps: { 42 | createdAt: 'createdAt', 43 | updatedAt: 'updatedAt', 44 | }, 45 | collection: 'post', 46 | }); 47 | 48 | export default mongoose.model('Post', Schema); 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Lucas Bento da Silva 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. 22 | -------------------------------------------------------------------------------- /packages/generator/src/list/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Generator from 'yeoman-generator'; 3 | import pluralize from 'pluralize'; 4 | import { getConfigDir } from '../config'; 5 | import { uppercaseFirstLetter } from '../ejsHelpers'; 6 | 7 | class ListGenerator extends Generator { 8 | constructor(args, options) { 9 | super(args, options); 10 | 11 | this.argument('name', { 12 | type: String, 13 | required: true, 14 | }); 15 | 16 | // TODO read schema.json 17 | 18 | this.destinationDir = getConfigDir('list'); 19 | } 20 | 21 | generateList() { 22 | const name = uppercaseFirstLetter(this.options.name); 23 | 24 | const templatePath = this.templatePath('List.js.template'); 25 | 26 | const pluralName = pluralize(this.options.name); 27 | 28 | const destinationPath = this.destinationPath(`${this.destinationDir}/${name}List.js`); 29 | 30 | const templateVars = { 31 | name, 32 | pluralName, 33 | }; 34 | 35 | this.fs.copyTpl(templatePath, destinationPath, templateVars); 36 | } 37 | 38 | end() { 39 | this.log('🔥 List created!'); 40 | } 41 | } 42 | 43 | module.exports = ListGenerator; 44 | -------------------------------------------------------------------------------- /packages/generator/src/edit/templates/EditMutation.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { commitMutation, graphql } from 'react-relay/compat'; 3 | 4 | import RelayStore from '../../relay/RelayStore'; 5 | 6 | import type { 7 | <%= name %>EditMutationVariables, 8 | <%= name %>EditMutationResponse, 9 | } from './__generated__/<%= name %>EditMutation.graphql'; 10 | 11 | type <%= name %>EditMutationInput = $PropertyType< 12 | <%= name %>EditMutationVariables, 13 | 'input', 14 | >; 15 | 16 | const mutation = graphql` 17 | mutation <%= name %>EditMutation($input: <%= name %>EditInput!) { 18 | <%= name %>Edit(input: $input) { 19 | <%= name.toLowerCase() %> { 20 | id 21 | } 22 | error 23 | } 24 | } 25 | `; 26 | 27 | const commit = ( 28 | input: <%= name %>EditMutationInput, 29 | onCompleted: (response: <%= name %>EditMutationResponse) => void, 30 | onError: (error: Error) => void, 31 | ) => { 32 | const variables = { 33 | input, 34 | }; 35 | 36 | commitMutation(RelayStore._env, { 37 | mutation, 38 | variables, 39 | onCompleted, 40 | onError, 41 | }); 42 | }; 43 | 44 | export default { commit }; 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | #### With Yarn: 4 | ```sh 5 | yarn global add @entria/create-graphql @entria/generator-graphql 6 | ``` 7 | 8 | #### With NPM: 9 | ```sh 10 | npm i -g @entria/create-graphql @entria/generator-graphql 11 | ``` 12 | 13 | ## Usage 14 | You can create a brand new GraphQL project: 15 | ```sh 16 | entria-graphql init GraphQLProject 17 | ``` 18 | 19 | And can generate single files for [Mutation](docs/Commands.md#--mutation--m), [Type](docs/Commands.md#--type--t) and [others](docs/Commands.md#generate--g): 20 | ```sh 21 | entria-graphql generate --mutation Story 22 | ``` 23 | This generates a `StoryAddMutation` and `StoryEditMutation` 24 | 25 | > See more usage examples in the [docs](docs) 26 | 27 | ## Contributing 28 | If you want to contribute, see the [Contributing guidelines](CONTRIBUTING.md) before and feel free to send your contributions. 29 | 30 | ## Feedbacks 31 | 32 | We love the feedbacks. It's help us to continue grow and improve. Give your feedbacks by open an [issue](https://github.com/graphql-community/create-graphql/issues/new). We will be glad to discuss your suggestions! 33 | 34 | tks for @lucasbento 35 | 36 | ## License 37 | 38 | MIT © [Entria](http://github.com/entria) 39 | -------------------------------------------------------------------------------- /packages/generator/src/add/templates/Add.js.template: -------------------------------------------------------------------------------- 1 | //@flow 2 | import * as React from 'react'; 3 | import { graphql, createFragmentContainer } from 'react-relay/compat'; 4 | import { hot } from 'react-hot-loader'; 5 | 6 | import createQueryRenderer from '../../relay/createQueryRenderer'; 7 | 8 | import <%= name %>Form from './<%= name %>Form'; 9 | import type { <%= name %>Add_viewer } from './__generated__/<%= name %>Add_viewer.graphql'; 10 | 11 | type Props = { 12 | viewer: <%= name %>Add_viewer, 13 | }; 14 | class <%= name %>Add extends React.PureComponent { 15 | render() { 16 | const { viewer } = this.props; 17 | return <<%= name %>Form viewer={viewer} />; 18 | } 19 | } 20 | 21 | const <%= name %>AddFragment = createFragmentContainer(<%= name %>Add, { 22 | viewer: graphql` 23 | fragment <%= name %>Add_viewer on Viewer { 24 | id 25 | ...<%= name %>Form_viewer 26 | } 27 | `, 28 | }); 29 | 30 | export default hot(module)( 31 | createQueryRenderer(<%= name %>AddFragment, <%= name %>Add, { 32 | query: graphql` 33 | query <%= name %>AddQuery { 34 | viewer { 35 | ...<%= name %>Add_viewer 36 | } 37 | } 38 | `, 39 | }), 40 | ); 41 | -------------------------------------------------------------------------------- /packages/generator/src/connection/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Generator from 'yeoman-generator'; 3 | import path from 'path'; 4 | import { getConfigDir } from '../config'; 5 | import { uppercaseFirstLetter } from '../ejsHelpers'; 6 | import { getModulePath } from '../paths'; 7 | 8 | class ConnectionGenerator extends Generator { 9 | constructor(args, options) { 10 | super(args, options); 11 | 12 | this.argument('name', { 13 | type: String, 14 | required: true, 15 | }); 16 | 17 | this.destinationDir = getConfigDir('connection'); 18 | } 19 | 20 | generateConnection() { 21 | const name = uppercaseFirstLetter(this.options.name); 22 | 23 | const templatePath = this.templatePath('Connection.js.template'); 24 | 25 | const moduleName = this.options.name.toLowerCase(); 26 | const modulePath = getModulePath(this.destinationDir, moduleName); 27 | 28 | const destinationPath = this.destinationPath( 29 | path.join(modulePath, `${name}Connection.js`), 30 | ); 31 | 32 | const templateVars = { 33 | name, 34 | }; 35 | 36 | this.fs.copyTpl(templatePath, destinationPath, templateVars); 37 | } 38 | 39 | end() { 40 | this.log('🔥 Connection created!'); 41 | } 42 | } 43 | 44 | module.exports = ConnectionGenerator; 45 | -------------------------------------------------------------------------------- /packages/generator/src/type/templates/TypeWithSchema.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | GraphQLObjectType, 4 | <%_ for (i in dependencies) { -%> 5 | <%= dependencies[i] %>, 6 | <%_ } -%> 7 | } from 'graphql'; 8 | import { globalIdField } from 'graphql-relay'; 9 | import type { GraphQLObjectTypeConfig } from 'graphql'; 10 | import type { GraphQLContext } from '../../TypeDefinition'; 11 | 12 | import { NodeInterface } from '<%= directories.interface %>/NodeInterface'; 13 | <% Object.keys(depsMap).sort((a, b) => b.localeCompare(a)).forEach(function (depKey) { -%> 14 | import <%= depsMap[depKey].importName %> from '<%= depsMap[depKey].relativePath %>'; 15 | <% }); -%> 16 | import <%= name %> from './<%= name %>Loader'; 17 | 18 | export default new GraphQLObjectType( 19 | ({ 20 | name: '<%= name %>', 21 | description: 'Represents <%= name %>', 22 | fields: () => ({ 23 | id: globalIdField('<%= name %>'), 24 | <%_ for (field of schema.fields) { -%> 25 | <%- field.name %>: { 26 | type: <%= field.type %>, 27 | description: '<%= field.description %>', 28 | resolve: <%if(field.resolveArgs){%><%=field.resolveArgs%><%}else{ %>obj<%} %> => <%= field.resolve %>, 29 | }, 30 | <%_ } -%> 31 | }), 32 | interfaces: () => [NodeInterface], 33 | }: GraphQLObjectTypeConfig<<%= name %>, GraphQLContext>), 34 | ); 35 | -------------------------------------------------------------------------------- /packages/generator/src/graphql-logo.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | 3 | \`-/+o+/- 4 | \`+ooooooo/ 5 | \`-+ooooooooo/-\` 6 | \`.:+++::oo+++o+:/++/-. 7 | ...\` \`-/++/-\` -o+. :o+. .:/++:.\` \`...\` 8 | \`:++oo++:+++:.\` :o+. -o+. \`-/++//++oo++- 9 | /oooooooo+.\` /o/\` .++- \`.ooooooooo- 10 | .+oooooo/\` \`+o: \`+o: .+oooooo/\` 11 | \`.:oo:. .+o: \`/o/\` \`./o+-\` 12 | .oo\` .++- \`:o/\` -o+ 13 | .oo\` -++. :o+. -o+ 14 | .oo\` :o+. -++. -o+ 15 | .oo\` :o/\` -++- -o+ 16 | .oo\`\`/o/\` .+o- -o+ 17 | .oo./o: \`+o:-o+ 18 | .:+oo+o/ \`+o+oo/-\` 19 | +oooooooo++++++++++++++++++++++++++++++ooooooooo: 20 | -oooooooo+/-.\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`\`.-/oooooooo+. 21 | .:++++:.:/++:-\` \`-/++:.-/+++/:\` 22 | \`-/++/-\` \`..\`\` \`-/+/-. 23 | .:+++:/oooo+::++/-\` 24 | \`/ooooooooo. 25 | /oooooo+- 26 | \`-:::-\` 27 | 28 | `; 29 | -------------------------------------------------------------------------------- /packages/generator/src/edit/templates/Edit.js.template: -------------------------------------------------------------------------------- 1 | //@flow 2 | import * as React from 'react'; 3 | import { graphql, createFragmentContainer } from 'react-relay/compat'; 4 | import { hot } from 'react-hot-loader'; 5 | 6 | import createQueryRenderer from '../../relay/createQueryRenderer'; 7 | 8 | import <%= name %>Form from './<%= name %>Form'; 9 | import type { <%= name %>Edit_viewer } from './__generated__/<%= name %>Edit_viewer.graphql'; 10 | 11 | type Props = { 12 | viewer: <%= name %>Edit_viewer, 13 | }; 14 | class <%= name %>Edit extends React.PureComponent { 15 | render() { 16 | const { viewer } = this.props; 17 | const { _node } = viewer; 18 | 19 | return <<%= name %>Form viewer={viewer} node={_node} />; 20 | } 21 | } 22 | 23 | const <%= name %>EditFragment = createFragmentContainer(<%= name %>Edit, { 24 | viewer: graphql` 25 | fragment <%= name %>Edit_viewer on Viewer { 26 | id 27 | ...<%= name %>Form_viewer 28 | _node(id: $id) { 29 | ...<%= name %>Form_node 30 | } 31 | } 32 | `, 33 | }); 34 | 35 | export default hot(module)( 36 | createQueryRenderer(<%= name %>EditFragment, <%= name %>Edit, { 37 | query: graphql` 38 | query <%= name %>EditQuery($id: ID!) { 39 | viewer { 40 | ...<%= name %>Edit_viewer 41 | } 42 | } 43 | `, 44 | queriesParams: ({ match: { params } }) => ({ id: params.id }), 45 | }), 46 | ); 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | 3 | 1. Fork this repository; 4 | 2. Clone the forked version of create-graphql: 5 | ```sh 6 | git clone git@github.com:/create-graphql.git 7 | ``` 8 | 9 | 3. Install [lerna/lerna](https://github.com/lerna/lerna) 10 | ```sh 11 | yarn global add lerna@prerelease # With NPM: `npm install --global lerna@prerelease` 12 | ``` 13 | 14 | 4. Install the main package dependencies 15 | ```sh 16 | yarn # With NPM: `npm install` 17 | ``` 18 | 19 | 5. Bootstrap all packages 20 | ```sh 21 | lerna bootstrap 22 | ``` 23 | This will install all dependencies of all `subpackages` and link them properly 24 | 25 | 6. Link the `generator` package 26 | ```sh 27 | cd packages/generator && yarn link # With NPM: `npm link` 28 | ``` 29 | 30 | 7. Watch all packages (create-graphql and generator) 31 | ```sh 32 | yarn watch # With NPM: `npm run watch` 33 | ``` 34 | 35 | 8. Create a new branch 36 | ```sh 37 | git checkout -b feature/more_awesomeness 38 | ``` 39 | 40 | 9. Make your changes 41 | 10. Run the CLI with your changes 42 | ```sh 43 | node packages/create-graphql/dist --help 44 | ``` 45 | 46 | 11. Commit your changes and push your branch 47 | ```sh 48 | git add . 49 | git commit -m 'more awesome for create-graphql' 50 | git push origin feature/more_awesomeness 51 | ``` 52 | 53 | 12. Open your Pull Request 54 | 13. Have your Pull Request merged! 😎 55 | -------------------------------------------------------------------------------- /packages/generator/src/view/templates/View.js.template: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Relay from 'react-relay'; 3 | import { withRouter } from 'react-router'; 4 | 5 | import <%= name %>Edit from './<%= name %>Edit'; 6 | 7 | import Tabs from '../../common/Tabs'; 8 | 9 | class <%= name %>View extends Component { 10 | render() { 11 | const { viewer } = this.props; 12 | const { <%= camelCaseName %> } = viewer; 13 | 14 | const tabs = [{ 15 | label: 'Details', 16 | component: ( 17 | <<%= name %>Edit 18 | <%= camelCaseName %>={<%= camelCaseName %>} 19 | viewer={viewer} 20 | /> 21 | ), 22 | icon: 'assignment', 23 | }]; 24 | 25 | return ( 26 |
27 |

<%= name %>: {<%= camelCaseName %>.id}

28 | 29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | const styles = { 36 | title: { 37 | fontSize: 25, 38 | fontWeight: 300, 39 | }, 40 | }; 41 | 42 | export default Relay.createContainer(withRouter(<%= name %>View), { 43 | initialVariables: { 44 | id: null, 45 | }, 46 | fragments: { 47 | viewer: ({ id }) => Relay.QL` 48 | fragment on Viewer { 49 | <%= camelCaseName %>(id: $id) { 50 | id 51 | ${<%= name %>Edit.getFragment('<%= camelCaseName %>')} 52 | } 53 | ${<%= name %>Edit.getFragment('viewer')} 54 | } 55 | `, 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /packages/generator/src/view/__tests__/__snapshots__/ViewGenerator.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generate view files 1`] = ` 4 | Object { 5 | "view": "import React, { Component } from 'react'; 6 | import Relay from 'react-relay'; 7 | import { withRouter } from 'react-router'; 8 | 9 | import ExampleEdit from './ExampleEdit'; 10 | 11 | import Tabs from '../../common/Tabs'; 12 | 13 | class ExampleView extends Component { 14 | render() { 15 | const { viewer } = this.props; 16 | const { example } = viewer; 17 | 18 | const tabs = [{ 19 | label: 'Details', 20 | component: ( 21 | 25 | ), 26 | icon: 'assignment', 27 | }]; 28 | 29 | return ( 30 |
31 |

Example: {example.id}

32 | 33 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | const styles = { 40 | title: { 41 | fontSize: 25, 42 | fontWeight: 300, 43 | }, 44 | }; 45 | 46 | export default Relay.createContainer(withRouter(ExampleView), { 47 | initialVariables: { 48 | id: null, 49 | }, 50 | fragments: { 51 | viewer: ({ id }) => Relay.QL\` 52 | fragment on Viewer { 53 | example(id: $id) { 54 | id 55 | \${ExampleEdit.getFragment('example')} 56 | } 57 | \${ExampleEdit.getFragment('viewer')} 58 | } 59 | \`, 60 | }, 61 | }); 62 | ", 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /packages/create-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@entria/create-graphql", 3 | "description": "Create production-ready GraphQL servers", 4 | "version": "0.0.16", 5 | "author": { 6 | "name": "Entria", 7 | "url": "https://github.com/entria" 8 | }, 9 | "bin": { 10 | "entria-graphql": "./bin/entria-graphql" 11 | }, 12 | "bugs": "https://github.com/entria/create-graphql/issues", 13 | "dependencies": { 14 | "babel-polyfill": "^6.26.0", 15 | "chalk": "^2.4.1", 16 | "commander": "^2.15.1", 17 | "cross-spawn": "^6.0.5", 18 | "cross-spawn-promise": "^0.10.1", 19 | "ora": "^2.1.0", 20 | "shelljs": "^0.8.2", 21 | "yeoman-generator": "^2.0.5", 22 | "yo": "^2.0.2" 23 | }, 24 | "homepage": "https://github.com/entria/create-graphql#readme", 25 | "keywords": [ 26 | "apollo", 27 | "create", 28 | "generator", 29 | "graphql", 30 | "koa", 31 | "relay", 32 | "server", 33 | "yeoman", 34 | "yo" 35 | ], 36 | "license": "MIT", 37 | "preferGlobal": true, 38 | "publishConfig": { 39 | "access": "public" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "http://github.com/entria/create-graphql" 44 | }, 45 | "scripts": { 46 | "build": "npm run clear && babel src --ignore *.spec.js --out-dir dist --copy-files", 47 | "clear": "rimraf ./dist", 48 | "lint": "./node_modules/.bin/eslint -c .eslintrc ./src", 49 | "prepare": "npm run build", 50 | "prepublish": "check-node-version --npm \">=4\" || npm run build", 51 | "watch": "babel -w -d ./dist ./src" 52 | }, 53 | "devDependencies": { 54 | "check-node-version": "^3.2.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/generator/src/add/templates/AddMutation.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { commitMutation, graphql } from 'react-relay/compat'; 3 | 4 | import RelayStore from '../../relay/RelayStore'; 5 | 6 | import type { 7 | <%= name %>AddMutationVariables, 8 | <%= name %>AddMutationResponse, 9 | } from './__generated__/<%= name %>AddMutation.graphql'; 10 | 11 | type <%= name %>AddMutationInput = $PropertyType< 12 | <%= name %>AddMutationVariables, 13 | 'input', 14 | >; 15 | 16 | // relay classic 17 | const getConfigs = viewerId => { 18 | return [ 19 | { 20 | type: 'RANGE_ADD', 21 | parentName: 'viewer', 22 | parentID: viewerId, 23 | connectionName: '<%= pluralName.toLowerCase() %>', 24 | edgeName: '<%= name.toLowerCase() %>Edge', 25 | rangeBehaviors: () => { 26 | return 'prepend'; 27 | }, 28 | }, 29 | ]; 30 | }; 31 | 32 | const mutation = graphql` 33 | mutation <%= name %>AddMutation($input: <%= name %>AddInput!) { 34 | <%= name %>Add(input: $input) { 35 | <%= name.toLowerCase() %>Edge { 36 | __typename 37 | cursor 38 | node { 39 | __typename 40 | id 41 | } 42 | } 43 | error 44 | } 45 | } 46 | `; 47 | 48 | const commit = ( 49 | viewerId: string, 50 | input: <%= name %>AddMutationInput, 51 | onCompleted: (response: <%= name %>AddMutationResponse) => void, 52 | onError: (error: Error) => void, 53 | ) => { 54 | const variables = { 55 | input, 56 | }; 57 | 58 | commitMutation(RelayStore._env, { 59 | mutation, 60 | variables, 61 | onCompleted, 62 | onError, 63 | configs: getConfigs(viewerId), 64 | }); 65 | }; 66 | 67 | export default { commit }; 68 | 69 | -------------------------------------------------------------------------------- /packages/create-graphql/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import 'babel-polyfill'; 3 | 4 | import program from 'commander'; 5 | 6 | import pkg from '../package.json'; 7 | import { 8 | init, 9 | generate, 10 | frontend, 11 | } from './commands'; 12 | import { verifyYeoman } from './utils'; 13 | 14 | program 15 | .version(pkg.version); 16 | 17 | program 18 | .command('init ') 19 | .alias('i') 20 | .description('Create a new GraphQL project') 21 | .action(async (project) => { 22 | await verifyYeoman(); 23 | 24 | init(project); 25 | }); 26 | 27 | program 28 | .command('generate ') 29 | .alias('g') 30 | .option('-t, --type', 'Generate a new Type') 31 | .option('-l, --loader', 'Generate a new Loader') 32 | .option('-c, --connection', 'Generate a new Connection') 33 | .option('-m, --mutation', 'Generate a new Mutation') 34 | .option('--schema ', 'Generate from a Mongoose Schema') 35 | .description('Generate a new file (Type, Loader, Mutation, etc)') 36 | .action(async (name, options) => { 37 | await verifyYeoman(); 38 | 39 | generate(name, options); 40 | }); 41 | 42 | program 43 | .command('frontend ') 44 | .alias('f') 45 | .option('-a, --add', 'Generate a new Add Form screen') 46 | .option('-e, --edit', 'Generate a new Edit Form screen') 47 | .option('-l, --list', 'Generate a new List screen') 48 | .option('-v, --view', 'Generate a new View for an ObjectType') 49 | .description('Generate a new frontend file (Add, Edit, List, View)') 50 | .action(async (name, options) => { 51 | await verifyYeoman(); 52 | 53 | frontend(name, options); 54 | }); 55 | 56 | program.parse(process.argv); 57 | 58 | if (process.argv.length <= 2) { 59 | program.help(); 60 | } 61 | -------------------------------------------------------------------------------- /packages/generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@entria/generator-graphql", 3 | "description": "Create production-ready GraphQL servers", 4 | "version": "0.0.16", 5 | "author": { 6 | "name": "Entria", 7 | "url": "https://github.com/entria" 8 | }, 9 | "bugs": "https://github.com/entria/create-graphql/issues", 10 | "dependencies": { 11 | "babel-polyfill": "^6.26.0", 12 | "babylon": "^6.18.0", 13 | "chalk": "^2.4.1", 14 | "colors": "^1.3.0", 15 | "fast-glob": "^2.2.2", 16 | "lodash.merge": "^4.6.1", 17 | "ora": "^2.1.0", 18 | "pkg-dir": "^2.0.0", 19 | "pluralize": "^7.0.0", 20 | "ramda": "^0.25.0", 21 | "recast": "^0.14.7", 22 | "relative": "^3.0.2", 23 | "semver": "^5.5.0", 24 | "shelljs": "^0.8.2", 25 | "yeoman-generator": "^2.0.5" 26 | }, 27 | "homepage": "https://github.com/entria/create-graphql#readme", 28 | "keywords": [ 29 | "apollo", 30 | "create", 31 | "generator", 32 | "graphql", 33 | "koa", 34 | "relay", 35 | "server", 36 | "yeoman", 37 | "yeoman-generator", 38 | "yo" 39 | ], 40 | "license": "MIT", 41 | "preferGlobal": true, 42 | "publishConfig": { 43 | "access": "public" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/entria/create-graphql" 48 | }, 49 | "scripts": { 50 | "build": "npm run clear && babel src --ignore *.spec.js --out-dir ./generators --copy-files", 51 | "clear": "rimraf ./generators", 52 | "test": "jest", 53 | "prepare": "npm run build", 54 | "prepublish": "check-node-version --npm \">=4\" || npm run build", 55 | "watch": "babel -w -d ./generators ./src" 56 | }, 57 | "devDependencies": { 58 | "check-node-version": "^3.2.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/templates/test/MutationAdd.js.template: -------------------------------------------------------------------------------- 1 | import { graphql } from 'graphql'; 2 | import { schema } from '../../../../graphql/schema'; 3 | import { 4 | getContext, 5 | connectMongoose, 6 | clearDbAndRestartCounters, 7 | disconnectMongoose, 8 | } from '../../../../../test/helper'; 9 | 10 | import { 11 | User, 12 | <%= name %>, 13 | } from '../../../../models'; 14 | 15 | beforeAll(connectMongoose); 16 | 17 | beforeEach(clearDbAndRestartCounters); 18 | 19 | afterAll(disconnectMongoose); 20 | 21 | it('should not allow anonymous user', async () => { 22 | //language=GraphQL 23 | const query = ` 24 | mutation M($example: String) { 25 | <%= mutationName %>(input: { 26 | example: $example 27 | }) { 28 | exampleFieldToRetrieve 29 | } 30 | } 31 | `; 32 | 33 | const variables = { 34 | }; 35 | const rootValue = {}; 36 | const context = getContext(); 37 | 38 | const result = await graphql(schema, query, rootValue, context, variables); 39 | 40 | expect(result).toMatchSnapshot(); 41 | }); 42 | 43 | it('should create a record on database', async () => { 44 | const user = new User({ 45 | name: 'user', 46 | email: 'user@example.com', 47 | }); 48 | 49 | await user.save(); 50 | 51 | //language=GraphQL 52 | const query = ` 53 | mutation M($example: String) { 54 | <%= mutationName %>(input: { 55 | example: $example 56 | }) { 57 | exampleFieldToRetrieve 58 | } 59 | } 60 | `; 61 | 62 | const variables = { 63 | }; 64 | const rootValue = {}; 65 | const context = getContext({ user }); 66 | 67 | const result = await graphql(schema, query, rootValue, context, variables); 68 | 69 | expect(result).toMatchSnapshot(); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/templates/MutationAdd.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | GraphQLString, 4 | GraphQLNonNull, 5 | } from 'graphql'; 6 | import { 7 | mutationWithClientMutationId, 8 | toGlobalId, 9 | } from 'graphql-relay'; 10 | 11 | import <%= name %>Model from '../<%= name %>Model'; 12 | 13 | import * as <%= name %>Loader from '../<%= name %>Loader'; 14 | import <%= name %>Connection from '../<%= name %>Connection'; 15 | 16 | export default mutationWithClientMutationId({ 17 | name: '<%= name %>Add', 18 | inputFields: { 19 | example: { 20 | type: new GraphQLNonNull(GraphQLString), 21 | description: 'My example field', 22 | }, 23 | }, 24 | mutateAndGetPayload: async ({ example }, context) => { 25 | // Verify if user is authorized 26 | if (!context.user) { 27 | throw new Error('Unauthorized user'); 28 | } 29 | 30 | // TODO: mutation logic 31 | 32 | return { 33 | // id: id, // ID of the newly created row 34 | error: null, 35 | }; 36 | }, 37 | outputFields: { 38 | <%= camelCaseName %>Edge: { 39 | type: <%= name %>Connection.edgeType, 40 | resolve: async ({ id }, args, context) => { 41 | // Load new edge from loader 42 | const <%= camelCaseName %> = await <%= name %>Loader.load( 43 | context, id, 44 | ); 45 | 46 | // Returns null if no node was loaded 47 | if (!<%= camelCaseName %>) { 48 | return null; 49 | } 50 | 51 | return { 52 | cursor: toGlobalId('<%= name %>', <%= camelCaseName %>._id), 53 | node: <%= camelCaseName %>, 54 | }; 55 | }, 56 | }, 57 | error: { 58 | type: GraphQLString, 59 | resolve: ({ error }) => error, 60 | }, 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /packages/generator/src/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import { getCreateGraphQLConfig } from './config'; 5 | import { getSchemaDefinition } from './parser/mongoose'; 6 | 7 | /** 8 | * Get the relative path directory between two directories specified on the config file 9 | * @param from {string} The calling directory of the script 10 | * @param to {[string]} The destination directories 11 | * @returns {string} The relative path, e.g. '../../src' 12 | */ 13 | export const getRelativeConfigDir = (from: string, to: string[]) => { 14 | const config = getCreateGraphQLConfig().directories; 15 | 16 | return to.reduce((directories, dir) => { 17 | const relativePath = path.posix.relative(config[from], config[dir]); 18 | 19 | return { 20 | ...directories, 21 | [dir]: relativePath === '' ? '.' : relativePath, 22 | }; 23 | }, {}); 24 | }; 25 | 26 | /** 27 | * Get the Mongoose model schema code 28 | * @param modelPath {string} The path of the Mongoose model 29 | * @returns {string} The code of the Mongoose model 30 | */ 31 | const getModelCode = (modelPath: string) => fs.readFileSync(modelPath, 'utf8'); 32 | 33 | type MongooseModelSchemaOptions = { 34 | model: string, 35 | withTimestamps: boolean, 36 | ref: boolean, 37 | } 38 | export const getMongooseModelSchema = ({ 39 | model, 40 | withTimestamps = false, 41 | ref = false, 42 | }: MongooseModelSchemaOptions) => { 43 | const config = getCreateGraphQLConfig(); 44 | 45 | const modelDir = config.directories.model; 46 | 47 | const modelPath = path.resolve(`${modelDir}/${model.toLowerCase()}/${model}Model.js`); 48 | 49 | const modelCode = getModelCode(modelPath); 50 | 51 | return getSchemaDefinition(modelCode, withTimestamps, ref); 52 | }; 53 | 54 | 55 | -------------------------------------------------------------------------------- /packages/generator/src/form/index.js: -------------------------------------------------------------------------------- 1 | import Generator from 'yeoman-generator'; 2 | import pluralize from 'pluralize'; 3 | import { 4 | getRelativeConfigDir, 5 | } from '../utils'; 6 | 7 | import { getConfigDir } from '../config'; 8 | import { camelCaseText, uppercaseFirstLetter } from '../ejsHelpers'; 9 | 10 | class AddGenerator extends Generator { 11 | constructor(args, options) { 12 | super(args, options); 13 | 14 | this.argument('name', { 15 | type: String, 16 | required: true, 17 | }); 18 | 19 | // TODO read schema.json 20 | 21 | this.destinationDir = getConfigDir('add'); 22 | } 23 | 24 | _getConfigDirectories() { 25 | return getRelativeConfigDir('loader', ['model', 'connection']); 26 | } 27 | 28 | generateList() { 29 | // const schema = this.options.model ? 30 | // getMongooseModelSchema(this.options.model, true) 31 | // : null; 32 | 33 | const name = uppercaseFirstLetter(this.options.name); 34 | 35 | // const templatePath = schema ? 36 | // this.templatePath('LoaderWithSchema.js.template') 37 | // : this.templatePath('Loader.js.template'); 38 | // 39 | // const directories = this._getConfigDirectories(); 40 | 41 | const pluralName = pluralize(this.options.name); 42 | 43 | const templateVars = { 44 | name, 45 | rawName: this.options.name, 46 | camelCaseName: camelCaseText(name), 47 | pluralName, 48 | pluralCamelCaseName: camelCaseText(pluralName), 49 | }; 50 | 51 | const filename = `${name}Form.js`; 52 | 53 | this.fs.copyTpl( 54 | this.templatePath('Form.js.template'), `${this.destinationDir}/${filename}`, templateVars, 55 | ); 56 | } 57 | 58 | end() { 59 | this.log('🔥 Form created!'); 60 | } 61 | } 62 | 63 | module.exports = AddGenerator; 64 | -------------------------------------------------------------------------------- /packages/generator/src/loader/templates/Loader.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import DataLoader from 'dataloader'; 3 | import { connectionFromMongoCursor, mongooseLoader } from '@entria/graphql-mongoose-loader'; 4 | import type { ConnectionArguments } from 'graphql-relay'; 5 | 6 | import <%= name %>Model from './<%= name %>Model'; 7 | import type { GraphQLContext } from '../../TypeDefinition'; 8 | 9 | type <%= name %>Type = { 10 | id: string, 11 | _id: string, 12 | exampleField: string, 13 | } 14 | 15 | export default class <%= name %> { 16 | id: string; 17 | _id: string; 18 | exampleField: string; 19 | 20 | constructor(data: <%= name %>Type) { 21 | this.id = data.id; 22 | this._id = data._id; 23 | this.exampleField = data.exampleField; 24 | } 25 | } 26 | 27 | export const getLoader = () => new DataLoader(ids => mongooseLoader(<%= name %>Model, ids)); 28 | 29 | const viewerCanSee = () => true; 30 | 31 | export const load = async ({ dataloaders }: GraphQLContext, id: ?string) => { 32 | if (!id) return null; 33 | 34 | try { 35 | const data = await dataloaders.<%= rawName %>Loader.load(id.toString()); 36 | 37 | if (!data) return null; 38 | 39 | return viewerCanSee() ? new <%= name %>(data) : null; 40 | } catch (err) { 41 | return null; 42 | } 43 | }; 44 | 45 | export const clearCache = ({ dataloaders }: GraphQLContext, id: string) => { 46 | return dataloaders.<%= rawName %>Loader.clear(id.toString()); 47 | }; 48 | 49 | export const load<%= pluralName %> = async (context: GraphQLContext, args: ConnectionArguments) => { 50 | // TODO: specify conditions 51 | const <%= pluralCamelCaseName %> = <%= name %>Model.find({}); 52 | 53 | return connectionFromMongoCursor({ 54 | cursor: <%= pluralCamelCaseName %>, 55 | context, 56 | args, 57 | loader: load, 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/templates/MutationEdit.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | GraphQLID, 4 | GraphQLString, 5 | GraphQLNonNull, 6 | } from 'graphql'; 7 | import { 8 | mutationWithClientMutationId, 9 | fromGlobalId, 10 | } from 'graphql-relay'; 11 | 12 | import <%= name %>Model from '../<%= name %>Model'; 13 | 14 | import <%= name %>Type from '../<%= name %>Type'; 15 | import * as <%= name %>Loader from '../<%= name %>Loader'; 16 | 17 | export default mutationWithClientMutationId({ 18 | name: '<%= name %>Edit', 19 | inputFields: { 20 | id: { 21 | type: new GraphQLNonNull(GraphQLID), 22 | }, 23 | example: { 24 | type: GraphQLString, 25 | }, 26 | }, 27 | mutateAndGetPayload: async (args, context) => { 28 | // Verify if user is authorized 29 | if (!context.user) { 30 | throw new Error('Unauthorized user'); 31 | } 32 | 33 | const { 34 | id, 35 | example, 36 | } = args; 37 | 38 | // Check if the provided ID is valid 39 | const <%= camelCaseName %> = await <%= name %>Model.findOne({ 40 | _id: fromGlobalId(id).id, 41 | }); 42 | 43 | // If not, throw an error 44 | if (!<%= camelCaseName %>) { 45 | throw new Error('Invalid <%= camelCaseName %>Id'); 46 | } 47 | 48 | // TODO: mutation logic 49 | 50 | // Clear dataloader cache 51 | <%= name %>Loader.clearCache(context, <%= camelCaseName %>._id); 52 | 53 | return { 54 | id: <%= camelCaseName %>._id, 55 | error: null, 56 | }; 57 | }, 58 | outputFields: { 59 | <%= camelCaseName %>: { 60 | type: <%= name %>Type, 61 | resolve: (obj, args, context) => <%= name %>Loader.load(context, obj.id), 62 | }, 63 | error: { 64 | type: GraphQLString, 65 | resolve: ({ error }) => error, 66 | }, 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /packages/generator/src/view/index.js: -------------------------------------------------------------------------------- 1 | import Generator from 'yeoman-generator'; 2 | import pluralize from 'pluralize'; 3 | import { 4 | getRelativeConfigDir, 5 | } from '../utils'; 6 | import { getConfigDir } from '../config'; 7 | import { camelCaseText, uppercaseFirstLetter } from '../ejsHelpers'; 8 | 9 | class ViewGenerator extends Generator { 10 | constructor(args, options) { 11 | super(args, options); 12 | 13 | this.argument('name', { 14 | type: String, 15 | required: true, 16 | }); 17 | 18 | // TODO read schema.json 19 | 20 | this.destinationDir = getConfigDir('view'); 21 | } 22 | 23 | _getConfigDirectories() { 24 | return getRelativeConfigDir('loader', ['model', 'connection']); 25 | } 26 | 27 | generateList() { 28 | // const schema = this.options.model ? 29 | // getMongooseModelSchema(this.options.model, true) 30 | // : null; 31 | 32 | const name = uppercaseFirstLetter(this.options.name); 33 | 34 | const templatePath = this.templatePath('View.js.template'); 35 | 36 | // const templatePath = schema ? 37 | // this.templatePath('LoaderWithSchema.js.template') 38 | // : this.templatePath('Loader.js.template'); 39 | // 40 | // const directories = this._getConfigDirectories(); 41 | 42 | const pluralName = pluralize(this.options.name); 43 | 44 | const destinationPath = this.destinationPath(`${this.destinationDir}/${name}View.js`); 45 | const templateVars = { 46 | name, 47 | rawName: this.options.name, 48 | camelCaseName: camelCaseText(name), 49 | pluralName, 50 | pluralCamelCaseName: camelCaseText(pluralName), 51 | }; 52 | 53 | this.fs.copyTpl(templatePath, destinationPath, templateVars); 54 | } 55 | 56 | end() { 57 | this.log('🔥 View created!'); 58 | } 59 | } 60 | 61 | module.exports = ViewGenerator; 62 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/templates/test/MutationEdit.js.template: -------------------------------------------------------------------------------- 1 | import { graphql } from 'graphql'; 2 | import { toGlobalId } from 'graphql-relay'; 3 | import { schema } from '../../../../graphql/schema'; 4 | import { 5 | getContext, 6 | connectMongoose, 7 | clearDbAndRestartCounters, 8 | disconnectMongoose, 9 | } from '../../../../../test/helper'; 10 | 11 | import { 12 | User, 13 | <%= name %>, 14 | } from '../../../../models'; 15 | 16 | beforeAll(connectMongoose); 17 | 18 | beforeEach(clearDbAndRestartCounters); 19 | 20 | afterAll(disconnectMongoose); 21 | 22 | it('should not allow anonymous user', async () => { 23 | //language=GraphQL 24 | const query = ` 25 | mutation M { 26 | <%= mutationName %>(input: { 27 | id: "Example Id" 28 | exampleField: "Example field" 29 | }) { 30 | exampleFieldToRetrieve 31 | } 32 | } 33 | `; 34 | 35 | const variables = { 36 | }; 37 | const rootValue = {}; 38 | const context = getContext(); 39 | 40 | const result = await graphql(schema, query, rootValue, context, variables); 41 | 42 | expect(result).toMatchSnapshot(); 43 | }); 44 | 45 | it('should create a record on database', async () => { 46 | const user = new User({ 47 | name: 'user', 48 | email: 'user@example.com', 49 | }); 50 | 51 | await user.save(); 52 | 53 | //language=GraphQL 54 | const query = ` 55 | mutation M { 56 | <%= mutationName %>(input: { 57 | id: "Example Id" 58 | exampleField: "Example field" 59 | }) { 60 | exampleFieldToRetrieve 61 | } 62 | } 63 | `; 64 | 65 | const variables = { 66 | }; 67 | const rootValue = {}; 68 | const context = getContext({ user }); 69 | 70 | const result = await graphql(schema, query, rootValue, context, variables); 71 | 72 | expect(result).toMatchSnapshot(); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/generator/test/helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from 'fs-extra'; 3 | import path from 'path'; 4 | import { uppercaseFirstLetter } from '../src/ejsHelpers'; 5 | 6 | /** 7 | * Get the content of a file 8 | * @param path {string} The path of the file 9 | * @returns {string} The content of the file 10 | */ 11 | export const getFileContent = (pathname: string) => fs.readFileSync(pathname, 'utf8'); 12 | 13 | /** 14 | * Get a fixture path 15 | * @param name {string} Name of the file of the fixture 16 | * @returns {string} The path of the fixture 17 | */ 18 | export const getFixturePath = (name: string) => path.join(__dirname, `../fixtures/${name}.js`); 19 | 20 | const FIXTURES_MODULES = [ 21 | 'adminUser', 22 | 'comment', 23 | 'post', 24 | 'user', 25 | ]; 26 | 27 | const getModulePath = (module: string) => `src/modules/${module}`; 28 | const getModelName = (name: string) => `${name}Model.js`; 29 | const getTypeName = (name: string) => `${name}Type.js`; 30 | const getLoaderName = (name: string) => `${name}Loader.js`; 31 | const getConnectionName = (name: string) => `${name}Connection.js`; 32 | 33 | const GENERATED_FILES = [ 34 | getModelName, 35 | getTypeName, 36 | getLoaderName, 37 | getConnectionName, 38 | ]; 39 | 40 | export const copyFixturesToModules = (dir: string, moduleName?: string) => { 41 | FIXTURES_MODULES.forEach((module) => { 42 | const name = uppercaseFirstLetter(module); 43 | const modulePath = getModulePath(module); 44 | 45 | // Generate only Model, the other files will be generated by generators 46 | if (module === moduleName) { 47 | fs.copySync( 48 | getFixturePath(name), 49 | path.join(dir, modulePath, getModelName(name)), 50 | ); 51 | return; 52 | } 53 | 54 | GENERATED_FILES.forEach((getFilename) => { 55 | fs.copySync( 56 | getFixturePath(name), 57 | path.join(dir, modulePath, getFilename(name)), 58 | ); 59 | }); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "entria", 4 | "url": "https://github.com/entria" 5 | }, 6 | "devDependencies": { 7 | "babel-cli": "^6.26.0", 8 | "babel-core": "^6.26.3", 9 | "babel-eslint": "^8.2.3", 10 | "babel-jest": "^23.0.1", 11 | "babel-plugin-add-module-exports": "^0.2.1", 12 | "babel-plugin-rewire": "^1.1.0", 13 | "babel-plugin-transform-class-properties": "^6.24.1", 14 | "babel-plugin-transform-export-extensions": "^6.22.0", 15 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 16 | "babel-preset-env": "^1.7.0", 17 | "babel-preset-flow": "^6.23.0", 18 | "check-node-version": "^3.2.0", 19 | "chokidar": "^2.0.3", 20 | "ejs-lint": "^0.3.0", 21 | "eslint": "^4.19.0", 22 | "eslint-config-airbnb": "^16.1.0", 23 | "eslint-plugin-flowtype": "^2.49.3", 24 | "eslint-plugin-import": "^2.12.0", 25 | "flow-mono-cli": "^1.3.1", 26 | "fs-extra": "^6.0.1", 27 | "husky": "^0.14.3", 28 | "jest": "^23.1.0", 29 | "jest-cli": "23.1.0", 30 | "lerna": "2.0.0-beta.30", 31 | "lerna-changelog": "^0.7.0", 32 | "lint-staged": "^7.1.3", 33 | "prettier": "^1.13.4", 34 | "rimraf": "^2.6.2", 35 | "yeoman-assert": "^3.1.1", 36 | "yeoman-test": "^1.7.2" 37 | }, 38 | "lint-staged": { 39 | "packages/*/src/**/*.js": [ 40 | "yarn prettier", 41 | "eslint --fix", 42 | "git add" 43 | ] 44 | }, 45 | "pre-commit": "lint:staged", 46 | "license": "MIT", 47 | "scripts": { 48 | "bootstrap": "lerna bootstrap --concurrency=1", 49 | "build": "lerna exec -- npm run build", 50 | "clean": "lerna clean", 51 | "lint": "eslint \"packages/*/src/**/*.js\" \"packages/*/__tests__\"", 52 | "lint:staged": "lint-staged", 53 | "prepare": "npm run build", 54 | "prepublish": "check-node-version --npm \">=4\" || npm run build", 55 | "test": "jest --config jest.json", 56 | "watch": "lerna exec -- npm run watch" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/generator/src/connection/__tests__/ConnectionGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | copyFixturesToModules, 7 | getFileContent, 8 | } from '../../../test/helpers'; 9 | 10 | import { getConfigDir } from '../../config'; 11 | import { uppercaseFirstLetter } from '../../ejsHelpers'; 12 | import { getModulePath } from '../../paths'; 13 | 14 | const connectionGenerator = path.join(__dirname, '..'); 15 | 16 | it('generate a connection', async () => { 17 | const moduleName = 'example'; 18 | const name = uppercaseFirstLetter(moduleName); 19 | 20 | const folder = await helper.run(connectionGenerator) 21 | .withArguments(name) 22 | .toPromise(); 23 | 24 | const destinationDir = getConfigDir('connection'); 25 | 26 | const modulePath = getModulePath(destinationDir, moduleName); 27 | const connectionFilepath = path.join(modulePath, `${name}Connection.js`); 28 | 29 | assert.file([ 30 | connectionFilepath, 31 | ]); 32 | 33 | const files = { 34 | connection: getFileContent(path.join(folder, connectionFilepath)), 35 | }; 36 | 37 | expect(files).toMatchSnapshot(); 38 | }); 39 | 40 | it('generate a connection with schema', async () => { 41 | const moduleName = 'post'; 42 | const name = uppercaseFirstLetter(moduleName); 43 | 44 | const folder = await helper.run(connectionGenerator) 45 | .inTmpDir(dir => copyFixturesToModules(dir, moduleName)) 46 | .withArguments('Post Post') 47 | .toPromise(); 48 | 49 | const destinationDir = getConfigDir('connection'); 50 | const modulePath = getModulePath(destinationDir, moduleName); 51 | const connectionFilepath = path.join(modulePath, `${name}Connection.js`); 52 | 53 | assert.file([ 54 | connectionFilepath, 55 | ]); 56 | 57 | const files = { 58 | connection: getFileContent(path.join(folder, connectionFilepath)), 59 | }; 60 | 61 | expect(files).toMatchSnapshot(); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/create-graphql/src/commands/generate.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import spawn from 'cross-spawn-promise'; 3 | 4 | function parseOptions(opts) { 5 | const availableOptions = ['type', 'loader', 'connection', 'mutation']; 6 | 7 | // Check if any commands was provided 8 | const anyCommandsProvided = Object.keys(opts).some(option => 9 | availableOptions.indexOf(option) !== -1, 10 | ); 11 | 12 | let options = opts; 13 | 14 | // If not, use the default options 15 | if (!anyCommandsProvided) { 16 | options = { 17 | type: true, 18 | loader: true, 19 | connection: true, 20 | mutation: true, 21 | ...options, 22 | }; 23 | } 24 | 25 | const { 26 | type, 27 | loader, 28 | connection, 29 | mutation, 30 | schema, 31 | } = options; 32 | 33 | return { 34 | type: type || false, 35 | loader: loader || false, 36 | connection: connection || false, 37 | mutation: mutation || false, 38 | schema: schema || false, 39 | }; 40 | } 41 | 42 | function generate(name, options) { 43 | // Parse all arguments 44 | const parsedOptions = parseOptions(options); 45 | // Get only the chose arguments 46 | const chosenOptions = Object.keys(parsedOptions).filter(opt => !!parsedOptions[opt]); 47 | 48 | // Check if schema argument has been passed 49 | const schemaIndex = chosenOptions.indexOf('schema'); 50 | chosenOptions.forEach(async (option) => { 51 | const payload = [`@entria/graphql:${option}`, name]; 52 | 53 | // If argument schema exists 54 | if (schemaIndex !== -1) { 55 | // Remove the next running option because the schema must be used along with this command 56 | chosenOptions.splice(schemaIndex, 1); 57 | 58 | // Push schema to the arguments to send to yeoman 59 | payload.push(parsedOptions.schema); 60 | } 61 | 62 | await spawn('yo', payload, { 63 | shell: true, 64 | stdio: 'inherit', 65 | }); 66 | }); 67 | } 68 | 69 | export default generate; 70 | -------------------------------------------------------------------------------- /packages/create-graphql/src/commands/frontend.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import spawn from 'cross-spawn-promise'; 3 | 4 | function parseOptions(opts) { 5 | const availableOptions = ['add', 'edit', 'list', 'view', 'form']; 6 | 7 | // Check if any commands was provided 8 | const anyCommandsProvided = Object.keys(opts).some(option => 9 | availableOptions.indexOf(option) !== -1, 10 | ); 11 | 12 | let options = opts; 13 | 14 | // If not, use the default options 15 | if (!anyCommandsProvided) { 16 | options = { 17 | add: true, 18 | edit: true, 19 | list: true, 20 | view: true, 21 | form: true, 22 | ...options, 23 | }; 24 | } 25 | 26 | const { 27 | add, 28 | edit, 29 | list, 30 | view, 31 | form, 32 | schema, 33 | } = options; 34 | 35 | return { 36 | add: add || false, 37 | edit: edit || false, 38 | list: list || false, 39 | view: view || false, 40 | form: form || false, 41 | schema: schema || false, 42 | }; 43 | } 44 | 45 | const generate = (name, options) => { 46 | // Parse all arguments 47 | const parsedOptions = parseOptions(options); 48 | // Get only the chose arguments 49 | const chosenOptions = Object.keys(parsedOptions).filter(opt => !!parsedOptions[opt]); 50 | 51 | // Check if schema argument has been passed 52 | const schemaIndex = chosenOptions.indexOf('schema'); 53 | chosenOptions.forEach(async (option) => { 54 | const payload = [`@entria/graphql:${option}`, name]; 55 | 56 | // If argument schema exists 57 | if (schemaIndex !== -1) { 58 | // Remove the next running option because the schema must be used along with this command 59 | chosenOptions.splice(schemaIndex, 1); 60 | 61 | // Push schema to the arguments to send to yeoman 62 | payload.push(parsedOptions.schema); 63 | } 64 | 65 | await spawn('yo', payload, { 66 | shell: true, 67 | stdio: 'inherit', 68 | }); 69 | }); 70 | } 71 | 72 | export default generate; 73 | -------------------------------------------------------------------------------- /packages/generator/src/loader/templates/LoaderWithSchema.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import DataLoader from 'dataloader'; 3 | import { connectionFromMongoCursor, mongooseLoader } from '@entria/graphql-mongoose-loader'; 4 | import type { ConnectionArguments } from 'graphql-relay'; 5 | 6 | import <%= name %>Model from './<%= name %>Model'; 7 | import type { GraphQLContext } from '../../TypeDefinition'; 8 | 9 | type <%= name %>Type = { 10 | id: string, 11 | _id: string, 12 | <%_ for (field of schema.fields) { -%> 13 | <%- field.name + ': ' + field.flowType -%>, 14 | <%_ } -%> 15 | } 16 | 17 | export default class <%= name %> { 18 | id: string; 19 | _id: string; 20 | <%_ for (field of schema.fields) { -%> 21 | <%- field.name + ': ' + field.flowType -%>; 22 | <%_ } -%> 23 | 24 | constructor(data: <%= name %>Type) { 25 | this.id = data.id; 26 | this._id = data._id; 27 | <%_ for (field of schema.fields) { -%> 28 | this.<%- field.name -%> = data.<%- field.name -%>; 29 | <%_ } -%> 30 | } 31 | } 32 | 33 | export const getLoader = () => new DataLoader(ids => mongooseLoader(<%= name %>Model, ids)); 34 | 35 | const viewerCanSee = () => true; 36 | 37 | export const load = async ({ dataloaders }: GraphQLContext, id: ?string) => { 38 | if (!id) return null; 39 | 40 | try { 41 | const data = await dataloaders.<%= rawName %>Loader.load(id.toString()); 42 | 43 | if (!data) return null; 44 | 45 | return viewerCanSee() ? new <%= name %>(data) : null; 46 | } catch (err) { 47 | return null 48 | }; 49 | }; 50 | 51 | export const clearCache = ({ dataloaders }: GraphQLContext, id: string) => { 52 | return dataloaders.<%= rawName %>Loader.clear(id.toString()); 53 | }; 54 | 55 | export const load<%= pluralName %> = async (context: GraphQLContext, args: ConnectionArguments) => { 56 | // TODO: specify conditions 57 | const <%= pluralCamelCaseName %> = <%= name %>Model.find({}); 58 | 59 | return connectionFromMongoCursor({ 60 | cursor: <%= pluralCamelCaseName %>, 61 | context, 62 | args, 63 | loader: load, 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /packages/generator/src/loader/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Generator from 'yeoman-generator'; 3 | import pluralize from 'pluralize'; 4 | import path from 'path'; 5 | 6 | import { 7 | getMongooseModelSchema, 8 | getRelativeConfigDir, 9 | } from '../utils'; 10 | import { getConfigDir } from '../config'; 11 | import { camelCaseText, uppercaseFirstLetter } from '../ejsHelpers'; 12 | import { getModulePath } from '../paths'; 13 | 14 | class LoaderGenerator extends Generator { 15 | constructor(args, options) { 16 | super(args, options); 17 | 18 | this.argument('name', { 19 | type: String, 20 | required: true, 21 | }); 22 | 23 | this.argument('model', { 24 | type: String, 25 | required: false, 26 | }); 27 | 28 | this.destinationDir = getConfigDir('loader'); 29 | } 30 | 31 | _getConfigDirectories() { 32 | return getRelativeConfigDir('loader', ['model', 'connection']); 33 | } 34 | 35 | generateLoader() { 36 | const schema = this.options.model ? 37 | getMongooseModelSchema({ model: this.options.model, withTimestamps: true }) 38 | : null; 39 | 40 | const name = uppercaseFirstLetter(this.options.name); 41 | 42 | const templatePath = schema ? 43 | this.templatePath('LoaderWithSchema.js.template') 44 | : this.templatePath('Loader.js.template'); 45 | 46 | const directories = this._getConfigDirectories(); 47 | 48 | const pluralName = pluralize(this.options.name); 49 | 50 | const moduleName = this.options.name.toLowerCase(); 51 | const modulePath = getModulePath(this.destinationDir, moduleName); 52 | 53 | const destinationPath = this.destinationPath( 54 | path.join(modulePath, `${name}Loader.js`), 55 | ); 56 | const templateVars = { 57 | name, 58 | rawName: this.options.name, 59 | pluralName, 60 | pluralCamelCaseName: camelCaseText(pluralName), 61 | schema, 62 | directories, 63 | }; 64 | 65 | this.fs.copyTpl(templatePath, destinationPath, templateVars); 66 | } 67 | 68 | end() { 69 | this.log('🔥 Loader created!'); 70 | } 71 | } 72 | 73 | module.exports = LoaderGenerator; 74 | -------------------------------------------------------------------------------- /packages/generator/src/add/index.js: -------------------------------------------------------------------------------- 1 | import Generator from 'yeoman-generator'; 2 | import pluralize from 'pluralize'; 3 | import { 4 | getRelativeConfigDir, 5 | } from '../utils'; 6 | 7 | import { getConfigDir } from '../config'; 8 | import { camelCaseText, uppercaseFirstLetter } from '../ejsHelpers'; 9 | 10 | class AddGenerator extends Generator { 11 | constructor(args, options) { 12 | super(args, options); 13 | 14 | this.argument('name', { 15 | type: String, 16 | required: true, 17 | }); 18 | 19 | // TODO read schema.json 20 | 21 | this.destinationDir = getConfigDir('add'); 22 | } 23 | 24 | _getConfigDirectories() { 25 | return getRelativeConfigDir('loader', ['model', 'connection']); 26 | } 27 | 28 | generateList() { 29 | // const schema = this.options.model ? 30 | // getMongooseModelSchema(this.options.model, true) 31 | // : null; 32 | 33 | const name = uppercaseFirstLetter(this.options.name); 34 | 35 | // const templatePath = schema ? 36 | // this.templatePath('LoaderWithSchema.js.template') 37 | // : this.templatePath('Loader.js.template'); 38 | // 39 | // const directories = this._getConfigDirectories(); 40 | 41 | const pluralName = pluralize(this.options.name); 42 | 43 | const templateVars = { 44 | name, 45 | rawName: this.options.name, 46 | camelCaseName: camelCaseText(name), 47 | pluralName, 48 | pluralCamelCaseName: camelCaseText(pluralName), 49 | }; 50 | 51 | const files = { 52 | add: { 53 | filename: `${name}Add.js`, 54 | template: 'Add.js.template', 55 | }, 56 | addMutation: { 57 | filename: `${name}AddMutation.js`, 58 | template: 'AddMutation.js.template', 59 | }, 60 | }; 61 | 62 | Object.keys(files).forEach((file) => { 63 | const { filename, template } = files[file]; 64 | 65 | this.fs.copyTpl( 66 | this.templatePath(template), `${this.destinationDir}/${filename}`, templateVars, 67 | ); 68 | }); 69 | } 70 | 71 | end() { 72 | this.log('🔥 Add created!'); 73 | } 74 | } 75 | 76 | module.exports = AddGenerator; 77 | -------------------------------------------------------------------------------- /packages/generator/src/edit/index.js: -------------------------------------------------------------------------------- 1 | import Generator from 'yeoman-generator'; 2 | import pluralize from 'pluralize'; 3 | import { 4 | getRelativeConfigDir, 5 | } from '../utils'; 6 | import { getConfigDir } from '../config'; 7 | import { camelCaseText, uppercaseFirstLetter } from '../ejsHelpers'; 8 | 9 | class EditGenerator extends Generator { 10 | constructor(args, options) { 11 | super(args, options); 12 | 13 | this.argument('name', { 14 | type: String, 15 | required: true, 16 | }); 17 | 18 | // TODO read schema.json 19 | this.destinationDir = getConfigDir('edit'); 20 | } 21 | 22 | _getConfigDirectories() { 23 | return getRelativeConfigDir('loader', ['model', 'connection']); 24 | } 25 | 26 | generateList() { 27 | // const schema = this.options.model ? 28 | // getMongooseModelSchema(this.options.model, true) 29 | // : null; 30 | 31 | const name = uppercaseFirstLetter(this.options.name); 32 | 33 | // const templatePath = schema ? 34 | // this.templatePath('LoaderWithSchema.js.template') 35 | // : this.templatePath('Loader.js.template'); 36 | // 37 | // const directories = this._getConfigDirectories(); 38 | 39 | const pluralName = pluralize(this.options.name); 40 | 41 | const templateVars = { 42 | name, 43 | rawName: this.options.name, 44 | camelCaseName: camelCaseText(name), 45 | pluralName, 46 | pluralCamelCaseName: camelCaseText(pluralName), 47 | }; 48 | 49 | const files = { 50 | edit: { 51 | filename: `${name}Edit.js`, 52 | template: 'Edit.js.template', 53 | }, 54 | editMutation: { 55 | filename: `${name}EditMutation.js`, 56 | template: 'EditMutation.js.template', 57 | }, 58 | }; 59 | 60 | Object.keys(files).forEach((file) => { 61 | const { filename, template } = files[file]; 62 | 63 | this.fs.copyTpl( 64 | this.templatePath(template), `${this.destinationDir}/${filename}`, templateVars, 65 | ); 66 | }); 67 | } 68 | 69 | end() { 70 | this.log('🔥 Edit created!'); 71 | } 72 | } 73 | 74 | module.exports = EditGenerator; 75 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/templates/MutationAddWithSchema.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | GraphQLString, 4 | <%_ for (i in schema.addDependencies) { -%> 5 | <%= schema.addDependencies[i] %>, 6 | <%_ } -%> 7 | } from 'graphql'; 8 | import { 9 | mutationWithClientMutationId, 10 | toGlobalId, 11 | } from 'graphql-relay'; 12 | 13 | import <%= name %>Model from '../<%= name %>Model'; 14 | 15 | import * as <%= name %>Loader from '../<%= name %>Loader'; 16 | import <%= name %>Connection from '../<%= name %>Connection'; 17 | 18 | export default mutationWithClientMutationId({ 19 | name: '<%= name %>Add', 20 | inputFields: { 21 | <%_ for (field of schema.fields) { -%> 22 | <%- field.name %>: { 23 | type: <%= field.type %>, 24 | }, 25 | <%_ } -%> 26 | }, 27 | mutateAndGetPayload: async (args, context) => { 28 | // Verify if user is authorized 29 | if (!context.user) { 30 | throw new Error('Unauthorized user'); 31 | } 32 | 33 | const { 34 | <%_ for (field of schema.fields) { -%> 35 | <%- field.name %>, 36 | <%_ } -%> 37 | } = args; 38 | 39 | // Create new record 40 | const <%= camelCaseName %> = await new <%= name %>Model({ 41 | <%_ for (field of schema.fields) { -%> 42 | <%- field.name %>, 43 | <%_ } -%> 44 | }).save(); 45 | 46 | // TODO: mutation logic 47 | 48 | return { 49 | id: <%= camelCaseName %>._id, 50 | error: null, 51 | }; 52 | }, 53 | outputFields: { 54 | <%= camelCaseName %>Edge: { 55 | type: <%= name %>Connection.edgeType, 56 | resolve: async ({ id }, args, context) => { 57 | // Load new edge from loader 58 | const <%= camelCaseName %> = await <%= name %>Loader.load( 59 | context, id, 60 | ); 61 | 62 | // Returns null if no node was loaded 63 | if (!<%= camelCaseName %>) { 64 | return null; 65 | } 66 | 67 | return { 68 | cursor: toGlobalId('<%= name %>', <%= camelCaseName %>._id), 69 | node: <%= camelCaseName %>, 70 | }; 71 | }, 72 | }, 73 | error: { 74 | type: GraphQLString, 75 | resolve: ({ error }) => error, 76 | }, 77 | }, 78 | }); 79 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/templates/test/MutationAddWithSchema.js.template: -------------------------------------------------------------------------------- 1 | import { graphql } from 'graphql'; 2 | import { schema } from '../../../../graphql/schema'; 3 | import { 4 | getContext, 5 | connectMongoose, 6 | clearDbAndRestartCounters, 7 | disconnectMongoose, 8 | } from '../../../../../test/helper'; 9 | 10 | import { 11 | User, 12 | <%= name %>, 13 | } from '../../../../models'; 14 | 15 | beforeAll(connectMongoose); 16 | 17 | beforeEach(clearDbAndRestartCounters); 18 | 19 | afterAll(disconnectMongoose); 20 | 21 | it('should not allow anonymous user', async () => { 22 | //language=GraphQL 23 | const query = ` 24 | mutation M { 25 | <%= mutationName %>(input: { 26 | <%_ for (field of schema.fields) { -%> 27 | <%- field.name %>: "Example value" 28 | <%_ } -%> 29 | }) { 30 | <%= camelCaseName %>Edge { 31 | node { 32 | <%_ for (field of schema.fields) { -%> 33 | <%- field.name %> 34 | <%_ } -%> 35 | } 36 | } 37 | } 38 | } 39 | `; 40 | 41 | const variables = { 42 | }; 43 | const rootValue = {}; 44 | const context = getContext(); 45 | 46 | const result = await graphql(schema, query, rootValue, context, variables); 47 | 48 | expect(result).toMatchSnapshot(); 49 | }); 50 | 51 | it('should create a record on database', async () => { 52 | const user = new User({ 53 | name: 'user', 54 | email: 'user@example.com', 55 | }); 56 | 57 | await user.save(); 58 | 59 | //language=GraphQL 60 | const query = ` 61 | mutation M { 62 | <%= mutationName %>(input: { 63 | <%_ for (field of schema.fields) { -%> 64 | <%- field.name %>: "Example value" 65 | <%_ } -%> 66 | }) { 67 | <%= camelCaseName %>Edge { 68 | node { 69 | <%_ for (field of schema.fields) { -%> 70 | <%- field.name %> 71 | <%_ } -%> 72 | } 73 | } 74 | } 75 | } 76 | `; 77 | 78 | const variables = { 79 | }; 80 | const rootValue = {}; 81 | const context = getContext({ user }); 82 | 83 | const result = await graphql(schema, query, rootValue, context, variables); 84 | 85 | expect(result).toMatchSnapshot(); 86 | }); 87 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/templates/MutationEditWithSchema.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | GraphQLString, 4 | GraphQLNonNull, 5 | GraphQLID, 6 | <%_ for (i in schema.editDependencies) { -%> 7 | <%= schema.editDependencies[i] %>, 8 | <%_ } -%> 9 | } from 'graphql'; 10 | import { 11 | mutationWithClientMutationId, 12 | fromGlobalId, 13 | } from 'graphql-relay'; 14 | 15 | import <%= name %>Model from '../<%= name %>Model'; 16 | 17 | import <%= name %>Type from '../<%= name %>Type'; 18 | import * as <%= name %>Loader from '../<%= name %>Loader'; 19 | 20 | export default mutationWithClientMutationId({ 21 | name: '<%= name %>Edit', 22 | inputFields: { 23 | id: { 24 | type: new GraphQLNonNull(GraphQLID), 25 | }, 26 | <%_ for (field of schema.fields) { -%> 27 | <%- field.name %>: { 28 | type: <%= field.type %>, 29 | }, 30 | <%_ } -%> 31 | }, 32 | mutateAndGetPayload: async (args, context) => { 33 | // Verify if user is authorized 34 | if (!context.user) { 35 | throw new Error('Unauthorized user'); 36 | } 37 | 38 | const { 39 | id, 40 | <%_ for (field of schema.fields) { -%> 41 | <%- field.name %>, 42 | <%_ } -%> 43 | } = args; 44 | 45 | // Check if the provided ID is valid 46 | const <%= camelCaseName %> = await <%= name %>Model.findOne({ 47 | _id: fromGlobalId(id).id, 48 | }); 49 | 50 | // If not, throw an error 51 | if (!<%= camelCaseName %>) { 52 | throw new Error('Invalid <%= camelCaseName %>Id'); 53 | } 54 | 55 | // Edit record 56 | await <%= camelCaseName %>.update({ 57 | <%_ for (field of schema.fields) { -%> 58 | <%- field.name %>, 59 | <%_ } -%> 60 | }); 61 | 62 | // TODO: mutation logic 63 | 64 | // Clear dataloader cache 65 | <%= name %>Loader.clearCache(context, <%= camelCaseName %>._id); 66 | 67 | return { 68 | id: <%= camelCaseName %>._id, 69 | error: null, 70 | }; 71 | }, 72 | outputFields: { 73 | <%= camelCaseName %>: { 74 | type: <%= name %>Type, 75 | resolve: (obj, args, context) => <%= name %>Loader.load(context, obj.id), 76 | }, 77 | error: { 78 | type: GraphQLString, 79 | resolve: ({ error }) => error, 80 | }, 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /packages/generator/src/loader/__tests__/LoaderGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | copyFixturesToModules, 7 | getFileContent, 8 | } from '../../../test/helpers'; 9 | import { getConfigDir } from '../../config'; 10 | import { uppercaseFirstLetter } from '../../ejsHelpers'; 11 | import { getModulePath } from '../../paths'; 12 | 13 | const loaderGenerator = path.join(__dirname, '..'); 14 | 15 | it('generate a loader', async () => { 16 | const moduleName = 'example'; 17 | const name = uppercaseFirstLetter(moduleName); 18 | 19 | const folder = await helper.run(loaderGenerator) 20 | .withArguments(name) 21 | .toPromise(); 22 | 23 | const destinationDir = getConfigDir('loader'); 24 | 25 | const modulePath = getModulePath(destinationDir, moduleName); 26 | const loaderFilepath = path.join(modulePath, `${name}Loader.js`); 27 | 28 | assert.file([ 29 | loaderFilepath, 30 | ]); 31 | 32 | const files = { 33 | loader: getFileContent(path.join(folder, loaderFilepath)), 34 | }; 35 | 36 | expect(files).toMatchSnapshot(); 37 | }); 38 | 39 | it('generate a loader with schema', async () => { 40 | const moduleName = 'post'; 41 | const name = uppercaseFirstLetter(moduleName); 42 | 43 | const folder = await helper.run(loaderGenerator) 44 | .inTmpDir(dir => copyFixturesToModules(dir, moduleName)) 45 | .withArguments('Post Post') 46 | .toPromise(); 47 | 48 | const destinationDir = getConfigDir('loader'); 49 | 50 | const modulePath = getModulePath(destinationDir, moduleName); 51 | const loaderFilepath = path.join(modulePath, `${name}Loader.js`); 52 | 53 | assert.file([ 54 | loaderFilepath, 55 | ]); 56 | 57 | const files = { 58 | loader: getFileContent(path.join(folder, loaderFilepath)), 59 | }; 60 | 61 | expect(files).toMatchSnapshot(); 62 | }); 63 | 64 | it('generate a loader with schema and without timestamps', async () => { 65 | const moduleName = 'user'; 66 | const name = uppercaseFirstLetter(moduleName); 67 | 68 | const folder = await helper.run(loaderGenerator) 69 | .inTmpDir(dir => copyFixturesToModules(dir, moduleName)) 70 | .withArguments('User User') 71 | .toPromise(); 72 | 73 | const destinationDir = getConfigDir('loader'); 74 | 75 | const modulePath = getModulePath(destinationDir, moduleName); 76 | const loaderFilepath = path.join(modulePath, `${name}Loader.js`); 77 | 78 | assert.file([ 79 | loaderFilepath, 80 | ]); 81 | 82 | const files = { 83 | loader: getFileContent(path.join(folder, loaderFilepath)), 84 | }; 85 | 86 | expect(files).toMatchSnapshot(); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/generator/src/edit/__tests__/__snapshots__/EditGenerator.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generate edit and edit mutation files 1`] = ` 4 | Object { 5 | "edit": "//@flow 6 | import * as React from 'react'; 7 | import { graphql, createFragmentContainer } from 'react-relay/compat'; 8 | import { hot } from 'react-hot-loader'; 9 | 10 | import createQueryRenderer from '../../relay/createQueryRenderer'; 11 | 12 | import ExampleForm from './ExampleForm'; 13 | import type { ExampleEdit_viewer } from './__generated__/ExampleEdit_viewer.graphql'; 14 | 15 | type Props = { 16 | viewer: ExampleEdit_viewer, 17 | }; 18 | class ExampleEdit extends React.PureComponent { 19 | render() { 20 | const { viewer } = this.props; 21 | const { _node } = viewer; 22 | 23 | return ; 24 | } 25 | } 26 | 27 | const ExampleEditFragment = createFragmentContainer(ExampleEdit, { 28 | viewer: graphql\` 29 | fragment ExampleEdit_viewer on Viewer { 30 | id 31 | ...ExampleForm_viewer 32 | _node(id: $id) { 33 | ...ExampleForm_node 34 | } 35 | } 36 | \`, 37 | }); 38 | 39 | export default hot(module)( 40 | createQueryRenderer(ExampleEditFragment, ExampleEdit, { 41 | query: graphql\` 42 | query ExampleEditQuery($id: ID!) { 43 | viewer { 44 | ...ExampleEdit_viewer 45 | } 46 | } 47 | \`, 48 | queriesParams: ({ match: { params } }) => ({ id: params.id }), 49 | }), 50 | ); 51 | ", 52 | "editMutation": "// @flow 53 | import { commitMutation, graphql } from 'react-relay/compat'; 54 | 55 | import RelayStore from '../../relay/RelayStore'; 56 | 57 | import type { 58 | ExampleEditMutationVariables, 59 | ExampleEditMutationResponse, 60 | } from './__generated__/ExampleEditMutation.graphql'; 61 | 62 | type ExampleEditMutationInput = $PropertyType< 63 | ExampleEditMutationVariables, 64 | 'input', 65 | >; 66 | 67 | const mutation = graphql\` 68 | mutation ExampleEditMutation($input: ExampleEditInput!) { 69 | ExampleEdit(input: $input) { 70 | example { 71 | id 72 | } 73 | error 74 | } 75 | } 76 | \`; 77 | 78 | const commit = ( 79 | input: ExampleEditMutationInput, 80 | onCompleted: (response: ExampleEditMutationResponse) => void, 81 | onError: (error: Error) => void, 82 | ) => { 83 | const variables = { 84 | input, 85 | }; 86 | 87 | commitMutation(RelayStore._env, { 88 | mutation, 89 | variables, 90 | onCompleted, 91 | onError, 92 | }); 93 | }; 94 | 95 | export default { commit }; 96 | 97 | ", 98 | } 99 | `; 100 | -------------------------------------------------------------------------------- /packages/create-graphql/README.md: -------------------------------------------------------------------------------- 1 | # ![Create-GraphQL Logo](https://github.com/graphql-community/create-graphql/raw/master/media/logo.png) 2 | 3 |

Create GraphQL

4 |

5 | Create production-ready GraphQL servers 6 |

7 | 8 |

9 | 10 | 11 | 12 | 13 | 14 |

15 | 16 | 17 | 18 | ## About 19 | **[Create-GraphQL](https://github.com/graphql-community/create-graphql)** is a command-line utility to build production-ready servers with GraphQL and also generate *Mutations*, *Types* and more into existent projects 20 | 21 | Check out the post *[Announcing Create-GraphQL](https://medium.com/entria/announcing-create-graphql-17bdd81b9f96)* on **[Entria](https://medium.com/entria)** medium 22 | 23 | ## Install 24 | 25 | #### With Yarn: 26 | ```sh 27 | yarn global add @entria/create-graphql 28 | ``` 29 | 30 | #### With NPM: 31 | ```sh 32 | npm i --g @entria/create-graphql 33 | ``` 34 | 35 | ## Usage 36 | You can create a brand new GraphQL project: 37 | ```sh 38 | create-graphql init GraphQLProject 39 | ``` 40 | 41 | And can generate single files for [Mutation](https://github.com/graphql-community/create-graphql/blob/master/docs/Commands.md#--mutation--m), [Type](https://github.com/graphql-community/create-graphql/blob/master/docs/Commands.md#--type--t) and [others](https://github.com/graphql-community/create-graphql/blob/master/docs/Commands.md#generate--g): 42 | ```sh 43 | create-graphql generate --mutation Story 44 | ``` 45 | This generates a `StoryAddMutation` and `StoryEditMutation` 46 | 47 | > See more usage examples in the [docs](docs) 48 | 49 | ## Contributing 50 | If you want to contribute, see the [Contributing guidelines](https://github.com/graphql-community/create-graphql/blob/master/CONTRIBUTING.md) before and feel free to send your contributions. 51 | 52 | ## Feedbacks 53 | 54 | We love the feedbacks. It's help us to continue grow and improve. Give your feedbacks by open an [issue](https://github.com/graphql-community/create-graphql/issues/new). We will be glad to discuss your suggestions! 55 | 56 | ## License 57 | 58 | MIT © [Entria](http://github.com/entria) 59 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/templates/test/MutationEditWithSchema.js.template: -------------------------------------------------------------------------------- 1 | import { graphql } from 'graphql'; 2 | import { toGlobalId } from 'graphql-relay'; 3 | import { schema } from '../../../../graphql/schema'; 4 | import { 5 | getContext, 6 | connectMongoose, 7 | clearDbAndRestartCounters, 8 | disconnectMongoose, 9 | } from '../../../../../test/helper'; 10 | 11 | import { 12 | User, 13 | <%= name %>, 14 | } from '../../../../models'; 15 | 16 | beforeAll(connectMongoose); 17 | 18 | beforeEach(clearDbAndRestartCounters); 19 | 20 | afterAll(disconnectMongoose); 21 | 22 | it('should not allow anonymous user', async () => { 23 | // TODO: specify fields to create a new <%= name %> 24 | const <%= camelCaseName %> = new <%= name %>({ 25 | <%_ for (field of schema.fields) { -%> 26 | <%- field.name %>: 'Example value', 27 | <%_ } -%> 28 | }); 29 | 30 | await <%= camelCaseName %>.save(); 31 | 32 | const <%= camelCaseName %>Id = toGlobalId('<%= name %>', <%= camelCaseName %>._id); 33 | 34 | //language=GraphQL 35 | const query = ` 36 | mutation M { 37 | <%= mutationName %>(input: { 38 | id: "${<%= camelCaseName %>Id}" 39 | example: "Example Field to Update" 40 | }) { 41 | <%= camelCaseName %> { 42 | <%_ for (field of schema.fields) { -%> 43 | <%- field.name %> 44 | <%_ } -%> 45 | } 46 | } 47 | } 48 | `; 49 | 50 | const variables = {}; 51 | const rootValue = {}; 52 | const context = getContext(); 53 | 54 | const result = await graphql(schema, query, rootValue, context, variables); 55 | 56 | expect(result).toMatchSnapshot(); 57 | }); 58 | 59 | it('should edit a record on database', async () => { 60 | const user = new User({ 61 | name: 'user', 62 | email: 'user@example.com', 63 | }); 64 | 65 | await user.save(); 66 | 67 | // TODO: specify fields to create a new <%= name %> 68 | const <%= camelCaseName %> = new <%= name %>({ 69 | <%_ for (field of schema.fields) { -%> 70 | <%- field.name %>: 'Example value', 71 | <%_ } -%> 72 | }); 73 | 74 | await <%= camelCaseName %>.save(); 75 | 76 | const <%= camelCaseName %>Id = toGlobalId('<%= name %>', <%= camelCaseName %>._id); 77 | 78 | //language=GraphQL 79 | const query = ` 80 | mutation M { 81 | <%= mutationName %>(input: { 82 | id: "${<%= camelCaseName %>Id}" 83 | example: "Example Field to Update" 84 | }) { 85 | <%= camelCaseName %> { 86 | <%_ for (field of schema.fields) { -%> 87 | <%- field.name %> 88 | <%_ } -%> 89 | } 90 | } 91 | } 92 | `; 93 | 94 | const variables = { 95 | }; 96 | const rootValue = {}; 97 | const context = getContext({ user }); 98 | 99 | const result = await graphql(schema, query, rootValue, context, variables); 100 | 101 | expect(result).toMatchSnapshot(); 102 | }); 103 | -------------------------------------------------------------------------------- /packages/generator/src/add/__tests__/__snapshots__/AddGenerator.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generate add and add mutation files 1`] = ` 4 | Object { 5 | "add": "//@flow 6 | import * as React from 'react'; 7 | import { graphql, createFragmentContainer } from 'react-relay/compat'; 8 | import { hot } from 'react-hot-loader'; 9 | 10 | import createQueryRenderer from '../../relay/createQueryRenderer'; 11 | 12 | import ExampleForm from './ExampleForm'; 13 | import type { ExampleAdd_viewer } from './__generated__/ExampleAdd_viewer.graphql'; 14 | 15 | type Props = { 16 | viewer: ExampleAdd_viewer, 17 | }; 18 | class ExampleAdd extends React.PureComponent { 19 | render() { 20 | const { viewer } = this.props; 21 | return ; 22 | } 23 | } 24 | 25 | const ExampleAddFragment = createFragmentContainer(ExampleAdd, { 26 | viewer: graphql\` 27 | fragment ExampleAdd_viewer on Viewer { 28 | id 29 | ...ExampleForm_viewer 30 | } 31 | \`, 32 | }); 33 | 34 | export default hot(module)( 35 | createQueryRenderer(ExampleAddFragment, ExampleAdd, { 36 | query: graphql\` 37 | query ExampleAddQuery { 38 | viewer { 39 | ...ExampleAdd_viewer 40 | } 41 | } 42 | \`, 43 | }), 44 | ); 45 | ", 46 | "addMutation": "// @flow 47 | import { commitMutation, graphql } from 'react-relay/compat'; 48 | 49 | import RelayStore from '../../relay/RelayStore'; 50 | 51 | import type { 52 | ExampleAddMutationVariables, 53 | ExampleAddMutationResponse, 54 | } from './__generated__/ExampleAddMutation.graphql'; 55 | 56 | type ExampleAddMutationInput = $PropertyType< 57 | ExampleAddMutationVariables, 58 | 'input', 59 | >; 60 | 61 | // relay classic 62 | const getConfigs = viewerId => { 63 | return [ 64 | { 65 | type: 'RANGE_ADD', 66 | parentName: 'viewer', 67 | parentID: viewerId, 68 | connectionName: 'examples', 69 | edgeName: 'exampleEdge', 70 | rangeBehaviors: () => { 71 | return 'prepend'; 72 | }, 73 | }, 74 | ]; 75 | }; 76 | 77 | const mutation = graphql\` 78 | mutation ExampleAddMutation($input: ExampleAddInput!) { 79 | ExampleAdd(input: $input) { 80 | exampleEdge { 81 | __typename 82 | cursor 83 | node { 84 | __typename 85 | id 86 | } 87 | } 88 | error 89 | } 90 | } 91 | \`; 92 | 93 | const commit = ( 94 | viewerId: string, 95 | input: ExampleAddMutationInput, 96 | onCompleted: (response: ExampleAddMutationResponse) => void, 97 | onError: (error: Error) => void, 98 | ) => { 99 | const variables = { 100 | input, 101 | }; 102 | 103 | commitMutation(RelayStore._env, { 104 | mutation, 105 | variables, 106 | onCompleted, 107 | onError, 108 | configs: getConfigs(viewerId), 109 | }); 110 | }; 111 | 112 | export default { commit }; 113 | 114 | ", 115 | } 116 | `; 117 | -------------------------------------------------------------------------------- /packages/generator/src/config.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import path from 'path'; 3 | import merge from 'lodash.merge'; 4 | import fs from 'fs'; 5 | import pkgDir from 'pkg-dir'; 6 | 7 | const rootPath = pkgDir.sync('.') || '.'; 8 | 9 | let cacheConfig = null; 10 | 11 | /** 12 | * Parse `.graphqlrc` config file and retrieve its contents 13 | * @param filePath {string} The path of the config file 14 | * @returns {*} 15 | */ 16 | const parseConfigFile = (filePath: string) => { 17 | const config = JSON.parse(fs.readFileSync(filePath, 'utf8')); 18 | 19 | const directories = Object.keys(config.directories).reduce((data, directory) => { 20 | if (directory === DIRECTORY_TYPE.SOURCE) { 21 | return { 22 | ...data, 23 | [directory]: `${rootPath}/${config.directories[directory]}`, 24 | [DIRECTORY_TYPE.SRC]: config.directories.source, 25 | }; 26 | } 27 | 28 | return { 29 | ...data, 30 | [directory]: `${config.directories.source}/${config.directories.module}/${config.directories[directory]}`, 31 | }; 32 | }, {}); 33 | 34 | return { 35 | ...config, 36 | directories: { 37 | ...config.directories, 38 | ...directories, 39 | }, 40 | }; 41 | }; 42 | 43 | export const DIRECTORY_TYPE = { 44 | SOURCE: 'source', 45 | SRC: 'src', 46 | MODULE: 'module', 47 | CONNECTION: 'connection', 48 | LOADER: 'loader', 49 | MODEL: 'model', 50 | MUTATION: 'mutation', 51 | MUTATION_TEST: 'mutation_test', 52 | TYPE: 'type', 53 | TYPE_TEST: 'type_test', 54 | }; 55 | 56 | type DirectoryType = $Values; 57 | type Directories = { 58 | source: string, 59 | module: string, 60 | connection: string, 61 | loader: string, 62 | model: string, 63 | mutation: string, 64 | mutation_test: string, 65 | type: string, 66 | type_test: string, 67 | interface: string, 68 | }; 69 | 70 | type Files = { 71 | schema: string, 72 | }; 73 | 74 | type Config = { 75 | directories: Directories, 76 | files: Files, 77 | } 78 | /** 79 | * Get the `.graphqlrc` config file 80 | * @returns {object} The content of the config 81 | */ 82 | export const getCreateGraphQLConfig = (): Config => { 83 | // if (cacheConfig) return cacheConfig; 84 | 85 | // Use default config 86 | const defaultFilePath = path.resolve(`${__dirname}/graphqlrc.json`); 87 | 88 | const config = parseConfigFile(defaultFilePath); 89 | 90 | try { 91 | // Check if there is a `.graphqlrc` file in the root path 92 | const customConfig = parseConfigFile(`${rootPath}/.graphqlrc`); 93 | 94 | merge(config, customConfig); 95 | 96 | cacheConfig = config; 97 | 98 | // If it does, extend default config with it, so if the custom config has a missing line 99 | // it won't throw errors 100 | return config; 101 | } catch (err) { 102 | cacheConfig = config; 103 | // Return the default config if the custom doesn't exist 104 | return config; 105 | } 106 | }; 107 | 108 | /** 109 | * Get a directory from the configuration file 110 | * @param directory {string} The name of the directory, e.g. 'source'/'mutation' 111 | * @returns {string} The directory path 112 | */ 113 | export const getConfigDir = (directory: DirectoryType) => getCreateGraphQLConfig().directories[directory]; 114 | -------------------------------------------------------------------------------- /packages/generator/src/app/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Generator from 'yeoman-generator'; 3 | import shell from 'shelljs'; 4 | import chalk from 'chalk'; 5 | import ora from 'ora'; 6 | import path from 'path'; 7 | import fs from 'fs'; 8 | 9 | import logo from '../graphql-logo'; 10 | 11 | const tic = chalk.green('✓'); 12 | const tac = chalk.red('✗'); 13 | const commitHash = '4a0eab74f021bac16e626c0ebc9fddec3d4d8c7e'; 14 | 15 | class AppGenerator extends Generator { 16 | constructor(args, options) { 17 | super(args, options); 18 | 19 | this.argument('name', { 20 | type: String, 21 | required: true, 22 | }); 23 | 24 | this.dir = path.resolve(this.options.name); 25 | } 26 | 27 | initializing() { 28 | this.spinner = ora(); 29 | 30 | this._printGraphQLLogo(); 31 | } 32 | 33 | cloneStarterCode() { 34 | this.spinner.start(); 35 | 36 | this._validateDirectory(); 37 | 38 | this.spinner.text = 'Creating a new GraphQL project...'; 39 | 40 | const repository = 'https://github.com/entria/graphql-dataloader-boilerplate.git'; 41 | 42 | const done = this.async(); 43 | const command = 'git'; 44 | const commandOpts = ['clone', repository, this.dir]; 45 | const checkoutCommandOpts = ['checkout', commitHash]; 46 | 47 | this.spawnCommand(command, commandOpts, { stdio: 'ignore' }) 48 | .on('close', () => { 49 | shell.cd(this.dir); 50 | 51 | this.spawnCommand(command, checkoutCommandOpts, { stdio: 'ignore' }) 52 | .on('close', () => { 53 | this.spinner.stop(); 54 | 55 | this.log(`${tic} GraphQL project ${this.options.name} created.`); 56 | 57 | done(); 58 | }); 59 | }); 60 | } 61 | 62 | _printGraphQLLogo() { 63 | this.log(chalk.magenta(logo)); 64 | } 65 | 66 | _validateDirectory() { 67 | try { 68 | fs.lstatSync(this.dir).isDirectory(); 69 | 70 | this._logAndExit( 71 | `${tac} Directory "${this.options.name}" already exists, 72 | please enter a new directory name or delete "${this.options.name}"!`, 73 | ); 74 | 75 | return false; 76 | } catch (err) { 77 | return true; 78 | } 79 | } 80 | 81 | installModules() { 82 | shell.cd(this.dir); 83 | 84 | this.spinner.start(); 85 | 86 | this.spinner.text = 'Installing dependencies...'; 87 | 88 | const done = this.async(); 89 | let command = 'yarn'; 90 | let args = []; 91 | 92 | if (!shell.which('yarn')) { 93 | command = 'npm'; 94 | args = ['install']; 95 | } 96 | 97 | this.spawnCommand(command, args, { stdio: 'ignore' }) 98 | .on('close', () => { 99 | this.spinner.stop(); 100 | 101 | this.log(`${tic} Dependencies installed! 😎`); 102 | 103 | done(); 104 | }); 105 | } 106 | 107 | _cleanDir() { 108 | shell.cd(this.dir); 109 | 110 | shell.rm('-rf', '.git'); 111 | } 112 | 113 | _logAndExit(message) { 114 | this.spinner.stop(); 115 | 116 | this.log(message); 117 | 118 | process.exit(1); 119 | } 120 | 121 | end() { 122 | this._cleanDir(); 123 | 124 | this.log(`${tic} Your new project with GraphQL has been created! 🔥`); 125 | } 126 | 127 | } 128 | 129 | module.exports = AppGenerator; 130 | -------------------------------------------------------------------------------- /packages/generator/src/type/__tests__/TypeGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | getFileContent, 7 | copyFixturesToModules, 8 | } from '../../../test/helpers'; 9 | 10 | import { getConfigDir } from '../../config'; 11 | import { getModulePath, getTestPath } from '../../paths'; 12 | import { uppercaseFirstLetter } from '../../ejsHelpers'; 13 | 14 | const typeGenerator = path.join(__dirname, '..'); 15 | 16 | it('generate a type', async () => { 17 | const moduleName = 'example'; 18 | const name = uppercaseFirstLetter(moduleName); 19 | 20 | const folder = await helper.run(typeGenerator) 21 | .withArguments(name) 22 | .toPromise(); 23 | 24 | const destinationDir = getConfigDir('type'); 25 | 26 | const modulePath = getModulePath(destinationDir, moduleName); 27 | const testPath = getTestPath(modulePath); 28 | 29 | const typeFilepath = path.join(modulePath, `${name}Type.js`); 30 | const typeTestFilepath = path.join(testPath, `${name}Type.spec.js`); 31 | 32 | assert.file([ 33 | typeFilepath, 34 | typeTestFilepath, 35 | ]); 36 | 37 | const files = { 38 | type: getFileContent(path.join(folder, typeFilepath)), 39 | typeTest: getFileContent(path.join(folder, typeTestFilepath)), 40 | }; 41 | 42 | expect(files).toMatchSnapshot(); 43 | }); 44 | 45 | it('generate a type with schema', async () => { 46 | const moduleName = 'post'; 47 | const name = uppercaseFirstLetter(moduleName); 48 | 49 | const folder = await helper.run(typeGenerator) 50 | .inTmpDir(dir => copyFixturesToModules(dir, moduleName)) 51 | .withArguments('Post Post') 52 | .toPromise(); 53 | 54 | const destinationDir = getConfigDir('type'); 55 | 56 | const modulePath = getModulePath(destinationDir, moduleName); 57 | const testPath = getTestPath(modulePath); 58 | 59 | const typeFilepath = path.join(modulePath, `${name}Type.js`); 60 | const typeTestFilepath = path.join(testPath, `${name}Type.spec.js`); 61 | 62 | assert.file([ 63 | typeFilepath, 64 | typeTestFilepath, 65 | ]); 66 | 67 | const files = { 68 | type: getFileContent(path.join(folder, typeFilepath)), 69 | typeTest: getFileContent(path.join(folder, typeTestFilepath)), 70 | }; 71 | 72 | expect(files).toMatchSnapshot(); 73 | }); 74 | 75 | it('generate a type with schema and without timestamps', async () => { 76 | const moduleName = 'user'; 77 | const name = uppercaseFirstLetter(moduleName); 78 | 79 | const folder = await helper.run(typeGenerator) 80 | .inTmpDir(dir => copyFixturesToModules(dir, moduleName)) 81 | .withArguments('User User') 82 | .toPromise(); 83 | 84 | const destinationDir = getConfigDir('type'); 85 | 86 | const modulePath = getModulePath(destinationDir, moduleName); 87 | const testPath = getTestPath(modulePath); 88 | 89 | const typeFilepath = path.join(modulePath, `${name}Type.js`); 90 | const typeTestFilepath = path.join(testPath, `${name}Type.spec.js`); 91 | 92 | assert.file([ 93 | typeFilepath, 94 | typeTestFilepath, 95 | ]); 96 | 97 | const files = { 98 | type: getFileContent(path.join(folder, typeFilepath)), 99 | typeTest: getFileContent(path.join(folder, typeTestFilepath)), 100 | }; 101 | 102 | expect(files).toMatchSnapshot(); 103 | }); 104 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/__tests__/MutationGenerator.spec.js: -------------------------------------------------------------------------------- 1 | import helper from 'yeoman-test'; 2 | import assert from 'yeoman-assert'; 3 | import path from 'path'; 4 | 5 | import { 6 | copyFixturesToModules, 7 | getFileContent, 8 | } from '../../../test/helpers'; 9 | 10 | import { getConfigDir } from '../../config'; 11 | import { uppercaseFirstLetter } from '../../ejsHelpers'; 12 | import { getModulePath, getTestPath, getMutationPath } from '../../paths'; 13 | 14 | const mutationGenerator = path.join(__dirname, '..'); 15 | 16 | it('generate mutation files', async () => { 17 | const moduleName = 'example'; 18 | const name = uppercaseFirstLetter(moduleName); 19 | 20 | const folder = await helper.run(mutationGenerator) 21 | .withArguments(name) 22 | .toPromise(); 23 | 24 | const destinationDir = getConfigDir('mutation'); 25 | 26 | const modulePath = getModulePath(destinationDir, moduleName); 27 | const mutationPath = getMutationPath(modulePath); 28 | const mutationTestPath = getTestPath(mutationPath); 29 | 30 | const mutationAddFilepath = path.join(mutationPath, `${name}AddMutation.js`); 31 | const mutationEditFilepath = path.join(mutationPath, `${name}EditMutation.js`); 32 | const mutationAddTestFilepath = path.join(mutationTestPath, `${name}AddMutation.spec.js`); 33 | const mutationEditTestFilepath = path.join(mutationTestPath, `${name}EditMutation.spec.js`); 34 | 35 | assert.file([ 36 | mutationAddFilepath, 37 | mutationEditFilepath, 38 | mutationAddTestFilepath, 39 | mutationEditTestFilepath, 40 | ]); 41 | 42 | const files = { 43 | add: getFileContent(path.join(folder, mutationAddFilepath)), 44 | edit: getFileContent(path.join(folder, mutationEditFilepath)), 45 | addTest: getFileContent(path.join(folder, mutationAddTestFilepath)), 46 | editTest: getFileContent(path.join(folder, mutationEditTestFilepath)), 47 | }; 48 | 49 | expect(files).toMatchSnapshot(); 50 | }); 51 | 52 | it('generate mutation files with schema', async () => { 53 | const moduleName = 'post'; 54 | const name = uppercaseFirstLetter(moduleName); 55 | 56 | const folder = await helper.run(mutationGenerator) 57 | .inTmpDir(dir => copyFixturesToModules(dir, moduleName)) 58 | .withArguments('Post Post') 59 | .toPromise(); 60 | 61 | const destinationDir = getConfigDir('mutation'); 62 | 63 | const modulePath = getModulePath(destinationDir, moduleName); 64 | const mutationPath = getMutationPath(modulePath); 65 | const mutationTestPath = getTestPath(mutationPath); 66 | 67 | const mutationAddFilepath = path.join(mutationPath, `${name}AddMutation.js`); 68 | const mutationEditFilepath = path.join(mutationPath, `${name}EditMutation.js`); 69 | const mutationAddTestFilepath = path.join(mutationTestPath, `${name}AddMutation.spec.js`); 70 | const mutationEditTestFilepath = path.join(mutationTestPath, `${name}EditMutation.spec.js`); 71 | 72 | assert.file([ 73 | mutationAddFilepath, 74 | mutationEditFilepath, 75 | mutationAddTestFilepath, 76 | mutationEditTestFilepath, 77 | ]); 78 | 79 | const files = { 80 | add: getFileContent(path.join(folder, mutationAddFilepath)), 81 | edit: getFileContent(path.join(folder, mutationEditFilepath)), 82 | addTest: getFileContent(path.join(folder, mutationAddTestFilepath)), 83 | editTest: getFileContent(path.join(folder, mutationEditTestFilepath)), 84 | }; 85 | 86 | expect(files).toMatchSnapshot(); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/generator/src/type/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Generator from 'yeoman-generator'; 3 | import path from 'path'; 4 | import { 5 | getMongooseModelSchema, 6 | getRelativeConfigDir, 7 | } from '../utils'; 8 | import { getConfigDir, DIRECTORY_TYPE } from '../config'; 9 | import { uppercaseFirstLetter } from '../ejsHelpers'; 10 | import { getModulePath, getTestPath } from '../paths'; 11 | import { getDependencies, getDependenciesPath } from '../parser/mongoose'; 12 | 13 | class TypeGenerator extends Generator { 14 | constructor(args, options) { 15 | super(args, options); 16 | 17 | this.argument('name', { 18 | type: String, 19 | required: true, 20 | }); 21 | 22 | this.argument('model', { 23 | type: Object, 24 | required: false, 25 | }); 26 | 27 | this.destinationDir = getConfigDir('type'); 28 | } 29 | 30 | generateType() { 31 | const schema = this.options.model ? 32 | getMongooseModelSchema({ 33 | model: this.options.model, 34 | withTimestamps: true, 35 | ref: true, 36 | }) 37 | : null; 38 | 39 | const directories = this._getConfigDirectories(); 40 | 41 | const name = uppercaseFirstLetter(this.options.name); 42 | const typeFileName = `${name}Type`; 43 | 44 | const templatePath = schema ? 45 | this.templatePath('TypeWithSchema.js.template') 46 | : this.templatePath('Type.js.template'); 47 | 48 | const moduleName = this.options.name.toLowerCase(); 49 | const modulePath = getModulePath(this.destinationDir, moduleName); 50 | 51 | const relativePath = path.join(modulePath, `${typeFileName}.js`); 52 | 53 | const destinationPath = this.destinationPath(relativePath); 54 | 55 | const deps = schema ? getDependencies(schema.fields) : null; 56 | 57 | const src = getConfigDir(DIRECTORY_TYPE.SRC); 58 | 59 | const depsMap = deps ? getDependenciesPath( 60 | this.destinationPath(src), 61 | [...deps.typeDependencies, ...deps.loaderDependencies], 62 | relativePath, 63 | ) : null; 64 | 65 | const templateVars = { 66 | name, 67 | schema, 68 | dependencies: deps ? deps.dependencies : null, 69 | depsMap, 70 | directories, 71 | }; 72 | 73 | this._generateTypeTest({ 74 | name, 75 | schema, 76 | depsMap, 77 | }); 78 | 79 | this.fs.copyTpl(templatePath, destinationPath, templateVars); 80 | } 81 | 82 | _getConfigDirectories() { 83 | return getRelativeConfigDir('type', ['model', 'type', 'loader', 'connection', 'interface']); 84 | } 85 | 86 | _generateTypeTest({ name, schema, depsMap }) { 87 | const templatePath = this.templatePath('test/Type.js.template'); 88 | 89 | const moduleName = this.options.name.toLowerCase(); 90 | const testPath = getTestPath( 91 | getModulePath(this.destinationDir, moduleName), 92 | ); 93 | 94 | const destinationPath = this.destinationPath( 95 | path.join(testPath, `${name}Type.spec.js`), 96 | ); 97 | 98 | const directories = this._getConfigDirectories(); 99 | 100 | const templateVars = { 101 | name, 102 | schema, 103 | depsMap, 104 | directories, 105 | }; 106 | 107 | this.fs.copyTpl(templatePath, destinationPath, templateVars); 108 | } 109 | 110 | end() { 111 | this.log('🔥 Type created!'); 112 | } 113 | } 114 | 115 | module.exports = TypeGenerator; 116 | -------------------------------------------------------------------------------- /docs/Commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | > List of available commands 3 | 4 | - #### **[`init`](#init--i)** 5 | - #### **[`generate`](#generate--g)** 6 | - [`--type`](#--type--t) 7 | - [`--mutation`](#--mutation--m) 8 | - [`--loader`](#--loader--l) 9 | - [`--connection`](#--connection--c) 10 | - #### **[`help`](#--help--h)** 11 | - #### **[`version`](#--version--V)** 12 | 13 | ## `init`, `-i` 14 | Provides an easy way to create a GraphQL server based on [@entria/graphql-dataloader-boilerplate](https://github.com/entria/graphql-dataloader-boilerplate) which is a production-ready server 15 | 16 | > We are currently using the same boilerplate on three applications running on production at **[@entria](https://github.com/entria)** 17 | 18 | ```sh 19 | create-graphql init GraphQLProject 20 | ``` 21 | Creates a new GraphQL project. The project contains the following structure: 22 | ``` 23 | ├── /data/ # GraphQL generated schema 24 | ├── /repl/ # Read-Eval-Print-Loop (REPL) configuration 25 | ├── /scripts/ # Generate GraphQL schema script 26 | ├── /src/ # Source code of GraphQL Server 27 | │ ├── /connection/ # Connections types (Relay) 28 | │ ├── /interface/ # NodeInterface (Relay) 29 | │ ├── /loader/ # Loaders of the models using DataLoader 30 | │ ├── /model/ # Models definition (Mongoose, SQL, Google DataStore) 31 | │ ├── /mutation/ # Mutations definition 32 | ├── /test/ # Test helpers 33 | ``` 34 | 35 | ## `generate`, `-g` 36 | Generates the files by passing these options: 37 | 38 | - [`--type`](#--type--t) 39 | - [`--mutation`](#--mutation--m) 40 | - [`--loader`](#--loader--l) 41 | - [`--connection`](#--connection--c) 42 | 43 | #### `--type`, `-t` 44 | Creates a type file and it's name will be automatically suffixed with `Type` 45 | 46 | ```sh 47 | create-graphql generate --type Story 48 | ``` 49 | 50 | Generates `StoryType` file under the path `./src/type` 51 | 52 | #### `--mutation`, `-m` 53 | Creates mutations, the files names will be automatically suffixed with `Mutation` 54 | 55 | ```sh 56 | create-graphql generate --mutation Story 57 | ``` 58 | Generates `StoryAddMutation` and `StoryEditMutation` under the path `./src/mutation` 59 | 60 | **Hint:** To generates mutations based on a [Mongoose](https://github.com/Automattic/mongoose) schema, use `--schema` option 61 | 62 | ```sh 63 | create-graphql generate --mutation Story --schema Story 64 | ``` 65 | 66 | Which will look for `Story` Mongoose schema file in `./src/model` and generate the mutations fields based on it 67 | 68 | **Hint**: you may use aliases commands to create multiple files in one single command: 69 | ```sh 70 | create-graphql generate -tm Story --schema Story 71 | ``` 72 | Will create a GraphQL *type* and *mutation* based on `Story` schema and automatically generated tests using [Jest](https://github.com/facebook/jest) 73 | 74 | #### `--loader`, `-l` 75 | Creates a GraphQL loader, the files names will be automatically suffixed with `Loader` 76 | 77 | ```sh 78 | create-graphql generate --loader Story 79 | ``` 80 | 81 | Generates a `StoryLoader` file importing `StoryConnection` under the path `./src/loader` 82 | 83 | #### `--connection`, `-c` 84 | Creates a Relay connection, the files names will be automatically suffixed with `Connection` 85 | 86 | ```sh 87 | create-graphql generate --connection Story 88 | ``` 89 | 90 | Generates a `StoryConnection` importing `StoryType` under the path `./src/connection` 91 | 92 | ## `--help`, `-h` 93 | Output usage information with all available commands 94 | 95 | ## `--version`, `-V` 96 | Output the current version 97 | 98 | If yout need, you can update **Create-GraphQL** with: 99 | ```sh 100 | yarn global upgrade create-graphql # Also works with NPM as `npm update --global create-graphql` 101 | ``` 102 | -------------------------------------------------------------------------------- /packages/generator/flow-typed/npm/yeoman-generator_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3d2c80af016afea597623472b7e31981 2 | // flow-typed version: <>/yeoman-generator_v2.0.5 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'yeoman-generator' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'yeoman-generator' { 17 | // A is a phantom type that ties an event instance... 18 | declare class Event {}; 19 | // ...to its handler 20 | type Handler = (a: A, ...rest: Array) => void; 21 | 22 | declare class EventEmitter { 23 | on>(event: Event, handler: F): void; 24 | emit(event: Event, a: A): void; 25 | }; 26 | 27 | declare class Storage { 28 | constructor(name: string, fs: any, configPath: string): Storage; 29 | _store: Object, 30 | _persist(val: string): void; 31 | save(): void; 32 | get(key: string): void; 33 | getAll(): Object; 34 | set(key: string, val: any): void; 35 | delete(key: string): void; 36 | defaults(defaults: Object): string; 37 | }; 38 | 39 | declare class Generator extends EventEmitter { 40 | constructor(args: Object, options: Object): Generator; 41 | prompt(questions: string[]): string[]; 42 | option(name: string, config: Object): Generator; 43 | argument(name: string, config: Object): Generator; 44 | parseOptions(): void; 45 | checkRequiredArgs(): void; 46 | run(cb: Function): void; 47 | composeWith(modulePath: string, options: Object): Generator; 48 | rootGeneratorName(): string; 49 | rootGeneratorVersion(): string; 50 | _getStorage(): Storage; 51 | _getGlobalStorage(): Storage; 52 | destinationRoot(rootPath: string): string; 53 | sourceRoot(rootPath: string): string; 54 | templatePath(): string; 55 | destinationPath(): string; 56 | determineAppname(): string; 57 | registerTransformStream(streams: any): Generator; 58 | _writeFiles(done: Function): void; 59 | } 60 | 61 | declare module.exports: Generator; 62 | } 63 | 64 | /** 65 | * We include stubs for each file inside this npm package in case you need to 66 | * require those files directly. Feel free to delete any files that aren't 67 | * needed. 68 | */ 69 | declare module 'yeoman-generator/lib/actions/help' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'yeoman-generator/lib/actions/install' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'yeoman-generator/lib/actions/spawn-command' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'yeoman-generator/lib/actions/user' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'yeoman-generator/lib/index' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'yeoman-generator/lib/util/binary-diff' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'yeoman-generator/lib/util/conflicter' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'yeoman-generator/lib/util/deprecate' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'yeoman-generator/lib/util/prompt-suggestion' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'yeoman-generator/lib/util/storage' { 106 | declare module.exports: any; 107 | } 108 | 109 | // Filename aliases 110 | declare module 'yeoman-generator/lib/actions/help.js' { 111 | declare module.exports: $Exports<'yeoman-generator/lib/actions/help'>; 112 | } 113 | declare module 'yeoman-generator/lib/actions/install.js' { 114 | declare module.exports: $Exports<'yeoman-generator/lib/actions/install'>; 115 | } 116 | declare module 'yeoman-generator/lib/actions/spawn-command.js' { 117 | declare module.exports: $Exports<'yeoman-generator/lib/actions/spawn-command'>; 118 | } 119 | declare module 'yeoman-generator/lib/actions/user.js' { 120 | declare module.exports: $Exports<'yeoman-generator/lib/actions/user'>; 121 | } 122 | declare module 'yeoman-generator/lib/index.js' { 123 | declare module.exports: $Exports<'yeoman-generator/lib/index'>; 124 | } 125 | declare module 'yeoman-generator/lib/util/binary-diff.js' { 126 | declare module.exports: $Exports<'yeoman-generator/lib/util/binary-diff'>; 127 | } 128 | declare module 'yeoman-generator/lib/util/conflicter.js' { 129 | declare module.exports: $Exports<'yeoman-generator/lib/util/conflicter'>; 130 | } 131 | declare module 'yeoman-generator/lib/util/deprecate.js' { 132 | declare module.exports: $Exports<'yeoman-generator/lib/util/deprecate'>; 133 | } 134 | declare module 'yeoman-generator/lib/util/prompt-suggestion.js' { 135 | declare module.exports: $Exports<'yeoman-generator/lib/util/prompt-suggestion'>; 136 | } 137 | declare module 'yeoman-generator/lib/util/storage.js' { 138 | declare module.exports: $Exports<'yeoman-generator/lib/util/storage'>; 139 | } 140 | -------------------------------------------------------------------------------- /packages/generator/src/parser/graphql.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { uppercaseFirstLetter } from '../ejsHelpers'; 3 | import type { MongooseFieldDefinition } from './mongoose'; 4 | 5 | const MONGOOSE_TYPE_TO_GRAPHQL_TYPE = { 6 | Number: 'GraphQLInt', // it could be a GraphQLFloat 7 | Boolean: 'GraphQLBoolean', 8 | Date: 'GraphQLString', 9 | ObjectId: 'GraphQLID', 10 | DEFAULT: 'GraphQLString', 11 | }; 12 | 13 | const getGraphQLTypeFromMongooseType = (mongooseType: string, mongooseChildType?: string) => { 14 | return mongooseType in MONGOOSE_TYPE_TO_GRAPHQL_TYPE 15 | ? MONGOOSE_TYPE_TO_GRAPHQL_TYPE[mongooseType] 16 | : MONGOOSE_TYPE_TO_GRAPHQL_TYPE.DEFAULT; 17 | }; 18 | 19 | const MONGOOSE_TYPE_TO_FLOWTYPE = { 20 | Number: 'number', 21 | Boolean: 'boolean', 22 | Date: 'Date', 23 | ObjectId: 'string', 24 | DEFAULT: 'string', 25 | }; 26 | 27 | const getFlowtypeFromMongooseType = (mongooseType: string, mongooseChildType?: string) => { 28 | if (mongooseType === 'Array') { 29 | const flowtype = mongooseChildType in MONGOOSE_TYPE_TO_FLOWTYPE 30 | ? MONGOOSE_TYPE_TO_FLOWTYPE[mongooseChildType] 31 | : MONGOOSE_TYPE_TO_FLOWTYPE.DEFAULT; 32 | 33 | return `${flowtype}[]`; 34 | } 35 | 36 | return mongooseType in MONGOOSE_TYPE_TO_FLOWTYPE 37 | ? MONGOOSE_TYPE_TO_FLOWTYPE[mongooseType] 38 | : MONGOOSE_TYPE_TO_FLOWTYPE.DEFAULT; 39 | }; 40 | 41 | type GraphQLField = { 42 | name: string, 43 | description: string, 44 | required: boolean, 45 | originalType: string, // type from mongoose 46 | resolve: string, // resolver of this field 47 | resolveArgs?: string, 48 | type: string, // GraphQL Type 49 | flowType: string, // Flow Type 50 | graphqlType?: string, // graphql type name 51 | graphqlLoader?: string, // graphql loader name 52 | listType?: string, // type when using GraphQLList 53 | } 54 | export const parseFieldToGraphQL = (field: MongooseFieldDefinition, ref: boolean): GraphQLField => { 55 | const graphQLField = { 56 | name: field.name, 57 | description: field.description, 58 | required: !!field.required, 59 | originalType: field.type, 60 | resolve: `obj.${field.name}`, 61 | }; 62 | 63 | const name = uppercaseFirstLetter(field.name); 64 | const typeFileName = field.ref ? `${field.ref}Type` : `${name}Type`; 65 | const loaderFileName = field.ref ? `${field.ref}Loader` : `${name}Loader`;; 66 | 67 | let parsedChildField; 68 | let typeFileNameSingular; 69 | let loaderFileNameSingular; 70 | 71 | switch (field.type) { 72 | case 'Number': 73 | return { 74 | ...graphQLField, 75 | type: getGraphQLTypeFromMongooseType(field.type), 76 | flowType: getFlowtypeFromMongooseType(field.type), 77 | }; 78 | case 'Boolean': 79 | return { 80 | ...graphQLField, 81 | type: getGraphQLTypeFromMongooseType(field.type), 82 | flowType: getFlowtypeFromMongooseType(field.type), 83 | }; 84 | case 'Array': 85 | field.type = field.childType; 86 | 87 | parsedChildField = parseFieldToGraphQL(field, ref); 88 | parsedChildField.flowType = getFlowtypeFromMongooseType('Array', parsedChildField.type); 89 | parsedChildField.type = [parsedChildField.type]; 90 | 91 | if (field.childType === 'ObjectId' && ref) { 92 | typeFileNameSingular = `${field.ref}Type`; 93 | loaderFileNameSingular = `${field.ref}Loader`; 94 | 95 | parsedChildField = { 96 | ...parsedChildField, 97 | type: [typeFileNameSingular], 98 | resolve: `await ${loaderFileNameSingular}.load${name}ByIds(context, obj.${field.name})`, 99 | resolveArgs: 'async (obj, args, context)', 100 | graphqlType: typeFileNameSingular, 101 | graphqlLoader: loaderFileNameSingular, 102 | }; 103 | } 104 | 105 | // TODO - review this 106 | parsedChildField.listType = parsedChildField.type[0]; 107 | parsedChildField.type = `GraphQLList(${parsedChildField.type[0]})`; 108 | 109 | return parsedChildField; 110 | case 'ObjectId': 111 | if (ref) { 112 | return { 113 | ...graphQLField, 114 | type: typeFileName, 115 | flowType: 'string', 116 | resolve: `await ${loaderFileName}.load(context, obj.${field.name})`, 117 | resolveArgs: 'async (obj, args, context)', 118 | graphqlType: typeFileName, 119 | graphqlLoader: loaderFileName, 120 | }; 121 | } 122 | 123 | return { 124 | ...graphQLField, 125 | type: getGraphQLTypeFromMongooseType(field.type), 126 | flowType: getFlowtypeFromMongooseType(field.type), 127 | }; 128 | case 'Date': 129 | return { 130 | ...graphQLField, 131 | type: getGraphQLTypeFromMongooseType(field.type), 132 | flowType: getFlowtypeFromMongooseType(field.type), 133 | resolve: `obj.${field.name} ? obj.${field.name}.toISOString() : null` 134 | }; 135 | default: 136 | return { 137 | ...graphQLField, 138 | type: getGraphQLTypeFromMongooseType(field.type), 139 | flowType: getFlowtypeFromMongooseType(field.type), 140 | }; 141 | } 142 | }; 143 | -------------------------------------------------------------------------------- /packages/generator/src/mutation/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Generator from 'yeoman-generator'; 3 | import path from 'path'; 4 | import { 5 | getMongooseModelSchema, 6 | getRelativeConfigDir, 7 | } from '../utils'; 8 | import { getConfigDir } from '../config'; 9 | import { camelCaseText, uppercaseFirstLetter } from '../ejsHelpers'; 10 | import { getModulePath, getMutationPath, getTestPath } from '../paths'; 11 | import { getDependencies } from '../parser/mongoose'; 12 | 13 | class MutationGenerator extends Generator { 14 | constructor(args, options) { 15 | super(args, options); 16 | 17 | this.argument('name', { 18 | type: String, 19 | required: true, 20 | }); 21 | 22 | this.argument('model', { 23 | type: Object, 24 | required: false, 25 | }); 26 | 27 | this.destinationDir = getConfigDir('mutation'); 28 | } 29 | 30 | _mutationPath(name) { 31 | return `${this.destinationDir}/${name}Mutation.js`; 32 | } 33 | 34 | _parseSchema(schema, deps) { 35 | // Remove `GraphQLString` dependency from import if it exists, 36 | // it's already hard-coded on `MutationAdd` template. 37 | const addDependencies = deps.dependencies.filter(dep => ['GraphQLString'].indexOf(dep) === -1); 38 | 39 | // Also remove `GraphQLString`, `GraphQLNonNull` & `GraphQLID` dependencies 40 | // from import if they exist, they are already hard-coded on `MutationEdit` template. 41 | const editDependencies = deps.dependencies.filter(dep => 42 | ['GraphQLString', 'GraphQLNonNull', 'GraphQLID'].indexOf(dep) === -1, 43 | ); 44 | 45 | // Map through the fields checking if any of them is `required: true`, if so, use `GraphQLNonNull` 46 | const fields = schema.fields.map((field) => { 47 | if (!field.required) { 48 | return field; 49 | } 50 | 51 | // Add `GraphQLNonNull` to `addDependencies` import if it hasn't been added yet. 52 | // Won't push to `editDependencies` because it's already specified on the template file. 53 | if (addDependencies.indexOf('GraphQLNonNull') === -1) { 54 | addDependencies.push('GraphQLNonNull'); 55 | } 56 | 57 | return { 58 | ...field, 59 | type: `GraphQLNonNull(${field.type})`, 60 | }; 61 | }); 62 | 63 | return { 64 | ...schema, 65 | fields, 66 | addDependencies, 67 | editDependencies, 68 | }; 69 | } 70 | 71 | _getConfigDirectories() { 72 | return getRelativeConfigDir('mutation', ['model', 'type', 'loader', 'connection']); 73 | } 74 | 75 | generateMutation() { 76 | let schema = null; 77 | if (this.options.model) { 78 | const modelSchema = getMongooseModelSchema({ model: this.options.model }); 79 | const deps = getDependencies(modelSchema.fields); 80 | schema = this._parseSchema(modelSchema, deps); 81 | } 82 | 83 | const name = uppercaseFirstLetter(this.options.name); 84 | 85 | const mutations = { 86 | add: { 87 | fileName: `${name}Add`, 88 | template: { 89 | withSchema: 'MutationAddWithSchema.js.template', 90 | regular: 'MutationAdd.js.template', 91 | }, 92 | }, 93 | edit: { 94 | fileName: `${name}Edit`, 95 | template: { 96 | withSchema: 'MutationEditWithSchema.js.template', 97 | regular: 'MutationEdit.js.template', 98 | }, 99 | }, 100 | }; 101 | 102 | const templateType = schema ? 'withSchema' : 'regular'; 103 | const directories = this._getConfigDirectories(); 104 | 105 | const templateVars = { 106 | name, 107 | camelCaseName: camelCaseText(this.options.name), 108 | schema, 109 | directories, 110 | }; 111 | 112 | const moduleName = this.options.name.toLowerCase(); 113 | const modulePath = getModulePath(this.destinationDir, moduleName); 114 | const mutationPath = getMutationPath(modulePath); 115 | 116 | Object.keys(mutations).forEach((mutationType) => { 117 | const { template, fileName } = mutations[mutationType]; 118 | 119 | const mutationFilePath = path.join(mutationPath, `${fileName}Mutation.js`); 120 | 121 | this.fs.copyTpl( 122 | this.templatePath(template[templateType]), 123 | mutationFilePath, 124 | templateVars, 125 | ); 126 | 127 | this._generateMutationTest({ 128 | name, 129 | mutationName: fileName, 130 | template: template[templateType], 131 | schema, 132 | }); 133 | }); 134 | } 135 | 136 | _generateMutationTest({ name, mutationName, template, schema }) { 137 | const templatePath = this.templatePath(`test/${template}`); 138 | 139 | const moduleName = this.options.name.toLowerCase(); 140 | const modulePath = getModulePath(this.destinationDir, moduleName); 141 | const mutationPath = getMutationPath(modulePath); 142 | const mutationTestPath = getTestPath(mutationPath); 143 | 144 | const destinationPath = this.destinationPath( 145 | path.join(mutationTestPath, `${mutationName}Mutation.spec.js`), 146 | ); 147 | 148 | const directories = this._getConfigDirectories(); 149 | 150 | const templateVars = { 151 | name, 152 | camelCaseName: camelCaseText(name), 153 | mutationName, 154 | schema, 155 | directories, 156 | }; 157 | 158 | this.fs.copyTpl(templatePath, destinationPath, templateVars); 159 | } 160 | 161 | end() { 162 | this.log('🔥 Mutation created!'); 163 | } 164 | } 165 | 166 | module.exports = MutationGenerator; 167 | -------------------------------------------------------------------------------- /packages/generator/src/loader/__tests__/__snapshots__/LoaderGenerator.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generate a loader 1`] = ` 4 | Object { 5 | "loader": "// @flow 6 | import DataLoader from 'dataloader'; 7 | import { connectionFromMongoCursor, mongooseLoader } from '@entria/graphql-mongoose-loader'; 8 | import type { ConnectionArguments } from 'graphql-relay'; 9 | 10 | import ExampleModel from './ExampleModel'; 11 | import type { GraphQLContext } from '../../TypeDefinition'; 12 | 13 | type ExampleType = { 14 | id: string, 15 | _id: string, 16 | exampleField: string, 17 | } 18 | 19 | export default class Example { 20 | id: string; 21 | _id: string; 22 | exampleField: string; 23 | 24 | constructor(data: ExampleType) { 25 | this.id = data.id; 26 | this._id = data._id; 27 | this.exampleField = data.exampleField; 28 | } 29 | } 30 | 31 | export const getLoader = () => new DataLoader(ids => mongooseLoader(ExampleModel, ids)); 32 | 33 | const viewerCanSee = () => true; 34 | 35 | export const load = async ({ dataloaders }: GraphQLContext, id: ?string) => { 36 | if (!id) return null; 37 | 38 | try { 39 | const data = await dataloaders.ExampleLoader.load(id.toString()); 40 | 41 | if (!data) return null; 42 | 43 | return viewerCanSee() ? new Example(data) : null; 44 | } catch (err) { 45 | return null; 46 | } 47 | }; 48 | 49 | export const clearCache = ({ dataloaders }: GraphQLContext, id: string) => { 50 | return dataloaders.ExampleLoader.clear(id.toString()); 51 | }; 52 | 53 | export const loadExamples = async (context: GraphQLContext, args: ConnectionArguments) => { 54 | // TODO: specify conditions 55 | const examples = ExampleModel.find({}); 56 | 57 | return connectionFromMongoCursor({ 58 | cursor: examples, 59 | context, 60 | args, 61 | loader: load, 62 | }); 63 | }; 64 | ", 65 | } 66 | `; 67 | 68 | exports[`generate a loader with schema 1`] = ` 69 | Object { 70 | "loader": "// @flow 71 | import DataLoader from 'dataloader'; 72 | import { connectionFromMongoCursor, mongooseLoader } from '@entria/graphql-mongoose-loader'; 73 | import type { ConnectionArguments } from 'graphql-relay'; 74 | 75 | import PostModel from './PostModel'; 76 | import type { GraphQLContext } from '../../TypeDefinition'; 77 | 78 | type PostType = { 79 | id: string, 80 | _id: string, 81 | title: string, 82 | author: string, 83 | slug: string, 84 | tags: string[], 85 | oldSlugs: string[], 86 | comments: string[], 87 | externalComments: string[], 88 | createdAt: Date, 89 | updatedAt: Date, 90 | } 91 | 92 | export default class Post { 93 | id: string; 94 | _id: string; 95 | title: string; 96 | author: string; 97 | slug: string; 98 | tags: string[]; 99 | oldSlugs: string[]; 100 | comments: string[]; 101 | externalComments: string[]; 102 | createdAt: Date; 103 | updatedAt: Date; 104 | 105 | constructor(data: PostType) { 106 | this.id = data.id; 107 | this._id = data._id; 108 | this.title = data.title; 109 | this.author = data.author; 110 | this.slug = data.slug; 111 | this.tags = data.tags; 112 | this.oldSlugs = data.oldSlugs; 113 | this.comments = data.comments; 114 | this.externalComments = data.externalComments; 115 | this.createdAt = data.createdAt; 116 | this.updatedAt = data.updatedAt; 117 | } 118 | } 119 | 120 | export const getLoader = () => new DataLoader(ids => mongooseLoader(PostModel, ids)); 121 | 122 | const viewerCanSee = () => true; 123 | 124 | export const load = async ({ dataloaders }: GraphQLContext, id: ?string) => { 125 | if (!id) return null; 126 | 127 | try { 128 | const data = await dataloaders.PostLoader.load(id.toString()); 129 | 130 | if (!data) return null; 131 | 132 | return viewerCanSee() ? new Post(data) : null; 133 | } catch (err) { 134 | return null 135 | }; 136 | }; 137 | 138 | export const clearCache = ({ dataloaders }: GraphQLContext, id: string) => { 139 | return dataloaders.PostLoader.clear(id.toString()); 140 | }; 141 | 142 | export const loadPosts = async (context: GraphQLContext, args: ConnectionArguments) => { 143 | // TODO: specify conditions 144 | const posts = PostModel.find({}); 145 | 146 | return connectionFromMongoCursor({ 147 | cursor: posts, 148 | context, 149 | args, 150 | loader: load, 151 | }); 152 | }; 153 | ", 154 | } 155 | `; 156 | 157 | exports[`generate a loader with schema and without timestamps 1`] = ` 158 | Object { 159 | "loader": "// @flow 160 | import DataLoader from 'dataloader'; 161 | import { connectionFromMongoCursor, mongooseLoader } from '@entria/graphql-mongoose-loader'; 162 | import type { ConnectionArguments } from 'graphql-relay'; 163 | 164 | import UserModel from './UserModel'; 165 | import type { GraphQLContext } from '../../TypeDefinition'; 166 | 167 | type UserType = { 168 | id: string, 169 | _id: string, 170 | name: string, 171 | password: string, 172 | email: string, 173 | active: boolean, 174 | lastLoginAt: Date, 175 | } 176 | 177 | export default class User { 178 | id: string; 179 | _id: string; 180 | name: string; 181 | password: string; 182 | email: string; 183 | active: boolean; 184 | lastLoginAt: Date; 185 | 186 | constructor(data: UserType) { 187 | this.id = data.id; 188 | this._id = data._id; 189 | this.name = data.name; 190 | this.password = data.password; 191 | this.email = data.email; 192 | this.active = data.active; 193 | this.lastLoginAt = data.lastLoginAt; 194 | } 195 | } 196 | 197 | export const getLoader = () => new DataLoader(ids => mongooseLoader(UserModel, ids)); 198 | 199 | const viewerCanSee = () => true; 200 | 201 | export const load = async ({ dataloaders }: GraphQLContext, id: ?string) => { 202 | if (!id) return null; 203 | 204 | try { 205 | const data = await dataloaders.UserLoader.load(id.toString()); 206 | 207 | if (!data) return null; 208 | 209 | return viewerCanSee() ? new User(data) : null; 210 | } catch (err) { 211 | return null 212 | }; 213 | }; 214 | 215 | export const clearCache = ({ dataloaders }: GraphQLContext, id: string) => { 216 | return dataloaders.UserLoader.clear(id.toString()); 217 | }; 218 | 219 | export const loadUsers = async (context: GraphQLContext, args: ConnectionArguments) => { 220 | // TODO: specify conditions 221 | const users = UserModel.find({}); 222 | 223 | return connectionFromMongoCursor({ 224 | cursor: users, 225 | context, 226 | args, 227 | loader: load, 228 | }); 229 | }; 230 | ", 231 | } 232 | `; 233 | -------------------------------------------------------------------------------- /packages/generator/src/form/templates/Form.js.template: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react'; 3 | import { graphql, createFragmentContainer } from 'react-relay/compat'; 4 | import styled from 'styled-components'; 5 | import { withRouter } from 'react-router-dom'; 6 | import { withFormik } from 'formik'; 7 | import Yup from 'yup'; 8 | 9 | import type { ContextRouter } from 'react-router-dom'; 10 | import type { FormikProps } from 'formik'; 11 | 12 | import withSnackbar, { type SnackbarContextProps } from '../../hoc/withSnackbar'; 13 | 14 | import { routeTo } from '../../router/utils'; 15 | import { Button, ToggleFormik, InputFieldFormik } from '../common'; 16 | 17 | import type { <%= name %>Form_viewer } from './__generated__/<%= name %>Form_viewer.graphql'; 18 | import type { <%= name %>Form_node } from './__generated__/<%= name %>Form_node.graphql'; 19 | 20 | import <%= name %>AddMutation from './<%= name %>AddMutation'; 21 | import <%= name %>EditMutation from './<%= name %>EditMutation'; 22 | 23 | const FormContainer = styled.div` 24 | display: flex; 25 | flex-wrap: wrap; 26 | padding-top: 25px; 27 | flex-direction: column; 28 | `; 29 | 30 | const ButtonWrapper = styled.div` 31 | display: flex; 32 | align-items: center; 33 | justify-content: flex-end; 34 | `; 35 | 36 | const InputField = styled(InputFieldFormik).attrs({ 37 | style: { 38 | marginTop: 25, 39 | marginBottom: 33, 40 | padding: 0, 41 | border: 'none', 42 | width: '100%', 43 | paddingBottom: 10, 44 | fontSize: 14, 45 | }, 46 | floatingLabelStyle: { 47 | height: '16px', 48 | fontSize: '15px', 49 | lineHeight: '1.33', 50 | letterSpacing: 'normal', 51 | textAlign: 'left', 52 | color: '#607389', 53 | }, 54 | underlineFocusStyle: { 55 | borderBottom: '2px solid rgb(72, 144, 229)', 56 | }, 57 | underlineStyle: { 58 | borderBottom: '2px solid #D0D0D0', 59 | }, 60 | underlineShow: true, 61 | })``; 62 | 63 | const ToggleBox = styled.div` 64 | display: flex; 65 | align-items: center; 66 | `; 67 | 68 | const ToggleText = styled.p` 69 | height: 16px; 70 | font-size: 12px; 71 | line-height: 1.33; 72 | letter-spacing: normal; 73 | text-align: left; 74 | color: #607389; 75 | width: 100px; 76 | `; 77 | 78 | type OwnProps = { 79 | viewer: <%= name %>Form_viewer, 80 | node: <%= name %>Form_node, 81 | }; 82 | type Props = OwnProps & FormikProps & SnackbarContextProps & ContextRouter; 83 | class <%= name %>FormInnerFrom extends React.PureComponent { 84 | handleBack = () => { 85 | const { history, node } = this.props; 86 | 87 | if (node) return history.push(routeTo('<%= name.toLowerCase() %>.view', { id: node.id })); 88 | 89 | return history.push(routeTo('<%= name.toLowerCase() %>.list')); 90 | }; 91 | 92 | render() { 93 | const { handleSubmit, isSubmitting, isValid } = this.props; 94 | 95 | return ( 96 |
97 | 98 | 99 | 105 | 106 | Está ativo? 107 | 108 | 109 | 110 |