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", "follow-redirects": "1.9.0",
"graphql": "14.5.8", "graphql": "14.5.8",
"graphql-list-fields": "2.0.2", "graphql-list-fields": "2.0.2",
"graphql-relay": "^0.6.0",
"graphql-tools": "^4.0.5", "graphql-tools": "^4.0.5",
"graphql-upload": "8.1.0", "graphql-upload": "8.1.0",
"intersect": "1.0.1", "intersect": "1.0.1",

View File

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

View File

@@ -97,7 +97,11 @@ describe('parseObjectToMongoObjectForCreate', () => {
const lng3 = 65; const lng3 = 65;
const polygon = { const polygon = {
__type: 'Polygon', __type: 'Polygon',
coordinates: [[lat1, lng1], [lat2, lng2], [lat3, lng3]], coordinates: [
[lat1, lng1],
[lat2, lng2],
[lat3, lng3],
],
}; };
const out = transform.parseObjectToMongoObjectForCreate( const out = transform.parseObjectToMongoObjectForCreate(
null, null,
@@ -107,7 +111,12 @@ describe('parseObjectToMongoObjectForCreate', () => {
} }
); );
expect(out.location.coordinates).toEqual([ expect(out.location.coordinates).toEqual([
[[lng1, lat1], [lng2, lat2], [lng3, lat3], [lng1, lat1]], [
[lng1, lat1],
[lng2, lat2],
[lng3, lat3],
[lng1, lat1],
],
]); ]);
done(); done();
}); });
@@ -217,7 +226,15 @@ describe('parseObjectToMongoObjectForCreate', () => {
const lng = 45; const lng = 45;
// Mongo stores polygon in WGS84 lng/lat // Mongo stores polygon in WGS84 lng/lat
const input = { const input = {
location: { type: 'Polygon', coordinates: [[[lat, lng], [lat, lng]]] }, location: {
type: 'Polygon',
coordinates: [
[
[lat, lng],
[lat, lng],
],
],
},
}; };
const output = transform.mongoObjectToParseObject(null, input, { const output = transform.mongoObjectToParseObject(null, input, {
fields: { location: { type: 'Polygon' } }, fields: { location: { type: 'Polygon' } },
@@ -225,7 +242,10 @@ describe('parseObjectToMongoObjectForCreate', () => {
expect(typeof output.location).toEqual('object'); expect(typeof output.location).toEqual('object');
expect(output.location).toEqual({ expect(output.location).toEqual({
__type: 'Polygon', __type: 'Polygon',
coordinates: [[lng, lat], [lng, lat]], coordinates: [
[lng, lat],
[lng, lat],
],
}); });
done(); 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 Parse from 'parse/node';
import { GraphQLSchema, GraphQLObjectType } from 'graphql'; import {
GraphQLSchema,
GraphQLObjectType,
DocumentNode,
GraphQLNamedType,
} from 'graphql';
import { mergeSchemas, SchemaDirectiveVisitor } from 'graphql-tools'; import { mergeSchemas, SchemaDirectiveVisitor } from 'graphql-tools';
import requiredParameter from '../requiredParameter'; import requiredParameter from '../requiredParameter';
import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes'; import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes';
@@ -16,6 +21,7 @@ import { toGraphQLError } from './parseGraphQLUtils';
import * as schemaDirectives from './loaders/schemaDirectives'; import * as schemaDirectives from './loaders/schemaDirectives';
import * as schemaTypes from './loaders/schemaTypes'; import * as schemaTypes from './loaders/schemaTypes';
import { getFunctionNames } from '../triggers'; import { getFunctionNames } from '../triggers';
import * as defaultRelaySchema from './loaders/defaultRelaySchema';
const RESERVED_GRAPHQL_TYPE_NAMES = [ const RESERVED_GRAPHQL_TYPE_NAMES = [
'String', 'String',
@@ -27,10 +33,25 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [
'Query', 'Query',
'Mutation', 'Mutation',
'Subscription', 'Subscription',
'CreateFileInput',
'CreateFilePayload',
'Viewer', 'Viewer',
'SignUpFieldsInput', 'SignUpInput',
'LogInFieldsInput', 'SignUpPayload',
'LogInInput',
'LogInPayload',
'LogOutInput',
'LogOutPayload',
'CloudCodeFunction', 'CloudCodeFunction',
'CallCloudCodeInput',
'CallCloudCodePayload',
'CreateClassInput',
'CreateClassPayload',
'UpdateClassInput',
'UpdateClassPayload',
'DeleteClassInput',
'DeleteClassPayload',
'PageInfo',
]; ];
const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes']; const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes'];
const RESERVED_GRAPHQL_MUTATION_NAMES = [ const RESERVED_GRAPHQL_MUTATION_NAMES = [
@@ -48,7 +69,14 @@ class ParseGraphQLSchema {
databaseController: DatabaseController; databaseController: DatabaseController;
parseGraphQLController: ParseGraphQLController; parseGraphQLController: ParseGraphQLController;
parseGraphQLConfig: ParseGraphQLConfig; parseGraphQLConfig: ParseGraphQLConfig;
graphQLCustomTypeDefs: any; log: any;
appId: string;
graphQLCustomTypeDefs: ?(
| string
| GraphQLSchema
| DocumentNode
| GraphQLNamedType[]
);
constructor( constructor(
params: { params: {
@@ -56,6 +84,12 @@ class ParseGraphQLSchema {
parseGraphQLController: ParseGraphQLController, parseGraphQLController: ParseGraphQLController,
log: any, log: any,
appId: string, appId: string,
graphQLCustomTypeDefs: ?(
| string
| GraphQLSchema
| DocumentNode
| GraphQLNamedType[]
),
} = {} } = {}
) { ) {
this.parseGraphQLController = this.parseGraphQLController =
@@ -105,8 +139,10 @@ class ParseGraphQLSchema {
this.graphQLSubscriptions = {}; this.graphQLSubscriptions = {};
this.graphQLSchemaDirectivesDefinitions = null; this.graphQLSchemaDirectivesDefinitions = null;
this.graphQLSchemaDirectives = {}; this.graphQLSchemaDirectives = {};
this.relayNodeInterface = null;
defaultGraphQLTypes.load(this); defaultGraphQLTypes.load(this);
defaultRelaySchema.load(this);
schemaTypes.load(this); schemaTypes.load(this);
this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach( this._getParseClassesWithConfig(parseClasses, parseGraphQLConfig).forEach(
@@ -208,10 +244,16 @@ class ParseGraphQLSchema {
return this.graphQLSchema; return this.graphQLSchema;
} }
addGraphQLType(type, throwError = false, ignoreReserved = false) { addGraphQLType(
type,
throwError = false,
ignoreReserved = false,
ignoreConnection = false
) {
if ( if (
(!ignoreReserved && RESERVED_GRAPHQL_TYPE_NAMES.includes(type.name)) || (!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.`; const message = `Type ${type.name} could not be added to the auto schema because it collided with an existing type.`;
if (throwError) { if (throwError) {

View File

@@ -1,4 +1,5 @@
import Parse from 'parse/node'; import Parse from 'parse/node';
import { offsetToCursor, cursorToOffset } from 'graphql-relay';
import rest from '../../rest'; import rest from '../../rest';
import { transformQueryInputToParse } from '../transformers/query'; import { transformQueryInputToParse } from '../transformers/query';
@@ -51,8 +52,11 @@ const findObjects = async (
className, className,
where, where,
order, order,
skip, skipInput,
limit, first,
after,
last,
before,
keys, keys,
include, include,
includeAll, includeAll,
@@ -68,11 +72,52 @@ const findObjects = async (
if (!where) { if (!where) {
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 = {}; const options = {};
if (selectedFields.includes('results')) { if (
selectedFields.find(
field => field.startsWith('edges.') || field.startsWith('pageInfo.')
)
) {
if (limit || limit === 0) { if (limit || limit === 0) {
options.limit = limit; options.limit = limit;
} }
@@ -104,7 +149,12 @@ const findObjects = async (
options.limit = 0; options.limit = 0;
} }
if (selectedFields.includes('count')) { if (
(selectedFields.includes('count') ||
selectedFields.includes('pageInfo.hasPreviousPage') ||
selectedFields.includes('pageInfo.hasNextPage')) &&
!needToPreCount
) {
options.count = true; options.count = true;
} }
@@ -115,7 +165,151 @@ const findObjects = async (
options.subqueryReadPreference = subqueryReadPreference; 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), 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 = { const OBJECT_ID_ATT = {
description: 'This is the object id.', description: 'This is the object id.',
type: OBJECT_ID, type: OBJECT_ID,
resolve: ({ objectId }) => objectId,
}; };
const CREATED_AT_ATT = { const CREATED_AT_ATT = {
@@ -611,7 +616,7 @@ const INPUT_FIELDS = {
}; };
const CREATE_RESULT_FIELDS = { const CREATE_RESULT_FIELDS = {
id: OBJECT_ID_ATT, objectId: OBJECT_ID_ATT,
createdAt: CREATED_AT_ATT, createdAt: CREATED_AT_ATT,
}; };
@@ -637,7 +642,7 @@ const PARSE_OBJECT = new GraphQLInterfaceType({
}); });
const SESSION_TOKEN_ATT = { const SESSION_TOKEN_ATT = {
description: 'The user session token', description: 'The current user session token.',
type: new GraphQLNonNull(GraphQLString), type: new GraphQLNonNull(GraphQLString),
}; };
@@ -926,6 +931,25 @@ const options = {
type: GraphQLString, 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({ const STRING_WHERE_INPUT = new GraphQLInputObjectType({
name: 'StringWhereInput', name: 'StringWhereInput',
description: 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({ const ELEMENT = new GraphQLObjectType({
name: 'Element', name: 'Element',
description: "The Element object type is used to return array items' value.", 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(CENTER_SPHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(GEO_WITHIN_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_WITHIN_INPUT, true);
parseGraphQLSchema.addGraphQLType(GEO_INTERSECTS_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_INTERSECTS_INPUT, true);
parseGraphQLSchema.addGraphQLType(ID_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(STRING_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(STRING_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(NUMBER_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(NUMBER_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(BOOLEAN_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(BOOLEAN_WHERE_INPUT, true);
@@ -1258,9 +1270,7 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLType(FILE_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(FILE_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(GEO_POINT_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(GEO_POINT_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(POLYGON_WHERE_INPUT, true); parseGraphQLSchema.addGraphQLType(POLYGON_WHERE_INPUT, true);
parseGraphQLSchema.addGraphQLType(FIND_RESULT, true);
parseGraphQLSchema.addGraphQLType(ELEMENT, true); parseGraphQLSchema.addGraphQLType(ELEMENT, true);
parseGraphQLSchema.addGraphQLType(OBJECT_ID, true);
parseGraphQLSchema.addGraphQLType(ACL_INPUT, true); parseGraphQLSchema.addGraphQLType(ACL_INPUT, true);
parseGraphQLSchema.addGraphQLType(USER_ACL_INPUT, true); parseGraphQLSchema.addGraphQLType(USER_ACL_INPUT, true);
parseGraphQLSchema.addGraphQLType(ROLE_ACL_INPUT, true); parseGraphQLSchema.addGraphQLType(ROLE_ACL_INPUT, true);
@@ -1296,6 +1306,7 @@ export {
POLYGON, POLYGON,
OBJECT_ID, OBJECT_ID,
CLASS_NAME_ATT, CLASS_NAME_ATT,
GLOBAL_OR_OBJECT_ID_ATT,
OBJECT_ID_ATT, OBJECT_ID_ATT,
UPDATED_AT_ATT, UPDATED_AT_ATT,
CREATED_AT_ATT, CREATED_AT_ATT,
@@ -1337,6 +1348,7 @@ export {
notInQueryKey, notInQueryKey,
matchesRegex, matchesRegex,
options, options,
ID_WHERE_INPUT,
STRING_WHERE_INPUT, STRING_WHERE_INPUT,
NUMBER_WHERE_INPUT, NUMBER_WHERE_INPUT,
BOOLEAN_WHERE_INPUT, BOOLEAN_WHERE_INPUT,
@@ -1348,7 +1360,6 @@ export {
FILE_WHERE_INPUT, FILE_WHERE_INPUT,
GEO_POINT_WHERE_INPUT, GEO_POINT_WHERE_INPUT,
POLYGON_WHERE_INPUT, POLYGON_WHERE_INPUT,
FIND_RESULT,
ARRAY_RESULT, ARRAY_RESULT,
ELEMENT, ELEMENT,
ACL_INPUT, 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 { GraphQLNonNull } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import { GraphQLUpload } from 'graphql-upload'; import { GraphQLUpload } from 'graphql-upload';
import Parse from 'parse/node'; import Parse from 'parse/node';
import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import logger from '../../logger'; import logger from '../../logger';
const load = parseGraphQLSchema => { const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLMutation( const createMutation = mutationWithClientMutationId({
'createFile', name: 'CreateFile',
{ description:
description: 'The createFile mutation can be used to create and upload a new file.',
'The create mutation can be used to create and upload a new file.', inputFields: {
args: { upload: {
upload: { description: 'This is the new file to be created and uploaded.',
description: 'This is the new file to be created and uploaded', type: new GraphQLNonNull(GraphQLUpload),
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 { try {
const { upload } = args; return {
const { config } = context; fileInfo: await config.filesController.createFile(
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(
config, config,
filename, filename,
data, data,
mimetype 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) { } 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,
true true
); );

View File

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

View File

@@ -1,4 +1,5 @@
import { GraphQLNonNull } from 'graphql'; import { GraphQLNonNull } from 'graphql';
import { fromGlobalId, mutationWithClientMutationId } from 'graphql-relay';
import getFieldNames from 'graphql-list-fields'; import getFieldNames from 'graphql-list-fields';
import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as defaultGraphQLTypes from './defaultGraphQLTypes';
import { import {
@@ -17,13 +18,16 @@ const getOnlyRequiredFields = (
includedFieldsString, includedFieldsString,
nativeObjectFields nativeObjectFields
) => { ) => {
const includedFields = includedFieldsString.split(','); const includedFields = includedFieldsString
const selectedFields = selectedFieldsString.split(','); ? includedFieldsString.split(',')
: [];
const selectedFields = selectedFieldsString
? selectedFieldsString.split(',')
: [];
const missingFields = selectedFields const missingFields = selectedFields
.filter( .filter(
field => field =>
!nativeObjectFields.includes(field) || !nativeObjectFields.includes(field) || includedFields.includes(field)
includedFields.includes(field)
) )
.join(','); .join(',');
if (!missingFields.length) { if (!missingFields.length) {
@@ -40,6 +44,8 @@ const load = function(
) { ) {
const className = parseClass.className; const className = parseClass.className;
const graphQLClassName = transformClassNameToGraphQL(className); const graphQLClassName = transformClassNameToGraphQL(className);
const getGraphQLQueryName =
graphQLClassName.charAt(0).toLowerCase() + graphQLClassName.slice(1);
const { const {
create: isCreateEnabled = true, create: isCreateEnabled = true,
@@ -55,18 +61,25 @@ const load = function(
if (isCreateEnabled) { if (isCreateEnabled) {
const createGraphQLMutationName = `create${graphQLClassName}`; 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.`, description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${graphQLClassName} class.`,
args: { inputFields: {
fields: { 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: classGraphQLCreateType || defaultGraphQLTypes.OBJECT,
}, },
}, },
type: new GraphQLNonNull( outputFields: {
classGraphQLOutputType || defaultGraphQLTypes.OBJECT [getGraphQLQueryName]: {
), description: 'This is the created object.',
async resolve(_source, args, context, mutationInfo) { type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
},
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try { try {
let { fields } = args; let { fields } = args;
if (!fields) fields = {}; if (!fields) fields = {};
@@ -85,13 +98,15 @@ const load = function(
auth, auth,
info 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, include } = extractKeysAndInclude(selectedFields);
const { keys: requiredKeys, needGet } = getOnlyRequiredFields( const { keys: requiredKeys, needGet } = getOnlyRequiredFields(
fields, fields,
keys, keys,
include, include,
['id', 'createdAt', 'updatedAt'] ['id', 'objectId', 'createdAt', 'updatedAt']
); );
let optimizedObject = {}; let optimizedObject = {};
if (needGet) { if (needGet) {
@@ -108,37 +123,65 @@ const load = function(
); );
} }
return { return {
...createdObject, [getGraphQLQueryName]: {
updatedAt: createdObject.createdAt, ...createdObject,
...parseFields, updatedAt: createdObject.createdAt,
...optimizedObject, ...parseFields,
...optimizedObject,
},
}; };
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(e); parseGraphQLSchema.handleError(e);
} }
}, },
}); });
if (
parseGraphQLSchema.addGraphQLType(
createGraphQLMutation.args.input.type.ofType
) &&
parseGraphQLSchema.addGraphQLType(createGraphQLMutation.type)
) {
parseGraphQLSchema.addGraphQLMutation(
createGraphQLMutationName,
createGraphQLMutation
);
}
} }
if (isUpdateEnabled) { if (isUpdateEnabled) {
const updateGraphQLMutationName = `update${graphQLClassName}`; 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.`, description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${graphQLClassName} class.`,
args: { inputFields: {
id: defaultGraphQLTypes.OBJECT_ID_ATT, id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT,
fields: { 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: classGraphQLUpdateType || defaultGraphQLTypes.OBJECT,
}, },
}, },
type: new GraphQLNonNull( outputFields: {
classGraphQLOutputType || defaultGraphQLTypes.OBJECT [getGraphQLQueryName]: {
), description: 'This is the updated object.',
async resolve(_source, args, context, mutationInfo) { type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
},
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try { try {
const { id, fields } = args; let { id, fields } = args;
if (!fields) fields = {};
const { config, auth, info } = context; const { config, auth, info } = context;
const globalIdObject = fromGlobalId(id);
if (globalIdObject.type === className) {
id = globalIdObject.id;
}
const parseFields = await transformTypes('update', fields, { const parseFields = await transformTypes('update', fields, {
className, className,
parseGraphQLSchema, parseGraphQLSchema,
@@ -154,15 +197,17 @@ const load = function(
info 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, include } = extractKeysAndInclude(selectedFields);
const { keys: requiredKeys, needGet } = getOnlyRequiredFields( const { keys: requiredKeys, needGet } = getOnlyRequiredFields(
fields, fields,
keys, keys,
include, include,
['id', 'updatedAt'] ['id', 'objectId', 'updatedAt']
); );
let optimizedObject = {}; let optimizedObject = {};
if (needGet) { if (needGet) {
optimizedObject = await objectsQueries.getObject( optimizedObject = await objectsQueries.getObject(
@@ -178,38 +223,69 @@ const load = function(
); );
} }
return { return {
id, [getGraphQLQueryName]: {
...updatedObject, objectId: id,
...parseFields, ...updatedObject,
...optimizedObject, ...parseFields,
...optimizedObject,
},
}; };
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(e); parseGraphQLSchema.handleError(e);
} }
}, },
}); });
if (
parseGraphQLSchema.addGraphQLType(
updateGraphQLMutation.args.input.type.ofType
) &&
parseGraphQLSchema.addGraphQLType(updateGraphQLMutation.type)
) {
parseGraphQLSchema.addGraphQLMutation(
updateGraphQLMutationName,
updateGraphQLMutation
);
}
} }
if (isDestroyEnabled) { if (isDestroyEnabled) {
const deleteGraphQLMutationName = `delete${graphQLClassName}`; 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.`, description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${graphQLClassName} class.`,
args: { inputFields: {
id: defaultGraphQLTypes.OBJECT_ID_ATT, id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT,
}, },
type: new GraphQLNonNull( outputFields: {
classGraphQLOutputType || defaultGraphQLTypes.OBJECT [getGraphQLQueryName]: {
), description: 'This is the deleted object.',
async resolve(_source, args, context, mutationInfo) { type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT
),
},
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try { try {
const { id } = args; let { id } = args;
const { config, auth, info } = context; 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 = {}; let optimizedObject = {};
const splitedKeys = keys.split(','); if (
if (splitedKeys.length > 1 || splitedKeys[0] !== 'id') { keys &&
keys.split(',').filter(key => !['id', 'objectId'].includes(key))
.length > 0
) {
optimizedObject = await objectsQueries.getObject( optimizedObject = await objectsQueries.getObject(
className, className,
id, id,
@@ -229,12 +305,29 @@ const load = function(
auth, auth,
info info
); );
return { id, ...optimizedObject }; return {
[getGraphQLQueryName]: {
objectId: id,
...optimizedObject,
},
};
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(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 { GraphQLNonNull } from 'graphql';
import { fromGlobalId } from 'graphql-relay';
import getFieldNames from 'graphql-list-fields'; import getFieldNames from 'graphql-list-fields';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import * as defaultGraphQLTypes from './defaultGraphQLTypes'; import * as defaultGraphQLTypes from './defaultGraphQLTypes';
@@ -14,11 +15,18 @@ const getParseClassQueryConfig = function(
}; };
const getQuery = async (className, _source, args, context, queryInfo) => { const getQuery = async (className, _source, args, context, queryInfo) => {
const { id, options } = args; let { id } = args;
const { options } = args;
const { readPreference, includeReadPreference } = options || {}; const { readPreference, includeReadPreference } = options || {};
const { config, auth, info } = context; const { config, auth, info } = context;
const selectedFields = getFieldNames(queryInfo); const selectedFields = getFieldNames(queryInfo);
const globalIdObject = fromGlobalId(id);
if (globalIdObject.type === className) {
id = globalIdObject.id;
}
const { keys, include } = extractKeysAndInclude(selectedFields); const { keys, include } = extractKeysAndInclude(selectedFields);
return await objectsQueries.getObject( return await objectsQueries.getObject(
@@ -58,7 +66,7 @@ const load = function(
parseGraphQLSchema.addGraphQLQuery(getGraphQLQueryName, { parseGraphQLSchema.addGraphQLQuery(getGraphQLQueryName, {
description: `The ${getGraphQLQueryName} query can be used to get an object of the ${graphQLClassName} class by its id.`, description: `The ${getGraphQLQueryName} query can be used to get an object of the ${graphQLClassName} class by its id.`,
args: { args: {
id: defaultGraphQLTypes.OBJECT_ID_ATT, id: defaultGraphQLTypes.GLOBAL_OR_OBJECT_ID_ATT,
options: defaultGraphQLTypes.READ_OPTIONS_ATT, options: defaultGraphQLTypes.READ_OPTIONS_ATT,
}, },
type: new GraphQLNonNull( type: new GraphQLNonNull(
@@ -82,11 +90,20 @@ const load = function(
description: `The ${findGraphQLQueryName} query can be used to find objects of the ${graphQLClassName} class.`, description: `The ${findGraphQLQueryName} query can be used to find objects of the ${graphQLClassName} class.`,
args: classGraphQLFindArgs, args: classGraphQLFindArgs,
type: new GraphQLNonNull( type: new GraphQLNonNull(
classGraphQLFindResultType || defaultGraphQLTypes.FIND_RESULT classGraphQLFindResultType || defaultGraphQLTypes.OBJECT
), ),
async resolve(_source, args, context, queryInfo) { async resolve(_source, args, context, queryInfo) {
try { try {
const { where, order, skip, limit, options } = args; const {
where,
order,
skip,
first,
after,
last,
before,
options,
} = args;
const { const {
readPreference, readPreference,
includeReadPreference, includeReadPreference,
@@ -97,8 +114,8 @@ const load = function(
const { keys, include } = extractKeysAndInclude( const { keys, include } = extractKeysAndInclude(
selectedFields selectedFields
.filter(field => field.includes('.')) .filter(field => field.startsWith('edges.node.'))
.map(field => field.slice(field.indexOf('.') + 1)) .map(field => field.replace('edges.node.', ''))
); );
const parseOrder = order && order.join(','); const parseOrder = order && order.join(',');
@@ -107,7 +124,10 @@ const load = function(
where, where,
parseOrder, parseOrder,
skip, skip,
limit, first,
after,
last,
before,
keys, keys,
include, include,
false, false,
@@ -117,7 +137,7 @@ const load = function(
config, config,
auth, auth,
info, info,
selectedFields.map(field => field.split('.', 1)[0]), selectedFields,
parseClass.fields parseClass.fields
); );
} catch (e) { } catch (e) {

View File

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

View File

@@ -1,5 +1,6 @@
import Parse from 'parse/node'; import Parse from 'parse/node';
import { GraphQLNonNull } from 'graphql'; import { GraphQLNonNull } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import * as schemaTypes from './schemaTypes'; import * as schemaTypes from './schemaTypes';
import { import {
transformToParse, transformToParse,
@@ -9,135 +10,183 @@ import { enforceMasterKeyAccess } from '../parseGraphQLUtils';
import { getClass } from './schemaQueries'; import { getClass } from './schemaQueries';
const load = parseGraphQLSchema => { 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( parseGraphQLSchema.addGraphQLMutation(
'createClass', 'createClass',
{ createClassMutation,
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);
}
},
},
true, true,
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( parseGraphQLSchema.addGraphQLMutation(
'updateClass', 'updateClass',
{ updateClassMutation,
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);
}
},
},
true, true,
true true
); );
parseGraphQLSchema.addGraphQLMutation( const deleteClassMutation = mutationWithClientMutationId({
'deleteClass', name: 'DeleteClass',
{ description:
description: 'The deleteClass mutation can be used to delete an existing object class.',
'The deleteClass mutation can be used to delete an existing object class.', inputFields: {
args: { name: schemaTypes.CLASS_NAME_ATT,
name: schemaTypes.CLASS_NAME_ATT, },
}, outputFields: {
type: new GraphQLNonNull(schemaTypes.CLASS), class: {
resolve: async (_source, args, context) => { description: 'This is the deleted class.',
try { type: new GraphQLNonNull(schemaTypes.CLASS),
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);
}
}, },
}, },
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,
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 UsersRouter from '../../Routers/UsersRouter';
import * as objectsMutations from '../helpers/objectsMutations'; import * as objectsMutations from '../helpers/objectsMutations';
import { getUserFromSessionToken } from './usersQueries'; import { getUserFromSessionToken } from './usersQueries';
@@ -10,110 +11,166 @@ const load = parseGraphQLSchema => {
return; return;
} }
parseGraphQLSchema.addGraphQLMutation( const signUpMutation = mutationWithClientMutationId({
'signUp', name: 'SignUp',
{ description:
description: 'The signUp mutation can be used to sign the user up.', 'The signUp mutation can be used to create and sign up a new user.',
args: { inputFields: {
fields: { userFields: {
descriptions: 'These are the fields of the user.', descriptions:
type: parseGraphQLSchema.parseClassTypes['_User'].signUpInputType, 'These are the fields of the new user to be created and signed up.',
}, type:
}, parseGraphQLSchema.parseClassTypes['_User'].classGraphQLCreateType,
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);
}
}, },
}, },
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,
true true
); );
parseGraphQLSchema.addGraphQLType(signUpMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation('signUp', signUpMutation, true, true);
parseGraphQLSchema.addGraphQLMutation( const logInMutation = mutationWithClientMutationId({
'logIn', name: 'LogIn',
{ description: 'The logIn mutation can be used to log in an existing user.',
description: 'The logIn mutation can be used to log the user in.', inputFields: {
args: { username: {
fields: { description: 'This is the username used to log in the user.',
description: 'This is data needed to login', type: new GraphQLNonNull(GraphQLString),
type: parseGraphQLSchema.parseClassTypes['_User'].logInInputType,
},
}, },
type: new GraphQLNonNull(parseGraphQLSchema.viewerType), password: {
async resolve(_source, args, context) { description: 'This is the password used to log in the user.',
try { type: new GraphQLNonNull(GraphQLString),
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);
}
}, },
}, },
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,
true true
); );
parseGraphQLSchema.addGraphQLType(logInMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation('logIn', logInMutation, true, true);
parseGraphQLSchema.addGraphQLMutation( const logOutMutation = mutationWithClientMutationId({
'logOut', name: 'LogOut',
{ description: 'The logOut mutation can be used to log out an existing user.',
description: 'The logOut mutation can be used to log the user out.', outputFields: {
type: new GraphQLNonNull(parseGraphQLSchema.viewerType), viewer: {
async resolve(_source, _args, context, mutationInfo) { description:
try { 'This is the existing user that was logged out and returned as a viewer.',
const { config, auth, info } = context; type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
const viewer = await getUserFromSessionToken(
config,
info,
mutationInfo
);
await usersRouter.handleLogOut({
config,
auth,
info,
});
return viewer;
} catch (e) {
parseGraphQLSchema.handleError(e);
}
}, },
}, },
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,
true true
); );
parseGraphQLSchema.addGraphQLType(logOutMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation('logOut', logOutMutation, true, true);
}; };
export { load }; export { load };

View File

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

View File

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

View File

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

View File

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