From 1d3eb5ddd671e6d8a3b787becceb0ce9a65a6df9 Mon Sep 17 00:00:00 2001 From: calebmer Date: Fri, 30 Sep 2016 18:37:19 -0400 Subject: [PATCH] feat(graphql): add collection update mutation --- .../createCollectionMutationFieldEntries.ts | 3 + ...teDeleteCollectionKeyMutationFieldEntry.ts | 19 +- ...reateDeleteCollectionMutationFieldEntry.ts | 40 +++-- ...reateUpdateCollectionMutationFieldEntry.ts | 118 ++++++++++++ .../postgraphqlIntegration-test.js.snap | 170 ++++++++++++++++++ .../__tests__/postgraphqlIntegration-test.js | 20 ++- 6 files changed, 332 insertions(+), 38 deletions(-) create mode 100644 src/graphql/schema/collection/mutations/createUpdateCollectionMutationFieldEntry.ts diff --git a/src/graphql/schema/collection/createCollectionMutationFieldEntries.ts b/src/graphql/schema/collection/createCollectionMutationFieldEntries.ts index e2abaca4c2..6cd03802f9 100644 --- a/src/graphql/schema/collection/createCollectionMutationFieldEntries.ts +++ b/src/graphql/schema/collection/createCollectionMutationFieldEntries.ts @@ -2,6 +2,7 @@ import { GraphQLFieldConfig } from 'graphql' import { Collection } from '../../../interface' import BuildToken from '../BuildToken' import createCreateCollectionMutationFieldEntry from './mutations/createCreateCollectionMutationFieldEntry' +import createUpdateCollectionMutationFieldEntry from './mutations/createUpdateCollectionMutationFieldEntry' import createDeleteCollectionMutationFieldEntry from './mutations/createDeleteCollectionMutationFieldEntry' import createDeleteCollectionKeyMutationFieldEntry from './mutations/createDeleteCollectionKeyMutationFieldEntry' @@ -19,6 +20,8 @@ export default function createCollectionMutationFieldEntries ( const optionalEntries: Array<[string, GraphQLFieldConfig] | undefined> = [ // Add the create collection mutation. createCreateCollectionMutationFieldEntry(buildToken, collection), + // Add the update collection mutation. Uses the collection’s primary key. + createUpdateCollectionMutationFieldEntry(buildToken, collection), // Add the delete collection mutation. Uses the collection’s primary key. createDeleteCollectionMutationFieldEntry(buildToken, collection), // Add the delete mutation for all of the collection keys. diff --git a/src/graphql/schema/collection/mutations/createDeleteCollectionKeyMutationFieldEntry.ts b/src/graphql/schema/collection/mutations/createDeleteCollectionKeyMutationFieldEntry.ts index 15d62d3fe4..02cd15e672 100644 --- a/src/graphql/schema/collection/mutations/createDeleteCollectionKeyMutationFieldEntry.ts +++ b/src/graphql/schema/collection/mutations/createDeleteCollectionKeyMutationFieldEntry.ts @@ -5,6 +5,7 @@ import BuildToken from '../../BuildToken' import createMutationField from '../../createMutationField' import getCollectionType from '../getCollectionType' import createCollectionKeyInputHelpers from '../createCollectionKeyInputHelpers' +import { createDeleteCollectionOutputFieldEntries } from './createDeleteCollectionMutationFieldEntry' /** * Creates a delete mutation which will delete a single value from a collection @@ -25,24 +26,8 @@ export default function createDeleteCollectionKeyMutationFieldEntry ( return [formatName.field(name), createMutationField(buildToken, { name, - // Add the input fields from our input helpers. inputFields: inputHelpers.fieldEntries, - outputFields: [ - // Add the deleted value as an output field so the user can see the - // object they just deleted. - [formatName.field(collection.type.name), { - type: getCollectionType(buildToken, collection), - resolve: value => value, - }], - // Add the deleted values globally unique id as well. This one is - // especially useful for removing old nodes from the cache. - collection.primaryKey && [formatName.field(`deleted-${collection.type.name}-id`), { - type: GraphQLID, - resolve: value => idSerde.serialize(collection.primaryKey!, collection.primaryKey!.getKeyFromValue(value)) - }], - ], - // Actually delete the value getting the key from our input with our - // helpers. + outputFields: createDeleteCollectionOutputFieldEntries(buildToken, collection), execute: (context, input) => collectionKey.delete!(context, inputHelpers.getKey(input)), })] diff --git a/src/graphql/schema/collection/mutations/createDeleteCollectionMutationFieldEntry.ts b/src/graphql/schema/collection/mutations/createDeleteCollectionMutationFieldEntry.ts index df70e9314c..594c9befaf 100644 --- a/src/graphql/schema/collection/mutations/createDeleteCollectionMutationFieldEntry.ts +++ b/src/graphql/schema/collection/mutations/createDeleteCollectionMutationFieldEntry.ts @@ -34,20 +34,7 @@ export default function createDeleteCollectionMutationFieldEntry ( type: new GraphQLNonNull(GraphQLID), }], ], - outputFields: [ - // Add the deleted value as an output field so the user can see the - // object they just deleted. - [formatName.field(collection.type.name), { - type: getCollectionType(buildToken, collection), - resolve: value => value, - }], - // Add the deleted values globally unique id as well. This one is - // especially useful for removing old nodes from the cache. - [formatName.field(`deleted-${collection.type.name}-id`), { - type: GraphQLID, - resolve: value => idSerde.serialize(primaryKey, primaryKey.getKeyFromValue(value)) - }], - ], + outputFields: createDeleteCollectionOutputFieldEntries(buildToken, collection), // Execute by deserializing the id into its component parts and delete a // value in the collection using that key. execute: (context, input) => { @@ -60,3 +47,28 @@ export default function createDeleteCollectionMutationFieldEntry ( }, })] } + +/** + * Creates the output fields returned by the collection delete mutation. + */ +export function createDeleteCollectionOutputFieldEntries ( + buildToken: BuildToken, + collection: Collection, +): Array<[string, GraphQLFieldConfig] | null> { + const { primaryKey } = collection + + return [ + // Add the deleted value as an output field so the user can see the + // object they just deleted. + [formatName.field(collection.type.name), { + type: getCollectionType(buildToken, collection), + resolve: value => value, + }], + // Add the deleted values globally unique id as well. This one is + // especially useful for removing old nodes from the cache. + primaryKey ? [formatName.field(`deleted-${collection.type.name}-id`), { + type: GraphQLID, + resolve: value => idSerde.serialize(primaryKey, primaryKey.getKeyFromValue(value)), + }] : null, + ] +} diff --git a/src/graphql/schema/collection/mutations/createUpdateCollectionMutationFieldEntry.ts b/src/graphql/schema/collection/mutations/createUpdateCollectionMutationFieldEntry.ts new file mode 100644 index 0000000000..ec4db338b7 --- /dev/null +++ b/src/graphql/schema/collection/mutations/createUpdateCollectionMutationFieldEntry.ts @@ -0,0 +1,118 @@ +import { + GraphQLInputType, + GraphQLNonNull, + GraphQLID, + GraphQLInputObjectType, + GraphQLInputFieldConfig, + GraphQLFieldConfig, + getNullableType, +} from 'graphql' +import { Collection, ObjectType } from '../../../../interface' +import { formatName, buildObject, idSerde, memoize2 } from '../../../utils' +import BuildToken from '../../BuildToken' +import getType from '../../getType' +import createMutationField from '../../createMutationField' +import transformInputValue, { $$inputValueKeyName } from '../../transformInputValue' +import getCollectionType from '../getCollectionType' + +/** + * Creates a delete mutation that uses the primary key of a collection and an + * object’s global GraphQL identifier to delete a value in the collection. + */ +// TODO: test +export default function createDeleteCollectionMutationFieldEntry ( + buildToken: BuildToken, + collection: Collection, +): [string, GraphQLFieldConfig] | undefined { + const { primaryKey } = collection + + // If there is no primary key, or the primary key has no delete method. End + // early. + if (!primaryKey || !primaryKey.update) + return + + const { options, inventory } = buildToken + const name = `update-${collection.type.name}` + const patchFieldName = formatName.field(`${collection.type.name}-patch`) + const patchType = getCollectionPatchType(buildToken, collection) + + return [formatName.field(name), createMutationField(buildToken, { + name, + inputFields: [ + // The only input field we want is the globally unique id which + // corresponds to the primary key of this collection. + [options.nodeIdFieldName, { + // TODO: description + type: new GraphQLNonNull(GraphQLID), + }], + // Also include the patch object type. This is its own object type so + // that people can just have a single patch object and not need to rename + // keys. This also means users can freely upload entire objects to this + // field. + [patchFieldName, { + // TODO: description + type: new GraphQLNonNull(patchType), + }], + ], + outputFields: createUpdateCollectionOutputFieldEntries(buildToken, collection), + // Execute by deserializing the id into its component parts and delete a + // value in the collection using that key. + execute: (context, input) => { + const { collectionKey, keyValue } = idSerde.deserialize(inventory, input[options.nodeIdFieldName] as string) + + if (collectionKey !== primaryKey) + throw new Error(`The provided id is for collection '${collectionKey.collection.name}', not the expected collection '${collection.name}'.`) + + // Get the patch from our input. + const patch = transformInputValue(patchType, input[patchFieldName]) + + return primaryKey.update!(context, keyValue, patch as any) + }, + })] +} + +/** + * Gets the patch type for a collection. The patch type allows us to define + * fine-grained changes to our object and is very similar to the input object + * type. + */ +export const getCollectionPatchType = memoize2(createCollectionPatchType) + +/** + * The internal un-memoized implementation of `getCollectionPatchType`. + * + * @private + */ +function createCollectionPatchType (buildToken: BuildToken, collection: Collection): GraphQLInputObjectType { + const { type } = collection + return new GraphQLInputObjectType({ + name: formatName.type(`${type.name}-patch`), + // TODO: description + fields: () => buildObject>( + Array.from(type.fields).map<[string, GraphQLInputFieldConfig]>(([fieldName, field]) => + [formatName.field(fieldName), { + // TODO: description + type: getNullableType(getType(buildToken, field.type, true)) as GraphQLInputType, + [$$inputValueKeyName]: fieldName, + }] + ) + ), + }) +} + +/** + * Creates the output fields returned by the collection update mutation. + */ +export function createUpdateCollectionOutputFieldEntries ( + buildToken: BuildToken, + collection: Collection, +): Array<[string, GraphQLFieldConfig]> { + return [ + // Add the updated value as an output field so the user can see the + // object they just updated. + [formatName.field(collection.type.name), { + type: getCollectionType(buildToken, collection), + resolve: value => value, + }], + ] +} diff --git a/src/postgraphql/__tests__/__snapshots__/postgraphqlIntegration-test.js.snap b/src/postgraphql/__tests__/__snapshots__/postgraphqlIntegration-test.js.snap index 5f1ee1957a..a6d0befb50 100644 --- a/src/postgraphql/__tests__/__snapshots__/postgraphqlIntegration-test.js.snap +++ b/src/postgraphql/__tests__/__snapshots__/postgraphqlIntegration-test.js.snap @@ -782,6 +782,12 @@ input CompoundKeyInput { extra: Boolean } +input CompoundKeyPatch { + personId2: Int + personId1: Int + extra: Boolean +} + # A connection to a list of \`CompoundKey\` values. type CompoundKeysConnection { # Information to aid in pagination. @@ -1098,16 +1104,20 @@ enum ForeignKeysOrderBy { type Mutation { createForeignKey(input: CreateForeignKeyInput!): CreateForeignKeyPayload createPost(input: CreatePostInput!): CreatePostPayload + updatePost(input: UpdatePostInput!): UpdatePostPayload deletePost(input: DeletePostInput!): DeletePostPayload deletePostById(input: DeletePostByIdInput!): DeletePostByIdPayload createTypes(input: CreateTypesInput!): CreateTypesPayload + updateTypes(input: UpdateTypesInput!): UpdateTypesPayload deleteTypes(input: DeleteTypesInput!): DeleteTypesPayload deleteTypesById(input: DeleteTypesByIdInput!): DeleteTypesByIdPayload createUpdatableView(input: CreateUpdatableViewInput!): CreateUpdatableViewPayload createCompoundKey(input: CreateCompoundKeyInput!): CreateCompoundKeyPayload + updateCompoundKey(input: UpdateCompoundKeyInput!): UpdateCompoundKeyPayload deleteCompoundKey(input: DeleteCompoundKeyInput!): DeleteCompoundKeyPayload deleteCompoundKeyByPersonId1AndPersonId2(input: DeleteCompoundKeyByPersonId1AndPersonId2Input!): DeleteCompoundKeyByPersonId1AndPersonId2Payload createPerson(input: CreatePersonInput!): CreatePersonPayload + updatePerson(input: UpdatePersonInput!): UpdatePersonPayload deletePerson(input: DeletePersonInput!): DeletePersonPayload deletePersonByEmail(input: DeletePersonByEmailInput!): DeletePersonByEmailPayload deletePersonById(input: DeletePersonByIdInput!): DeletePersonByIdPayload @@ -1246,6 +1256,14 @@ input PersonInput { createdAt: String } +input PersonPatch { + id: Int + name: String + about: String + email: Email + createdAt: String +} + type Post implements Node { # A globally unique identifier. Can be used in various places throughout the system to identify this single value. __id: ID! @@ -1265,6 +1283,13 @@ input PostInput { authorId: Int } +input PostPatch { + id: Int + headline: String + body: String + authorId: Int +} + # A connection to a list of \`Post\` values. type PostsConnection { # Information to aid in pagination. @@ -1576,6 +1601,18 @@ enum TypesOrderBy { NESTED_COMPOUND_TYPE_DESC } +input TypesPatch { + id: Int + bigint: Int + boolean: Boolean + varchar: String + enum: Color + domain: AnInt + domain2: AnotherInt + compoundType: CompoundTypeInput + nestedCompoundType: NestedCompoundTypeInput +} + # YOYOYO!! type UpdatableView { x: Int @@ -1633,6 +1670,54 @@ enum UpdatableViewsOrderBy { CONSTANT_DESC } +input UpdateCompoundKeyInput { + clientMutationId: String + __id: ID! + compoundKeyPatch: CompoundKeyPatch! +} + +type UpdateCompoundKeyPayload { + clientMutationId: String + compoundKey: CompoundKey + query: Query +} + +input UpdatePersonInput { + clientMutationId: String + __id: ID! + personPatch: PersonPatch! +} + +type UpdatePersonPayload { + clientMutationId: String + person: Person + query: Query +} + +input UpdatePostInput { + clientMutationId: String + __id: ID! + postPatch: PostPatch! +} + +type UpdatePostPayload { + clientMutationId: String + post: Post + query: Query +} + +input UpdateTypesInput { + clientMutationId: String + __id: ID! + typesPatch: TypesPatch! +} + +type UpdateTypesPayload { + clientMutationId: String + types: Types + query: Query +} + # A universally unique identifier as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122). scalar Uuid " @@ -1669,6 +1754,12 @@ input CompoundKeyInput { extra: Boolean } +input CompoundKeyPatch { + personId2: Int + personId1: Int + extra: Boolean +} + # A connection to a list of \`CompoundKey\` values. type CompoundKeysConnection { # Information to aid in pagination. @@ -1985,16 +2076,20 @@ enum ForeignKeysOrderBy { type Mutation { createForeignKey(input: CreateForeignKeyInput!): CreateForeignKeyPayload createPost(input: CreatePostInput!): CreatePostPayload + updatePost(input: UpdatePostInput!): UpdatePostPayload deletePost(input: DeletePostInput!): DeletePostPayload deletePostByRowId(input: DeletePostByRowIdInput!): DeletePostByRowIdPayload createTypes(input: CreateTypesInput!): CreateTypesPayload + updateTypes(input: UpdateTypesInput!): UpdateTypesPayload deleteTypes(input: DeleteTypesInput!): DeleteTypesPayload deleteTypesByRowId(input: DeleteTypesByRowIdInput!): DeleteTypesByRowIdPayload createUpdatableView(input: CreateUpdatableViewInput!): CreateUpdatableViewPayload createCompoundKey(input: CreateCompoundKeyInput!): CreateCompoundKeyPayload + updateCompoundKey(input: UpdateCompoundKeyInput!): UpdateCompoundKeyPayload deleteCompoundKey(input: DeleteCompoundKeyInput!): DeleteCompoundKeyPayload deleteCompoundKeyByPersonId1AndPersonId2(input: DeleteCompoundKeyByPersonId1AndPersonId2Input!): DeleteCompoundKeyByPersonId1AndPersonId2Payload createPerson(input: CreatePersonInput!): CreatePersonPayload + updatePerson(input: UpdatePersonInput!): UpdatePersonPayload deletePerson(input: DeletePersonInput!): DeletePersonPayload deletePersonByEmail(input: DeletePersonByEmailInput!): DeletePersonByEmailPayload deletePersonByRowId(input: DeletePersonByRowIdInput!): DeletePersonByRowIdPayload @@ -2133,6 +2228,14 @@ input PersonInput { createdAt: String } +input PersonPatch { + rowId: Int + name: String + about: String + email: Email + createdAt: String +} + type Post implements Node { # A globally unique identifier. Can be used in various places throughout the system to identify this single value. id: ID! @@ -2152,6 +2255,13 @@ input PostInput { authorId: Int } +input PostPatch { + rowId: Int + headline: String + body: String + authorId: Int +} + # A connection to a list of \`Post\` values. type PostsConnection { # Information to aid in pagination. @@ -2463,6 +2573,18 @@ enum TypesOrderBy { NESTED_COMPOUND_TYPE_DESC } +input TypesPatch { + rowId: Int + bigint: Int + boolean: Boolean + varchar: String + enum: Color + domain: AnInt + domain2: AnotherInt + compoundType: CompoundTypeInput + nestedCompoundType: NestedCompoundTypeInput +} + # YOYOYO!! type UpdatableView { x: Int @@ -2520,6 +2642,54 @@ enum UpdatableViewsOrderBy { CONSTANT_DESC } +input UpdateCompoundKeyInput { + clientMutationId: String + id: ID! + compoundKeyPatch: CompoundKeyPatch! +} + +type UpdateCompoundKeyPayload { + clientMutationId: String + compoundKey: CompoundKey + query: Query +} + +input UpdatePersonInput { + clientMutationId: String + id: ID! + personPatch: PersonPatch! +} + +type UpdatePersonPayload { + clientMutationId: String + person: Person + query: Query +} + +input UpdatePostInput { + clientMutationId: String + id: ID! + postPatch: PostPatch! +} + +type UpdatePostPayload { + clientMutationId: String + post: Post + query: Query +} + +input UpdateTypesInput { + clientMutationId: String + id: ID! + typesPatch: TypesPatch! +} + +type UpdateTypesPayload { + clientMutationId: String + types: Types + query: Query +} + # A universally unique identifier as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122). scalar Uuid " diff --git a/src/postgraphql/__tests__/postgraphqlIntegration-test.js b/src/postgraphql/__tests__/postgraphqlIntegration-test.js index c939a0d0cb..72595caebe 100644 --- a/src/postgraphql/__tests__/postgraphqlIntegration-test.js +++ b/src/postgraphql/__tests__/postgraphqlIntegration-test.js @@ -21,15 +21,21 @@ const kitchenSinkData = new Promise((resolve, reject) => { let schema1, schema2 beforeAll(withPGClient(async client => { - const catalog = await introspectDatabase(client, ['a', 'b', 'c']) + try { + const catalog = await introspectDatabase(client, ['a', 'b', 'c']) - const inventory1 = new Inventory() - addPGCatalogToInventory(inventory1, catalog) - schema1 = createGraphqlSchema(inventory1) + const inventory1 = new Inventory() + addPGCatalogToInventory(inventory1, catalog) + schema1 = createGraphqlSchema(inventory1) - const inventory2 = new Inventory() - addPGCatalogToInventory(inventory2, catalog, { renameIdToRowId: true }) - schema2 = createGraphqlSchema(inventory2, { nodeIdFieldName: 'id' }) + const inventory2 = new Inventory() + addPGCatalogToInventory(inventory2, catalog, { renameIdToRowId: true }) + schema2 = createGraphqlSchema(inventory2, { nodeIdFieldName: 'id' }) + } + catch (error) { + console.error(error.stack) + throw error + } })) test('schema', async () => {