From 483a8ed4c4287311eaafd4be5cb15ca8d475c878 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Tue, 19 Oct 2021 15:53:17 +0200 Subject: [PATCH 1/2] feat: unset field on null value through graphql api --- spec/ParseGraphQLServer.spec.js | 158 ++++++++++++++++++++- src/GraphQL/loaders/defaultGraphQLTypes.js | 10 +- src/GraphQL/loaders/parseClassMutations.js | 12 +- src/GraphQL/transformers/mutation.js | 25 +++- 4 files changed, 192 insertions(+), 13 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 9f75b94210..52427efcc2 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -6606,6 +6606,162 @@ describe('ParseGraphQLServer', () => { ); }); }); + + it('should unset fields when null used on update/create', async () => { + const customerSchema = new Parse.Schema('Customer'); + customerSchema.addString('aString'); + customerSchema.addBoolean('aBoolean'); + customerSchema.addDate('aDate'); + customerSchema.addArray('aArray'); + customerSchema.addGeoPoint('aGeoPoint'); + customerSchema.addPointer('aPointer', 'Customer'); + customerSchema.addObject('aObject'); + customerSchema.addPolygon('aPolygon'); + await customerSchema.save(); + + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + + const cus = new Parse.Object('Customer'); + await cus.save({ aString: 'hello' }); + + const fields = { + aString: "i'm string", + aBoolean: true, + aDate: new Date().toISOString(), + aArray: ['hello', 1], + aGeoPoint: { latitude: 30, longitude: 30 }, + aPointer: { link: cus.id }, + aObject: { prop: { subprop: 1 }, prop2: 'test' }, + aPolygon: [ + { latitude: 30, longitude: 30 }, + { latitude: 31, longitude: 31 }, + { latitude: 32, longitude: 32 }, + { latitude: 30, longitude: 30 }, + ], + }; + const nullFields = Object.keys(fields).reduce((acc, k) => ({ ...acc, [k]: null }), {}); + const result = await apolloClient.mutate({ + mutation: gql` + mutation CreateCustomer($input: CreateCustomerInput!) { + createCustomer(input: $input) { + customer { + id + aString + aBoolean + aDate + aArray { + ... on Element { + value + } + } + aGeoPoint { + longitude + latitude + } + aPointer { + objectId + } + aObject + aPolygon { + longitude + latitude + } + } + } + } + `, + variables: { + input: { fields }, + }, + }); + const { + data: { + createCustomer: { + customer: { aPointer, aArray, id, ...otherFields }, + }, + }, + } = result; + expect(id).toBeDefined(); + delete otherFields.__typename; + delete otherFields.aGeoPoint.__typename; + otherFields.aPolygon.forEach(v => { + delete v.__typename; + }); + expect({ + ...otherFields, + aPointer: { link: aPointer.objectId }, + aArray: aArray.map(({ value }) => value), + }).toEqual(fields); + + const updated = await apolloClient.mutate({ + mutation: gql` + mutation UpdateCustomer($input: UpdateCustomerInput!) { + updateCustomer(input: $input) { + customer { + aString + aBoolean + aDate + aArray { + ... on Element { + value + } + } + aGeoPoint { + longitude + latitude + } + aPointer { + objectId + } + aObject + aPolygon { + longitude + latitude + } + } + } + } + `, + variables: { + input: { fields: nullFields, id }, + }, + }); + const { + data: { + updateCustomer: { customer }, + }, + } = updated; + delete customer.__typename; + expect(Object.keys(customer).length).toEqual(8); + Object.keys(customer).forEach(k => { + expect(customer[k]).toBeNull(); + }); + try { + const queryResult = await apolloClient.query({ + query: gql` + query getEmptyCustomer($where: CustomerWhereInput!) { + customers(where: $where) { + edges { + node { + id + } + } + } + } + `, + variables: { + where: Object.keys(fields).reduce( + (acc, k) => ({ ...acc, [k]: { exists: false } }), + {} + ), + }, + }); + + expect(queryResult.data.customers.edges.length).toEqual(1); + } catch (e) { + console.log(JSON.stringify(e)); + } + }); }); describe('Files Mutations', () => { @@ -9141,7 +9297,7 @@ describe('ParseGraphQLServer', () => { const mutationResult = await apolloClient.mutate({ mutation: gql` mutation UnlinkFile($id: ID!) { - updateSomeClass(input: { id: $id, fields: { someField: { file: null } } }) { + updateSomeClass(input: { id: $id, fields: { someField: null } }) { someClass { someField { name diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index d1d092ef6f..f340cfb35f 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -357,21 +357,17 @@ const FILE_INFO = new GraphQLObjectType({ const FILE_INPUT = new GraphQLInputObjectType({ name: 'FileInput', + description: + 'If this field is set to null the file will be unlinked (the file will not be deleted on cloud storage).', fields: { file: { - description: - 'A File Scalar can be an url or a FileInfo object. If this field is set to null the file will be unlinked.', + description: 'A File Scalar can be an url or a FileInfo object.', type: FILE, }, upload: { description: 'Use this field if you want to create a new file.', type: GraphQLUpload, }, - unlink: { - description: - 'Use this field if you want to unlink the file (the file will not be deleted on cloud storage)', - type: GraphQLBoolean, - }, }, }); diff --git a/src/GraphQL/loaders/parseClassMutations.js b/src/GraphQL/loaders/parseClassMutations.js index 0ebca5b077..41d7c09dde 100644 --- a/src/GraphQL/loaders/parseClassMutations.js +++ b/src/GraphQL/loaders/parseClassMutations.js @@ -10,6 +10,14 @@ import { ParseGraphQLClassConfig } from '../../Controllers/ParseGraphQLControlle import { transformClassNameToGraphQL } from '../transformers/className'; import { transformTypes } from '../transformers/mutation'; +const filterDeletedFields = fields => + Object.keys(fields).reduce((acc, key) => { + if (typeof fields[key] === 'object' && fields[key]?.__op === 'Delete') { + acc[key] = null; + } + return acc; + }, fields); + const getOnlyRequiredFields = ( updatedFields, selectedFieldsString, @@ -131,7 +139,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG [getGraphQLQueryName]: { ...createdObject, updatedAt: createdObject.createdAt, - ...parseFields, + ...filterDeletedFields(parseFields), ...optimizedObject, }, }; @@ -240,7 +248,7 @@ const load = function (parseGraphQLSchema, parseClass, parseClassConfig: ?ParseG [getGraphQLQueryName]: { objectId: id, ...updatedObject, - ...parseFields, + ...filterDeletedFields(parseFields), ...optimizedObject, }, }; diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 583d330620..2a21816fe9 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -30,9 +30,17 @@ const transformTypes = async ( if (inputTypeField) { switch (true) { case inputTypeField.type === defaultGraphQLTypes.GEO_POINT_INPUT: + if (fields[field] === null) { + fields[field] = { __op: 'Delete' }; + break; + } fields[field] = transformers.geoPoint(fields[field]); break; case inputTypeField.type === defaultGraphQLTypes.POLYGON_INPUT: + if (fields[field] === null) { + fields[field] = { __op: 'Delete' }; + break; + } fields[field] = transformers.polygon(fields[field]); break; case inputTypeField.type === defaultGraphQLTypes.FILE_INPUT: @@ -48,6 +56,10 @@ const transformTypes = async ( ); break; case parseClass.fields[field].type === 'Pointer': + if (fields[field] === null) { + fields[field] = { __op: 'Delete' }; + break; + } fields[field] = await transformers.pointer( parseClass.fields[field].targetClass, field, @@ -56,6 +68,12 @@ const transformTypes = async ( req ); break; + default: + if (fields[field] === null) { + fields[field] = { __op: 'Delete' }; + return; + } + break; } } }); @@ -66,10 +84,11 @@ const transformTypes = async ( }; const transformers = { - file: async ({ file, upload }, { config }) => { - if (file === null && !upload) { - return null; + file: async (input, { config }) => { + if (input === null) { + return { __op: 'Delete' }; } + const { file, upload } = input; if (upload) { const { fileInfo } = await handleUpload(upload, config); return { ...fileInfo, __type: 'File' }; From 4fc4e59b05167f1953cae363734fafedb549fca7 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Tue, 19 Oct 2021 15:59:18 +0200 Subject: [PATCH 2/2] docs: add changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c876810e6b..50dfa2ffa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,7 +101,7 @@ ___ # [Unreleased (master branch)](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.1...master) ## Breaking Changes - - (none) + - feat: `null` value on field during graphql mutation now unset the value from the database, file unset changed ## Features - (none) ## Bug Fixes