Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create await new ParseServer(config).startApp() to resolve cloud code and schema before express app is exposed #7914

Closed
wants to merge 53 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9ef1968
ci: fix node engine check (#7891)
mtrezza Mar 25, 2022
f63fb2b
fix: return correct response when revert is used in beforeSave (#7839)
dblythy Mar 26, 2022
b3199d7
chore(release): 5.2.1-alpha.1 [skip ci]
semantic-release-bot Mar 26, 2022
48bd512
perf: reduce database operations when using the constant parameter in…
dblythy Mar 26, 2022
e0cca58
chore(release): 5.2.1-alpha.2 [skip ci]
semantic-release-bot Mar 26, 2022
90155cf
feat: add MongoDB 5.1 compatibility (#7682)
github-actions[bot] Mar 27, 2022
499cead
chore(release): 5.3.0-alpha.1 [skip ci]
semantic-release-bot Mar 27, 2022
ef56e98
fix: security upgrade parse push adapter from 4.1.0 to 4.1.2 (#7893)
mtrezza Mar 27, 2022
119dbe0
chore(release): 5.3.0-alpha.2 [skip ci]
semantic-release-bot Mar 27, 2022
6b4b358
feat: add MongoDB 5.2 support (#7894)
mtrezza Mar 27, 2022
75eca2d
chore(release): 5.3.0-alpha.3 [skip ci]
semantic-release-bot Mar 27, 2022
0cec09f
fix: allow ES import for cloud string if package type is module
dblythy Mar 31, 2022
4e14aa2
revert helper
dblythy Mar 31, 2022
132a3e0
refactor: upgrade mongodb from 4.3.1 to 4.4.1 (#7906)
snyk-bot Apr 3, 2022
d74b6a3
ci: bump environment (#7907)
github-actions[bot] Apr 3, 2022
2b0957c
refactor: add missing schema definitions (#7917)
Moumouls Apr 3, 2022
a72b384
fix: custom database options are not passed to MongoDB GridFS (#7911)
dblythy Apr 4, 2022
0b929db
chore(release): 5.3.0-alpha.4 [skip ci]
semantic-release-bot Apr 4, 2022
cbdf7e8
Update ParseServer.js
dblythy Apr 5, 2022
ef3680a
refactor: upgrade @apollo/client from 3.5.8 to 3.5.9 (#7921)
snyk-bot Apr 9, 2022
6b68593
fix: security upgrade moment from 2.29.1 to 2.29.2 (#7931)
dependabot[bot] Apr 9, 2022
e88fb72
chore(release): 5.3.0-alpha.5 [skip ci]
semantic-release-bot Apr 9, 2022
a648709
Merge branch 'alpha' into await-cloud-code
mtrezza Apr 9, 2022
b7a1d76
fix: peer dependency mismatch for GraphQL dependencies (#7934)
marvinroger Apr 11, 2022
34b8110
chore(release): 5.3.0-alpha.6 [skip ci]
semantic-release-bot Apr 11, 2022
43ad5b3
refactor: upgrade @graphql-tools/links from 8.2.2 to 8.2.4 (#7932)
snyk-bot Apr 11, 2022
b45d44e
refactor: upgrade @graphql-tools/links from 8.2.4 to 8.2.6 (#7935)
snyk-bot Apr 15, 2022
8b91961
refactor: upgrade @graphql-tools/links from 8.2.6 to 8.2.7 (#7941)
dplewis Apr 17, 2022
122afcf
refactor: upgrade body-parser from 1.19.1 to 1.19.2 (#7944)
dplewis Apr 20, 2022
8868628
refactor: upgrade @graphql-tools/links from 8.2.7 to 8.2.8 (#7945)
dplewis Apr 22, 2022
20fc4e2
fix: security upgrade @parse/fs-files-adapter from 1.2.1 to 1.2.2 (#7…
snyk-bot Apr 25, 2022
8cf0b5e
chore(release): 5.3.0-alpha.7 [skip ci]
semantic-release-bot Apr 25, 2022
ac80bdd
refactor: upgrade winston from 3.5.1 to 3.7.2 (#7954)
mtrezza Apr 27, 2022
901a3ea
rename method to create
dblythy Apr 28, 2022
08e6c34
resolve .app
dblythy Apr 28, 2022
29fc673
Merge branch 'alpha' into await-cloud-code
dblythy Apr 28, 2022
efe75c2
Update index.spec.js
dblythy Apr 28, 2022
e6c45f5
create startApp function
dblythy Apr 28, 2022
b23d346
Update index.spec.js
dblythy Apr 28, 2022
2ae3980
increase coverage
dblythy Apr 28, 2022
81de641
Update DefinedSchemas.js
dblythy Apr 28, 2022
d7ee114
close servers
dblythy Apr 28, 2022
1f8a4ee
Update index.js
dblythy Apr 28, 2022
7f99592
fix tests
dblythy Apr 28, 2022
caa75f6
Update ParseServer.js
dblythy Apr 29, 2022
dec4082
Update ParseServer.js
dblythy Apr 29, 2022
d65be73
add holdPublicRoutes
dblythy May 3, 2022
61ef23f
Merge remote-tracking branch 'upstream/alpha' into alpha
dblythy May 3, 2022
60145a2
Merge branch 'alpha' into await-cloud-code
dblythy May 3, 2022
7cd5e38
definitions
dblythy May 3, 2022
871f13c
Merge branch 'alpha' into await-cloud-code
mtrezza May 3, 2022
d378e72
Merge branch 'alpha' into await-cloud-code
dblythy May 16, 2022
c29ba4f
Merge branch 'alpha' into await-cloud-code
dblythy Sep 18, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
["@babel/preset-env", {
"targets": {
"node": "12"
}
},
"exclude": ["proposal-dynamic-import"]
}]
],
"sourceMaps": "inline"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ jobs:
image: redis
ports:
- 6379:6379
env:
env:
MONGODB_VERSION: ${{ matrix.MONGODB_VERSION }}
MONGODB_TOPOLOGY: ${{ matrix.MONGODB_TOPOLOGY }}
MONGODB_STORAGE_ENGINE: ${{ matrix.MONGODB_STORAGE_ENGINE }}
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![parse-repository-header-server](https://user-images.githubusercontent.com/5673677/138278489-7d0cebc5-1e31-4d3c-8ffb-53efcda6f29d.png)

---

[![Build Status](https://github.com/parse-community/parse-server/workflows/ci/badge.svg?branch=alpha)](https://github.com/parse-community/parse-server/actions?query=workflow%3Aci+branch%3Aalpha)
[![Snyk Badge](https://snyk.io/test/github/parse-community/parse-server/badge.svg)](https://snyk.io/test/github/parse-community/parse-server)
[![Coverage](https://img.shields.io/codecov/c/github/parse-community/parse-server/alpha.svg)](https://codecov.io/github/parse-community/parse-server?branch=alpha)
Expand Down Expand Up @@ -393,7 +393,7 @@ const server = ParseServer({
},

// The password policy
passwordPolicy: {
passwordPolicy: {
// Enforce a password of at least 8 characters which contain at least 1 lower case, 1 upper case and 1 digit
validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/,
// Do not allow the username as part of the password
Expand Down Expand Up @@ -434,7 +434,7 @@ const api = new ParseServer({

The above route can be invoked by sending a `GET` request to:
`https://[parseServerPublicUrl]/[parseMount]/[pagesEndpoint]/[appId]/[customRoute]`

The `handler` receives the `request` and returns a `custom_page.html` webpage from the `pages.pagesPath` directory as response. The advantage of building a custom route this way is that it automatically makes use of Parse Server's built-in capabilities, such as [page localization](#pages) and [dynamic placeholders](#dynamic-placeholders).

### Reserved Paths
Expand Down Expand Up @@ -522,7 +522,7 @@ Parse Server allows developers to choose from several options when hosting files
`GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using Amazon S3, Google Cloud Storage, or local file storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters).

## Idempotency Enforcement

**Caution, this is an experimental feature that may not be appropriate for production.**

This feature deduplicates identical requests that are received by Parse Server multiple times, typically due to network issues or network adapter access restrictions on mobile operating systems.
Expand Down
58 changes: 58 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
const Config = require('../lib/Config');
const Parse = require('parse/node');
const ParseServer = require('../lib/index').ParseServer;
const request = require('../lib/request');
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter')
.InMemoryCacheAdapter;
Expand Down Expand Up @@ -39,6 +40,63 @@ describe('Cloud Code', () => {
});
});

it('should wait for cloud code to load', async () => {
const initiated = new Date();
const parseServer = await new ParseServer({
appId: 'test2',
masterKey: 'abc',
serverURL: 'http://localhost:12668/parse',
silent: true,
async cloud() {
await new Promise(resolve => setTimeout(resolve, 1000));
Parse.Cloud.beforeSave('Test', () => {
throw 'Cannot save.';
});
},
}).startApp();
const express = require('express');
const app = express();
app.use('/parse', parseServer);
const server = app.listen(12668);

const now = new Date();
expect(now.getTime() - initiated.getTime() > 1000).toBeTrue();
await expectAsync(new Parse.Object('Test').save()).toBeRejectedWith(
new Parse.Error(141, 'Cannot save.')
);
await new Promise(resolve => server.close(resolve));
});

it('can call startApp twice', async () => {
const server = await new ParseServer({
appId: 'test2',
masterKey: 'abc',
serverURL: 'http://localhost:12668/parse',
silent: true,
async cloud() {
await new Promise(resolve => setTimeout(resolve, 1000));
Parse.Cloud.beforeSave('Test', () => {
throw 'Cannot save.';
});
},
}).startApp();
await server.startApp();
});

it('can load cloud code as a module', async () => {
process.env.npm_package_type = 'module';
await reconfigureServer({ cloud: './spec/cloud/cloudCodeModuleFile.js' });
const result = await Parse.Cloud.run('cloudCodeInFile');
expect(result).toEqual('It is possible to define cloud code in a file.');
delete process.env.npm_package_type;
});

it('cloud code must be valid type', async () => {
await expectAsync(reconfigureServer({ cloud: true })).toBeRejectedWith(
"argument 'cloud' must either be a string or a function"
);
});

it('can create functions', done => {
Parse.Cloud.define('hello', () => {
return 'Hello world!';
Expand Down
3 changes: 2 additions & 1 deletion spec/DefinedSchemas.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ describe('DefinedSchemas', () => {
const logger = require('../lib/logger').logger;
spyOn(DefinedSchemas.prototype, 'wait').and.resolveTo();
spyOn(logger, 'error').and.callThrough();
spyOn(Parse.Schema, 'all').and.callFake(() => {
spyOn(DefinedSchemas.prototype, 'createDeleteSession').and.callFake(() => {
throw error;
});

Expand All @@ -642,6 +642,7 @@ describe('DefinedSchemas', () => {

expect(logger.error).toHaveBeenCalledWith(`Failed to run migrations: ${error.toString()}`);
});

it('should perform migration in parallel without failing', async () => {
const server = await reconfigureServer();
const logger = require('../lib/logger').logger;
Expand Down
3 changes: 3 additions & 0 deletions spec/cloud/cloudCodeModuleFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Parse.Cloud.define('cloudCodeInFile', () => {
return 'It is possible to define cloud code in a file.';
});
2 changes: 2 additions & 0 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ beforeAll(async () => {
}
}
await reconfigureServer();
});

beforeEach(() => {
Parse.initialize('test', 'test', 'test');
Parse.serverURL = 'http://localhost:' + port + '/1';
});
Expand Down
51 changes: 51 additions & 0 deletions spec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,57 @@ describe('server', () => {
.catch(done.fail);
});

it('should wait for schemas to load', async () => {
const spy = spyOn(process.stdout, 'write');
const config = {
appId: 'aTestApp',
masterKey: 'aTestMasterKey',
serverURL: 'http://localhost:12669/parse',
schema: {
definitions: [
{ className: '_User' },
{ className: 'Test', classLevelPermissions: { addField: { '*': true } } },
],
},
serverStartComplete() {},
};
const startSpy = spyOn(config, 'serverStartComplete').and.callFake(() => {});
const parseServer = await new ParseServer.ParseServer(config).startApp();
expect(startSpy).toHaveBeenCalled();
expect(Parse.applicationId).toEqual('aTestApp');
expect(spy).toHaveBeenCalledWith('info: Running Migrations Completed\n');
const app = express();
app.use('/parse', parseServer);
const server = app.listen(12669);
const schema = await Parse.Schema.all();
expect(schema.length).toBeGreaterThanOrEqual(3);
await new Promise(resolve => server.close(resolve));
});

it('call call startApp with mountPublicRoutes', async () => {
const config = {
appId: 'aTestApp',
masterKey: 'aTestMasterKey',
serverURL: 'http://localhost:12701/parse',
holdPublicRoutes: true,
};
const parseServer = await new ParseServer.ParseServer(config).startApp();
expect(Parse.applicationId).toEqual('aTestApp');
expect(Parse.serverURL).toEqual('http://localhost:12701/parse');
const app = express();
app.use('/parse', parseServer);
const server = app.listen(12701);
const testObject = new Parse.Object('TestObject');
// api usage requires masterKey until mountPublicRoutes is called
await expectAsync(testObject.save(null, { useMasterKey: true })).toBeResolved();
await expectAsync(testObject.save()).toBeRejectedWith(
new Parse.Error(undefined, 'unauthorized: master key is required')
);
parseServer.mountPublicRoutes();
await expectAsync(testObject.save()).toBeResolved();
await new Promise(resolve => server.close(resolve));
});

it('should not fail when Google signin is introduced without the optional clientId', done => {
const jwt = require('jsonwebtoken');

Expand Down
5 changes: 5 additions & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class Config {
enforcePrivateUsers,
schema,
requestKeywordDenylist,
holdPublicRoutes,
}) {
if (masterKey === readOnlyMasterKey) {
throw new Error('masterKey and readOnlyMasterKey should be different');
Expand All @@ -103,6 +104,10 @@ export class Config {
throw 'revokeSessionOnPasswordReset must be a boolean value';
}

if (typeof holdPublicRoutes !== 'boolean') {
throw 'holdPublicRoutes must be a boolean value';
}

if (publicServerURL) {
if (!publicServerURL.startsWith('http://') && !publicServerURL.startsWith('https://')) {
throw 'publicServerURL should be a valid HTTPS URL starting with https://';
Expand Down
7 changes: 7 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_GRAPH_QLSCHEMA',
help: 'Full path to your GraphQL custom schema.graphql file',
},
holdPublicRoutes: {
env: 'PARSE_SERVER_HOLD_PUBLIC_ROUTES',
help:
'Set to true if Parse Server should require masterKey access before `mountPublicRoutes()` is called.',
action: parsers.booleanParser,
default: false,
},
host: {
env: 'PARSE_SERVER_HOST',
help: 'The host to serve ParseServer on, defaults to 0.0.0.0',
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
* @property {FileUploadOptions} fileUpload Options for file uploads
* @property {String} graphQLPath Mount path for the GraphQL endpoint, defaults to /graphql
* @property {String} graphQLSchema Full path to your GraphQL custom schema.graphql file
* @property {Boolean} holdPublicRoutes Set to true if Parse Server should require masterKey access before `mountPublicRoutes()` is called.
* @property {String} host The host to serve ParseServer on, defaults to 0.0.0.0
* @property {IdempotencyOptions} idempotencyOptions Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.
* @property {String} javascriptKey Key for the Javascript SDK
Expand Down
3 changes: 3 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@ export interface ParseServerOptions {
/* An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.
:DEFAULT: [{"key":"_bsontype","value":"Code"},{"key":"constructor"},{"key":"__proto__"}] */
requestKeywordDenylist: ?(RequestKeywordDenylist[]);
/* Set to true if Parse Server should require masterKey access before `mountPublicRoutes()` is called.
:DEFAULT: false */
holdPublicRoutes: ?boolean;
}

export interface SecurityOptions {
Expand Down
87 changes: 68 additions & 19 deletions src/ParseServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,45 +83,83 @@ class ParseServer {
logging.setLogger(loggerController);

// Note: Tests will start to fail if any validation happens after this is called.
databaseController
.performInitialization()
.then(() => hooksController.load())
.then(async () => {
(async () => {
try {
await databaseController.performInitialization();
await hooksController.load();
if (schema) {
await new DefinedSchemas(schema, this.config).execute();
}
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
await cloud(Parse);
} else if (typeof cloud === 'string') {
if (process.env.npm_package_type === 'module') {
await import(path.resolve(process.cwd(), cloud));
} else {
require(path.resolve(process.cwd(), cloud));
}
} else {
throw "argument 'cloud' must either be a string or a function";
}
}
if (serverStartComplete) {
serverStartComplete();
await Promise.resolve(serverStartComplete());
}
if (this.startCallbackSuccess) {
this.startCallbackSuccess(this.app);
}
this.started = true;
} catch (error) {
if (this.startCallbackError) {
this.startCallbackError(error);
}
})
.catch(error => {
if (serverStartComplete) {
serverStartComplete(error);
} else {
console.error(error);
process.exit(1);
}
});

if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
cloud(Parse);
} else if (typeof cloud === 'string') {
require(path.resolve(process.cwd(), cloud));
} else {
throw "argument 'cloud' must either be a string or a function";
}
}
})();

if (security && security.enableCheck && security.enableCheckLog) {
new CheckRunner(options.security).run();
}
}

/**
* Starts the Parse Server to be served as an express. Resolves when parse-server is ready to accept external traffic.
* Note: when using `await startApp()`, server JS SDK methods will not be available until expressApp.listen(port) is called.
* @returns {Promise<function>} express middleware
*/

startApp() {
return new Promise((resolve, reject) => {
if (this.started) {
resolve(this.app);
return;
}
this.startCallbackSuccess = resolve;
this.startCallbackError = reject;
dblythy marked this conversation as resolved.
Show resolved Hide resolved
});
}

/**
* Method that is called when Parse Server is ready to
* @returns {Promise<function>} express middleware
*/

mountPublicRoutes() {
this.config.publicRoutesAvailable = true;
}

get app() {
if (!this._app) {
this._app = ParseServer.app(this.config);
this._app.startApp = async () => await this.startApp();
this._app.mountPublicRoutes = () => this.mountPublicRoutes();
}
return this._app;
}
Expand Down Expand Up @@ -152,7 +190,7 @@ class ParseServer {
* Create an express app for the parse server
* @param {Object} options let you specify the maxUploadSize when creating the express app */
static app(options) {
const { maxUploadSize = '20mb', appId, directAccess, pages } = options;
const { maxUploadSize = '20mb', appId, directAccess, pages, holdPublicRoutes } = options;
// This app serves the Parse API directly.
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
var api = express();
Expand Down Expand Up @@ -184,6 +222,17 @@ class ParseServer {
api.use(middlewares.allowMethodOverride);
api.use(middlewares.handleParseHeaders);

if (holdPublicRoutes) {
options.publicRoutesAvailable = false;
api.use((req, res, next) => {
if (options.publicRoutesAvailable) {
next();
return;
}
middlewares.enforceMasterKeyAccess(req, res, next);
});
}

const appRouter = ParseServer.promiseRouter({ appId });
api.use(appRouter.expressRouter());

Expand Down
Loading