Skip to content

Commit

Permalink
Merge branch 'alpha' into feat/sendLiveQueryEvent
Browse files Browse the repository at this point in the history
  • Loading branch information
mtrezza authored Jan 6, 2022
2 parents 70e6de8 + a5ffb95 commit dd3ce1a
Show file tree
Hide file tree
Showing 28 changed files with 903 additions and 724 deletions.
15 changes: 9 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,17 @@ jobs:
- name: PostgreSQL 11, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:11-3.1
NODE_VERSION: 16.13.0
- name: PostgreSQL 12, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:12-3.1
- name: PostgreSQL 11, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:11-3.2
NODE_VERSION: 16.13.0
- name: PostgreSQL 13, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:13-3.1
- name: PostgreSQL 12, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:12-3.2
NODE_VERSION: 16.13.0
- name: PostgreSQL 14, PostGIS 3.1
POSTGRES_IMAGE: postgis/postgis:14-3.1
- name: PostgreSQL 13, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:13-3.2
NODE_VERSION: 16.13.0
- name: PostgreSQL 14, PostGIS 3.2
POSTGRES_IMAGE: postgis/postgis:14-3.2
NODE_VERSION: 16.13.0
fail-fast: false
name: ${{ matrix.name }}
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,15 @@ If your pull request introduces a change that may affect the storage or retrieva
[PostGIS images (select one with v2.2 or higher) on docker dashboard](https://hub.docker.com/r/postgis/postgis) is based off of the official [postgres](https://registry.hub.docker.com/_/postgres/) image and will work out-of-the-box (as long as you create a user with the necessary extensions for each of your Parse databases; see below). To launch the compatible Postgres instance, copy and paste the following line into your shell:

```
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.1-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
docker run -d --name parse-postgres -p 5432:5432 -e POSTGRES_PASSWORD=password --rm postgis/postgis:13-3.2-alpine && sleep 20 && docker exec -it parse-postgres psql -U postgres -c 'CREATE DATABASE parse_server_postgres_adapter_test_database;' && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION pgcrypto; CREATE EXTENSION postgis;' -d parse_server_postgres_adapter_test_database && docker exec -it parse-postgres psql -U postgres -c 'CREATE EXTENSION postgis_topology;' -d parse_server_postgres_adapter_test_database
```
To stop the Postgres instance:

```
docker stop parse-postgres
```

You can also use the [postgis/postgis:13-3.1-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:
You can also use the [postgis/postgis:13-3.2-alpine](https://hub.docker.com/r/postgis/postgis) image in a Dockerfile and copy this [script](https://github.com/parse-community/parse-server/blob/master/scripts/before_script_postgres.sh) to the image by adding the following lines:

```
#Install additional scripts. These are run in abc order during initial start
Expand Down
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,10 @@ Parse Server is continuously tested with the most recent releases of PostgreSQL

| Version | PostGIS Version | End-of-Life | Parse Server Support End | Compatible |
|-------------|-----------------|---------------|--------------------------|------------|
| Postgres 11 | 3.0, 3.1 | November 2023 | April 2022 | ✅ Yes |
| Postgres 12 | 3.1 | November 2024 | April 2023 | ✅ Yes |
| Postgres 13 | 3.1 | November 2025 | April 2024 | ✅ Yes |
| Postgres 14 | 3.1 | November 2026 | April 2025 | ✅ Yes |
| Postgres 11 | 3.0, 3.1, 3.2 | November 2023 | April 2022 | ✅ Yes |
| Postgres 12 | 3.2 | November 2024 | April 2023 | ✅ Yes |
| Postgres 13 | 3.2 | November 2025 | April 2024 | ✅ Yes |
| Postgres 14 | 3.2 | November 2026 | April 2025 | ✅ Yes |

### Locally
```bash
Expand Down Expand Up @@ -525,9 +525,26 @@ let api = new ParseServer({
| `idempotencyOptions.paths` | yes | `Array<String>` | `[]` | `.*` (all paths, includes the examples below), <br>`functions/.*` (all functions), <br>`jobs/.*` (all jobs), <br>`classes/.*` (all classes), <br>`functions/.*` (all functions), <br>`users` (user creation / update), <br>`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`. |
### Notes <!-- omit in toc -->
### Postgres <!-- omit in toc -->
To use this feature in Postgres, you will need to create a cron job using [pgAdmin](https://www.pgadmin.org/docs/pgadmin4/development/pgagent_jobs.html) or similar to call the Postgres function `idempotency_delete_expired_records()` that deletes expired idempotency records. You can find an example script below. Make sure the script has the same privileges to log into Postgres as Parse Server.
```bash
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
SELECT idempotency_delete_expired_records();
EOSQL
- This feature is currently only available for MongoDB and not for Postgres.
exec "$@"
```
Assuming the script above is named, `parse_idempotency_delete_expired_records.sh`, a cron job that runs the script every 2 minutes may look like:
```bash
2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1
```
## Localization
Expand Down
21 changes: 21 additions & 0 deletions changelogs/CHANGELOG_alpha.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
# [5.0.0-alpha.16](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.15...5.0.0-alpha.16) (2022-01-02)


### Features

* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538))

# [5.0.0-alpha.15](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.14...5.0.0-alpha.15) (2022-01-02)


### Features

* support `postgresql` protocol in database URI ([#7757](https://github.com/parse-community/parse-server/issues/7757)) ([caf4a23](https://github.com/parse-community/parse-server/commit/caf4a2341f554b28e3918c53e7e897a3ca47bf8b))

# [5.0.0-alpha.14](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.13...5.0.0-alpha.14) (2022-01-02)


### Features

* support relativeTime query constraint on Postgres ([#7747](https://github.com/parse-community/parse-server/issues/7747)) ([16b1b2a](https://github.com/parse-community/parse-server/commit/16b1b2a19714535ca805f2dbb3b561d8f6a519a7))

# [5.0.0-alpha.13](https://github.com/parse-community/parse-server/compare/5.0.0-alpha.12...5.0.0-alpha.13) (2021-12-08)


Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "5.0.0-alpha.13",
"version": "5.0.0-alpha.16",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
Expand Down Expand Up @@ -33,7 +33,7 @@
"deepcopy": "2.1.0",
"express": "4.17.1",
"follow-redirects": "1.14.6",
"graphql": "15.7.0",
"graphql": "15.7.1",
"graphql-list-fields": "2.0.2",
"graphql-relay": "0.7.0",
"graphql-tag": "2.12.6",
Expand Down
18 changes: 18 additions & 0 deletions spec/AuthenticationAdapters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,24 @@ describe('Apple Game Center Auth adapter', () => {
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
}
});

it('validateAuthData invalid public key http url', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'http://static.gc.apple.com/public-key/gc-prod-4.cer',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};

try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: http://static.gc.apple.com/public-key/gc-prod-4.cer');
}
});
});

describe('phant auth adapter', () => {
Expand Down
35 changes: 31 additions & 4 deletions spec/Idempotency.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ const rest = require('../lib/rest');
const auth = require('../lib/Auth');
const uuid = require('uuid');

describe_only_db('mongo')('Idempotency', () => {
describe('Idempotency', () => {
// Parameters
/** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which
runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */
const SIMULATE_TTL = true;
const ttl = 2;
const maxTimeOut = 4000;

// Helpers
async function deleteRequestEntry(reqId) {
const config = Config.get(Parse.applicationId);
Expand Down Expand Up @@ -38,9 +41,10 @@ describe_only_db('mongo')('Idempotency', () => {
}
await setup({
paths: ['functions/.*', 'jobs/.*', 'classes/.*', 'users', 'installations'],
ttl: 30,
ttl: ttl,
});
});

// Tests
it('should enforce idempotency for cloud code function', async () => {
let counter = 0;
Expand All @@ -56,7 +60,7 @@ describe_only_db('mongo')('Idempotency', () => {
'X-Parse-Request-Id': 'abc-123',
},
};
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30);
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(ttl);
await request(params);
await request(params).then(fail, e => {
expect(e.status).toEqual(400);
Expand All @@ -83,12 +87,35 @@ describe_only_db('mongo')('Idempotency', () => {
if (SIMULATE_TTL) {
await deleteRequestEntry('abc-123');
} else {
await new Promise(resolve => setTimeout(resolve, 130000));
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
}
await expectAsync(request(params)).toBeResolved();
expect(counter).toBe(2);
});

it_only_db('postgres')('should delete request entry when postgress ttl function is called', async () => {
const client = Config.get(Parse.applicationId).database.adapter._client;
let counter = 0;
Parse.Cloud.define('myFunction', () => {
counter++;
});
const params = {
method: 'POST',
url: 'http://localhost:8378/1/functions/myFunction',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'X-Parse-Request-Id': 'abc-123',
},
};
await expectAsync(request(params)).toBeResolved();
await expectAsync(request(params)).toBeRejected();
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
await client.one('SELECT idempotency_delete_expired_records()');
await expectAsync(request(params)).toBeResolved();
expect(counter).toBe(2);
});

it('should enforce idempotency for cloud code jobs', async () => {
let counter = 0;
Parse.Cloud.job('myJob', () => {
Expand Down
23 changes: 12 additions & 11 deletions spec/MongoTransform.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
const dd = require('deep-diff');
const mongodb = require('mongodb');
const Utils = require('../lib/Utils');

describe('parseObjectToMongoObjectForCreate', () => {
it('a basic number', done => {
Expand Down Expand Up @@ -592,7 +593,7 @@ describe('relativeTimeToDate', () => {
describe('In the future', () => {
it('should parse valid natural time', () => {
const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds';
const { result, status, info } = transform.relativeTimeToDate(text, now);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z');
expect(status).toBe('success');
expect(info).toBe('future');
Expand All @@ -602,7 +603,7 @@ describe('relativeTimeToDate', () => {
describe('In the past', () => {
it('should parse valid natural time', () => {
const text = '2 days 12 hours 1 minute 12 seconds ago';
const { result, status, info } = transform.relativeTimeToDate(text, now);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
expect(status).toBe('success');
expect(info).toBe('past');
Expand All @@ -612,7 +613,7 @@ describe('relativeTimeToDate', () => {
describe('From now', () => {
it('should equal current time', () => {
const text = 'now';
const { result, status, info } = transform.relativeTimeToDate(text, now);
const { result, status, info } = Utils.relativeTimeToDate(text, now);
expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z');
expect(status).toBe('success');
expect(info).toBe('present');
Expand All @@ -621,54 +622,54 @@ describe('relativeTimeToDate', () => {

describe('Error cases', () => {
it('should error if string is completely gibberish', () => {
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
expect(Utils.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
status: 'error',
info: "Time should either start with 'in' or end with 'ago'",
});
});

it('should error if string contains neither `ago` nor `in`', () => {
expect(transform.relativeTimeToDate('12 hours 1 minute')).toEqual({
expect(Utils.relativeTimeToDate('12 hours 1 minute')).toEqual({
status: 'error',
info: "Time should either start with 'in' or end with 'ago'",
});
});

it('should error if there are missing units or numbers', () => {
expect(transform.relativeTimeToDate('in 12 hours 1')).toEqual({
expect(Utils.relativeTimeToDate('in 12 hours 1')).toEqual({
status: 'error',
info: 'Invalid time string. Dangling unit or number.',
});

expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({
expect(Utils.relativeTimeToDate('12 hours minute ago')).toEqual({
status: 'error',
info: 'Invalid time string. Dangling unit or number.',
});
});

it('should error on floating point numbers', () => {
expect(transform.relativeTimeToDate('in 12.3 hours')).toEqual({
expect(Utils.relativeTimeToDate('in 12.3 hours')).toEqual({
status: 'error',
info: "'12.3' is not an integer.",
});
});

it('should error if numbers are invalid', () => {
expect(transform.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
expect(Utils.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
status: 'error',
info: "'123a' is not an integer.",
});
});

it('should error on invalid interval units', () => {
expect(transform.relativeTimeToDate('4 score 7 years ago')).toEqual({
expect(Utils.relativeTimeToDate('4 score 7 years ago')).toEqual({
status: 'error',
info: "Invalid interval: 'score'",
});
});

it("should error when string contains 'ago' and 'in'", () => {
expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
expect(Utils.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
status: 'error',
info: "Time cannot have both 'in' and 'ago'",
});
Expand Down
10 changes: 9 additions & 1 deletion spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2600,11 +2600,19 @@ describe('ParseGraphQLServer', () => {
// "SecondaryObject:bBRgmzIRRM" < "SecondaryObject:nTMcuVbATY" true
// base64("SecondaryObject:bBRgmzIRRM"") < base64(""SecondaryObject:nTMcuVbATY"") false
// "U2Vjb25kYXJ5T2JqZWN0OmJCUmdteklSUk0=" < "U2Vjb25kYXJ5T2JqZWN0Om5UTWN1VmJBVFk=" false
const originalIds = [getSecondaryObjectsResult.data.secondaryObject2.objectId,
getSecondaryObjectsResult.data.secondaryObject4.objectId];
expect(
findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId
).toBeLessThan(
).not.toBe(
findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId
);
expect(
originalIds.includes(findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId)
).toBeTrue();
expect(
originalIds.includes(findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId)
).toBeTrue();

const createPrimaryObjectResult = await apolloClient.mutate({
mutation: gql`
Expand Down
Loading

0 comments on commit dd3ce1a

Please sign in to comment.