Skip to content

feat: Add installation deviceToken deduplication options#10451

Merged
mtrezza merged 11 commits intoparse-community:alphafrom
mtrezza:feat/installation-dedup-options-v9
Apr 30, 2026
Merged

feat: Add installation deviceToken deduplication options#10451
mtrezza merged 11 commits intoparse-community:alphafrom
mtrezza:feat/installation-dedup-options-v9

Conversation

@mtrezza
Copy link
Copy Markdown
Member

@mtrezza mtrezza commented Apr 30, 2026

Pull Request

Issue

Parse Server's _Installation deduplication on deviceToken collisions today runs as master and always destroys the conflicting row, regardless of the caller's auth context or the developer's intent. This means:

  1. ACL and CLP set on _Installation are not honored on the dedup path — the dedup bypasses them silently.
  2. The conflicting row is destroyed wholesale, losing any custom fields, channels, or history stored on it.
  3. In the merge case (where the new install matches one row by installationId and another by deviceToken only), the deviceToken-only orphan always wins, even when the active install carries more meaningful data.

Developers have no way to opt out of any of these behaviors.

Approach

Add a new installation server option block with three options:

  • installation.duplicateDeviceTokenActionEnforceAuth: Boolean — default false. When true, the dedup operation runs with the caller's auth context so ACL and CLP are honored. Master and maintenance keys always bypass regardless. The default false is registered as deprecated (DEPPS24); the planned default in 10.0.0 is true.
  • installation.duplicateDeviceTokenAction: 'delete' | 'update' — default 'delete'. 'update' clears the now-conflicting ID field on the conflicting row instead of destroying it, preserving custom fields, channels, and history.
  • installation.duplicateDeviceTokenMergePriority: 'deviceToken' | 'installationId' — default 'deviceToken'. Selects which side wins the merge case (when the new install matches an existing row by installationId and another by deviceToken without an installationId).

Implementation:

  • New module src/InstallationDedup.js with two helpers (removeConflictingDeviceToken for new-install/PUT-deviceToken-change collisions, applyDuplicateDeviceTokenMerge for the merge case). Both honor the option-driven dispatch and log per a uniform policy (verbose for success / no-match, warn for permission denial, error for unexpected errors).
  • RestWrite.handleInstallation refactored at three sites to call the helpers. All three sites are now consistently awaited; today's two new-install / PUT sites were fire-and-forget. The latency cost is bounded to requests that actually fire dedup.
  • Config.validateInstallations validates the new option block; throws on unknown keys, wrong types, or invalid enum values; defaults missing nested keys.
  • DEPRECATIONS.md entry DEPPS24 for the future default flip on duplicateDeviceTokenActionEnforceAuth.
  • README section under Configuration documenting all three options with examples.

Default option values preserve today's behavior, so this is non-breaking for existing apps.

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)

Summary by CodeRabbit

  • New Features

    • Added configurable installation options to control duplicate device-token handling: action (delete/update), auth enforcement, and merge priority.
  • Refactor

    • Centralized installation dedup/merge logic into shared helpers and updated write paths to use them.
  • Configuration

    • Validation and defaults for the new installation settings; environment-driven option surface exposed.
  • Documentation / Deprecations

    • New "Installations" docs and a deprecation announcing the upcoming default change for auth enforcement.
  • Tests

    • Comprehensive tests covering dedup, merge, auth enforcement, validation, and edge cases.

@parse-github-assistant
Copy link
Copy Markdown

parse-github-assistant Bot commented Apr 30, 2026

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Tip

  • Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs.
  • Use Test Driven Development. Write failing tests before implementing functionality. Ensure tests pass.
  • Group code into logical blocks. Add a short comment before each block to explain its purpose.
  • We offer conceptual guidance. Coding is up to you. PRs must be merge-ready for human review.
  • Our review focuses on concept, not quality. PRs with code issues will be rejected. Use an AI agent.
  • Human review time is precious. Avoid review ping-pong. Inspect and test your AI-generated code.

Note

Please respond to review comments from AI agents just like you would to comments from a human reviewer. Let the reviewer resolve their own comments, unless they have reviewed and accepted your commit, or agreed with your explanation for why the feedback was incorrect.

Caution

Pull requests must be written using an AI agent with human supervision. Pull requests written entirely by a human will likely be rejected, because of lower code quality, higher review effort and the higher risk of introducing bugs. Please note that AI review comments on this pull request alone do not satisfy this requirement. Our CI and AI review are safeguards, not development tools. If many issues are flagged, rethink your development approach. Invest more effort in planning and design rather than using review cycles to fix low-quality code.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

Adds configurable _Installation deduplication: new installation options and env prefix, options validation, an InstallationDedup helper module, RestWrite integration to delegate dedup/merge behavior, a deprecation entry, tests, and docs/README updates.

Changes

Cohort / File(s) Summary
Options & Build Config
resources/buildConfigDefinitions.js, src/Options/Definitions.js, src/Options/index.js, src/Options/docs.js
Introduce InstallationOptions and add ParseServerOptions.installation; register nested option type so env vars use PARSE_SERVER_INSTALLATION_; document three dedup-related fields.
Configuration Validation
src/Config.js
Add validateInstallation(installation) and extend validateOptions to validate shape, whitelist keys, apply defaults, and enforce enum/boolean constraints.
Deprecations
src/Deprecator/Deprecations.js, DEPRECATIONS.md
Add deprecation entry for installation.duplicateDeviceTokenActionEnforceAuth (new default 'true') and record it in deprecations docs.
Installation Dedup Module & Integration
src/InstallationDedup.js, src/RestWrite.js
New helper exporting removeConflictingDeviceToken and applyDuplicateDeviceTokenMerge; RestWrite now delegates _Installation conflict/merge handling to these helpers and passes config-driven action, mergePriority, enforceAuth, plus runOptions and validSchemaController.
Tests
spec/Deprecator.spec.js, spec/InstallationDedup.spec.js, spec/ParseInstallation.spec.js
Add deprecation tests and extensive unit/integration tests covering dedup/merge scenarios, error handling, enforceAuth behavior, and options validation.
Docs & README
src/Options/docs.js, README.md
Document new installation options and add "Installations" README section with examples and TOC link.

Sequence Diagram(s)

sequenceDiagram
    participant RestWrite as RestWrite
    participant InstallationDedup as InstallationDedup
    participant Database as Database
    participant Logger as Logger

    rect rgba(100,150,255,0.5)
    Note over RestWrite,InstallationDedup: Remove conflicting deviceToken flow
    RestWrite->>InstallationDedup: removeConflictingDeviceToken(query, action, enforceAuth, runOptions, validSchemaController)
    InstallationDedup->>Database: destroy(query, options) or update(query, clearField, options)
    alt Success
        Database-->>InstallationDedup: OK
        InstallationDedup->>Logger: Log deleted/updated rows
        InstallationDedup-->>RestWrite: Resolve
    else OBJECT_NOT_FOUND
        Database-->>InstallationDedup: Error OBJECT_NOT_FOUND
        InstallationDedup->>Logger: Verbose no-op
        InstallationDedup-->>RestWrite: Resolve silently
    else OPERATION_FORBIDDEN (enforceAuth)
        Database-->>InstallationDedup: Error OPERATION_FORBIDDEN
        InstallationDedup->>Logger: Warn permission skip
        InstallationDedup-->>RestWrite: Resolve silently
    else Unexpected Error
        Database-->>InstallationDedup: Error
        InstallationDedup->>Logger: Error and rethrow
        InstallationDedup-->>RestWrite: Reject
    end
    end

    rect rgba(150,200,150,0.5)
    Note over RestWrite,InstallationDedup: Merge duplicate installation flow
    RestWrite->>InstallationDedup: applyDuplicateDeviceTokenMerge(idMatch, deviceTokenMatch, mergePriority, action, enforceAuth, runOptions, validSchemaController)
    InstallationDedup->>InstallationDedup: Select survivor per mergePriority
    alt Winner is idMatch
        InstallationDedup->>Database: operate on deviceTokenMatch (destroy/update)
    else Winner is deviceTokenMatch
        InstallationDedup->>Database: operate on idMatch (destroy/update)
    end
    alt Success
        Database-->>InstallationDedup: OK
        InstallationDedup->>Logger: Log merge result
        InstallationDedup-->>RestWrite: Return survivor objectId
    else OPERATION_FORBIDDEN (enforceAuth)
        Database-->>InstallationDedup: Error OPERATION_FORBIDDEN
        InstallationDedup->>Logger: Warn and return survivor objectId
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Engage In Review Feedback ❓ Inconclusive Specific review feedback comments from GitHub PR #10451 are not accessible through git history or provided context. Provide actual review feedback comments from the GitHub PR and evidence of how each feedback item was addressed.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title "feat: Add installation deviceToken deduplication options" uses the required 'feat:' prefix and clearly describes the main feature addition.
Description check ✅ Passed The description follows the template structure with Issue, Approach, and Tasks sections; it comprehensively explains the problem, implementation details, and confirms test/documentation additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed PR implements installation deduplication with proper auth enforcement through conditional runOptions passing, validates enum parameters, and handles errors securely without information disclosure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/InstallationDedup.js`:
- Around line 116-163: The code in applyDuplicateDeviceTokenMerge can pick a
loser that is the same row as the survivor when idMatch.objectId ===
deviceTokenMatch.objectId; add an early self-merge guard at the top of
applyDuplicateDeviceTokenMerge that detects when idMatch.objectId ===
deviceTokenMatch.objectId and immediately returns that objectId (and optionally
log a no-op) so no performAction/delete is attempted on the survivor; keep the
rest of the mergePriority logic unchanged for non-self-merge cases.

In `@src/Options/docs.js`:
- Around line 152-157: The docs still show duplicateDeviceTokenActionEnforceAuth
as normal; add a deprecation notice to the option's help text where the options
are defined (the duplicateDeviceTokenActionEnforceAuth entry in the Options
help/definitions source) and then regenerate derived docs and types by running
the definitions generation step so InstallationOptions in the generated docs and
Definitions reflect the deprecation; ensure the help text explicitly mentions
"deprecated" and any replacement guidance so the generated src/Options/docs.js
InstallationOptions entry is updated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f3f1cd3-570b-4135-83a2-cd889539529a

📥 Commits

Reviewing files that changed from the base of the PR and between 35207c2 and 50b381f.

📒 Files selected for processing (11)
  • resources/buildConfigDefinitions.js
  • spec/Deprecator.spec.js
  • spec/InstallationDedup.spec.js
  • spec/ParseInstallation.spec.js
  • src/Config.js
  • src/Deprecator/Deprecations.js
  • src/InstallationDedup.js
  • src/Options/Definitions.js
  • src/Options/docs.js
  • src/Options/index.js
  • src/RestWrite.js

Comment thread src/InstallationDedup.js
Comment thread src/Options/docs.js
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 93.02326% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.59%. Comparing base (725be0d) to head (ea95995).
⚠️ Report is 3 commits behind head on alpha.

Files with missing lines Patch % Lines
src/InstallationDedup.js 90.56% 5 Missing ⚠️
src/Config.js 95.65% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            alpha   #10451      +/-   ##
==========================================
+ Coverage   92.51%   92.59%   +0.07%     
==========================================
  Files         193      194       +1     
  Lines       16831    16901      +70     
  Branches      234      234              
==========================================
+ Hits        15572    15650      +78     
+ Misses       1236     1228       -8     
  Partials       23       23              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
spec/InstallationDedup.spec.js (1)

83-104: ⚡ Quick win

Tighten the update-path assertions.

These expectations currently mirror the helper’s buggy positional call shape, so they would not catch the database.update() argument-order regression above. Please assert the full signature in both update cases so a shifted many / schema-controller argument fails the suite.

Example
-      expect(database.update).toHaveBeenCalledWith(
-        '_Installation',
-        { deviceToken: 'X' },
-        { deviceToken: { __op: 'Delete' } },
-        {},
-        true,
-        undefined
-      );
+      expect(database.update).toHaveBeenCalledWith(
+        '_Installation',
+        { deviceToken: 'X' },
+        { deviceToken: { __op: 'Delete' } },
+        { many: true },
+        undefined,
+        undefined,
+        undefined
+      );

Also applies to: 179-202, 227-249

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/InstallationDedup.spec.js` around lines 83 - 104, The test is too lax
about the positional arguments passed to database.update in
InstallationDedup.removeConflictingDeviceToken so a slipped/misordered signature
(many vs schema controller) won't be caught; update the assertions to check the
full call signature for the update-paths by asserting database.update was called
with all arguments in the exact expected order (for example: '_Installation', {
deviceToken: 'X' }, { deviceToken: { __op: 'Delete' } }, {}, true, undefined)
and do the same tightened full-signature assertion for the other update case(s)
referenced (lines around 179-202 and 227-249), while keeping the logSpy.verbose
assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/InstallationDedup.js`:
- Around line 39-46: The call to database.update in InstallationDedup.js is
passing arguments in the wrong order (you passed many as the 5th positional arg
which shifts the expected options/flags); change the call to pass the options
object (runOptions) as the 4th argument, then the skipSanitization and
validateOnly booleans (use false or the appropriate variables), and finally
validSchemaController as the last argument—for example, remove the positional
many argument (or move it into runOptions if it is an option like
runOptions.many) and call database.update(CLASS_NAME, query, { [fieldToClear]: {
__op: 'Delete' } }, runOptions, /*skipSanitization*/ false, /*validateOnly*/
false, validSchemaController).

---

Nitpick comments:
In `@spec/InstallationDedup.spec.js`:
- Around line 83-104: The test is too lax about the positional arguments passed
to database.update in InstallationDedup.removeConflictingDeviceToken so a
slipped/misordered signature (many vs schema controller) won't be caught; update
the assertions to check the full call signature for the update-paths by
asserting database.update was called with all arguments in the exact expected
order (for example: '_Installation', { deviceToken: 'X' }, { deviceToken: {
__op: 'Delete' } }, {}, true, undefined) and do the same tightened
full-signature assertion for the other update case(s) referenced (lines around
179-202 and 227-249), while keeping the logSpy.verbose assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1c33484f-985b-4714-b069-660a0d3d5a6d

📥 Commits

Reviewing files that changed from the base of the PR and between 001ffa6 and 7ce2ff3.

📒 Files selected for processing (2)
  • spec/InstallationDedup.spec.js
  • src/InstallationDedup.js

Comment thread src/InstallationDedup.js
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@spec/ParseInstallation.spec.js`:
- Around line 1721-1727: Test expectation is mismatched:
Config.validateInstallations (in src/Config.js, function validateInstallations)
throws "installation.<key> is not a valid installation option." for unknown
nested keys, so update the test in ParseInstallation.spec.js that calls
reconfigureServer({ installation: { unknownKey: 'foo' } }) to expect rejection
with "installation.unknownKey is not a valid installation option." instead of
the current message; ensure the assertion string exactly matches the error
format produced by validateInstallations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 104bf91e-966f-40fa-bdef-e8c4c212a39f

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce2ff3 and 0c05d6b.

📒 Files selected for processing (3)
  • spec/InstallationDedup.spec.js
  • spec/ParseInstallation.spec.js
  • src/InstallationDedup.js
✅ Files skipped from review due to trivial changes (1)
  • spec/InstallationDedup.spec.js

Comment thread spec/ParseInstallation.spec.js
@mtrezza
Copy link
Copy Markdown
Member Author

mtrezza commented Apr 30, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
spec/ParseInstallation.spec.js (2)

1360-1366: ⚡ Quick win

Assert the original channel payloads, not just field presence.

These tests are meant to lock down the "update" contract, but toBeDefined() would still pass if the loser rows kept a channels field with the wrong value. Please assert the exact arrays for each cleared row so a regression that overwrites or empties channels is caught.

Proposed tightening of the assertions
       const cleared = all.filter(r => r.installationId !== 'iid-c');
-      cleared.forEach(r => {
-        expect(r.deviceToken).toBeUndefined();
-        expect(r.channels).toBeDefined();
-      });
+      expect(
+        Object.fromEntries(cleared.map(r => [r.installationId, r.channels]))
+      ).toEqual({
+        'iid-a': ['old-news'],
+        'iid-b': ['old-sports'],
+      });
       const cleared = all.filter(r => r.installationId !== 'multi-iid-d');
       expect(cleared.length).toBe(3);
-      cleared.forEach(r => {
-        expect(r.deviceToken).toBeUndefined();
-      });
+      expect(
+        Object.fromEntries(cleared.map(r => [r.installationId, r.channels]))
+      ).toEqual({
+        'multi-iid-a': ['c-multi-iid-a'],
+        'multi-iid-b': ['c-multi-iid-b'],
+        'multi-iid-c': ['c-multi-iid-c'],
+      });

Also applies to: 1466-1470

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/ParseInstallation.spec.js` around lines 1360 - 1366, The tests currently
only check presence of the channels field using
expect(r.channels).toBeDefined(), which won't catch regressions that change its
contents; update the assertions in the loop that iterates over cleared (and the
similar block around lines 1466-1470) to assert exact channel arrays for each
installation row (use expect(r.channels).toEqual(...) with the original expected
arrays for each installationId such as those created in the test setup) instead
of toBeDefined(), and keep the deviceToken assertion as-is.

1580-1694: ⚡ Quick win

Add an enforceAuth=true integration case for the merge path.

This block exercises applyDuplicateDeviceTokenMerge() for delete/update and both priorities, but never with auth enforcement enabled. That helper has its own enforceAuth branch and permission-denial handling in src/InstallationDedup.js, so a focused ACL-protected merge test would cover a materially different path than the non-merge dedup specs above.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/ParseInstallation.spec.js` around lines 1580 - 1694, Add a new
integration test case in this describe that enables ACL enforcement by calling
reconfigureWithInstallationOptions with enforceAuth: true (plus whichever
duplicateDeviceTokenAction/duplicateDeviceTokenMergePriority you want to cover),
then create the merge fixture via setupMergeFixture and trigger the merge with
rest.create using auth.nobody(config); assert the outcome expected under the
enforceAuth branch (permission denied/403 or no-merge behavior according to
applyDuplicateDeviceTokenMerge in src/InstallationDedup.js) — repeat for at
least one combination of mergePriority/action to exercise the enforceAuth branch
and its permission-denial handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@spec/ParseInstallation.spec.js`:
- Around line 1360-1366: The tests currently only check presence of the channels
field using expect(r.channels).toBeDefined(), which won't catch regressions that
change its contents; update the assertions in the loop that iterates over
cleared (and the similar block around lines 1466-1470) to assert exact channel
arrays for each installation row (use expect(r.channels).toEqual(...) with the
original expected arrays for each installationId such as those created in the
test setup) instead of toBeDefined(), and keep the deviceToken assertion as-is.
- Around line 1580-1694: Add a new integration test case in this describe that
enables ACL enforcement by calling reconfigureWithInstallationOptions with
enforceAuth: true (plus whichever
duplicateDeviceTokenAction/duplicateDeviceTokenMergePriority you want to cover),
then create the merge fixture via setupMergeFixture and trigger the merge with
rest.create using auth.nobody(config); assert the outcome expected under the
enforceAuth branch (permission denied/403 or no-merge behavior according to
applyDuplicateDeviceTokenMerge in src/InstallationDedup.js) — repeat for at
least one combination of mergePriority/action to exercise the enforceAuth branch
and its permission-denial handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd8d1e5f-ea95-4e7e-8458-c1638288ba61

📥 Commits

Reviewing files that changed from the base of the PR and between 0c05d6b and ea95995.

📒 Files selected for processing (1)
  • spec/ParseInstallation.spec.js

@mtrezza mtrezza merged commit 9fee1a0 into parse-community:alpha Apr 30, 2026
24 checks passed
parseplatformorg pushed a commit that referenced this pull request Apr 30, 2026
# [9.9.0-alpha.3](9.9.0-alpha.2...9.9.0-alpha.3) (2026-04-30)

### Features

* Add installation deviceToken deduplication options ([#10451](#10451)) ([9fee1a0](9fee1a0))
@parseplatformorg
Copy link
Copy Markdown
Contributor

🎉 This change has been released in version 9.9.0-alpha.3

@parseplatformorg parseplatformorg added the state:released-alpha Released as alpha version label Apr 30, 2026
@mtrezza mtrezza deleted the feat/installation-dedup-options-v9 branch April 30, 2026 22:30
parseplatformorg pushed a commit that referenced this pull request May 1, 2026
# [9.9.0](9.8.0...9.9.0) (2026-05-01)

### Bug Fixes

* Context mutations leak across requests in `ParseServerRESTController` ([#10291](#10291)) ([60a58ec](60a58ec))
* MFA SMS one-time password accepted twice under concurrent login ([GHSA-jpq4-7fmq-q5fj](GHSA-jpq4-7fmq-q5fj)) ([#10448](#10448)) ([725be0d](725be0d))

### Features

* Add `rawValues` and `rawFieldNames` options for aggregation queries ([#10438](#10438)) ([f26700e](f26700e))
* Add installation deviceToken deduplication options ([#10451](#10451)) ([9fee1a0](9fee1a0))
@parseplatformorg
Copy link
Copy Markdown
Contributor

🎉 This change has been released in version 9.9.0

@parseplatformorg parseplatformorg added the state:released Released as stable version label May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state:released Released as stable version state:released-alpha Released as alpha version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants