diff --git a/.eslintrc.json b/.eslintrc.json index b47bc599df..f1eade9305 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,6 +24,7 @@ "no-await-in-loop" : 1 }, "globals" : { - "Parse" : true + "Parse" : true, + "document": true } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a3d4ae74b..fbe3152d94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x] + node-version: [15.x] name: ${{ matrix.name }} steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index b627d4e64a..96c1a4e992 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,15 @@ [![License][license-svg]][license-link] [![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social)](https://twitter.com/intent/follow?screen_name=ParsePlatform) -Example project using the [parse-server](https://github.com/ParsePlatform/parse-server) module on Express. Read the full [Parse Server Guide](https://docs.parseplatform.org/parse-server/guide/) for more information. +Example project using the [parse-server](https://github.com/ParsePlatform/parse-server) module on Express, utilising AWS Secret Manager Read the full [Parse Server Guide](https://docs.parseplatform.org/parse-server/guide/) for more information. + +Please note: this example uses top level await which is only available in Node >= v14.8.0. # Table of Contents - [Local Development](#local-development) + - [Creating AWS Secrets](#creating-aws-secrets) + - [File Setup](#file-setup) - [Helpful Scripts](#helpful-scripts) - [Remote Deployment](#remote-deployment) - [Heroku](#heroku) @@ -29,6 +33,20 @@ Example project using the [parse-server](https://github.com/ParsePlatform/parse- # Local Development +## Creating AWS Secrets +* Log into the AWS Console and navigate to AWS Secrets Manager +* Click "store a new secret" +* Select "other type of secret" +* Enter the initial secret value +* Name the secret (`/src/config` will reference this secret name). If you have selected key pairs, make sure you properly destructure the returned secret. +* If you would like to automatically rotate the key, follow [this](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_turn-on-for-other.html) guide. + + +## Local Development + +* Install AWS SDK with `npm install aws-sdk -g` +* Create an AWS profile with `aws configure --profile profileName` +* Update `npm start`'s `AWS_Profile` and `AWS_REGION` * Make sure you have at least Node 4.3. `node --version` * Clone this repo and change directory to it. * `npm install` @@ -39,6 +57,16 @@ Example project using the [parse-server](https://github.com/ParsePlatform/parse- * You now have a database named "dev" that contains your Parse data * Install ngrok and you can test with devices +## File Setup +Feel free to change this at your discretion. Example projects are just that - an example. + +* `/spec` contains unit tests you can write to validate your Parse Server. +* `/src/cloud` contains Parse.Cloud files to run custom cloud code. +* `/src/public` contains public assets. +* `/src/views` contains views that express can render. +* `/src/config.js` contains all Parse Server settings. +* `index.js` is the main entry point for `npm start`, and includes express routing. + ## Helpful Scripts These scripts can help you to develop your app for Parse Server: @@ -83,7 +111,7 @@ Detailed information is available here: ## Google App Engine -1. Clone the repo and change directory to it +1. Clone the repo and change directory to it 1. Create a project in the [Google Cloud Platform Console](https://console.cloud.google.com/). 1. [Enable billing](https://console.cloud.google.com/project/_/settings) for your project. 1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/). @@ -164,9 +192,11 @@ curl -X POST \ ### JavaScript +We have built an example page to show JS SDK usage, available at [http://localhost:1337/](http://localhost:1337/). + ```js // Initialize SDK -Parse.initialize("YOUR_APP_ID", "unused"); +Parse.initialize("YOUR_APP_ID"); Parse.serverURL = 'http://localhost:1337/parse'; // Save object diff --git a/index.js b/index.js index e720980bf0..b4c57bda09 100644 --- a/index.js +++ b/index.js @@ -1,65 +1,33 @@ // Example express application adding the parse-server module to expose Parse // compatible API routes. -const express = require('express'); -const ParseServer = require('parse-server').ParseServer; -const path = require('path'); -const args = process.argv || []; -const test = args.some(arg => arg.includes('jasmine')); +import express from 'express'; +import { ParseServer } from 'parse-server'; +import { createServer } from 'http'; +import { config } from './src/config.js'; +import { renderFile } from 'ejs'; -const databaseUri = process.env.DATABASE_URI || process.env.MONGODB_URI; - -if (!databaseUri) { - console.log('DATABASE_URI not specified, falling back to localhost.'); -} -const config = { - databaseURI: databaseUri || 'mongodb://localhost:27017/dev', - cloud: process.env.CLOUD_CODE_MAIN || __dirname + '/cloud/main.js', - appId: process.env.APP_ID || 'myAppId', - masterKey: process.env.MASTER_KEY || '', //Add your master key here. Keep it secret! - serverURL: process.env.SERVER_URL || 'http://localhost:1337/parse', // Don't forget to change to https if needed - liveQuery: { - classNames: ['Posts', 'Comments'], // List of classes to support for query subscriptions - }, -}; -// Client-keys like the javascript key or the .NET key are not necessary with parse-server -// If you wish you require them, you can set them as options in the initialization above: -// javascriptKey, restAPIKey, dotNetKey, clientKey - -const app = express(); +export const app = express(); +app.set('view engine', 'ejs'); +app.engine('html', renderFile); +app.set('views', `./src/views`); // Serve static assets from the /public folder -app.use('/public', express.static(path.join(__dirname, '/public'))); - -// Serve the Parse API on the /parse URL prefix -const mountPath = process.env.PARSE_MOUNT || '/parse'; -if (!test) { - const api = new ParseServer(config); - app.use(mountPath, api); -} +app.use('/public', express.static('./src/public')); // Parse Server plays nicely with the rest of your web routes -app.get('/', function (req, res) { - res.status(200).send('I dream of being a website. Please star the parse-server repo on GitHub!'); +app.get('/', (req, res) => { + res.render('test.html', { appId: config.appId, serverUrl: config.serverURL }); }); -// There will be a test page available on the /test path of your server url -// Remove this before launching your app -app.get('/test', function (req, res) { - res.sendFile(path.join(__dirname, '/public/test.html')); -}); +if (!process.env.TESTING) { + const api = new ParseServer(config); + app.use('/parse', api); -const port = process.env.PORT || 1337; -if (!test) { - const httpServer = require('http').createServer(app); - httpServer.listen(port, function () { - console.log('parse-server-example running on port ' + port + '.'); + const httpServer = createServer(app); + const port = 1337; + httpServer.listen(port, () => { + console.log(`parse-server-example running on port ${port}.`); }); - // This will enable the Live Query real-time server ParseServer.createLiveQueryServer(httpServer); } - -module.exports = { - app, - config, -}; diff --git a/package.json b/package.json index 32c43e7a7f..cb3cc7196d 100644 --- a/package.json +++ b/package.json @@ -9,34 +9,38 @@ }, "license": "MIT", "dependencies": { + "aws-sdk": "2.994.0", + "ejs": "3.1.6", "express": "4.17.1", - "kerberos": "1.1.4", - "parse": "2.19.0", - "parse-server": "4.5.0" + "kerberos": "1.1.6", + "parse": "3.3.0", + "parse-server": "4.10.3" }, "scripts": { - "start": "node index.js", - "lint": "eslint --cache ./cloud && eslint --cache index.js && eslint --cache ./spec", - "lint-fix": "eslint --cache --fix ./cloud && eslint --cache --fix index.js && eslint --cache --fix ./spec", - "test": "mongodb-runner start && jasmine", + "start": "AWS_PROFILE=aws_profile AWS_REGION=aws_region node index.js", + "lint": "eslint --cache ./src && eslint --cache index.js && eslint --cache ./spec", + "lint-fix": "eslint --cache --fix ./src && eslint --cache --fix index.js && eslint --cache --fix ./spec", + "test": "mongodb-runner start && TESTING=true jasmine", + "test:kill": "kill $(lsof -ti:27017) && npm test", "coverage": "nyc jasmine", - "prettier": "prettier --write '{cloud,spec}/{**/*,*}.js' 'index.js'", + "prettier": "prettier --write '{src,spec}/{**/*,*}{.js,.html,.css}' 'index.js'", "watch": "babel-watch index.js" }, "engines": { "node": ">=4.3" }, + "type": "module", "devDependencies": { "babel-eslint": "10.1.0", - "babel-watch": "7.4.0", - "eslint": "7.19.0", - "eslint-config-standard": "16.0.2", - "eslint-plugin-import": "2.22.1", + "babel-watch": "7.5.0", + "eslint": "7.32.0", + "eslint-config-standard": "16.0.3", + "eslint-plugin-import": "2.24.2", "eslint-plugin-node": "11.1.0", - "eslint-plugin-promise": "4.2.1", - "jasmine": "3.6.4", - "mongodb-runner": "4.8.1", + "eslint-plugin-promise": "5.1.0", + "jasmine": "3.9.0", + "mongodb-runner": "4.8.3", "nyc": "15.1.0", - "prettier": "2.2.1" + "prettier": "2.3.2" } } diff --git a/public/assets/css/style.css b/public/assets/css/style.css deleted file mode 100644 index 699311429d..0000000000 --- a/public/assets/css/style.css +++ /dev/null @@ -1,243 +0,0 @@ -body { - margin: 0; - padding: 0; - font-family: Helvetica, Arial, sans-serif; - font-size: 14px; - letter-spacing: 0.2px; - line-height: 24px; - color: #585858; -} - -a { - color: #169CEE; - text-decoration: underline; -} - -a:hover { - color: #2C3D50; -} - -a:visited { - color: #2a6496; -} - - -/* - helpers - */ - -.align-center { - text-align: center; -} - -.hidden { - display: none; -} - -/* - app css - */ - -.container { - margin: 0 auto; - margin-top: 45px; - max-width: 860px; -} - -#parse-logo { - width: 109px; - height: 110px; - margin: 0 0 20px; - text-align: center; -} - -.up-and-running, .time-to-deploy { - font-weight: bold; -} - -.advice { - margin-bottom: 40px; -} - -.advice { - background: #f4f4f4; - border-radius: 4px; - -webkit-border-radius: 4px 4px; - -moz-border-radius: 4px 4px; - -ms-border-radius: 4px 4px; - -o-border-radius: 4px 4px; - padding: 10px 20px; -} - -#parse-url { - color: #169CEE; - font-weight: bold; -} - -.step--container { - margin: 30px 0 20px; - border-top: 1px solid #E2E2E2; - padding-top: 30px; -} - -/* Disabled step */ -.step--disabled .step--number { - background: #fff; - border-color: #B5B5B5; - color: #B5B5B5; -} - -.step--disabled .step--info { - border-color: #B5B5B5; - color: #B5B5B5; -} - -.step--disabled .step--action-btn, - .step--disabled .step--action-btn:hover { - border-color: #B5B5B5; - background: #fff; - color: #B5B5B5; - cursor: default; -} - -/* Disabled step eof */ - -.step--action-btn.success, -.step--action-btn.success:hover { - background: #57C689; - border-color: #57C689; - color: #fff; - cursor: default; - font-weight: bold; -} - -.step--number { - background: #169CEE; - border: 1px solid #169CEE; - border-radius: 28px; - -webkit-border-radius: 28px 28px; - -moz-border-radius: 28px 28px; - -ms-border-radius: 28px 28px; - -o-border-radius: 28px 28px; - display: block; - margin: auto; - width: 47px; - height: 47px; - font-weight: bolder; - font-size: 20px; - color: #FFFFFF; - line-height: 47px; /* follows width and height */ -} - -.step--info { } - -.step--action-btn { - color: #169CEE; - font-size: 14px; - font-weight: 100; - border: 1px solid #169CEE; - padding: 12px 18px; - border-radius: 28px; - -webkit-border-radius: 28px 28px; - -moz-border-radius: 28px 28px; - -ms-border-radius: 28px 28px; - -o-border-radius: 28px 28px; - cursor: pointer; - text-decoration: none; - display:inline-block; - text-align: center; - text-transform: uppercase; -} - -.step--action-btn:hover { - background: #169CEE; - color: white; -} - -.step--pre { - margin-top: 4px; - margin-bottom: 0; - background: #f4f4f4; - border-radius: 4px; - -webkit-border-radius: 4px 4px; - -moz-border-radius: 4px 4px; - -ms-border-radius: 4px 4px; - -o-border-radius: 4px 4px; - padding: 10px 20px; - word-wrap: break-word; - white-space: inherit; - font-size: 13px; -} - -#local-parse-working { - font-size: 18px; - line-height: 24px; - color: #57C689; - font-weight: bold; -} - -#step-4 .step--number { - background: #57C689; - border-color: #57C689; - color: #fff; - display: inline-block; -} - -.step--deploy-btn { - display: block; - margin-top: 20px; - width: 170px; - color: #57C689 !important; - font-weight: bold; - border-color: #57C689; -} - -.step--deploy-btn:hover { - background: #57C689; - color: #fff !important; -} - -.step--error { - color: red; - font-weight: bold; -} - -#prod-test { - margin-bottom: 60px; -} - -#prod-test input { - background-color: #fff; - border: 1px solid #B5B5B5; - color: #000000; - font-family: "Inconsolata"; - font-size: 16px; - line-height: 17px; - padding: 12px; - width: 260px; - border-radius: 4px; - -webkit-border-radius: 4px 4px; - -moz-border-radius: 4px 4px; - -ms-border-radius: 4px 4px; - -o-border-radius: 4px 4px; - display:block; - margin-bottom: 10px; -} - -#footer { - border-top: 1px solid #E2E2E2; - padding: 20px; -} - -#footer ul li { - list-style-type: none; - display:inline-block; -} -#footer ul li:after { - content: "-"; - padding: 10px; -} -#footer ul li:last-child:after { - content: ""; -} - diff --git a/public/assets/js/script.js b/public/assets/js/script.js deleted file mode 100644 index 96cc688e53..0000000000 --- a/public/assets/js/script.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Steps handler - */ - -var Steps = {}; - -Steps.init = function() { - this.buildParseUrl(); - this.bindBtn('#step-1-btn', function(e){ - ParseRequest.postData(); - e.preventDefault(); - }) -} - -Steps.buildParseUrl = function() { - var url = Config.getUrl(); - $('#parse-url').html(url + '/parse'); -} - -Steps.bindBtn = function(id, callback) { - $(id).click(callback); -} - -Steps.closeStep = function(id) { - $(id).addClass('step--disabled'); -} - -Steps.openStep = function(id) { - $(id).removeClass('step--disabled'); -} - -Steps.fillStepOutput = function(id, data) { - $(id).html('Output: ' + data).slideDown(); -} - -Steps.fillStepError = function(id, errorMsg) { - $(id).html(errorMsg).slideDown(); -} - - -Steps.fillBtn = function(id, message) { - $(id).addClass('success').html('✓ ' + message); -} - -Steps.showWorkingMessage = function() { - $('#step-4').delay(500).slideDown(); -} - - -/** - * Parse requests handler - */ - -var ParseRequest = {}; - -ParseRequest.postData = function() { - XHR.setCallback(function(data){ - // store objectID - Store.objectId = JSON.parse(data).objectId; - // close first step - Steps.closeStep('#step-1'); - Steps.fillStepOutput('#step-1-output', data); - Steps.fillBtn('#step-1-btn', 'Posted'); - // open second step - Steps.openStep('#step-2'); - Steps.bindBtn('#step-2-btn', function(e){ - ParseRequest.getData(); - e.preventDefault(); - }); - }, - function(error) { - Steps.fillStepError('#step-1-error', 'There was a failure: ' + error); - }); - XHR.POST('/parse/classes/GameScore'); -}; - -ParseRequest.getData = function() { - XHR.setCallback(function(data){ - // close second step - Steps.closeStep('#step-2'); - Steps.fillStepOutput('#step-2-output', data); - Steps.fillBtn('#step-2-btn', 'Fetched'); - // open third step - Steps.openStep('#step-3'); - Steps.bindBtn('#step-3-btn', function(e){ - ParseRequest.postCloudCodeData(); - e.preventDefault(); - }); - }, - function(error) { - Steps.fillStepError('#step-2-error', 'There was a failure: ' + error); - }); - XHR.GET('/parse/classes/GameScore'); -}; - -ParseRequest.postCloudCodeData = function() { - XHR.setCallback(function(data){ - // close second step - Steps.closeStep('#step-3'); - Steps.fillStepOutput('#step-3-output', data); - Steps.fillBtn('#step-3-btn', 'Tested'); - // open third step - Steps.showWorkingMessage(); - }, - function(error) { - Steps.fillStepError('#step-3-error', 'There was a failure: ' + error); - }); - XHR.POST('/parse/functions/hello'); -} - - -/** - * Store objectId and other references - */ - -var Store = { - objectId: "" -}; - -var Config = {}; - -Config.getUrl = function() { - if (url) return url; - var port = window.location.port; - var url = window.location.protocol + '//' + window.location.hostname; - if (port) url = url + ':' + port; - return url; -} - - -/** - * XHR object - */ - -var XHR = {}; - -XHR.setCallback = function(callback, failureCallback) { - this.xhttp = new XMLHttpRequest(); - var _self = this; - this.xhttp.onreadystatechange = function() { - if (_self.xhttp.readyState == 4) { - if (_self.xhttp.status >= 200 && _self.xhttp.status <= 299) { - callback(_self.xhttp.responseText); - } else { - failureCallback(_self.xhttp.responseText); - } - } - }; -} - -XHR.POST = function(path, callback) { - var seed = {"score":1337,"playerName":"Sean Plott","cheatMode":false} - this.xhttp.open("POST", Config.getUrl() + path, true); - this.xhttp.setRequestHeader("X-Parse-Application-Id", $('#appId').val()); - this.xhttp.setRequestHeader("Content-type", "application/json"); - this.xhttp.send(JSON.stringify(seed)); -} - -XHR.GET = function(path, callback) { - this.xhttp.open("GET", Config.getUrl() + path + '/' + Store.objectId, true); - this.xhttp.setRequestHeader("X-Parse-Application-Id", $('#appId').val()); - this.xhttp.setRequestHeader("Content-type", "application/json"); - this.xhttp.send(null); -} - - -/** - * Boot - */ - -Steps.init(); diff --git a/public/test.html b/public/test.html deleted file mode 100644 index 429470bd0c..0000000000 --- a/public/test.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - Parse Server Example - - - - - -
-
- -
- -
-

Hi! We've prepared a small 3-steps page to assist you testing your local Parse server.

-

These first steps will help you run and test the Parse server locally and were referrenced by the migration guide provided by Parse Platform.

-
- -

Looks like our local Parse Serve is running under .... Let’s test it?


- -

We'll use an app id of "myAppId" to connect to Parse Server. Or, you can - change it. - -

We have an express server with Parse server running on top of it connected to a MongoDB.

- -

The following steps will try to save some data on parse server and then fetch it back. Hey ho?

- -
-
- 1 -
-
-

Post data to local parse server:

-
-
- Post -
-
- - -
-
-
- -
- -
-
- 2 -
-
-

Fetch data from local parse server:

-
-
- Fetch -
-
- -
-
-
- -
- -
-
- 3 -
-
-

Test Cloud Code function from ./cloud/main.js:

-
-
- TEST -
-
- -
-
-
- -
- - - - - -
- - - - - diff --git a/spec/Tests.spec.js b/spec/Tests.spec.js index 8893f067f8..96fd5add13 100644 --- a/spec/Tests.spec.js +++ b/spec/Tests.spec.js @@ -10,27 +10,8 @@ describe('Parse Server example', () => { }); it('failing test', async () => { const obj = new Parse.Object('Test'); - try { - await obj.save(); - fail('should not have been able to save test object.'); - } catch (e) { - expect(e).toBeDefined(); - expect(e.code).toBe(9001); - expect(e.message).toBe('Saving test objects is not available.'); - } - }); - it('coverage for /', async () => { - const { text, headers } = await Parse.Cloud.httpRequest({ - url: 'http://localhost:30001/', - }); - expect(headers['content-type']).toContain('text/html'); - expect(text).toBe('I dream of being a website. Please star the parse-server repo on GitHub!'); - }); - it('coverage for /test', async () => { - const { text, headers } = await Parse.Cloud.httpRequest({ - url: 'http://localhost:30001/test', - }); - expect(headers['content-type']).toContain('text/html'); - expect(text).toContain('Parse Server Example'); + await expectAsync(obj.save()).toBeRejectedWith( + new Parse.Error(9001, 'Saving test objects is not available.') + ); }); }); diff --git a/spec/helper.js b/spec/helper.js index a68b934dfb..c6caeb6074 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,8 +1,8 @@ -const Parse = require('parse/node'); +import Parse from 'parse'; Parse.initialize('test'); Parse.serverURL = 'http://localhost:30001/test'; Parse.masterKey = 'test'; -const { startParseServer, stopParseServer, dropDB } = require('./utils/test-runner.js'); +import { startParseServer, stopParseServer, dropDB } from './utils/test-runner.js'; beforeAll(async () => { await startParseServer(); }, 100 * 60 * 2); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index cf56823161..97dcf80389 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -5,5 +5,6 @@ ], "helpers": ["helper.js"], "stopSpecOnExpectationFailure": false, - "random": false + "random": false, + "jsLoader": "import" } diff --git a/spec/utils/test-runner.js b/spec/utils/test-runner.js index 1220bb5c9d..b7ae02569f 100644 --- a/spec/utils/test-runner.js +++ b/spec/utils/test-runner.js @@ -1,10 +1,11 @@ -const http = require('http'); -const { ParseServer } = require('parse-server'); -const { config, app } = require('../../index.js'); -const Config = require('../../node_modules/parse-server/lib/Config'); +import http from 'http'; +import { ParseServer } from 'parse-server'; +import { app } from './../../index.js'; +import { config } from './../../src/config.js'; +import Config from './../../node_modules/parse-server/lib/Config.js'; -let parseServerState = {}; -const dropDB = async () => { +export let parseServerState = {}; +export const dropDB = async () => { await Parse.User.logOut(); const app = Config.get('test'); return await app.database.deleteEverything(true); @@ -15,7 +16,7 @@ const dropDB = async () => { * @param {Object} parseServerOptions Used for creating the `ParseServer` * @return {Promise} Runner state */ -async function startParseServer() { +export async function startParseServer() { delete config.databaseAdapter; const parseServerOptions = Object.assign(config, { databaseURI: 'mongodb://localhost:27017/parse-test', @@ -26,18 +27,19 @@ async function startParseServer() { mountPath: '/test', serverURL: `http://localhost:30001/test`, logLevel: 'error', - silent: true + silent: true, }); const parseServer = new ParseServer(parseServerOptions); app.use(parseServerOptions.mountPath, parseServer); const httpServer = http.createServer(app); - await new Promise((resolve) => httpServer.listen(parseServerOptions.port, resolve)); + await new Promise(resolve => httpServer.listen(parseServerOptions.port, resolve)); Object.assign(parseServerState, { parseServer, httpServer, expressApp: app, parseServerOptions, }); + await new Promise(resolve => setTimeout(resolve, 500)); return parseServerOptions; } @@ -45,15 +47,8 @@ async function startParseServer() { * Stops the ParseServer instance * @return {Promise} */ -async function stopParseServer() { +export async function stopParseServer() { const { httpServer } = parseServerState; - await new Promise((resolve) => httpServer.close(resolve)); + await new Promise(resolve => httpServer.close(resolve)); parseServerState = {}; } - -module.exports = { - dropDB, - startParseServer, - stopParseServer, - parseServerState, -}; diff --git a/src/cloud/TestObject.js b/src/cloud/TestObject.js new file mode 100644 index 0000000000..24d38701bc --- /dev/null +++ b/src/cloud/TestObject.js @@ -0,0 +1,28 @@ +Parse.Cloud.beforeSave( + 'TestObject', + ({ object, user }) => { + if (!object.existed()) { + object.set('creator', user); + const acl = new Parse.ACL(user); + // this creates a private TestObject that only the creator can view and edit + object.setACL(acl); + return object; + } + object.revert('creator'); + }, + { + requireUser: true, + skipWithMasterKey: true, + fields: ['name'], + } +); +Parse.Cloud.beforeFind( + 'TestObject', + ({ query, user }) => { + query.equalTo('creator', user); + }, + { + requireUser: true, + skipWithMasterKey: true, + } +); diff --git a/src/cloud/User.js b/src/cloud/User.js new file mode 100644 index 0000000000..e5f67eb806 --- /dev/null +++ b/src/cloud/User.js @@ -0,0 +1,13 @@ +Parse.Cloud.beforeSave( + Parse.User, + ({ object }) => { + if (!object.existed()) { + // new Parse.User. Let's set their ACL to them only. + object.setACL(new Parse.ACL()); + return object; + } + }, + { + skipWithMasterKey: true, + } +); diff --git a/cloud/functions.js b/src/cloud/functions.js similarity index 100% rename from cloud/functions.js rename to src/cloud/functions.js diff --git a/cloud/main.js b/src/cloud/main.js similarity index 62% rename from cloud/main.js rename to src/cloud/main.js index 7c8d64a859..e5faafc954 100644 --- a/cloud/main.js +++ b/src/cloud/main.js @@ -1,2 +1,4 @@ // It is best practise to organize your cloud functions group into their own file. You can then import them in your main.js. -require('./functions.js'); +import('./functions.js'); +import('./User.js'); +import('./TestObject.js'); diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000000..9c8311f481 --- /dev/null +++ b/src/config.js @@ -0,0 +1,9 @@ +import { getSecrets } from './utils/secrets.js'; +const { MASTER_KEY, DATABASE_URI } = await getSecrets('MASTER_KEY', 'DATABASE_URI'); +export const config = { + databaseURI: DATABASE_URI, + cloud: () => import('./cloud/main.js'), + appId: 'myAppId', + masterKey: MASTER_KEY, + serverURL: 'http://localhost:1337/parse', +}; diff --git a/src/public/assets/css/style.css b/src/public/assets/css/style.css new file mode 100644 index 0000000000..80aa5590a0 --- /dev/null +++ b/src/public/assets/css/style.css @@ -0,0 +1,163 @@ +body { + margin: 0; + padding: 0; + font-family: Helvetica, Arial, sans-serif; + font-size: 14px; + letter-spacing: 0.2px; + line-height: 24px; + color: #585858; +} + +a { + color: #169cee; + text-decoration: underline; +} + +a:hover { + color: #2c3d50; +} + +a:visited { + color: #2a6496; +} + +/* + helpers + */ + +.align-center { + text-align: center; +} + +.hidden { + display: none; +} + +/* + app css + */ + +.container { + margin: 0 auto; + margin-top: 45px; + max-width: 860px; +} + +#parse-logo { + width: 109px; + height: 110px; + margin: 0 0 20px; + text-align: center; +} + +.up-and-running, +.time-to-deploy { + font-weight: bold; +} + +.advice { + margin-bottom: 40px; +} + +.advice { + background: #f4f4f4; + border-radius: 4px; + -webkit-border-radius: 4px 4px; + -moz-border-radius: 4px 4px; + -ms-border-radius: 4px 4px; + -o-border-radius: 4px 4px; + padding: 10px 20px; +} + +#parse-url { + color: #169cee; + font-weight: bold; +} + +#regForm { + background-color: #ffffff; + margin: 20px auto; + padding: 20px; + width: 70%; + min-width: 300px; +} + +table { + table-layout: fixed; + width: 100%; +} + +/* Style the input fields */ +input { + padding: 10px; + width: 100%; + font-size: 17px; + border: 1px solid #aaaaaa; +} + +/* Mark input boxes that gets an error on validation: */ +input.invalid { + background-color: #ffdddd; +} + +/* Hide all steps by default: */ +.tab { + display: none; + text-align: center; +} +#userDetails { + display: none; + text-align: center; +} + +button { + background-color: #169cee; + color: #ffffff; + border: none; + padding: 10px 20px; + font-size: 17px; + cursor: pointer; +} + +button:hover { + opacity: 0.8; +} + +#prod-test { + margin-bottom: 60px; +} + +#prod-test input { + background-color: #fff; + border: 1px solid #b5b5b5; + color: #000000; + font-family: 'Inconsolata'; + font-size: 16px; + line-height: 17px; + padding: 12px; + width: 260px; + border-radius: 4px; + -webkit-border-radius: 4px 4px; + -moz-border-radius: 4px 4px; + -ms-border-radius: 4px 4px; + -o-border-radius: 4px 4px; + display: block; + margin-bottom: 10px; +} + +#footer { + border-top: 1px solid #e2e2e2; + padding: 20px; +} + +#footer ul li { + list-style-type: none; + display: inline-block; +} +#footer ul li:after { + content: '-'; + padding: 10px; +} +#footer ul li:last-child:after { + content: ''; +} diff --git a/public/assets/images/parse-logo.png b/src/public/assets/images/parse-logo.png similarity index 100% rename from public/assets/images/parse-logo.png rename to src/public/assets/images/parse-logo.png diff --git a/src/public/assets/js/script.js b/src/public/assets/js/script.js new file mode 100644 index 0000000000..bf2ba1f84e --- /dev/null +++ b/src/public/assets/js/script.js @@ -0,0 +1,127 @@ +/* eslint-disable no-unused-vars */ +function loadParse(appId, serverURL) { + Parse.initialize(appId); + Parse.serverURL = serverURL; + if (Parse.User.current()) { + showLoggedIn(); + showTab(1); + } +} + +async function login() { + const { username, password } = getFormValues(); + if (!username || !password) { + updateStatus('Please correct the invalid fields.'); + return; + } + await resolve(Parse.User.logIn(username, password)); + showLoggedIn(); + showTab(1); + updateStatus('Successfully logged in! Now, lets save an object.'); +} + +async function signup() { + const { username, password } = getFormValues(); + if (!username || !password) { + updateStatus('Please correct the invalid fields.'); + return; + } + await resolve(Parse.User.signUp(username, password)); + showLoggedIn(); + showTab(1); + updateStatus('Successfully signed up! Now, lets save an object.'); +} +function showLoggedIn() { + document.getElementById('userDetails').style.display = 'block'; + document.getElementById('currentUser').innerHTML = Parse.User.current().getUsername(); +} + +async function logout() { + await resolve(Parse.User.logOut()); + showTab(0); + updateStatus('Successfully logged out.'); + document.getElementById('userDetails').style.display = 'none'; +} +let testObjectSaved = false; +async function saveObject() { + if (testObjectSaved) { + showTab(2); + findObjects(); + return; + } + const nameField = document.getElementById('name'); + const name = nameField.value; + if (!name) { + nameField.className = 'invalid'; + updateStatus('Please enter an object name.'); + return; + } + const TestObject = new Parse.Object('TestObject'); + TestObject.set('name', name); + await resolve(TestObject.save()); + updateStatus(`Test Object saved with id: ${TestObject.id}.`); + document.getElementById('saveButton').innerHTML = 'Next'; + testObjectSaved = true; +} + +async function findObjects() { + const query = new Parse.Query('TestObject'); + const objects = await resolve(query.find()); + let innerHTML = 'IDNameCreated At'; + for (const object of objects) { + innerHTML += `${object.id}${object.get('name')}${object + .get('createdAt') + .toISOString()}`; + } + document.getElementById('testTable').innerHTML = innerHTML; + updateStatus(`${objects.length} TestObject's found.`); +} + +async function callFunction() { + const cloudResult = await resolve(Parse.Cloud.run('hello')); + updateStatus(`Cloud function 'hello' ran with result: ${cloudResult}`); +} + +// Utilities + +function getFormValues() { + const usernameField = document.getElementById('username'); + const username = usernameField.value; + if (!username) { + usernameField.className = 'invalid'; + } + const passwordField = document.getElementById('password'); + const password = passwordField.value; + if (!password) { + passwordField.className = 'invalid'; + } + return { username, password }; +} + +function showTab(n) { + updateStatus(''); + const tabs = document.getElementsByClassName('tab'); + for (const tab of tabs) { + tab.style.display = 'none'; + } + tabs[n].style.display = 'block'; +} +function updateStatus(text) { + const statuses = document.getElementsByClassName('status'); + for (const status of statuses) { + status.innerHTML = text; + } +} +async function resolve(promise) { + try { + updateStatus('Loading...'); + const result = await Promise.resolve(promise); + updateStatus(''); + return result; + } catch (e) { + updateStatus(e && e.message); + throw e; + } +} +showTab(0); +/* eslint-enable no-unused-vars */ diff --git a/src/utils/secrets.js b/src/utils/secrets.js new file mode 100644 index 0000000000..7bc15a84f9 --- /dev/null +++ b/src/utils/secrets.js @@ -0,0 +1,30 @@ +import AWS from 'aws-sdk'; +export const getSecret = secretName => { + if (process.env.TESTING) { + return; + } + const client = new AWS.SecretsManager({ + region: process.env.AWS_REGION, + }); + return new Promise((resolve, reject) => + client.getSecretValue({ SecretId: secretName }, (err, data) => { + if (err) { + reject(err); + return; + } + try { + resolve(JSON.parse(data.SecretString)); + } catch (e) { + resolve(data.SecretString); + } + }) + ); +}; +export const getSecrets = async (...secretsArray) => { + const results = await Promise.all(secretsArray.map(secret => getSecret(secret))); + const result = {}; + for (let i = 0; i < secretsArray.length; i++) { + result[secretsArray[i]] = results[i] || {}; + } + return result; +}; diff --git a/src/views/test.html b/src/views/test.html new file mode 100644 index 0000000000..b9439e82bd --- /dev/null +++ b/src/views/test.html @@ -0,0 +1,123 @@ + + + + Parse Server Example + + + + + +
+
+ +
+ +
+

+ Hi, and welcome to Parse Server! We've prepared a small 3-steps page to + assist you testing your local Parse server. +

+

These first steps will help you run and test the Parse server locally.

+
+ +

+ Looks like our local Parse Server is running under + <%= serverUrl %>. Let’s test it? +

+ +

+ We have an express server with Parse server running on top of it connected to a MongoDB. +

+ +

+ The following steps will try to save some data on parse server and then fetch it back. Hey + ho? +

+ +
+ Currently logged in as: +

+ + +
+ await Parse.User.logOut();
+
+
+
+
+

Step One: Login or Signup

+

+

+ +

+ + +

+ + await Parse.User.logIn(username, password);
+ // or
+ const user = new Parse.User();
+ user.setUsername(username);
+ user.setPassword(password);
+ await user.signUp(); +
+
+ +
+

Step Two: Save a TestObject

+

+ +

+ + + const obj = new Parse.Object('TestObject');
+ obj.set('name', name);
+ await obj.save(); +
+
+ +
+

Step Three: Find TestObjects

+
+ +

+ + + const query = new Parse.Query('TestObject');
+ const objects = await query.find(); +
+
+ +
+

Step Four: Call a Cloud Function

+ +

+ + const result = await Parse.Cloud.run('hello'); +
+
+ + +
+ + + +