-
Notifications
You must be signed in to change notification settings - Fork 88
Add Duplicate Writes documentation #1103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- 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
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughAdds 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
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
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate 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. Comment |
|
There was a problem hiding this 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
📒 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 onexpectedResponse
in other examples.Ensure consistency across all instances or remove the prop.
Also applies to: 147-176
<WriteRequestViewer | ||
skipSetup={true} | ||
relationshipTuples={[ | ||
{ | ||
user: 'user:anne', | ||
relation: 'reader', | ||
object: 'document:2025-budget', | ||
}, | ||
]} | ||
writeOptions={{ | ||
on_duplicate: 'ignore', | ||
}} | ||
expectedResponse={{ | ||
data: {}, | ||
}} | ||
allowedLanguages={[ | ||
SupportedLanguage.CURL, | ||
]} | ||
|
||
/> |
There was a problem hiding this comment.
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.
// 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); | ||
|
There was a problem hiding this comment.
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.
tuple_keys: opts.relationshipTuples.map((tuple) => { | ||
const { _description, ...cleanTuple } = tuple; | ||
return cleanTuple; | ||
}), | ||
}; |
There was a problem hiding this comment.
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)
There was a problem hiding this 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
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.** |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
Closing this PR in preference of new PR #1105, actioning on all the comments in this PR. |
Summary
This PR adds comprehensive documentation for the Duplicate Writes feature on the Write API endpoint, which introduces
on_duplicate
andon_missing
parameters to handle tuple conflicts and missing deletes.Changes
Documentation
docs/content/interacting/duplicate-writes.mdx
- Complete documentation for the duplicate writes featuredocs/content/interacting/transactional-writes.mdx
- Added cross-reference to duplicate writes featuredocs/sidebars.js
- Added new page to navigation under "Interacting" sectionComponent Enhancements
src/components/Docs/SnippetViewer/WriteRequestViewer.tsx
writeOptions
anddeleteOptions
propson_duplicate
andon_missing
authorization_model_id
at the bottom of JSON payloads for better readabilityFeatures Documented
Core Functionality
on_duplicate
parameter: Controls behavior when tuples already existreject
(default): Returns error on duplicate tuplesignore
: Silently skips duplicate tupleson_missing
parameter: Controls behavior when delete targets don't existreject
(default): Returns error on missing tuplesignore
: Silently skips missing tuplesUse Cases
Important Concepts
Technical Details
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
Documentation