GraphQL Support (#5674)
* GraphQL boilerplate
* Create GraphQL schema without using gql
* Introducing loaders
* Generic create mutation
* create mutation is now working for any data type
* Create mutation for each parse class - partial
* Adding more data types to the class
* Get parse class query
* Generic get query
* Generic delete mutation
* Parse class delete mutation
* Parse class find mutation
* Generic update mutation
* Parse class update mutation
* Fixing initialization problems
* Installing node-fetch again
* Basic implementation for Pointer
* Constructor tests
* API tests boilerplate
* _getGraphQLOptions
* applyGraphQL tests
* GraphQL API initial tests
* applyPlayground tests
* createSubscriptions tests
* ParseGrapjQLSchema tests file
* ParseGraphQLSchema tests
* TypeValidationError
* TypeValidationError
* parseStringValue test
* parseIntValue tests
* parseBooleanValue tests
* parseDateValue tests
* parseValue tests
* parseListValues tests
* parseObjectFields tests
* Default types tests
* Get tests
* First permission test at generic Get operation
* Fixing prepare data
* ApolloClient does not work well with different queries runnning in paralell with different headers
* ApolloClient does not work well with different queries runnning in paralell with different headers
* User 3 tests
* User 3 tests
* Get level permission tests
* Get User specific tests
* Get now support keys argument
* Get now supports include argument
* Get now supports read preferences
* Adding tests for read preference enum type
* Find basic test
* Find permissions test
* Find where argument test
* Order, skip and limit tests
* Error handler
* Find now supports count
* Test for FindResult type
* Improving find count
* Find max limit test
* Find now supports keys, include and includeAll
* Find now supports read preferences
* Basic Create test
* Generic create mutation tests
* Basic update test
* UpdateResult object type test
* Update level permissions tests
* Error handler for default mutations
* Delete mutation basic test
* Delete mutation level permission tests
* Test for string
* String test
* Date test
* Pointer test
* Relation tests
* Changing objects mutations location
* Changing objects queries location
* Create file mutation
* Test for file fields
* Test for null values
* Changing parse classes operations location
* Objects mutations refactoring
* Class specific create object mutation now working
* Update class specific mutation now working
* Specific class delete mutation now working
* Get class specific mutation now working
* Find class specific query now working without where and sort
* Find query for custom classes working with where partially
* Almost all data types working for specfic class find where
* Now only missing relation, geopoint, file and ACL
* Additional tests with Parse classes queries and mutations
* Now only missing relation, geopoint, file and ACL
* Files
* Fiels are now working
* Excluding missing order test temporarly
* Refactoring dates
* Refactoring files
* Default types review
* Refeactoring object queries
* Refactoring class scalar type
* Refactoring class types
* Geo queries are now working
* Fixing centerSphere
* Allow sort on class specific queries
* Supporting bytes
* ACL constraint
* Temporarly removing xit tests
* Fixing some tests because of schema cache
* Removing session token from users
* Parse.User queries and mutations
* Remove test using fit
* Fixing include test that was failing because of schema cache
* Fixing count test for postgres. Postgres does not count with where={} (legacy problem). We should solve it later
* Fix null values test for postgres. It is evaluating null as undefined (legacy problem) and we should fix is later.
* Fixing schema change test that was failing because of schema cache
* Add GraphQL File type parseLiteral tests
* Refeactoring users
* Including sign up mutation
* Fix failing test
* Improve default GraphQL types tests coverage
* Including some tests for data types
* Including additional pointer test:
* Fixing some tests
* more data type tests
* Include Bytes and Polygon data types tests
* Polygons test
* Merging other tests
* Fixing some postgres tests
This commit is contained in:
committed by
GitHub
parent
922251a398
commit
fe2e95622f
117
src/GraphQL/ParseGraphQLSchema.js
Normal file
117
src/GraphQL/ParseGraphQLSchema.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import Parse from 'parse/node';
|
||||
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
|
||||
import { ApolloError } from 'apollo-server-core';
|
||||
import requiredParameter from '../requiredParameter';
|
||||
import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes';
|
||||
import * as parseClassTypes from './loaders/parseClassTypes';
|
||||
import * as parseClassQueries from './loaders/parseClassQueries';
|
||||
import * as parseClassMutations from './loaders/parseClassMutations';
|
||||
import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries';
|
||||
import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations';
|
||||
|
||||
class ParseGraphQLSchema {
|
||||
constructor(databaseController, log) {
|
||||
this.databaseController =
|
||||
databaseController ||
|
||||
requiredParameter('You must provide a databaseController instance!');
|
||||
this.log = log || requiredParameter('You must provide a log instance!');
|
||||
}
|
||||
|
||||
async load() {
|
||||
const schemaController = await this.databaseController.loadSchema();
|
||||
const parseClasses = await schemaController.getAllClasses();
|
||||
const parseClassesString = JSON.stringify(parseClasses);
|
||||
|
||||
if (this.graphQLSchema) {
|
||||
if (this.parseClasses === parseClasses) {
|
||||
return this.graphQLSchema;
|
||||
}
|
||||
|
||||
if (this.parseClassesString === parseClassesString) {
|
||||
this.parseClasses = parseClasses;
|
||||
return this.graphQLSchema;
|
||||
}
|
||||
}
|
||||
|
||||
this.parseClasses = parseClasses;
|
||||
this.parseClassesString = parseClassesString;
|
||||
this.parseClassTypes = {};
|
||||
this.meType = null;
|
||||
this.graphQLSchema = null;
|
||||
this.graphQLTypes = [];
|
||||
this.graphQLObjectsQueries = {};
|
||||
this.graphQLQueries = {};
|
||||
this.graphQLObjectsMutations = {};
|
||||
this.graphQLMutations = {};
|
||||
this.graphQLSubscriptions = {};
|
||||
|
||||
defaultGraphQLTypes.load(this);
|
||||
|
||||
parseClasses.forEach(parseClass => {
|
||||
parseClassTypes.load(this, parseClass);
|
||||
|
||||
parseClassQueries.load(this, parseClass);
|
||||
|
||||
parseClassMutations.load(this, parseClass);
|
||||
});
|
||||
|
||||
defaultGraphQLQueries.load(this);
|
||||
|
||||
defaultGraphQLMutations.load(this);
|
||||
|
||||
let graphQLQuery = undefined;
|
||||
if (Object.keys(this.graphQLQueries).length > 0) {
|
||||
graphQLQuery = new GraphQLObjectType({
|
||||
name: 'Query',
|
||||
description: 'Query is the top level type for queries.',
|
||||
fields: this.graphQLQueries,
|
||||
});
|
||||
this.graphQLTypes.push(graphQLQuery);
|
||||
}
|
||||
|
||||
let graphQLMutation = undefined;
|
||||
if (Object.keys(this.graphQLMutations).length > 0) {
|
||||
graphQLMutation = new GraphQLObjectType({
|
||||
name: 'Mutation',
|
||||
description: 'Mutation is the top level type for mutations.',
|
||||
fields: this.graphQLMutations,
|
||||
});
|
||||
this.graphQLTypes.push(graphQLMutation);
|
||||
}
|
||||
|
||||
let graphQLSubscription = undefined;
|
||||
if (Object.keys(this.graphQLSubscriptions).length > 0) {
|
||||
graphQLSubscription = new GraphQLObjectType({
|
||||
name: 'Subscription',
|
||||
description: 'Subscription is the top level type for subscriptions.',
|
||||
fields: this.graphQLSubscriptions,
|
||||
});
|
||||
this.graphQLTypes.push(graphQLSubscription);
|
||||
}
|
||||
|
||||
this.graphQLSchema = new GraphQLSchema({
|
||||
types: this.graphQLTypes,
|
||||
query: graphQLQuery,
|
||||
mutation: graphQLMutation,
|
||||
subscription: graphQLSubscription,
|
||||
});
|
||||
|
||||
return this.graphQLSchema;
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
let code, message;
|
||||
if (error instanceof Parse.Error) {
|
||||
this.log.error('Parse error: ', error);
|
||||
code = error.code;
|
||||
message = error.message;
|
||||
} else {
|
||||
this.log.error('Uncaught internal server error.', error, error.stack);
|
||||
code = Parse.Error.INTERNAL_SERVER_ERROR;
|
||||
message = 'Internal server error.';
|
||||
}
|
||||
throw new ApolloError(message, code);
|
||||
}
|
||||
}
|
||||
|
||||
export { ParseGraphQLSchema };
|
||||
110
src/GraphQL/ParseGraphQLServer.js
Normal file
110
src/GraphQL/ParseGraphQLServer.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import corsMiddleware from 'cors';
|
||||
import bodyParser from 'body-parser';
|
||||
import { graphqlUploadExpress } from 'graphql-upload';
|
||||
import { graphqlExpress } from 'apollo-server-express/dist/expressApollo';
|
||||
import { renderPlaygroundPage } from '@apollographql/graphql-playground-html';
|
||||
import { execute, subscribe } from 'graphql';
|
||||
import { SubscriptionServer } from 'subscriptions-transport-ws';
|
||||
import { handleParseHeaders } from '../middlewares';
|
||||
import requiredParameter from '../requiredParameter';
|
||||
import defaultLogger from '../logger';
|
||||
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
|
||||
|
||||
class ParseGraphQLServer {
|
||||
constructor(parseServer, config) {
|
||||
this.parseServer =
|
||||
parseServer ||
|
||||
requiredParameter('You must provide a parseServer instance!');
|
||||
if (!config || !config.graphQLPath) {
|
||||
requiredParameter('You must provide a config.graphQLPath!');
|
||||
}
|
||||
this.config = config;
|
||||
this.parseGraphQLSchema = new ParseGraphQLSchema(
|
||||
this.parseServer.config.databaseController,
|
||||
(this.parseServer.config && this.parseServer.config.loggerController) ||
|
||||
defaultLogger
|
||||
);
|
||||
}
|
||||
|
||||
async _getGraphQLOptions(req) {
|
||||
return {
|
||||
schema: await this.parseGraphQLSchema.load(),
|
||||
context: {
|
||||
info: req.info,
|
||||
config: req.config,
|
||||
auth: req.auth,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
applyGraphQL(app) {
|
||||
if (!app || !app.use) {
|
||||
requiredParameter('You must provide an Express.js app instance!');
|
||||
}
|
||||
|
||||
const maxUploadSize = this.parseServer.config.maxUploadSize || '20mb';
|
||||
const maxFileSize =
|
||||
(Number(maxUploadSize.slice(0, -2)) * 1024) ^
|
||||
{
|
||||
kb: 1,
|
||||
mb: 2,
|
||||
gb: 3,
|
||||
}[maxUploadSize.slice(-2).toLowerCase()];
|
||||
|
||||
app.use(this.config.graphQLPath, graphqlUploadExpress({ maxFileSize }));
|
||||
app.use(this.config.graphQLPath, corsMiddleware());
|
||||
app.use(this.config.graphQLPath, bodyParser.json());
|
||||
app.use(this.config.graphQLPath, handleParseHeaders);
|
||||
app.use(
|
||||
this.config.graphQLPath,
|
||||
graphqlExpress(async req => await this._getGraphQLOptions(req))
|
||||
);
|
||||
}
|
||||
|
||||
applyPlayground(app) {
|
||||
if (!app || !app.get) {
|
||||
requiredParameter('You must provide an Express.js app instance!');
|
||||
}
|
||||
app.get(
|
||||
this.config.playgroundPath ||
|
||||
requiredParameter(
|
||||
'You must provide a config.playgroundPath to applyPlayground!'
|
||||
),
|
||||
(_req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.write(
|
||||
renderPlaygroundPage({
|
||||
endpoint: this.config.graphQLPath,
|
||||
subscriptionEndpoint: this.config.subscriptionsPath,
|
||||
})
|
||||
);
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createSubscriptions(server) {
|
||||
SubscriptionServer.create(
|
||||
{
|
||||
execute,
|
||||
subscribe,
|
||||
onOperation: async (_message, params, webSocket) =>
|
||||
Object.assign(
|
||||
{},
|
||||
params,
|
||||
await this._getGraphQLOptions(webSocket.upgradeReq)
|
||||
),
|
||||
},
|
||||
{
|
||||
server,
|
||||
path:
|
||||
this.config.subscriptionsPath ||
|
||||
requiredParameter(
|
||||
'You must provide a config.subscriptionsPath to createSubscriptions!'
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ParseGraphQLServer };
|
||||
11
src/GraphQL/loaders/defaultGraphQLMutations.js
Normal file
11
src/GraphQL/loaders/defaultGraphQLMutations.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as objectsMutations from './objectsMutations';
|
||||
import * as filesMutations from './filesMutations';
|
||||
import * as usersMutations from './usersMutations';
|
||||
|
||||
const load = parseGraphQLSchema => {
|
||||
objectsMutations.load(parseGraphQLSchema);
|
||||
filesMutations.load(parseGraphQLSchema);
|
||||
usersMutations.load(parseGraphQLSchema);
|
||||
};
|
||||
|
||||
export { load };
|
||||
17
src/GraphQL/loaders/defaultGraphQLQueries.js
Normal file
17
src/GraphQL/loaders/defaultGraphQLQueries.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { GraphQLNonNull, GraphQLBoolean } from 'graphql';
|
||||
import * as objectsQueries from './objectsQueries';
|
||||
import * as usersQueries from './usersQueries';
|
||||
|
||||
const load = parseGraphQLSchema => {
|
||||
parseGraphQLSchema.graphQLQueries.health = {
|
||||
description:
|
||||
'The health query can be used to check if the server is up and running.',
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
resolve: () => true,
|
||||
};
|
||||
|
||||
objectsQueries.load(parseGraphQLSchema);
|
||||
usersQueries.load(parseGraphQLSchema);
|
||||
};
|
||||
|
||||
export { load };
|
||||
1125
src/GraphQL/loaders/defaultGraphQLTypes.js
Normal file
1125
src/GraphQL/loaders/defaultGraphQLTypes.js
Normal file
File diff suppressed because it is too large
Load Diff
93
src/GraphQL/loaders/filesMutations.js
Normal file
93
src/GraphQL/loaders/filesMutations.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { GraphQLObjectType, GraphQLNonNull } from 'graphql';
|
||||
import { GraphQLUpload } from 'graphql-upload';
|
||||
import Parse from 'parse/node';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import logger from '../../logger';
|
||||
|
||||
const load = parseGraphQLSchema => {
|
||||
const fields = {};
|
||||
|
||||
fields.create = {
|
||||
description:
|
||||
'The create mutation can be used to create and upload a new file.',
|
||||
args: {
|
||||
file: {
|
||||
description: 'This is the new file to be created and uploaded',
|
||||
type: new GraphQLNonNull(GraphQLUpload),
|
||||
},
|
||||
},
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.FILE_INFO),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { file } = args;
|
||||
const { config } = context;
|
||||
|
||||
const { createReadStream, filename, mimetype } = await file;
|
||||
let data = null;
|
||||
if (createReadStream) {
|
||||
const stream = createReadStream();
|
||||
data = await new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
stream
|
||||
.on('error', reject)
|
||||
.on('data', chunk => (data += chunk))
|
||||
.on('end', () => resolve(data));
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
filename,
|
||||
data,
|
||||
mimetype
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Error creating a file: ', e);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.FILE_SAVE_ERROR,
|
||||
`Could not store file: ${filename}.`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const filesMutation = new GraphQLObjectType({
|
||||
name: 'FilesMutation',
|
||||
description: 'FilesMutation is the top level type for files mutations.',
|
||||
fields,
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(filesMutation);
|
||||
|
||||
parseGraphQLSchema.graphQLMutations.files = {
|
||||
description: 'This is the top level for files mutations.',
|
||||
type: filesMutation,
|
||||
resolve: () => new Object(),
|
||||
};
|
||||
};
|
||||
|
||||
export { load };
|
||||
148
src/GraphQL/loaders/objectsMutations.js
Normal file
148
src/GraphQL/loaders/objectsMutations.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import { GraphQLNonNull, GraphQLBoolean, GraphQLObjectType } from 'graphql';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import rest from '../../rest';
|
||||
|
||||
const parseMap = {
|
||||
_op: '__op',
|
||||
};
|
||||
|
||||
const transformToParse = fields => {
|
||||
if (!fields || typeof fields !== 'object') {
|
||||
return;
|
||||
}
|
||||
Object.keys(fields).forEach(fieldName => {
|
||||
const fieldValue = fields[fieldName];
|
||||
if (parseMap[fieldName]) {
|
||||
delete fields[fieldName];
|
||||
fields[parseMap[fieldName]] = fieldValue;
|
||||
}
|
||||
if (typeof fieldValue === 'object') {
|
||||
transformToParse(fieldValue);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createObject = async (className, fields, config, auth, info) => {
|
||||
if (!fields) {
|
||||
fields = {};
|
||||
}
|
||||
|
||||
transformToParse(fields);
|
||||
|
||||
return (await rest.create(config, auth, className, fields, info.clientSDK))
|
||||
.response;
|
||||
};
|
||||
|
||||
const updateObject = async (
|
||||
className,
|
||||
objectId,
|
||||
fields,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
) => {
|
||||
if (!fields) {
|
||||
fields = {};
|
||||
}
|
||||
|
||||
transformToParse(fields);
|
||||
|
||||
return (await rest.update(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
{ objectId },
|
||||
fields,
|
||||
info.clientSDK
|
||||
)).response;
|
||||
};
|
||||
|
||||
const deleteObject = async (className, objectId, config, auth, info) => {
|
||||
await rest.del(config, auth, className, objectId, info.clientSDK);
|
||||
return true;
|
||||
};
|
||||
|
||||
const load = parseGraphQLSchema => {
|
||||
parseGraphQLSchema.graphQLObjectsMutations.create = {
|
||||
description:
|
||||
'The create mutation can be used to create a new object of a certain class.',
|
||||
args: {
|
||||
className: defaultGraphQLTypes.CLASS_NAME_ATT,
|
||||
fields: defaultGraphQLTypes.FIELDS_ATT,
|
||||
},
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { className, fields } = args;
|
||||
const { config, auth, info } = context;
|
||||
|
||||
return await createObject(className, fields, config, auth, info);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
parseGraphQLSchema.graphQLObjectsMutations.update = {
|
||||
description:
|
||||
'The update mutation can be used to update an object of a certain class.',
|
||||
args: {
|
||||
className: defaultGraphQLTypes.CLASS_NAME_ATT,
|
||||
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
|
||||
fields: defaultGraphQLTypes.FIELDS_ATT,
|
||||
},
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.UPDATE_RESULT),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { className, objectId, fields } = args;
|
||||
const { config, auth, info } = context;
|
||||
|
||||
return await updateObject(
|
||||
className,
|
||||
objectId,
|
||||
fields,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
parseGraphQLSchema.graphQLObjectsMutations.delete = {
|
||||
description:
|
||||
'The delete mutation can be used to delete an object of a certain class.',
|
||||
args: {
|
||||
className: defaultGraphQLTypes.CLASS_NAME_ATT,
|
||||
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
|
||||
},
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { className, objectId } = args;
|
||||
const { config, auth, info } = context;
|
||||
|
||||
return await deleteObject(className, objectId, config, auth, info);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const objectsMutation = new GraphQLObjectType({
|
||||
name: 'ObjectsMutation',
|
||||
description: 'ObjectsMutation is the top level type for objects mutations.',
|
||||
fields: parseGraphQLSchema.graphQLObjectsMutations,
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(objectsMutation);
|
||||
|
||||
parseGraphQLSchema.graphQLMutations.objects = {
|
||||
description: 'This is the top level for objects mutations.',
|
||||
type: objectsMutation,
|
||||
resolve: () => new Object(),
|
||||
};
|
||||
};
|
||||
|
||||
export { createObject, updateObject, deleteObject, load };
|
||||
367
src/GraphQL/loaders/objectsQueries.js
Normal file
367
src/GraphQL/loaders/objectsQueries.js
Normal file
@@ -0,0 +1,367 @@
|
||||
import {
|
||||
GraphQLNonNull,
|
||||
GraphQLBoolean,
|
||||
GraphQLString,
|
||||
GraphQLObjectType,
|
||||
} from 'graphql';
|
||||
import getFieldNames from 'graphql-list-fields';
|
||||
import Parse from 'parse/node';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import rest from '../../rest';
|
||||
|
||||
const getObject = async (
|
||||
className,
|
||||
objectId,
|
||||
keys,
|
||||
include,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
) => {
|
||||
const options = {};
|
||||
if (keys) {
|
||||
options.keys = keys;
|
||||
}
|
||||
if (include) {
|
||||
options.include = include;
|
||||
if (includeReadPreference) {
|
||||
options.includeReadPreference = includeReadPreference;
|
||||
}
|
||||
}
|
||||
if (readPreference) {
|
||||
options.readPreference = readPreference;
|
||||
}
|
||||
|
||||
const response = await rest.get(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
objectId,
|
||||
options,
|
||||
info.clientSDK
|
||||
);
|
||||
|
||||
if (!response.results || response.results.length == 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
}
|
||||
|
||||
if (className === '_User') {
|
||||
delete response.results[0].sessionToken;
|
||||
}
|
||||
|
||||
return response.results[0];
|
||||
};
|
||||
|
||||
const parseMap = {
|
||||
_or: '$or',
|
||||
_and: '$and',
|
||||
_nor: '$nor',
|
||||
_relatedTo: '$relatedTo',
|
||||
_eq: '$eq',
|
||||
_ne: '$ne',
|
||||
_lt: '$lt',
|
||||
_lte: '$lte',
|
||||
_gt: '$gt',
|
||||
_gte: '$gte',
|
||||
_in: '$in',
|
||||
_nin: '$nin',
|
||||
_exists: '$exists',
|
||||
_select: '$select',
|
||||
_dontSelect: '$dontSelect',
|
||||
_inQuery: '$inQuery',
|
||||
_notInQuery: '$notInQuery',
|
||||
_containedBy: '$containedBy',
|
||||
_all: '$all',
|
||||
_regex: '$regex',
|
||||
_options: '$options',
|
||||
_text: '$text',
|
||||
_search: '$search',
|
||||
_term: '$term',
|
||||
_language: '$language',
|
||||
_caseSensitive: '$caseSensitive',
|
||||
_diacriticSensitive: '$diacriticSensitive',
|
||||
_nearSphere: '$nearSphere',
|
||||
_maxDistance: '$maxDistance',
|
||||
_maxDistanceInRadians: '$maxDistanceInRadians',
|
||||
_maxDistanceInMiles: '$maxDistanceInMiles',
|
||||
_maxDistanceInKilometers: '$maxDistanceInKilometers',
|
||||
_within: '$within',
|
||||
_box: '$box',
|
||||
_geoWithin: '$geoWithin',
|
||||
_polygon: '$polygon',
|
||||
_centerSphere: '$centerSphere',
|
||||
_geoIntersects: '$geoIntersects',
|
||||
_point: '$point',
|
||||
};
|
||||
|
||||
const transformToParse = constraints => {
|
||||
if (!constraints || typeof constraints !== 'object') {
|
||||
return;
|
||||
}
|
||||
Object.keys(constraints).forEach(fieldName => {
|
||||
let fieldValue = constraints[fieldName];
|
||||
if (parseMap[fieldName]) {
|
||||
delete constraints[fieldName];
|
||||
fieldName = parseMap[fieldName];
|
||||
constraints[fieldName] = fieldValue;
|
||||
}
|
||||
switch (fieldName) {
|
||||
case '$point':
|
||||
case '$nearSphere':
|
||||
if (typeof fieldValue === 'object' && !fieldValue.__type) {
|
||||
fieldValue.__type = 'GeoPoint';
|
||||
}
|
||||
break;
|
||||
case '$box':
|
||||
if (
|
||||
typeof fieldValue === 'object' &&
|
||||
fieldValue.bottomLeft &&
|
||||
fieldValue.upperRight
|
||||
) {
|
||||
fieldValue = [
|
||||
{
|
||||
__type: 'GeoPoint',
|
||||
...fieldValue.bottomLeft,
|
||||
},
|
||||
{
|
||||
__type: 'GeoPoint',
|
||||
...fieldValue.upperRight,
|
||||
},
|
||||
];
|
||||
constraints[fieldName] = fieldValue;
|
||||
}
|
||||
break;
|
||||
case '$polygon':
|
||||
if (fieldValue instanceof Array) {
|
||||
fieldValue.forEach(geoPoint => {
|
||||
if (typeof geoPoint === 'object' && !geoPoint.__type) {
|
||||
geoPoint.__type = 'GeoPoint';
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case '$centerSphere':
|
||||
if (
|
||||
typeof fieldValue === 'object' &&
|
||||
fieldValue.center &&
|
||||
fieldValue.distance
|
||||
) {
|
||||
fieldValue = [
|
||||
{
|
||||
__type: 'GeoPoint',
|
||||
...fieldValue.center,
|
||||
},
|
||||
fieldValue.distance,
|
||||
];
|
||||
constraints[fieldName] = fieldValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (typeof fieldValue === 'object') {
|
||||
transformToParse(fieldValue);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const findObjects = async (
|
||||
className,
|
||||
where,
|
||||
order,
|
||||
skip,
|
||||
limit,
|
||||
keys,
|
||||
include,
|
||||
includeAll,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
subqueryReadPreference,
|
||||
config,
|
||||
auth,
|
||||
info,
|
||||
selectedFields
|
||||
) => {
|
||||
if (!where) {
|
||||
where = {};
|
||||
}
|
||||
|
||||
transformToParse(where);
|
||||
|
||||
const options = {};
|
||||
|
||||
if (selectedFields.includes('results')) {
|
||||
if (limit || limit === 0) {
|
||||
options.limit = limit;
|
||||
}
|
||||
if (options.limit !== 0) {
|
||||
if (order) {
|
||||
options.order = order;
|
||||
}
|
||||
if (skip) {
|
||||
options.skip = skip;
|
||||
}
|
||||
if (config.maxLimit && options.limit > config.maxLimit) {
|
||||
// Silently replace the limit on the query with the max configured
|
||||
options.limit = config.maxLimit;
|
||||
}
|
||||
if (keys) {
|
||||
options.keys = keys;
|
||||
}
|
||||
if (includeAll === true) {
|
||||
options.includeAll = includeAll;
|
||||
}
|
||||
if (!options.includeAll && include) {
|
||||
options.include = include;
|
||||
}
|
||||
if ((options.includeAll || options.include) && includeReadPreference) {
|
||||
options.includeReadPreference = includeReadPreference;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
options.limit = 0;
|
||||
}
|
||||
|
||||
if (selectedFields.includes('count')) {
|
||||
options.count = true;
|
||||
}
|
||||
|
||||
if (readPreference) {
|
||||
options.readPreference = readPreference;
|
||||
}
|
||||
if (Object.keys(where).length > 0 && subqueryReadPreference) {
|
||||
options.subqueryReadPreference = subqueryReadPreference;
|
||||
}
|
||||
|
||||
return await rest.find(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
where,
|
||||
options,
|
||||
info.clientSDK
|
||||
);
|
||||
};
|
||||
|
||||
const load = parseGraphQLSchema => {
|
||||
parseGraphQLSchema.graphQLObjectsQueries.get = {
|
||||
description:
|
||||
'The get query can be used to get an object of a certain class by its objectId.',
|
||||
args: {
|
||||
className: defaultGraphQLTypes.CLASS_NAME_ATT,
|
||||
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
|
||||
keys: defaultGraphQLTypes.KEYS_ATT,
|
||||
include: defaultGraphQLTypes.INCLUDE_ATT,
|
||||
readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT,
|
||||
includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT,
|
||||
},
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.OBJECT),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const {
|
||||
className,
|
||||
objectId,
|
||||
keys,
|
||||
include,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
} = args;
|
||||
const { config, auth, info } = context;
|
||||
|
||||
return await getObject(
|
||||
className,
|
||||
objectId,
|
||||
keys,
|
||||
include,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
parseGraphQLSchema.graphQLObjectsQueries.find = {
|
||||
description:
|
||||
'The find query can be used to find objects of a certain class.',
|
||||
args: {
|
||||
className: defaultGraphQLTypes.CLASS_NAME_ATT,
|
||||
where: defaultGraphQLTypes.WHERE_ATT,
|
||||
order: {
|
||||
description:
|
||||
'This is the order in which the objects should be returned',
|
||||
type: GraphQLString,
|
||||
},
|
||||
skip: defaultGraphQLTypes.SKIP_ATT,
|
||||
limit: defaultGraphQLTypes.LIMIT_ATT,
|
||||
keys: defaultGraphQLTypes.KEYS_ATT,
|
||||
include: defaultGraphQLTypes.INCLUDE_ATT,
|
||||
includeAll: {
|
||||
description: 'All pointers will be returned',
|
||||
type: GraphQLBoolean,
|
||||
},
|
||||
readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT,
|
||||
includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT,
|
||||
subqueryReadPreference: defaultGraphQLTypes.SUBQUERY_READ_PREFERENCE_ATT,
|
||||
},
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.FIND_RESULT),
|
||||
async resolve(_source, args, context, queryInfo) {
|
||||
try {
|
||||
const {
|
||||
className,
|
||||
where,
|
||||
order,
|
||||
skip,
|
||||
limit,
|
||||
keys,
|
||||
include,
|
||||
includeAll,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
subqueryReadPreference,
|
||||
} = args;
|
||||
const { config, auth, info } = context;
|
||||
const selectedFields = getFieldNames(queryInfo);
|
||||
|
||||
return await findObjects(
|
||||
className,
|
||||
where,
|
||||
order,
|
||||
skip,
|
||||
limit,
|
||||
keys,
|
||||
include,
|
||||
includeAll,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
subqueryReadPreference,
|
||||
config,
|
||||
auth,
|
||||
info,
|
||||
selectedFields
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const objectsQuery = new GraphQLObjectType({
|
||||
name: 'ObjectsQuery',
|
||||
description: 'ObjectsQuery is the top level type for objects queries.',
|
||||
fields: parseGraphQLSchema.graphQLObjectsQueries,
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(objectsQuery);
|
||||
|
||||
parseGraphQLSchema.graphQLQueries.objects = {
|
||||
description: 'This is the top level for objects queries.',
|
||||
type: objectsQuery,
|
||||
resolve: () => new Object(),
|
||||
};
|
||||
};
|
||||
|
||||
export { getObject, findObjects, load };
|
||||
121
src/GraphQL/loaders/parseClassMutations.js
Normal file
121
src/GraphQL/loaders/parseClassMutations.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { GraphQLNonNull, GraphQLBoolean } from 'graphql';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import * as objectsMutations from './objectsMutations';
|
||||
|
||||
const load = (parseGraphQLSchema, parseClass) => {
|
||||
const className = parseClass.className;
|
||||
|
||||
const classGraphQLInputType =
|
||||
parseGraphQLSchema.parseClassTypes[className].classGraphQLInputType;
|
||||
const fields = {
|
||||
description: 'These are the fields of the object.',
|
||||
type: classGraphQLInputType,
|
||||
};
|
||||
const classGraphQLInputTypeFields = classGraphQLInputType.getFields();
|
||||
|
||||
const transformTypes = fields => {
|
||||
if (fields) {
|
||||
Object.keys(fields).forEach(field => {
|
||||
if (classGraphQLInputTypeFields[field]) {
|
||||
switch (classGraphQLInputTypeFields[field].type) {
|
||||
case defaultGraphQLTypes.GEO_POINT:
|
||||
fields[field].__type = 'GeoPoint';
|
||||
break;
|
||||
case defaultGraphQLTypes.POLYGON:
|
||||
fields[field] = {
|
||||
__type: 'Polygon',
|
||||
coordinates: fields[field].map(geoPoint => [
|
||||
geoPoint.latitude,
|
||||
geoPoint.longitude,
|
||||
]),
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createGraphQLMutationName = `create${className}`;
|
||||
parseGraphQLSchema.graphQLObjectsMutations[createGraphQLMutationName] = {
|
||||
description: `The ${createGraphQLMutationName} mutation can be used to create a new object of the ${className} class.`,
|
||||
args: {
|
||||
fields,
|
||||
},
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.CREATE_RESULT),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { fields } = args;
|
||||
const { config, auth, info } = context;
|
||||
|
||||
transformTypes(fields);
|
||||
|
||||
return await objectsMutations.createObject(
|
||||
className,
|
||||
fields,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const updateGraphQLMutationName = `update${className}`;
|
||||
parseGraphQLSchema.graphQLObjectsMutations[updateGraphQLMutationName] = {
|
||||
description: `The ${updateGraphQLMutationName} mutation can be used to update an object of the ${className} class.`,
|
||||
args: {
|
||||
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
|
||||
fields,
|
||||
},
|
||||
type: defaultGraphQLTypes.UPDATE_RESULT,
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { objectId, fields } = args;
|
||||
const { config, auth, info } = context;
|
||||
|
||||
transformTypes(fields);
|
||||
|
||||
return await objectsMutations.updateObject(
|
||||
className,
|
||||
objectId,
|
||||
fields,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const deleteGraphQLMutationName = `delete${className}`;
|
||||
parseGraphQLSchema.graphQLObjectsMutations[deleteGraphQLMutationName] = {
|
||||
description: `The ${deleteGraphQLMutationName} mutation can be used to delete an object of the ${className} class.`,
|
||||
args: {
|
||||
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
|
||||
},
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { objectId } = args;
|
||||
const { config, auth, info } = context;
|
||||
|
||||
return await objectsMutations.deleteObject(
|
||||
className,
|
||||
objectId,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { load };
|
||||
102
src/GraphQL/loaders/parseClassQueries.js
Normal file
102
src/GraphQL/loaders/parseClassQueries.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { GraphQLNonNull } from 'graphql';
|
||||
import getFieldNames from 'graphql-list-fields';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import * as objectsQueries from './objectsQueries';
|
||||
import * as parseClassTypes from './parseClassTypes';
|
||||
|
||||
const load = (parseGraphQLSchema, parseClass) => {
|
||||
const className = parseClass.className;
|
||||
|
||||
const {
|
||||
classGraphQLOutputType,
|
||||
classGraphQLFindArgs,
|
||||
classGraphQLFindResultType,
|
||||
} = parseGraphQLSchema.parseClassTypes[className];
|
||||
|
||||
const getGraphQLQueryName = `get${className}`;
|
||||
parseGraphQLSchema.graphQLObjectsQueries[getGraphQLQueryName] = {
|
||||
description: `The ${getGraphQLQueryName} query can be used to get an object of the ${className} class by its id.`,
|
||||
args: {
|
||||
objectId: defaultGraphQLTypes.OBJECT_ID_ATT,
|
||||
readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT,
|
||||
includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT,
|
||||
},
|
||||
type: new GraphQLNonNull(classGraphQLOutputType),
|
||||
async resolve(_source, args, context, queryInfo) {
|
||||
try {
|
||||
const { objectId, readPreference, includeReadPreference } = args;
|
||||
const { config, auth, info } = context;
|
||||
const selectedFields = getFieldNames(queryInfo);
|
||||
|
||||
const { keys, include } = parseClassTypes.extractKeysAndInclude(
|
||||
selectedFields
|
||||
);
|
||||
|
||||
return await objectsQueries.getObject(
|
||||
className,
|
||||
objectId,
|
||||
keys,
|
||||
include,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const findGraphQLQueryName = `find${className}`;
|
||||
parseGraphQLSchema.graphQLObjectsQueries[findGraphQLQueryName] = {
|
||||
description: `The ${findGraphQLQueryName} query can be used to find objects of the ${className} class.`,
|
||||
args: classGraphQLFindArgs,
|
||||
type: new GraphQLNonNull(classGraphQLFindResultType),
|
||||
async resolve(_source, args, context, queryInfo) {
|
||||
try {
|
||||
const {
|
||||
where,
|
||||
order,
|
||||
skip,
|
||||
limit,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
subqueryReadPreference,
|
||||
} = args;
|
||||
const { config, auth, info } = context;
|
||||
const selectedFields = getFieldNames(queryInfo);
|
||||
|
||||
const { keys, include } = parseClassTypes.extractKeysAndInclude(
|
||||
selectedFields
|
||||
.filter(field => field.includes('.'))
|
||||
.map(field => field.slice(field.indexOf('.') + 1))
|
||||
);
|
||||
const parseOrder = order && order.join(',');
|
||||
|
||||
return await objectsQueries.findObjects(
|
||||
className,
|
||||
where,
|
||||
parseOrder,
|
||||
skip,
|
||||
limit,
|
||||
keys,
|
||||
include,
|
||||
false,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
subqueryReadPreference,
|
||||
config,
|
||||
auth,
|
||||
info,
|
||||
selectedFields.map(field => field.split('.', 1)[0])
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { load };
|
||||
557
src/GraphQL/loaders/parseClassTypes.js
Normal file
557
src/GraphQL/loaders/parseClassTypes.js
Normal file
@@ -0,0 +1,557 @@
|
||||
import {
|
||||
Kind,
|
||||
GraphQLObjectType,
|
||||
GraphQLString,
|
||||
GraphQLFloat,
|
||||
GraphQLBoolean,
|
||||
GraphQLList,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLScalarType,
|
||||
GraphQLEnumType,
|
||||
} from 'graphql';
|
||||
import getFieldNames from 'graphql-list-fields';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import * as objectsQueries from './objectsQueries';
|
||||
|
||||
const mapInputType = (parseType, targetClass, parseClassTypes) => {
|
||||
switch (parseType) {
|
||||
case 'String':
|
||||
return GraphQLString;
|
||||
case 'Number':
|
||||
return GraphQLFloat;
|
||||
case 'Boolean':
|
||||
return GraphQLBoolean;
|
||||
case 'Array':
|
||||
return new GraphQLList(defaultGraphQLTypes.ANY);
|
||||
case 'Object':
|
||||
return defaultGraphQLTypes.OBJECT;
|
||||
case 'Date':
|
||||
return defaultGraphQLTypes.DATE;
|
||||
case 'Pointer':
|
||||
if (parseClassTypes[targetClass]) {
|
||||
return parseClassTypes[targetClass].classGraphQLScalarType;
|
||||
} else {
|
||||
return defaultGraphQLTypes.OBJECT;
|
||||
}
|
||||
case 'Relation':
|
||||
if (parseClassTypes[targetClass]) {
|
||||
return parseClassTypes[targetClass].classGraphQLRelationOpType;
|
||||
} else {
|
||||
return defaultGraphQLTypes.OBJECT;
|
||||
}
|
||||
case 'File':
|
||||
return defaultGraphQLTypes.FILE;
|
||||
case 'GeoPoint':
|
||||
return defaultGraphQLTypes.GEO_POINT;
|
||||
case 'Polygon':
|
||||
return defaultGraphQLTypes.POLYGON;
|
||||
case 'Bytes':
|
||||
return defaultGraphQLTypes.BYTES;
|
||||
case 'ACL':
|
||||
return defaultGraphQLTypes.OBJECT;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const mapOutputType = (parseType, targetClass, parseClassTypes) => {
|
||||
switch (parseType) {
|
||||
case 'String':
|
||||
return GraphQLString;
|
||||
case 'Number':
|
||||
return GraphQLFloat;
|
||||
case 'Boolean':
|
||||
return GraphQLBoolean;
|
||||
case 'Array':
|
||||
return new GraphQLList(defaultGraphQLTypes.ANY);
|
||||
case 'Object':
|
||||
return defaultGraphQLTypes.OBJECT;
|
||||
case 'Date':
|
||||
return defaultGraphQLTypes.DATE;
|
||||
case 'Pointer':
|
||||
if (parseClassTypes[targetClass]) {
|
||||
return parseClassTypes[targetClass].classGraphQLOutputType;
|
||||
} else {
|
||||
return defaultGraphQLTypes.OBJECT;
|
||||
}
|
||||
case 'Relation':
|
||||
if (parseClassTypes[targetClass]) {
|
||||
return new GraphQLNonNull(
|
||||
parseClassTypes[targetClass].classGraphQLFindResultType
|
||||
);
|
||||
} else {
|
||||
return new GraphQLNonNull(defaultGraphQLTypes.FIND_RESULT);
|
||||
}
|
||||
case 'File':
|
||||
return defaultGraphQLTypes.FILE_INFO;
|
||||
case 'GeoPoint':
|
||||
return defaultGraphQLTypes.GEO_POINT_INFO;
|
||||
case 'Polygon':
|
||||
return defaultGraphQLTypes.POLYGON_INFO;
|
||||
case 'Bytes':
|
||||
return defaultGraphQLTypes.BYTES;
|
||||
case 'ACL':
|
||||
return defaultGraphQLTypes.OBJECT;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const mapConstraintType = (parseType, targetClass, parseClassTypes) => {
|
||||
switch (parseType) {
|
||||
case 'String':
|
||||
return defaultGraphQLTypes.STRING_CONSTRAINT;
|
||||
case 'Number':
|
||||
return defaultGraphQLTypes.NUMBER_CONSTRAINT;
|
||||
case 'Boolean':
|
||||
return defaultGraphQLTypes.BOOLEAN_CONSTRAINT;
|
||||
case 'Array':
|
||||
return defaultGraphQLTypes.ARRAY_CONSTRAINT;
|
||||
case 'Object':
|
||||
return defaultGraphQLTypes.OBJECT_CONSTRAINT;
|
||||
case 'Date':
|
||||
return defaultGraphQLTypes.DATE_CONSTRAINT;
|
||||
case 'Pointer':
|
||||
if (parseClassTypes[targetClass]) {
|
||||
return parseClassTypes[targetClass].classGraphQLConstraintType;
|
||||
} else {
|
||||
return defaultGraphQLTypes.OBJECT;
|
||||
}
|
||||
case 'File':
|
||||
return defaultGraphQLTypes.FILE_CONSTRAINT;
|
||||
case 'GeoPoint':
|
||||
return defaultGraphQLTypes.GEO_POINT_CONSTRAINT;
|
||||
case 'Polygon':
|
||||
return defaultGraphQLTypes.POLYGON_CONSTRAINT;
|
||||
case 'Bytes':
|
||||
return defaultGraphQLTypes.BYTES_CONSTRAINT;
|
||||
case 'ACL':
|
||||
return defaultGraphQLTypes.OBJECT_CONSTRAINT;
|
||||
case 'Relation':
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const extractKeysAndInclude = selectedFields => {
|
||||
selectedFields = selectedFields.filter(
|
||||
field => !field.includes('__typename')
|
||||
);
|
||||
let keys = undefined;
|
||||
let include = undefined;
|
||||
if (selectedFields && selectedFields.length > 0) {
|
||||
keys = selectedFields.join(',');
|
||||
include = selectedFields
|
||||
.reduce((fields, field) => {
|
||||
fields = fields.slice();
|
||||
let pointIndex = field.lastIndexOf('.');
|
||||
while (pointIndex > 0) {
|
||||
const lastField = field.slice(pointIndex + 1);
|
||||
field = field.slice(0, pointIndex);
|
||||
if (!fields.includes(field) && lastField !== 'objectId') {
|
||||
fields.push(field);
|
||||
}
|
||||
pointIndex = field.lastIndexOf('.');
|
||||
}
|
||||
return fields;
|
||||
}, [])
|
||||
.join(',');
|
||||
}
|
||||
return { keys, include };
|
||||
};
|
||||
|
||||
const load = (parseGraphQLSchema, parseClass) => {
|
||||
const className = parseClass.className;
|
||||
|
||||
const classFields = Object.keys(parseClass.fields);
|
||||
|
||||
const classCustomFields = classFields.filter(
|
||||
field => !Object.keys(defaultGraphQLTypes.CLASS_FIELDS).includes(field)
|
||||
);
|
||||
|
||||
const classGraphQLScalarTypeName = `${className}Pointer`;
|
||||
const parseScalarValue = value => {
|
||||
if (typeof value === 'string') {
|
||||
return {
|
||||
__type: 'Pointer',
|
||||
className,
|
||||
objectId: value,
|
||||
};
|
||||
} else if (
|
||||
typeof value === 'object' &&
|
||||
value.__type === 'Pointer' &&
|
||||
value.className === className &&
|
||||
typeof value.objectId === 'string'
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new defaultGraphQLTypes.TypeValidationError(
|
||||
value,
|
||||
classGraphQLScalarTypeName
|
||||
);
|
||||
};
|
||||
const classGraphQLScalarType = new GraphQLScalarType({
|
||||
name: classGraphQLScalarTypeName,
|
||||
description: `The ${classGraphQLScalarTypeName} is used in operations that involve ${className} pointers.`,
|
||||
parseValue: parseScalarValue,
|
||||
serialize(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
} else if (
|
||||
typeof value === 'object' &&
|
||||
value.__type === 'Pointer' &&
|
||||
value.className === className &&
|
||||
typeof value.objectId === 'string'
|
||||
) {
|
||||
return value.objectId;
|
||||
}
|
||||
|
||||
throw new defaultGraphQLTypes.TypeValidationError(
|
||||
value,
|
||||
classGraphQLScalarTypeName
|
||||
);
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind === Kind.STRING) {
|
||||
return parseScalarValue(ast.value);
|
||||
} else if (ast.kind === Kind.OBJECT) {
|
||||
const __type = ast.fields.find(field => field.name.value === '__type');
|
||||
const className = ast.fields.find(
|
||||
field => field.name.value === 'className'
|
||||
);
|
||||
const objectId = ast.fields.find(
|
||||
field => field.name.value === 'objectId'
|
||||
);
|
||||
if (
|
||||
__type &&
|
||||
__type.value &&
|
||||
className &&
|
||||
className.value &&
|
||||
objectId &&
|
||||
objectId.value
|
||||
) {
|
||||
return parseScalarValue({
|
||||
__type: __type.value.value,
|
||||
className: className.value.value,
|
||||
objectId: objectId.value.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new defaultGraphQLTypes.TypeValidationError(
|
||||
ast.kind,
|
||||
classGraphQLScalarTypeName
|
||||
);
|
||||
},
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(classGraphQLScalarType);
|
||||
|
||||
const classGraphQLRelationOpTypeName = `${className}RelationOp`;
|
||||
const classGraphQLRelationOpType = new GraphQLInputObjectType({
|
||||
name: classGraphQLRelationOpTypeName,
|
||||
description: `The ${classGraphQLRelationOpTypeName} input type is used in operations that involve relations with the ${className} class.`,
|
||||
fields: () => ({
|
||||
_op: {
|
||||
description: 'This is the operation to be executed.',
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.RELATION_OP),
|
||||
},
|
||||
ops: {
|
||||
description:
|
||||
'In the case of a Batch operation, this is the list of operations to be executed.',
|
||||
type: new GraphQLList(new GraphQLNonNull(classGraphQLRelationOpType)),
|
||||
},
|
||||
objects: {
|
||||
description:
|
||||
'In the case of a AddRelation or RemoveRelation operation, this is the list of objects to be added/removed.',
|
||||
type: new GraphQLList(new GraphQLNonNull(classGraphQLScalarType)),
|
||||
},
|
||||
}),
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(classGraphQLRelationOpType);
|
||||
|
||||
const classGraphQLInputTypeName = `${className}Fields`;
|
||||
const classGraphQLInputType = new GraphQLInputObjectType({
|
||||
name: classGraphQLInputTypeName,
|
||||
description: `The ${classGraphQLInputTypeName} input type is used in operations that involve inputting objects of ${className} class.`,
|
||||
fields: () =>
|
||||
classCustomFields.reduce(
|
||||
(fields, field) => {
|
||||
const type = mapInputType(
|
||||
parseClass.fields[field].type,
|
||||
parseClass.fields[field].targetClass,
|
||||
parseGraphQLSchema.parseClassTypes
|
||||
);
|
||||
if (type) {
|
||||
return {
|
||||
...fields,
|
||||
[field]: {
|
||||
description: `This is the object ${field}.`,
|
||||
type,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return fields;
|
||||
}
|
||||
},
|
||||
{
|
||||
ACL: defaultGraphQLTypes.ACL_ATT,
|
||||
}
|
||||
),
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(classGraphQLInputType);
|
||||
|
||||
const classGraphQLConstraintTypeName = `${className}PointerConstraint`;
|
||||
const classGraphQLConstraintType = new GraphQLInputObjectType({
|
||||
name: classGraphQLConstraintTypeName,
|
||||
description: `The ${classGraphQLConstraintTypeName} input type is used in operations that involve filtering objects by a pointer field to ${className} class.`,
|
||||
fields: {
|
||||
_eq: defaultGraphQLTypes._eq(classGraphQLScalarType),
|
||||
_ne: defaultGraphQLTypes._ne(classGraphQLScalarType),
|
||||
_in: defaultGraphQLTypes._in(classGraphQLScalarType),
|
||||
_nin: defaultGraphQLTypes._nin(classGraphQLScalarType),
|
||||
_exists: defaultGraphQLTypes._exists,
|
||||
_select: defaultGraphQLTypes._select,
|
||||
_dontSelect: defaultGraphQLTypes._dontSelect,
|
||||
_inQuery: {
|
||||
description:
|
||||
'This is the $inQuery operator to specify a constraint to select the objects where a field equals to any of the ids in the result of a different query.',
|
||||
type: defaultGraphQLTypes.SUBQUERY,
|
||||
},
|
||||
_notInQuery: {
|
||||
description:
|
||||
'This is the $notInQuery operator to specify a constraint to select the objects where a field do not equal to any of the ids in the result of a different query.',
|
||||
type: defaultGraphQLTypes.SUBQUERY,
|
||||
},
|
||||
},
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(classGraphQLConstraintType);
|
||||
|
||||
const classGraphQLConstraintsTypeName = `${className}Constraints`;
|
||||
const classGraphQLConstraintsType = new GraphQLInputObjectType({
|
||||
name: classGraphQLConstraintsTypeName,
|
||||
description: `The ${classGraphQLConstraintsTypeName} input type is used in operations that involve filtering objects of ${className} class.`,
|
||||
fields: () => ({
|
||||
...classFields.reduce((fields, field) => {
|
||||
const type = mapConstraintType(
|
||||
parseClass.fields[field].type,
|
||||
parseClass.fields[field].targetClass,
|
||||
parseGraphQLSchema.parseClassTypes
|
||||
);
|
||||
if (type) {
|
||||
return {
|
||||
...fields,
|
||||
[field]: {
|
||||
description: `This is the object ${field}.`,
|
||||
type,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return fields;
|
||||
}
|
||||
}, {}),
|
||||
_or: {
|
||||
description: 'This is the $or operator to compound constraints.',
|
||||
type: new GraphQLList(new GraphQLNonNull(classGraphQLConstraintsType)),
|
||||
},
|
||||
_and: {
|
||||
description: 'This is the $and operator to compound constraints.',
|
||||
type: new GraphQLList(new GraphQLNonNull(classGraphQLConstraintsType)),
|
||||
},
|
||||
_nor: {
|
||||
description: 'This is the $nor operator to compound constraints.',
|
||||
type: new GraphQLList(new GraphQLNonNull(classGraphQLConstraintsType)),
|
||||
},
|
||||
}),
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(classGraphQLConstraintsType);
|
||||
|
||||
const classGraphQLOrderTypeName = `${className}Order`;
|
||||
const classGraphQLOrderType = new GraphQLEnumType({
|
||||
name: classGraphQLOrderTypeName,
|
||||
description: `The ${classGraphQLOrderTypeName} input type is used when sorting objects of the ${className} class.`,
|
||||
values: classFields.reduce((orderFields, field) => {
|
||||
return {
|
||||
...orderFields,
|
||||
[`${field}_ASC`]: { value: field },
|
||||
[`${field}_DESC`]: { value: `-${field}` },
|
||||
};
|
||||
}, {}),
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(classGraphQLOrderType);
|
||||
|
||||
const classGraphQLFindArgs = {
|
||||
where: {
|
||||
description:
|
||||
'These are the conditions that the objects need to match in order to be found.',
|
||||
type: classGraphQLConstraintsType,
|
||||
},
|
||||
order: {
|
||||
description: 'The fields to be used when sorting the data fetched.',
|
||||
type: new GraphQLList(new GraphQLNonNull(classGraphQLOrderType)),
|
||||
},
|
||||
skip: defaultGraphQLTypes.SKIP_ATT,
|
||||
limit: defaultGraphQLTypes.LIMIT_ATT,
|
||||
readPreference: defaultGraphQLTypes.READ_PREFERENCE_ATT,
|
||||
includeReadPreference: defaultGraphQLTypes.INCLUDE_READ_PREFERENCE_ATT,
|
||||
subqueryReadPreference: defaultGraphQLTypes.SUBQUERY_READ_PREFERENCE_ATT,
|
||||
};
|
||||
|
||||
const classGraphQLOutputTypeName = `${className}Class`;
|
||||
const outputFields = () => {
|
||||
return classCustomFields.reduce((fields, field) => {
|
||||
const type = mapOutputType(
|
||||
parseClass.fields[field].type,
|
||||
parseClass.fields[field].targetClass,
|
||||
parseGraphQLSchema.parseClassTypes
|
||||
);
|
||||
if (parseClass.fields[field].type === 'Relation') {
|
||||
const targetParseClassTypes =
|
||||
parseGraphQLSchema.parseClassTypes[
|
||||
parseClass.fields[field].targetClass
|
||||
];
|
||||
const args = targetParseClassTypes
|
||||
? targetParseClassTypes.classGraphQLFindArgs
|
||||
: undefined;
|
||||
return {
|
||||
...fields,
|
||||
[field]: {
|
||||
description: `This is the object ${field}.`,
|
||||
args,
|
||||
type,
|
||||
async resolve(source, args, context, queryInfo) {
|
||||
try {
|
||||
const {
|
||||
where,
|
||||
order,
|
||||
skip,
|
||||
limit,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
subqueryReadPreference,
|
||||
} = args;
|
||||
const { config, auth, info } = context;
|
||||
const selectedFields = getFieldNames(queryInfo);
|
||||
|
||||
const { keys, include } = extractKeysAndInclude(
|
||||
selectedFields
|
||||
.filter(field => field.includes('.'))
|
||||
.map(field => field.slice(field.indexOf('.') + 1))
|
||||
);
|
||||
|
||||
return await objectsQueries.findObjects(
|
||||
source[field].className,
|
||||
{
|
||||
_relatedTo: {
|
||||
object: {
|
||||
__type: 'Pointer',
|
||||
className,
|
||||
objectId: source.objectId,
|
||||
},
|
||||
key: field,
|
||||
},
|
||||
...(where || {}),
|
||||
},
|
||||
order,
|
||||
skip,
|
||||
limit,
|
||||
keys,
|
||||
include,
|
||||
false,
|
||||
readPreference,
|
||||
includeReadPreference,
|
||||
subqueryReadPreference,
|
||||
config,
|
||||
auth,
|
||||
info,
|
||||
selectedFields.map(field => field.split('.', 1)[0])
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (parseClass.fields[field].type === 'Polygon') {
|
||||
return {
|
||||
...fields,
|
||||
[field]: {
|
||||
description: `This is the object ${field}.`,
|
||||
type,
|
||||
async resolve(source) {
|
||||
if (source[field] && source[field].coordinates) {
|
||||
return source[field].coordinates.map(coordinate => ({
|
||||
latitude: coordinate[0],
|
||||
longitude: coordinate[1],
|
||||
}));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (type) {
|
||||
return {
|
||||
...fields,
|
||||
[field]: {
|
||||
description: `This is the object ${field}.`,
|
||||
type,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return fields;
|
||||
}
|
||||
}, defaultGraphQLTypes.CLASS_FIELDS);
|
||||
};
|
||||
const classGraphQLOutputType = new GraphQLObjectType({
|
||||
name: classGraphQLOutputTypeName,
|
||||
description: `The ${classGraphQLOutputTypeName} object type is used in operations that involve outputting objects of ${className} class.`,
|
||||
interfaces: [defaultGraphQLTypes.CLASS],
|
||||
fields: outputFields,
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(classGraphQLOutputType);
|
||||
|
||||
const classGraphQLFindResultTypeName = `${className}FindResult`;
|
||||
const classGraphQLFindResultType = new GraphQLObjectType({
|
||||
name: classGraphQLFindResultTypeName,
|
||||
description: `The ${classGraphQLFindResultTypeName} object type is used in the ${className} 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))
|
||||
),
|
||||
},
|
||||
count: defaultGraphQLTypes.COUNT_ATT,
|
||||
},
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(classGraphQLFindResultType);
|
||||
|
||||
parseGraphQLSchema.parseClassTypes[className] = {
|
||||
classGraphQLScalarType,
|
||||
classGraphQLRelationOpType,
|
||||
classGraphQLInputType,
|
||||
classGraphQLConstraintType,
|
||||
classGraphQLConstraintsType,
|
||||
classGraphQLFindArgs,
|
||||
classGraphQLOutputType,
|
||||
classGraphQLFindResultType,
|
||||
};
|
||||
|
||||
if (className === '_User') {
|
||||
const meType = new GraphQLObjectType({
|
||||
name: 'Me',
|
||||
description: `The Me object type is used in operations that involve outputting the current user data.`,
|
||||
interfaces: [defaultGraphQLTypes.CLASS],
|
||||
fields: () => ({
|
||||
...outputFields(),
|
||||
sessionToken: defaultGraphQLTypes.SESSION_TOKEN_ATT,
|
||||
}),
|
||||
});
|
||||
parseGraphQLSchema.meType = meType;
|
||||
parseGraphQLSchema.graphQLTypes.push(meType);
|
||||
}
|
||||
};
|
||||
|
||||
export { extractKeysAndInclude, load };
|
||||
110
src/GraphQL/loaders/usersMutations.js
Normal file
110
src/GraphQL/loaders/usersMutations.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType,
|
||||
GraphQLString,
|
||||
} from 'graphql';
|
||||
import UsersRouter from '../../Routers/UsersRouter';
|
||||
import * as defaultGraphQLTypes from './defaultGraphQLTypes';
|
||||
import * as objectsMutations from './objectsMutations';
|
||||
|
||||
const usersRouter = new UsersRouter();
|
||||
|
||||
const load = parseGraphQLSchema => {
|
||||
const fields = {};
|
||||
|
||||
fields.signUp = {
|
||||
description: 'The signUp mutation can be used to sign the user up.',
|
||||
args: {
|
||||
fields: {
|
||||
descriptions: 'These are the fields of the user.',
|
||||
type: parseGraphQLSchema.parseClassTypes['_User'].classGraphQLInputType,
|
||||
},
|
||||
},
|
||||
type: new GraphQLNonNull(defaultGraphQLTypes.SIGN_UP_RESULT),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { fields } = args;
|
||||
const { config, auth, info } = context;
|
||||
|
||||
return await objectsMutations.createObject(
|
||||
'_User',
|
||||
fields,
|
||||
config,
|
||||
auth,
|
||||
info
|
||||
);
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
fields.logIn = {
|
||||
description: 'The logIn mutation can be used to log the user in.',
|
||||
args: {
|
||||
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),
|
||||
},
|
||||
},
|
||||
type: new GraphQLNonNull(parseGraphQLSchema.meType),
|
||||
async resolve(_source, args, context) {
|
||||
try {
|
||||
const { 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
fields.logOut = {
|
||||
description: 'The logOut mutation can be used to log the user out.',
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
async resolve(_source, _args, context) {
|
||||
try {
|
||||
const { config, auth, info } = context;
|
||||
|
||||
await usersRouter.handleLogOut({
|
||||
config,
|
||||
auth,
|
||||
info,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const usersMutation = new GraphQLObjectType({
|
||||
name: 'UsersMutation',
|
||||
description: 'UsersMutation is the top level type for files mutations.',
|
||||
fields,
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(usersMutation);
|
||||
|
||||
parseGraphQLSchema.graphQLMutations.users = {
|
||||
description: 'This is the top level for users mutations.',
|
||||
type: usersMutation,
|
||||
resolve: () => new Object(),
|
||||
};
|
||||
};
|
||||
|
||||
export { load };
|
||||
36
src/GraphQL/loaders/usersQueries.js
Normal file
36
src/GraphQL/loaders/usersQueries.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { GraphQLNonNull, GraphQLObjectType } from 'graphql';
|
||||
import UsersRouter from '../../Routers/UsersRouter';
|
||||
|
||||
const usersRouter = new UsersRouter();
|
||||
|
||||
const load = parseGraphQLSchema => {
|
||||
const fields = {};
|
||||
|
||||
fields.me = {
|
||||
description: 'The Me query can be used to return the current user data.',
|
||||
type: new GraphQLNonNull(parseGraphQLSchema.meType),
|
||||
async resolve(_source, _args, context) {
|
||||
try {
|
||||
const { config, auth, info } = context;
|
||||
return (await usersRouter.handleMe({ config, auth, info })).response;
|
||||
} catch (e) {
|
||||
parseGraphQLSchema.handleError(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const usersQuery = new GraphQLObjectType({
|
||||
name: 'UsersQuery',
|
||||
description: 'UsersQuery is the top level type for users queries.',
|
||||
fields,
|
||||
});
|
||||
parseGraphQLSchema.graphQLTypes.push(usersQuery);
|
||||
|
||||
parseGraphQLSchema.graphQLQueries.users = {
|
||||
description: 'This is the top level for users queries.',
|
||||
type: usersQuery,
|
||||
resolve: () => new Object(),
|
||||
};
|
||||
};
|
||||
|
||||
export { load };
|
||||
Reference in New Issue
Block a user