├── .gitignore ├── packages ├── graffiti │ ├── index.js │ ├── jest.config.js │ ├── test │ │ ├── __snapshots__ │ │ │ ├── plugin.auth.test.js.snap │ │ │ └── default-queries.test.js.snap │ │ ├── fixtures │ │ │ ├── queries.basic.js │ │ │ ├── queries.custom-resolvers.js │ │ │ ├── queries.auth.js │ │ │ ├── queries.relations.js │ │ │ ├── queries.default-queries.js │ │ │ └── queries.relations-manual.js │ │ ├── helpers │ │ │ ├── graphql.js │ │ │ └── exec.js │ │ ├── __setup │ │ │ └── environment.js │ │ ├── basic.test.js │ │ ├── custom-folder.test.js │ │ ├── custom-resolvers.test.js │ │ ├── plugin.next.test.js │ │ ├── default-queries.test.js │ │ ├── relations.test.js │ │ ├── relations-manual.test.js │ │ └── plugin.auth.test.js │ ├── lib │ │ ├── plugins │ │ │ └── index.js │ │ ├── __mocks__ │ │ │ └── config.js │ │ ├── config.js │ │ ├── mongoose.js │ │ ├── graphql │ │ │ ├── createRelations.js │ │ │ ├── index.js │ │ │ └── createType.js │ │ └── index.js │ ├── README.md │ ├── CHANGELOG.md │ ├── package.json │ ├── LICENSE │ └── bin │ │ └── graffiti.js ├── graffiti-plugin-auth │ ├── CHANGELOG.md │ ├── package.json │ ├── LICENSE │ ├── login.html │ ├── register.html │ ├── README.md │ └── index.js └── graffiti-plugin-next │ ├── CHANGELOG.md │ ├── package.json │ ├── LICENSE │ ├── index.js │ └── README.md ├── logo ├── png │ ├── splash.png │ ├── splash@2x.png │ ├── splash@3x.png │ ├── line_black.png │ ├── line_white.png │ ├── centered_black.png │ ├── centered_white.png │ ├── line_black@2x.png │ ├── line_black@3x.png │ ├── line_white@2x.png │ ├── line_white@3x.png │ ├── symbol_black.png │ ├── symbol_white.png │ ├── symbol_black@2x.png │ ├── symbol_black@3x.png │ ├── symbol_white@2x.png │ ├── symbol_white@3x.png │ ├── centered_black@2x.png │ ├── centered_black@3x.png │ ├── centered_white@2x.png │ └── centered_white@3x.png ├── README.md └── svg │ ├── symbol_black.svg │ ├── symbol_white.svg │ ├── centered_white.svg │ ├── line_white.svg │ ├── centered_black.svg │ ├── line_black.svg │ └── splash.svg ├── examples ├── basic │ ├── schema │ │ └── note.js │ ├── graffiti.config.js │ ├── package.json │ └── README.md ├── plugin-next │ ├── schema │ │ └── note.js │ ├── util │ │ └── client.js │ ├── graffiti.config.js │ ├── pages │ │ └── index.js │ ├── package.json │ └── README.md ├── custom-folder │ ├── models │ │ └── note.js │ ├── graffiti.config.js │ ├── package.json │ └── README.md ├── relations │ ├── graffiti.config.js │ ├── schema │ │ ├── collection.js │ │ └── note.js │ ├── package.json │ └── README.md ├── relations-manual │ ├── graffiti.config.js │ ├── schema │ │ ├── note.js │ │ └── collection.js │ ├── package.json │ └── README.md ├── custom-resolvers │ ├── graffiti.config.js │ ├── package.json │ ├── README.md │ └── schema │ │ └── note.js ├── default-queries │ ├── graffiti.config.js │ ├── package.json │ ├── schema │ │ └── note.js │ └── README.md └── plugin-auth │ ├── schema │ ├── note.js │ └── userNote.js │ ├── graffiti.config.js │ ├── package.json │ └── README.md ├── .prettierrc ├── docs ├── Links.md ├── README.md ├── Contributing.md ├── FAQ.md ├── Basics.md ├── Plugins.md ├── Advanced.md ├── TutorialNotesNext.md └── TutorialNotesNextAuth.md ├── .eslintrc ├── package.json ├── LICENSE ├── .github └── workflows │ ├── test.yml │ └── release.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | .next/ -------------------------------------------------------------------------------- /packages/graffiti/index.js: -------------------------------------------------------------------------------- 1 | const { start } = require('./lib'); 2 | 3 | start(); 4 | -------------------------------------------------------------------------------- /logo/png/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/splash.png -------------------------------------------------------------------------------- /logo/png/splash@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/splash@2x.png -------------------------------------------------------------------------------- /logo/png/splash@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/splash@3x.png -------------------------------------------------------------------------------- /logo/png/line_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/line_black.png -------------------------------------------------------------------------------- /logo/png/line_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/line_white.png -------------------------------------------------------------------------------- /logo/png/centered_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/centered_black.png -------------------------------------------------------------------------------- /logo/png/centered_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/centered_white.png -------------------------------------------------------------------------------- /logo/png/line_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/line_black@2x.png -------------------------------------------------------------------------------- /logo/png/line_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/line_black@3x.png -------------------------------------------------------------------------------- /logo/png/line_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/line_white@2x.png -------------------------------------------------------------------------------- /logo/png/line_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/line_white@3x.png -------------------------------------------------------------------------------- /logo/png/symbol_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/symbol_black.png -------------------------------------------------------------------------------- /logo/png/symbol_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/symbol_white.png -------------------------------------------------------------------------------- /examples/basic/schema/note.js: -------------------------------------------------------------------------------- 1 | // notes 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | }; 6 | -------------------------------------------------------------------------------- /logo/png/symbol_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/symbol_black@2x.png -------------------------------------------------------------------------------- /logo/png/symbol_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/symbol_black@3x.png -------------------------------------------------------------------------------- /logo/png/symbol_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/symbol_white@2x.png -------------------------------------------------------------------------------- /logo/png/symbol_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/symbol_white@3x.png -------------------------------------------------------------------------------- /examples/plugin-next/schema/note.js: -------------------------------------------------------------------------------- 1 | // notes 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | }; 6 | -------------------------------------------------------------------------------- /logo/png/centered_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/centered_black@2x.png -------------------------------------------------------------------------------- /logo/png/centered_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/centered_black@3x.png -------------------------------------------------------------------------------- /logo/png/centered_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/centered_white@2x.png -------------------------------------------------------------------------------- /logo/png/centered_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamalight/graffiti/HEAD/logo/png/centered_white@3x.png -------------------------------------------------------------------------------- /packages/graffiti/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: './test/__setup/environment.js', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/basic/graffiti.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongoUrl: 'mongodb://localhost/graffiti-example', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/custom-folder/models/note.js: -------------------------------------------------------------------------------- 1 | // notes 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/relations/graffiti.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongoUrl: 'mongodb://localhost/graffiti-test', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/relations/schema/collection.js: -------------------------------------------------------------------------------- 1 | // define notes collection 2 | exports.schema = { 3 | name: String, 4 | }; 5 | -------------------------------------------------------------------------------- /examples/relations-manual/graffiti.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongoUrl: 'mongodb://localhost/graffiti-test', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/custom-resolvers/graffiti.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongoUrl: 'mongodb://localhost/graffiti-example', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/default-queries/graffiti.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongoUrl: 'mongodb://localhost/graffiti-example', 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /examples/custom-folder/graffiti.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongoUrl: 'mongodb://localhost/graffiti-example', 3 | basePath: './models', 4 | }; 5 | -------------------------------------------------------------------------------- /examples/relations/schema/note.js: -------------------------------------------------------------------------------- 1 | // notes 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | group: { type: 'ObjectId', ref: 'Collection' }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/plugin-next/util/client.js: -------------------------------------------------------------------------------- 1 | import { createClient } from 'urql'; 2 | 3 | export const client = createClient({ 4 | url: 'http://localhost:3000/graphql', 5 | }); 6 | -------------------------------------------------------------------------------- /examples/relations-manual/schema/note.js: -------------------------------------------------------------------------------- 1 | // notes 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | group: { type: 'ObjectId', ref: 'Collection' }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/plugin-auth/schema/note.js: -------------------------------------------------------------------------------- 1 | // notes 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | // link to user 6 | user: { type: 'ObjectId', ref: 'User' }, 7 | }; 8 | -------------------------------------------------------------------------------- /examples/plugin-next/graffiti.config.js: -------------------------------------------------------------------------------- 1 | const nextPlugin = require('graffiti-plugin-next'); 2 | 3 | module.exports = { 4 | mongoUrl: 'mongodb://localhost/graffiti-example', 5 | plugins: [nextPlugin()], 6 | }; 7 | -------------------------------------------------------------------------------- /examples/plugin-auth/graffiti.config.js: -------------------------------------------------------------------------------- 1 | const authPlugin = require('graffiti-plugin-auth'); 2 | 3 | module.exports = { 4 | mongoUrl: 'mongodb://localhost/graffiti-example', 5 | plugins: [authPlugin({ secret: 'my_super_secret_jwt_secret' })], 6 | }; 7 | -------------------------------------------------------------------------------- /docs/Links.md: -------------------------------------------------------------------------------- 1 | # Tutorials, articles, video and related links 2 | 3 | ## Tutorials 4 | 5 | - [Tutorial: Build a simple note-taking app with Graffiti and Next](./TutorialNotesNext.md) 6 | - [Tutorial: Adding auth plugin to note-taking app](./TutorialNotesNextAuth.md) 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Graffiti.js Documentation 2 | 3 | - [Basics](./Basics.md) 4 | - [Advanced topics](./Advanced.md) 5 | - [Plugins](./Plugins.md) 6 | - [FAQ](./FAQ.md) 7 | - [Contribution Guidelines](./Contributing.md) 8 | - [Tutorials, articles, video and related links](./Links.md) 9 | -------------------------------------------------------------------------------- /packages/graffiti/test/__snapshots__/plugin.auth.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Auth plugin setup Should not create new note without auth 1`] = `undefined`; 4 | 5 | exports[`Auth plugin setup Should not get notes without token 1`] = `undefined`; 6 | -------------------------------------------------------------------------------- /packages/graffiti-plugin-auth/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 / 2020-09-10 2 | 3 | - First stable release with complete docs 4 | 5 | # 0.3.0 / 2020-09-10 6 | 7 | - Allow specifying redirect in auth plugin 8 | - Change how auth plugin checks URLs, allow next.js in dev mode by default 9 | 10 | # 0.2.0 / 2020-09-08 11 | 12 | - Initial release 13 | -------------------------------------------------------------------------------- /packages/graffiti/lib/plugins/index.js: -------------------------------------------------------------------------------- 1 | exports.loadPlugins = async ({ projectConfig }) => { 2 | // get list of plugins or use empty array 3 | const plugins = projectConfig?.plugins ?? []; 4 | // allow plugins init (if needed) 5 | await Promise.all(plugins.map((plugin) => plugin.init?.())); 6 | // return plugins 7 | return plugins; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/graffiti/README.md: -------------------------------------------------------------------------------- 1 | # Graffiti.js 2 | 3 | ## Getting Started 4 | 5 | Visit [https://github.com/yamalight/graffiti/](https://github.com/yamalight/graffiti/) to get started with Graffiti.js. 6 | 7 | ## Documentation 8 | 9 | Visit [github.com repo](https://github.com/yamalight/graffiti/tree/master/docs) to view the full documentation. 10 | -------------------------------------------------------------------------------- /packages/graffiti-plugin-next/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 / 2020-09-14 2 | 3 | - Move Next.js, React and React-DOM to peerDeps 4 | - Include license files 5 | 6 | # 1.0.0 / 2020-09-10 7 | 8 | - First stable release with complete docs 9 | 10 | # 0.4.0 / 2020-09-10 11 | 12 | - Allow specifying autobuild tag during init 13 | 14 | # 0.3.0 / 2020-09-08 15 | 16 | - Initial release 17 | -------------------------------------------------------------------------------- /packages/graffiti/lib/__mocks__/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const { getConfig } = jest.requireActual('../config'); 3 | 4 | // mock for config, return globally set mongo URI instead of real config 5 | exports.getConfig = () => { 6 | const originalConfig = getConfig(); 7 | return { 8 | ...originalConfig, 9 | // replace mongo URL with one in-mem 10 | mongoUrl: global.__MONGO_URI__, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/graffiti/test/fixtures/queries.basic.js: -------------------------------------------------------------------------------- 1 | exports.CREATE_NOTE_QUERY = ` 2 | mutation CreateNote($name:String!, $body:String!) { 3 | noteCreate(record:{name:$name, body:$body}) { 4 | record { 5 | _id 6 | name 7 | body 8 | } 9 | } 10 | } 11 | `; 12 | 13 | exports.GET_NOTES_QUERY = ` 14 | query GetNotes { 15 | noteMany { 16 | _id 17 | name 18 | body 19 | } 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /examples/relations-manual/schema/collection.js: -------------------------------------------------------------------------------- 1 | // define notes collection 2 | exports.schema = { 3 | name: String, 4 | }; 5 | 6 | exports.relations = ({ typedefs }) => { 7 | // define relation between collection and notes 8 | typedefs.collection.addRelation('notes', { 9 | resolver: () => typedefs.note.getResolver('findMany'), 10 | prepareArgs: { 11 | group: (source) => source._id, 12 | }, 13 | projection: { _id: 1 }, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/graffiti/test/fixtures/queries.custom-resolvers.js: -------------------------------------------------------------------------------- 1 | exports.CREATE_NOTE_QUERY = ` 2 | mutation CreateNote($name:String!, $body:String!) { 3 | noteCreate(record:{name:$name, body:$body}) { 4 | record { 5 | _id 6 | name 7 | body 8 | } 9 | } 10 | } 11 | `; 12 | 13 | exports.GET_CUSTOM_NOTE_QUERY = ` 14 | query GetCustomNote ($id:MongoID!) { 15 | noteCustomById(id:$id) { 16 | _id 17 | name 18 | body 19 | } 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-basic", 3 | "version": "0.1.0", 4 | "description": "", 5 | "private": "true", 6 | "scripts": { 7 | "start": "graffiti", 8 | "develop": "graffiti dev", 9 | "mongo": "docker run --name mongodb -p 27017:27017 -d mongo" 10 | }, 11 | "keywords": [], 12 | "author": "Tim Ermilov (http://codezen.net)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "graffiti": "*" 16 | }, 17 | "engines": { 18 | "node": ">=14.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/relations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-relations", 3 | "version": "0.1.0", 4 | "description": "", 5 | "private": "true", 6 | "scripts": { 7 | "start": "graffiti", 8 | "develop": "graffiti dev", 9 | "mongo": "docker run --name mongodb -p 27017:27017 -d mongo" 10 | }, 11 | "keywords": [], 12 | "author": "Tim Ermilov (http://codezen.net)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "graffiti": "*" 16 | }, 17 | "engines": { 18 | "node": ">=14.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/custom-folder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-custom-path", 3 | "version": "0.1.0", 4 | "description": "", 5 | "private": "true", 6 | "scripts": { 7 | "start": "graffiti", 8 | "develop": "graffiti dev", 9 | "mongo": "docker run --name mongodb -p 27017:27017 -d mongo" 10 | }, 11 | "keywords": [], 12 | "author": "Tim Ermilov (http://codezen.net)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "graffiti": "*" 16 | }, 17 | "engines": { 18 | "node": ">=14.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/custom-resolvers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-custom-resolvers", 3 | "version": "0.1.0", 4 | "description": "", 5 | "private": "true", 6 | "scripts": { 7 | "start": "graffiti", 8 | "develop": "graffiti dev", 9 | "mongo": "docker run --name mongodb -p 27017:27017 -d mongo" 10 | }, 11 | "keywords": [], 12 | "author": "Tim Ermilov (http://codezen.net)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "graffiti": "*" 16 | }, 17 | "engines": { 18 | "node": ">=14.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/default-queries/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-default-queries", 3 | "version": "0.1.0", 4 | "description": "", 5 | "private": "true", 6 | "scripts": { 7 | "start": "graffiti", 8 | "develop": "graffiti dev", 9 | "mongo": "docker run --name mongodb -p 27017:27017 -d mongo" 10 | }, 11 | "keywords": [], 12 | "author": "Tim Ermilov (http://codezen.net)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "graffiti": "*" 16 | }, 17 | "engines": { 18 | "node": ">=14.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/relations-manual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-relations-manual", 3 | "version": "0.1.0", 4 | "description": "", 5 | "private": "true", 6 | "scripts": { 7 | "start": "graffiti", 8 | "develop": "graffiti dev", 9 | "mongo": "docker run --name mongodb -p 27017:27017 -d mongo" 10 | }, 11 | "keywords": [], 12 | "author": "Tim Ermilov (http://codezen.net)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "graffiti": "*" 16 | }, 17 | "engines": { 18 | "node": ">=14.8.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/graffiti/test/helpers/graphql.js: -------------------------------------------------------------------------------- 1 | // executes query or mutation with variables on server and returns JSON result 2 | exports.executeGraphql = async ({ 3 | server, 4 | mutation, 5 | query, 6 | variables, 7 | token, 8 | }) => { 9 | const req = { 10 | method: 'POST', 11 | url: '/graphql', 12 | body: { 13 | query: query ?? mutation, 14 | variables, 15 | }, 16 | }; 17 | if (token) { 18 | req.headers = { Authorization: `Bearer ${token}` }; 19 | } 20 | const res = await server.inject(req); 21 | 22 | return res.json(); 23 | }; 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard", "prettier"], 3 | "plugins": ["prettier"], 4 | "env": { 5 | "node": true 6 | }, 7 | "rules": { 8 | "max-len": ["error", 120, 4], 9 | "camelcase": "off", 10 | "promise/param-names": "off", 11 | "prefer-promise-reject-errors": "off", 12 | "no-control-regex": "off", 13 | "prettier/prettier": [ 14 | "error", 15 | { 16 | "endOfLine": "lf", 17 | "trailingComma": "es5", 18 | "tabWidth": 2, 19 | "singleQuote": true, 20 | "semi": true 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/plugin-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-plugin-auth", 3 | "version": "0.1.0", 4 | "description": "", 5 | "private": "true", 6 | "scripts": { 7 | "start": "graffiti", 8 | "develop": "graffiti dev", 9 | "mongo": "docker run --name mongodb -p 27017:27017 -d mongo" 10 | }, 11 | "keywords": [], 12 | "author": "Tim Ermilov (http://codezen.net)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "graffiti": "*", 16 | "graffiti-plugin-auth": "*" 17 | }, 18 | "engines": { 19 | "node": ">=14.8.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/graffiti/test/fixtures/queries.auth.js: -------------------------------------------------------------------------------- 1 | exports.GET_USER_QUERY = ` 2 | query GetUser($id:MongoID!) { 3 | userById(_id:$id) { 4 | _id 5 | email 6 | password 7 | } 8 | } 9 | `; 10 | 11 | exports.CREATE_USER_NOTE_QUERY = ` 12 | mutation AddUserNote($name:String!, $body:String!) { 13 | userNoteCreate(name:$name, body:$body) { 14 | _id 15 | name 16 | body 17 | user { 18 | _id 19 | email 20 | } 21 | } 22 | } 23 | `; 24 | 25 | exports.GET_USER_NOTES_QUERY = ` 26 | query GetUserNotes { 27 | userNotes { 28 | _id 29 | name 30 | body 31 | user { 32 | _id 33 | email 34 | } 35 | } 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /examples/plugin-next/pages/index.js: -------------------------------------------------------------------------------- 1 | import { client } from '../util/client'; 2 | 3 | const notesQuery = ` 4 | query AllNotes { 5 | noteMany { 6 | name 7 | body 8 | } 9 | } 10 | `; 11 | 12 | export default function HomePage({ notes }) { 13 | return ( 14 |
15 |
Welcome to Next.js & Graffiti!
16 |
{JSON.stringify(notes, null, 2)}
17 |
18 | ); 19 | } 20 | 21 | export const getServerSideProps = async () => { 22 | const result = await client.query(notesQuery).toPromise(); 23 | const notes = result?.data?.noteMany ?? []; 24 | 25 | return { 26 | props: { 27 | notes, 28 | }, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /docs/Contributing.md: -------------------------------------------------------------------------------- 1 | ## I want to contribute 2 | 3 | Awesome! All contributions are welcome. 4 | If you want to add new feature or implement a significant change that hasn't been discussed yet - please _open an issue first_! 5 | 6 | ## How to send pull requests 7 | 8 | 1. Fork this repository to your own GitHub account 9 | 2. Create new branch that is named accordingly to the issue you are working on (e.g. `feature/new-thing` or `fix/bug-name`) 10 | 3. Make sure tests are passing (if you are adding new feature - _add tests_ to cover basics of that feature) 11 | 4. Make sure your branch is up to date with `master` branch 12 | 5. Open pull request towards `master` branch 13 | 6. Wait for feedback 14 | -------------------------------------------------------------------------------- /packages/graffiti/test/__setup/environment.js: -------------------------------------------------------------------------------- 1 | const { MongoMemoryServer } = require('mongodb-memory-server'); 2 | const NodeEnvironment = require('jest-environment-node'); 3 | 4 | class Environment extends NodeEnvironment { 5 | async setup() { 6 | await super.setup(); 7 | 8 | // create new in-mem mongodb server 9 | const mongo = new MongoMemoryServer(); 10 | // get new URI 11 | const uri = await mongo.getUri(); 12 | 13 | this.global.__MONGOD__ = mongo; 14 | this.global.__MONGO_URI__ = uri.replace(/\?$/g, ''); 15 | } 16 | 17 | async teardown() { 18 | await this.global.__MONGOD__.stop(); 19 | super.teardown(); 20 | } 21 | } 22 | 23 | module.exports = Environment; 24 | -------------------------------------------------------------------------------- /examples/custom-folder/README.md: -------------------------------------------------------------------------------- 1 | # Custom path example 2 | 3 | This example shows the custom path config for Graffiti. 4 | Schema definition files are located in `models/` folder. 5 | 6 | ## How to use 7 | 8 | Download the example: 9 | 10 | ```bash 11 | curl https://codeload.github.com/yamalight/graffiti/tar.gz/master | tar -xz --strip=2 graffiti-master/examples/custom-folders 12 | cd custom-folders 13 | ``` 14 | 15 | Install it and run: 16 | 17 | ```bash 18 | npm install 19 | # if you don't have mongo - there's an npm script to start one using docker 20 | npm run mongo 21 | npm start 22 | 23 | # or 24 | 25 | yarn 26 | yarn mongo 27 | yarn start 28 | ``` 29 | 30 | Navigate to `http://localhost:3000/graphql` to see GraphQL endpoint. 31 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic example 2 | 3 | This example shows the most basic idea behind Graffiti. 4 | Schema definition file `schema/note.js` is used to create basic GraphQL endpoint with Notes schema. 5 | 6 | ## How to use 7 | 8 | Download the example: 9 | 10 | ```bash 11 | curl https://codeload.github.com/yamalight/graffiti/tar.gz/master | tar -xz --strip=2 graffiti-master/examples/basic 12 | cd basic 13 | ``` 14 | 15 | Install it and run: 16 | 17 | ```bash 18 | npm install 19 | # if you don't have mongo - there's an npm script to start one using docker 20 | npm run mongo 21 | npm start 22 | 23 | # or 24 | 25 | yarn 26 | yarn mongo 27 | yarn start 28 | ``` 29 | 30 | Navigate to `http://localhost:3000/graphql` to see GraphQL endpoint. 31 | -------------------------------------------------------------------------------- /examples/plugin-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-plugin-next", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": "true", 6 | "scripts": { 7 | "start": "NODE_ENV=production graffiti", 8 | "develop": "graffiti dev", 9 | "build": "next build", 10 | "mongo": "docker run --name mongodb -p 27017:27017 -d mongo" 11 | }, 12 | "keywords": [], 13 | "author": "Tim Ermilov (http://codezen.net)", 14 | "license": "MIT", 15 | "dependencies": { 16 | "graffiti": "*", 17 | "graffiti-plugin-next": "*", 18 | "graphql": "^15.3.0", 19 | "next": "^9.5.3", 20 | "react": "^16.13.1", 21 | "react-dom": "^16.13.1", 22 | "urql": "^1.10.0" 23 | }, 24 | "engines": { 25 | "node": ">=14.8.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Is it ready for production? 4 | 5 | Yes! If you find any issues - please [create a new ticket](https://github.com/yamalight/graffiti/issues). 6 | 7 | ## How does it work? 8 | 9 | Graffiti uses [graphql-compose](https://graphql-compose.github.io/) along with [graphql-compose-mongoose](https://graphql-compose.github.io/docs/plugins/plugin-mongoose.html) and [Mongoose](https://mongoosejs.com/) to automatically generate GraphQL schema from files you create. 10 | It then uses [fastify](https://www.fastify.io/) together with [mercurius](https://github.com/mercurius-js/mercurius) to create GraphQL API from generated schema. 11 | All the GraphQL configuration of your projects happens automatically based on file structures, exposed schema definitions and helper functions. 12 | -------------------------------------------------------------------------------- /packages/graffiti/test/__snapshots__/default-queries.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Default queries setup Should fail to get all notes 1`] = ` 4 | Array [ 5 | Object { 6 | "locations": Array [ 7 | Object { 8 | "column": 3, 9 | "line": 3, 10 | }, 11 | ], 12 | "message": "Cannot query field \\"noteMany\\" on type \\"Query\\". Did you mean \\"noteById\\"?", 13 | }, 14 | ] 15 | `; 16 | 17 | exports[`Default queries setup Should fail to update note 1`] = ` 18 | Array [ 19 | Object { 20 | "locations": Array [ 21 | Object { 22 | "column": 3, 23 | "line": 3, 24 | }, 25 | ], 26 | "message": "Cannot query field \\"noteUpdateById\\" on type \\"Mutation\\".", 27 | }, 28 | ] 29 | `; 30 | -------------------------------------------------------------------------------- /packages/graffiti/test/fixtures/queries.relations.js: -------------------------------------------------------------------------------- 1 | exports.CREATE_NOTE_QUERY = ` 2 | mutation CreateNote($name:String!, $body:String!, $group: MongoID!) { 3 | noteCreate(record:{name:$name, body:$body, group: $group}) { 4 | record { 5 | _id 6 | name 7 | body 8 | group { 9 | _id 10 | name 11 | } 12 | } 13 | } 14 | } 15 | `; 16 | 17 | exports.CREATE_COLLECTION_QUERY = ` 18 | mutation CreateCollection($name:String) { 19 | collectionCreate(record:{name:$name}) { 20 | record { 21 | _id 22 | name 23 | } 24 | } 25 | } 26 | `; 27 | 28 | exports.GET_NOTES_QUERY = ` 29 | query GetNotes { 30 | noteMany { 31 | _id 32 | name 33 | body 34 | group { 35 | _id 36 | name 37 | } 38 | } 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*", 5 | "examples/*" 6 | ], 7 | "scripts": { 8 | "lint": "yarn workspace graffiti lint", 9 | "test": "yarn workspace graffiti test", 10 | "test-ci": "yarn workspace graffiti test --silent" 11 | }, 12 | "devDependencies": { 13 | "eslint": "^7.7.0", 14 | "eslint-config-prettier": "^6.11.0", 15 | "eslint-config-standard": "^14.1.1", 16 | "eslint-plugin-import": "^2.22.0", 17 | "eslint-plugin-node": "^11.1.0", 18 | "eslint-plugin-prettier": "^3.1.4", 19 | "eslint-plugin-promise": "^4.2.1", 20 | "eslint-plugin-standard": "^4.0.1", 21 | "jest": "^26.4.2", 22 | "jest-environment-node": "^26.3.0", 23 | "mongodb-memory-server": "^6.7.0", 24 | "prettier": "^2.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/graffiti/lib/config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { join } = require('path'); 3 | 4 | const defaultConfig = { 5 | mongoUrl: 'mongodb://localhost/graffiti', 6 | port: 3000, 7 | host: '0.0.0.0', 8 | basePath: 'schema', 9 | }; 10 | 11 | exports.getConfig = () => { 12 | // get current work folder 13 | const workFolder = process.cwd(); 14 | // construct path to config file 15 | const configFilePath = join(workFolder, 'graffiti.config.js'); 16 | // if config doesn't exist - return default config 17 | if (!fs.existsSync(configFilePath)) { 18 | return defaultConfig; 19 | } 20 | // otherwise - require and return config 21 | const config = require(configFilePath); 22 | // merge with default config to still have default values 23 | return { ...defaultConfig, ...config }; 24 | }; 25 | -------------------------------------------------------------------------------- /examples/plugin-auth/README.md: -------------------------------------------------------------------------------- 1 | # Auth plugin example 2 | 3 | This example shows the most basic idea behind auth plugin Graffiti. 4 | Schema definition file `schema/note.js` is used to create basic GraphQL endpoint with Notes schema protected by simple user-password auth. 5 | 6 | ## How to use 7 | 8 | Download the example: 9 | 10 | ```bash 11 | curl https://codeload.github.com/yamalight/graffiti/tar.gz/master | tar -xz --strip=2 graffiti-master/examples/plugin-auth 12 | cd plugin-auth 13 | ``` 14 | 15 | Install it and run: 16 | 17 | ```bash 18 | npm install 19 | # if you don't have mongo - there's an npm script to start one using docker 20 | npm run mongo 21 | npm start 22 | 23 | # or 24 | 25 | yarn 26 | yarn mongo 27 | yarn start 28 | ``` 29 | 30 | Navigate to `http://localhost:3000/graphql` to see GraphQL endpoint. 31 | -------------------------------------------------------------------------------- /packages/graffiti-plugin-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graffiti-plugin-next", 3 | "version": "2.0.0", 4 | "description": "Next.js plugin for Graffiti framework", 5 | "repository": "yamalight/graffiti", 6 | "bugs": "https://github.com/yamalight/graffiti/issues", 7 | "homepage": "https://github.com/yamalight/graffiti", 8 | "main": "index.js", 9 | "scripts": { 10 | "lint": "eslint lib/ test/", 11 | "test": "NODE_ENV=test jest" 12 | }, 13 | "keywords": [ 14 | "graffiti", 15 | "graffiti-plugin", 16 | "next", 17 | "next.js" 18 | ], 19 | "author": "Tim Ermilov (http://codezen.net)", 20 | "license": "MIT", 21 | "peerDependencies": { 22 | "graffiti": "^1.0.0", 23 | "next": "^9.5.3", 24 | "react": "^16.13.1", 25 | "react-dom": "^16.13.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/graffiti/test/helpers/exec.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | exports.exec = async ({ workdir, command, args }) => { 4 | return new Promise((resolve, reject) => { 5 | const proc = spawn(command, args, { cwd: workdir }); 6 | const log = []; 7 | const errorLog = []; 8 | proc.stdout.on('data', (data) => { 9 | const message = data.toString(); 10 | log.push(message); 11 | }); 12 | proc.stderr.on('data', (data) => { 13 | const message = data.toString(); 14 | errorLog.push(message); 15 | }); 16 | proc.on('exit', (code) => { 17 | if (errorLog.length > 0 || code !== 0) { 18 | reject({ code: code.toString(), log, errorLog }); 19 | } else { 20 | resolve({ code: code.toString(), log, errorLog }); 21 | } 22 | }); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /examples/plugin-next/README.md: -------------------------------------------------------------------------------- 1 | # Next.js plugin example 2 | 3 | This example shows the most basic idea behind Graffiti with Next.js plugin. 4 | Schema definition file `schema/note.js` is used to create basic GraphQL endpoint with Notes schema and simple Next.js front-end. 5 | 6 | ## How to use 7 | 8 | Download the example: 9 | 10 | ```bash 11 | curl https://codeload.github.com/yamalight/graffiti/tar.gz/master | tar -xz --strip=2 graffiti-master/examples/plugin-next 12 | cd plugin-next 13 | ``` 14 | 15 | Install it and run: 16 | 17 | ```bash 18 | npm install 19 | # if you don't have mongo - there's an npm script to start one using docker 20 | npm run mongo 21 | npm run build 22 | npm start 23 | 24 | # or 25 | 26 | yarn 27 | yarn mongo 28 | yarn build 29 | yarn start 30 | ``` 31 | 32 | Navigate to `http://localhost:3000/graphql` to see GraphQL endpoint. 33 | -------------------------------------------------------------------------------- /packages/graffiti/test/fixtures/queries.default-queries.js: -------------------------------------------------------------------------------- 1 | exports.CREATE_NOTE_QUERY = ` 2 | mutation CreateNote($name:String!, $body:String!) { 3 | noteCreate(record:{name:$name, body:$body}) { 4 | record { 5 | _id 6 | name 7 | body 8 | } 9 | } 10 | } 11 | `; 12 | 13 | exports.UPDATE_NOTE_QUERY = ` 14 | mutation UpdateNote($id:MongoID!, $name:String, $body:String) { 15 | noteUpdateById(record:{_id:$id, name:$name, body:$body}) { 16 | record { 17 | _id 18 | name 19 | body 20 | } 21 | } 22 | } 23 | `; 24 | 25 | exports.GET_NOTE_QUERY = ` 26 | query GetNoteById($id:MongoID!) { 27 | noteById(_id:$id) { 28 | _id 29 | name 30 | body 31 | } 32 | } 33 | `; 34 | 35 | exports.GET_NOTES_QUERY = ` 36 | query GetNotes { 37 | noteMany { 38 | _id 39 | name 40 | body 41 | } 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /examples/default-queries/schema/note.js: -------------------------------------------------------------------------------- 1 | // notes 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | }; 6 | 7 | // config for default queries 8 | exports.config = { 9 | // define defaults 10 | defaults: { 11 | // change what queries are allowed 12 | queries: { 13 | byId: true, // normally, this can be omitted since `true` is default 14 | byIds: false, 15 | one: false, 16 | many: false, 17 | total: false, 18 | connection: false, 19 | pagination: false, 20 | }, 21 | // you can also use `mutations: false` to disable all mutations (or queries) 22 | mutations: { 23 | create: true, 24 | createMany: false, 25 | updateById: false, 26 | updateOne: false, 27 | updateMany: false, 28 | removeById: false, 29 | removeOne: false, 30 | removeMany: false, 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /examples/default-queries/README.md: -------------------------------------------------------------------------------- 1 | # Default queries control example 2 | 3 | This example shows how to control default queries and mutations created by Graffiti. 4 | Schema definition file `schema/note.js` is used to create basic GraphQL endpoint with Notes schema. 5 | Exported `exports.config` variable in that file defines what default queries and mutations should be disabled. 6 | 7 | ## How to use 8 | 9 | Download the example: 10 | 11 | ```bash 12 | curl https://codeload.github.com/yamalight/graffiti/tar.gz/master | tar -xz --strip=2 graffiti-master/examples/default-queries 13 | cd default-queries 14 | ``` 15 | 16 | Install it and run: 17 | 18 | ```bash 19 | npm install 20 | # if you don't have mongo - there's an npm script to start one using docker 21 | npm run mongo 22 | npm start 23 | 24 | # or 25 | 26 | yarn 27 | yarn mongo 28 | yarn start 29 | ``` 30 | 31 | Navigate to `http://localhost:3000/graphql` to see GraphQL endpoint. 32 | -------------------------------------------------------------------------------- /examples/relations/README.md: -------------------------------------------------------------------------------- 1 | # Relations example 2 | 3 | This example shows how to define relations for your GraphQL using Graffiti. 4 | Schema definition file `schema/collection.js` is used to define basic notes collection that just has name. 5 | While schema definition file `schema/note.js` is used to create Notes schema (just as it is in basic example), while linking it to new collections schema. 6 | 7 | ## How to use 8 | 9 | Download the example: 10 | 11 | ```bash 12 | curl https://codeload.github.com/yamalight/graffiti/tar.gz/master | tar -xz --strip=2 graffiti-master/examples/relations 13 | cd relations 14 | ``` 15 | 16 | Install it and run: 17 | 18 | ```bash 19 | npm install 20 | # if you don't have mongo - there's an npm script to start one using docker 21 | npm run mongo 22 | npm start 23 | 24 | # or 25 | 26 | yarn 27 | yarn mongo 28 | yarn start 29 | ``` 30 | 31 | Navigate to `http://localhost:3000/graphql` to see GraphQL endpoint. 32 | -------------------------------------------------------------------------------- /packages/graffiti-plugin-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graffiti-plugin-auth", 3 | "version": "1.0.0", 4 | "description": "Authentication plugin for Graffiti framework", 5 | "repository": "yamalight/graffiti", 6 | "bugs": "https://github.com/yamalight/graffiti/issues", 7 | "homepage": "https://github.com/yamalight/graffiti", 8 | "main": "index.js", 9 | "scripts": { 10 | "lint": "eslint lib/ test/", 11 | "test": "NODE_ENV=test jest" 12 | }, 13 | "keywords": [ 14 | "graffiti", 15 | "graffiti-plugin", 16 | "auth", 17 | "authentication" 18 | ], 19 | "author": "Tim Ermilov (http://codezen.net)", 20 | "license": "MIT", 21 | "dependencies": { 22 | "bcrypt": "^5.0.0", 23 | "fastify-auth": "^1.0.1", 24 | "fastify-cookie": "^4.1.0", 25 | "fastify-jwt": "^2.1.3", 26 | "fastify-plugin": "^2.3.3" 27 | }, 28 | "engines": { 29 | "node": ">=14.8.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /logo/README.md: -------------------------------------------------------------------------------- 1 | # Logo 2 | 3 | This folder contains Graffiti logo and its variations 4 | 5 | ## Logo variations 6 | 7 | Centered black: 8 | 9 | Graffiti 10 | 11 | Centered white: 12 | 13 | Graffiti 14 | 15 | Line black: 16 | 17 | Graffiti 18 | 19 | Line white: 20 | 21 | Graffiti 22 | 23 | Splash: 24 | 25 | Graffiti 26 | 27 | Symbol black: 28 | 29 | Graffiti 30 | 31 | Symbol white: 32 | 33 | Graffiti 34 | 35 | ## Made by 36 | 37 | Logo was made by [Ivan Semenov](https://www.behance.net/ivan_semenov). 38 | -------------------------------------------------------------------------------- /examples/custom-resolvers/README.md: -------------------------------------------------------------------------------- 1 | # Custom resolvers example 2 | 3 | This example shows how to use custom resolvers with Graffiti. 4 | Schema definition file `schema/note.js` is used to create basic GraphQL endpoint with Notes schema. 5 | Exported `exports.resolvers` function in that file creates additional custom resolvers, while exported `exports.compose` function registers new custom resolver in our GraphQL schema. 6 | 7 | ## How to use 8 | 9 | Download the example: 10 | 11 | ```bash 12 | curl https://codeload.github.com/yamalight/graffiti/tar.gz/master | tar -xz --strip=2 graffiti-master/examples/custom-resolvers 13 | cd custom-resolvers 14 | ``` 15 | 16 | Install it and run: 17 | 18 | ```bash 19 | npm install 20 | # if you don't have mongo - there's an npm script to start one using docker 21 | npm run mongo 22 | npm start 23 | 24 | # or 25 | 26 | yarn 27 | yarn mongo 28 | yarn start 29 | ``` 30 | 31 | Navigate to `http://localhost:3000/graphql` to see GraphQL endpoint. 32 | -------------------------------------------------------------------------------- /packages/graffiti/lib/mongoose.js: -------------------------------------------------------------------------------- 1 | const { createConnection, Schema } = require('mongoose'); 2 | const capitalize = require('lodash/capitalize'); 3 | 4 | // create db connection 5 | exports.connect = async ({ projectConfig }) => { 6 | // connect to given URL 7 | const db = createConnection(projectConfig.mongoUrl, { 8 | useNewUrlParser: true, 9 | useCreateIndex: true, 10 | useUnifiedTopology: true, 11 | }); 12 | 13 | // handle DB errors 14 | db.on('error', (error) => { 15 | console.error('MongoDB connection error:', error); 16 | // exit immediately on error 17 | process.exit(1); 18 | }); 19 | 20 | // connection ready 21 | await new Promise((resolve) => db.once('open', resolve)); 22 | 23 | return db; 24 | }; 25 | 26 | exports.buildModel = ({ db, schema, name }) => { 27 | const mongooseSchema = new Schema(schema); 28 | const schemaName = capitalize(name); 29 | const Model = db.model(schemaName, mongooseSchema); 30 | return Model; 31 | }; 32 | -------------------------------------------------------------------------------- /examples/custom-resolvers/schema/note.js: -------------------------------------------------------------------------------- 1 | // notes schema 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | }; 6 | 7 | // define new resolvers function that will create 8 | // our custom resolver 9 | exports.resolvers = ({ typedef, model }) => { 10 | // Define new resolver 'customGetNoteById' resolver 11 | // This example is just a simple resolver that gets note by given ID 12 | typedef.addResolver({ 13 | name: 'customGetNoteById', 14 | type: typedef, 15 | args: { id: 'MongoID!' }, 16 | resolve: async ({ source, args: { id }, context, info }) => { 17 | const note = await model.findById(id).lean(); 18 | return note; 19 | }, 20 | }); 21 | }; 22 | 23 | // define new compose function that will register our custom resolver 24 | // in graphql schema so we can actually use it 25 | exports.compose = ({ schemaComposer, typedef }) => { 26 | schemaComposer.Query.addFields({ 27 | noteCustomById: typedef.getResolver('customGetNoteById'), 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /examples/relations-manual/README.md: -------------------------------------------------------------------------------- 1 | # Manual relations example 2 | 3 | This example shows how to define relations for your GraphQL using Graffiti. 4 | Similarly to basic relations example - schema definition file `schema/collection.js` defines notes collection; 5 | and schema definition file `schema/note.js` create notes themselves. 6 | Finally, `exports.relations` in `schema/collection.js` is used to define custom relations between collection and notes. 7 | 8 | ## How to use 9 | 10 | Download the example: 11 | 12 | ```bash 13 | curl https://codeload.github.com/yamalight/graffiti/tar.gz/master | tar -xz --strip=2 graffiti-master/examples/relations-manual 14 | cd relations-manual 15 | ``` 16 | 17 | Install it and run: 18 | 19 | ```bash 20 | npm install 21 | # if you don't have mongo - there's an npm script to start one using docker 22 | npm run mongo 23 | npm start 24 | 25 | # or 26 | 27 | yarn 28 | yarn mongo 29 | yarn start 30 | ``` 31 | 32 | Navigate to `http://localhost:3000/graphql` to see GraphQL endpoint. 33 | -------------------------------------------------------------------------------- /packages/graffiti/test/fixtures/queries.relations-manual.js: -------------------------------------------------------------------------------- 1 | exports.CREATE_NOTE_QUERY = ` 2 | mutation CreateNote($name:String!, $body:String!, $group: MongoID!) { 3 | noteCreate(record:{name:$name, body:$body, group: $group}) { 4 | record { 5 | _id 6 | name 7 | body 8 | group { 9 | _id 10 | name 11 | } 12 | } 13 | } 14 | } 15 | `; 16 | 17 | exports.CREATE_COLLECTION_QUERY = ` 18 | mutation CreateCollection($name:String) { 19 | collectionCreate(record:{name:$name}) { 20 | record { 21 | _id 22 | name 23 | } 24 | } 25 | } 26 | `; 27 | 28 | exports.GET_NOTES_QUERY = ` 29 | query GetNotes { 30 | noteMany { 31 | _id 32 | name 33 | body 34 | group { 35 | _id 36 | name 37 | } 38 | } 39 | } 40 | `; 41 | 42 | exports.GET_COLLECTIONS_QUERY = ` 43 | query GetCollections { 44 | collectionMany { 45 | _id 46 | name 47 | notes { 48 | _id 49 | name 50 | body 51 | } 52 | } 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /packages/graffiti/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.2 / 2020-10-06 2 | 3 | - Replace fastify-gql with mercurius (was renamed) 4 | 5 | # 1.1.1 / 2020-09-16 6 | 7 | - Fix playground credentials setting 8 | 9 | # 1.1.0 / 2020-09-15 10 | 11 | - Allow using custom path for schema folder (via `basePath` config option) 12 | 13 | # 1.0.0 / 2020-09-10 14 | 15 | - First stable release with complete docs 16 | 17 | # 0.8.0 / 2020-09-10 18 | 19 | - Allow providing custom nodemon config 20 | 21 | # 0.7.0 / 2020-09-08 22 | 23 | - Add plugins support 24 | 25 | # 0.6.0 / 2020-09-01 26 | 27 | - Add nodemon-powered dev mode that autoreloads server on changes 28 | 29 | # 0.5.0 / 2020-09-01 30 | 31 | - Replace apollo-server-fastify with fastify-gql 32 | - Apply set of tweaks when passing NODE_ENV=production (see docs for more info) 33 | 34 | # 0.4.0 / 2020-08-30 35 | 36 | - Add support for custom resolvers and GraphQL methods 37 | 38 | # 0.3.0 / 2020-08-30 39 | 40 | - Add way to change / remove default GraphQL methods 41 | - Add github CI workflow for testing 42 | - Add linting & tests 43 | 44 | # 0.2.1 / 2020-08-30 45 | 46 | Initial release 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tim Ermilov 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. -------------------------------------------------------------------------------- /packages/graffiti/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graffiti", 3 | "version": "1.1.2", 4 | "description": "Minimalistic GraphQL framework", 5 | "repository": "yamalight/graffiti", 6 | "bugs": "https://github.com/yamalight/graffiti/issues", 7 | "homepage": "https://github.com/yamalight/graffiti", 8 | "bin": { 9 | "graffiti": "bin/graffiti.js" 10 | }, 11 | "main": "lib/index.js", 12 | "scripts": { 13 | "start": "./bin/graffiti.js", 14 | "lint": "eslint bin/ lib/ test/", 15 | "test": "NODE_ENV=test jest" 16 | }, 17 | "keywords": [ 18 | "graphql", 19 | "framework", 20 | "mongo", 21 | "mongoose" 22 | ], 23 | "author": "Tim Ermilov (http://codezen.net)", 24 | "license": "MIT", 25 | "dependencies": { 26 | "fastify": "^3.3.0", 27 | "graphql": "^15.3.0", 28 | "graphql-compose": "^7.19.4", 29 | "graphql-compose-mongoose": "^8.0.2", 30 | "lodash": "^4.17.20", 31 | "mercurius": "^6.0.0", 32 | "mongoose": "^5.10.0", 33 | "nodemon": "^2.0.4", 34 | "pino-pretty": "^4.2.1" 35 | }, 36 | "engines": { 37 | "node": ">=14.8.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/graffiti/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tim Ermilov 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. -------------------------------------------------------------------------------- /packages/graffiti-plugin-auth/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tim Ermilov 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. -------------------------------------------------------------------------------- /packages/graffiti-plugin-next/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tim Ermilov 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. -------------------------------------------------------------------------------- /packages/graffiti-plugin-next/index.js: -------------------------------------------------------------------------------- 1 | const Next = require('next'); 2 | 3 | const dev = 4 | process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test'; 5 | 6 | const fastifyPlugin = async (fastify, opts, next) => { 7 | // get workdir to use as next project folder 8 | const workDir = process.cwd(); 9 | 10 | // init next app 11 | const app = Next({ dev, quiet: !dev, dir: workDir }); 12 | const handle = app.getRequestHandler(); 13 | await app.prepare(); 14 | 15 | // if running in dev mode - expose HMR related things 16 | if (dev) { 17 | fastify.get('/_next/*', (req, reply) => { 18 | return handle(req.raw, reply.raw).then(() => { 19 | reply.sent = true; 20 | }); 21 | }); 22 | } 23 | 24 | fastify.all('/*', (req, reply) => { 25 | return handle(req.raw, reply.raw).then(() => { 26 | reply.sent = true; 27 | }); 28 | }); 29 | 30 | fastify.setNotFoundHandler((request, reply) => { 31 | return app.render404(request.raw, reply.raw).then(() => { 32 | reply.sent = true; 33 | }); 34 | }); 35 | 36 | next(); 37 | }; 38 | 39 | module.exports = () => { 40 | return { 41 | setup: async ({ server }) => { 42 | await server.register(fastifyPlugin); 43 | return server; 44 | }, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/graffiti/lib/graphql/createRelations.js: -------------------------------------------------------------------------------- 1 | exports.createRelations = async ({ typedefs, model }) => { 2 | // if user has defined custom relations resolution - use it 3 | if (model.relations) { 4 | await model.relations({ typedefs }); 5 | return; 6 | } 7 | 8 | // get current model key 9 | const key = model.name.toLowerCase(); 10 | 11 | // otherwise go through model properties and try to figure out 12 | // which fields need to be linked using basic resolvers 13 | const props = Object.keys(model.schema); 14 | for (const prop of props) { 15 | const propDef = model.schema[prop]; 16 | // find properties that use ObjectId as typedef 17 | if (propDef?.type === 'ObjectId') { 18 | // get target collection 19 | const target = propDef?.ref?.toLowerCase(); 20 | // skip if no target is specified 21 | if (!target) { 22 | continue; 23 | } 24 | 25 | const sourceTC = typedefs[key]; 26 | const targetTC = typedefs[target]; 27 | 28 | if (!targetTC) { 29 | throw new Error('Target type is not defined!'); 30 | } 31 | 32 | // remove existing field type 33 | sourceTC.removeField(prop); 34 | // add new field with same name as relation 35 | sourceTC.addRelation(prop, { 36 | resolver: () => targetTC.getResolver('findById'), 37 | prepareArgs: { 38 | _id: (source) => source[prop], 39 | }, 40 | projection: { [prop]: 1 }, 41 | }); 42 | } 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /docs/Basics.md: -------------------------------------------------------------------------------- 1 | # Basics 2 | 3 | ## Development / production mode 4 | 5 | Setting `NODE_ENV=production` before running your Graffiti app will ensure all required changes for production are applied. 6 | 7 | Running `graffiti dev` will start a server while watching your files for changes and reloading server upon said changes. 8 | 9 | So, usually you want your `package.json` scripts to looks something like: 10 | 11 | ```json 12 | { 13 | "name": "example-basic", 14 | "scripts": { 15 | "start": "NODE_ENV=production graffiti", 16 | "develop": "graffiti dev" 17 | }, 18 | "dependencies": { 19 | "graffiti": "*" 20 | } 21 | } 22 | ``` 23 | 24 | ## GraphQL Playground 25 | 26 | GraphQL playground only works in dev mode and is accessible at `http://localhost:3000/playground` URL (if you are running with default config). 27 | 28 | ## Configuring Graffiti.js 29 | 30 | You can provide additional options to Graffiti using `graffiti.config.js` file in your project. 31 | Supported fields are described below: 32 | 33 | ```js 34 | module.exports = { 35 | // MongoDB URL used by Mongoose for connection to DB 36 | // optional, defaults to "mongodb://localhost/graffiti" 37 | mongoUrl: 'mongodb://localhost/graffiti-test', 38 | // Port for Fastify server to listen on 39 | // optional, defaults to 3000 40 | port: 3000, 41 | // Host for Fastify server to listen on 42 | // optional, defaults to 0.0.0.0 43 | host: '0.0.0.0', 44 | // Array of plugins you want to use with graffiti 45 | plugins: [], 46 | // Path to folder with your Mongoose models 47 | // optional, defaults to "./schema" 48 | basePath: 'schema', 49 | }; 50 | ``` 51 | -------------------------------------------------------------------------------- /packages/graffiti-plugin-next/README.md: -------------------------------------------------------------------------------- 1 | # Next.js plugin for Graffiti.js 2 | 3 | Create Next.js pages with [Graffiti.js](https://github.com/yamalight/graffiti/) GraphQL backend. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install graffiti-plugin-next next react react-dom 9 | ``` 10 | 11 | Note: Next.js, React and React-DOM are peer dependencies and should be installed along with the plugin. 12 | 13 | ## Usage 14 | 15 | Create a `graffiti.config.js` in your project 16 | 17 | ```js 18 | const nextPlugin = require('graffiti-plugin-next'); 19 | 20 | module.exports = { 21 | mongoUrl: 'mongodb://localhost/graffiti', 22 | plugins: [nextPlugin()], 23 | }; 24 | ``` 25 | 26 | ## Development mode 27 | 28 | By default Graffiti will use [nodemon](https://github.com/remy/nodemon) in development mode to auto-restart server on file changes. 29 | This makes Next.js development experience suboptimal. If you wish to use hot reload provided by Next.js, you'll need to create custom `nodemon.json` config that ignores changes to `pages/` folder, e.g.: 30 | 31 | ```json 32 | { 33 | "ignore": [".git", "node_modules", "pages/**/*"] 34 | } 35 | ``` 36 | 37 | ## Building for production 38 | 39 | Please remember that to create Next.js build for production you need to execute `next build` manually as usual, e.g.: 40 | 41 | ```json 42 | { 43 | "name": "example-plugin-next", 44 | "scripts": { 45 | "start": "NODE_ENV=production graffiti", 46 | "build": "next build", 47 | "develop": "graffiti dev" 48 | }, 49 | "dependencies": { 50 | "graffiti": "*", 51 | "graffiti-plugin-next": "*", 52 | "next": "*", 53 | "react": "*", 54 | "react-dom": "*" 55 | } 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /packages/graffiti/bin/graffiti.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // parse args 3 | const args = process.argv.slice(2); 4 | const [isDev] = args; 5 | 6 | // construct entrypoint path 7 | const path = require('path'); 8 | const entrypointPath = path.join(__dirname, '..', 'index.js'); 9 | 10 | // execute main function 11 | const main = () => { 12 | // if dev arg is not passed - just start server normally 13 | if (isDev !== 'dev') { 14 | require(entrypointPath); 15 | return; 16 | } 17 | 18 | // if dev arg IS passed - run code via nodemon 19 | const nodemon = require('nodemon'); 20 | 21 | // get current workdir 22 | const currentPath = process.cwd(); 23 | 24 | // create default nodemon config 25 | let nodemonConfig = { 26 | script: entrypointPath, 27 | ignore: ['.git', 'node_modules'], 28 | ext: 'js', 29 | }; 30 | // try to read local nodemon config if present 31 | const fs = require('fs'); 32 | const localConfigPath = path.join(currentPath, 'nodemon.json'); 33 | // if it is present - merge it with default config 34 | if (fs.existsSync(localConfigPath)) { 35 | nodemonConfig = { 36 | ...nodemonConfig, 37 | ...require(localConfigPath), 38 | }; 39 | } 40 | 41 | // start nodemon 42 | nodemon(nodemonConfig); 43 | 44 | // report changes 45 | nodemon 46 | .on('start', function () { 47 | console.log('Graffiti app has started in development mode'); 48 | }) 49 | .on('quit', function () { 50 | process.exit(); 51 | }) 52 | .on('restart', function (files) { 53 | console.log('Graffiti app restarted due to changes in: '); 54 | files 55 | .map((file) => file.replace(currentPath, '')) 56 | .forEach((file) => console.log(` > ${file}`)); 57 | }); 58 | }; 59 | 60 | // invoke main function 61 | main(); 62 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js 14 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: 14 14 | - name: Get yarn cache directory path 15 | id: yarn-cache-dir-path 16 | run: echo "::set-output name=dir::$(yarn cache dir)" 17 | - name: Cache node modules 18 | uses: actions/cache@v2 19 | id: yarn-cache 20 | with: 21 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 22 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 23 | restore-keys: | 24 | ${{ runner.os }}-yarn- 25 | - name: install deps 26 | run: | 27 | yarn install --frozen-lockfile 28 | - name: lint 29 | run: | 30 | yarn lint 31 | test: 32 | runs-on: ubuntu-latest 33 | timeout-minutes: 5 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Use Node.js 14 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: 14 40 | - name: Get yarn cache directory path 41 | id: yarn-cache-dir-path 42 | run: echo "::set-output name=dir::$(yarn cache dir)" 43 | - name: Cache node modules 44 | uses: actions/cache@v2 45 | id: yarn-cache 46 | with: 47 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 48 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 49 | restore-keys: | 50 | ${{ runner.os }}-yarn- 51 | - name: install 52 | run: | 53 | yarn install --frozen-lockfile 54 | - name: test 55 | run: | 56 | yarn test-ci 57 | -------------------------------------------------------------------------------- /packages/graffiti/test/basic.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const path = require('path'); 3 | const { build } = require('../lib'); 4 | const { executeGraphql } = require('./helpers/graphql'); 5 | const { 6 | CREATE_NOTE_QUERY, 7 | GET_NOTES_QUERY, 8 | } = require('./fixtures/queries.basic'); 9 | 10 | // mock current workdir 11 | const testPath = path.join(__dirname, '..', '..', '..', 'examples', 'basic'); 12 | jest.spyOn(process, 'cwd').mockImplementation(() => testPath); 13 | 14 | // mock config to use in-mem mongo server 15 | jest.mock('../lib/config'); 16 | 17 | // global vars to store server and test utils 18 | let server; 19 | 20 | // test data 21 | const testNote = { name: 'test note', body: 'test note body' }; 22 | 23 | // cleanup after we're done 24 | afterAll(() => server?.close()); 25 | 26 | beforeAll(async () => { 27 | // build new server 28 | const fastifyServer = await build(); 29 | server = fastifyServer; 30 | // wait for it to be ready 31 | await server.ready(); 32 | }); 33 | 34 | describe('Basic setup', () => { 35 | test('Should create new note', async () => { 36 | const { 37 | data: { 38 | noteCreate: { record }, 39 | }, 40 | } = await executeGraphql({ 41 | server, 42 | mutation: CREATE_NOTE_QUERY, 43 | variables: { name: testNote.name, body: testNote.body }, 44 | }); 45 | 46 | expect(record.name).toBe(testNote.name); 47 | expect(record.body).toBe(testNote.body); 48 | }); 49 | 50 | test('Should get all notes', async () => { 51 | const { 52 | data: { noteMany: items }, 53 | } = await executeGraphql({ 54 | server, 55 | query: GET_NOTES_QUERY, 56 | }); 57 | 58 | expect(items).toHaveLength(1); 59 | expect(items[0].name).toBe(testNote.name); 60 | expect(items[0].body).toBe(testNote.body); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /examples/plugin-auth/schema/userNote.js: -------------------------------------------------------------------------------- 1 | // notes 2 | exports.schema = { 3 | name: String, 4 | body: String, 5 | // link to user 6 | user: { type: 'ObjectId', ref: 'User' }, 7 | }; 8 | 9 | // config for default queries 10 | exports.config = { 11 | // define defaults 12 | defaults: { 13 | // disable default queries and mutations 14 | queries: false, 15 | mutations: false, 16 | }, 17 | }; 18 | 19 | // define new resolvers function that will create 20 | // our custom resolver 21 | exports.resolvers = ({ typedef, model: Model }) => { 22 | // Define new resolver 'userNoteCreate' resolver 23 | // This example is just a simple resolver that creates note with current user 24 | typedef.addResolver({ 25 | name: 'userNoteCreate', 26 | type: typedef, 27 | args: { name: 'String!', body: 'String!' }, 28 | resolve: async ({ source, args: { name, body }, context, info }) => { 29 | const note = new Model({ name, body, user: context.user._id }); 30 | await note.save(); 31 | return note; 32 | }, 33 | }); 34 | 35 | // Define new resolver 'userNotes' resolver 36 | // This example is just a simple resolver that gets note for current user 37 | typedef.addResolver({ 38 | name: 'userNotes', 39 | type: [typedef], 40 | resolve: async ({ source, args: { name, body }, context, info }) => { 41 | const notes = await Model.find({ user: context.user._id }).lean(); 42 | return notes; 43 | }, 44 | }); 45 | }; 46 | 47 | // define new compose function that will register our custom resolver 48 | // in graphql schema so we can actually use it 49 | exports.compose = ({ schemaComposer, typedef }) => { 50 | schemaComposer.Query.addFields({ 51 | userNotes: typedef.getResolver('userNotes'), 52 | }); 53 | schemaComposer.Mutation.addFields({ 54 | userNoteCreate: typedef.getResolver('userNoteCreate'), 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /packages/graffiti/test/custom-folder.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const path = require('path'); 3 | const { build } = require('../lib'); 4 | const { executeGraphql } = require('./helpers/graphql'); 5 | const { 6 | CREATE_NOTE_QUERY, 7 | GET_NOTES_QUERY, 8 | } = require('./fixtures/queries.basic'); 9 | 10 | // mock current workdir 11 | const testPath = path.join( 12 | __dirname, 13 | '..', 14 | '..', 15 | '..', 16 | 'examples', 17 | 'custom-folder' 18 | ); 19 | jest.spyOn(process, 'cwd').mockImplementation(() => testPath); 20 | 21 | // mock config to use in-mem mongo server 22 | jest.mock('../lib/config'); 23 | 24 | // global vars to store server and test utils 25 | let server; 26 | 27 | // test data 28 | const testNote = { name: 'test note', body: 'test note body' }; 29 | 30 | // cleanup after we're done 31 | afterAll(() => server?.close()); 32 | 33 | beforeAll(async () => { 34 | // build new server 35 | const fastifyServer = await build(); 36 | server = fastifyServer; 37 | // wait for it to be ready 38 | await server.ready(); 39 | }); 40 | 41 | describe('Basic setup', () => { 42 | test('Should create new note', async () => { 43 | const { 44 | data: { 45 | noteCreate: { record }, 46 | }, 47 | } = await executeGraphql({ 48 | server, 49 | mutation: CREATE_NOTE_QUERY, 50 | variables: { name: testNote.name, body: testNote.body }, 51 | }); 52 | 53 | expect(record.name).toBe(testNote.name); 54 | expect(record.body).toBe(testNote.body); 55 | }); 56 | 57 | test('Should get all notes', async () => { 58 | const { 59 | data: { noteMany: items }, 60 | } = await executeGraphql({ 61 | server, 62 | query: GET_NOTES_QUERY, 63 | }); 64 | 65 | expect(items).toHaveLength(1); 66 | expect(items[0].name).toBe(testNote.name); 67 | expect(items[0].body).toBe(testNote.body); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/graffiti/test/custom-resolvers.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const path = require('path'); 3 | const { build } = require('../lib'); 4 | const { executeGraphql } = require('./helpers/graphql'); 5 | const { 6 | CREATE_NOTE_QUERY, 7 | GET_CUSTOM_NOTE_QUERY, 8 | } = require('./fixtures/queries.custom-resolvers'); 9 | 10 | // mock current workdir 11 | const testPath = path.join( 12 | __dirname, 13 | '..', 14 | '..', 15 | '..', 16 | 'examples', 17 | 'custom-resolvers' 18 | ); 19 | jest.spyOn(process, 'cwd').mockImplementation(() => testPath); 20 | 21 | // mock config to use in-mem mongo server 22 | jest.mock('../lib/config'); 23 | 24 | // global vars to store server and test utils 25 | let server; 26 | 27 | // test data 28 | const testNote = { name: 'test note', body: 'test note body' }; 29 | 30 | // created note 31 | let createdNote; 32 | 33 | // cleanup after we're done 34 | afterAll(() => server?.close()); 35 | 36 | beforeAll(async () => { 37 | // build new server 38 | const fastifyServer = await build(); 39 | server = fastifyServer; 40 | // wait for it to be ready 41 | await server.ready(); 42 | }); 43 | 44 | describe('Custom resolvers setup', () => { 45 | test('Should create new note', async () => { 46 | const { 47 | data: { 48 | noteCreate: { record }, 49 | }, 50 | } = await executeGraphql({ 51 | server, 52 | mutation: CREATE_NOTE_QUERY, 53 | variables: { name: testNote.name, body: testNote.body }, 54 | }); 55 | 56 | expect(record.name).toBe(testNote.name); 57 | expect(record.body).toBe(testNote.body); 58 | 59 | // store for future test 60 | createdNote = record; 61 | }); 62 | 63 | test('Should use custom resolver', async () => { 64 | const { 65 | data: { noteCustomById: item }, 66 | } = await executeGraphql({ 67 | server, 68 | query: GET_CUSTOM_NOTE_QUERY, 69 | variables: { id: createdNote._id }, 70 | }); 71 | 72 | expect(item.name).toBe(testNote.name); 73 | expect(item.body).toBe(testNote.body); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /docs/Plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Graffiti provides a way to extend its functionality with plugins. 4 | You can find two example plugins in this repo: 5 | 6 | - [graffiti-plugin-auth](../packages/graffiti-plugin-auth) - simple email & password-based auth 7 | - [graffiti-plugin-next](../packages/graffiti-plugin-next) - Next.js integration to allow for easy creation of frontend 8 | 9 | ## Using plugins 10 | 11 | Plugins can be added to your Graffiti project by simply create `graffiti.config.js` file and adding corresponding plugin(s) to it. 12 | 13 | In example shown below we add auth plugin and pass `secret` as option to it: 14 | 15 | ```js 16 | const authPlugin = require('graffiti-plugin-auth'); 17 | 18 | module.exports = { 19 | mongoUrl: 'mongodb://localhost/graffiti-example', 20 | plugins: [authPlugin({ secret: 'my_super_secret_jwt_secret' })], 21 | }; 22 | ``` 23 | 24 | ## Creating plugins 25 | 26 | Plugins are separate modules, that might optionally take in user options, and contain set of properties that are automatically used withing Graffiti when plugin is registered. 27 | 28 | ```js 29 | module.exports = (options) => { 30 | return { 31 | // additional schemas that should be registered 32 | schemas: [ 33 | { 34 | // new schema name, in this case - user 35 | name: 'user', 36 | // other properties follow the structure of schema/*.js files 37 | schema: { 38 | username: 'String', 39 | password: 'String', 40 | }, 41 | }, 42 | ], 43 | // setup function that has access to fastify server instance 44 | // usually, you want to add fastify plugins here 45 | setup: async ({ server }) => { 46 | await server.register(fastifyPlugin); 47 | }, 48 | // context function that allows adding data to graphql via context 49 | // in this case add `context.hello === 'world'` 50 | // to consume context - you will need to use custom resolvers 51 | context: (request, reply) => { 52 | return { 53 | hello: 'world', 54 | }; 55 | }, 56 | }; 57 | }; 58 | ``` 59 | -------------------------------------------------------------------------------- /packages/graffiti/test/plugin.next.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const path = require('path'); 3 | const { build } = require('../lib'); 4 | const { executeGraphql } = require('./helpers/graphql'); 5 | const { exec } = require('./helpers/exec'); 6 | const { CREATE_NOTE_QUERY } = require('./fixtures/queries.basic'); 7 | 8 | // mock current workdir 9 | const testPath = path.join( 10 | __dirname, 11 | '..', 12 | '..', 13 | '..', 14 | 'examples', 15 | 'plugin-next' 16 | ); 17 | jest.spyOn(process, 'cwd').mockImplementation(() => testPath); 18 | 19 | // mock config to use in-mem mongo server 20 | jest.mock('../lib/config'); 21 | 22 | // increase timeout to 30s (for nextjs builds) 23 | jest.setTimeout(30000); 24 | 25 | // global vars to store server and test utils 26 | let server; 27 | 28 | // test data 29 | const testNote = { name: 'test note', body: 'test note body' }; 30 | 31 | // cleanup after we're done 32 | afterAll(() => server?.close()); 33 | 34 | beforeAll(async () => { 35 | // run "next build" in project workdirn 36 | await exec({ workdir: testPath, command: 'npx', args: ['next', 'build'] }); 37 | // build new server 38 | const fastifyServer = await build(); 39 | server = fastifyServer; 40 | // wait for it to be ready 41 | await server.ready(); 42 | }); 43 | 44 | describe('Next.js plugin setup', () => { 45 | test('Should create new note', async () => { 46 | const { 47 | data: { 48 | noteCreate: { record }, 49 | }, 50 | } = await executeGraphql({ 51 | server, 52 | mutation: CREATE_NOTE_QUERY, 53 | variables: { name: testNote.name, body: testNote.body }, 54 | }); 55 | 56 | expect(record.name).toBe(testNote.name); 57 | expect(record.body).toBe(testNote.body); 58 | }); 59 | 60 | test('Should render home page with next.js', async () => { 61 | const res = await server.inject({ 62 | method: 'GET', 63 | url: '/', 64 | }); 65 | 66 | expect(res.body).toContain( 67 | '
Welcome to Next.js & Graffiti!
[]
' 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/graffiti-plugin-auth/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Graffiti DevMode - Login 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /packages/graffiti-plugin-auth/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Graffiti DevMode - Register 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /packages/graffiti-plugin-auth/README.md: -------------------------------------------------------------------------------- 1 | # Basic auth plugin for Graffiti.js 2 | 3 | Add basic email & password based authentication to [Graffiti.js](https://github.com/yamalight/graffiti/) GraphQL backend. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install graffiti-plugin-auth 9 | ``` 10 | 11 | ## Usage 12 | 13 | Create a `graffiti.config.js` in your project: 14 | 15 | ```js 16 | const authPlugin = require('graffiti-plugin-auth'); 17 | 18 | module.exports = { 19 | mongoUrl: 'mongodb://localhost/graffiti-example', 20 | plugins: [authPlugin({ secret: 'my_super_secret_jwt_secret' })], 21 | }; 22 | ``` 23 | 24 | ## Plugin settings 25 | 26 | Auth plugin accepts the following options during init: 27 | 28 | ```js 29 | authPlugin({ 30 | // secret used as base for JWT generation and cookies 31 | secret, 32 | // number of salt rounds used in bcrypt (optional) 33 | saltRounds = 10, 34 | // cookie settings (optional) 35 | cookie: { 36 | domain = 'localhost', 37 | httpOnly = true, 38 | secure = false, 39 | sameSite = false, 40 | } = {}, 41 | // additional permit paths that are allowed without auth (optional) 42 | permitPaths = [], 43 | // redirect path, executed when user requests text/html and is not authed (optional) 44 | redirectPath, 45 | }); 46 | ``` 47 | 48 | ## Dev-mode auth forms 49 | 50 | For convenience, when running in development mode, auth plugin creates two pages `/dev/register` and `/dev/login` that allow you to register and login without setting up any front-end. 51 | 52 | ## Accessing current user 53 | 54 | You might need to access current user in some cases. 55 | This can be either done from fastify (through `request.user`), or through GraphQL context, e.g.: 56 | 57 | ```js 58 | // define new resolvers function that will create our custom resolver 59 | exports.resolvers = ({ typedef, model: Model }) => { 60 | // Define new resolver 'myResolver' resolver 61 | // This example is just a simple resolver that shows how to access current user 62 | typedef.addResolver({ 63 | name: 'myResolver', 64 | type: typedef, 65 | resolve: async ({ source, args, context, info }) => { 66 | // This is your current authed user 67 | const currentUser = context.user; 68 | // ... 69 | return result; 70 | }, 71 | }); 72 | }; 73 | ``` 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Exoframe 2 | 3 | > Graffiti.js is a minimalistic GraphQL framework 4 | 5 | ## How to use 6 | 7 | Install it: 8 | 9 | ``` 10 | $ npm install graffiti --save 11 | ``` 12 | 13 | After that, the file-system is the main API. Every `.js` file becomes a schema definition that gets automatically processed and converted to GraphQL API. 14 | 15 | Populate `./schema/note.js` inside your project: 16 | 17 | ```js 18 | // export new Mongoose.js schema definition 19 | exports.schema = { 20 | name: String, 21 | body: String, 22 | // NOTE: Mongoose model names will always be capitalized versions of your filenames 23 | group: { type: 'ObjectId', ref: 'Collection' }, 24 | }; 25 | ``` 26 | 27 | and `/schema/collection.js`: 28 | 29 | ```js 30 | exports.schema = { 31 | name: String, 32 | }; 33 | ``` 34 | 35 | and then just run `graffiti dev` and go to `http://localhost:3000/playground` 36 | 37 | So far, you get: 38 | 39 | - Automatic creation of GraphQL APIs 40 | - Automatic relations between types (when using `ObjectId` as type) 41 | - Access to GraphQL playground (in development mode) 42 | - Way to add manual resolvers or GraphQL methods 43 | - Way to setup manual complex relations 44 | - Automatic app reload on schema changes (in development mode) 45 | - Extensibility via third-party plugins 46 | 47 | ## Requirements 48 | 49 | Graffiti assumes you have: 50 | 51 | - MongoDB v4.0+ (easiest way is to start one using docker: `docker run --name mongodb -p 27017:27017 -d mongo`) 52 | - Node.js v14.8+ 53 | 54 | ## How it works 55 | 56 | Graffiti.js is built on top of [fastify](https://www.fastify.io/), [graphql-compose](https://graphql-compose.github.io/) and [Mongoose](https://mongoosejs.com/). 57 | Graffiti is heavily inspired by awesome [Next.js](https://nextjs.org/) and is mostly there to remove the need to write boilerplate code yourself. 58 | 59 | You can find detailed documentation in [`./docs` folder](./docs/README.md). 60 | 61 | You can also find more examples in [`./examples` folder](./examples). 62 | 63 | ## Special thanks 64 | 65 | A huge thank you to: 66 | 67 | - [Jay Phelps](https://github.com/jayphelps) for releasing the "graffiti" npm package name to me! 68 | - [Ivan Semenov](https://www.behance.net/ivan_semenov) for making [an awesome logo](./logo/README.md) 69 | -------------------------------------------------------------------------------- /packages/graffiti/test/default-queries.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const path = require('path'); 3 | const { build } = require('../lib'); 4 | const { executeGraphql } = require('./helpers/graphql'); 5 | const { 6 | CREATE_NOTE_QUERY, 7 | UPDATE_NOTE_QUERY, 8 | GET_NOTE_QUERY, 9 | GET_NOTES_QUERY, 10 | } = require('./fixtures/queries.default-queries'); 11 | 12 | // mock current workdir 13 | const testPath = path.join( 14 | __dirname, 15 | '..', 16 | '..', 17 | '..', 18 | 'examples', 19 | 'default-queries' 20 | ); 21 | jest.spyOn(process, 'cwd').mockImplementation(() => testPath); 22 | 23 | // mock config to use in-mem mongo server 24 | jest.mock('../lib/config'); 25 | 26 | // global vars to store server and test utils 27 | let server; 28 | 29 | // test data 30 | const testNote = { name: 'test note', body: 'test note body' }; 31 | 32 | // created note 33 | let createdNote; 34 | 35 | // cleanup after we're done 36 | afterAll(() => server?.close()); 37 | 38 | beforeAll(async () => { 39 | // build new server 40 | const fastifyServer = await build(); 41 | server = fastifyServer; 42 | // wait for it to be ready 43 | await server.ready(); 44 | }); 45 | 46 | describe('Default queries setup', () => { 47 | test('Should create new note', async () => { 48 | const { 49 | data: { 50 | noteCreate: { record }, 51 | }, 52 | } = await executeGraphql({ 53 | server, 54 | mutation: CREATE_NOTE_QUERY, 55 | variables: { name: testNote.name, body: testNote.body }, 56 | }); 57 | 58 | expect(record.name).toBe(testNote.name); 59 | expect(record.body).toBe(testNote.body); 60 | 61 | // store for future tests 62 | createdNote = record; 63 | }); 64 | 65 | test('Should get note by id', async () => { 66 | const { 67 | data: { noteById: item }, 68 | } = await executeGraphql({ 69 | server, 70 | query: GET_NOTE_QUERY, 71 | variables: { id: createdNote._id }, 72 | }); 73 | 74 | expect(item.name).toBe(testNote.name); 75 | expect(item.body).toBe(testNote.body); 76 | }); 77 | 78 | test('Should fail to update note', async () => { 79 | const { errors } = await executeGraphql({ 80 | server, 81 | mutation: UPDATE_NOTE_QUERY, 82 | variables: { id: createdNote._id, name: 'up', body: 'fail' }, 83 | }); 84 | 85 | expect(errors).toMatchSnapshot(); 86 | }); 87 | 88 | test('Should fail to get all notes', async () => { 89 | const { errors } = await executeGraphql({ 90 | server, 91 | query: GET_NOTES_QUERY, 92 | }); 93 | 94 | expect(errors).toMatchSnapshot(); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /packages/graffiti/test/relations.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const path = require('path'); 3 | const { build } = require('../lib'); 4 | const { executeGraphql } = require('./helpers/graphql'); 5 | const { 6 | CREATE_COLLECTION_QUERY, 7 | CREATE_NOTE_QUERY, 8 | GET_NOTES_QUERY, 9 | } = require('./fixtures/queries.relations'); 10 | 11 | // mock current workdir 12 | const testPath = path.join( 13 | __dirname, 14 | '..', 15 | '..', 16 | '..', 17 | 'examples', 18 | 'relations' 19 | ); 20 | jest.spyOn(process, 'cwd').mockImplementation(() => testPath); 21 | 22 | // mock config to use in-mem mongo server 23 | jest.mock('../lib/config'); 24 | 25 | // global vars to store server and test utils 26 | let server; 27 | 28 | // test data 29 | const testNote = { name: 'test note', body: 'test note body' }; 30 | const testCollection = { name: 'test collection' }; 31 | 32 | // create data 33 | let createdCollection; 34 | 35 | // cleanup after we're done 36 | afterAll(() => server?.close()); 37 | 38 | beforeAll(async () => { 39 | // build new server 40 | const fastifyServer = await build(); 41 | server = fastifyServer; 42 | // wait for it to be ready 43 | await server.ready(); 44 | }); 45 | 46 | describe('Relations setup', () => { 47 | test('Should create new collection', async () => { 48 | const { 49 | data: { 50 | collectionCreate: { record }, 51 | }, 52 | } = await executeGraphql({ 53 | server, 54 | mutation: CREATE_COLLECTION_QUERY, 55 | variables: { name: testCollection.name }, 56 | }); 57 | 58 | expect(record.name).toBe(testCollection.name); 59 | 60 | // store new collection for next tests 61 | createdCollection = record; 62 | }); 63 | 64 | test('Should create new note', async () => { 65 | const { 66 | data: { 67 | noteCreate: { record }, 68 | }, 69 | } = await executeGraphql({ 70 | server, 71 | mutation: CREATE_NOTE_QUERY, 72 | variables: { 73 | name: testNote.name, 74 | body: testNote.body, 75 | group: createdCollection._id, 76 | }, 77 | }); 78 | 79 | expect(record.name).toBe(testNote.name); 80 | expect(record.body).toBe(testNote.body); 81 | expect(record.group._id).toBe(createdCollection._id); 82 | expect(record.group.name).toBe(createdCollection.name); 83 | }); 84 | 85 | test('Should get all notes', async () => { 86 | const { 87 | data: { noteMany: items }, 88 | } = await executeGraphql({ 89 | server, 90 | query: GET_NOTES_QUERY, 91 | }); 92 | 93 | expect(items).toHaveLength(1); 94 | expect(items[0].name).toBe(testNote.name); 95 | expect(items[0].body).toBe(testNote.body); 96 | expect(items[0].group._id).toBe(createdCollection._id); 97 | expect(items[0].group.name).toBe(createdCollection.name); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /packages/graffiti/lib/graphql/index.js: -------------------------------------------------------------------------------- 1 | const { readdir } = require('fs/promises'); 2 | const { schemaComposer } = require('graphql-compose'); 3 | const { join } = require('path'); 4 | const { buildModel } = require('../mongoose'); 5 | const { createGraphQLType } = require('./createType'); 6 | const { createRelations } = require('./createRelations'); 7 | 8 | exports.buildSchema = async ({ db, plugins, projectConfig }) => { 9 | // get current work folder 10 | const workFolder = process.cwd(); 11 | // construct path to schema folder 12 | const schemaFolder = join(workFolder, projectConfig.basePath); 13 | 14 | // create array to hold new models 15 | const models = []; 16 | // get list of files in schema folder 17 | const fileList = await readdir(schemaFolder); 18 | // iterate over files and create graphql type defs 19 | for (const filename of fileList) { 20 | // create module path from filename 21 | const modulePath = join(schemaFolder, filename); 22 | // require given path 23 | const { 24 | schema, 25 | config, 26 | relations, 27 | resolvers, 28 | compose, 29 | } = require(modulePath); 30 | // derive module name from filename 31 | const name = filename.replace(/\.js$/, ''); 32 | // push info into models list 33 | models.push({ name, config, schema, relations, resolvers, compose }); 34 | } 35 | 36 | // iterate over plugins and create graphql type defs if given 37 | for (const plugin of plugins) { 38 | // if plugin doesn't export schemas - just skip it 39 | if (!plugin.schemas?.length) { 40 | continue; 41 | } 42 | // iterate over schemas exported from plugin 43 | for (const pluginSchema of plugin.schemas) { 44 | // get data from plugin schema 45 | const { 46 | name, 47 | schema, 48 | config, 49 | relations, 50 | resolvers, 51 | compose, 52 | } = pluginSchema; 53 | // push info into models list 54 | models.push({ name, config, schema, relations, resolvers, compose }); 55 | } 56 | } 57 | 58 | // create new name->modelTC mapping 59 | const typedefs = {}; 60 | // create new name->mongo model mappings 61 | const mongoModels = {}; 62 | // iterate over models and create graphql type defs 63 | for (const model of models) { 64 | // create key to store resulting model 65 | const key = model.name.toLowerCase(); 66 | // create mongo model 67 | const mongoModel = buildModel({ 68 | db, 69 | schema: model.schema, 70 | name: model.name, 71 | }); 72 | // create new graphql typedef with default methods and resolvers 73 | const modelTc = createGraphQLType({ 74 | mongoModel, 75 | config: model.config, 76 | name: model.name, 77 | }); 78 | // apply custom resolvers if needed 79 | model.resolvers?.({ typedef: modelTc, model: mongoModel }); 80 | // compose custom methods if needed 81 | model.compose?.({ schemaComposer, typedef: modelTc }); 82 | // store model and typedef 83 | typedefs[key] = modelTc; 84 | mongoModels[key] = mongoModel; 85 | } 86 | 87 | // iterate over models and setup relation 88 | for (const model of models) { 89 | await createRelations({ typedefs, model }); 90 | } 91 | 92 | const graphqlSchema = schemaComposer.buildSchema(); 93 | return { graphqlSchema, schemaComposer, typedefs, mongoModels }; 94 | }; 95 | -------------------------------------------------------------------------------- /packages/graffiti/lib/index.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify'); 2 | const GQL = require('mercurius'); 3 | const { buildSchema } = require('./graphql'); 4 | const { loadPlugins } = require('./plugins'); 5 | const { connect } = require('./mongoose'); 6 | const { getConfig } = require('./config'); 7 | 8 | // detect if we're running in production 9 | const isProduction = process.env.NODE_ENV === 'production'; 10 | const isTesting = process.env.NODE_ENV === 'test'; 11 | // change default logging level based on production state 12 | const loggingLevel = isProduction || isTesting ? 'error' : 'info'; 13 | // pass logging settings to fastify config 14 | const fastifyConfig = { 15 | logger: { 16 | prettyPrint: !isProduction, 17 | level: loggingLevel, 18 | }, 19 | }; 20 | // playground config 21 | const playgroundConf = { 22 | graphiql: 'playground', 23 | playgroundSettings: { 24 | 'request.credentials': 'include', 25 | }, 26 | }; 27 | 28 | // Build the server 29 | const build = async () => { 30 | // get config for settings 31 | const projectConfig = getConfig(); 32 | // connect to db 33 | const db = await connect({ projectConfig }); 34 | // create fastify instance 35 | const server = fastify(fastifyConfig); 36 | // load plugins 37 | const plugins = await loadPlugins({ projectConfig }); 38 | // create graphql schema 39 | const { 40 | graphqlSchema, 41 | schemaComposer, 42 | typedefs, 43 | mongoModels, 44 | } = await buildSchema({ 45 | db, 46 | plugins, 47 | projectConfig, 48 | }); 49 | // expose newly constructed typedefs, models and composer using fastify 50 | server.decorate('graffiti', { 51 | schemaComposer, 52 | typedefs, 53 | mongoModels, 54 | }); 55 | // construct fastify server 56 | // database cleanup on close 57 | await server.register(async (instance, opts, done) => { 58 | instance.addHook('onClose', async (_instance, done) => { 59 | db.close(); 60 | done(); 61 | }); 62 | done(); 63 | }); 64 | // apply plugins to fastify 65 | await Promise.all(plugins.map((plugin) => plugin.setup?.({ server }))); 66 | // register graphql with new schema server in fastify 67 | await server.register(GQL, { 68 | schema: graphqlSchema, 69 | // only enable playground in dev mode 70 | ...(isProduction ? undefined : playgroundConf), 71 | context: (request, reply) => { 72 | // apply plugins context functions if available 73 | const context = plugins 74 | // get new context from plugins 75 | .map((plugin) => plugin.context?.(request, reply)) 76 | // remove empty values 77 | .filter((context) => context !== undefined) 78 | // reduce to object 79 | .reduce((acc, val) => ({ ...acc, ...val }), {}); 80 | return context; 81 | }, 82 | }); 83 | return server; 84 | }; 85 | exports.build = build; 86 | 87 | // Run the server 88 | exports.start = async () => { 89 | try { 90 | // create graphql server 91 | const instance = await build(); 92 | // get config for host-port settings 93 | const config = getConfig(); 94 | await instance.listen(config.port, config.host); 95 | // log port 96 | console.log(`Graffiti started on ${instance.server.address().port}`); 97 | } catch (err) { 98 | console.error('Error starting Graffiti server:', err); 99 | process.exit(1); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /packages/graffiti/lib/graphql/createType.js: -------------------------------------------------------------------------------- 1 | const { schemaComposer } = require('graphql-compose'); 2 | const { composeWithMongoose } = require('graphql-compose-mongoose'); 3 | 4 | // graphql-compose options 5 | const customizationOptions = {}; 6 | 7 | exports.createGraphQLType = ({ mongoModel, config, name }) => { 8 | const modelTC = composeWithMongoose(mongoModel, customizationOptions); 9 | 10 | // generate prefix based on model name 11 | const prefix = name.toLowerCase(); 12 | 13 | // determine whether we need to create mutations based on model config 14 | const createQueries = config?.defaults?.queries !== false; 15 | if (createQueries) { 16 | // default queries 17 | const queries = {}; 18 | // add queries if they are not explicitly disabled 19 | if (config?.defaults?.queries?.byId !== false) { 20 | queries[`${prefix}ById`] = modelTC.getResolver('findById'); 21 | } 22 | if (config?.defaults?.queries?.byIds !== false) { 23 | queries[`${prefix}ByIds`] = modelTC.getResolver('findByIds'); 24 | } 25 | if (config?.defaults?.queries?.one !== false) { 26 | queries[`${prefix}One`] = modelTC.getResolver('findOne'); 27 | } 28 | if (config?.defaults?.queries?.many !== false) { 29 | queries[`${prefix}Many`] = modelTC.getResolver('findMany'); 30 | } 31 | if (config?.defaults?.queries?.total !== false) { 32 | queries[`${prefix}Total`] = modelTC.getResolver('count'); 33 | } 34 | if (config?.defaults?.queries?.connection !== false) { 35 | queries[`${prefix}Connection`] = modelTC.getResolver('connection'); 36 | } 37 | if (config?.defaults?.queries?.pagination !== false) { 38 | queries[`${prefix}Pagination`] = modelTC.getResolver('pagination'); 39 | } 40 | // register queries using composer 41 | schemaComposer.Query.addFields(queries); 42 | } 43 | 44 | // determine whether we need to create mutations based on model config 45 | const createMutations = config?.defaults?.mutations !== false; 46 | if (createMutations) { 47 | // default mutations 48 | const mutations = {}; 49 | // add queries if they are not explicitly disabled 50 | if (config?.defaults?.mutations?.create !== false) { 51 | mutations[`${prefix}Create`] = modelTC.getResolver('createOne'); 52 | } 53 | if (config?.defaults?.mutations?.createMany !== false) { 54 | mutations[`${prefix}CreateMany`] = modelTC.getResolver('createMany'); 55 | } 56 | if (config?.defaults?.mutations?.updateById !== false) { 57 | mutations[`${prefix}UpdateById`] = modelTC.getResolver('updateById'); 58 | } 59 | if (config?.defaults?.mutations?.updateOne !== false) { 60 | mutations[`${prefix}UpdateOne`] = modelTC.getResolver('updateOne'); 61 | } 62 | if (config?.defaults?.mutations?.updateMany !== false) { 63 | mutations[`${prefix}UpdateMany`] = modelTC.getResolver('updateMany'); 64 | } 65 | if (config?.defaults?.mutations?.removeById !== false) { 66 | mutations[`${prefix}RemoveById`] = modelTC.getResolver('removeById'); 67 | } 68 | if (config?.defaults?.mutations?.removeOne !== false) { 69 | mutations[`${prefix}RemoveOne`] = modelTC.getResolver('removeOne'); 70 | } 71 | if (config?.defaults?.mutations?.removeMany !== false) { 72 | mutations[`${prefix}RemoveMany`] = modelTC.getResolver('removeMany'); 73 | } 74 | // register mutations using composer 75 | schemaComposer.Mutation.addFields(mutations); 76 | } 77 | 78 | return modelTC; 79 | }; 80 | -------------------------------------------------------------------------------- /packages/graffiti/test/relations-manual.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const path = require('path'); 3 | const { build } = require('../lib'); 4 | const { executeGraphql } = require('./helpers/graphql'); 5 | const { 6 | CREATE_COLLECTION_QUERY, 7 | CREATE_NOTE_QUERY, 8 | GET_NOTES_QUERY, 9 | GET_COLLECTIONS_QUERY, 10 | } = require('./fixtures/queries.relations-manual'); 11 | 12 | // mock current workdir 13 | const testPath = path.join( 14 | __dirname, 15 | '..', 16 | '..', 17 | '..', 18 | 'examples', 19 | 'relations-manual' 20 | ); 21 | jest.spyOn(process, 'cwd').mockImplementation(() => testPath); 22 | 23 | // mock config to use in-mem mongo server 24 | jest.mock('../lib/config'); 25 | 26 | // global vars to store server and test utils 27 | let server; 28 | 29 | // test data 30 | const testNote = { name: 'test note', body: 'test note body' }; 31 | const testCollection = { name: 'test collection' }; 32 | 33 | // create data 34 | let createdCollection; 35 | let createdNote; 36 | 37 | // cleanup after we're done 38 | afterAll(() => server?.close()); 39 | 40 | beforeAll(async () => { 41 | // build new server 42 | const fastifyServer = await build(); 43 | server = fastifyServer; 44 | // wait for it to be ready 45 | await server.ready(); 46 | }); 47 | 48 | describe('Manual relations setup', () => { 49 | test('Should create new collection', async () => { 50 | const { 51 | data: { 52 | collectionCreate: { record }, 53 | }, 54 | } = await executeGraphql({ 55 | server, 56 | mutation: CREATE_COLLECTION_QUERY, 57 | variables: { name: testCollection.name }, 58 | }); 59 | 60 | expect(record.name).toBe(testCollection.name); 61 | 62 | // store new collection for next tests 63 | createdCollection = record; 64 | }); 65 | 66 | test('Should create new note', async () => { 67 | const { 68 | data: { 69 | noteCreate: { record }, 70 | }, 71 | } = await executeGraphql({ 72 | server, 73 | mutation: CREATE_NOTE_QUERY, 74 | variables: { 75 | name: testNote.name, 76 | body: testNote.body, 77 | group: createdCollection._id, 78 | }, 79 | }); 80 | 81 | expect(record.name).toBe(testNote.name); 82 | expect(record.body).toBe(testNote.body); 83 | expect(record.group._id).toBe(createdCollection._id); 84 | expect(record.group.name).toBe(createdCollection.name); 85 | 86 | // store for future tests 87 | createdNote = record; 88 | }); 89 | 90 | test('Should get all notes', async () => { 91 | const { 92 | data: { noteMany: items }, 93 | } = await executeGraphql({ 94 | server, 95 | query: GET_NOTES_QUERY, 96 | }); 97 | 98 | expect(items).toHaveLength(1); 99 | expect(items[0].name).toBe(testNote.name); 100 | expect(items[0].body).toBe(testNote.body); 101 | expect(items[0].group._id).toBe(createdCollection._id); 102 | expect(items[0].group.name).toBe(createdCollection.name); 103 | }); 104 | 105 | test('Should get collection with notes', async () => { 106 | const { 107 | data: { collectionMany: items }, 108 | } = await executeGraphql({ 109 | server, 110 | query: GET_COLLECTIONS_QUERY, 111 | }); 112 | 113 | expect(items).toHaveLength(1); 114 | expect(items[0].name).toBe(createdCollection.name); 115 | expect(items[0].notes[0]._id).toBe(createdNote._id); 116 | expect(items[0].notes[0].name).toBe(createdNote.name); 117 | expect(items[0].notes[0].body).toBe(createdNote.body); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /logo/svg/symbol_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /logo/svg/symbol_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js 14 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 14 16 | - name: Get yarn cache directory path 17 | id: yarn-cache-dir-path 18 | run: echo "::set-output name=dir::$(yarn cache dir)" 19 | - name: Cache node modules 20 | uses: actions/cache@v2 21 | id: yarn-cache 22 | with: 23 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 24 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-yarn- 27 | - name: install deps 28 | run: | 29 | yarn install 30 | - name: lint 31 | run: | 32 | yarn lint 33 | test: 34 | runs-on: ubuntu-latest 35 | timeout-minutes: 5 36 | steps: 37 | - uses: actions/checkout@v2 38 | - name: Use Node.js 14 39 | uses: actions/setup-node@v1 40 | with: 41 | node-version: 14 42 | - name: Get yarn cache directory path 43 | id: yarn-cache-dir-path 44 | run: echo "::set-output name=dir::$(yarn cache dir)" 45 | - name: Cache node modules 46 | uses: actions/cache@v2 47 | id: yarn-cache 48 | with: 49 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 50 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 51 | restore-keys: | 52 | ${{ runner.os }}-yarn- 53 | - name: install 54 | run: | 55 | yarn install 56 | - name: test 57 | run: | 58 | yarn test-ci 59 | publish-npm: 60 | # only publish if not pre-release 61 | if: '!github.event.release.prerelease' 62 | # only publish if lint / test succeeded 63 | needs: [lint, test] 64 | runs-on: ubuntu-latest 65 | steps: 66 | - uses: actions/checkout@v2 67 | - name: Use Node.js 14 68 | uses: actions/setup-node@v1 69 | with: 70 | node-version: 14 71 | registry-url: 'https://registry.npmjs.org' 72 | - name: Get yarn cache directory path 73 | id: yarn-cache-dir-path 74 | run: echo "::set-output name=dir::$(yarn cache dir)" 75 | - name: Cache node modules 76 | uses: actions/cache@v2 77 | id: yarn-cache 78 | with: 79 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 80 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 81 | restore-keys: | 82 | ${{ runner.os }}-yarn- 83 | - name: install deps 84 | run: | 85 | yarn --frozen-lockfile 86 | - name: publish graffiti to npm if newer version 87 | run: | 88 | export CURRENT_VERSION=$(node -pe "require('./packages/graffiti/package.json').version") 89 | export PUBLISHED_VERSION=$(yarn info graffiti | grep latest | sed "s/[( +?)latest:,\',]//g") 90 | if [ "$CURRENT_VERSION" != "$PUBLISHED_VERSION" ]; then yarn workspace graffiti publish --non-interactive; fi 91 | env: 92 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 93 | - name: publish graffiti-plugin-next to npm if newer version 94 | run: | 95 | export CURRENT_VERSION=$(node -pe "require('./packages/graffiti-plugin-next/package.json').version") 96 | export PUBLISHED_VERSION=$(yarn info graffiti-plugin-next | grep latest | sed "s/[( +?)latest:,\',]//g") 97 | if [ "$CURRENT_VERSION" != "$PUBLISHED_VERSION" ]; then yarn workspace graffiti-plugin-next publish --non-interactive; fi 98 | env: 99 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 100 | - name: publish graffiti-plugin-auth to npm if newer version 101 | run: | 102 | export CURRENT_VERSION=$(node -pe "require('./packages/graffiti-plugin-auth/package.json').version") 103 | export PUBLISHED_VERSION=$(yarn info graffiti-plugin-auth | grep latest | sed "s/[( +?)latest:,\',]//g") 104 | if [ "$CURRENT_VERSION" != "$PUBLISHED_VERSION" ]; then yarn workspace graffiti-plugin-auth publish --non-interactive; fi 105 | env: 106 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 107 | -------------------------------------------------------------------------------- /docs/Advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | 3 | ## Manual relations 4 | 5 | When you need to manually specify relations between models, you can do so by specifying custom `relations` functions as exports from your schema definition file. 6 | For example, if we'd want to be able to list all notes in given collection for example above, you'd do: 7 | 8 | ```jsx 9 | // define notes collection in schema/collection.js 10 | exports.schema = { 11 | name: String, 12 | }; 13 | 14 | // define custom relation that resolves notes 15 | exports.relations = ({ typedefs }) => { 16 | // define relation between collection and notes 17 | // NOTE: typedefs will always be lowercased versions of your filenames 18 | typedefs.collection.addRelation('notes', { 19 | resolver: () => typedefs.note.getResolver('findMany'), 20 | prepareArgs: { 21 | group: (source) => source._id, 22 | }, 23 | projection: { _id: 1 }, 24 | }); 25 | }; 26 | ``` 27 | 28 | Property `typedefs` your function will receive here is an object that contains [graphql-compose types](https://graphql-compose.github.io/docs/basics/understanding-types.html) mapped to corresponding (lowercased) file names. 29 | E.g. if you defined `schema/collection.js` schema as described above, it would be accessible via `typedefs.collection`. 30 | 31 | ## Disabling default queries and mutations 32 | 33 | By default, Graffiti generates all possible mutations and queries for given schema. 34 | That might not always be desirable. 35 | You can selectively disable mutations and queries by exporting `config` object from your schema definition file. 36 | E.g.: 37 | 38 | ```js 39 | // notes Mongoose schema 40 | exports.schema = { 41 | name: String, 42 | body: String, 43 | }; 44 | 45 | // config for default queries 46 | exports.config = { 47 | // define defaults 48 | defaults: { 49 | // change what queries are allowed 50 | queries: { 51 | byId: true, // normally, this can be omitted since `true` is default 52 | byIds: false, // will disable this query 53 | one: false, 54 | many: false, 55 | total: false, 56 | connection: false, 57 | pagination: false, 58 | }, 59 | // you can also use `mutations: false` to disable all mutations (or queries) 60 | mutations: { 61 | create: true, 62 | createMany: false, // will disable this mutation 63 | updateById: false, 64 | updateOne: false, 65 | updateMany: false, 66 | removeById: false, 67 | removeOne: false, 68 | removeMany: false, 69 | }, 70 | }, 71 | }; 72 | ``` 73 | 74 | ## Custom resolvers 75 | 76 | There are cases when you might want to extend your schema with custom resolvers. 77 | Graffiti provides a way to create custom resolvers and then add them to your GraphQL schema. 78 | Resolvers can be created using `exports.resolvers` function in your schema definition file. 79 | While registering them in GraphQL schema is achieved by using `exports.compose` function. 80 | E.g.: 81 | 82 | ```js 83 | // notes schema 84 | exports.schema = { 85 | name: String, 86 | body: String, 87 | }; 88 | 89 | // define new resolvers function that will create 90 | // our custom resolver 91 | exports.resolvers = ({ typedef, model }) => { 92 | // Define new resolver 'customGetNoteById' resolver 93 | // This example is just a simple resolver that gets note by given ID 94 | typedef.addResolver({ 95 | name: 'customGetNoteById', 96 | type: typedef, 97 | args: { id: 'MongoID!' }, 98 | resolve: async ({ source, args: { id }, context, info }) => { 99 | const note = await model.findById(id).lean(); 100 | return note; 101 | }, 102 | }); 103 | }; 104 | 105 | // define new compose function that will register our custom resolver 106 | // in graphql schema so we can actually use it 107 | exports.compose = ({ schemaComposer, typedef }) => { 108 | schemaComposer.Query.addFields({ 109 | noteCustomById: typedef.getResolver('customGetNoteById'), 110 | }); 111 | }; 112 | ``` 113 | 114 | Property `model` your `resolvers` function will receive here is a [Mongoose model](https://mongoosejs.com/docs/models.html) generated from schema definition in current file. 115 | 116 | Property `schemaComposer` passed to `compose` function is instance of [SchemaCompose from graphql-compose](https://graphql-compose.github.io/docs/api/SchemaComposer.html), while `typedef` is a [graphql-compose type](https://graphql-compose.github.io/docs/basics/understanding-types.html) generated from Mongoose model created in current file. 117 | 118 | ## Ignoring files and folders to prevent reloads during development mode 119 | 120 | By default Graffiti will use [nodemon](https://github.com/remy/nodemon) in development mode to auto-restart server on file changes. 121 | This can lead to undesirable reloads (e.g. when working with Next.js). 122 | To change that behaviour you can create a custom `nodemon.json` config that ignores specific files or folders (e.g. `pages/` in case of Next.js): 123 | 124 | ```json 125 | { 126 | "ignore": [".git", "node_modules", "pages/**/*"] 127 | } 128 | ``` 129 | -------------------------------------------------------------------------------- /docs/TutorialNotesNext.md: -------------------------------------------------------------------------------- 1 | # Build a simple note-taking app with Graffiti and Next 2 | 3 | ## Prerequisites 4 | 5 | - Node.js v14 6 | - MongoDB: 7 | - install it locally 8 | - start via docker (e.g. `docker run --name mongodb -p 27017:27017 -d mongo`) 9 | 10 | ## Step 1: Create new project 11 | 12 | ```sh 13 | mkdir graffiti-notes && cd graffiti-notes 14 | npm init -y 15 | ``` 16 | 17 | ## Step 2: Install Graffiti 18 | 19 | ```sh 20 | npm install graffiti 21 | ``` 22 | 23 | ## Step 3: Add Graffiti start and dev scripts to package.json 24 | 25 | ```json 26 | { 27 | // rest of your package.json 28 | "scripts": { 29 | "start": "NODE_ENV=production graffiti", 30 | "develop": "graffiti dev" 31 | } 32 | // rest of your package.json 33 | } 34 | ``` 35 | 36 | ## Step 4: Define notes schema 37 | 38 | First, create new schema folder: 39 | 40 | ```sh 41 | mkdir schema 42 | ``` 43 | 44 | Then, create new `note.js` file in it and describe our notes schema: 45 | 46 | ```js 47 | // schema/note.js 48 | exports.schema = { 49 | name: String, 50 | body: String, 51 | }; 52 | ``` 53 | 54 | ## Step 5: Test in playground 55 | 56 | Make sure your MongoDB is running. 57 | Once it is - execute `npm run develop` to start your Graffiti app in dev mode 58 | and navigate to [http://localhost:3000/playground](http://localhost:3000/playground). 59 | 60 | Playground should now allow you to execute variety of queries and mutations for notes. 61 | 62 | ## Step 6: Add Next.js plugin 63 | 64 | First, install the plugin using npm: 65 | 66 | ```sh 67 | npm install graffiti-plugin-next 68 | ``` 69 | 70 | Then, create new file `graffiti.config.js` and add the following: 71 | 72 | ```js 73 | const nextPlugin = require('graffiti-plugin-next'); 74 | 75 | module.exports = { 76 | mongoUrl: 'mongodb://localhost/graffiti', 77 | plugins: [nextPlugin()], 78 | }; 79 | ``` 80 | 81 | ## Step 7: Add new page 82 | 83 | First, create `pages/` folder for Next.js to use: 84 | 85 | ```sh 86 | mkdir pages 87 | ``` 88 | 89 | Then, create new `index.js` file and create your page using React (as you normally would using Next.js): 90 | 91 | ```js 92 | export default () =>

Hello Graffiti + Next.js!

; 93 | ``` 94 | 95 | ## Step 8: Test in browser 96 | 97 | Execute `npm run develop` to start your Graffiti app in dev mode 98 | and navigate to [http://localhost:3000/](http://localhost:3000/). 99 | 100 | You should see your newly created React page. 101 | 102 | ## Step 9: Add notes creation and browsing 103 | 104 | First, let's add simplest GraphQL client for us to use - we'll use [graphql-request](https://github.com/prisma-labs/graphql-request): 105 | 106 | ```sh 107 | npm install graphql-request graphql 108 | ``` 109 | 110 | Edit your `pages/index.js` and create new form for notes creation and new list of current notes, e.g.: 111 | 112 | ```js 113 | import { gql, GraphQLClient } from 'graphql-request'; 114 | import { useState } from 'react'; 115 | 116 | // create a GraphQL client instance to send requests 117 | const client = new GraphQLClient('http://localhost:3000/graphql', { 118 | headers: {}, 119 | }); 120 | 121 | // define notes query 122 | const notesQuery = gql` 123 | { 124 | noteMany { 125 | _id 126 | name 127 | body 128 | } 129 | } 130 | `; 131 | 132 | // define create note mutation 133 | const createNoteQuery = gql` 134 | mutation AddNote($name: String!, $body: String!) { 135 | noteCreate(record: { name: $name, body: $body }) { 136 | record { 137 | _id 138 | name 139 | body 140 | } 141 | } 142 | } 143 | `; 144 | 145 | // define simple create note function that returns new note 146 | const createNote = async ({ name, body }) => { 147 | const variables = { 148 | name, 149 | body, 150 | }; 151 | const data = await client.request(createNoteQuery, variables); 152 | return data?.noteCreate?.record; 153 | }; 154 | 155 | // define our page 156 | export default ({ notes }) => { 157 | const [allNotes, setAllNotes] = useState(notes); 158 | const [name, setName] = useState(); 159 | const [body, setBody] = useState(); 160 | 161 | // add button handler 162 | const handleAdd = async () => { 163 | const newNote = await createNote({ name, body }); 164 | if (newNote) { 165 | // add new note to render list 166 | setAllNotes(allNotes.concat(newNote)); 167 | } 168 | // reset old values 169 | setName(''); 170 | setBody(''); 171 | }; 172 | 173 | return ( 174 |
175 |
176 |
177 | setName(e.target.value)} 182 | /> 183 |
184 |
185 |