diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 4534ffe7..46e50cde 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -9522,6 +9522,29 @@ describe('ParseGraphQLServer', () => { expect(res.status).toEqual(200); expect(await res.text()).toEqual('My File Content'); + + const mutationResult = await apolloClient.mutate({ + mutation: gql` + mutation UnlinkFile($id: ID!) { + updateSomeClass( + input: { id: $id, fields: { someField: { file: null } } } + ) { + someClass { + someField { + name + url + } + } + } + } + `, + variables: { + id: result2.data.createSomeClass3.someClass.id, + }, + }); + expect( + mutationResult.data.updateSomeClass.someClass.someField + ).toEqual(null); } catch (e) { handleError(e); } diff --git a/src/GraphQL/loaders/defaultGraphQLTypes.js b/src/GraphQL/loaders/defaultGraphQLTypes.js index 8a59089e..e172059f 100644 --- a/src/GraphQL/loaders/defaultGraphQLTypes.js +++ b/src/GraphQL/loaders/defaultGraphQLTypes.js @@ -23,7 +23,7 @@ class TypeValidationError extends Error { } } -const parseStringValue = value => { +const parseStringValue = (value) => { if (typeof value === 'string') { return value; } @@ -31,7 +31,7 @@ const parseStringValue = value => { throw new TypeValidationError(value, 'String'); }; -const parseIntValue = value => { +const parseIntValue = (value) => { if (typeof value === 'string') { const int = Number(value); if (Number.isInteger(int)) { @@ -42,7 +42,7 @@ const parseIntValue = value => { throw new TypeValidationError(value, 'Int'); }; -const parseFloatValue = value => { +const parseFloatValue = (value) => { if (typeof value === 'string') { const float = Number(value); if (!isNaN(float)) { @@ -53,7 +53,7 @@ const parseFloatValue = value => { throw new TypeValidationError(value, 'Float'); }; -const parseBooleanValue = value => { +const parseBooleanValue = (value) => { if (typeof value === 'boolean') { return value; } @@ -61,7 +61,7 @@ const parseBooleanValue = value => { throw new TypeValidationError(value, 'Boolean'); }; -const parseValue = value => { +const parseValue = (value) => { switch (value.kind) { case Kind.STRING: return parseStringValue(value.value); @@ -86,15 +86,15 @@ const parseValue = value => { } }; -const parseListValues = values => { +const parseListValues = (values) => { if (Array.isArray(values)) { - return values.map(value => parseValue(value)); + return values.map((value) => parseValue(value)); } throw new TypeValidationError(values, 'List'); }; -const parseObjectFields = fields => { +const parseObjectFields = (fields) => { if (Array.isArray(fields)) { return fields.reduce( (object, field) => ({ @@ -112,9 +112,9 @@ const ANY = new GraphQLScalarType({ name: 'Any', description: 'The Any scalar type is used in operations and types that involve any type of value.', - parseValue: value => value, - serialize: value => value, - parseLiteral: ast => parseValue(ast), + parseValue: (value) => value, + serialize: (value) => value, + parseLiteral: (ast) => parseValue(ast), }); const OBJECT = new GraphQLScalarType({ @@ -144,7 +144,7 @@ const OBJECT = new GraphQLScalarType({ }, }); -const parseDateIsoValue = value => { +const parseDateIsoValue = (value) => { if (typeof value === 'string') { const date = new Date(value); if (!isNaN(date)) { @@ -157,7 +157,7 @@ const parseDateIsoValue = value => { throw new TypeValidationError(value, 'Date'); }; -const serializeDateIso = value => { +const serializeDateIso = (value) => { if (typeof value === 'string') { return value; } @@ -168,7 +168,7 @@ const serializeDateIso = value => { throw new TypeValidationError(value, 'Date'); }; -const parseDateIsoLiteral = ast => { +const parseDateIsoLiteral = (ast) => { if (ast.kind === Kind.STRING) { return parseDateIsoValue(ast.value); } @@ -219,8 +219,8 @@ const DATE = new GraphQLScalarType({ iso: parseDateIsoLiteral(ast), }; } else if (ast.kind === Kind.OBJECT) { - const __type = ast.fields.find(field => field.name.value === '__type'); - const iso = ast.fields.find(field => field.name.value === 'iso'); + const __type = ast.fields.find((field) => field.name.value === '__type'); + const iso = ast.fields.find((field) => field.name.value === 'iso'); if (__type && __type.value && __type.value.value === 'Date' && iso) { return { __type: __type.value.value, @@ -273,8 +273,8 @@ const BYTES = new GraphQLScalarType({ base64: ast.value, }; } else if (ast.kind === Kind.OBJECT) { - const __type = ast.fields.find(field => field.name.value === '__type'); - const base64 = ast.fields.find(field => field.name.value === 'base64'); + const __type = ast.fields.find((field) => field.name.value === '__type'); + const base64 = ast.fields.find((field) => field.name.value === 'base64'); if ( __type && __type.value && @@ -294,7 +294,7 @@ const BYTES = new GraphQLScalarType({ }, }); -const parseFileValue = value => { +const parseFileValue = (value) => { if (typeof value === 'string') { return { __type: 'File', @@ -317,7 +317,7 @@ const FILE = new GraphQLScalarType({ description: 'The File scalar type is used in operations and types that involve files.', parseValue: parseFileValue, - serialize: value => { + serialize: (value) => { if (typeof value === 'string') { return value; } else if ( @@ -335,9 +335,9 @@ const FILE = new GraphQLScalarType({ if (ast.kind === Kind.STRING) { return parseFileValue(ast.value); } else if (ast.kind === Kind.OBJECT) { - const __type = ast.fields.find(field => field.name.value === '__type'); - const name = ast.fields.find(field => field.name.value === 'name'); - const url = ast.fields.find(field => field.name.value === 'url'); + const __type = ast.fields.find((field) => field.name.value === '__type'); + const name = ast.fields.find((field) => field.name.value === 'name'); + const url = ast.fields.find((field) => field.name.value === 'url'); if (__type && __type.value && name && name.value) { return parseFileValue({ __type: __type.value.value, @@ -371,13 +371,19 @@ const FILE_INPUT = new GraphQLInputObjectType({ name: 'FileInput', fields: { file: { - description: 'A File Scalar can be an url or a FileInfo object.', + description: + 'A File Scalar can be an url or a FileInfo object. If this field is set to null the file will be unlinked.', 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, + }, }, }); @@ -551,7 +557,7 @@ const ACL = new GraphQLObjectType({ type: new GraphQLList(new GraphQLNonNull(USER_ACL)), resolve(p) { const users = []; - Object.keys(p).forEach(rule => { + Object.keys(p).forEach((rule) => { if (rule !== '*' && rule.indexOf('role:') !== 0) { users.push({ userId: toGlobalId('_User', rule), @@ -568,7 +574,7 @@ const ACL = new GraphQLObjectType({ type: new GraphQLList(new GraphQLNonNull(ROLE_ACL)), resolve(p) { const roles = []; - Object.keys(p).forEach(rule => { + Object.keys(p).forEach((rule) => { if (rule.indexOf('role:') === 0) { roles.push({ roleName: rule.replace('role:', ''), @@ -839,49 +845,49 @@ const GEO_INTERSECTS_INPUT = new GraphQLInputObjectType({ }, }); -const equalTo = type => ({ +const equalTo = (type) => ({ description: 'This is the equalTo operator to specify a constraint to select the objects where the value of a field equals to a specified value.', type, }); -const notEqualTo = type => ({ +const notEqualTo = (type) => ({ description: 'This is the notEqualTo operator to specify a constraint to select the objects where the value of a field do not equal to a specified value.', type, }); -const lessThan = type => ({ +const lessThan = (type) => ({ description: 'This is the lessThan operator to specify a constraint to select the objects where the value of a field is less than a specified value.', type, }); -const lessThanOrEqualTo = type => ({ +const lessThanOrEqualTo = (type) => ({ description: 'This is the lessThanOrEqualTo operator to specify a constraint to select the objects where the value of a field is less than or equal to a specified value.', type, }); -const greaterThan = type => ({ +const greaterThan = (type) => ({ description: 'This is the greaterThan operator to specify a constraint to select the objects where the value of a field is greater than a specified value.', type, }); -const greaterThanOrEqualTo = type => ({ +const greaterThanOrEqualTo = (type) => ({ description: 'This is the greaterThanOrEqualTo operator to specify a constraint to select the objects where the value of a field is greater than or equal to a specified value.', type, }); -const inOp = type => ({ +const inOp = (type) => ({ description: 'This is the in operator to specify a constraint to select the objects where the value of a field equals any value in the specified array.', type: new GraphQLList(type), }); -const notIn = type => ({ +const notIn = (type) => ({ description: 'This is the notIn operator to specify a constraint to select the objects where the value of a field do not equal any value in the specified array.', type: new GraphQLList(type), @@ -1219,14 +1225,14 @@ let ARRAY_RESULT; const loadArrayResult = (parseGraphQLSchema, parseClasses) => { const classTypes = parseClasses - .filter(parseClass => + .filter((parseClass) => parseGraphQLSchema.parseClassTypes[parseClass.className] .classGraphQLOutputType ? true : false ) .map( - parseClass => + (parseClass) => parseGraphQLSchema.parseClassTypes[parseClass.className] .classGraphQLOutputType ); @@ -1235,7 +1241,7 @@ const loadArrayResult = (parseGraphQLSchema, parseClasses) => { description: 'Use Inline Fragment on Array to get results: https://graphql.org/learn/queries/#inline-fragments', types: () => [ELEMENT, ...classTypes], - resolveType: value => { + resolveType: (value) => { if (value.__type === 'Object' && value.className && value.objectId) { if (parseGraphQLSchema.parseClassTypes[value.className]) { return parseGraphQLSchema.parseClassTypes[value.className] @@ -1251,7 +1257,7 @@ const loadArrayResult = (parseGraphQLSchema, parseClasses) => { parseGraphQLSchema.graphQLTypes.push(ARRAY_RESULT); }; -const load = parseGraphQLSchema => { +const load = (parseGraphQLSchema) => { parseGraphQLSchema.addGraphQLType(GraphQLUpload, true); parseGraphQLSchema.addGraphQLType(ANY, true); parseGraphQLSchema.addGraphQLType(OBJECT, true); diff --git a/src/GraphQL/transformers/mutation.js b/src/GraphQL/transformers/mutation.js index 7ce48ffa..06cc7dee 100644 --- a/src/GraphQL/transformers/mutation.js +++ b/src/GraphQL/transformers/mutation.js @@ -15,7 +15,7 @@ const transformTypes = async ( config: { isCreateEnabled, isUpdateEnabled }, } = parseGraphQLSchema.parseClassTypes[className]; const parseClass = parseGraphQLSchema.parseClasses.find( - clazz => clazz.className === className + (clazz) => clazz.className === className ); if (fields) { const classGraphQLCreateTypeFields = @@ -26,7 +26,7 @@ const transformTypes = async ( isUpdateEnabled && classGraphQLUpdateType ? classGraphQLUpdateType.getFields() : null; - const promises = Object.keys(fields).map(async field => { + const promises = Object.keys(fields).map(async (field) => { let inputTypeField; if (inputType === 'create' && classGraphQLCreateTypeFields) { inputTypeField = classGraphQLCreateTypeFields[field]; @@ -73,6 +73,9 @@ const transformTypes = async ( const transformers = { file: async ({ file, upload }, { config }) => { + if (file === null && !upload) { + return null; + } if (upload) { const { fileInfo } = await handleUpload(upload, config); return { ...fileInfo, __type: 'File' }; @@ -81,15 +84,18 @@ const transformers = { } throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.'); }, - polygon: value => ({ + polygon: (value) => ({ __type: 'Polygon', - coordinates: value.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]), + coordinates: value.map((geoPoint) => [ + geoPoint.latitude, + geoPoint.longitude, + ]), }), - geoPoint: value => ({ + geoPoint: (value) => ({ ...value, __type: 'GeoPoint', }), - ACL: value => { + ACL: (value) => { const parseACL = {}; if (value.public) { parseACL['*'] = { @@ -98,7 +104,7 @@ const transformers = { }; } if (value.users) { - value.users.forEach(rule => { + value.users.forEach((rule) => { const globalIdObject = fromGlobalId(rule.userId); if (globalIdObject.type === '_User') { rule.userId = globalIdObject.id; @@ -110,7 +116,7 @@ const transformers = { }); } if (value.roles) { - value.roles.forEach(rule => { + value.roles.forEach((rule) => { parseACL[`role:${rule.roleName}`] = { read: rule.read, write: rule.write, @@ -141,7 +147,7 @@ const transformers = { if (value.createAndAdd) { nestedObjectsToAdd = ( await Promise.all( - value.createAndAdd.map(async input => { + value.createAndAdd.map(async (input) => { const parseFields = await transformTypes('create', input, { className: targetClass, parseGraphQLSchema, @@ -156,7 +162,7 @@ const transformers = { ); }) ) - ).map(object => ({ + ).map((object) => ({ __type: 'Pointer', className: targetClass, objectId: object.objectId, @@ -165,7 +171,7 @@ const transformers = { if (value.add || nestedObjectsToAdd.length > 0) { if (!value.add) value.add = []; - value.add = value.add.map(input => { + value.add = value.add.map((input) => { const globalIdObject = fromGlobalId(input); if (globalIdObject.type === targetClass) { input = globalIdObject.id; @@ -185,7 +191,7 @@ const transformers = { if (value.remove) { op.ops.push({ __op: 'RemoveRelation', - objects: value.remove.map(input => { + objects: value.remove.map((input) => { const globalIdObject = fromGlobalId(input); if (globalIdObject.type === targetClass) { input = globalIdObject.id;