From 12395b9fec07300d96ab584361b2fba17ccb7881 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 27 Feb 2021 04:55:02 +0100 Subject: [PATCH 01/10] added custom routes --- public/custom_page.html | 15 ++++ resources/buildConfigDefinitions.js | 3 +- spec/PagesRouter.spec.js | 126 ++++++++++++++++++++++++++++ src/Options/Definitions.js | 18 ++++ src/Options/docs.js | 7 ++ src/Options/index.js | 10 +++ src/Routers/PagesRouter.js | 21 +++++ 7 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 public/custom_page.html diff --git a/public/custom_page.html b/public/custom_page.html new file mode 100644 index 0000000000..08a2b3e63c --- /dev/null +++ b/public/custom_page.html @@ -0,0 +1,15 @@ + + + + + + {{appName}} + + + +

{{appName}}

+ + + diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 81492a75d4..6ea88b4d65 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -43,6 +43,7 @@ function getENVPrefix(iface) { const options = { 'ParseServerOptions' : 'PARSE_SERVER_', 'PagesOptions' : 'PARSE_SERVER_PAGES_', + 'PagesRoute': 'PARSE_SERVER_PAGES_ROUTE_', 'PagesCustomUrlsOptions' : 'PARSE_SERVER_PAGES_CUSTOM_URL_', 'CustomPagesOptions' : 'PARSE_SERVER_CUSTOM_PAGES_', 'LiveQueryServerOptions' : 'PARSE_LIVE_QUERY_SERVER_', @@ -166,7 +167,7 @@ function parseDefaultValue(elt, value, t) { if (type == 'NumberOrBoolean') { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } - const literalTypes = ['Object', 'IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions']; + const literalTypes = ['Object', 'PagesRoute', 'IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions']; if (literalTypes.includes(type)) { const object = parsers.objectParser(value); const props = Object.keys(object).map((key) => { diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 6a22657ba7..24acbb2f03 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -971,5 +971,131 @@ describe('Pages Router', () => { expect(response.text).toBe('Not found.'); }); }); + + describe('custom route', () => { + it('handles custom route with GET', async () => { + config.pages.customRoutes = [ + { + method: 'GET', + path: 'custom_page', + handler: async req => { + expect(req).toBeDefined(); + expect(req.method).toBe('GET'); + return { file: 'custom_page.html' }; + }, + }, + ]; + await reconfigureServer(config); + const handlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough(); + + const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`; + const response = await request({ + url: url, + followRedirects: false, + }).catch(e => e); + expect(response.status).toBe(200); + expect(response.text).toMatch(config.appName); + expect(handlerSpy).toHaveBeenCalled(); + }); + + it('handles custom route with POST', async () => { + config.pages.customRoutes = [ + { + method: 'POST', + path: 'custom_page', + handler: async req => { + expect(req).toBeDefined(); + expect(req.method).toBe('POST'); + return { file: 'custom_page.html' }; + }, + }, + ]; + const handlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough(); + await reconfigureServer(config); + + const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`; + const response = await request({ + url: url, + followRedirects: false, + method: 'POST', + }).catch(e => e); + expect(response.status).toBe(200); + expect(response.text).toMatch(config.appName); + expect(handlerSpy).toHaveBeenCalled(); + }); + + it('handles multiple custom routes', async () => { + config.pages.customRoutes = [ + { + method: 'GET', + path: 'custom_page', + handler: async req => { + expect(req).toBeDefined(); + expect(req.method).toBe('GET'); + return { file: 'custom_page.html' }; + }, + }, + { + method: 'POST', + path: 'custom_page', + handler: async req => { + expect(req).toBeDefined(); + expect(req.method).toBe('POST'); + return { file: 'custom_page.html' }; + }, + }, + ]; + const getHandlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough(); + const postHandlerSpy = spyOn(config.pages.customRoutes[1], 'handler').and.callThrough(); + await reconfigureServer(config); + + const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`; + const getResponse = await request({ + url: url, + followRedirects: false, + method: 'GET', + }).catch(e => e); + expect(getResponse.status).toBe(200); + expect(getResponse.text).toMatch(config.appName); + expect(getHandlerSpy).toHaveBeenCalled(); + + const postResponse = await request({ + url: url, + followRedirects: false, + method: 'POST', + }).catch(e => e); + expect(postResponse.status).toBe(200); + expect(postResponse.text).toMatch(config.appName); + expect(postHandlerSpy).toHaveBeenCalled(); + }); + + it('handles custom route with async handler', async () => { + config.pages.customRoutes = [ + { + method: 'GET', + path: 'custom_page', + handler: async req => { + expect(req).toBeDefined(); + expect(req.method).toBe('GET'); + const file = await new Promise(resolve => + setTimeout(resolve('custom_page.html'), 1000) + ); + return { file }; + }, + }, + ]; + await reconfigureServer(config); + const handlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough(); + + const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`; + const response = await request({ + url: url, + followRedirects: false, + }).catch(e => e); + expect(response.status).toBe(200); + expect(response.text).toMatch(config.appName); + expect(handlerSpy).toHaveBeenCalled(); + }); + }); }); }); diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index c67017a585..5b07c8a061 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -425,6 +425,12 @@ module.exports.ParseServerOptions = { }, }; module.exports.PagesOptions = { + customRoutes: { + env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES', + help: 'The custom routes.', + action: parsers.arrayParser, + default: [], + }, customUrls: { env: 'PARSE_SERVER_PAGES_CUSTOM_URLS', help: 'The URLs to the custom pages.', @@ -481,6 +487,18 @@ module.exports.PagesOptions = { default: {}, }, }; +module.exports.PagesRoute = { + method: { + env: 'PARSE_SERVER_PAGES_ROUTE_METHOD', + help: "The route method, e.g. 'GET' or 'POST'.", + required: true, + }, + path: { + env: 'PARSE_SERVER_PAGES_ROUTE_PATH', + help: 'The route path.', + required: true, + }, +}; module.exports.PagesCustomUrlsOptions = { emailVerificationLinkExpired: { env: 'PARSE_SERVER_PAGES_CUSTOM_URL_EMAIL_VERIFICATION_LINK_EXPIRED', diff --git a/src/Options/docs.js b/src/Options/docs.js index da90760389..a6d64b9176 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -82,6 +82,7 @@ /** * @interface PagesOptions + * @property {Generic[]} customRoutes The custom routes. * @property {PagesCustomUrlsOptions} customUrls The URLs to the custom pages. * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. * @property {Boolean} enableRouter Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production. @@ -93,6 +94,12 @@ * @property {Object} placeholders The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function. */ +/** + * @interface PagesRoute + * @property {String} method The route method, e.g. 'GET' or 'POST'. + * @property {String} path The route path. + */ + /** * @interface PagesCustomUrlsOptions * @property {String} emailVerificationLinkExpired The URL to the custom page for email verification -> link expired. diff --git a/src/Options/index.js b/src/Options/index.js index e333b53694..24ca19df3f 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -256,6 +256,16 @@ export interface PagesOptions { /* The URLs to the custom pages. :DEFAULT: {} */ customUrls: ?PagesCustomUrlsOptions; + /* The custom routes. + :DEFAULT: [] */ + customRoutes: ?(PagesRoute[]); +} + +export interface PagesRoute { + /* The route path. */ + path: string; + /* The route method, e.g. 'GET' or 'POST'. */ + method: string; } export interface PagesCustomUrlsOptions { diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 3243d5a58c..2348fe92cf 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -77,6 +77,8 @@ export class PagesRouter extends PromiseRouter { : path.resolve(__dirname, '../../public'); this.loadJsonResource(); this.mountPagesRoutes(); + this.mountCustomRoutes(); + this.mountStaticRoute(); } verifyEmail(req) { @@ -696,7 +698,26 @@ export class PagesRouter extends PromiseRouter { return this.requestResetPassword(req); } ); + } + + mountCustomRoutes() { + for (const route of this.pagesConfig.customRoutes || []) { + this.route( + route.method, + `/${this.pagesEndpoint}/:appId/${route.path}`, + req => { + this.setConfig(req); + }, + async req => { + const { file, query = {} } = await route.handler(req); + const page = new Page({ id: file, defaultFile: file }); + return this.goToPage(req, page, query, false); + } + ); + } + } + mountStaticRoute() { this.route( 'GET', `/${this.pagesEndpoint}/(*)?`, From fd09d0e7d1799bee1a35b24e3858ee7a5d8f3882 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 27 Feb 2021 04:55:17 +0100 Subject: [PATCH 02/10] fixed docs typos --- src/Routers/PagesRouter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 2348fe92cf..a26b68de6e 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -285,7 +285,7 @@ export class PagesRouter extends PromiseRouter { // Add locale to params to ensure it is passed on with every request; // that means, once a locale is set, it is passed on to any follow-up page, - // e.g. request_password_reset -> password_reset -> passwort_reset_success + // e.g. request_password_reset -> password_reset -> password_reset_success const locale = this.getLocale(req); params[pageParams.locale] = locale; @@ -565,7 +565,7 @@ export class PagesRouter extends PromiseRouter { } /** - * Creates a response with http rediret. + * Creates a response with http redirect. * @param {Object} req The express request. * @param {String} path The path of the file to return. * @param {Object} params The query parameters to include. From b9115cb9b04cdc87e56094afc5a4f81cd6830028 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 27 Feb 2021 17:21:10 +0100 Subject: [PATCH 03/10] added page.customRoutes config validation --- spec/PagesRouter.spec.js | 7 +++++++ src/Config.js | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 24acbb2f03..7e77899fb9 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -252,6 +252,9 @@ describe('Pages Router', () => { expect(Config.get(Parse.applicationId).pages.customUrls).toBe( Definitions.PagesOptions.customUrls.default ); + expect(Config.get(Parse.applicationId).pages.customRoutes).toBe( + Definitions.PagesOptions.customRoutes.default + ); }); it('throws on invalid configuration', async () => { @@ -296,6 +299,10 @@ describe('Pages Router', () => { { localizationFallbackLocale: 0 }, { localizationFallbackLocale: {} }, { localizationFallbackLocale: [] }, + { customRoutes: true }, + { customRoutes: 0 }, + { customRoutes: 'a' }, + { customRoutes: {} }, ]; for (const option of options) { await expectAsync(reconfigureServerWithPagesConfig(option)).toBeRejected(); diff --git a/src/Config.js b/src/Config.js index 0dacc5cbe0..93f4525187 100644 --- a/src/Config.js +++ b/src/Config.js @@ -168,6 +168,11 @@ export class Config { } else if (Object.prototype.toString.call(pages.customUrls) !== '[object Object]') { throw 'Parse Server option pages.customUrls must be an object.'; } + if (pages.customRoutes === undefined) { + pages.customRoutes = PagesOptions.customRoutes.default; + } else if (!(pages.customRoutes instanceof Array)) { + throw 'Parse Server option pages.customRoutes must be an array.'; + } } static validateIdempotencyOptions(idempotencyOptions) { From 8c7e3d46241d5c22261c6f6213be4297e8c62581 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 27 Feb 2021 17:22:24 +0100 Subject: [PATCH 04/10] added 404 response if missing custom route response --- spec/PagesRouter.spec.js | 21 +++++++++++++++++++++ src/Routers/PagesRouter.js | 9 ++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 7e77899fb9..2fea5f5c60 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -1103,6 +1103,27 @@ describe('Pages Router', () => { expect(response.text).toMatch(config.appName); expect(handlerSpy).toHaveBeenCalled(); }); + + it('returns 404 if custom route does not return page', async () => { + config.pages.customRoutes = [ + { + method: 'GET', + path: 'custom_page', + handler: async () => {}, + }, + ]; + await reconfigureServer(config); + const handlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough(); + + const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`; + const response = await request({ + url: url, + followRedirects: false, + }).catch(e => e); + expect(response.status).toBe(404); + expect(response.text).toMatch('Not found'); + expect(handlerSpy).toHaveBeenCalled(); + }); }); }); }); diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index a26b68de6e..5d5a1467a7 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -709,7 +709,14 @@ export class PagesRouter extends PromiseRouter { this.setConfig(req); }, async req => { - const { file, query = {} } = await route.handler(req); + const { file, query = {} } = (await route.handler(req)) || {}; + + // If route handler did not return a page send 404 response + if (!file) { + return this.notFound(); + } + + // Send page response const page = new Page({ id: file, defaultFile: file }); return this.goToPage(req, page, query, false); } From 164640eccd245045b1c20bdd7bb58de4424bcc06 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 27 Feb 2021 17:23:22 +0100 Subject: [PATCH 05/10] added docs --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 46ced77bb7..8beb032cc8 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,10 @@ The full documentation for Parse Server is available in the [wiki](https://githu - [Basic Options](#basic-options) - [Client Key Options](#client-key-options) - [Email Verification and Password Reset](#email-verification-and-password-reset) + - [Custom Routes](#custom-routes) + - [Example](#example) + - [Reserved Paths](#reserved-paths) + - [Parameters](#parameters) - [Custom Pages](#custom-pages) - [Using Environment Variables](#using-environment-variables) - [Available Adapters](#available-adapters) @@ -67,7 +71,9 @@ The full documentation for Parse Server is available in the [wiki](https://githu - [Pages](#pages) - [Localization with Directory Structure](#localization-with-directory-structure) - [Localization with JSON Resource](#localization-with-json-resource) - - [Parameters](#parameters) + - [Dynamic placeholders](#dynamic-placeholders) + - [Reserved Keys](#reserved-keys) + - [Parameters](#parameters-1) - [Logging](#logging) - [Live Query](#live-query) - [GraphQL](#graphql) @@ -388,6 +394,60 @@ You can also use other email adapters contributed by the community such as: - [parse-server-generic-email-adapter](https://www.npmjs.com/package/parse-server-generic-email-adapter) - [parse-server-api-mail-adapter](https://www.npmjs.com/package/parse-server-api-mail-adapter) +## Custom Routes +**Caution, this is an experimental feature that may not be appropriate for production.** + +Custom routes allow to build user flows with webpages, similar to the existing password reset and email verification features. Custom routes are defined with the `pages` option in the Parse Server configuration: + +### Example + +```js +const api = new ParseServer({ + ...otherOptions, + + pages: { + enableRouter: true, // Enables the experimental feature; required for custom routes + customRoutes: [{ + method: 'GET', + path: 'custom_route', + handler: async request => { + // custom logic + // ... + // then, depending on the outcome, return a HTML file as response + return { file: 'custom_page.html' }; + } + }] + } +} +``` + +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 +The following paths are already used by Parse Server's built-in features and are therefore not available for custom routes. Custom routes with an identical combination of `path` and `method` are ignored. + +| Path | HTTP Method | Feature | +|-----------------------------|-------------|--------------------| +| `verify_email` | `GET` | email verification | +| `resend_verification_email` | `POST` | email verification | +| `choose_password` | `GET` | password reset | +| `request_password_reset` | `GET` | password reset | +| `request_password_reset` | `POST` | password reset | + +### Parameters + +| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | +|------------------------------|----------|-----------------|---------------|-----------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `pages` | yes | `Object` | `undefined` | - | `PARSE_SERVER_PAGES` | The options for pages such as password reset and email verification. | +| `pages.enableRouter` | yes | `Boolean` | `false` | - | `PARSE_SERVER_PAGES_ENABLE_ROUTER` | Is `true` if the pages router should be enabled; this is required for any of the pages options to take effect. **Caution, this is an experimental feature that may not be appropriate for production.** | +| `pages.customRoutes` | yes | `Array` | `[]` | - | `PARSE_SERVER_PAGES_CUSTOM_ROUTES` | The custom routes. The routes are added in the order they are defined here, which has to be considered since requests traverse routes in an ordered manner. Custom routes are traversed after build-in routes such as password reset and email verification. | +| `pages.customRoutes.method` | | `String` | - | `GET`, `POST` | - | The HTTP method of the custom route. | +| `pages.customRoutes.path` | | `String` | - | `custom_page` | - | The path of the custom route. Note that the same path can used if the `method` is different, for example a path `custom_page` can have two routes, a `GET` and `POST` route, which will be invoked depending on the HTTP request method. | +| `pages.customRoutes.handler` | | `AsyncFunction` | - | `async () => { ... }` | - | The route handler that is invoked when the route matches the HTTP request. If the handler does not return a page, the request is answered with a 404 `Not found.` response. | + ## Custom Pages It’s possible to change the default pages of the app and redirect the user to another path or domain. From dc3a0ff986e673aa6aeb3fd5ba767f8ff102f621 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 27 Feb 2021 17:23:35 +0100 Subject: [PATCH 06/10] minor README formatting --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8beb032cc8..c6fca7dccc 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Parse Server is continuously tested with the most recent releases of Node.js to | Node.js 10 | 10.24.0 | April 2021 | ✅ Fully compatible | | Node.js 12 | 12.21.0 | April 2022 | ✅ Fully compatible | | Node.js 14 | 14.16.0 | April 2023 | ✅ Fully compatible | -| Node.js 15 | 15.10.0 | June 2021 | ✅ Fully compatible | +| Node.js 15 | 15.10.0 | June 2021 | ✅ Fully compatible | #### MongoDB Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and only test against versions that are officially supported and have not reached their end-of-life date. @@ -130,12 +130,12 @@ Parse Server is continuously tested with the most recent releases of MongoDB to #### PostgreSQL Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility. We follow the [PostGIS docker tags](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated) and only test against versions that are officially supported and have not reached their end-of-life date. -| Version | PostGIS Version | End-of-Life Date | Compatibility | -|------------------|-----------------|------------------|--------------------| -| Postgres 10.x | 3.0.x, 3.1.x | November 2022 | ✅ Fully compatible | -| Postgres 11.x | 3.0.x, 3.1.x | November 2023 | ✅ Fully compatible | -| Postgres 12.x | 3.0.x, 3.1.x | November 2024 | ✅ Fully compatible | -| Postgres 13.x | 3.0.x, 3.1.x | November 2025 | ✅ Fully compatible | +| Version | PostGIS Version | End-of-Life Date | Compatibility | +|---------------|-----------------|------------------|--------------------| +| Postgres 10.x | 3.0.x, 3.1.x | November 2022 | ✅ Fully compatible | +| Postgres 11.x | 3.0.x, 3.1.x | November 2023 | ✅ Fully compatible | +| Postgres 12.x | 3.0.x, 3.1.x | November 2024 | ✅ Fully compatible | +| Postgres 13.x | 3.0.x, 3.1.x | November 2025 | ✅ Fully compatible | ### Locally ```bash @@ -531,11 +531,11 @@ let api = new ParseServer({ ``` ### Parameters -| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | -|----------------------------|----------|-----------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. | +| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | +|----------------------------|----------|-----------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. | | `idempotencyOptions.paths` | yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specify the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | -| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | +| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | ### Notes From 68dc593bda42d0f783e85c6c577d942042620d55 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 27 Feb 2021 17:30:02 +0100 Subject: [PATCH 07/10] added CHANGELOG entry --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e0421c50..2d19bfb42a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,8 @@ __BREAKING CHANGES:__ - NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy), [Manuel Trezza](https://github.com/mtrezza). ___ -- IMPROVE: Allow Cloud Validator `options` to be async [#7155](https://github.com/parse-community/parse-server/pull/7155). Thanks to [dblythy](https://github.com/dblythy) - NEW (EXPERIMENTAL): Added new page router with placeholder rendering and localization of custom and feature pages such as password reset and email verification. **Caution, this is an experimental feature that may not be appropriate for production.** [#6891](https://github.com/parse-community/parse-server/issues/6891). Thanks to [Manuel Trezza](https://github.com/mtrezza). +- NEW (EXPERIMENTAL): Added custom routes to easily customize flows for password reset, email verification or build entirely new flows. **Caution, this is an experimental feature that may not be appropriate for production.** [#7231](https://github.com/parse-community/parse-server/issues/7231). Thanks to [Manuel Trezza](https://github.com/mtrezza). - NEW: Added convenience method `Parse.Cloud.sendEmail(...)` to send email via email adapter in Cloud Code. [#7089](https://github.com/parse-community/parse-server/pull/7089). Thanks to [dblythy](https://github.com/dblythy) - NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis) - NEW: Supporting patterns in LiveQuery server's config parameter `classNames` [#7131](https://github.com/parse-community/parse-server/pull/7131). Thanks to [Nes-si](https://github.com/Nes-si) @@ -29,6 +29,7 @@ ___ - IMPROVE: Allow Cloud Validator `options` to be async [#7155](https://github.com/parse-community/parse-server/pull/7155). Thanks to [dblythy](https://github.com/dblythy) - IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz) - IMPROVE: Parse Server will from now on be continuously tested against all relevant Postgres versions (minor versions). Added Postgres compatibility table to Parse Server docs. [#7176](https://github.com/parse-community/parse-server/pull/7176). Thanks to [Corey Baker](https://github.com/cbaker6). +- IMPROVE: Allow Cloud Validator `options` to be async [#7155](https://github.com/parse-community/parse-server/pull/7155). Thanks to [dblythy](https://github.com/dblythy) - FIX: Fix error when a not yet inserted job is updated [#7196](https://github.com/parse-community/parse-server/pull/7196). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo). - FIX: request.context for afterFind triggers. [#7078](https://github.com/parse-community/parse-server/pull/7078). Thanks to [dblythy](https://github.com/dblythy) - FIX: Winston Logger interpolating stdout to console [#7114](https://github.com/parse-community/parse-server/pull/7114). Thanks to [dplewis](https://github.com/dplewis) From d5aa98bc9d04b04b190ab289412d36f3a87d001c Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 2 Mar 2021 19:59:33 +0100 Subject: [PATCH 08/10] fixed bug in definitions builder that did not recognize array of custom type --- resources/buildConfigDefinitions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 6ea88b4d65..4f3ccde245 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -218,7 +218,9 @@ function inject(t, list) { type = elt.typeAnnotation.id.name; } if (type === 'Array') { - type = `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`; + type = elt.typeAnnotation.elementType.id + ? `${elt.typeAnnotation.elementType.id.name}[]` + : `${elt.typeAnnotation.elementType.type.replace('TypeAnnotation', '')}[]`; } if (type === 'NumberOrBoolean') { type = 'Number|Boolean'; From 90a68e4a89cdb8040dc784ea468a8bebce38ff4d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 2 Mar 2021 20:00:08 +0100 Subject: [PATCH 09/10] added missing route handler definition --- src/Options/Definitions.js | 5 +++++ src/Options/docs.js | 1 + src/Options/index.js | 2 ++ 3 files changed, 8 insertions(+) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 5b07c8a061..e06d1c8fc3 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -488,6 +488,11 @@ module.exports.PagesOptions = { }, }; module.exports.PagesRoute = { + handler: { + env: 'PARSE_SERVER_PAGES_ROUTE_HANDLER', + help: 'The route handler that is an async function.', + required: true, + }, method: { env: 'PARSE_SERVER_PAGES_ROUTE_METHOD', help: "The route method, e.g. 'GET' or 'POST'.", diff --git a/src/Options/docs.js b/src/Options/docs.js index a6d64b9176..717edda473 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -96,6 +96,7 @@ /** * @interface PagesRoute + * @property {Function} handler The route handler that is an async function. * @property {String} method The route method, e.g. 'GET' or 'POST'. * @property {String} path The route path. */ diff --git a/src/Options/index.js b/src/Options/index.js index 24ca19df3f..1114e4fe0f 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -266,6 +266,8 @@ export interface PagesRoute { path: string; /* The route method, e.g. 'GET' or 'POST'. */ method: string; + /* The route handler that is an async function. */ + handler: () => void; } export interface PagesCustomUrlsOptions { From 73290b635982388e8ee9a9eaf9458754ffdd8628 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 2 Mar 2021 20:00:36 +0100 Subject: [PATCH 10/10] fixed custom routes definition --- src/Options/docs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/docs.js b/src/Options/docs.js index 717edda473..da3013c0b7 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -82,7 +82,7 @@ /** * @interface PagesOptions - * @property {Generic[]} customRoutes The custom routes. + * @property {PagesRoute[]} customRoutes The custom routes. * @property {PagesCustomUrlsOptions} customUrls The URLs to the custom pages. * @property {Boolean} enableLocalization Is true if pages should be localized; this has no effect on custom page redirects. * @property {Boolean} enableRouter Is true if the pages router should be enabled; this is required for any of the pages options to take effect. Caution, this is an experimental feature that may not be appropriate for production.