Skip to content

Conversation

tylernix
Copy link
Contributor

@tylernix tylernix commented Sep 10, 2025

Summary

This PR adds comprehensive documentation for the Duplicate Writes feature on the Write API endpoint, which introduces on_duplicate and on_missing parameters to handle tuple conflicts and missing deletes.

Changes

Documentation

  • New file: docs/content/interacting/duplicate-writes.mdx - Complete documentation for the duplicate writes feature
  • Updated: docs/content/interacting/transactional-writes.mdx - Added cross-reference to duplicate writes feature
  • Updated: docs/sidebars.js - Added new page to navigation under "Interacting" section

Component Enhancements

  • Enhanced: src/components/Docs/SnippetViewer/WriteRequestViewer.tsx
    • Added support for writeOptions and deleteOptions props
    • Implemented pretty-printed JSON formatting in CURL examples
    • Added conditional parameter inclusion for on_duplicate and on_missing
    • Positioned authorization_model_id at the bottom of JSON payloads for better readability

Features Documented

Core Functionality

  • on_duplicate parameter: Controls behavior when tuples already exist
    • reject (default): Returns error on duplicate tuples
    • ignore: Silently skips duplicate tuples
  • on_missing parameter: Controls behavior when delete targets don't exist
    • reject (default): Returns error on missing tuples
    • ignore: Silently skips missing tuples

Use Cases

  • Data synchronization scenarios
  • Idempotent operations
  • Batch processing with mixed operations
  • Race condition handling

Important Concepts

  • Atomicity guarantees (all operations succeed or fail together)
  • Performance considerations
  • Best practices for different scenarios
  • Troubleshooting common conflicts

Technical Details

  • All examples include working CURL commands
  • Interactive code viewers support multiple programming languages
  • Cross-references to related API documentation
  • Comprehensive error scenarios and solutions

The documentation provides both conceptual explanations and practical examples to help developers understand when and how to use these new parameters effectively.

Summary by CodeRabbit

  • New Features

    • Docs code samples now support on_duplicate and on_missing options, with improved cURL snippets using structured JSON payloads for clarity.
  • Documentation

    • Added a new guide on handling duplicate writes and missing deletes in the Write API, including behavior, atomicity, conflict nuances, and practical examples.
    • Updated the transactional writes page with tips linking to the new guide and added a related link for easier discovery.
    • Added a “Duplicate Writes” item to the navigation under “Interacting with the API.”

- Create new duplicate-writes.mdx file with comprehensive documentation
- Document on_duplicate and on_missing parameters for Write API
- Include practical examples showing default vs permissive behavior
- Add API parameter reference with CURL examples
- Update WriteRequestViewer component to support writeOptions and deleteOptions
- Implement pretty-printed JSON formatting in CURL examples
- Update transactional-writes.mdx to reference new duplicate writes feature
- Add duplicate-writes to sidebar navigation
- Document use cases for data synchronization and idempotent operations
- Include important concepts about atomicity and race conditions
- Add detailed explanations for tuple conditions and conflict scenarios
- Create new duplicate-writes.mdx file with comprehensive documentation
- Document on_duplicate and on_missing parameters for Write API
- Include practical examples showing default vs permissive behavior
- Add API parameter reference with CURL examples
- Update WriteRequestViewer component to support writeOptions and deleteOptions
- Implement pretty-printed JSON formatting in CURL examples
- Update transactional-writes.mdx to reference new duplicate writes feature
- Add duplicate-writes to sidebar navigation
- Document use cases for data synchronization and idempotent operations
- Include important concepts about atomicity and race conditions
- Add detailed explanations for tuple conditions and conflict scenarios
@tylernix tylernix requested review from a team as code owners September 10, 2025 18:05
Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds a new “Duplicate Writes” docs page, links to it from “Transactional Writes,” updates the docs sidebar to include it, and enhances WriteRequestViewer to accept write/delete option flags and build a structured JSON payload for the CURL snippet, conditionally including writes/deletes and options.

Changes

Cohort / File(s) Summary
Docs: New guide — Duplicate Writes
docs/content/interacting/duplicate-writes.mdx
New page explaining handling duplicates/missing deletes in Write API using on_duplicate/on_missing, atomicity notes, examples, and UI snippets.
Docs: Cross-references in Transactional Writes
docs/content/interacting/transactional-writes.mdx
Adds two tip blocks linking to Duplicate Writes and a RelatedSection entry. Content-only updates.
Docs Sidebar Update
docs/sidebars.js
Adds “Duplicate Writes” doc item (content/interacting/duplicate-writes) under “Interacting with the API,” after “Transactional Writes.”
Snippet Viewer: CURL payload builder + options
src/components/Docs/SnippetViewer/WriteRequestViewer.tsx
Extends WriteRequestViewerOpts with writeOptions/deleteOptions. For CURL, switches to building a structured JSON request body, conditionally adds writes/deletes, strips _description, applies on_duplicate/on_missing, includes authorization_model_id, and pretty-prints JSON for curl -d.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant DocPage as Docs Page (MDX)
  participant Viewer as WriteRequestViewer
  participant Builder as CURL Payload Builder
  participant API as Write API

  DocPage->>Viewer: render with tuples, auth_model_id, writeOptions/deleteOptions
  Viewer->>Builder: build requestBody for CURL
  rect rgba(200,220,255,0.25)
    note over Builder: Construct JSON payload
    Builder->>Builder: If writes exist, add writes.tuple_keys (strip _description)
    Builder->>Builder: If writeOptions.on_duplicate provided, set on_duplicate
    Builder->>Builder: If deletes exist, add deletes.tuple_keys
    Builder->>Builder: If deleteOptions.on_missing provided, set on_missing
    Builder->>Builder: Set authorization_model_id
  end
  Builder-->>Viewer: pretty-printed JSON for curl -d
  Viewer->>API: curl POST /write -d '<json>'
  API-->>Viewer: 200 OK / 409 Conflict per server semantics
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Add Duplicate Writes documentation” accurately and concisely describes the primary change in this PR, which is the addition of documentation for the Duplicate Writes feature, and it aligns directly with the content and objectives of the pull request.
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/duplicate-writes-docs

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

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

Copy link
Contributor

github-actions bot commented Sep 10, 2025

PR Preview Action v1.6.2
Preview removed because the pull request was closed.
2025-09-12 19:42 UTC

Copy link
Contributor

@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: 3

🧹 Nitpick comments (3)
src/components/Docs/SnippetViewer/WriteRequestViewer.tsx (1)

98-104: Optional: safer curl payload quoting.

Pretty JSON inside single quotes can break if values contain '. Consider a heredoc to avoid escaping issues.

Example:

-  -d '${prettyJson}'`;
+  --data-binary @- <<'JSON'
+${prettyJson}
+JSON`;
docs/content/interacting/transactional-writes.mdx (1)

170-173: Unify link style (drop extension).

Elsewhere you link to ./duplicate-writes (no extension). Make this tip consistent to avoid broken links in some setups.

Apply this diff:

-For handling cases where you need to write tuples that might already exist or delete tuples that might not exist, check out [Duplicate Writes](./duplicate-writes.mdx) which provides resilient write operations.
+For handling cases where you need to write tuples that might already exist or delete tuples that might not exist, check out [Duplicate Writes](./duplicate-writes) which provides resilient write operations.
docs/content/interacting/duplicate-writes.mdx (1)

188-192: Potentially contentious claim about delete race conditions.

“Deletes are immune to race conditions due to DB-level locks” may be storage-implementation dependent. Consider softening to “less prone” or scoping to current backends.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c6d7f1 and f4bb1c5.

📒 Files selected for processing (4)
  • docs/content/interacting/duplicate-writes.mdx (1 hunks)
  • docs/content/interacting/transactional-writes.mdx (2 hunks)
  • docs/sidebars.js (1 hunks)
  • src/components/Docs/SnippetViewer/WriteRequestViewer.tsx (2 hunks)
🧰 Additional context used
🪛 GitHub Check: Run Checks
src/components/Docs/SnippetViewer/WriteRequestViewer.tsx

[failure] 86-86:
'_description' is assigned a value but never used


[failure] 74-74:
'_description' is assigned a value but never used


[failure] 69-69:
Unexpected any. Specify a different type

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: markdown-link-check-md
  • GitHub Check: deploy-preview
  • GitHub Check: Test deployment
🔇 Additional comments (4)
src/components/Docs/SnippetViewer/WriteRequestViewer.tsx (1)

28-33: Nice: options shape added for writes/deletes.

Prop names match docs (on_duplicate, on_missing) and string unions are clear.

docs/content/interacting/transactional-writes.mdx (1)

236-241: LGTM: Related link wired correctly.

link/id target ./duplicate-writes matches the new doc’s slug.

docs/sidebars.js (1)

366-370: LGTM: sidebar entry added in the right section/order.

id: 'content/interacting/duplicate-writes' matches the page path/slug.

docs/content/interacting/duplicate-writes.mdx (1)

116-136: Same note on expectedResponse in other examples.

Ensure consistency across all instances or remove the prop.

Also applies to: 147-176

Comment on lines 84 to 103
<WriteRequestViewer
skipSetup={true}
relationshipTuples={[
{
user: 'user:anne',
relation: 'reader',
object: 'document:2025-budget',
},
]}
writeOptions={{
on_duplicate: 'ignore',
}}
expectedResponse={{
data: {},
}}
allowedLanguages={[
SupportedLanguage.CURL,
]}

/>
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify expectedResponse prop is supported by WriteRequestViewer.

The component’s TS signature (WriteRequestViewerOpts) in this PR doesn’t include expectedResponse; MDX usage may fail type-checking depending on build settings.

Run:

If types fail, either add expectedResponse?: unknown to WriteRequestViewerOpts or switch the MDX to omit it.


🏁 Script executed:

#!/bin/bash
# Find all uses of expectedResponse on WriteRequestViewer
rg -n "WriteRequestViewer\\(" -C2 docs/content | rg -n "expectedResponse"
# Inspect the component props
rg -n "interface WriteRequestViewerOpts" -n src/components/Docs/SnippetViewer/WriteRequestViewer.tsx -C3
# Look for a shared opts type that might already include expectedResponse
rg -n "defaultOperationsViewer" -C3 src/components

Length of output: 11031


Add expectedResponse to WriteRequestViewerOpts

src/components/Docs/SnippetViewer/WriteRequestViewer.tsx’s interface WriteRequestViewerOpts omits expectedResponse, so the MDX snippet will fail type-checking. Add:

interface WriteRequestViewerOpts {
  authorizationModelId?: string;
  relationshipTuples: RelationshipTuple[];
  deleteRelationshipTuples: RelationshipTupleWithoutCondition[];
+ expectedResponse?: unknown;
}

Or remove expectedResponse from the docs snippet.

🤖 Prompt for AI Agents
In docs/content/interacting/duplicate-writes.mdx around lines 84 to 103, the MDX
snippet uses the prop expectedResponse but the TypeScript interface
WriteRequestViewerOpts in
src/components/Docs/SnippetViewer/WriteRequestViewer.tsx does not declare it,
causing type errors; add an optional expectedResponse?: unknown property to the
WriteRequestViewerOpts interface (or remove expectedResponse from the MDX
snippet) and update any usages/props typing to accept the new field so the
component compiles.

Comment on lines 68 to 99
// Build the JSON object for pretty printing
const requestBody: any = {};

if (opts.relationshipTuples?.length) {
requestBody.writes = {
tuple_keys: opts.relationshipTuples.map((tuple) => {
const { _description, ...cleanTuple } = tuple;
return cleanTuple;
}),
};
if (opts.writeOptions?.on_duplicate) {
requestBody.writes.on_duplicate = opts.writeOptions.on_duplicate;
}
}

if (opts.deleteRelationshipTuples?.length) {
requestBody.deletes = {
tuple_keys: opts.deleteRelationshipTuples.map((tuple) => {
const { _description, ...cleanTuple } = tuple;
return cleanTuple;
}),
};
if (opts.deleteOptions?.on_missing) {
requestBody.deletes.on_missing = opts.deleteOptions.on_missing;
}
}

// Add authorization_model_id at the end
requestBody.authorization_model_id = modelId;

const prettyJson = JSON.stringify(requestBody, null, 2);

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Type the request body; avoid any.

Use a typed shape for the CURL payload to catch drift and improve DX.

Apply this diff inside the shown range:

-      // Build the JSON object for pretty printing
-      const requestBody: any = {};
+      // Build the JSON object for pretty printing
+      const requestBody: Partial<CurlRequestBody> = {};

And add this type near the interfaces (outside this hunk):

type CurlWriteBody = {
  tuple_keys: Array<Omit<RelationshipTuple, '_description'>>;
  on_duplicate?: 'error' | 'ignore';
};
type CurlDeleteBody = {
  tuple_keys: Array<Omit<RelationshipTupleWithoutCondition, '_description'>>;
  on_missing?: 'error' | 'ignore';
};
type CurlRequestBody = {
  authorization_model_id: string;
  writes?: CurlWriteBody;
  deletes?: CurlDeleteBody;
};
🧰 Tools
🪛 GitHub Check: Run Checks

[failure] 86-86:
'_description' is assigned a value but never used


[failure] 74-74:
'_description' is assigned a value but never used


[failure] 69-69:
Unexpected any. Specify a different type

🤖 Prompt for AI Agents
In src/components/Docs/SnippetViewer/WriteRequestViewer.tsx around lines 68 to
99, the requestBody is currently typed as any; replace it with a strongly typed
CurlRequestBody and use the provided
CurlWriteBody/CurlDeleteBody/CurlRequestBody types (add those type definitions
near the existing interfaces, outside this hunk). Change const requestBody: any
= {} to const requestBody: CurlRequestBody = { authorization_model_id: modelId }
and adjust the code that assigns writes/deletes to build objects matching the
new types (tuple_keys arrays should be typed as Omit<RelationshipTuple,
'_description'> or Omit<RelationshipTupleWithoutCondition, '_description'> and
optional on_duplicate/on_missing typed as 'error'|'ignore'), ensuring
authorization_model_id is present and JSON.stringify(prettyJson) remains the
same.

Comment on lines 73 to 77
tuple_keys: opts.relationshipTuples.map((tuple) => {
const { _description, ...cleanTuple } = tuple;
return cleanTuple;
}),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix lint: “'_description' assigned but never used” and make sanitization explicit.

Pick fields instead of binding and dropping _description.

Apply this diff:

-          tuple_keys: opts.relationshipTuples.map((tuple) => {
-            const { _description, ...cleanTuple } = tuple;
-            return cleanTuple;
-          }),
+          tuple_keys: opts.relationshipTuples.map(({ user, relation, object, condition }) => {
+            const out: Omit<RelationshipTuple, '_description'> =
+              condition ? { user, relation, object, condition } : { user, relation, object };
+            return out;
+          }),
-          tuple_keys: opts.deleteRelationshipTuples.map((tuple) => {
-            const { _description, ...cleanTuple } = tuple;
-            return cleanTuple;
-          }),
+          tuple_keys: opts.deleteRelationshipTuples.map(({ user, relation, object }) => ({
+            user,
+            relation,
+            object,
+          })),

Also applies to: 85-89

🧰 Tools
🪛 GitHub Check: Run Checks

[failure] 74-74:
'_description' is assigned a value but never used

🤖 Prompt for AI Agents
In src/components/Docs/SnippetViewer/WriteRequestViewer.tsx around lines 73-77
(and similarly 85-89), avoid the "_description assigned but never used" lint by
explicitly constructing sanitized tuple objects instead of destructuring to drop
_description; replace the current map that does "const { _description,
...cleanTuple } = tuple; return cleanTuple" with a pick of the allowed tuple
fields (for example: return { namespace: tuple.namespace, object: tuple.object,
relation: tuple.relation, subject: tuple.subject } or whatever the concrete
tuple shape is in this codebase) so sanitization is explicit and the linter
error is resolved — apply the same change in the 85-89 range.

- Replace 'any' type with proper RequestBody interface
- Use eslint-disable-next-line for unused _description variables
- Add proper TypeScript types for request body structure
- Fix error message example to match the actual tuple (writer vs reader)
- Correct relation in first WriteRequestViewer example (writer vs reader)
- Add line break for better formatting in 'Best effort' ignore section
- Update RelatedSection description for clarity
- Fix title in related links (Write API vs {ProductName} API)
Copy link
Member

@adriantam adriantam left a comment

Choose a reason for hiding this comment

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

- Add new card for Duplicate Writes in the interacting section overview
- Position it after Transactional Writes since they are related concepts
- Include descriptive text about handling duplicate writes and missing deletes
@aaguiarz
Copy link
Member

I'm thinking we should have a single page for /Write...

Combine this https://openfga.dev/docs/getting-started/update-tuples and this https://openfga.dev/docs/interacting/transactional-writes which are pretty similar, and explain the additional parameters.

@rhamzeh wdyt?


The `on_duplicate` parameter controls the behavior when writing tuples.

- **"error" (Default)**: The request fails if any tuple in the writes array already exists. This maintains backward compatibility.
Copy link
Member

Choose a reason for hiding this comment

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

We don't need to mention backward compatibility in the docs (someone will read it in a year and not have any context)


### "Best effort" ignore

For writes: An `on_duplicate: 'ignore'` operation uses a "best effort" approach. We will attempt *once* to ignore duplicates and write non-duplicates, but if there is ever a conflict writing to the database (i.e. write a tuple we don’t think exists but it suddenly exists, probably due to a parallel request), we will abort the race condition immediately, and just return a `409 Conflict` error. These errors are rare, but can happen.
Copy link
Member

Choose a reason for hiding this comment

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

Let's verify if this happens in SQL databases


For writes: An `on_duplicate: 'ignore'` operation uses a "best effort" approach. We will attempt *once* to ignore duplicates and write non-duplicates, but if there is ever a conflict writing to the database (i.e. write a tuple we don’t think exists but it suddenly exists, probably due to a parallel request), we will abort the race condition immediately, and just return a `409 Conflict` error. These errors are rare, but can happen.

For deletes: An `on_missing: 'ignore'` operation is immune to race conditions due to database-level locks. It is not possible for another request to interfere since a delete operation will always succeed if the tuple exists or not.
Copy link
Member

Choose a reason for hiding this comment

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

We don't need to mention this


### "Ignore" is Not an "Upsert"

It is critical to understand that `on_duplicate: "ignore"` will not update an existing tuple, only ignore an identical tuple. This is why we do not call the operation an "idempotent" operation.
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to explain this? We don't need to mention idempotent or explain why we don't use it


The behavior of `on_duplicate: "ignore"` is more nuanced for tuples with conditions.
- **Identical Tuples**: If a tuple in the request is 100% identical to an existing tuple (same user, relation, object, condition name, and condition context), it will be safely ignored.
- **Conflicting Tuples**: If a tuple key (user, relation, object) matches an existing tuple, but the condition is different, this is a conflict. The write attempt will be rejected, and the entire transaction will fail with a `409 Conflict` error. **The correct pattern to safely update a tuple's condition requires explicitly deleting the old tuple and writing the new one within the same atomic Write request.**
Copy link
Member

Choose a reason for hiding this comment

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

@tylernix can you confirm adding/deleting the same in a single request works well?


The decision to have separate `on_duplicate` and `on_missing` parameters is intentional. This design gives you granular, independent control over the behavior of writes and deletes within a single atomic transaction. You can mix and match strict and permissive behaviors to suit your exact needs.

For example, you might perform a strict delete (`on_missing: "error"`) to confirm that a specific permission has been successfully removed before making a permissive write (`on_duplicate: "ignore"`) that guarantees the new permission exists.
Copy link
Member

Choose a reason for hiding this comment

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

I don't follow the example, if I perform a delete with 'on_missing:error' and the relation does not exist, it's fine, I can add the new permission safelty.

@tylernix tylernix linked an issue Sep 12, 2025 that may be closed by this pull request
@tylernix
Copy link
Contributor Author

Closing this PR in preference of new PR #1105, actioning on all the comments in this PR.

@tylernix tylernix closed this Sep 12, 2025
@tylernix tylernix deleted the feature/duplicate-writes-docs branch September 12, 2025 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add documentation for the new Ignore Duplicate Write feature

3 participants