Skip to content

Commit 9fee1a0

Browse files
authored
feat: Add installation deviceToken deduplication options (#10451)
1 parent 35207c2 commit 9fee1a0

13 files changed

Lines changed: 1189 additions & 36 deletions

DEPRECATIONS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
2727
| DEPPS21 | Config option `protectedFieldsOwnerExempt` defaults to `false` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
2828
| DEPPS22 | Config option `protectedFieldsTriggerExempt` defaults to `true` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
2929
| DEPPS23 | Config option `protectedFieldsSaveResponseExempt` defaults to `false` | | 9.7.0 (2026) | 10.0.0 (2027) | deprecated | - |
30+
| DEPPS24 | Config option `installation.duplicateDeviceTokenActionEnforceAuth` defaults to `true` | [#10451](https://github.com/parse-community/parse-server/pull/10451) | 9.9.0 (2026) | 10.0.0 (2027) | deprecated | - |
3031

3132
[i_deprecation]: ## "The version and date of the deprecation."
3233
[i_change]: ## "The version and date of the planned change."

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ A big _thank you_ 🙏 to our [sponsors](#sponsors) and [backers](#backers) who
7272
- [Configuring File Adapters](#configuring-file-adapters)
7373
- [Restricting File URL Domains](#restricting-file-url-domains)
7474
- [Idempotency Enforcement](#idempotency-enforcement)
75+
- [Installations](#installations)
7576
- [Localization](#localization)
7677
- [Pages](#pages)
7778
- [Localization with Directory Structure](#localization-with-directory-structure)
@@ -658,6 +659,49 @@ Assuming the script above is named, `parse_idempotency_delete_expired_records.sh
658659
2 * * * * /root/parse_idempotency_delete_expired_records.sh >/dev/null 2>&1
659660
```
660661
662+
## Installations
663+
664+
Parse Server deduplicates `_Installation` records when a new install collides with an existing row's `deviceToken`. The `installation` option block configures the dedup behavior.
665+
666+
### Options
667+
668+
| Parameter | Optional | Type | Default | Environment Variable |
669+
|---|---|---|---|---|
670+
| `installation.duplicateDeviceTokenActionEnforceAuth` | yes | `Boolean` | `false` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_ACTION_ENFORCE_AUTH` |
671+
| `installation.duplicateDeviceTokenAction` | yes | `String` | `'delete'` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_ACTION` |
672+
| `installation.duplicateDeviceTokenMergePriority` | yes | `String` | `'deviceToken'` | `PARSE_SERVER_INSTALLATION_DUPLICATE_DEVICE_TOKEN_MERGE_PRIORITY` |
673+
674+
#### `duplicateDeviceTokenActionEnforceAuth`
675+
676+
When `true`, the dedup operation runs with the caller's auth context so ACL and CLP are honored. When `false`, the dedup runs as master and bypasses both. Master and maintenance keys always bypass regardless of this flag.
677+
678+
#### `duplicateDeviceTokenAction`
679+
680+
What Parse Server does to the conflicting `_Installation` row(s) when a new install's `deviceToken` collides with an existing row.
681+
682+
- `'delete'`: destroys the conflicting row.
683+
- `'update'`: clears the now-conflicting ID field on the conflicting row, preserving custom fields, channels, and history.
684+
685+
#### `duplicateDeviceTokenMergePriority`
686+
687+
When an existing row holds the new `deviceToken` but has no `installationId` of its own, Parse Server merges the two rows. This option controls which side wins.
688+
689+
- `'deviceToken'`: the deviceToken-only row survives; the request's installationId-matched row is the loser.
690+
- `'installationId'`: the request's installationId-matched row survives; the deviceToken-only orphan is the loser.
691+
692+
### Configuration example
693+
694+
```javascript
695+
const parseServer = new ParseServer({
696+
...otherOptions,
697+
installation: {
698+
duplicateDeviceTokenActionEnforceAuth: true,
699+
duplicateDeviceTokenAction: 'update',
700+
duplicateDeviceTokenMergePriority: 'installationId',
701+
},
702+
});
703+
```
704+
661705
## Localization
662706

663707
### Pages

resources/buildConfigDefinitions.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const nestedOptionTypes = [
1818
'FileDownloadOptions',
1919
'FileUploadOptions',
2020
'IdempotencyOptions',
21+
'InstallationOptions',
2122
'Object',
2223
'PagesCustomUrlsOptions',
2324
'PagesOptions',
@@ -39,6 +40,7 @@ const nestedOptionEnvPrefix = {
3940
FileDownloadOptions: 'PARSE_SERVER_FILE_DOWNLOAD_',
4041
FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_',
4142
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
43+
InstallationOptions: 'PARSE_SERVER_INSTALLATION_',
4244
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
4345
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
4446
LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',

spec/Deprecator.spec.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,39 @@ describe('Deprecator', () => {
234234
);
235235
}
236236
});
237+
238+
it('registers a deprecation entry for installation.duplicateDeviceTokenActionEnforceAuth', () => {
239+
const Deprecations = require('../lib/Deprecator/Deprecations');
240+
const entry = Deprecations.find(
241+
d => d.optionKey === 'installation.duplicateDeviceTokenActionEnforceAuth'
242+
);
243+
expect(entry).toBeDefined();
244+
expect(entry.changeNewDefault).toBe('true');
245+
expect(entry.solution).toContain('duplicateDeviceTokenActionEnforceAuth');
246+
});
247+
248+
it('logs deprecation for installation.duplicateDeviceTokenActionEnforceAuth when not set', async () => {
249+
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
250+
251+
await reconfigureServer();
252+
expect(logSpy).toHaveBeenCalledWith(
253+
jasmine.objectContaining({
254+
optionKey: 'installation.duplicateDeviceTokenActionEnforceAuth',
255+
changeNewDefault: 'true',
256+
})
257+
);
258+
});
259+
260+
it('does not log deprecation for installation.duplicateDeviceTokenActionEnforceAuth when explicitly set', async () => {
261+
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
262+
263+
await reconfigureServer({
264+
installation: { duplicateDeviceTokenActionEnforceAuth: false },
265+
});
266+
expect(logSpy).not.toHaveBeenCalledWith(
267+
jasmine.objectContaining({
268+
optionKey: 'installation.duplicateDeviceTokenActionEnforceAuth',
269+
})
270+
);
271+
});
237272
});

0 commit comments

Comments
 (0)