Skip to content

Commit dd3ce1a

Browse files
authored
Merge branch 'alpha' into feat/sendLiveQueryEvent
2 parents 70e6de8 + a5ffb95 commit dd3ce1a

28 files changed

+903
-724
lines changed

.github/workflows/ci.yml

+9-6
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,17 @@ jobs:
187187
- name: PostgreSQL 11, PostGIS 3.1
188188
POSTGRES_IMAGE: postgis/postgis:11-3.1
189189
NODE_VERSION: 16.13.0
190-
- name: PostgreSQL 12, PostGIS 3.1
191-
POSTGRES_IMAGE: postgis/postgis:12-3.1
190+
- name: PostgreSQL 11, PostGIS 3.2
191+
POSTGRES_IMAGE: postgis/postgis:11-3.2
192192
NODE_VERSION: 16.13.0
193-
- name: PostgreSQL 13, PostGIS 3.1
194-
POSTGRES_IMAGE: postgis/postgis:13-3.1
193+
- name: PostgreSQL 12, PostGIS 3.2
194+
POSTGRES_IMAGE: postgis/postgis:12-3.2
195195
NODE_VERSION: 16.13.0
196-
- name: PostgreSQL 14, PostGIS 3.1
197-
POSTGRES_IMAGE: postgis/postgis:14-3.1
196+
- name: PostgreSQL 13, PostGIS 3.2
197+
POSTGRES_IMAGE: postgis/postgis:13-3.2
198+
NODE_VERSION: 16.13.0
199+
- name: PostgreSQL 14, PostGIS 3.2
200+
POSTGRES_IMAGE: postgis/postgis:14-3.2
198201
NODE_VERSION: 16.13.0
199202
fail-fast: false
200203
name: ${{ matrix.name }}

CONTRIBUTING.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,15 @@ If your pull request introduces a change that may affect the storage or retrieva
154154
[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:
155155

156156
```
157-
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
157+
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
158158
```
159159
To stop the Postgres instance:
160160

161161
```
162162
docker stop parse-postgres
163163
```
164164

165-
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:
165+
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:
166166

167167
```
168168
#Install additional scripts. These are run in abc order during initial start

README.md

+23-6
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ Parse Server is continuously tested with the most recent releases of PostgreSQL
134134

135135
| Version | PostGIS Version | End-of-Life | Parse Server Support End | Compatible |
136136
|-------------|-----------------|---------------|--------------------------|------------|
137-
| Postgres 11 | 3.0, 3.1 | November 2023 | April 2022 | ✅ Yes |
138-
| Postgres 12 | 3.1 | November 2024 | April 2023 | ✅ Yes |
139-
| Postgres 13 | 3.1 | November 2025 | April 2024 | ✅ Yes |
140-
| Postgres 14 | 3.1 | November 2026 | April 2025 | ✅ Yes |
137+
| Postgres 11 | 3.0, 3.1, 3.2 | November 2023 | April 2022 | ✅ Yes |
138+
| Postgres 12 | 3.2 | November 2024 | April 2023 | ✅ Yes |
139+
| Postgres 13 | 3.2 | November 2025 | April 2024 | ✅ Yes |
140+
| Postgres 14 | 3.2 | November 2026 | April 2025 | ✅ Yes |
141141

142142
### Locally
143143
```bash
@@ -525,9 +525,26 @@ let api = new ParseServer({
525525
| `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/`. |
526526
| `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`. |
527527
528-
### Notes <!-- omit in toc -->
528+
### Postgres <!-- omit in toc -->
529+
530+
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.
531+
532+
```bash
533+
#!/bin/bash
534+
535+
set -e
536+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
537+
SELECT idempotency_delete_expired_records();
538+
EOSQL
529539
530-
- This feature is currently only available for MongoDB and not for Postgres.
540+
exec "$@"
541+
```
542+
543+
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:
544+
545+
```bash
546+
2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1
547+
```
531548
532549
## Localization
533550

changelogs/CHANGELOG_alpha.md

+21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
# [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)
2+
3+
4+
### Features
5+
6+
* add Idempotency to Postgres ([#7750](https://github.com/parse-community/parse-server/issues/7750)) ([0c3feaa](https://github.com/parse-community/parse-server/commit/0c3feaaa1751964c0db89f25674935c3354b1538))
7+
8+
# [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)
9+
10+
11+
### Features
12+
13+
* 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))
14+
15+
# [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)
16+
17+
18+
### Features
19+
20+
* 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))
21+
122
# [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)
223

324

package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse-server",
3-
"version": "5.0.0-alpha.13",
3+
"version": "5.0.0-alpha.16",
44
"description": "An express module providing a Parse-compatible API server",
55
"main": "lib/index.js",
66
"repository": {
@@ -33,7 +33,7 @@
3333
"deepcopy": "2.1.0",
3434
"express": "4.17.1",
3535
"follow-redirects": "1.14.6",
36-
"graphql": "15.7.0",
36+
"graphql": "15.7.1",
3737
"graphql-list-fields": "2.0.2",
3838
"graphql-relay": "0.7.0",
3939
"graphql-tag": "2.12.6",

spec/AuthenticationAdapters.spec.js

+18
Original file line numberDiff line numberDiff line change
@@ -1707,6 +1707,24 @@ describe('Apple Game Center Auth adapter', () => {
17071707
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
17081708
}
17091709
});
1710+
1711+
it('validateAuthData invalid public key http url', async () => {
1712+
const authData = {
1713+
id: 'G:1965586982',
1714+
publicKeyUrl: 'http://static.gc.apple.com/public-key/gc-prod-4.cer',
1715+
timestamp: 1565257031287,
1716+
signature: '1234',
1717+
salt: 'DzqqrQ==',
1718+
bundleId: 'cloud.xtralife.gamecenterauth',
1719+
};
1720+
1721+
try {
1722+
await gcenter.validateAuthData(authData);
1723+
fail();
1724+
} catch (e) {
1725+
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: http://static.gc.apple.com/public-key/gc-prod-4.cer');
1726+
}
1727+
});
17101728
});
17111729

17121730
describe('phant auth adapter', () => {

spec/Idempotency.spec.js

+31-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ const rest = require('../lib/rest');
66
const auth = require('../lib/Auth');
77
const uuid = require('uuid');
88

9-
describe_only_db('mongo')('Idempotency', () => {
9+
describe('Idempotency', () => {
1010
// Parameters
1111
/** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which
1212
runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */
1313
const SIMULATE_TTL = true;
14+
const ttl = 2;
15+
const maxTimeOut = 4000;
16+
1417
// Helpers
1518
async function deleteRequestEntry(reqId) {
1619
const config = Config.get(Parse.applicationId);
@@ -38,9 +41,10 @@ describe_only_db('mongo')('Idempotency', () => {
3841
}
3942
await setup({
4043
paths: ['functions/.*', 'jobs/.*', 'classes/.*', 'users', 'installations'],
41-
ttl: 30,
44+
ttl: ttl,
4245
});
4346
});
47+
4448
// Tests
4549
it('should enforce idempotency for cloud code function', async () => {
4650
let counter = 0;
@@ -56,7 +60,7 @@ describe_only_db('mongo')('Idempotency', () => {
5660
'X-Parse-Request-Id': 'abc-123',
5761
},
5862
};
59-
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30);
63+
expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(ttl);
6064
await request(params);
6165
await request(params).then(fail, e => {
6266
expect(e.status).toEqual(400);
@@ -83,12 +87,35 @@ describe_only_db('mongo')('Idempotency', () => {
8387
if (SIMULATE_TTL) {
8488
await deleteRequestEntry('abc-123');
8589
} else {
86-
await new Promise(resolve => setTimeout(resolve, 130000));
90+
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
8791
}
8892
await expectAsync(request(params)).toBeResolved();
8993
expect(counter).toBe(2);
9094
});
9195

96+
it_only_db('postgres')('should delete request entry when postgress ttl function is called', async () => {
97+
const client = Config.get(Parse.applicationId).database.adapter._client;
98+
let counter = 0;
99+
Parse.Cloud.define('myFunction', () => {
100+
counter++;
101+
});
102+
const params = {
103+
method: 'POST',
104+
url: 'http://localhost:8378/1/functions/myFunction',
105+
headers: {
106+
'X-Parse-Application-Id': Parse.applicationId,
107+
'X-Parse-Master-Key': Parse.masterKey,
108+
'X-Parse-Request-Id': 'abc-123',
109+
},
110+
};
111+
await expectAsync(request(params)).toBeResolved();
112+
await expectAsync(request(params)).toBeRejected();
113+
await new Promise(resolve => setTimeout(resolve, maxTimeOut));
114+
await client.one('SELECT idempotency_delete_expired_records()');
115+
await expectAsync(request(params)).toBeResolved();
116+
expect(counter).toBe(2);
117+
});
118+
92119
it('should enforce idempotency for cloud code jobs', async () => {
93120
let counter = 0;
94121
Parse.Cloud.job('myJob', () => {

spec/MongoTransform.spec.js

+12-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform');
55
const dd = require('deep-diff');
66
const mongodb = require('mongodb');
7+
const Utils = require('../lib/Utils');
78

89
describe('parseObjectToMongoObjectForCreate', () => {
910
it('a basic number', done => {
@@ -592,7 +593,7 @@ describe('relativeTimeToDate', () => {
592593
describe('In the future', () => {
593594
it('should parse valid natural time', () => {
594595
const text = 'in 1 year 2 weeks 12 days 10 hours 24 minutes 30 seconds';
595-
const { result, status, info } = transform.relativeTimeToDate(text, now);
596+
const { result, status, info } = Utils.relativeTimeToDate(text, now);
596597
expect(result.toISOString()).toBe('2018-10-22T23:52:46.617Z');
597598
expect(status).toBe('success');
598599
expect(info).toBe('future');
@@ -602,7 +603,7 @@ describe('relativeTimeToDate', () => {
602603
describe('In the past', () => {
603604
it('should parse valid natural time', () => {
604605
const text = '2 days 12 hours 1 minute 12 seconds ago';
605-
const { result, status, info } = transform.relativeTimeToDate(text, now);
606+
const { result, status, info } = Utils.relativeTimeToDate(text, now);
606607
expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
607608
expect(status).toBe('success');
608609
expect(info).toBe('past');
@@ -612,7 +613,7 @@ describe('relativeTimeToDate', () => {
612613
describe('From now', () => {
613614
it('should equal current time', () => {
614615
const text = 'now';
615-
const { result, status, info } = transform.relativeTimeToDate(text, now);
616+
const { result, status, info } = Utils.relativeTimeToDate(text, now);
616617
expect(result.toISOString()).toBe('2017-09-26T13:28:16.617Z');
617618
expect(status).toBe('success');
618619
expect(info).toBe('present');
@@ -621,54 +622,54 @@ describe('relativeTimeToDate', () => {
621622

622623
describe('Error cases', () => {
623624
it('should error if string is completely gibberish', () => {
624-
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
625+
expect(Utils.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
625626
status: 'error',
626627
info: "Time should either start with 'in' or end with 'ago'",
627628
});
628629
});
629630

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

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

643-
expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({
644+
expect(Utils.relativeTimeToDate('12 hours minute ago')).toEqual({
644645
status: 'error',
645646
info: 'Invalid time string. Dangling unit or number.',
646647
});
647648
});
648649

649650
it('should error on floating point numbers', () => {
650-
expect(transform.relativeTimeToDate('in 12.3 hours')).toEqual({
651+
expect(Utils.relativeTimeToDate('in 12.3 hours')).toEqual({
651652
status: 'error',
652653
info: "'12.3' is not an integer.",
653654
});
654655
});
655656

656657
it('should error if numbers are invalid', () => {
657-
expect(transform.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
658+
expect(Utils.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
658659
status: 'error',
659660
info: "'123a' is not an integer.",
660661
});
661662
});
662663

663664
it('should error on invalid interval units', () => {
664-
expect(transform.relativeTimeToDate('4 score 7 years ago')).toEqual({
665+
expect(Utils.relativeTimeToDate('4 score 7 years ago')).toEqual({
665666
status: 'error',
666667
info: "Invalid interval: 'score'",
667668
});
668669
});
669670

670671
it("should error when string contains 'ago' and 'in'", () => {
671-
expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
672+
expect(Utils.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
672673
status: 'error',
673674
info: "Time cannot have both 'in' and 'ago'",
674675
});

spec/ParseGraphQLServer.spec.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -2600,11 +2600,19 @@ describe('ParseGraphQLServer', () => {
26002600
// "SecondaryObject:bBRgmzIRRM" < "SecondaryObject:nTMcuVbATY" true
26012601
// base64("SecondaryObject:bBRgmzIRRM"") < base64(""SecondaryObject:nTMcuVbATY"") false
26022602
// "U2Vjb25kYXJ5T2JqZWN0OmJCUmdteklSUk0=" < "U2Vjb25kYXJ5T2JqZWN0Om5UTWN1VmJBVFk=" false
2603+
const originalIds = [getSecondaryObjectsResult.data.secondaryObject2.objectId,
2604+
getSecondaryObjectsResult.data.secondaryObject4.objectId];
26032605
expect(
26042606
findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId
2605-
).toBeLessThan(
2607+
).not.toBe(
26062608
findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId
26072609
);
2610+
expect(
2611+
originalIds.includes(findSecondaryObjectsResult.data.secondaryObjects.edges[0].node.objectId)
2612+
).toBeTrue();
2613+
expect(
2614+
originalIds.includes(findSecondaryObjectsResult.data.secondaryObjects.edges[1].node.objectId)
2615+
).toBeTrue();
26082616

26092617
const createPrimaryObjectResult = await apolloClient.mutate({
26102618
mutation: gql`

0 commit comments

Comments
 (0)