Relay Spec (#6089)

* Install graphql-relay

* Add relayNodeInterface to ParseGraphQLSchema

* Add support to global id

* Add support to global id in other operations

* Fix sort by glboal id

* Fix where by global id

* Introduce IdWhereInput

* Add Relay object identification tests

* Client mutation id on createFile mutation

* Client mutation id on callCloudCode mutation

* Client mutation id on signUp mutation

* Client mutation id on logIn mutation

* Client mutation id on logOut mutation

* Client mutation id on createClass mutation

* Client mutation id on updateClass mutation

* Client mutation id on deleteClass mutation

* Client mutation id on create object mutation

* Improve Viewer type

* Client mutation id on update object mutation

* Client mutation id on delete object mutation

* Introducing connections

* Fix tests

* Add pagination test

* Fix file location

* Fix postgres tests

* Add comments

* Tests to calculateSkipAndLimit
This commit is contained in:
Antonio Davi Macedo Coelho de Castro
2019-12-01 21:43:08 -08:00
committed by GitHub
parent 67e3c33ffe
commit a9066e20dc
22 changed files with 4685 additions and 2816 deletions

1817
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,7 @@
"follow-redirects": "1.9.0",
"graphql": "14.5.8",
"graphql-list-fields": "2.0.2",
"graphql-relay": "^0.6.0",
"graphql-tools": "^4.0.5",
"graphql-upload": "8.1.0",
"intersect": "1.0.1",

View File

@@ -54,8 +54,14 @@ describe('AuthenticationProviders', function() {
Promise.prototype.constructor
);
jequal(validateAppIdPromise.constructor, Promise.prototype.constructor);
validateAuthDataPromise.then(() => {}, () => {});
validateAppIdPromise.then(() => {}, () => {});
validateAuthDataPromise.then(
() => {},
() => {}
);
validateAppIdPromise.then(
() => {},
() => {}
);
done();
});

View File

@@ -97,7 +97,11 @@ describe('parseObjectToMongoObjectForCreate', () => {
const lng3 = 65;
const polygon = {
__type: 'Polygon',
coordinates: [[lat1, lng1], [lat2, lng2], [lat3, lng3]],
coordinates: [
[lat1, lng1],
[lat2, lng2],
[lat3, lng3],
],
};
const out = transform.parseObjectToMongoObjectForCreate(
null,
@@ -107,7 +111,12 @@ describe('parseObjectToMongoObjectForCreate', () => {
}
);
expect(out.location.coordinates).toEqual([
[[lng1, lat1], [lng2, lat2], [lng3, lat3], [lng1, lat1]],
[
[lng1, lat1],
[lng2, lat2],
[lng3, lat3],
[lng1, lat1],
],
]);
done();
});
@@ -217,7 +226,15 @@ describe('parseObjectToMongoObjectForCreate', () => {
const lng = 45;
// Mongo stores polygon in WGS84 lng/lat
const input = {
location: { type: 'Polygon', coordinates: [[[lat, lng], [lat, lng]]] },
location: {
type: 'Polygon',
coordinates: [
[
[lat, lng],
[lat, lng],
],
],
},
};
const output = transform.mongoObjectToParseObject(null, input, {
fields: { location: { type: 'Polygon' } },
@@ -225,7 +242,10 @@ describe('parseObjectToMongoObjectForCreate', () => {
expect(typeof output.location).toEqual('object');
expect(output.location).toEqual({
__type: 'Polygon',
coordinates: [[lng, lat], [lng, lat]],
coordinates: [
[lng, lat],
[lng, lat],
],
});
done();
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,158 @@
const { offsetToCursor } = require('graphql-relay');
const {
calculateSkipAndLimit,
} = require('../lib/GraphQL/helpers/objectsQueries');
describe('GraphQL objectsQueries', () => {
describe('calculateSkipAndLimit', () => {
it('should fail with invalid params', () => {
expect(() => calculateSkipAndLimit(-1)).toThrow(
jasmine.stringMatching('Skip should be a positive number')
);
expect(() => calculateSkipAndLimit(1, -1)).toThrow(
jasmine.stringMatching('First should be a positive number')
);
expect(() => calculateSkipAndLimit(1, 1, offsetToCursor(-1))).toThrow(
jasmine.stringMatching('After is not a valid curso')
);
expect(() => calculateSkipAndLimit(1, 1, offsetToCursor(1), -1)).toThrow(
jasmine.stringMatching('Last should be a positive number')
);
expect(() =>
calculateSkipAndLimit(1, 1, offsetToCursor(1), 1, offsetToCursor(-1))
).toThrow(jasmine.stringMatching('Before is not a valid curso'));
});
it('should work only with skip', () => {
expect(calculateSkipAndLimit(10)).toEqual({
skip: 10,
limit: undefined,
needToPreCount: false,
});
});
it('should work only with after', () => {
expect(
calculateSkipAndLimit(undefined, undefined, offsetToCursor(9))
).toEqual({
skip: 10,
limit: undefined,
needToPreCount: false,
});
});
it('should work with limit and after', () => {
expect(calculateSkipAndLimit(10, undefined, offsetToCursor(9))).toEqual({
skip: 20,
limit: undefined,
needToPreCount: false,
});
});
it('first alone should set the limit', () => {
expect(calculateSkipAndLimit(10, 30, offsetToCursor(9))).toEqual({
skip: 20,
limit: 30,
needToPreCount: false,
});
});
it('if before cursor is less than skipped items, no objects will be returned', () => {
expect(
calculateSkipAndLimit(
10,
30,
offsetToCursor(9),
undefined,
offsetToCursor(5)
)
).toEqual({
skip: 20,
limit: 0,
needToPreCount: false,
});
});
it('if before cursor is greater than returned objects set by limit, nothing is changed', () => {
expect(
calculateSkipAndLimit(
10,
30,
offsetToCursor(9),
undefined,
offsetToCursor(100)
)
).toEqual({
skip: 20,
limit: 30,
needToPreCount: false,
});
});
it('if before cursor is less than returned objects set by limit, limit is adjusted', () => {
expect(
calculateSkipAndLimit(
10,
30,
offsetToCursor(9),
undefined,
offsetToCursor(40)
)
).toEqual({
skip: 20,
limit: 20,
needToPreCount: false,
});
});
it('last should work alone but requires pre count', () => {
expect(
calculateSkipAndLimit(undefined, undefined, undefined, 10)
).toEqual({
skip: undefined,
limit: 10,
needToPreCount: true,
});
});
it('last should be adjusted to max limit', () => {
expect(
calculateSkipAndLimit(undefined, undefined, undefined, 10, undefined, 5)
).toEqual({
skip: undefined,
limit: 5,
needToPreCount: true,
});
});
it('no objects will be returned if last is equal to 0', () => {
expect(calculateSkipAndLimit(undefined, undefined, undefined, 0)).toEqual(
{
skip: undefined,
limit: 0,
needToPreCount: false,
}
);
});
it('nothing changes if last is bigger than the calculared limit', () => {
expect(
calculateSkipAndLimit(10, 30, offsetToCursor(9), 30, offsetToCursor(40))
).toEqual({
skip: 20,
limit: 20,
needToPreCount: false,
});
});
it('If last is small than limit, new limit is calculated', () => {
expect(
calculateSkipAndLimit(10, 30, offsetToCursor(9), 10, offsetToCursor(40))
).toEqual({
skip: 30,
limit: 10,
needToPreCount: false,
});
});
});
});

View File

@@ -1,5 +1,10 @@
import Parse from 'parse/node';
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
import {
GraphQLSchema,
GraphQLObjectType,
DocumentNode,
GraphQLNamedType,
} from 'graphql';
import { mergeSchemas, SchemaDirectiveVisitor } from 'graphql-tools';
import requiredParameter from '../requiredParameter';
import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes';
@@ -16,6 +21,7 @@ import { toGraphQLError } from './parseGraphQLUtils';
import * as schemaDirectives from './loaders/schemaDirectives';
import * as schemaTypes from './loaders/schemaTypes';
import { getFunctionNames } from '../triggers';
import * as defaultRelaySchema from './loaders/defaultRelaySchema';
const RESERVED_GRAPHQL_TYPE_NAMES = [
'String',
@@ -27,10 +33,25 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [
'Query',
'Mutation',
'Subscription',
'CreateFileInput',
'CreateFilePayload',
'Viewer',
'SignUpFieldsInput',
'LogInFieldsInput',
'SignUpInput',
'SignUpPayload',
'LogInInput',
'LogInPayload',
'LogOutInput',
'LogOutPayload',
'CloudCodeFunction',
'CallCloudCodeInput',
'CallCloudCodePayload',
'CreateClassInput',
'CreateClassPayload',
'UpdateClassInput',
'UpdateClassPayload',
'DeleteClassInput',
'DeleteClassPayload',
'PageInfo',
];
const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes'];
const RESERVED_GRAPHQL_MUTATION_NAMES = [
@@ -48,7 +69,14 @@ class ParseGraphQLSchema {
databaseController: DatabaseController;
parseGraphQLController: ParseGraphQLController;
parseGraphQLConfig: ParseGraphQLConfig;
graphQLCustomTypeDefs: any;
log: any;
appId: string;
graphQLCustomTypeDefs: ?(
| string
| GraphQLSchema
| DocumentNode
| GraphQLNamedType[]
);
constructor(
params: {
@@ -56,6 +84,12 @@ class ParseGraphQLSchema {
parseGraphQLController: ParseGraphQLController,
log: any,
appId: string,
graphQLCustomTypeDefs: ?(
| string
| GraphQLSchema
| DocumentNode
| GraphQLNamedType[]
),
} = {}
) {
this.parseGraphQLController =
@@ -105,8 +139,10 @@ class ParseGraphQLSchema {
this.graphQLSubscriptions = {};
this.graphQLSchemaDirectivesDefinitions = null;
this.graphQLSchemaDirectives = {};
this.relayNodeInterface = null;
defaultGraphQLTypes.load(this);
defaultRelaySchema.load(this);
schemaTypes.load(this);
this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach(
@@ -208,10 +244,16 @@ class ParseGraphQLSchema {
return this.graphQLSchema;
}
addGraphQLType(type, throwError = false, ignoreReserved = false) {
addGraphQLType(
type,
throwError = false,
ignoreReserved = false,
ignoreConnection = false
) {
if (
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) ||
this.graphQLTypes.find(existingType => existingType.name === type.name)
this.graphQLTypes.find(existingType => existingType.name === type.name) ||
(!ignoreConnection && type.name.endsWith('Connection'))
) {
const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`;
if (throwError) {

View File

@@ -1,4 +1,5 @@
import Parse from 'parse/node';
import { offsetToCursor, cursorToOffset } from 'graphql-relay';
import rest from '../../rest';
import { transformQueryInputToParse } from '../transformers/query';
@@ -51,8 +52,11 @@ const findObjects = async (
className,
where,
order,
skip,
limit,
skipInput,
first,
after,
last,
before,
keys,
include,
includeAll,
@@ -68,11 +72,52 @@ const findObjects = async (
if (!where) {
where = {};
}
transformQueryInputToParse(where, fields);
transformQueryInputToParse(where, fields, className);
const skipAndLimitCalculation = calculateSkipAndLimit(
skipInput,
first,
after,
last,
before,
config.maxLimit
);
let { skip } = skipAndLimitCalculation;
const { limit, needToPreCount } = skipAndLimitCalculation;
let preCount = undefined;
if (needToPreCount) {
const preCountOptions = {
limit: 0,
count: true,
};
if (readPreference) {
preCountOptions.readPreference = readPreference;
}
if (Object.keys(where).length > 0 && subqueryReadPreference) {
preCountOptions.subqueryReadPreference = subqueryReadPreference;
}
preCount = (
await rest.find(
config,
auth,
className,
where,
preCountOptions,
info.clientSDK
)
).count;
if ((skip || 0) + limit < preCount) {
skip = preCount - limit;
}
}
const options = {};
if (selectedFields.includes('results')) {
if (
selectedFields.find(
field => field.startsWith('edges.') || field.startsWith('pageInfo.')
)
) {
if (limit || limit === 0) {
options.limit = limit;
}
@@ -104,7 +149,12 @@ const findObjects = async (
options.limit = 0;
}
if (selectedFields.includes('count')) {
if (
(selectedFields.includes('count') ||
selectedFields.includes('pageInfo.hasPreviousPage') ||
selectedFields.includes('pageInfo.hasNextPage')) &&
!needToPreCount
) {
options.count = true;
}
@@ -115,7 +165,151 @@ const findObjects = async (
options.subqueryReadPreference = subqueryReadPreference;
}
return rest.find(config, auth, className, where, options, info.clientSDK);
let results, count;
if (options.count || !options.limit || (options.limit && options.limit > 0)) {
const findResult = await rest.find(
config,
auth,
className,
where,
options,
info.clientSDK
);
results = findResult.results;
count = findResult.count;
}
let edges = null;
let pageInfo = null;
if (results) {
edges = results.map((result, index) => ({
cursor: offsetToCursor((skip || 0) + index),
node: result,
}));
pageInfo = {
hasPreviousPage:
((preCount && preCount > 0) || (count && count > 0)) &&
skip !== undefined &&
skip > 0,
startCursor: offsetToCursor(skip || 0),
endCursor: offsetToCursor((skip || 0) + (results.length || 1) - 1),
hasNextPage: (preCount || count) > (skip || 0) + results.length,
};
}
return {
edges,
pageInfo,
count: preCount || count,
};
};
export { getObject, findObjects };
const calculateSkipAndLimit = (
skipInput,
first,
after,
last,
before,
maxLimit
) => {
let skip = undefined;
let limit = undefined;
let needToPreCount = false;
// Validates the skip input
if (skipInput || skipInput === 0) {
if (skipInput < 0) {
throw new Parse.Error(
Parse.Error.INVALID_QUERY,
'Skip should be a positive number'
);
}
skip = skipInput;
}
// Validates the after param
if (after) {
after = cursorToOffset(after);
if ((!after && after !== 0) || after < 0) {
throw new Parse.Error(
Parse.Error.INVALID_QUERY,
'After is not a valid cursor'
);
}
// If skip and after are passed, a new skip is calculated by adding them
skip = (skip || 0) + (after + 1);
}
// Validates the first param
if (first || first === 0) {
if (first < 0) {
throw new Parse.Error(
Parse.Error.INVALID_QUERY,
'First should be a positive number'
);
}
// The first param is translated to the limit param of the Parse legacy API
limit = first;
}
// Validates the before param
if (before || before === 0) {
// This method converts the cursor to the index of the object
before = cursorToOffset(before);
if ((!before && before !== 0) || before < 0) {
throw new Parse.Error(
Parse.Error.INVALID_QUERY,
'Before is not a valid cursor'
);
}
if ((skip || 0) >= before) {
// If the before index is less then the skip, no objects will be returned
limit = 0;
} else if ((!limit && limit !== 0) || (skip || 0) + limit > before) {
// If there is no limit set, the limit is calculated. Or, if the limit (plus skip) is bigger than the before index, the new limit is set.
limit = before - (skip || 0);
}
}
// Validates the last param
if (last || last === 0) {
if (last < 0) {
throw new Parse.Error(
Parse.Error.INVALID_QUERY,
'Last should be a positive number'
);
}
if (last > maxLimit) {
// Last can't be bigger than Parse server maxLimit config.
last = maxLimit;
}
if (limit || limit === 0) {
// If there is a previous limit set, it may be adjusted
if (last < limit) {
// if last is less than the current limit
skip = (skip || 0) + (limit - last); // The skip is adjusted
limit = last; // the limit is adjusted
}
} else if (last === 0) {
// No objects will be returned
limit = 0;
} else {
// No previous limit set, the limit will be equal to last and pre count is needed.
limit = last;
needToPreCount = true;
}
}
return {
skip,
limit,
needToPreCount,
};
};
export { getObject, findObjects, calculateSkipAndLimit };

View File

@@ -588,10 +588,15 @@ const CLASS_NAME_ATT = {
type: new GraphQLNonNull(GraphQLString),
};
const GLOBAL_OR_OBJECT_ID_ATT = {
description:
'This is the object id. You can use either the global or the object id.',
type: OBJECT_ID,
};
const OBJECT_ID_ATT = {
description: 'This is the object id.',
type: OBJECT_ID,
resolve: ({ objectId }) => objectId,
};
const CREATED_AT_ATT = {
@@ -611,7 +616,7 @@ const INPUT_FIELDS = {
};
const CREATE_RESULT_FIELDS = {
id: OBJECT_ID_ATT,
objectId: OBJECT_ID_ATT,
createdAt: CREATED_AT_ATT,
};
@@ -637,7 +642,7 @@ const PARSE_OBJECT = new GraphQLInterfaceType({
});
const SESSION_TOKEN_ATT = {
description: 'The user session token',
description: 'The current user session token.',
type: new GraphQLNonNull(GraphQLString),
};
@@ -926,6 +931,25 @@ const options = {
type: GraphQLString,
};
const ID_WHERE_INPUT = new GraphQLInputObjectType({
name: 'IdWhereInput',
description:
'The IdWhereInput input type is used in operations that involve filtering objects by an id.',
fields: {
equalTo: equalTo(GraphQLID),
notEqualTo: notEqualTo(GraphQLID),
lessThan: lessThan(GraphQLID),
lessThanOrEqualTo: lessThanOrEqualTo(GraphQLID),
greaterThan: greaterThan(GraphQLID),
greaterThanOrEqualTo: greaterThanOrEqualTo(GraphQLID),
in: inOp(GraphQLID),
notIn: notIn(GraphQLID),
exists,
inQueryKey,
notInQueryKey,
},
});
const STRING_WHERE_INPUT = new GraphQLInputObjectType({
name: 'StringWhereInput',
description:
@@ -1164,19 +1188,6 @@ const POLYGON_WHERE_INPUT = new GraphQLInputObjectType({
},
});
const FIND_RESULT = new GraphQLObjectType({
name: 'FindResult',
description:
'The FindResult object type is used in the find queries to return the data of the matched objects.',
fields: {
results: {
description: 'This is the objects returned by the query',
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(OBJECT))),
},
count: COUNT_ATT,
},
});
const ELEMENT = new GraphQLObjectType({
name: 'Element',
description: "The Element object type is used to return array items' value.",
@@ -1247,6 +1258,7 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLType(CENTER_SPHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(GEO_WITHIN_INPUT, true);
parseGraphQLSchema.addGraphQLType(GEO_INTERSECTS_INPUT, true);
parseGraphQLSchema.addGraphQLType(ID_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(STRING_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(NUMBER_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(BOOLEAN_WHERE_INPUT, true);
@@ -1258,9 +1270,7 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLType(FILE_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(GEO_POINT_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(POLYGON_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(FIND_RESULT, true);
parseGraphQLSchema.addGraphQLType(ELEMENT, true);
parseGraphQLSchema.addGraphQLType(OBJECT_ID, true);
parseGraphQLSchema.addGraphQLType(ACL_INPUT, true);
parseGraphQLSchema.addGraphQLType(USER_ACL_INPUT, true);
parseGraphQLSchema.addGraphQLType(ROLE_ACL_INPUT, true);
@@ -1296,6 +1306,7 @@ export {
POLYGON,
OBJECT_ID,
CLASS_NAME_ATT,
GLOBAL_OR_OBJECT_ID_ATT,
OBJECT_ID_ATT,
UPDATED_AT_ATT,
CREATED_AT_ATT,
@@ -1337,6 +1348,7 @@ export {
notInQueryKey,
matchesRegex,
options,
ID_WHERE_INPUT,
STRING_WHERE_INPUT,
NUMBER_WHERE_INPUT,
BOOLEAN_WHERE_INPUT,
@@ -1348,7 +1360,6 @@ export {
FILE_WHERE_INPUT,
GEO_POINT_WHERE_INPUT,
POLYGON_WHERE_INPUT,
FIND_RESULT,
ARRAY_RESULT,
ELEMENT,
ACL_INPUT,

View File

@@ -0,0 +1,51 @@
import { nodeDefinitions, fromGlobalId } from 'graphql-relay';
import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as objectsQueries from '../helpers/objectsQueries';
import { extractKeysAndInclude } from './parseClassTypes';
const GLOBAL_ID_ATT = {
description: 'This is the global id.',
type: defaultGraphQLTypes.OBJECT_ID,
};
const load = parseGraphQLSchema => {
const { nodeInterface, nodeField } = nodeDefinitions(
async (globalId, context, queryInfo) => {
try {
const { type, id } = fromGlobalId(globalId);
const { config, auth, info } = context;
const selectedFields = getFieldNames(queryInfo);
const { keys, include } = extractKeysAndInclude(selectedFields);
return {
className: type,
...(await objectsQueries.getObject(
type,
id,
keys,
include,
undefined,
undefined,
config,
auth,
info
)),
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
obj => {
return parseGraphQLSchema.parseClassTypes[obj.className]
.classGraphQLOutputType;
}
);
parseGraphQLSchema.addGraphQLType(nodeInterface, true);
parseGraphQLSchema.relayNodeInterface = nodeInterface;
parseGraphQLSchema.addGraphQLQuery('node', nodeField, true);
};
export { GLOBAL_ID_ATT, load };

View File

@@ -1,80 +1,97 @@
import { GraphQLNonNull } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import { GraphQLUpload } from 'graphql-upload';
import Parse from 'parse/node';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import logger from '../../logger';
const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLMutation(
'createFile',
{
description:
'The create mutation can be used to create and upload a new file.',
args: {
upload: {
description: 'This is the new file to be created and uploaded',
type: new GraphQLNonNull(GraphQLUpload),
},
const createMutation = mutationWithClientMutationId({
name: 'CreateFile',
description:
'The createFile mutation can be used to create and upload a new file.',
inputFields: {
upload: {
description: 'This is the new file to be created and uploaded.',
type: new GraphQLNonNull(GraphQLUpload),
},
type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO),
async resolve(_source, args, context) {
},
outputFields: {
fileInfo: {
description: 'This is the created file info.',
type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO),
},
},
mutateAndGetPayload: async (args, context) => {
try {
const { upload } = args;
const { config } = context;
const { createReadStream, filename, mimetype } = await upload;
let data = null;
if (createReadStream) {
const stream = createReadStream();
data = await new Promise((resolve, reject) => {
const chunks = [];
stream
.on('error', reject)
.on('data', chunk => chunks.push(chunk))
.on('end', () => resolve(Buffer.concat(chunks)));
});
}
if (!data || !data.length) {
throw new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'Invalid file upload.'
);
}
if (filename.length > 128) {
throw new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename too long.'
);
}
if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
throw new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename contains invalid characters.'
);
}
try {
const { upload } = args;
const { config } = context;
const { createReadStream, filename, mimetype } = await upload;
let data = null;
if (createReadStream) {
const stream = createReadStream();
data = await new Promise((resolve, reject) => {
const chunks = [];
stream
.on('error', reject)
.on('data', chunk => chunks.push(chunk))
.on('end', () => resolve(Buffer.concat(chunks)));
});
}
if (!data || !data.length) {
throw new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'Invalid file upload.'
);
}
if (filename.length > 128) {
throw new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename too long.'
);
}
if (!filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
throw new Parse.Error(
Parse.Error.INVALID_FILE_NAME,
'Filename contains invalid characters.'
);
}
try {
return await config.filesController.createFile(
return {
fileInfo: await config.filesController.createFile(
config,
filename,
data,
mimetype
);
} catch (e) {
logger.error('Error creating a file: ', e);
throw new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
`Could not store file: ${filename}.`
);
}
),
};
} catch (e) {
parseGraphQLSchema.handleError(e);
logger.error('Error creating a file: ', e);
throw new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
`Could not store file: ${filename}.`
);
}
},
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
parseGraphQLSchema.addGraphQLType(
createMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(createMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation(
'createFile',
createMutation,
true,
true
);

View File

@@ -1,4 +1,5 @@
import { GraphQLNonNull, GraphQLEnumType } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import { FunctionsRouter } from '../../Routers/FunctionsRouter';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
@@ -21,28 +22,34 @@ const load = parseGraphQLSchema => {
true
);
parseGraphQLSchema.addGraphQLMutation(
'callCloudCode',
{
description:
'The call mutation can be used to invoke a cloud code function.',
args: {
functionName: {
description: 'This is the function to be called.',
type: new GraphQLNonNull(cloudCodeFunctionEnum),
},
params: {
description: 'These are the params to be passed to the function.',
type: defaultGraphQLTypes.OBJECT,
},
const callCloudCodeMutation = mutationWithClientMutationId({
name: 'CallCloudCode',
description:
'The callCloudCode mutation can be used to invoke a cloud code function.',
inputFields: {
functionName: {
description: 'This is the function to be called.',
type: new GraphQLNonNull(cloudCodeFunctionEnum),
},
type: defaultGraphQLTypes.ANY,
async resolve(_source, args, context) {
try {
const { functionName, params } = args;
const { config, auth, info } = context;
params: {
description: 'These are the params to be passed to the function.',
type: defaultGraphQLTypes.OBJECT,
},
},
outputFields: {
result: {
description:
'This is the result value of the cloud code function execution.',
type: defaultGraphQLTypes.ANY,
},
},
mutateAndGetPayload: async (args, context) => {
try {
const { functionName, params } = args;
const { config, auth, info } = context;
return (await FunctionsRouter.handleCloudFunction({
return {
result: (await FunctionsRouter.handleCloudFunction({
params: {
functionName,
},
@@ -50,12 +57,23 @@ const load = parseGraphQLSchema => {
auth,
info,
body: params,
})).response.result;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
})).response.result,
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
parseGraphQLSchema.addGraphQLType(
callCloudCodeMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(callCloudCodeMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation(
'callCloudCode',
callCloudCodeMutation,
true,
true
);

View File

@@ -1,4 +1,5 @@
import { GraphQLNonNull } from 'graphql';
import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay';
import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import {
@@ -17,13 +18,16 @@ const getOnlyRequiredFields = (
includedFieldsString,
nativeObjectFields
) => {
const includedFields = includedFieldsString.split(',');
const selectedFields = selectedFieldsString.split(',');
const includedFields = includedFieldsString
? includedFieldsString.split(',')
: [];
const selectedFields = selectedFieldsString
? selectedFieldsString.split(',')
: [];
const missingFields = selectedFields
.filter(
field =>
!nativeObjectFields.includes(field) ||
includedFields.includes(field)
!nativeObjectFields.includes(field) || includedFields.includes(field)
)
.join(',');
if (!missingFields.length) {
@@ -40,6 +44,8 @@ const load = function(
) {
const className = parseClass.className;
const graphQLClassName = transformClassNameToGraphQL(className);
const getGraphQLQueryName =
graphQLClassName.charAt(0).toLowerCase() + graphQLClassName.slice(1);
const {
create: isCreateEnabled = true,
@@ -55,18 +61,25 @@ const load = function(
if (isCreateEnabled) {
const createGraphQLMutationName = `create${graphQLClassName}`;
parseGraphQLSchema.addGraphQLMutation(createGraphQLMutationName, {
const createGraphQLMutation = mutationWithClientMutationId({
name: `Create${graphQLClassName}`,
description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${graphQLClassName} class.`,
args: {
inputFields: {
fields: {
description: 'These are the fields used to create the object.',
description:
'These are the fields that will be used to create the new object.',
type: classGraphQLCreateType || defaultGraphQLTypes.OBJECT,
},
},
type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
async resolve(_source, args, context, mutationInfo) {
outputFields: {
[getGraphQLQueryName]: {
description: 'This is the created object.',
type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
},
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try {
let { fields } = args;
if (!fields) fields = {};
@@ -85,13 +98,15 @@ const load = function(
auth,
info
);
const selectedFields = getFieldNames(mutationInfo);
const selectedFields = getFieldNames(mutationInfo)
.filter(field => field.startsWith(`${getGraphQLQueryName}.`))
.map(field => field.replace(`${getGraphQLQueryName}.`, ''));
const { keys, include } = extractKeysAndInclude(selectedFields);
const { keys: requiredKeys, needGet } = getOnlyRequiredFields(
fields,
keys,
include,
['id', 'createdAt', 'updatedAt']
['id', 'objectId', 'createdAt', 'updatedAt']
);
let optimizedObject = {};
if (needGet) {
@@ -108,37 +123,65 @@ const load = function(
);
}
return {
...createdObject,
updatedAt: createdObject.createdAt,
...parseFields,
...optimizedObject,
[getGraphQLQueryName]: {
...createdObject,
updatedAt: createdObject.createdAt,
...parseFields,
...optimizedObject,
},
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
if (
parseGraphQLSchema.addGraphQLType(
createGraphQLMutation.args.input.type.ofType
) &&
parseGraphQLSchema.addGraphQLType(createGraphQLMutation.type)
) {
parseGraphQLSchema.addGraphQLMutation(
createGraphQLMutationName,
createGraphQLMutation
);
}
}
if (isUpdateEnabled) {
const updateGraphQLMutationName = `update${graphQLClassName}`;
parseGraphQLSchema.addGraphQLMutation(updateGraphQLMutationName, {
const updateGraphQLMutation = mutationWithClientMutationId({
name: `Update${graphQLClassName}`,
description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${graphQLClassName} class.`,
args: {
id: defaultGraphQLTypes.OBJECT_ID_ATT,
inputFields: {
id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT,
fields: {
description: 'These are the fields used to update the object.',
description:
'These are the fields that will be used to update the object.',
type: classGraphQLUpdateType || defaultGraphQLTypes.OBJECT,
},
},
type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
async resolve(_source, args, context, mutationInfo) {
outputFields: {
[getGraphQLQueryName]: {
description: 'This is the updated object.',
type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
},
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try {
const { id, fields } = args;
let { id, fields } = args;
if (!fields) fields = {};
const { config, auth, info } = context;
const globalIdObject = fromGlobalId(id);
if (globalIdObject.type === className) {
id = globalIdObject.id;
}
const parseFields = await transformTypes('update', fields, {
className,
parseGraphQLSchema,
@@ -154,15 +197,17 @@ const load = function(
info
);
const selectedFields = getFieldNames(mutationInfo);
const selectedFields = getFieldNames(mutationInfo)
.filter(field => field.startsWith(`${getGraphQLQueryName}.`))
.map(field => field.replace(`${getGraphQLQueryName}.`, ''));
const { keys, include } = extractKeysAndInclude(selectedFields);
const { keys: requiredKeys, needGet } = getOnlyRequiredFields(
fields,
keys,
include,
['id', 'updatedAt']
['id', 'objectId', 'updatedAt']
);
let optimizedObject = {};
if (needGet) {
optimizedObject = await objectsQueries.getObject(
@@ -178,38 +223,69 @@ const load = function(
);
}
return {
id,
...updatedObject,
...parseFields,
...optimizedObject,
[getGraphQLQueryName]: {
objectId: id,
...updatedObject,
...parseFields,
...optimizedObject,
},
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
if (
parseGraphQLSchema.addGraphQLType(
updateGraphQLMutation.args.input.type.ofType
) &&
parseGraphQLSchema.addGraphQLType(updateGraphQLMutation.type)
) {
parseGraphQLSchema.addGraphQLMutation(
updateGraphQLMutationName,
updateGraphQLMutation
);
}
}
if (isDestroyEnabled) {
const deleteGraphQLMutationName = `delete${graphQLClassName}`;
parseGraphQLSchema.addGraphQLMutation(deleteGraphQLMutationName, {
const deleteGraphQLMutation = mutationWithClientMutationId({
name: `Delete${graphQLClassName}`,
description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${graphQLClassName} class.`,
args: {
id: defaultGraphQLTypes.OBJECT_ID_ATT,
inputFields: {
id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT,
},
type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
async resolve(_source, args, context, mutationInfo) {
outputFields: {
[getGraphQLQueryName]: {
description: 'This is the deleted object.',
type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
},
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try {
const { id } = args;
let { id } = args;
const { config, auth, info } = context;
const selectedFields = getFieldNames(mutationInfo);
const { keys, include } = extractKeysAndInclude(selectedFields);
const globalIdObject = fromGlobalId(id);
if (globalIdObject.type === className) {
id = globalIdObject.id;
}
const selectedFields = getFieldNames(mutationInfo)
.filter(field => field.startsWith(`${getGraphQLQueryName}.`))
.map(field => field.replace(`${getGraphQLQueryName}.`, ''));
const { keys, include } = extractKeysAndInclude(selectedFields);
let optimizedObject = {};
const splitedKeys = keys.split(',');
if (splitedKeys.length > 1 || splitedKeys[0] !== 'id') {
if (
keys &&
keys.split(',').filter(key => !['id', 'objectId'].includes(key))
.length > 0
) {
optimizedObject = await objectsQueries.getObject(
className,
id,
@@ -229,12 +305,29 @@ const load = function(
auth,
info
);
return { id, ...optimizedObject };
return {
[getGraphQLQueryName]: {
objectId: id,
...optimizedObject,
},
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
if (
parseGraphQLSchema.addGraphQLType(
deleteGraphQLMutation.args.input.type.ofType
) &&
parseGraphQLSchema.addGraphQLType(deleteGraphQLMutation.type)
) {
parseGraphQLSchema.addGraphQLMutation(
deleteGraphQLMutationName,
deleteGraphQLMutation
);
}
}
};

View File

@@ -1,4 +1,5 @@
import { GraphQLNonNull } from 'graphql';
import { fromGlobalId } from 'graphql-relay';
import getFieldNames from 'graphql-list-fields';
import pluralize from 'pluralize';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
@@ -14,11 +15,18 @@ const getParseClassQueryConfig = function(
};
const getQuery = async (className, _source, args, context, queryInfo) => {
const { id, options } = args;
let { id } = args;
const { options } = args;
const { readPreference, includeReadPreference } = options || {};
const { config, auth, info } = context;
const selectedFields = getFieldNames(queryInfo);
const globalIdObject = fromGlobalId(id);
if (globalIdObject.type === className) {
id = globalIdObject.id;
}
const { keys, include } = extractKeysAndInclude(selectedFields);
return await objectsQueries.getObject(
@@ -58,7 +66,7 @@ const load = function(
parseGraphQLSchema.addGraphQLQuery(getGraphQLQueryName, {
description: `The ${getGraphQLQueryName} query can be used to get an object of the ${graphQLClassName} class by its id.`,
args: {
id: defaultGraphQLTypes.OBJECT_ID_ATT,
id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT,
options: defaultGraphQLTypes.READ_OPTIONS_ATT,
},
type: new GraphQLNonNull(
@@ -82,11 +90,20 @@ const load = function(
description: `The ${findGraphQLQueryName} query can be used to find objects of the ${graphQLClassName} class.`,
args: classGraphQLFindArgs,
type: new GraphQLNonNull(
classGraphQLFindResultType || defaultGraphQLTypes.FIND_RESULT
classGraphQLFindResultType || defaultGraphQLTypes.OBJECT
),
async resolve(_source, args, context, queryInfo) {
try {
const { where, order, skip, limit, options } = args;
const {
where,
order,
skip,
first,
after,
last,
before,
options,
} = args;
const {
readPreference,
includeReadPreference,
@@ -97,8 +114,8 @@ const load = function(
const { keys, include } = extractKeysAndInclude(
selectedFields
.filter(field => field.includes('.'))
.map(field => field.slice(field.indexOf('.') + 1))
.filter(field => field.startsWith('edges.node.'))
.map(field => field.replace('edges.node.', ''))
);
const parseOrder = order && order.join(',');
@@ -107,7 +124,10 @@ const load = function(
where,
parseOrder,
skip,
limit,
first,
after,
last,
before,
keys,
include,
false,
@@ -117,7 +137,7 @@ const load = function(
config,
auth,
info,
selectedFields.map(field => field.split('.', 1)[0]),
selectedFields,
parseClass.fields
);
} catch (e) {

View File

@@ -7,6 +7,11 @@ import {
GraphQLNonNull,
GraphQLEnumType,
} from 'graphql';
import {
globalIdField,
connectionArgs,
connectionDefinitions,
} from 'graphql-relay';
import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import * as objectsQueries from '../helpers/objectsQueries';
@@ -30,9 +35,7 @@ const getInputFieldsAndConstraints = function(
parseClass,
parseClassConfig: ?ParseGraphQLClassConfig
) {
const classFields = Object.keys(parseClass.fields)
.filter(field => field !== 'objectId')
.concat('id');
const classFields = Object.keys(parseClass.fields).concat('id');
const {
inputFields: allowedInputFields,
outputFields: allowedOutputFields,
@@ -48,8 +51,9 @@ const getInputFieldsAndConstraints = function(
// All allowed customs fields
const classCustomFields = classFields.filter(field => {
return !Object.keys(defaultGraphQLTypes.PARSE_OBJECT_FIELDS).includes(
field
return (
!Object.keys(defaultGraphQLTypes.PARSE_OBJECT_FIELDS).includes(field) &&
field !== 'id'
);
});
@@ -153,7 +157,11 @@ const load = (
...fields,
[field]: {
description: `This is the object ${field}.`,
type,
type:
className === '_User' &&
(field === 'username' || field === 'password')
? new GraphQLNonNull(type)
: type,
},
};
} else {
@@ -209,7 +217,7 @@ const load = (
fields: () => {
const fields = {
link: {
description: `Link an existing object from ${graphQLClassName} class.`,
description: `Link an existing object from ${graphQLClassName} class. You can use either the global or the object id.`,
type: GraphQLID,
},
};
@@ -233,17 +241,17 @@ const load = (
fields: () => {
const fields = {
add: {
description: `Add an existing object from the ${graphQLClassName} class into the relation.`,
description: `Add existing objects from the ${graphQLClassName} class into the relation. You can use either the global or the object ids.`,
type: new GraphQLList(defaultGraphQLTypes.OBJECT_ID),
},
remove: {
description: `Remove an existing object from the ${graphQLClassName} class out of the relation.`,
description: `Remove existing objects from the ${graphQLClassName} class out of the relation. You can use either the global or the object ids.`,
type: new GraphQLList(defaultGraphQLTypes.OBJECT_ID),
},
};
if (isCreateEnabled) {
fields['createAndAdd'] = {
description: `Create and add an object of the ${graphQLClassName} class into the relation.`,
description: `Create and add objects of the ${graphQLClassName} class into the relation.`,
type: new GraphQLList(new GraphQLNonNull(classGraphQLCreateType)),
};
}
@@ -268,12 +276,12 @@ const load = (
notInQueryKey: defaultGraphQLTypes.notInQueryKey,
inQuery: {
description:
'This is the inQuery operator to specify a constraint to select the objects where a field equals to any of the ids in the result of a different query.',
'This is the inQuery operator to specify a constraint to select the objects where a field equals to any of the object ids in the result of a different query.',
type: defaultGraphQLTypes.SUBQUERY_INPUT,
},
notInQuery: {
description:
'This is the notInQuery operator to specify a constraint to select the objects where a field do not equal to any of the ids in the result of a different query.',
'This is the notInQuery operator to specify a constraint to select the objects where a field do not equal to any of the object ids in the result of a different query.',
type: defaultGraphQLTypes.SUBQUERY_INPUT,
},
},
@@ -298,7 +306,8 @@ const load = (
const type = transformConstraintTypeToGraphQL(
parseClass.fields[parseField].type,
parseClass.fields[parseField].targetClass,
parseGraphQLSchema.parseClassTypes
parseGraphQLSchema.parseClassTypes,
field
);
if (type) {
return {
@@ -339,11 +348,12 @@ const load = (
const updatedSortFields = {
...sortFields,
};
const value = field === 'id' ? 'objectId' : field;
if (asc) {
updatedSortFields[`${field}_ASC`] = { value: field };
updatedSortFields[`${field}_ASC`] = { value };
}
if (desc) {
updatedSortFields[`${field}_DESC`] = { value: `-${field}` };
updatedSortFields[`${field}_DESC`] = { value: `-${value}` };
}
return updatedSortFields;
}, {}),
@@ -365,10 +375,18 @@ const load = (
: GraphQLString,
},
skip: defaultGraphQLTypes.SKIP_ATT,
limit: defaultGraphQLTypes.LIMIT_ATT,
...connectionArgs,
options: defaultGraphQLTypes.READ_OPTIONS_ATT,
};
const classGraphQLOutputTypeName = `${graphQLClassName}`;
const interfaces = [
defaultGraphQLTypes.PARSE_OBJECT,
parseGraphQLSchema.relayNodeInterface,
];
const parseObjectFields = {
id: globalIdField(className, obj => obj.objectId),
...defaultGraphQLTypes.PARSE_OBJECT_FIELDS,
};
const outputFields = () => {
return classOutputFields.reduce((fields, field) => {
const type = transformOutputTypeToGraphQL(
@@ -392,7 +410,16 @@ const load = (
type,
async resolve(source, args, context, queryInfo) {
try {
const { where, order, skip, limit, options } = args;
const {
where,
order,
skip,
first,
after,
last,
before,
options,
} = args;
const {
readPreference,
includeReadPreference,
@@ -403,9 +430,11 @@ const load = (
const { keys, include } = extractKeysAndInclude(
selectedFields
.filter(field => field.includes('.'))
.map(field => field.slice(field.indexOf('.') + 1))
.filter(field => field.startsWith('edges.node.'))
.map(field => field.replace('edges.node.', ''))
);
const parseOrder = order && order.join(',');
return await objectsQueries.findObjects(
source[field].className,
{
@@ -419,9 +448,12 @@ const load = (
},
...(where || {}),
},
order,
parseOrder,
skip,
limit,
first,
after,
last,
before,
keys,
include,
false,
@@ -431,8 +463,11 @@ const load = (
config,
auth,
info,
selectedFields.map(field => field.split('.', 1)[0]),
parseClass.fields
selectedFields,
parseGraphQLSchema.parseClasses.find(
parseClass =>
parseClass.className === source[field].className
).fields
);
} catch (e) {
parseGraphQLSchema.handleError(e);
@@ -491,39 +526,32 @@ const load = (
} else {
return fields;
}
}, defaultGraphQLTypes.PARSE_OBJECT_FIELDS);
}, parseObjectFields);
};
let classGraphQLOutputType = new GraphQLObjectType({
name: classGraphQLOutputTypeName,
description: `The ${classGraphQLOutputTypeName} object type is used in operations that involve outputting objects of ${graphQLClassName} class.`,
interfaces: [defaultGraphQLTypes.PARSE_OBJECT],
interfaces,
fields: outputFields,
});
classGraphQLOutputType = parseGraphQLSchema.addGraphQLType(
classGraphQLOutputType
);
const classGraphQLFindResultTypeName = `${graphQLClassName}FindResult`;
let classGraphQLFindResultType = new GraphQLObjectType({
name: classGraphQLFindResultTypeName,
description: `The ${classGraphQLFindResultTypeName} object type is used in the ${graphQLClassName} find query to return the data of the matched objects.`,
fields: {
results: {
description: 'This is the objects returned by the query',
type: new GraphQLNonNull(
new GraphQLList(
new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
)
)
),
},
const { connectionType, edgeType } = connectionDefinitions({
name: graphQLClassName,
connectionFields: {
count: defaultGraphQLTypes.COUNT_ATT,
},
nodeType: classGraphQLOutputType || defaultGraphQLTypes.OBJECT,
});
classGraphQLFindResultType = parseGraphQLSchema.addGraphQLType(
classGraphQLFindResultType
);
let classGraphQLFindResultType = undefined;
if (
parseGraphQLSchema.addGraphQLType(edgeType) &&
parseGraphQLSchema.addGraphQLType(connectionType, false, false, true)
) {
classGraphQLFindResultType = connectionType;
}
parseGraphQLSchema.parseClassTypes[className] = {
classGraphQLPointerType,
@@ -546,67 +574,16 @@ const load = (
const viewerType = new GraphQLObjectType({
name: 'Viewer',
description: `The Viewer object type is used in operations that involve outputting the current user data.`,
interfaces: [defaultGraphQLTypes.PARSE_OBJECT],
fields: () => ({
...outputFields(),
sessionToken: defaultGraphQLTypes.SESSION_TOKEN_ATT,
user: {
description: 'This is the current user.',
type: new GraphQLNonNull(classGraphQLOutputType),
},
}),
});
parseGraphQLSchema.viewerType = viewerType;
parseGraphQLSchema.addGraphQLType(viewerType, true, true);
const userSignUpInputTypeName = 'SignUpFieldsInput';
const userSignUpInputType = new GraphQLInputObjectType({
name: userSignUpInputTypeName,
description: `The ${userSignUpInputTypeName} input type is used in operations that involve inputting objects of ${graphQLClassName} class when signing up.`,
fields: () =>
classCreateFields.reduce((fields, field) => {
const type = transformInputTypeToGraphQL(
parseClass.fields[field].type,
parseClass.fields[field].targetClass,
parseGraphQLSchema.parseClassTypes
);
if (type) {
return {
...fields,
[field]: {
description: `This is the object ${field}.`,
type:
field === 'username' || field === 'password'
? new GraphQLNonNull(type)
: type,
},
};
} else {
return fields;
}
}, {}),
});
parseGraphQLSchema.addGraphQLType(userSignUpInputType, true, true);
const userLogInInputTypeName = 'LogInFieldsInput';
const userLogInInputType = new GraphQLInputObjectType({
name: userLogInInputTypeName,
description: `The ${userLogInInputTypeName} input type is used to login.`,
fields: {
username: {
description: 'This is the username used to log the user in.',
type: new GraphQLNonNull(GraphQLString),
},
password: {
description: 'This is the password used to log the user in.',
type: new GraphQLNonNull(GraphQLString),
},
},
});
parseGraphQLSchema.addGraphQLType(userLogInInputType, true, true);
parseGraphQLSchema.parseClassTypes[
className
].signUpInputType = userSignUpInputType;
parseGraphQLSchema.parseClassTypes[
className
].logInInputType = userLogInInputType;
parseGraphQLSchema.viewerType = viewerType;
}
};

View File

@@ -1,5 +1,6 @@
import Parse from 'parse/node';
import { GraphQLNonNull } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import * as schemaTypes from './schemaTypes';
import {
transformToParse,
@@ -9,135 +10,183 @@ import { enforceMasterKeyAccess } from '../parseGraphQLUtils';
import { getClass } from './schemaQueries';
const load = parseGraphQLSchema => {
const createClassMutation = mutationWithClientMutationId({
name: 'CreateClass',
description:
'The createClass mutation can be used to create the schema for a new object class.',
inputFields: {
name: schemaTypes.CLASS_NAME_ATT,
schemaFields: {
description: "These are the schema's fields of the object class.",
type: schemaTypes.SCHEMA_FIELDS_INPUT,
},
},
outputFields: {
class: {
description: 'This is the created class.',
type: new GraphQLNonNull(schemaTypes.CLASS),
},
},
mutateAndGetPayload: async (args, context) => {
try {
const { name, schemaFields } = args;
const { config, auth } = context;
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to create a schema."
);
}
const schema = await config.database.loadSchema({ clearCache: true });
const parseClass = await schema.addClassIfNotExists(
name,
transformToParse(schemaFields)
);
return {
class: {
name: parseClass.className,
schemaFields: transformToGraphQL(parseClass.fields),
},
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
parseGraphQLSchema.addGraphQLType(
createClassMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(createClassMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation(
'createClass',
{
description:
'The createClass mutation can be used to create the schema for a new object class.',
args: {
name: schemaTypes.CLASS_NAME_ATT,
schemaFields: {
description: "These are the schema's fields of the object class.",
type: schemaTypes.SCHEMA_FIELDS_INPUT,
},
},
type: new GraphQLNonNull(schemaTypes.CLASS),
resolve: async (_source, args, context) => {
try {
const { name, schemaFields } = args;
const { config, auth } = context;
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to create a schema."
);
}
const schema = await config.database.loadSchema({ clearCache: true });
const parseClass = await schema.addClassIfNotExists(
name,
transformToParse(schemaFields)
);
return {
name: parseClass.className,
schemaFields: transformToGraphQL(parseClass.fields),
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
},
createClassMutation,
true,
true
);
const updateClassMutation = mutationWithClientMutationId({
name: 'UpdateClass',
description:
'The updateClass mutation can be used to update the schema for an existing object class.',
inputFields: {
name: schemaTypes.CLASS_NAME_ATT,
schemaFields: {
description: "These are the schema's fields of the object class.",
type: schemaTypes.SCHEMA_FIELDS_INPUT,
},
},
outputFields: {
class: {
description: 'This is the updated class.',
type: new GraphQLNonNull(schemaTypes.CLASS),
},
},
mutateAndGetPayload: async (args, context) => {
try {
const { name, schemaFields } = args;
const { config, auth } = context;
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update a schema."
);
}
const schema = await config.database.loadSchema({ clearCache: true });
const existingParseClass = await getClass(name, schema);
const parseClass = await schema.updateClass(
name,
transformToParse(schemaFields, existingParseClass.fields),
undefined,
undefined,
config.database
);
return {
class: {
name: parseClass.className,
schemaFields: transformToGraphQL(parseClass.fields),
},
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
parseGraphQLSchema.addGraphQLType(
updateClassMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(updateClassMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation(
'updateClass',
{
description:
'The updateClass mutation can be used to update the schema for an existing object class.',
args: {
name: schemaTypes.CLASS_NAME_ATT,
schemaFields: {
description: "These are the schema's fields of the object class.",
type: schemaTypes.SCHEMA_FIELDS_INPUT,
},
},
type: new GraphQLNonNull(schemaTypes.CLASS),
resolve: async (_source, args, context) => {
try {
const { name, schemaFields } = args;
const { config, auth } = context;
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to update a schema."
);
}
const schema = await config.database.loadSchema({ clearCache: true });
const existingParseClass = await getClass(name, schema);
const parseClass = await schema.updateClass(
name,
transformToParse(schemaFields, existingParseClass.fields),
undefined,
undefined,
config.database
);
return {
name: parseClass.className,
schemaFields: transformToGraphQL(parseClass.fields),
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
},
updateClassMutation,
true,
true
);
parseGraphQLSchema.addGraphQLMutation(
'deleteClass',
{
description:
'The deleteClass mutation can be used to delete an existing object class.',
args: {
name: schemaTypes.CLASS_NAME_ATT,
},
type: new GraphQLNonNull(schemaTypes.CLASS),
resolve: async (_source, args, context) => {
try {
const { name } = args;
const { config, auth } = context;
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to delete a schema."
);
}
const schema = await config.database.loadSchema({ clearCache: true });
const existingParseClass = await getClass(name, schema);
await config.database.deleteSchema(name);
return {
name: existingParseClass.className,
schemaFields: transformToGraphQL(existingParseClass.fields),
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
const deleteClassMutation = mutationWithClientMutationId({
name: 'DeleteClass',
description:
'The deleteClass mutation can be used to delete an existing object class.',
inputFields: {
name: schemaTypes.CLASS_NAME_ATT,
},
outputFields: {
class: {
description: 'This is the deleted class.',
type: new GraphQLNonNull(schemaTypes.CLASS),
},
},
mutateAndGetPayload: async (args, context) => {
try {
const { name } = args;
const { config, auth } = context;
enforceMasterKeyAccess(auth);
if (auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
"read-only masterKey isn't allowed to delete a schema."
);
}
const schema = await config.database.loadSchema({ clearCache: true });
const existingParseClass = await getClass(name, schema);
await config.database.deleteSchema(name);
return {
class: {
name: existingParseClass.className,
schemaFields: transformToGraphQL(existingParseClass.fields),
},
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
parseGraphQLSchema.addGraphQLType(
deleteClassMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(deleteClassMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation(
'deleteClass',
deleteClassMutation,
true,
true
);

View File

@@ -1,4 +1,5 @@
import { GraphQLNonNull } from 'graphql';
import { GraphQLNonNull, GraphQLString } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import UsersRouter from '../../Routers/UsersRouter';
import * as objectsMutations from '../helpers/objectsMutations';
import { getUserFromSessionToken } from './usersQueries';
@@ -10,110 +11,166 @@ const load = parseGraphQLSchema => {
return;
}
parseGraphQLSchema.addGraphQLMutation(
'signUp',
{
description: 'The signUp mutation can be used to sign the user up.',
args: {
fields: {
descriptions: 'These are the fields of the user.',
type: parseGraphQLSchema.parseClassTypes['_User'].signUpInputType,
},
},
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, args, context, mutationInfo) {
try {
const { fields } = args;
const { config, auth, info } = context;
const { sessionToken } = await objectsMutations.createObject(
'_User',
fields,
config,
auth,
info
);
info.sessionToken = sessionToken;
return await getUserFromSessionToken(config, info, mutationInfo);
} catch (e) {
parseGraphQLSchema.handleError(e);
}
const signUpMutation = mutationWithClientMutationId({
name: 'SignUp',
description:
'The signUp mutation can be used to create and sign up a new user.',
inputFields: {
userFields: {
descriptions:
'These are the fields of the new user to be created and signed up.',
type:
parseGraphQLSchema.parseClassTypes['_User'].classGraphQLCreateType,
},
},
outputFields: {
viewer: {
description:
'This is the new user that was created, signed up and returned as a viewer.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
},
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try {
const { userFields } = args;
const { config, auth, info } = context;
const { sessionToken } = await objectsMutations.createObject(
'_User',
userFields,
config,
auth,
info
);
info.sessionToken = sessionToken;
return {
viewer: await getUserFromSessionToken(
config,
info,
mutationInfo,
'viewer.user.',
true
),
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
parseGraphQLSchema.addGraphQLType(
signUpMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(signUpMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation('signUp', signUpMutation, true, true);
parseGraphQLSchema.addGraphQLMutation(
'logIn',
{
description: 'The logIn mutation can be used to log the user in.',
args: {
fields: {
description: 'This is data needed to login',
type: parseGraphQLSchema.parseClassTypes['_User'].logInInputType,
},
const logInMutation = mutationWithClientMutationId({
name: 'LogIn',
description: 'The logIn mutation can be used to log in an existing user.',
inputFields: {
username: {
description: 'This is the username used to log in the user.',
type: new GraphQLNonNull(GraphQLString),
},
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, args, context) {
try {
const {
fields: { username, password },
} = args;
const { config, auth, info } = context;
return (await usersRouter.handleLogIn({
body: {
username,
password,
},
query: {},
config,
auth,
info,
})).response;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
password: {
description: 'This is the password used to log in the user.',
type: new GraphQLNonNull(GraphQLString),
},
},
outputFields: {
viewer: {
description:
'This is the existing user that was logged in and returned as a viewer.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
},
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try {
const { username, password } = args;
const { config, auth, info } = context;
const { sessionToken } = (await usersRouter.handleLogIn({
body: {
username,
password,
},
query: {},
config,
auth,
info,
})).response;
info.sessionToken = sessionToken;
return {
viewer: await getUserFromSessionToken(
config,
info,
mutationInfo,
'viewer.user.',
true
),
};
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
parseGraphQLSchema.addGraphQLType(
logInMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(logInMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation('logIn', logInMutation, true, true);
parseGraphQLSchema.addGraphQLMutation(
'logOut',
{
description: 'The logOut mutation can be used to log the user out.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, _args, context, mutationInfo) {
try {
const { config, auth, info } = context;
const viewer = await getUserFromSessionToken(
config,
info,
mutationInfo
);
await usersRouter.handleLogOut({
config,
auth,
info,
});
return viewer;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
const logOutMutation = mutationWithClientMutationId({
name: 'LogOut',
description: 'The logOut mutation can be used to log out an existing user.',
outputFields: {
viewer: {
description:
'This is the existing user that was logged out and returned as a viewer.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
},
},
mutateAndGetPayload: async (_args, context, mutationInfo) => {
try {
const { config, auth, info } = context;
const viewer = await getUserFromSessionToken(
config,
info,
mutationInfo,
'viewer.user.',
true
);
await usersRouter.handleLogOut({
config,
auth,
info,
});
return { viewer };
} catch (e) {
parseGraphQLSchema.handleError(e);
}
},
});
parseGraphQLSchema.addGraphQLType(
logOutMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(logOutMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation('logOut', logOutMutation, true, true);
};
export { load };

View File

@@ -5,7 +5,13 @@ import rest from '../../rest';
import Auth from '../../Auth';
import { extractKeysAndInclude } from './parseClassTypes';
const getUserFromSessionToken = async (config, info, queryInfo) => {
const getUserFromSessionToken = async (
config,
info,
queryInfo,
keysPrefix,
validatedToken
) => {
if (!info || !info.sessionToken) {
throw new Parse.Error(
Parse.Error.INVALID_SESSION_TOKEN,
@@ -13,20 +19,42 @@ const getUserFromSessionToken = async (config, info, queryInfo) => {
);
}
const sessionToken = info.sessionToken;
const selectedFields = getFieldNames(queryInfo);
const selectedFields = getFieldNames(queryInfo)
.filter(field => field.startsWith(keysPrefix))
.map(field => field.replace(keysPrefix, ''));
const keysAndInclude = extractKeysAndInclude(selectedFields);
const { keys } = keysAndInclude;
let { include } = keysAndInclude;
if (validatedToken && !keys && !include) {
return {
sessionToken,
};
} else if (keys && !include) {
include = 'user';
}
const options = {};
if (keys) {
options.keys = keys
.split(',')
.map(key => `user.${key}`)
.join(',');
}
if (include) {
options.include = include
.split(',')
.map(included => `user.${included}`)
.join(',');
}
const { include } = extractKeysAndInclude(selectedFields);
const response = await rest.find(
config,
Auth.master(config),
'_Session',
{ sessionToken },
{
include: include
.split(',')
.map(included => `user.${included}`)
.join(','),
},
options,
info.clientVersion
);
if (
@@ -40,8 +68,10 @@ const getUserFromSessionToken = async (config, info, queryInfo) => {
);
} else {
const user = response.results[0].user;
user.sessionToken = sessionToken;
return user;
return {
sessionToken,
user,
};
}
};
@@ -59,7 +89,13 @@ const load = parseGraphQLSchema => {
async resolve(_source, _args, context, queryInfo) {
try {
const { config, info } = context;
return await getUserFromSessionToken(config, info, queryInfo);
return await getUserFromSessionToken(
config,
info,
queryInfo,
'user.',
false
);
} catch (e) {
parseGraphQLSchema.handleError(e);
}

View File

@@ -3,8 +3,13 @@ import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
const transformConstraintTypeToGraphQL = (
parseType,
targetClass,
parseClassTypes
parseClassTypes,
fieldName
) => {
if (fieldName === 'id' || fieldName === 'objectId') {
return defaultGraphQLTypes.ID_WHERE_INPUT;
}
switch (parseType) {
case 'String':
return defaultGraphQLTypes.STRING_WHERE_INPUT;

View File

@@ -1,3 +1,5 @@
import Parse from 'parse/node';
import { fromGlobalId } from 'graphql-relay';
import * as defaultGraphQLTypes from '../loaders/defaultGraphQLTypes';
import * as objectsMutations from '../helpers/objectsMutations';
@@ -108,8 +110,9 @@ const transformers = {
{ config, auth, info }
) => {
if (Object.keys(value) === 0)
throw new Error(
`You need to provide atleast one operation on the relation mutation of field ${field}`
throw new Parse.Error(
Parse.Error.INVALID_POINTER,
`You need to provide at least one operation on the relation mutation of field ${field}`
);
const op = {
@@ -143,11 +146,17 @@ const transformers = {
if (value.add || nestedObjectsToAdd.length > 0) {
if (!value.add) value.add = [];
value.add = value.add.map(input => ({
__type: 'Pointer',
className: targetClass,
objectId: input,
}));
value.add = value.add.map(input => {
const globalIdObject = fromGlobalId(input);
if (globalIdObject.type === targetClass) {
input = globalIdObject.id;
}
return {
__type: 'Pointer',
className: targetClass,
objectId: input,
};
});
op.ops.push({
__op: 'AddRelation',
objects: [...value.add, ...nestedObjectsToAdd],
@@ -157,11 +166,17 @@ const transformers = {
if (value.remove) {
op.ops.push({
__op: 'RemoveRelation',
objects: value.remove.map(input => ({
__type: 'Pointer',
className: targetClass,
objectId: input,
})),
objects: value.remove.map(input => {
const globalIdObject = fromGlobalId(input);
if (globalIdObject.type === targetClass) {
input = globalIdObject.id;
}
return {
__type: 'Pointer',
className: targetClass,
objectId: input,
};
}),
});
}
return op;
@@ -174,7 +189,8 @@ const transformers = {
{ config, auth, info }
) => {
if (Object.keys(value) > 1 || Object.keys(value) === 0)
throw new Error(
throw new Parse.Error(
Parse.Error.INVALID_POINTER,
`You need to provide link OR createLink on the pointer mutation of field ${field}`
);
@@ -199,10 +215,15 @@ const transformers = {
};
}
if (value.link) {
let objectId = value.link;
const globalIdObject = fromGlobalId(objectId);
if (globalIdObject.type === targetClass) {
objectId = globalIdObject.id;
}
return {
__type: 'Pointer',
className: targetClass,
objectId: value.link,
objectId,
};
}
},

View File

@@ -45,7 +45,7 @@ const transformOutputTypeToGraphQL = (
parseClassTypes[targetClass].classGraphQLFindResultType
);
} else {
return new GraphQLNonNull(defaultGraphQLTypes.FIND_RESULT);
return new GraphQLNonNull(defaultGraphQLTypes.OBJECT);
}
case 'File':
return defaultGraphQLTypes.FILE_INFO;

View File

@@ -1,3 +1,5 @@
import { fromGlobalId } from 'graphql-relay';
const parseQueryMap = {
id: 'objectId',
OR: '$or',
@@ -102,10 +104,15 @@ const transformQueryConstraintInputToParse = (
typeof fieldValue === 'string'
) {
const { targetClass } = fields[parentFieldName];
let objectId = fieldValue;
const globalIdObject = fromGlobalId(objectId);
if (globalIdObject.type === targetClass) {
objectId = globalIdObject.id;
}
constraints[fieldName] = {
__type: 'Pointer',
className: targetClass,
objectId: fieldValue,
objectId,
};
}
}
@@ -176,7 +183,7 @@ const transformQueryConstraintInputToParse = (
});
};
const transformQueryInputToParse = (constraints, fields) => {
const transformQueryInputToParse = (constraints, fields, className) => {
if (!constraints || typeof constraints !== 'object') {
return;
}
@@ -191,9 +198,30 @@ const transformQueryInputToParse = (constraints, fields) => {
if (fieldName !== 'objectId') {
fieldValue.forEach(fieldValueItem => {
transformQueryInputToParse(fieldValueItem, fields);
transformQueryInputToParse(fieldValueItem, fields, className);
});
return;
} else if (className) {
Object.keys(fieldValue).forEach(constraintName => {
const constraintValue = fieldValue[constraintName];
if (typeof constraintValue === 'string') {
const globalIdObject = fromGlobalId(constraintValue);
if (globalIdObject.type === className) {
fieldValue[constraintName] = globalIdObject.id;
}
} else if (Array.isArray(constraintValue)) {
fieldValue[constraintName] = constraintValue.map(value => {
const globalIdObject = fromGlobalId(value);
if (globalIdObject.type === className) {
return globalIdObject.id;
}
return value;
});
}
});
}
}