├── examples └── books │ ├── .babelrc │ ├── README.md │ ├── package.json │ ├── mock │ ├── data.json │ └── schemas.json │ └── server.js ├── .gitignore ├── .babelrc ├── README.md ├── .flowconfig ├── .eslintrc ├── src ├── utils │ ├── object.js │ ├── array.js │ ├── definitions.js │ └── graphql.js ├── mutations │ ├── simpleMutation.js │ ├── mutations.js │ ├── signinUser.js │ ├── delete.js │ ├── removeFromConnection.js │ ├── addToConnection.js │ ├── create.js │ └── update.js ├── types │ ├── GraphQLDateTime.js │ └── types.js ├── index.js └── queries │ └── queries.js ├── LICENSE ├── package.json ├── interfaces └── backend.js.flow └── tests └── parseValue.test.js /examples/books/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | /lib/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["syntax-flow", "transform-flow-strip-types"] 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `graphcool-api` [![Slack Status](https://slack.graph.cool/badge.svg)](https://slack.graph.cool) [![npm version](https://badge.fury.io/js/graphcool-api.svg)](https://badge.fury.io/js/graphcool-api) 2 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/flow-bin/.* 3 | .*/node_modules/babel-.* 4 | ./examples/.* 5 | 6 | [include] 7 | 8 | [libs] 9 | ./interfaces/ 10 | 11 | [options] 12 | module.system=haste 13 | munge_underscores=true 14 | -------------------------------------------------------------------------------- /examples/books/README.md: -------------------------------------------------------------------------------- 1 | # books 2 | 3 | This is a simple example with two models: `Person` & `Book` 4 | 5 | ## Install 6 | 7 | ```sh 8 | $ npm install 9 | ``` 10 | 11 | ## Start local server 12 | 13 | ```sh 14 | $ npm start 15 | ``` 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "extends" : [ 4 | "standard" 5 | ], 6 | "env" : { 7 | "node" : true 8 | }, 9 | "rules": { 10 | "semi" : [2, "never"], 11 | "space-infix-ops": 0, 12 | "max-len": [2, 120, 2] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/object.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export function mergeObjects (obj1: Object, obj2: Object): Object { 4 | var obj3 = {} 5 | for (const attrname in obj1) { obj3[attrname] = obj1[attrname] } 6 | for (const attrname in obj2) { obj3[attrname] = obj2[attrname] } 7 | 8 | return obj3 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/array.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export function mapArrayToObject ( 4 | array: Array, 5 | keyFunction: (e: E) => K, 6 | mapFunction: (e: E) => V, 7 | initialObject: { [key: K]: V } = {} 8 | ): { [key: K]: V } { 9 | return array.reduce((obj, val) => { 10 | obj[keyFunction(val)] = mapFunction(val) 11 | return obj 12 | }, initialObject) 13 | } 14 | -------------------------------------------------------------------------------- /src/mutations/simpleMutation.js: -------------------------------------------------------------------------------- 1 | export default function simpleMutation (config, outputType, outputResolve) { 2 | var {inputFields, mutateAndGetPayload} = config 3 | 4 | return { 5 | type: outputType, 6 | args: inputFields, 7 | resolve: (_, input, context, info) => { 8 | return Promise.resolve(mutateAndGetPayload(input, context, info)) 9 | .then((payload) => { 10 | return outputResolve(payload) 11 | }) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/books/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "babel-node server.js", 5 | "dev": "nodemon server.js --exec babel-node", 6 | "preinstall": "npm install --ignore-scripts ../.." 7 | }, 8 | "dependencies": { 9 | "cuid": "^1.3.8", 10 | "express": "^4.13.4", 11 | "express-graphql": "git://github.com/sorenbs/express-graphql.git#master", 12 | "graphql": "^0.4.18" 13 | }, 14 | "devDependencies": { 15 | "babel-cli": "^6.5.1", 16 | "babel-preset-es2015": "^6.5.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/books/mock/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "Person": { 3 | "cil5cizq60000j00jyntjokov": { 4 | "id": "cil5cizq60000j00jyntjokov", 5 | "firstName": "Joanne", 6 | "lastName": "Rowling" 7 | }, 8 | "cil5clesy0001j00jup6vldlp": { 9 | "id": "cil5clesy0001j00jup6vldlp", 10 | "firstName": "John Ronald Reuel", 11 | "lastName": "Tolkien" 12 | } 13 | }, 14 | "Book": { 15 | "cil5cmghi0002j00jx7su5fe4": { 16 | "id": "cil5cmghi0002j00jx7su5fe4", 17 | "title": "Harry Potter", 18 | "authorID": "cil5cizq60000j00jyntjokov" 19 | }, 20 | "cil5covw50003j00jedvys0tp": { 21 | "id": "cil5covw50003j00jedvys0tp", 22 | "title": "The Lord of the Rings", 23 | "authorID": "cil5clesy0001j00jup6vldlp" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/books/mock/schemas.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "modelName": "Person", 4 | "fields": [ 5 | { 6 | "fieldName": "id", 7 | "typeName": "GraphQLID", 8 | "nullable": false, 9 | "list": false 10 | }, 11 | { 12 | "fieldName": "firstName", 13 | "typeName": "String", 14 | "nullable": true, 15 | "list": false 16 | }, 17 | { 18 | "fieldName": "lastName", 19 | "typeName": "String", 20 | "nullable": false, 21 | "list": false 22 | } 23 | ] 24 | }, 25 | { 26 | "modelName": "Book", 27 | "fields": [ 28 | { 29 | "fieldName": "id", 30 | "typeName": "GraphQLID", 31 | "nullable": false, 32 | "list": false 33 | }, 34 | { 35 | "fieldName": "title", 36 | "typeName": "String", 37 | "nullable": false, 38 | "list": false 39 | }, 40 | { 41 | "fieldName": "author", 42 | "typeName": "Person", 43 | "nullable": false, 44 | "list": false 45 | } 46 | ] 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /src/types/GraphQLDateTime.js: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql' 2 | import { GraphQLError } from 'graphql/error' 3 | import { Kind } from 'graphql/language' 4 | import moment from 'moment' 5 | 6 | const ISO8601 = 'YYYY-MM-DDTHH:mm:ss.SSSZ' 7 | 8 | export function isValidDateTime (dateTime: string): boolean { 9 | if (!dateTime) { 10 | return false 11 | } 12 | 13 | return _parseAsMoment(dateTime).isValid() 14 | } 15 | 16 | function _parseAsMoment (value) { 17 | return moment.utc(value, ISO8601) 18 | } 19 | 20 | export default new GraphQLScalarType({ 21 | name: 'DateTime', 22 | serialize: (value) => { return value }, 23 | parseValue: (value) => { return value }, 24 | parseLiteral (ast) { 25 | if (ast.kind !== Kind.STRING) { 26 | throw new GraphQLError('Query error: Can only parse strings to dates but got a: ' + ast.kind, [ast]) 27 | } 28 | 29 | if (!isValidDateTime(ast.value)) { 30 | throw new GraphQLError(`Query error: Invalid date format, only accepts: ${ISO8601}`, [ast]) 31 | } 32 | 33 | return _parseAsMoment(ast.value).format(ISO8601) 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 graph.cool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | GraphQLSchema, 5 | GraphQLObjectType 6 | } from 'graphql' 7 | 8 | import { 9 | createTypes 10 | } from './types/types.js' 11 | 12 | import { 13 | createQueryEndpoints 14 | } from './queries/queries.js' 15 | 16 | import { 17 | createMutationEndpoints 18 | } from './mutations/mutations.js' 19 | 20 | import type { 21 | ClientSchema, 22 | AllTypes, 23 | SchemaType, 24 | Relation 25 | } from './utils/definitions.js' 26 | 27 | export function generateSchema (clientSchemas: [ClientSchema], schemaType: SchemaType = 'RELAY', relations: [Relation]): GraphQLSchema { 28 | // create types from client schemas 29 | const clientTypes: AllTypes = createTypes(clientSchemas, relations, schemaType) 30 | 31 | // generate query endpoints 32 | const queryFields = createQueryEndpoints(clientTypes, schemaType) 33 | 34 | // generate mutation endpoints 35 | const mutationFields = createMutationEndpoints(clientTypes, schemaType) 36 | 37 | return new GraphQLSchema({ 38 | query: new GraphQLObjectType({ 39 | name: 'RootQueryType', 40 | fields: queryFields 41 | }), 42 | mutation: new GraphQLObjectType({ 43 | name: 'RootMutationType', 44 | fields: mutationFields 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphcool-api", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "LICENSE", 8 | "README.md", 9 | "lib/" 10 | ], 11 | "scripts": { 12 | "prepublish": "npm test && npm run build", 13 | "start": "babel-node .", 14 | "build": "rm -rf lib/* && babel src --optional runtime --out-dir lib", 15 | "lint": "eslint --ignore-path .gitignore src examples", 16 | "check": "flow check", 17 | "test": "npm run lint && npm run check" 18 | }, 19 | "author": [ 20 | "Johannes Schickling ", 21 | "Søren Bramer Schmidt " 22 | ], 23 | "license": "MIT", 24 | "dependencies": { 25 | "deepcopy": "^0.6.1", 26 | "graphql": "^0.4.18", 27 | "graphql-relay": "^0.3.6", 28 | "moment": "^2.13.0" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.6.5", 32 | "babel-eslint": "^5.0.0", 33 | "babel-plugin-syntax-flow": "^6.5.0", 34 | "babel-plugin-transform-flow-strip-types": "^6.7.0", 35 | "babel-preset-es2015": "^6.6.0", 36 | "eslint": "^2.3.0", 37 | "eslint-config-standard": "^5.1.0", 38 | "eslint-plugin-babel": "^3.1.0", 39 | "eslint-plugin-promise": "^1.1.0", 40 | "eslint-plugin-standard": "^1.3.2", 41 | "flow-bin": "^0.22.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /interfaces/backend.js.flow: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLFieldConfigArgumentMap 3 | } from 'graphql' 4 | 5 | type BackendData = { 6 | id: string, 7 | [key: string]: string | number | boolean | BackendData 8 | } 9 | 10 | type InputData = { 11 | [key: string]: string | number | boolean 12 | } 13 | 14 | type ClientSchemaField = { 15 | fieldName: string, 16 | typeName: string, 17 | nullable: boolean, 18 | list: boolean 19 | } 20 | 21 | declare interface Backend { 22 | // schema updates 23 | addModel(modelName: string): Promise; 24 | 25 | deleteModel(modelName: string): Promise; 26 | 27 | addFieldToModel(field: ClientSchemaField, modelName: string): Promise; 28 | 29 | deleteFieldFromModel(fieldName: string, modelName: string): Promise; 30 | 31 | // queries 32 | node(id: string): Promise; 33 | 34 | allNodesByType( 35 | type: string, 36 | args: GraphQLFieldConfigArgumentMap 37 | ): Promise>; 38 | 39 | allNodesByRelation( 40 | parentId: string, 41 | relationFieldName: string, 42 | args: GraphQLFieldConfigArgumentMap 43 | ): Promise>; 44 | 45 | // mutations 46 | createNode(inputData: InputData): Promise; 47 | 48 | updateNode(id: string, inputData: InputData): Promise; 49 | 50 | deleteNode(id: string): Promise; 51 | 52 | associateNodes( 53 | childId: string, 54 | parentId: string, 55 | relationFieldName: string 56 | ): Promise; 57 | 58 | disassociateNodes( 59 | childId: string, 60 | parentId: string, 61 | relationFieldName: string 62 | ): Promise; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/mutations/mutations.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { 4 | GraphQLFields, 5 | AllTypes, 6 | SchemaType 7 | } from '../utils/definitions.js' 8 | 9 | import { isScalar } from '../utils/graphql.js' 10 | import signinUser from './signinUser' 11 | import createNode from './create' 12 | import updateNode from './update' 13 | import deleteNode from './delete' 14 | import addToConnection from './addToConnection' 15 | import removeFromConnection from './removeFromConnection' 16 | 17 | export function createMutationEndpoints ( 18 | input: AllTypes, 19 | schemaType: SchemaType 20 | ): GraphQLFields { 21 | const fields = {} 22 | const clientTypes = input.clientTypes 23 | const viewerType = input.viewerType 24 | 25 | fields.signinUser = signinUser(viewerType, schemaType) 26 | 27 | for (const modelName in clientTypes) { 28 | fields[`create${modelName}`] = createNode(viewerType, clientTypes, modelName, schemaType) 29 | fields[`update${modelName}`] = updateNode(viewerType, clientTypes, modelName, schemaType) 30 | fields[`delete${modelName}`] = deleteNode(viewerType, clientTypes, modelName, schemaType) 31 | 32 | clientTypes[modelName].clientSchema.fields 33 | .filter((field) => field.isList && !isScalar(field.typeIdentifier)) 34 | .forEach((connectionField) => { 35 | fields[`add${connectionField.typeIdentifier}To${connectionField.fieldName}ConnectionOn${modelName}`] = 36 | addToConnection(viewerType, clientTypes, modelName, connectionField, schemaType) 37 | 38 | fields[`remove${connectionField.typeIdentifier}From${connectionField.fieldName}ConnectionOn${modelName}`] = 39 | removeFromConnection(viewerType, clientTypes, modelName, connectionField, schemaType) 40 | }) 41 | } 42 | 43 | return fields 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/definitions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | GraphQLObjectType, 5 | GraphQLInterfaceType 6 | } from 'graphql' 7 | 8 | export type ClientTypes = { 9 | [key: string]: { 10 | objectType: GraphQLObjectType, 11 | createMutationInputArguments: GraphQLObjectType, 12 | updateMutationInputArguments: GraphQLObjectType, 13 | queryFilterInputArguments: GraphQLObjectType, 14 | uniqueQueryInputArguments: GraphQLObjectType, 15 | edgeType: GraphQLObjectType, 16 | connectionType: GraphQLObjectType, 17 | clientSchema: ClientSchema 18 | } 19 | } 20 | 21 | export type AllTypes = { 22 | clientTypes: ClientTypes, 23 | NodeInterfaceType: GraphQLInterfaceType, 24 | viewerType: GraphQLObjectType, 25 | viewerFields: GraphQLFields 26 | } 27 | 28 | export type GraphQLFields = { 29 | [key: string]: GraphQLObjectType 30 | } 31 | 32 | export type ClientSchema = { 33 | modelName: string, 34 | fields: Array 35 | } 36 | 37 | export type Relation = { 38 | id: string, 39 | modelA: string, 40 | modelB: string 41 | } 42 | 43 | export type permission = { 44 | id: string, 45 | userType: string, 46 | userPath: ?string, 47 | userRole: ?string, 48 | allowRead: boolean, 49 | allowCreate: boolean, 50 | allowUpdate: boolean, 51 | allowDelete: boolean 52 | } 53 | 54 | export type ClientSchemaField = { 55 | fieldName: string, 56 | typeIdentifier: string, 57 | backRelationName: ?string, 58 | relationId: ?string, 59 | relation: ?Relation, 60 | relationSide: ?string, 61 | enumValues: [string], 62 | isRequired: boolean, 63 | isList: boolean, 64 | isUnique: boolean, 65 | isSystem: boolean, 66 | defaultValue: ?string, 67 | permissions: [permission], 68 | description: ?string 69 | } 70 | 71 | export type SchemaType = 'SIMPLE' | 'RELAY'; 72 | -------------------------------------------------------------------------------- /src/mutations/signinUser.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { 4 | SchemaType 5 | } from '../utils/definitions.js' 6 | 7 | import simpleMutation from './simpleMutation.js' 8 | 9 | import { 10 | GraphQLNonNull, 11 | GraphQLString, 12 | GraphQLObjectType 13 | } from 'graphql' 14 | 15 | import { 16 | mutationWithClientMutationId 17 | } from 'graphql-relay' 18 | 19 | export default function ( 20 | viewerType: GraphQLObjectType, schemaType: SchemaType 21 | ): GraphQLObjectType { 22 | const config = { 23 | name: 'SigninUser', 24 | outputFields: { 25 | token: { 26 | type: GraphQLString 27 | }, 28 | viewer: { 29 | type: viewerType 30 | } 31 | }, 32 | inputFields: { 33 | email: { 34 | type: new GraphQLNonNull(GraphQLString) 35 | }, 36 | password: { 37 | type: new GraphQLNonNull(GraphQLString) 38 | } 39 | }, 40 | mutateAndGetPayload: (args, { rootValue: { backend } }) => ( 41 | // todo: efficiently get user by email 42 | backend.NO_PERMISSION_CHECK_allNodesByType('User') 43 | .then((allUsers) => allUsers.filter((node) => node.email === args.email)[0]) 44 | .then((user) => 45 | !user 46 | ? Promise.reject(`no user with the email '${args.email}'`) 47 | : backend.compareHashAsync(args.password, user.password) 48 | .then((result) => 49 | !result 50 | ? Promise.reject(`incorrect password for email '${args.email}'`) 51 | : user 52 | ) 53 | ) 54 | .then((user) => ({ 55 | token: backend.tokenForUser(user), 56 | viewer: { 57 | id: user.id 58 | } 59 | })) 60 | ) 61 | } 62 | 63 | if (schemaType === 'SIMPLE') { 64 | return simpleMutation(config, 65 | new GraphQLObjectType({ 66 | name: 'SigninUserPayload', 67 | fields: { 68 | token: { 69 | type: new GraphQLNonNull(GraphQLString) 70 | } 71 | } 72 | }), 73 | (root) => ({token: root.token})) 74 | } else { 75 | return mutationWithClientMutationId(config) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/queries/queries.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLID 6 | } from 'graphql' 7 | 8 | import type { 9 | AllTypes, 10 | GraphQLFields, 11 | SchemaType 12 | } from '../utils/definitions.js' 13 | 14 | import { 15 | fromGlobalId 16 | } from 'graphql-relay' 17 | 18 | import { 19 | mergeObjects 20 | } from '../utils/object.js' 21 | 22 | export function createQueryEndpoints ( 23 | input: AllTypes, 24 | schemaType: SchemaType 25 | ): GraphQLFields { 26 | var queryFields = {} 27 | const clientTypes = input.clientTypes 28 | const viewerType = input.viewerType 29 | const viewerFields = input.viewerFields 30 | 31 | for (const modelName in clientTypes) { 32 | // query single model by id 33 | queryFields[modelName] = { 34 | type: clientTypes[modelName].objectType, 35 | args: clientTypes[modelName].uniqueQueryInputArguments, 36 | resolve: (_, args, { operation, rootValue: { currentUser, backend } }) => { 37 | return backend.allNodesByType(modelName, {filter: args}, clientTypes[modelName].clientSchema, currentUser, operation) 38 | .then(({array}) => array[0]) 39 | } 40 | } 41 | } 42 | 43 | if (schemaType === 'RELAY') { 44 | queryFields['viewer'] = { 45 | type: viewerType, 46 | resolve: (_, args, { rootValue: { backend } }) => ( 47 | backend.user() 48 | ) 49 | } 50 | } 51 | if (schemaType === 'SIMPLE') { 52 | queryFields = mergeObjects(queryFields, viewerFields) 53 | } 54 | 55 | queryFields['node'] = { 56 | name: 'node', 57 | description: 'Fetches an object given its ID', 58 | type: input.NodeInterfaceType, 59 | args: { 60 | id: { 61 | type: new GraphQLNonNull(GraphQLID), 62 | description: 'The ID of an object' 63 | } 64 | }, 65 | resolve: (obj, {id}, { operation, rootValue: { currentUser, backend } }) => { 66 | const {id: internalId, type} = fromGlobalId(id) 67 | 68 | return backend.node(type, internalId, clientTypes[type].clientSchema, currentUser, operation) 69 | .then((node) => { 70 | return node 71 | }) 72 | } 73 | } 74 | 75 | return queryFields 76 | } 77 | -------------------------------------------------------------------------------- /examples/books/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import graphqlHTTP from 'express-graphql' 3 | import { graphql } from 'graphql' 4 | import { introspectionQuery } from 'graphql/utilities' 5 | // import { generateSchema } from 'graphcool-api' 6 | import { generateSchema } from '../../src' 7 | import clientSchemas from './mock/schemas.json' 8 | import database from './mock/data.json' 9 | import cuid from 'cuid' 10 | 11 | const getNode = (type, id) => ( 12 | database[type][id] 13 | ? Promise.resolve(database[type][id]) 14 | : Promise.reject(`no ${type} exists with id ${id}`) 15 | ) 16 | const backend = { 17 | node: getNode, 18 | allNodesByType: (type, args) => ( 19 | new Promise((resolve, reject) => { 20 | if (database[type]) { 21 | resolve(Object.values(database[type])) 22 | } 23 | reject() 24 | }) 25 | ), 26 | allNodesByRelation: (parentId, relationFieldName, args) => ( 27 | new Promise((resolve, reject) => resolve([])) 28 | ), 29 | 30 | createNode: (type, node) => ( 31 | new Promise((resolve, reject) => { 32 | node.id = cuid() 33 | database[type][node.id] = node 34 | 35 | resolve(node) 36 | }) 37 | ), 38 | updateNode: (type, id, newNode) => ( 39 | getNode(type, id).then((node) => { 40 | Object.keys(newNode).forEach((key) => { 41 | if (key !== 'clientMutationId') { 42 | node[key] = newNode[key] 43 | } 44 | }) 45 | database[type][id] = node 46 | 47 | return node 48 | }) 49 | ), 50 | deleteNode: (type, id) => ( 51 | new Promise((resolve, reject) => { 52 | const node = database[type][id] 53 | delete database[type][id] 54 | 55 | resolve(node) 56 | }) 57 | ) 58 | } 59 | 60 | const fetchTypes = () => new Promise((resolve, reject) => resolve(clientSchemas)) 61 | 62 | const app = express() 63 | 64 | app.get('/schema.json', (req, res) => { 65 | fetchTypes() 66 | .then((clientSchemas) => generateSchema(clientSchemas)) 67 | .then((schema) => graphql(schema, introspectionQuery)) 68 | .then((result) => res.send(JSON.stringify(result, null, 2))) 69 | }) 70 | 71 | app.use('/', graphqlHTTP((req) => ( 72 | fetchTypes() 73 | .then((clientSchemas) => generateSchema(clientSchemas)) 74 | .then((schema) => ({ 75 | schema, 76 | rootValue: { backend }, 77 | graphiql: true, 78 | pretty: true 79 | })) 80 | .catch((error) => console.error(error.stack)) 81 | ))) 82 | 83 | const APP_PORT = parseInt(process.env.PORT || 60000) 84 | app.listen(APP_PORT) 85 | console.log('API listening on port ' + APP_PORT) 86 | -------------------------------------------------------------------------------- /src/mutations/delete.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import deepcopy from 'deepcopy' 3 | 4 | import type { 5 | ClientTypes, 6 | SchemaType 7 | } from '../utils/definitions.js' 8 | 9 | import { 10 | isScalar 11 | } from '../utils/graphql.js' 12 | 13 | import { 14 | GraphQLNonNull, 15 | GraphQLID, 16 | GraphQLObjectType 17 | } from 'graphql' 18 | 19 | import { 20 | mutationWithClientMutationId, 21 | toGlobalId 22 | } from 'graphql-relay' 23 | 24 | import simpleMutation from './simpleMutation.js' 25 | 26 | import { 27 | getFieldNameFromModelName, 28 | convertInputFieldsToInternalIds, 29 | convertIdToExternal } from '../utils/graphql.js' 30 | 31 | export default function ( 32 | viewerType: GraphQLObjectType, clientTypes: ClientTypes, modelName: string, schemaType: SchemaType 33 | ): GraphQLObjectType { 34 | const config = { 35 | name: `Delete${modelName}`, 36 | outputFields: { 37 | [getFieldNameFromModelName(modelName)]: { 38 | type: clientTypes[modelName].objectType 39 | }, 40 | deletedId: { 41 | type: new GraphQLNonNull(GraphQLID) 42 | }, 43 | viewer: { 44 | type: viewerType, 45 | resolve: (_, args, { rootValue: { backend } }) => ( 46 | backend.user() 47 | ) 48 | } 49 | }, 50 | inputFields: { 51 | id: { 52 | type: new GraphQLNonNull(GraphQLID) 53 | } 54 | }, 55 | mutateAndGetPayload: (args, { rootValue: { currentUser, backend, webhooksProcessor } }) => { 56 | const node = convertInputFieldsToInternalIds(args, clientTypes[modelName].clientSchema) 57 | 58 | return backend.node( 59 | modelName, 60 | node.id, 61 | clientTypes[modelName].clientSchema, 62 | currentUser) 63 | .then((nodeToDelete) => { 64 | if (nodeToDelete === null) { 65 | return Promise.reject(`'${modelName}' with id '${node.id}' does not exist`) 66 | } 67 | 68 | return backend.deleteNode(modelName, node.id, clientTypes[modelName].clientSchema, currentUser) 69 | .then((node) => { 70 | webhooksProcessor.nodeDeleted(convertIdToExternal(modelName, node), modelName) 71 | return node 72 | }) 73 | .then((node) => ({[getFieldNameFromModelName(modelName)]: node, deletedId: args.id})) 74 | }) 75 | } 76 | } 77 | 78 | if (schemaType === 'SIMPLE') { 79 | return simpleMutation(config, 80 | clientTypes[modelName].objectType, 81 | (root) => root[getFieldNameFromModelName(modelName)]) 82 | } else { 83 | return mutationWithClientMutationId(config) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/mutations/removeFromConnection.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { 4 | ClientTypes, 5 | ClientSchemaField, 6 | SchemaType 7 | } from '../utils/definitions.js' 8 | 9 | import { 10 | GraphQLNonNull, 11 | GraphQLID, 12 | GraphQLObjectType 13 | } from 'graphql' 14 | 15 | import { 16 | mutationWithClientMutationId 17 | } from 'graphql-relay' 18 | 19 | import { getFieldNameFromModelName, convertInputFieldsToInternalIds } from '../utils/graphql.js' 20 | 21 | import simpleMutation from './simpleMutation.js' 22 | 23 | export default function ( 24 | viewerType: GraphQLObjectType, 25 | clientTypes: ClientTypes, 26 | modelName: string, 27 | connectionField: ClientSchemaField, 28 | schemaType: SchemaType 29 | ): GraphQLObjectType { 30 | const config = { 31 | name: `Remove${connectionField.typeIdentifier}From${connectionField.fieldName}ConnectionOn${modelName}`, 32 | outputFields: { 33 | [getFieldNameFromModelName(modelName)]: { 34 | type: clientTypes[modelName].objectType 35 | }, 36 | viewer: { 37 | type: viewerType, 38 | resolve: (_, args, { rootValue: { backend } }) => ( 39 | backend.user() 40 | ) 41 | } 42 | }, 43 | inputFields: { 44 | fromId: { 45 | type: new GraphQLNonNull(GraphQLID) 46 | }, 47 | toId: { 48 | type: new GraphQLNonNull(GraphQLID) 49 | } 50 | }, 51 | mutateAndGetPayload: (args, { rootValue: { currentUser, backend, webhooksProcessor } }) => { 52 | args = convertInputFieldsToInternalIds(args, clientTypes[modelName].clientSchema, ['fromId', 'toId']) 53 | 54 | const fromType = modelName 55 | const fromId = args.fromId 56 | const toType = connectionField.typeIdentifier 57 | const toId = args.toId 58 | 59 | const relation = connectionField.relation 60 | 61 | const aId = connectionField.relationSide === 'A' ? fromId : toId 62 | const bId = connectionField.relationSide === 'B' ? fromId : toId 63 | 64 | return backend.removeRelation(relation.id, aId, bId, fromType, fromId, toType, toId) 65 | .then(({fromNode, toNode}) => { 66 | webhooksProcessor.nodeRemovedFromConnection( 67 | toNode, 68 | connectionField.typeIdentifier, 69 | fromNode, 70 | modelName, 71 | connectionField.fieldName) 72 | return {[getFieldNameFromModelName(modelName)]: fromNode} 73 | }) 74 | } 75 | } 76 | 77 | if (schemaType === 'SIMPLE') { 78 | return simpleMutation(config, 79 | clientTypes[modelName].objectType, 80 | (root) => root[getFieldNameFromModelName(modelName)]) 81 | } else { 82 | return mutationWithClientMutationId(config) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/mutations/addToConnection.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLID, 6 | GraphQLObjectType 7 | } from 'graphql' 8 | 9 | import type { 10 | ClientTypes, 11 | ClientSchemaField, 12 | SchemaType 13 | } from '../utils/definitions.js' 14 | 15 | import { 16 | mutationWithClientMutationId, 17 | offsetToCursor 18 | } from 'graphql-relay' 19 | 20 | import { getFieldNameFromModelName, convertInputFieldsToInternalIds } from '../utils/graphql.js' 21 | 22 | import simpleMutation from './simpleMutation.js' 23 | 24 | export default function ( 25 | viewerType: GraphQLObjectType, 26 | clientTypes: ClientTypes, 27 | modelName: string, 28 | connectionField: ClientSchemaField, 29 | schemaType: SchemaType 30 | ): GraphQLObjectType { 31 | const config = { 32 | name: `Add${connectionField.typeIdentifier}To${connectionField.fieldName}ConnectionOn${modelName}`, 33 | outputFields: { 34 | [getFieldNameFromModelName(modelName)]: { 35 | type: clientTypes[modelName].objectType, 36 | resolve: (root) => root.fromNode 37 | }, 38 | viewer: { 39 | type: viewerType, 40 | resolve: (_, args, { rootValue: { backend } }) => ( 41 | backend.user() 42 | ) 43 | }, 44 | edge: { 45 | type: clientTypes[connectionField.typeIdentifier].edgeType, 46 | resolve: (root) => ({ 47 | cursor: offsetToCursor(0), // cursorForObjectInConnection(backend.allNodesByType(modelName), root.node), 48 | node: root.toNode 49 | }) 50 | } 51 | }, 52 | inputFields: { 53 | fromId: { 54 | type: new GraphQLNonNull(GraphQLID) 55 | }, 56 | toId: { 57 | type: new GraphQLNonNull(GraphQLID) 58 | } 59 | }, 60 | mutateAndGetPayload: (args, { rootValue: { currentUser, backend, webhooksProcessor } }) => { 61 | args = convertInputFieldsToInternalIds(args, clientTypes[modelName].clientSchema, ['fromId', 'toId']) 62 | 63 | const fromType = modelName 64 | const fromId = args.fromId 65 | const toType = connectionField.typeIdentifier 66 | const toId = args.toId 67 | 68 | const relation = connectionField.relation 69 | const aId = connectionField.relationSide === 'A' ? fromId : toId 70 | const bId = connectionField.relationSide === 'B' ? fromId : toId 71 | 72 | return backend.createRelation(relation.id, aId, bId, fromType, fromId, toType, toId) 73 | 74 | // return backend.createRelation(fromType, fromFieldName, fromId, toType, toFieldName, toId) 75 | .then(({fromNode, toNode}) => { 76 | webhooksProcessor.nodeAddedToConnection( 77 | toNode, 78 | connectionField.typeIdentifier, 79 | fromNode, 80 | modelName, 81 | connectionField.fieldName) 82 | return {fromNode, toNode} 83 | }) 84 | } 85 | } 86 | 87 | if (schemaType === 'SIMPLE') { 88 | return simpleMutation(config, clientTypes[modelName].objectType, (root) => root.fromNode) 89 | } else { 90 | return mutationWithClientMutationId(config) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/parseValue.test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | const { assert } = chai 3 | 4 | import { parseValue, isValidValueForType } from '../src/utils/graphql.js' 5 | 6 | describe('parseValue', () => { 7 | it('should parse string', () => { 8 | return Promise.all([ 9 | assert.equal(parseValue('aaa', 'String'), 'aaa'), 10 | assert.equal(isValidValueForType('aaa', 'String'), true) 11 | ]) 12 | }) 13 | 14 | it('should parse Boolean', () => { 15 | return Promise.all([ 16 | assert.equal(parseValue('True', 'Boolean'), true), 17 | assert.equal(parseValue('true', 'Boolean'), true), 18 | assert.equal(parseValue('False', 'Boolean'), false), 19 | assert.equal(parseValue('false', 'Boolean'), false), 20 | assert.equal(parseValue('TRUE', 'Boolean'), null), 21 | assert.equal(isValidValueForType('true', 'Boolean'), true), 22 | assert.equal(isValidValueForType('horse', 'Boolean'), false) 23 | ]) 24 | }) 25 | 26 | it('should parse Int', () => { 27 | return Promise.all([ 28 | assert.equal(parseValue('1', 'Int'), 1), 29 | assert.equal(parseValue('one', 'Int'), null), 30 | assert.equal(isValidValueForType('1', 'Int'), true), 31 | assert.equal(isValidValueForType('one', 'Int'), false) 32 | ]) 33 | }) 34 | 35 | it('should parse Float', () => { 36 | return Promise.all([ 37 | assert.equal(parseValue('1', 'Float'), 1), 38 | assert.equal(parseValue('1.4', 'Float'), 1.4), 39 | assert.equal(parseValue('thousands', 'Float'), null), 40 | assert.equal(isValidValueForType('1.4', 'Float'), true), 41 | assert.equal(isValidValueForType('one', 'Float'), false) 42 | ]) 43 | }) 44 | 45 | it('should parse GraphQLID', () => { 46 | return Promise.all([ 47 | assert.equal(parseValue('some id', 'GraphQLID'), 'some id'), 48 | assert.equal(parseValue('1.4', 'GraphQLID'), 1.4), 49 | assert.equal(isValidValueForType('1.4', 'GraphQLID'), true) 50 | ]) 51 | }) 52 | 53 | it('should parse Password', () => { 54 | return Promise.all([ 55 | assert.equal(parseValue('some password', 'Password'), 'some password'), 56 | assert.equal(parseValue('1.4', 'Password'), 1.4), 57 | assert.equal(isValidValueForType('1.4', 'Password'), true) 58 | ]) 59 | }) 60 | 61 | it('should parse Enum', () => { 62 | return Promise.all([ 63 | assert.equal(parseValue('SOME_ENUM', 'Enum'), 'SOME_ENUM'), 64 | assert.equal(parseValue('1.4', 'Enum'), null), 65 | assert.equal(isValidValueForType('1.4', 'Enum'), false), 66 | assert.equal(isValidValueForType('NAME', 'Enum'), true) 67 | ]) 68 | }) 69 | 70 | it('should parse DateTime', () => { 71 | return Promise.all([ 72 | assert.equal(isValidValueForType('', 'DateTime'), false), 73 | assert.equal(isValidValueForType('now', 'DateTime'), false), 74 | assert.equal(isValidValueForType('Thu, 19 May 2016 21:09:24 +02:00', 'DateTime'), false), 75 | assert.equal(isValidValueForType('Thu, 19 May 2016', 'DateTime'), false), 76 | 77 | assert.equal(isValidValueForType('2016', 'DateTime'), true), 78 | assert.equal(isValidValueForType('2016-01', 'DateTime'), true), 79 | assert.equal(isValidValueForType('2016-01-01', 'DateTime'), true), 80 | assert.equal(isValidValueForType('2016-01-01T', 'DateTime'), true), 81 | assert.equal(isValidValueForType('2016-05-19T17', 'DateTime'), true), 82 | assert.equal(isValidValueForType('2016-05-19T17:09', 'DateTime'), true), 83 | assert.equal(isValidValueForType('2016-05-19T17:09:24', 'DateTime'), true), 84 | assert.equal(isValidValueForType('2016-01-01T17:09:24+02:00', 'DateTime'), true), 85 | assert.equal(isValidValueForType('2016-05-19T17:09:24Z', 'DateTime'), true), 86 | assert.equal(isValidValueForType('2016-05-19T17:09:24.1', 'DateTime'), true), 87 | assert.equal(isValidValueForType('2016-05-19T17:09:24.12', 'DateTime'), true), 88 | assert.equal(isValidValueForType('2016-05-19T17:09:24.123', 'DateTime'), true), 89 | assert.equal(isValidValueForType('2016-05-19T17:09:24.1234', 'DateTime'), true), 90 | assert.equal(isValidValueForType('2016-05-19T17:09:24.1Z', 'DateTime'), true), 91 | assert.equal(isValidValueForType('2016-05-19T17:09:24.12Z', 'DateTime'), true), 92 | assert.equal(isValidValueForType('2016-05-19T17:09:24.1234Z', 'DateTime'), true), 93 | 94 | assert.equal(isValidValueForType('2016-05-19T17:09:24.123Z', 'DateTime'), true), 95 | 96 | assert.equal(parseValue('2016-05-19T17:09:24.123Z', 'DateTime'), '2016-05-19T17:09:24.123Z') 97 | ]) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/utils/graphql.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { 4 | ClientSchema, 5 | ClientSchemaField 6 | } from './definitions.js' 7 | 8 | import { 9 | fromGlobalId, 10 | toGlobalId 11 | } from 'graphql-relay' 12 | 13 | import {isValidDateTime} from '../types/GraphQLDateTime' 14 | 15 | import deepcopy from 'deepcopy' 16 | 17 | export function isValidName (name: string): boolean { 18 | return /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(name) 19 | } 20 | 21 | export function isValidProjectName (name: string): boolean { 22 | return /^[_a-zA-Z][_a-zA-Z0-9\s-]*$/.test(name) 23 | } 24 | 25 | export function isScalar (typeIdentifier: string): boolean { 26 | const scalarTypes = ['String', 'Int', 'Float', 'Boolean', 'GraphQLID', 'Password', 'Enum', 'DateTime'] 27 | return scalarTypes.filter((x) => x === typeIdentifier).length > 0 28 | } 29 | 30 | export function isReservedType (typeIdentifier: string): boolean { 31 | const reservedTypeIdentifiers = ['User'] 32 | return reservedTypeIdentifiers.filter((x) => x === typeIdentifier).length > 0 33 | } 34 | 35 | export function getFieldNameFromModelName (modelName: string): string { 36 | return modelName.charAt(0).toLowerCase() + modelName.slice(1) 37 | } 38 | 39 | export function getFieldsForBackRelations ( 40 | args: { [key: string]: any }, clientSchema: ClientSchema 41 | ): [ClientSchemaField] { 42 | return clientSchema.fields.filter((field) => field.backRelationName && args[`${field.fieldName}Id`]) 43 | } 44 | 45 | export function getRelationFields (args: { [key: string]: any }, clientSchema: ClientSchema): [ClientSchemaField] { 46 | return clientSchema.fields.filter((field) => !isScalar(field.typeIdentifier) && args[`${field.fieldName}Id`]) 47 | } 48 | 49 | export function convertInputFieldsToInternalIds ( 50 | originalArgs: { [key: string]: any }, clientSchema: ClientSchema, alsoConvert: [string] = [] 51 | ): { [key: string]: any } { 52 | const args = deepcopy(originalArgs) 53 | const fieldsToConvert = getRelationFields(args, clientSchema) 54 | fieldsToConvert.forEach((field) => { 55 | if (args[`${field.fieldName}Id`]) { 56 | args[`${field.fieldName}Id`] = fromGlobalId(args[`${field.fieldName}Id`]).id 57 | } 58 | }) 59 | 60 | if (args.id) { 61 | args.id = fromGlobalId(args.id).id 62 | } 63 | 64 | alsoConvert.forEach((fieldName) => { 65 | args[fieldName] = fromGlobalId(args[fieldName]).id 66 | }) 67 | 68 | return args 69 | } 70 | 71 | export function convertIdToExternal (typeIdentifier: string, node: Object): Object { 72 | const nodeWithExternalId = deepcopy(node) 73 | nodeWithExternalId.id = toGlobalId(typeIdentifier, nodeWithExternalId.id) 74 | 75 | return nodeWithExternalId 76 | } 77 | 78 | export function convertScalarListsToInternalRepresentation (node: Object, clientSchema: ClientSchema): Object { 79 | const nodeClone = deepcopy(node) 80 | clientSchema.fields.forEach((field) => { 81 | if (field.isList && node[field.fieldName] !== undefined) { 82 | if (Array.isArray(node[field.fieldName])) { 83 | nodeClone[field.fieldName] = JSON.stringify(node[field.fieldName]) 84 | } else { 85 | if ((typeof nodeClone[field.fieldName]) === 'string' && nodeClone[field.fieldName].indexOf("]") < 0) { 86 | console.log(`Assert: expected ${node[field.fieldName]} to be array`) 87 | nodeClone[field.fieldName] = '[]' 88 | } 89 | } 90 | } 91 | }) 92 | 93 | return nodeClone 94 | } 95 | 96 | export function convertScalarListsToExternalRepresentation (node: Object, clientSchema: ClientSchema): Object { 97 | const nodeClone = deepcopy(node) 98 | clientSchema.fields.forEach((field) => { 99 | if (field.isList) { 100 | if ( 101 | node[field.fieldName] === undefined || 102 | node[field.fieldName] === null || 103 | node[field.fieldName].trim() === '') { 104 | nodeClone[field.fieldName] = [] 105 | } else { 106 | nodeClone[field.fieldName] = JSON.parse(node[field.fieldName]) 107 | } 108 | } 109 | }) 110 | 111 | return nodeClone 112 | } 113 | 114 | export function ensureIsList (value) { 115 | if (value === null || value === undefined) { 116 | return [] 117 | } 118 | 119 | // todo: this check is too hacky 120 | if (typeof value === 'string' && value.indexOf('[') !== -1) { 121 | return JSON.parse(value) 122 | } else { 123 | return [value] 124 | } 125 | } 126 | 127 | export function patchConnectedNodesOnIdFields ( 128 | node: Object, connectedNodes: [Object], clientSchema: ClientSchema 129 | ): Object { 130 | const nodeClone = deepcopy(node) 131 | getRelationFields(node, clientSchema).forEach((field) => { 132 | const connectedNode = connectedNodes.filter((x) => x.id === node[`${field.fieldName}Id`])[0] 133 | if (connectedNode) { 134 | const nodeWithConvertedId = convertIdToExternal(field.typeIdentifier, connectedNode) 135 | nodeClone[field.fieldName] = nodeWithConvertedId 136 | nodeClone[`${field.fieldName}Id`] = nodeWithConvertedId.id 137 | } 138 | }) 139 | 140 | return nodeClone 141 | } 142 | 143 | export function externalIdFromQueryInfo (info: Object): string { 144 | // relies on the fact that the `node` query has 1 argument that is the external id 145 | const idArgument = info.operation.selectionSet.selections[0].arguments[0] 146 | const variables = info.variableValues 147 | return idArgument.value.kind === 'Variable' 148 | ? variables[idArgument.value.name.value] 149 | : idArgument.value.value 150 | } 151 | 152 | export function parseValue (value: string, typeIdentifier: string): any { 153 | return { 154 | String: () => value, 155 | Boolean: () => 156 | (value === 'true' || value === 'True') ? true : (value === 'false' || value === 'False') ? false : null, 157 | Int: () => isNaN(parseInt(value)) ? null : parseInt(value), 158 | Float: () => isNaN(parseFloat(value)) ? null : parseFloat(value), 159 | GraphQLID: () => value, 160 | Password: () => value, 161 | Enum: () => isValidName(value) ? value : null, 162 | DateTime: () => isValidDateTime(value) ? value : null 163 | }[typeIdentifier]() 164 | } 165 | 166 | export function isValidValueForType (value: string, typeIdentifier: string): boolean { 167 | const parsedValue = parseValue(value, typeIdentifier) 168 | return parsedValue !== null && parsedValue !== undefined 169 | } 170 | -------------------------------------------------------------------------------- /src/mutations/create.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | GraphQLObjectType 5 | } from 'graphql' 6 | 7 | import type { 8 | ClientTypes, 9 | SchemaType 10 | } from '../utils/definitions.js' 11 | 12 | import { 13 | mutationWithClientMutationId, 14 | offsetToCursor 15 | } from 'graphql-relay' 16 | 17 | import { 18 | getFieldNameFromModelName, 19 | patchConnectedNodesOnIdFields, 20 | convertInputFieldsToInternalIds, 21 | convertIdToExternal, 22 | isScalar, 23 | convertScalarListsToInternalRepresentation 24 | } from '../utils/graphql.js' 25 | 26 | import simpleMutation from './simpleMutation.js' 27 | 28 | function getFieldsOfType (args, clientSchema, typeIdentifier) { 29 | return clientSchema.fields.filter((field) => field.typeIdentifier === typeIdentifier && args[field.fieldName]) 30 | } 31 | 32 | export default function ( 33 | viewerType: GraphQLObjectType, clientTypes: ClientTypes, modelName: string, schemaType: SchemaType 34 | ): GraphQLObjectType { 35 | const outputFields = { 36 | [getFieldNameFromModelName(modelName)]: { 37 | type: clientTypes[modelName].objectType, 38 | resolve: (root) => root.node 39 | }, 40 | viewer: { 41 | type: viewerType, 42 | resolve: (_, args, { rootValue: { backend } }) => ( 43 | backend.user() 44 | ) 45 | }, 46 | edge: { 47 | type: clientTypes[modelName].edgeType, 48 | resolve: (root, args, { rootValue: { currentUser, backend } }) => 49 | ({ 50 | cursor: offsetToCursor(0), // todo: do we sort ascending or descending? 51 | node: root.node, 52 | viewer: backend.user() 53 | }) 54 | } 55 | } 56 | 57 | const oneConnections = clientTypes[modelName].clientSchema.fields 58 | .filter((field) => !isScalar(field.typeIdentifier) && !field.isList) 59 | 60 | oneConnections.forEach((connectionField) => { 61 | if (!outputFields[connectionField.fieldName]) { 62 | outputFields[connectionField.fieldName] = { 63 | type: clientTypes[connectionField.typeIdentifier].objectType, 64 | resolve: (root, args, { rootValue: { currentUser, backend } }) => { 65 | console.log('connectionArg') 66 | console.log(root) 67 | 68 | return backend.allNodesByRelation( 69 | connectionField.typeIdentifier, 70 | root.node.id, 71 | connectionField.fieldName, 72 | {}, 73 | clientTypes[connectionField.typeIdentifier].clientSchema, 74 | currentUser, 75 | clientTypes[modelName].clientSchema) 76 | .then(({array}) => { 77 | return array[0] 78 | }) 79 | } 80 | } 81 | } 82 | }) 83 | 84 | const config = { 85 | name: `Create${modelName}`, 86 | outputFields: outputFields, 87 | inputFields: clientTypes[modelName].createMutationInputArguments, 88 | mutateAndGetPayload: (node, { rootValue: { currentUser, backend, webhooksProcessor } }) => { 89 | function getConnectionFields () { 90 | return clientTypes[modelName].clientSchema.fields 91 | .filter((field) => !isScalar(field.typeIdentifier) && node[`${field.fieldName}Id`] !== undefined) 92 | } 93 | 94 | function getScalarFields () { 95 | return clientTypes[modelName].clientSchema.fields 96 | .filter((field) => isScalar(field.typeIdentifier)) 97 | } 98 | 99 | node = convertInputFieldsToInternalIds(node, clientTypes[modelName].clientSchema) 100 | 101 | return Promise.all(getFieldsOfType(node, clientTypes[modelName].clientSchema, 'Password').map((field) => 102 | backend.hashAsync(node[field.fieldName]).then((hashed) => { 103 | node[field.fieldName] = hashed 104 | }) 105 | )) 106 | .then(() => { 107 | let newNode = {} 108 | getScalarFields().forEach((field) => { 109 | if (node[field.fieldName] !== undefined) { 110 | newNode[field.fieldName] = node[field.fieldName] 111 | } 112 | }) 113 | 114 | newNode = convertScalarListsToInternalRepresentation(newNode, clientTypes[modelName].clientSchema) 115 | 116 | return backend.beginTransaction() 117 | .then(() => backend.createNode(modelName, newNode, clientTypes[modelName].clientSchema, currentUser)) 118 | }).then((dbNode) => { 119 | node.id = dbNode.id 120 | // add in corresponding connection 121 | return Promise.all(getConnectionFields() 122 | .map((field) => { 123 | const fromType = modelName 124 | const fromId = dbNode.id 125 | const toType = field.typeIdentifier 126 | const toId = node[`${field.fieldName}Id`] 127 | 128 | const relation = field.relation 129 | 130 | const aId = field.relationSide === 'A' ? fromId : toId 131 | const bId = field.relationSide === 'B' ? fromId : toId 132 | 133 | return backend.createRelation(relation.id, aId, bId, fromType, fromId, toType, toId) 134 | .then(({fromNode, toNode}) => toNode) 135 | }) 136 | ) 137 | .then((connectedNodes) => { 138 | backend.commitTransaction() 139 | return {connectedNodes, node} 140 | }) 141 | }) 142 | .then(({connectedNodes, node}) => { 143 | return backend.getNodeWithoutUserValidation(modelName, node.id) 144 | .then((nodeWithAllFields) => { 145 | getConnectionFields().forEach((field) => { 146 | const fieldName = `${field.fieldName}Id` 147 | console.log(fieldName, node[fieldName]) 148 | if (node[fieldName]) { 149 | nodeWithAllFields[fieldName] = node[fieldName] 150 | } 151 | }) 152 | 153 | getScalarFields().forEach((field) => { 154 | if (field.typeIdentifier === 'Boolean') { 155 | if (nodeWithAllFields[field.fieldName] === 0) { 156 | nodeWithAllFields[field.fieldName] = false 157 | } 158 | if (nodeWithAllFields[field.fieldName] === 1) { 159 | nodeWithAllFields[field.fieldName] = true 160 | } 161 | } 162 | }) 163 | const patchedNode = patchConnectedNodesOnIdFields( 164 | nodeWithAllFields, 165 | connectedNodes, 166 | clientTypes[modelName].clientSchema) 167 | webhooksProcessor.nodeCreated(convertIdToExternal(modelName, patchedNode), modelName) 168 | return node 169 | }) 170 | }) 171 | .then((node) => ({ node })) 172 | } 173 | } 174 | 175 | if (schemaType === 'SIMPLE') { 176 | return simpleMutation(config, clientTypes[modelName].objectType, (root) => root.node) 177 | } else { 178 | return mutationWithClientMutationId(config) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/mutations/update.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { 4 | ClientTypes, 5 | SchemaType 6 | } from '../utils/definitions.js' 7 | 8 | import { 9 | GraphQLObjectType 10 | } from 'graphql' 11 | 12 | import { 13 | mutationWithClientMutationId, 14 | cursorForObjectInConnection 15 | } from 'graphql-relay' 16 | 17 | import { 18 | getFieldNameFromModelName, 19 | convertInputFieldsToInternalIds, 20 | isScalar, 21 | convertIdToExternal, 22 | convertScalarListsToInternalRepresentation 23 | } from '../utils/graphql.js' 24 | 25 | import simpleMutation from './simpleMutation.js' 26 | 27 | function getFieldsOfType (args, clientSchema, typeIdentifier) { 28 | return clientSchema.fields.filter((field) => field.typeIdentifier === typeIdentifier && args[field.fieldName]) 29 | } 30 | 31 | export default function ( 32 | viewerType: GraphQLObjectType, clientTypes: ClientTypes, modelName: string, schemaType: SchemaType 33 | ): GraphQLObjectType { 34 | const outputFields = { 35 | [getFieldNameFromModelName(modelName)]: { 36 | type: clientTypes[modelName].objectType, 37 | resolve: (root) => root.node 38 | }, 39 | viewer: { 40 | type: viewerType, 41 | resolve: (_, args, { rootValue: { backend } }) => ( 42 | backend.user() 43 | ) 44 | }, 45 | edge: { 46 | type: clientTypes[modelName].edgeType, 47 | resolve: (root, args, { rootValue: { currentUser, backend } }) => 48 | backend.allNodesByType(modelName, args, clientTypes[modelName].clientSchema, currentUser) 49 | .then(({array}) => { 50 | return ({ 51 | // todo: getting all nodes is not efficient 52 | cursor: cursorForObjectInConnection(array, array.filter((x) => x.id === root.node.id)[0]), 53 | node: root.node 54 | }) 55 | }) 56 | } 57 | } 58 | 59 | const oneConnections = clientTypes[modelName].clientSchema.fields 60 | .filter((field) => !isScalar(field.typeIdentifier) && !field.isList) 61 | 62 | oneConnections.forEach((connectionField) => { 63 | if (!outputFields[connectionField.fieldName]) { 64 | outputFields[connectionField.fieldName] = { 65 | type: clientTypes[connectionField.typeIdentifier].objectType, 66 | resolve: (root, args, { rootValue: { currentUser, backend } }) => { 67 | 68 | return backend.allNodesByRelation( 69 | connectionField.typeIdentifier, 70 | root.node.id, 71 | connectionField.fieldName, 72 | {}, 73 | clientTypes[connectionField.typeIdentifier].clientSchema, 74 | currentUser, 75 | clientTypes[modelName].clientSchema) 76 | .then(({array}) => { 77 | return array[0] 78 | }) 79 | } 80 | } 81 | } 82 | }) 83 | 84 | const config = { 85 | name: `Update${modelName}`, 86 | outputFields: outputFields, 87 | inputFields: clientTypes[modelName].updateMutationInputArguments, 88 | mutateAndGetPayload: (node, { rootValue: { currentUser, backend, webhooksProcessor } }) => { 89 | // todo: currently we don't handle setting a relation to null. Use removeXfromConnection instead 90 | function getConnectionFields () { 91 | return clientTypes[modelName].clientSchema.fields 92 | .filter((field) => !isScalar(field.typeIdentifier) && node[`${field.fieldName}Id`] !== undefined) 93 | } 94 | 95 | function getScalarFields () { 96 | return clientTypes[modelName].clientSchema.fields 97 | .filter((field) => isScalar(field.typeIdentifier)) 98 | } 99 | 100 | const externalId = node.id 101 | node = convertInputFieldsToInternalIds(node, clientTypes[modelName].clientSchema) 102 | 103 | return backend.node( 104 | modelName, 105 | node.id, 106 | clientTypes[modelName].clientSchema, 107 | currentUser) 108 | .then((oldNode) => { 109 | if (oldNode === null) { 110 | return Promise.reject(`'No ${modelName}' with id '${externalId}' exists`) 111 | } 112 | 113 | return Promise.all(getFieldsOfType(node, clientTypes[modelName].clientSchema, 'Password').map((field) => 114 | backend.hashAsync(node[field.fieldName]).then((hashed) => { 115 | node[field.fieldName] = hashed 116 | }) 117 | )).then(() => { 118 | const changedFields = {} 119 | getScalarFields().forEach((field) => { 120 | if (node[field.fieldName] !== undefined && oldNode[field.fieldName] !== node[field.fieldName]) { 121 | changedFields[field.fieldName] = true 122 | } else { 123 | changedFields[field.fieldName] = false 124 | } 125 | }) 126 | 127 | getScalarFields().forEach((field) => { 128 | if (node[field.fieldName] !== undefined) { 129 | oldNode[field.fieldName] = node[field.fieldName] 130 | } 131 | }) 132 | 133 | oldNode = convertScalarListsToInternalRepresentation(oldNode, clientTypes[modelName].clientSchema) 134 | 135 | return backend.beginTransaction() 136 | .then(() => backend.updateNode(modelName, node.id, oldNode, clientTypes[modelName].clientSchema, currentUser)) 137 | .then((dbNode) => { 138 | return Promise.all(getConnectionFields() 139 | .map((field) => { 140 | const fromType = modelName 141 | const fromId = dbNode.id 142 | const toType = field.typeIdentifier 143 | const toId = node[`${field.fieldName}Id`] 144 | 145 | const relation = field.relation 146 | 147 | const aId = field.relationSide === 'A' ? fromId : toId 148 | const bId = field.relationSide === 'B' ? fromId : toId 149 | const fromField = field.relationSide 150 | 151 | return backend.removeAllRelationsFrom(relation.id, fromType, fromId, fromField) 152 | .then(() => backend.createRelation(relation.id, aId, bId, fromType, fromId, toType, toId)) 153 | .then(({fromNode, toNode}) => toNode) 154 | })) 155 | .then((connectedNodes) => { 156 | backend.commitTransaction() 157 | return {connectedNodes, node: dbNode, args: node, changedFields} 158 | }) 159 | }) 160 | }) 161 | .then(({node, args, changedFields}) => { 162 | return backend.getNodeWithoutUserValidation(modelName, node.id) 163 | .then((nodeWithAllFields) => { 164 | getConnectionFields().forEach((field) => { 165 | const fieldName = `${field.fieldName}Id` 166 | console.log(fieldName, args[fieldName]) 167 | if (args[fieldName]) { 168 | nodeWithAllFields[fieldName] = args[fieldName] 169 | } 170 | }) 171 | 172 | getScalarFields().forEach((field) => { 173 | if (field.typeIdentifier === 'Boolean') { 174 | if (nodeWithAllFields[field.fieldName] === 0) { 175 | nodeWithAllFields[field.fieldName] = false 176 | } 177 | if (nodeWithAllFields[field.fieldName] === 1) { 178 | nodeWithAllFields[field.fieldName] = true 179 | } 180 | } 181 | }) 182 | webhooksProcessor.nodeUpdated(convertIdToExternal(modelName, nodeWithAllFields), modelName, changedFields) 183 | 184 | return {node} 185 | }) 186 | }) 187 | }) 188 | } 189 | } 190 | 191 | if (schemaType === 'SIMPLE') { 192 | return simpleMutation(config, clientTypes[modelName].objectType, (root) => root.node) 193 | } else { 194 | return mutationWithClientMutationId(config) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/types/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLFloat, 7 | GraphQLBoolean, 8 | GraphQLID, 9 | GraphQLObjectType, 10 | GraphQLNonNull, 11 | GraphQLInterfaceType, 12 | GraphQLEnumType, 13 | GraphQLInputObjectType, 14 | GraphQLList 15 | } from 'graphql' 16 | 17 | import GraphQLDateTime from './GraphQLDateTime.js' 18 | 19 | import { 20 | connectionDefinitions, 21 | connectionArgs, 22 | connectionFromArray, 23 | toGlobalId, 24 | fromGlobalId 25 | } from 'graphql-relay' 26 | 27 | import { 28 | mapArrayToObject 29 | } from '../utils/array.js' 30 | 31 | import { 32 | mergeObjects 33 | } from '../utils/object.js' 34 | 35 | import { 36 | isScalar, 37 | convertInputFieldsToInternalIds, 38 | externalIdFromQueryInfo, 39 | ensureIsList 40 | } from '../utils/graphql.js' 41 | 42 | import type { 43 | ClientSchema, 44 | ClientSchemaField, 45 | ClientTypes, 46 | AllTypes, 47 | GraphQLFields, 48 | SchemaType, 49 | Relation 50 | } from '../utils/definitions.js' 51 | 52 | function getFilterPairsFromFilterArgument (filter) { 53 | if (!filter) { 54 | return [] 55 | } 56 | 57 | var filters = [] 58 | for (const field in filter) { 59 | if (filter[field] != null) { 60 | filters.push({ field, value: filter[field] }) 61 | } 62 | } 63 | 64 | return filters 65 | } 66 | 67 | function injectRelationships ( 68 | objectType: GraphQLObjectType, 69 | clientSchema: ClientSchema, 70 | allClientTypes: ClientTypes, 71 | schemaType: SchemaType 72 | ): void { 73 | const objectTypeFields = objectType._typeConfig.fields 74 | 75 | clientSchema.fields 76 | .filter((field) => objectTypeFields[field.fieldName].type.__isRelation) 77 | .forEach((clientSchemaField: ClientSchemaField) => { 78 | const fieldName = clientSchemaField.fieldName 79 | const objectTypeField = objectTypeFields[fieldName] 80 | const typeIdentifier = objectTypeField.type.typeIdentifier 81 | 82 | // 1:n relationship 83 | if (clientSchemaField.isList) { 84 | objectTypeField.type = schemaType === 'RELAY' 85 | ? allClientTypes[typeIdentifier].connectionType 86 | : new GraphQLList(allClientTypes[typeIdentifier].objectType) 87 | objectTypeField.args = allClientTypes[typeIdentifier].queryFilterInputArguments 88 | 89 | objectTypeField.resolve = (obj, args, { operation, rootValue: { currentUser, backend } }) => { 90 | if (args.filter) { 91 | args.filter = convertInputFieldsToInternalIds(args.filter, allClientTypes[typeIdentifier].clientSchema) 92 | } 93 | 94 | return backend.allNodesByRelation( 95 | clientSchema.modelName, 96 | obj.id, 97 | clientSchemaField.fieldName, 98 | args, 99 | allClientTypes[typeIdentifier].clientSchema, 100 | currentUser, 101 | allClientTypes[clientSchema.modelName].clientSchema) 102 | .then(({array, hasMorePages}) => { 103 | if (schemaType === 'RELAY') { 104 | const edges = array.map((item) => ({node: item, cursor: toGlobalId(args.orderBy || 'id_ASC', item.id)})) 105 | const pageInfo = { 106 | hasNextPage: hasMorePages, 107 | hasPreviousPage: false, 108 | startCursor: edges[0] ? edges[0].cursor : null, 109 | endCursor: edges[edges.length-1] ? edges[edges.length-1].cursor : null 110 | } 111 | return { 112 | edges, 113 | pageInfo, 114 | totalCount: array.length 115 | } 116 | } else { 117 | return array 118 | } 119 | }) 120 | } 121 | 122 | // 1:1 relationship 123 | } else { 124 | objectTypeField.type = allClientTypes[typeIdentifier].objectType 125 | objectTypeField.resolve = (obj, args, { operation, rootValue: { backend, currentUser } }) => ( 126 | backend.allNodesByRelation( 127 | clientSchema.modelName, 128 | obj.id, 129 | fieldName, 130 | args, 131 | allClientTypes[typeIdentifier].clientSchema, 132 | currentUser, 133 | allClientTypes[clientSchema.modelName].clientSchema) 134 | .then(({array}) => { 135 | return array[0] 136 | }) 137 | ) 138 | } 139 | }) 140 | } 141 | 142 | function wrapWithNonNull ( 143 | objectType: GraphQLObjectType, 144 | clientSchema: ClientSchema 145 | ): void { 146 | clientSchema.fields 147 | .filter((field) => field.isRequired) 148 | .forEach((clientSchemaField: ClientSchemaField) => { 149 | const fieldName = clientSchemaField.fieldName 150 | const objectTypeField = objectType._typeConfig.fields[fieldName] 151 | objectTypeField.type = new GraphQLNonNull(objectTypeField.type) 152 | }) 153 | } 154 | 155 | export function createTypes (clientSchemas: [ClientSchema], relations: [Relation], schemaType: SchemaType): AllTypes { 156 | const enumTypes = {} 157 | function parseClientType (field: ClientSchemaField, modelName: string) { 158 | const listify = field.isList ? (type) => new GraphQLList(type) : (type) => type 159 | switch (field.typeIdentifier) { 160 | case 'String': return listify(GraphQLString) 161 | case 'Boolean': return listify(GraphQLBoolean) 162 | case 'Int': return listify(GraphQLInt) 163 | case 'Float': return listify(GraphQLFloat) 164 | case 'GraphQLID': return listify(GraphQLID) 165 | case 'Password': return listify(GraphQLString) 166 | case 'DateTime': return listify(GraphQLDateTime) 167 | case 'Enum' : 168 | const enumTypeName = `${modelName}_${field.fieldName}` 169 | if (!enumTypes[enumTypeName]) { 170 | console.log('creating new enum type', enumTypeName, field) 171 | enumTypes[enumTypeName] = new GraphQLEnumType({ 172 | name: enumTypeName, 173 | values: mapArrayToObject(JSON.parse(field.enumValues || '[]'), (x) => x, (x) => ({value: x})) 174 | }) 175 | } 176 | 177 | return listify(enumTypes[enumTypeName]) 178 | // NOTE this marks a relation type which will be overwritten by `injectRelationships` 179 | default: return { __isRelation: true, typeIdentifier: field.typeIdentifier } 180 | } 181 | } 182 | 183 | function hasDefaultValue (field) { 184 | return field.defaultValue !== undefined && field.defaultValue !== null 185 | } 186 | 187 | function getValueOrDefault (obj, field) { 188 | if (obj[field.fieldName] !== undefined && obj[field.fieldName] !== null) { 189 | return obj[field.fieldName] 190 | } else { 191 | if (hasDefaultValue(field)) { 192 | return field.defaultValue 193 | } else { 194 | return null 195 | } 196 | } 197 | } 198 | 199 | function generateDescription (field) { 200 | const defaultValue = hasDefaultValue(field) ? `**Default value: '${field.defaultValue}'**` : '' 201 | const description = 202 | // note: this is markdown syntax... 203 | `${defaultValue} 204 | 205 | ${field.description || ''}` 206 | 207 | return description 208 | } 209 | 210 | function generateObjectType ( 211 | clientSchema: ClientSchema, 212 | NodeInterfaceType: GraphQLInterfaceType 213 | ): GraphQLObjectType { 214 | const graphQLFields: GraphQLFields = mapArrayToObject( 215 | clientSchema.fields, 216 | (field) => field.fieldName, 217 | (field) => { 218 | const type = parseClientType(field, clientSchema.modelName) 219 | const resolve = field.fieldName === 'id' 220 | ? (obj) => toGlobalId(clientSchema.modelName, getValueOrDefault(obj, field)) 221 | : (obj) => field.isList ? ensureIsList(getValueOrDefault(obj, field)) : getValueOrDefault(obj, field) 222 | 223 | return {type, resolve, description: generateDescription(field)} 224 | } 225 | ) 226 | 227 | return new GraphQLObjectType({ 228 | name: clientSchema.modelName, 229 | fields: graphQLFields, 230 | interfaces: [NodeInterfaceType] 231 | }) 232 | } 233 | 234 | function generateUniqueQueryInputArguments (clientSchema: ClientSchema) { 235 | const fields = clientSchema.fields.filter((field) => field.isUnique && !field.isList) 236 | return mapArrayToObject( 237 | fields, 238 | (field) => field.fieldName, 239 | (field) => ({ 240 | type: parseClientType(field, clientSchema.modelName), 241 | description: generateDescription(field) 242 | }) 243 | ) 244 | } 245 | 246 | function defaultValue (field) { 247 | switch (field.typeIdentifier) { 248 | case 'String': return field.defaultValue 249 | case 'Boolean': return field.defaultValue === 'True' || field.defaultValue === 'true' 250 | case 'Int': return parseInt(field.defaultValue) 251 | case 'Float': return parseFloat(field.defaultValue) 252 | case 'GraphQLID': return field.defaultValue 253 | case 'Password': return field.defaultValue 254 | case 'DateTime': return field.defaultValue 255 | case 'Enum' : return field.defaultValue 256 | default: return field.defaultValue 257 | } 258 | } 259 | 260 | function generateObjectMutationInputArguments ( 261 | clientSchema: ClientSchema, 262 | scalarFilter: (field: ClientSchemaField) => boolean, 263 | oneToOneFilter: (field: ClientSchemaField) => boolean, 264 | forceFieldsOptional: boolean = false, 265 | forceIdFieldOptional: boolean = false, 266 | allowDefaultValues: boolean = true 267 | ): GraphQLObjectType { 268 | function isRequired (field) { 269 | if (!field.isRequired) { 270 | return false 271 | } 272 | 273 | if (field.fieldName === 'id' && forceIdFieldOptional) { 274 | return false 275 | } 276 | 277 | if (field.fieldName !== 'id' && forceFieldsOptional) { 278 | return false 279 | } 280 | 281 | if (hasDefaultValue(field)) { 282 | return false 283 | } 284 | 285 | return true 286 | } 287 | 288 | const scalarFields = clientSchema.fields.filter(scalarFilter) 289 | const scalarArguments = mapArrayToObject( 290 | scalarFields, 291 | (field) => field.fieldName, 292 | (field) => ({ 293 | type: isRequired(field) 294 | ? new GraphQLNonNull(parseClientType(field, clientSchema.modelName)) 295 | : parseClientType(field, clientSchema.modelName), 296 | description: generateDescription(field), 297 | defaultValue: (hasDefaultValue(field) && allowDefaultValues) ? defaultValue(field) : null 298 | }) 299 | ) 300 | 301 | const onetoOneFields = clientSchema.fields.filter(oneToOneFilter) 302 | const oneToOneArguments = mapArrayToObject( 303 | onetoOneFields, 304 | (field) => `${field.fieldName}Id`, 305 | (field) => ({ 306 | type: (field.isRequired && !forceFieldsOptional) ? new GraphQLNonNull(GraphQLID) : GraphQLID, 307 | description: generateDescription(field), 308 | defaultValue: (hasDefaultValue(field) && allowDefaultValues) ? defaultValue(field) : null 309 | })) 310 | 311 | return mergeObjects(scalarArguments, oneToOneArguments) 312 | } 313 | 314 | function generateCreateObjectMutationInputArguments ( 315 | clientSchema: ClientSchema 316 | ): GraphQLObjectType { 317 | return generateObjectMutationInputArguments( 318 | clientSchema, 319 | (field) => !parseClientType(field, clientSchema.modelName).__isRelation && field.fieldName !== 'id', 320 | (field) => parseClientType(field, clientSchema.modelName).__isRelation && !field.isList, 321 | false 322 | ) 323 | } 324 | 325 | function generateUpdateObjectMutationInputArguments ( 326 | clientSchema: ClientSchema 327 | ): GraphQLObjectType { 328 | return generateObjectMutationInputArguments( 329 | clientSchema, 330 | (field) => !parseClientType(field, clientSchema.modelName).__isRelation, 331 | (field) => parseClientType(field, clientSchema.modelName).__isRelation && !field.isList, 332 | true, 333 | false, 334 | false 335 | ) 336 | } 337 | 338 | const simpleConnectionArgs = { 339 | skip: { 340 | type: GraphQLInt 341 | }, 342 | take: { 343 | type: GraphQLInt 344 | } 345 | } 346 | 347 | function generateQueryFilterInputArguments ( 348 | clientSchema: ClientSchema 349 | ): GraphQLObjectType { 350 | const args = generateObjectMutationInputArguments( 351 | clientSchema, 352 | (field) => !parseClientType(field, clientSchema.modelName).__isRelation, 353 | (field) => parseClientType(field, clientSchema.modelName).__isRelation && !field.isList, 354 | true, 355 | true, 356 | false 357 | ) 358 | 359 | return mergeObjects( 360 | schemaType === 'RELAY' ? connectionArgs : simpleConnectionArgs, 361 | { 362 | filter: { 363 | type: new GraphQLInputObjectType({ 364 | name: `${clientSchema.modelName}Filter`, 365 | fields: args 366 | }) 367 | }, 368 | orderBy: { 369 | type: generateQueryOrderByEnum(clientSchema) 370 | } 371 | } 372 | ) 373 | } 374 | 375 | function generateQueryOrderByEnum ( 376 | clientSchema: ClientSchema 377 | ): GraphQLEnumType { 378 | const values = [] 379 | clientSchema.fields.filter((field) => isScalar(field.typeIdentifier)).forEach((field) => { 380 | values.push(`${field.fieldName}_ASC`) 381 | values.push(`${field.fieldName}_DESC`) 382 | }) 383 | return new GraphQLEnumType({ 384 | name: `${clientSchema.modelName}SortBy`, 385 | values: mapArrayToObject(values, (x) => x, (x) => ({value: x})) 386 | }) 387 | } 388 | 389 | function patchRelations (clientSchema: ClientSchema) : ClientSchema { 390 | clientSchema.fields.forEach((field) => { 391 | if (field.relationId !== null) { 392 | field.relation = relations.filter((relation) => relation.id === field.relationId)[0] 393 | } 394 | }) 395 | 396 | return clientSchema 397 | } 398 | 399 | const clientTypes: ClientTypes = {} 400 | 401 | const NodeInterfaceType = new GraphQLInterfaceType({ 402 | name: 'NodeInterface', 403 | fields: () => ({ 404 | id: { type: GraphQLID } 405 | }), 406 | resolveType: (node, info) => { 407 | const externalId = externalIdFromQueryInfo(info) 408 | const {type} = fromGlobalId(externalId) 409 | return clientTypes[type].objectType 410 | } 411 | }) 412 | 413 | // generate object types without relationships properties since we need all of the object types first 414 | mapArrayToObject( 415 | clientSchemas, 416 | (clientSchema) => clientSchema.modelName, 417 | (clientSchema) => { 418 | const objectType = generateObjectType(clientSchema, NodeInterfaceType) 419 | const { connectionType, edgeType } = connectionDefinitions({ 420 | name: clientSchema.modelName, 421 | nodeType: objectType, 422 | connectionFields: () => ({ 423 | totalCount: { 424 | type: GraphQLInt, 425 | resolve: (conn) => conn.totalCount 426 | } 427 | }) 428 | }) 429 | const createMutationInputArguments = generateCreateObjectMutationInputArguments(clientSchema) 430 | const updateMutationInputArguments = generateUpdateObjectMutationInputArguments(clientSchema) 431 | const queryFilterInputArguments = generateQueryFilterInputArguments(clientSchema) 432 | const uniqueQueryInputArguments = generateUniqueQueryInputArguments(clientSchema) 433 | clientSchema = patchRelations(clientSchema) 434 | return { 435 | clientSchema, 436 | objectType, 437 | connectionType, 438 | edgeType, 439 | createMutationInputArguments, 440 | updateMutationInputArguments, 441 | queryFilterInputArguments, 442 | uniqueQueryInputArguments 443 | } 444 | }, 445 | clientTypes 446 | ) 447 | 448 | // set relationship properties 449 | for (const modelName in clientTypes) { 450 | injectRelationships( 451 | clientTypes[modelName].objectType, 452 | clientTypes[modelName].clientSchema, 453 | clientTypes, 454 | schemaType 455 | ) 456 | } 457 | 458 | // set nullable properties 459 | for (const modelName in clientTypes) { 460 | wrapWithNonNull( 461 | clientTypes[modelName].objectType, 462 | clientTypes[modelName].clientSchema 463 | ) 464 | } 465 | 466 | const viewerFields = {} 467 | for (const modelName in clientTypes) { 468 | viewerFields[`all${modelName}s`] = { 469 | type: schemaType === 'RELAY' 470 | ? clientTypes[modelName].connectionType 471 | : new GraphQLList(clientTypes[modelName].objectType), 472 | args: clientTypes[modelName].queryFilterInputArguments, 473 | resolve: (_, args, { operation, rootValue: { currentUser, backend } }) => { 474 | return backend.allNodesByType(modelName, args, clientTypes[modelName].clientSchema, currentUser, operation) 475 | .then(({array, hasMorePages}) => { 476 | if (schemaType === 'RELAY') { 477 | const edges = array.map((item) => ({node: item, cursor: toGlobalId(args.orderBy || 'id_ASC', item.id)})) 478 | const pageInfo = { 479 | hasNextPage: hasMorePages, 480 | hasPreviousPage: false, 481 | startCursor: edges[0] ? edges[0].cursor : null, 482 | endCursor: edges[edges.length-1] ? edges[edges.length-1].cursor : null 483 | } 484 | return { 485 | edges, 486 | pageInfo, 487 | totalCount: array.length 488 | } 489 | } else { 490 | return array 491 | } 492 | }) 493 | } 494 | } 495 | } 496 | 497 | viewerFields.id = { 498 | type: GraphQLID, 499 | resolve: (obj) => toGlobalId('User', obj.id) 500 | } 501 | 502 | if (clientTypes.User) { 503 | viewerFields.user = { 504 | type: clientTypes.User.objectType, 505 | resolve: (_, args, { rootValue: { backend } }) => ( 506 | backend.user() 507 | ) 508 | } 509 | } 510 | 511 | const viewerType = new GraphQLObjectType({ 512 | name: 'Viewer', 513 | fields: viewerFields, 514 | interfaces: [NodeInterfaceType] 515 | }) 516 | 517 | return {clientTypes, NodeInterfaceType, viewerType, viewerFields} 518 | } 519 | --------------------------------------------------------------------------------