From 9957fe914bedd21ad805a3d99f38aa6580fbe9e0 Mon Sep 17 00:00:00 2001 From: calebmer Date: Sun, 17 Apr 2016 14:48:05 -0400 Subject: [PATCH] feat: have insert mutation follow relay specification --- src/graphql/createTableInsertField.js | 88 +++++-- tests/graphql/createTableInsertField.test.js | 17 +- tests/integration/fixtures/insert.graphql | 52 +++-- tests/integration/fixtures/insert.json | 232 ++++++++++--------- 4 files changed, 232 insertions(+), 157 deletions(-) diff --git a/src/graphql/createTableInsertField.js b/src/graphql/createTableInsertField.js index a7fdd8583b..fb34b813ce 100644 --- a/src/graphql/createTableInsertField.js +++ b/src/graphql/createTableInsertField.js @@ -1,8 +1,17 @@ import { fromPairs, camelCase, upperFirst, identity } from 'lodash' -import { getNullableType, GraphQLNonNull, GraphQLInputObjectType } from 'graphql' import createTableType from './createTableType.js' import getColumnType from './getColumnType.js' +import { + getNullableType, + GraphQLNonNull, + GraphQLObjectType, + GraphQLInputObjectType, + GraphQLString, +} from 'graphql' + +const pascalCase = string => upperFirst(camelCase(string)) + /** * Creates a field which will create a new row. * @@ -10,12 +19,12 @@ import getColumnType from './getColumnType.js' * @returns {GraphQLFieldConfig} */ const createTableInsertField = table => ({ - type: createTableType(table), - description: `Creates a new node of type \`${upperFirst(camelCase(table.name))}\`.`, + type: createPayloadType(table), + description: `Creates a new node of type \`${pascalCase(table.name)}\`.`, args: { - [camelCase(table.name)]: { - type: new GraphQLNonNull(createTableInputType(table)), + input: { + type: new GraphQLNonNull(createInputType(table)), description: 'The new node to be created.', }, }, @@ -25,23 +34,50 @@ const createTableInsertField = table => ({ export default createTableInsertField -/** - * Similar to `createTableType` except it exclusively creates an input type. - * - * @param {Table} table - * @returns {GraphQLInputObjectType} - */ -const createTableInputType = table => +const createInputType = table => new GraphQLInputObjectType({ - name: upperFirst(camelCase(`${table.name}_input`)), - description: table.description, - fields: fromPairs( - table.columns - .map(column => [camelCase(column.name), { - type: (column.hasDefault ? getNullableType : identity)(getColumnType(column)), - description: column.description, - }]), - ), + name: pascalCase(`insert_${table.name}_input`), + description: `Inserts a \`${pascalCase(table.name)}\` into the backend.`, + fields: { + ...fromPairs( + table.columns + .map(column => [camelCase(column.name), { + type: (column.hasDefault ? getNullableType : identity)(getColumnType(column)), + description: column.description, + }]), + ), + clientMutationId: { + type: GraphQLString, + description: + 'An optional mutation ID for client’s to use in tracking mutations. ' + + 'This field has no meaning to the server and is simply returned as ' + + 'is.', + }, + }, + }) + +const createPayloadType = table => + new GraphQLObjectType({ + name: pascalCase(`insert_${table.name}_payload`), + description: + `Returns the full newly inserted \`${pascalCase(table.name)}\` after the ` + + ' mutation.', + + fields: { + [camelCase(table.name)]: { + type: createTableType(table), + description: `The newly inserted \`${pascalCase(table.name)}\`.`, + resolve: source => source[table.name], + }, + + clientMutationId: { + type: GraphQLString, + description: + 'If the mutation was passed a `clientMutationId` this is the exact ' + + 'same value.', + resolve: ({ clientMutationId }) => clientMutationId, + }, + }, }) const resolveCreate = table => { @@ -53,7 +89,8 @@ const resolveCreate = table => { return async (source, args, { client }) => { // Get the input object value from the args. - const object = args[camelCase(table.name)] + const { input } = args + const { clientMutationId } = input // Insert the thing making sure we return the newly inserted row. const result = await client.queryAsync( tableSql @@ -63,7 +100,7 @@ const resolveCreate = table => { // Get the value for this column, if it does not exist, we will not try // inserting it. Rather letting the database choose how to handle the // null/default. - const value = object[camelCase(name)] + const value = input[camelCase(name)] if (!value) return null return tableSql[name].value(value) }) @@ -73,6 +110,9 @@ const resolveCreate = table => { .toQuery() ) // Return the first (and likely only) row. - return result.rows[0] + return { + [table.name]: result.rows[0], + clientMutationId, + } } } diff --git a/tests/graphql/createTableInsertField.test.js b/tests/graphql/createTableInsertField.test.js index f579afe539..9ffd1469cc 100644 --- a/tests/graphql/createTableInsertField.test.js +++ b/tests/graphql/createTableInsertField.test.js @@ -1,15 +1,20 @@ import expect from 'expect' -import { GraphQLNonNull, GraphQLInputObjectType, GraphQLScalarType } from 'graphql' +import { GraphQLNonNull, GraphQLInputObjectType, GraphQLScalarType, GraphQLString } from 'graphql' import { TestTable, TestColumn } from '../helpers.js' import createTableInsertField from '#/graphql/createTableInsertField.js' describe('createTableInsertField', () => { it('returns field with single argument', () => { const field = createTableInsertField(new TestTable()) - expect(field.args.test.type).toBeA(GraphQLNonNull) - expect(field.args.test.type.ofType).toBeA(GraphQLInputObjectType) - expect(field.args.test.type.ofType.name).toEqual('TestInput') - expect(field.args.test.type.ofType.getFields()).toIncludeKeys(['test']) + expect(field.args.input.type).toBeA(GraphQLNonNull) + expect(field.args.input.type.ofType).toBeA(GraphQLInputObjectType) + expect(field.args.input.type.ofType.name).toEqual('InsertTestInput') + expect(field.args.input.type.ofType.getFields()).toIncludeKeys(['test']) + }) + + it('will have a `clientMutationId` field in input', () => { + const field = createTableInsertField(new TestTable()) + expect(field.args.input.type.ofType.getFields().clientMutationId.type).toBe(GraphQLString) }) it('will make nullable columns with a default', () => { @@ -22,7 +27,7 @@ describe('createTableInsertField', () => { new TestColumn({ name: 'status', isNullable: false, hasDefault: true }), ], })) - const inputFields = field.args.test.type.ofType.getFields() + const inputFields = field.args.input.type.ofType.getFields() expect(inputFields.id.type).toBeA(GraphQLScalarType) expect(inputFields.givenName.type).toBeA(GraphQLNonNull) expect(inputFields.givenName.type.ofType).toBeA(GraphQLScalarType) diff --git a/tests/integration/fixtures/insert.graphql b/tests/integration/fixtures/insert.graphql index 630ef88768..d6d15f3006 100644 --- a/tests/integration/fixtures/insert.graphql +++ b/tests/integration/fixtures/insert.graphql @@ -1,27 +1,41 @@ mutation InsertTest { - a: insertPost(post: {headline: "Hello, world!", authorId: 10}) { ...post } - b: insertPost(post: {headline: "Hello, world!", authorId: 10}) { ...post } - c: insertPost(post: {headline: "Hello, world!", authorId: 10}) { ...post } - d: insertPost(post: {headline: "Hello, world!", authorId: 10}) { ...post } - e: insertPost(post: {headline: "Hello, world!", authorId: 10}) { ...post } - f: insertPost(post: {id: 200, headline: "Manually set stuffs.", authorId: 1}) { - id - headline - authorId + a: insertPost(input: {headline: "Hello, world!", authorId: 10}) { ...post } + b: insertPost(input: {headline: "Hello, world!", authorId: 10}) { ...post } + c: insertPost(input: {headline: "Hello, world!", authorId: 10}) { + clientMutationId + ...post + } + d: insertPost(input: {headline: "Hello, world!", authorId: 10, clientMutationId: "abcde"}) { + clientMutationId + ...post + } + e: insertPost(input: {headline: "Hello, world!", authorId: 10, clientMutationId: "123456"}) { + clientMutationId + ...post + } + f: insertPost(input: {id: 200, headline: "Manually set stuffs.", authorId: 1}) { + clientMutationId + post { + id + headline + authorId + } } } -fragment post on Post { - id - headline - personByAuthorId { +fragment post on InsertPostPayload { + post { id - givenName - familyName - postListByAuthorId { - list { - id - headline + headline + personByAuthorId { + id + givenName + familyName + postListByAuthorId { + list { + id + headline + } } } } diff --git a/tests/integration/fixtures/insert.json b/tests/integration/fixtures/insert.json index cf88cae031..06eec80498 100644 --- a/tests/integration/fixtures/insert.json +++ b/tests/integration/fixtures/insert.json @@ -1,134 +1,150 @@ { "data": { "a": { - "id": 13, - "headline": "Hello, world!", - "personByAuthorId": { - "id": 10, - "givenName": "Jonathan", - "familyName": "Campbell", - "postListByAuthorId": { - "list": [ - { - "id": 13, - "headline": "Hello, world!" - } - ] + "post": { + "id": 13, + "headline": "Hello, world!", + "personByAuthorId": { + "id": 10, + "givenName": "Jonathan", + "familyName": "Campbell", + "postListByAuthorId": { + "list": [ + { + "id": 13, + "headline": "Hello, world!" + } + ] + } } } }, "b": { - "id": 14, - "headline": "Hello, world!", - "personByAuthorId": { - "id": 10, - "givenName": "Jonathan", - "familyName": "Campbell", - "postListByAuthorId": { - "list": [ - { - "id": 13, - "headline": "Hello, world!" - }, - { - "id": 14, - "headline": "Hello, world!" - } - ] + "post": { + "id": 14, + "headline": "Hello, world!", + "personByAuthorId": { + "id": 10, + "givenName": "Jonathan", + "familyName": "Campbell", + "postListByAuthorId": { + "list": [ + { + "id": 13, + "headline": "Hello, world!" + }, + { + "id": 14, + "headline": "Hello, world!" + } + ] + } } } }, "c": { - "id": 15, - "headline": "Hello, world!", - "personByAuthorId": { - "id": 10, - "givenName": "Jonathan", - "familyName": "Campbell", - "postListByAuthorId": { - "list": [ - { - "id": 13, - "headline": "Hello, world!" - }, - { - "id": 14, - "headline": "Hello, world!" - }, - { - "id": 15, - "headline": "Hello, world!" - } - ] + "clientMutationId": null, + "post": { + "id": 15, + "headline": "Hello, world!", + "personByAuthorId": { + "id": 10, + "givenName": "Jonathan", + "familyName": "Campbell", + "postListByAuthorId": { + "list": [ + { + "id": 13, + "headline": "Hello, world!" + }, + { + "id": 14, + "headline": "Hello, world!" + }, + { + "id": 15, + "headline": "Hello, world!" + } + ] + } } } }, "d": { - "id": 16, - "headline": "Hello, world!", - "personByAuthorId": { - "id": 10, - "givenName": "Jonathan", - "familyName": "Campbell", - "postListByAuthorId": { - "list": [ - { - "id": 13, - "headline": "Hello, world!" - }, - { - "id": 14, - "headline": "Hello, world!" - }, - { - "id": 15, - "headline": "Hello, world!" - }, - { - "id": 16, - "headline": "Hello, world!" - } - ] + "clientMutationId": "abcde", + "post": { + "id": 16, + "headline": "Hello, world!", + "personByAuthorId": { + "id": 10, + "givenName": "Jonathan", + "familyName": "Campbell", + "postListByAuthorId": { + "list": [ + { + "id": 13, + "headline": "Hello, world!" + }, + { + "id": 14, + "headline": "Hello, world!" + }, + { + "id": 15, + "headline": "Hello, world!" + }, + { + "id": 16, + "headline": "Hello, world!" + } + ] + } } } }, "e": { - "id": 17, - "headline": "Hello, world!", - "personByAuthorId": { - "id": 10, - "givenName": "Jonathan", - "familyName": "Campbell", - "postListByAuthorId": { - "list": [ - { - "id": 13, - "headline": "Hello, world!" - }, - { - "id": 14, - "headline": "Hello, world!" - }, - { - "id": 15, - "headline": "Hello, world!" - }, - { - "id": 16, - "headline": "Hello, world!" - }, - { - "id": 17, - "headline": "Hello, world!" - } - ] + "clientMutationId": "123456", + "post": { + "id": 17, + "headline": "Hello, world!", + "personByAuthorId": { + "id": 10, + "givenName": "Jonathan", + "familyName": "Campbell", + "postListByAuthorId": { + "list": [ + { + "id": 13, + "headline": "Hello, world!" + }, + { + "id": 14, + "headline": "Hello, world!" + }, + { + "id": 15, + "headline": "Hello, world!" + }, + { + "id": 16, + "headline": "Hello, world!" + }, + { + "id": 17, + "headline": "Hello, world!" + } + ] + } } } }, "f": { - "id": 200, - "headline": "Manually set stuffs.", - "authorId": 1 + "clientMutationId": null, + "post": { + "id": 200, + "headline": "Manually set stuffs.", + "authorId": 1 + } } } }