Allow to unset file from graphql (#6651)

This commit is contained in:
Antoine Cormouls
2020-04-30 07:18:25 +02:00
committed by GitHub
parent d279198db7
commit 4d1bd9346f
3 changed files with 85 additions and 50 deletions

View File

@@ -9522,6 +9522,29 @@ describe('ParseGraphQLServer', () => {
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(await res.text()).toEqual('My File Content'); 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) { } catch (e) {
handleError(e); handleError(e);
} }

View File

@@ -23,7 +23,7 @@ class TypeValidationError extends Error {
} }
} }
const parseStringValue = value => { const parseStringValue = (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return value; return value;
} }
@@ -31,7 +31,7 @@ const parseStringValue = value => {
throw new TypeValidationError(value, 'String'); throw new TypeValidationError(value, 'String');
}; };
const parseIntValue = value => { const parseIntValue = (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
const int = Number(value); const int = Number(value);
if (Number.isInteger(int)) { if (Number.isInteger(int)) {
@@ -42,7 +42,7 @@ const parseIntValue = value => {
throw new TypeValidationError(value, 'Int'); throw new TypeValidationError(value, 'Int');
}; };
const parseFloatValue = value => { const parseFloatValue = (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
const float = Number(value); const float = Number(value);
if (!isNaN(float)) { if (!isNaN(float)) {
@@ -53,7 +53,7 @@ const parseFloatValue = value => {
throw new TypeValidationError(value, 'Float'); throw new TypeValidationError(value, 'Float');
}; };
const parseBooleanValue = value => { const parseBooleanValue = (value) => {
if (typeof value === 'boolean') { if (typeof value === 'boolean') {
return value; return value;
} }
@@ -61,7 +61,7 @@ const parseBooleanValue = value => {
throw new TypeValidationError(value, 'Boolean'); throw new TypeValidationError(value, 'Boolean');
}; };
const parseValue = value => { const parseValue = (value) => {
switch (value.kind) { switch (value.kind) {
case Kind.STRING: case Kind.STRING:
return parseStringValue(value.value); return parseStringValue(value.value);
@@ -86,15 +86,15 @@ const parseValue = value => {
} }
}; };
const parseListValues = values => { const parseListValues = (values) => {
if (Array.isArray(values)) { if (Array.isArray(values)) {
return values.map(value => parseValue(value)); return values.map((value) => parseValue(value));
} }
throw new TypeValidationError(values, 'List'); throw new TypeValidationError(values, 'List');
}; };
const parseObjectFields = fields => { const parseObjectFields = (fields) => {
if (Array.isArray(fields)) { if (Array.isArray(fields)) {
return fields.reduce( return fields.reduce(
(object, field) => ({ (object, field) => ({
@@ -112,9 +112,9 @@ const ANY = new GraphQLScalarType({
name: 'Any', name: 'Any',
description: description:
'The Any scalar type is used in operations and types that involve any type of value.', 'The Any scalar type is used in operations and types that involve any type of value.',
parseValue: value => value, parseValue: (value) => value,
serialize: value => value, serialize: (value) => value,
parseLiteral: ast => parseValue(ast), parseLiteral: (ast) => parseValue(ast),
}); });
const OBJECT = new GraphQLScalarType({ const OBJECT = new GraphQLScalarType({
@@ -144,7 +144,7 @@ const OBJECT = new GraphQLScalarType({
}, },
}); });
const parseDateIsoValue = value => { const parseDateIsoValue = (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
const date = new Date(value); const date = new Date(value);
if (!isNaN(date)) { if (!isNaN(date)) {
@@ -157,7 +157,7 @@ const parseDateIsoValue = value => {
throw new TypeValidationError(value, 'Date'); throw new TypeValidationError(value, 'Date');
}; };
const serializeDateIso = value => { const serializeDateIso = (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return value; return value;
} }
@@ -168,7 +168,7 @@ const serializeDateIso = value => {
throw new TypeValidationError(value, 'Date'); throw new TypeValidationError(value, 'Date');
}; };
const parseDateIsoLiteral = ast => { const parseDateIsoLiteral = (ast) => {
if (ast.kind === Kind.STRING) { if (ast.kind === Kind.STRING) {
return parseDateIsoValue(ast.value); return parseDateIsoValue(ast.value);
} }
@@ -219,8 +219,8 @@ const DATE = new GraphQLScalarType({
iso: parseDateIsoLiteral(ast), iso: parseDateIsoLiteral(ast),
}; };
} else if (ast.kind === Kind.OBJECT) { } else if (ast.kind === Kind.OBJECT) {
const __type = ast.fields.find(field => field.name.value === '__type'); const __type = ast.fields.find((field) => field.name.value === '__type');
const iso = ast.fields.find(field => field.name.value === 'iso'); const iso = ast.fields.find((field) => field.name.value === 'iso');
if (__type && __type.value && __type.value.value === 'Date' && iso) { if (__type && __type.value && __type.value.value === 'Date' && iso) {
return { return {
__type: __type.value.value, __type: __type.value.value,
@@ -273,8 +273,8 @@ const BYTES = new GraphQLScalarType({
base64: ast.value, base64: ast.value,
}; };
} else if (ast.kind === Kind.OBJECT) { } else if (ast.kind === Kind.OBJECT) {
const __type = ast.fields.find(field => field.name.value === '__type'); const __type = ast.fields.find((field) => field.name.value === '__type');
const base64 = ast.fields.find(field => field.name.value === 'base64'); const base64 = ast.fields.find((field) => field.name.value === 'base64');
if ( if (
__type && __type &&
__type.value && __type.value &&
@@ -294,7 +294,7 @@ const BYTES = new GraphQLScalarType({
}, },
}); });
const parseFileValue = value => { const parseFileValue = (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return { return {
__type: 'File', __type: 'File',
@@ -317,7 +317,7 @@ const FILE = new GraphQLScalarType({
description: description:
'The File scalar type is used in operations and types that involve files.', 'The File scalar type is used in operations and types that involve files.',
parseValue: parseFileValue, parseValue: parseFileValue,
serialize: value => { serialize: (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return value; return value;
} else if ( } else if (
@@ -335,9 +335,9 @@ const FILE = new GraphQLScalarType({
if (ast.kind === Kind.STRING) { if (ast.kind === Kind.STRING) {
return parseFileValue(ast.value); return parseFileValue(ast.value);
} else if (ast.kind === Kind.OBJECT) { } else if (ast.kind === Kind.OBJECT) {
const __type = ast.fields.find(field => field.name.value === '__type'); const __type = ast.fields.find((field) => field.name.value === '__type');
const name = ast.fields.find(field => field.name.value === 'name'); const name = ast.fields.find((field) => field.name.value === 'name');
const url = ast.fields.find(field => field.name.value === 'url'); const url = ast.fields.find((field) => field.name.value === 'url');
if (__type && __type.value && name && name.value) { if (__type && __type.value && name && name.value) {
return parseFileValue({ return parseFileValue({
__type: __type.value.value, __type: __type.value.value,
@@ -371,13 +371,19 @@ const FILE_INPUT = new GraphQLInputObjectType({
name: 'FileInput', name: 'FileInput',
fields: { fields: {
file: { 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, type: FILE,
}, },
upload: { upload: {
description: 'Use this field if you want to create a new file.', description: 'Use this field if you want to create a new file.',
type: GraphQLUpload, 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)), type: new GraphQLList(new GraphQLNonNull(USER_ACL)),
resolve(p) { resolve(p) {
const users = []; const users = [];
Object.keys(p).forEach(rule => { Object.keys(p).forEach((rule) => {
if (rule !== '*' && rule.indexOf('role:') !== 0) { if (rule !== '*' && rule.indexOf('role:') !== 0) {
users.push({ users.push({
userId: toGlobalId('_User', rule), userId: toGlobalId('_User', rule),
@@ -568,7 +574,7 @@ const ACL = new GraphQLObjectType({
type: new GraphQLList(new GraphQLNonNull(ROLE_ACL)), type: new GraphQLList(new GraphQLNonNull(ROLE_ACL)),
resolve(p) { resolve(p) {
const roles = []; const roles = [];
Object.keys(p).forEach(rule => { Object.keys(p).forEach((rule) => {
if (rule.indexOf('role:') === 0) { if (rule.indexOf('role:') === 0) {
roles.push({ roles.push({
roleName: rule.replace('role:', ''), roleName: rule.replace('role:', ''),
@@ -839,49 +845,49 @@ const GEO_INTERSECTS_INPUT = new GraphQLInputObjectType({
}, },
}); });
const equalTo = type => ({ const equalTo = (type) => ({
description: 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.', '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, type,
}); });
const notEqualTo = type => ({ const notEqualTo = (type) => ({
description: 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.', '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, type,
}); });
const lessThan = type => ({ const lessThan = (type) => ({
description: 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.', '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, type,
}); });
const lessThanOrEqualTo = type => ({ const lessThanOrEqualTo = (type) => ({
description: 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.', '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, type,
}); });
const greaterThan = type => ({ const greaterThan = (type) => ({
description: 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.', '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, type,
}); });
const greaterThanOrEqualTo = type => ({ const greaterThanOrEqualTo = (type) => ({
description: 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.', '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, type,
}); });
const inOp = type => ({ const inOp = (type) => ({
description: 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.', '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), type: new GraphQLList(type),
}); });
const notIn = type => ({ const notIn = (type) => ({
description: 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.', '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), type: new GraphQLList(type),
@@ -1219,14 +1225,14 @@ let ARRAY_RESULT;
const loadArrayResult = (parseGraphQLSchema, parseClasses) => { const loadArrayResult = (parseGraphQLSchema, parseClasses) => {
const classTypes = parseClasses const classTypes = parseClasses
.filter(parseClass => .filter((parseClass) =>
parseGraphQLSchema.parseClassTypes[parseClass.className] parseGraphQLSchema.parseClassTypes[parseClass.className]
.classGraphQLOutputType .classGraphQLOutputType
? true ? true
: false : false
) )
.map( .map(
parseClass => (parseClass) =>
parseGraphQLSchema.parseClassTypes[parseClass.className] parseGraphQLSchema.parseClassTypes[parseClass.className]
.classGraphQLOutputType .classGraphQLOutputType
); );
@@ -1235,7 +1241,7 @@ const loadArrayResult = (parseGraphQLSchema, parseClasses) => {
description: description:
'Use Inline Fragment on Array to get results: https://graphql.org/learn/queries/#inline-fragments', 'Use Inline Fragment on Array to get results: https://graphql.org/learn/queries/#inline-fragments',
types: () => [ELEMENT, ...classTypes], types: () => [ELEMENT, ...classTypes],
resolveType: value => { resolveType: (value) => {
if (value.__type === 'Object' && value.className && value.objectId) { if (value.__type === 'Object' && value.className && value.objectId) {
if (parseGraphQLSchema.parseClassTypes[value.className]) { if (parseGraphQLSchema.parseClassTypes[value.className]) {
return parseGraphQLSchema.parseClassTypes[value.className] return parseGraphQLSchema.parseClassTypes[value.className]
@@ -1251,7 +1257,7 @@ const loadArrayResult = (parseGraphQLSchema, parseClasses) => {
parseGraphQLSchema.graphQLTypes.push(ARRAY_RESULT); parseGraphQLSchema.graphQLTypes.push(ARRAY_RESULT);
}; };
const load = parseGraphQLSchema => { const load = (parseGraphQLSchema) => {
parseGraphQLSchema.addGraphQLType(GraphQLUpload, true); parseGraphQLSchema.addGraphQLType(GraphQLUpload, true);
parseGraphQLSchema.addGraphQLType(ANY, true); parseGraphQLSchema.addGraphQLType(ANY, true);
parseGraphQLSchema.addGraphQLType(OBJECT, true); parseGraphQLSchema.addGraphQLType(OBJECT, true);

View File

@@ -15,7 +15,7 @@ const transformTypes = async (
config: { isCreateEnabled, isUpdateEnabled }, config: { isCreateEnabled, isUpdateEnabled },
} = parseGraphQLSchema.parseClassTypes[className]; } = parseGraphQLSchema.parseClassTypes[className];
const parseClass = parseGraphQLSchema.parseClasses.find( const parseClass = parseGraphQLSchema.parseClasses.find(
clazz => clazz.className === className (clazz) => clazz.className === className
); );
if (fields) { if (fields) {
const classGraphQLCreateTypeFields = const classGraphQLCreateTypeFields =
@@ -26,7 +26,7 @@ const transformTypes = async (
isUpdateEnabled && classGraphQLUpdateType isUpdateEnabled && classGraphQLUpdateType
? classGraphQLUpdateType.getFields() ? classGraphQLUpdateType.getFields()
: null; : null;
const promises = Object.keys(fields).map(async field => { const promises = Object.keys(fields).map(async (field) => {
let inputTypeField; let inputTypeField;
if (inputType === 'create' && classGraphQLCreateTypeFields) { if (inputType === 'create' && classGraphQLCreateTypeFields) {
inputTypeField = classGraphQLCreateTypeFields[field]; inputTypeField = classGraphQLCreateTypeFields[field];
@@ -73,6 +73,9 @@ const transformTypes = async (
const transformers = { const transformers = {
file: async ({ file, upload }, { config }) => { file: async ({ file, upload }, { config }) => {
if (file === null && !upload) {
return null;
}
if (upload) { if (upload) {
const { fileInfo } = await handleUpload(upload, config); const { fileInfo } = await handleUpload(upload, config);
return { ...fileInfo, __type: 'File' }; return { ...fileInfo, __type: 'File' };
@@ -81,15 +84,18 @@ const transformers = {
} }
throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.'); throw new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.');
}, },
polygon: value => ({ polygon: (value) => ({
__type: 'Polygon', __type: 'Polygon',
coordinates: value.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]), coordinates: value.map((geoPoint) => [
geoPoint.latitude,
geoPoint.longitude,
]),
}), }),
geoPoint: value => ({ geoPoint: (value) => ({
...value, ...value,
__type: 'GeoPoint', __type: 'GeoPoint',
}), }),
ACL: value => { ACL: (value) => {
const parseACL = {}; const parseACL = {};
if (value.public) { if (value.public) {
parseACL['*'] = { parseACL['*'] = {
@@ -98,7 +104,7 @@ const transformers = {
}; };
} }
if (value.users) { if (value.users) {
value.users.forEach(rule => { value.users.forEach((rule) => {
const globalIdObject = fromGlobalId(rule.userId); const globalIdObject = fromGlobalId(rule.userId);
if (globalIdObject.type === '_User') { if (globalIdObject.type === '_User') {
rule.userId = globalIdObject.id; rule.userId = globalIdObject.id;
@@ -110,7 +116,7 @@ const transformers = {
}); });
} }
if (value.roles) { if (value.roles) {
value.roles.forEach(rule => { value.roles.forEach((rule) => {
parseACL[`role:${rule.roleName}`] = { parseACL[`role:${rule.roleName}`] = {
read: rule.read, read: rule.read,
write: rule.write, write: rule.write,
@@ -141,7 +147,7 @@ const transformers = {
if (value.createAndAdd) { if (value.createAndAdd) {
nestedObjectsToAdd = ( nestedObjectsToAdd = (
await Promise.all( await Promise.all(
value.createAndAdd.map(async input => { value.createAndAdd.map(async (input) => {
const parseFields = await transformTypes('create', input, { const parseFields = await transformTypes('create', input, {
className: targetClass, className: targetClass,
parseGraphQLSchema, parseGraphQLSchema,
@@ -156,7 +162,7 @@ const transformers = {
); );
}) })
) )
).map(object => ({ ).map((object) => ({
__type: 'Pointer', __type: 'Pointer',
className: targetClass, className: targetClass,
objectId: object.objectId, objectId: object.objectId,
@@ -165,7 +171,7 @@ const transformers = {
if (value.add || nestedObjectsToAdd.length > 0) { if (value.add || nestedObjectsToAdd.length > 0) {
if (!value.add) value.add = []; if (!value.add) value.add = [];
value.add = value.add.map(input => { value.add = value.add.map((input) => {
const globalIdObject = fromGlobalId(input); const globalIdObject = fromGlobalId(input);
if (globalIdObject.type === targetClass) { if (globalIdObject.type === targetClass) {
input = globalIdObject.id; input = globalIdObject.id;
@@ -185,7 +191,7 @@ const transformers = {
if (value.remove) { if (value.remove) {
op.ops.push({ op.ops.push({
__op: 'RemoveRelation', __op: 'RemoveRelation',
objects: value.remove.map(input => { objects: value.remove.map((input) => {
const globalIdObject = fromGlobalId(input); const globalIdObject = fromGlobalId(input);
if (globalIdObject.type === targetClass) { if (globalIdObject.type === targetClass) {
input = globalIdObject.id; input = globalIdObject.id;