diff --git a/README.md b/README.md index 5db66436f9..a5b832558c 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,148 @@ Live queries are meant to be used in real-time reactive applications, where just Take a look at [Live Query Guide](https://docs.parseplatform.org/parse-server/guide/#live-queries), [Live Query Server Setup Guide](https://docs.parseplatform.org/parse-server/guide/#scalability) and [Live Query Protocol Specification](https://github.com/parse-community/parse-server/wiki/Parse-LiveQuery-Protocol-Specification). You can setup a standalone server or multiple instances for scalability (recommended). +# GraphQL + +[GraphQL](https://graphql.org/), developed by Facebook, is an open-source data query and manipulation language for APIs. In addition to the traditional REST API, Parse Server automatically generates a GraphQL API based on your current application schema. + +## Running + +``` +$ npm install -g parse-server mongodb-runner +$ mongodb-runner start +$ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test --mountGraphQL --mountPlayground +``` + +After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API. + +***Note:*** Do ***NOT*** use --mountPlayground option in production. + +## Checking the API health + +Run the following: + +```graphql +query Health { + health +} +``` + +You should receive the following response: + +```json +{ + "data": { + "health": true + } +} +``` + +## Creating your first object + +Since your application does not have a schema yet, you can use the generic `create` mutation to create your first object. Run the following: + +```graphql +mutation CreateObject { + objects { + create(className: "GameScore" fields: { score: 1337 playerName: "Sean Plott" cheatMode: false }) { + objectId + createdAt + } + } +} +``` + +You should receive a response similar to this: + +```json +{ + "data": { + "objects": { + "create": { + "objectId": "7jfBmbGgyF", + "createdAt": "2019-06-20T23:50:50.825Z" + } + } + } +} +``` + +## Using automatically generated operations + +Parse Server learned from the first object that you created and now you have the `GameScore` class in your schema. You can now start using the automatically generated operations! + +Run the following to create a second object: + +```graphql +mutation CreateGameScore { + objects { + createGameScore(fields: { score: 2558 playerName: "Luke Skywalker" cheatMode: false }) { + objectId + createdAt + } + } +} +``` + +You should receive a response similar to this: + +```json +{ + "data": { + "objects": { + "createGameScore": { + "objectId": "gySYolb2CL", + "createdAt": "2019-06-20T23:56:37.114Z" + } + } + } +} +``` + +You can also run a query to this new class: + +```graphql +query FindGameScore { + objects { + findGameScore { + results { + playerName + score + } + } + } +} +``` + +You should receive a response similar to this: + +```json +{ + "data": { + "objects": { + "findGameScore": { + "results": [ + { + "playerName": "Sean Plott", + "score": 1337 + }, + { + "playerName": "Luke Skywalker", + "score": 2558 + } + ] + } + } + } +} +``` + +## Learning more + +Please look at the right side of your GraphQL Playground. You will see the `DOCS` and `SCHEMA` menus. They are automatically generated by analysing your application schema. Please refer to them and learn more about everything that you can do with your Parse GraphQL API. + +Additionally, the [GraphQL Learn Section](https://graphql.org/learn/) is a very good source to start learning about the power of the GraphQL language. + # Upgrading to 3.0.0 Starting 3.0.0, parse-server uses the JS SDK version 2.0. diff --git a/spec/CLI.spec.js b/spec/CLI.spec.js index ea820d7764..1c4b567512 100644 --- a/spec/CLI.spec.js +++ b/spec/CLI.spec.js @@ -3,6 +3,8 @@ const commander = require('../lib/cli/utils/commander').default; const definitions = require('../lib/cli/definitions/parse-server').default; const liveQueryDefinitions = require('../lib/cli/definitions/parse-live-query-server') .default; +const path = require('path'); +const { spawn } = require('child_process'); const testDefinitions = { arg0: 'PROGRAM_ARG_0', @@ -231,3 +233,84 @@ describe('LiveQuery definitions', () => { } }); }); + +describe('execution', () => { + const binPath = path.resolve(__dirname, '../bin/parse-server'); + let childProcess; + + afterEach(async () => { + if (childProcess) { + childProcess.kill(); + } + }); + + it('shoud start Parse Server', done => { + childProcess = spawn(binPath, [ + '--appId', + 'test', + '--masterKey', + 'test', + '--databaseURI', + 'mongodb://localhost/test', + ]); + childProcess.stdout.on('data', data => { + data = data.toString(); + if (data.includes('parse-server running on')) { + done(); + } + }); + childProcess.stderr.on('data', data => { + done.fail(data.toString()); + }); + }); + + it('shoud start Parse Server with GraphQL', done => { + childProcess = spawn(binPath, [ + '--appId', + 'test', + '--masterKey', + 'test', + '--databaseURI', + 'mongodb://localhost/test', + '--mountGraphQL', + ]); + let output = ''; + childProcess.stdout.on('data', data => { + data = data.toString(); + output += data; + if (data.includes('GraphQL running on')) { + expect(output).toMatch('parse-server running on'); + done(); + } + }); + childProcess.stderr.on('data', data => { + done.fail(data.toString()); + }); + }); + + it('shoud start Parse Server with GraphQL and Playground', done => { + childProcess = spawn(binPath, [ + '--appId', + 'test', + '--masterKey', + 'test', + '--databaseURI', + 'mongodb://localhost/test', + '--mountGraphQL', + '--mountPlayground', + ]); + let output = ''; + childProcess.stdout.on('data', data => { + data = data.toString(); + output += data; + if (data.includes('Playground running on')) { + expect(output).toMatch('GraphQL running on'); + expect(output).toMatch('parse-server running on'); + done(); + } + }); + childProcess.stderr.on('data', data => { + done.fail(data.toString()); + }); + }); +}); diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index aefaed7660..5cb4c1c747 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -76,6 +76,10 @@ class ParseGraphQLServer { renderPlaygroundPage({ endpoint: this.config.graphQLPath, subscriptionEndpoint: this.config.subscriptionsPath, + headers: { + 'X-Parse-Application-Id': this.parseServer.config.appId, + 'X-Parse-Master-Key': this.parseServer.config.masterKey, + }, }) ); res.end(); diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 6bde68b3c2..4005b2504d 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -153,6 +153,11 @@ module.exports.ParseServerOptions = { help: 'Adapter module for the files sub-system', action: parsers.moduleOrObjectParser, }, + graphQLPath: { + env: 'PARSE_SERVER_GRAPHQL_PATH', + help: 'Mount path for the GraphQL endpoint, defaults to /graphql', + default: '/graphql', + }, host: { env: 'PARSE_SERVER_HOST', help: 'The host to serve ParseServer on, defaults to 0.0.0.0', @@ -219,11 +224,23 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_MIDDLEWARE', help: 'middleware for express server, can be string or function', }, + mountGraphQL: { + env: 'PARSE_SERVER_MOUNT_GRAPHQL', + help: 'Mounts the GraphQL endpoint', + action: parsers.booleanParser, + default: false, + }, mountPath: { env: 'PARSE_SERVER_MOUNT_PATH', help: 'Mount path for the server, defaults to /parse', default: '/parse', }, + mountPlayground: { + env: 'PARSE_SERVER_MOUNT_PLAYGROUND', + help: 'Mounts the GraphQL Playground - never use this option in production', + action: parsers.booleanParser, + default: false, + }, objectIdSize: { env: 'PARSE_SERVER_OBJECT_ID_SIZE', help: "Sets the number of characters in generated object id's, default 10", @@ -235,6 +252,11 @@ module.exports.ParseServerOptions = { help: 'Password policy for enforcing password related rules', action: parsers.objectParser, }, + playgroundPath: { + env: 'PARSE_SERVER_PLAYGROUND_PATH', + help: 'Mount path for the GraphQL Playground, defaults to /playground', + default: '/playground', + }, port: { env: 'PORT', help: 'The port to run the ParseServer, defaults to 1337.', diff --git a/src/Options/docs.js b/src/Options/docs.js index 481fb911fb..fa8d584c94 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -27,6 +27,7 @@ * @property {Boolean} expireInactiveSessions Sets wether we should expire the inactive sessions, defaults to true * @property {String} fileKey Key for your files * @property {Adapter} filesAdapter Adapter module for the files sub-system + * @property {String} graphQLPath Mount path for the GraphQL endpoint, defaults to /graphql * @property {String} host The host to serve ParseServer on, defaults to 0.0.0.0 * @property {String} javascriptKey Key for the Javascript SDK * @property {Boolean} jsonLogs Log as structured JSON objects @@ -40,9 +41,12 @@ * @property {Number} maxLimit Max value for limit option on queries, defaults to unlimited * @property {String} maxUploadSize Max file size for uploads, defaults to 20mb * @property {Union} middleware middleware for express server, can be string or function + * @property {Boolean} mountGraphQL Mounts the GraphQL endpoint * @property {String} mountPath Mount path for the server, defaults to /parse + * @property {Boolean} mountPlayground Mounts the GraphQL Playground - never use this option in production * @property {Number} objectIdSize Sets the number of characters in generated object id's, default 10 * @property {Any} passwordPolicy Password policy for enforcing password related rules + * @property {String} playgroundPath Mount path for the GraphQL Playground, defaults to /playground * @property {Number} port The port to run the ParseServer, defaults to 1337. * @property {Boolean} preserveFileName Enable (or disable) the addition of a unique hash to the file names * @property {Boolean} preventLoginWithUnverifiedEmail Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false diff --git a/src/Options/index.js b/src/Options/index.js index fea9eef0e0..a1d3c377a2 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -180,6 +180,22 @@ export interface ParseServerOptions { startLiveQueryServer: ?boolean; /* Live query server configuration options (will start the liveQuery server) */ liveQueryServerOptions: ?LiveQueryServerOptions; + /* Mounts the GraphQL endpoint + :ENV: PARSE_SERVER_MOUNT_GRAPHQL + :DEFAULT: false */ + mountGraphQL: ?boolean; + /* Mount path for the GraphQL endpoint, defaults to /graphql + :ENV: PARSE_SERVER_GRAPHQL_PATH + :DEFAULT: /graphql */ + graphQLPath: ?string; + /* Mounts the GraphQL Playground - never use this option in production + :ENV: PARSE_SERVER_MOUNT_PLAYGROUND + :DEFAULT: false */ + mountPlayground: ?boolean; + /* Mount path for the GraphQL Playground, defaults to /playground + :ENV: PARSE_SERVER_PLAYGROUND_PATH + :DEFAULT: /playground */ + playgroundPath: ?string; serverStartComplete: ?(error: ?Error) => void; } diff --git a/src/ParseServer.js b/src/ParseServer.js index fb28c4708b..738a0624b9 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -34,9 +34,10 @@ import { UsersRouter } from './Routers/UsersRouter'; import { PurgeRouter } from './Routers/PurgeRouter'; import { AudiencesRouter } from './Routers/AudiencesRouter'; import { AggregateRouter } from './Routers/AggregateRouter'; - import { ParseServerRESTController } from './ParseServerRESTController'; import * as controllers from './Controllers'; +import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer'; + // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -264,6 +265,22 @@ class ParseServer { } app.use(options.mountPath, this.app); + + if (options.mountGraphQL === true || options.mountPlayground === true) { + const parseGraphQLServer = new ParseGraphQLServer(this, { + graphQLPath: options.graphQLPath, + playgroundPath: options.playgroundPath, + }); + + if (options.mountGraphQL) { + parseGraphQLServer.applyGraphQL(app); + } + + if (options.mountPlayground) { + parseGraphQLServer.applyPlayground(app); + } + } + const server = app.listen(options.port, options.host, callback); this.server = server; diff --git a/src/cli/parse-server.js b/src/cli/parse-server.js index 6328e920ae..69611bd905 100755 --- a/src/cli/parse-server.js +++ b/src/cli/parse-server.js @@ -84,19 +84,39 @@ runner({ }); } else { ParseServer.start(options, () => { - console.log( - '[' + process.pid + '] parse-server running on ' + options.serverURL - ); + printSuccessMessage(); }); } } else { ParseServer.start(options, () => { logOptions(); console.log(''); + printSuccessMessage(); + }); + } + + function printSuccessMessage() { + console.log( + '[' + process.pid + '] parse-server running on ' + options.serverURL + ); + if (options.mountGraphQL) { console.log( - '[' + process.pid + '] parse-server running on ' + options.serverURL + '[' + + process.pid + + '] GraphQL running on http://localhost:' + + options.port + + options.graphQLPath ); - }); + } + if (options.mountPlayground) { + console.log( + '[' + + process.pid + + '] Playground running on http://localhost:' + + options.port + + options.playgroundPath + ); + } } }, });