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

1797
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,23 +1,28 @@
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 create mutation can be used to create and upload a new file.', 'The createFile mutation can be used to create and upload a new file.',
args: { inputFields: {
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),
}, },
}, },
outputFields: {
fileInfo: {
description: 'This is the created file info.',
type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO), type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO),
async resolve(_source, args, context) { },
},
mutateAndGetPayload: async (args, context) => {
try { try {
const { upload } = args; const { upload } = args;
const { config } = context; const { config } = context;
@@ -57,12 +62,14 @@ const load = parseGraphQLSchema => {
} }
try { try {
return await config.filesController.createFile( return {
fileInfo: await config.filesController.createFile(
config, config,
filename, filename,
data, data,
mimetype mimetype
); ),
};
} catch (e) { } catch (e) {
logger.error('Error creating a file: ', e); logger.error('Error creating a file: ', e);
throw new Parse.Error( throw new Parse.Error(
@@ -74,7 +81,17 @@ const load = parseGraphQLSchema => {
parseGraphQLSchema.handleError(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,12 +22,11 @@ const load = parseGraphQLSchema => {
true true
); );
parseGraphQLSchema.addGraphQLMutation( const callCloudCodeMutation = mutationWithClientMutationId({
'callCloudCode', name: 'CallCloudCode',
{
description: description:
'The call mutation can be used to invoke a cloud code function.', 'The callCloudCode mutation can be used to invoke a cloud code function.',
args: { inputFields: {
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),
@@ -36,13 +36,20 @@ const load = parseGraphQLSchema => {
type: defaultGraphQLTypes.OBJECT, type: defaultGraphQLTypes.OBJECT,
}, },
}, },
outputFields: {
result: {
description:
'This is the result value of the cloud code function execution.',
type: defaultGraphQLTypes.ANY, type: defaultGraphQLTypes.ANY,
async resolve(_source, args, context) { },
},
mutateAndGetPayload: async (args, context) => {
try { try {
const { functionName, params } = args; const { functionName, params } = args;
const { config, auth, info } = context; 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) { } catch (e) {
parseGraphQLSchema.handleError(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,
}, },
}, },
outputFields: {
[getGraphQLQueryName]: {
description: 'This is the created object.',
type: new GraphQLNonNull( type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT classGraphQLOutputType || defaultGraphQLTypes.OBJECT
), ),
async resolve(_source, args, context, mutationInfo) { },
},
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 {
[getGraphQLQueryName]: {
...createdObject, ...createdObject,
updatedAt: createdObject.createdAt, updatedAt: createdObject.createdAt,
...parseFields, ...parseFields,
...optimizedObject, ...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,
}, },
}, },
outputFields: {
[getGraphQLQueryName]: {
description: 'This is the updated object.',
type: new GraphQLNonNull( type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT classGraphQLOutputType || defaultGraphQLTypes.OBJECT
), ),
async resolve(_source, args, context, mutationInfo) { },
},
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]: {
objectId: id,
...updatedObject, ...updatedObject,
...parseFields, ...parseFields,
...optimizedObject, ...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,
}, },
outputFields: {
[getGraphQLQueryName]: {
description: 'This is the deleted object.',
type: new GraphQLNonNull( type: new GraphQLNonNull(
classGraphQLOutputType || defaultGraphQLTypes.OBJECT classGraphQLOutputType || defaultGraphQLTypes.OBJECT
), ),
async resolve(_source, args, context, mutationInfo) { },
},
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,20 +10,24 @@ import { enforceMasterKeyAccess } from '../parseGraphQLUtils';
import { getClass } from './schemaQueries'; import { getClass } from './schemaQueries';
const load = parseGraphQLSchema => { const load = parseGraphQLSchema => {
parseGraphQLSchema.addGraphQLMutation( const createClassMutation = mutationWithClientMutationId({
'createClass', name: 'CreateClass',
{
description: description:
'The createClass mutation can be used to create the schema for a new object class.', 'The createClass mutation can be used to create the schema for a new object class.',
args: { inputFields: {
name: schemaTypes.CLASS_NAME_ATT, name: schemaTypes.CLASS_NAME_ATT,
schemaFields: { schemaFields: {
description: "These are the schema's fields of the object class.", description: "These are the schema's fields of the object class.",
type: schemaTypes.SCHEMA_FIELDS_INPUT, type: schemaTypes.SCHEMA_FIELDS_INPUT,
}, },
}, },
outputFields: {
class: {
description: 'This is the created class.',
type: new GraphQLNonNull(schemaTypes.CLASS), type: new GraphQLNonNull(schemaTypes.CLASS),
resolve: async (_source, args, context) => { },
},
mutateAndGetPayload: async (args, context) => {
try { try {
const { name, schemaFields } = args; const { name, schemaFields } = args;
const { config, auth } = context; const { config, auth } = context;
@@ -42,32 +47,48 @@ const load = parseGraphQLSchema => {
transformToParse(schemaFields) transformToParse(schemaFields)
); );
return { return {
class: {
name: parseClass.className, name: parseClass.className,
schemaFields: transformToGraphQL(parseClass.fields), schemaFields: transformToGraphQL(parseClass.fields),
},
}; };
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(e); parseGraphQLSchema.handleError(e);
} }
}, },
}, });
parseGraphQLSchema.addGraphQLType(
createClassMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(createClassMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation(
'createClass',
createClassMutation,
true, true,
true true
); );
parseGraphQLSchema.addGraphQLMutation( const updateClassMutation = mutationWithClientMutationId({
'updateClass', name: 'UpdateClass',
{
description: description:
'The updateClass mutation can be used to update the schema for an existing object class.', 'The updateClass mutation can be used to update the schema for an existing object class.',
args: { inputFields: {
name: schemaTypes.CLASS_NAME_ATT, name: schemaTypes.CLASS_NAME_ATT,
schemaFields: { schemaFields: {
description: "These are the schema's fields of the object class.", description: "These are the schema's fields of the object class.",
type: schemaTypes.SCHEMA_FIELDS_INPUT, type: schemaTypes.SCHEMA_FIELDS_INPUT,
}, },
}, },
outputFields: {
class: {
description: 'This is the updated class.',
type: new GraphQLNonNull(schemaTypes.CLASS), type: new GraphQLNonNull(schemaTypes.CLASS),
resolve: async (_source, args, context) => { },
},
mutateAndGetPayload: async (args, context) => {
try { try {
const { name, schemaFields } = args; const { name, schemaFields } = args;
const { config, auth } = context; const { config, auth } = context;
@@ -91,28 +112,44 @@ const load = parseGraphQLSchema => {
config.database config.database
); );
return { return {
class: {
name: parseClass.className, name: parseClass.className,
schemaFields: transformToGraphQL(parseClass.fields), schemaFields: transformToGraphQL(parseClass.fields),
},
}; };
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(e); parseGraphQLSchema.handleError(e);
} }
}, },
}, });
parseGraphQLSchema.addGraphQLType(
updateClassMutation.args.input.type.ofType,
true,
true
);
parseGraphQLSchema.addGraphQLType(updateClassMutation.type, true, true);
parseGraphQLSchema.addGraphQLMutation(
'updateClass',
updateClassMutation,
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.',
args: { inputFields: {
name: schemaTypes.CLASS_NAME_ATT, name: schemaTypes.CLASS_NAME_ATT,
}, },
outputFields: {
class: {
description: 'This is the deleted class.',
type: new GraphQLNonNull(schemaTypes.CLASS), type: new GraphQLNonNull(schemaTypes.CLASS),
resolve: async (_source, args, context) => { },
},
mutateAndGetPayload: async (args, context) => {
try { try {
const { name } = args; const { name } = args;
const { config, auth } = context; const { config, auth } = context;
@@ -130,14 +167,26 @@ const load = parseGraphQLSchema => {
const existingParseClass = await getClass(name, schema); const existingParseClass = await getClass(name, schema);
await config.database.deleteSchema(name); await config.database.deleteSchema(name);
return { return {
class: {
name: existingParseClass.className, name: existingParseClass.className,
schemaFields: transformToGraphQL(existingParseClass.fields), schemaFields: transformToGraphQL(existingParseClass.fields),
},
}; };
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(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,26 +11,33 @@ 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,
}, },
}, },
outputFields: {
viewer: {
description:
'This is the new user that was created, signed up and returned as a viewer.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType), type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, args, context, mutationInfo) { },
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try { try {
const { fields } = args; const { userFields } = args;
const { config, auth, info } = context; const { config, auth, info } = context;
const { sessionToken } = await objectsMutations.createObject( const { sessionToken } = await objectsMutations.createObject(
'_User', '_User',
fields, userFields,
config, config,
auth, auth,
info info
@@ -37,35 +45,55 @@ const load = parseGraphQLSchema => {
info.sessionToken = sessionToken; info.sessionToken = sessionToken;
return await getUserFromSessionToken(config, info, mutationInfo); return {
viewer: await getUserFromSessionToken(
config,
info,
mutationInfo,
'viewer.user.',
true
),
};
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(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, },
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), type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, args, context) { },
},
mutateAndGetPayload: async (args, context, mutationInfo) => {
try { try {
const { const { username, password } = args;
fields: { username, password },
} = args;
const { config, auth, info } = context; const { config, auth, info } = context;
return (await usersRouter.handleLogIn({ const { sessionToken } = (await usersRouter.handleLogIn({
body: { body: {
username, username,
password, password,
@@ -75,28 +103,52 @@ const load = parseGraphQLSchema => {
auth, auth,
info, info,
})).response; })).response;
info.sessionToken = sessionToken;
return {
viewer: await getUserFromSessionToken(
config,
info,
mutationInfo,
'viewer.user.',
true
),
};
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(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: {
viewer: {
description:
'This is the existing user that was logged out and returned as a viewer.',
type: new GraphQLNonNull(parseGraphQLSchema.viewerType), type: new GraphQLNonNull(parseGraphQLSchema.viewerType),
async resolve(_source, _args, context, mutationInfo) { },
},
mutateAndGetPayload: async (_args, context, mutationInfo) => {
try { try {
const { config, auth, info } = context; const { config, auth, info } = context;
const viewer = await getUserFromSessionToken( const viewer = await getUserFromSessionToken(
config, config,
info, info,
mutationInfo mutationInfo,
'viewer.user.',
true
); );
await usersRouter.handleLogOut({ await usersRouter.handleLogOut({
@@ -105,15 +157,20 @@ const load = parseGraphQLSchema => {
info, info,
}); });
return viewer; return { viewer };
} catch (e) { } catch (e) {
parseGraphQLSchema.handleError(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,7 +110,8 @@ 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(
Parse.Error.INVALID_POINTER,
`You need to provide at least one operation on the relation mutation of field ${field}` `You need to provide at least one operation on the relation mutation of field ${field}`
); );
@@ -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 => {
const globalIdObject = fromGlobalId(input);
if (globalIdObject.type === targetClass) {
input = globalIdObject.id;
}
return {
__type: 'Pointer', __type: 'Pointer',
className: targetClass, className: targetClass,
objectId: input, 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 => {
const globalIdObject = fromGlobalId(input);
if (globalIdObject.type === targetClass) {
input = globalIdObject.id;
}
return {
__type: 'Pointer', __type: 'Pointer',
className: targetClass, className: targetClass,
objectId: input, 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;
});
}
});
} }
} }