Skip to content

Commit

Permalink
feat: have insert mutation follow relay specification
Browse files Browse the repository at this point in the history
  • Loading branch information
calebmer committed Apr 17, 2016
1 parent 10ca562 commit 9957fe9
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 157 deletions.
88 changes: 64 additions & 24 deletions src/graphql/createTableInsertField.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
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.
*
* @param {Table} table
* @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.',
},
},
Expand All @@ -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 => {
Expand All @@ -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
Expand All @@ -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)
})
Expand All @@ -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,
}
}
}
17 changes: 11 additions & 6 deletions tests/graphql/createTableInsertField.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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)
Expand Down
52 changes: 33 additions & 19 deletions tests/integration/fixtures/insert.graphql
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Expand Down
Loading

0 comments on commit 9957fe9

Please sign in to comment.