Skip to content
Merged
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Main config
OPENFGA_DOCKER_TAG = v1
OPEN_API_REF ?= e53c69cc55317404d02a6d8e418d626268f28a59
OPEN_API_REF ?= 0ac19aac54f21f3c78970126b84b4c69c6e3b9a2
OPEN_API_URL = https://raw.githubusercontent.com/openfga/api/${OPEN_API_REF}/docs/openapiv2/apidocs.swagger.json
OPENAPI_GENERATOR_CLI_DOCKER_TAG ?= v6.4.0
NODE_DOCKER_TAG = 20-alpine
Expand Down
79 changes: 79 additions & 0 deletions config/clients/js/template/README_calling_api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,85 @@ response = {
*/
```

#### Conflict Options for Write Operations

The SDK supports conflict options for write operations, allowing you to control how the API handles duplicate writes and missing deletes.

> **Note**: This requires OpenFGA [v1.10.0](https://github.com/openfga/openfga/releases/tag/v1.10.0) or later.

##### Using Conflict Options with Write
```javascript
const options = {
conflict: {
// Control what happens when writing a tuple that already exists
onDuplicateWrites: OnDuplicateWrites.Ignore, // or OnDuplicateWrites.Error (the current default behavior)
// Control what happens when deleting a tuple that doesn't exist
onMissingDeletes: OnMissingDeletes.Ignore, // or OnMissingDeletes.Error (the current default behavior)
}
};

const body = {
writes: [{
user: 'user:anne',
relation: 'writer',
object: 'document:2021-budget',
}],
deletes: [{
user: 'user:bob',
relation: 'reader',
object: 'document:2021-budget',
}],
};

const response = await fgaClient.write(body, options);
```

##### Using Conflict Options with WriteTuples
```javascript
const tuples = [{
user: 'user:anne',
relation: 'writer',
object: 'document:2021-budget',
}];

const options = {
conflict: {
onDuplicateWrites: OnDuplicateWrites.Ignore,
}
};

const response = await fgaClient.writeTuples(tuples, options);
```

##### Using Conflict Options with DeleteTuples
```javascript
const tuples = [{
user: 'user:bob',
relation: 'reader',
object: 'document:2021-budget',
}];

const options = {
conflict: {
onMissingDeletes: OnMissingDeletes.Ignore,
}
};

const response = await fgaClient.deleteTuples(tuples, options);
```

##### Conflict Options Behavior

- **`onDuplicateWrites`**:
- `OnDuplicateWrites.Error` (default): Returns an error if an identical tuple already exists (matching on user, relation, object, and condition)
- `OnDuplicateWrites.Ignore`: Treats duplicate writes as no-ops, allowing idempotent write operations

- **`onMissingDeletes`**:
- `OnMissingDeletes.Error` (default): Returns an error when attempting to delete a tuple that doesn't exist
- `OnMissingDeletes.Ignore`: Treats deletes of non-existent tuples as no-ops, allowing idempotent delete operations

> **Important**: If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back.

#### Relationship Queries

##### Check
Expand Down
72 changes: 65 additions & 7 deletions config/clients/js/template/client.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
WriteAuthorizationModelRequest,
WriteAuthorizationModelResponse,
WriteRequest,
WriteRequestWritesOnDuplicateEnum,
WriteRequestDeletesOnMissingEnum,
} from "./apiModel";
import { BaseAPI } from "./base";
import { CallResult, PromiseResult } from "./common";
Expand Down Expand Up @@ -176,12 +178,55 @@ export interface ClientBatchCheckResponse {
result: ClientBatchCheckSingleResponse[];
}

export const OnDuplicateWrites = WriteRequestWritesOnDuplicateEnum;

export const OnMissingDeletes = WriteRequestDeletesOnMissingEnum;

export interface ClientWriteConflictOptions {
/**
* Controls behavior when writing a tuple that already exists
* - `OnDuplicateWrites.Error`: Return error on duplicates (default)
* - `OnDuplicateWrites.Ignore`: Silently skip duplicate writes
*/
onDuplicateWrites?: typeof OnDuplicateWrites[keyof typeof OnDuplicateWrites];

/**
* Controls behavior when deleting a tuple that doesn't exist
* - `OnMissingDeletes.Error`: Return error on missing deletes (default)
* - `OnMissingDeletes.Ignore`: Silently skip missing deletes
*/
onMissingDeletes?: typeof OnMissingDeletes[keyof typeof OnMissingDeletes];
}

export interface ClientWriteRequestOpts {
transaction?: {
disable?: boolean;
maxPerChunk?: number;
maxParallelRequests?: number;
}
conflict?: ClientWriteConflictOptions;
}

export interface ClientWriteTuplesRequestOpts {
transaction?: {
disable?: boolean;
maxPerChunk?: number;
maxParallelRequests?: number;
};
conflict?: {
onDuplicateWrites?: typeof OnDuplicateWrites[keyof typeof OnDuplicateWrites];
};
}

export interface ClientDeleteTuplesRequestOpts {
transaction?: {
disable?: boolean;
maxPerChunk?: number;
maxParallelRequests?: number;
};
conflict?: {
onMissingDeletes?: typeof OnMissingDeletes[keyof typeof OnMissingDeletes];
};
}

export interface ClientWriteRequest {
Expand Down Expand Up @@ -463,6 +508,9 @@ export class {{appShortName}}Client extends BaseAPI {
* @param {ClientWriteRequest} body
* @param {ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts} [options]
* @param {string} [options.authorizationModelId] - Overrides the authorization model id in the configuration
* @param {object} [options.conflict] - Conflict handling options
* @param {OnDuplicateWrites} [options.conflict.onDuplicateWrites] - Controls behavior when writing duplicate tuples. Defaults to `OnDuplicateWrites.Error`
* @param {OnMissingDeletes} [options.conflict.onMissingDeletes] - Controls behavior when deleting non-existent tuples. Defaults to `OnMissingDeletes.Error`
* @param {object} [options.transaction]
* @param {boolean} [options.transaction.disable] - Disables running the write in a transaction mode. Defaults to `false`
* @param {number} [options.transaction.maxPerChunk] - Max number of items to send in a single transaction chunk. Defaults to `1`
Expand All @@ -473,7 +521,7 @@ export class {{appShortName}}Client extends BaseAPI {
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
*/
async write(body: ClientWriteRequest, options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise<ClientWriteResponse> {
const { transaction = {}, headers = {} } = options;
const { transaction = {}, headers = {}, conflict } = options;
const {
maxPerChunk = 1, // 1 has to be the default otherwise the chunks will be sent in transactions
maxParallelRequests = DEFAULT_MAX_METHOD_PARALLEL_REQS,
Expand All @@ -486,10 +534,16 @@ export class {{appShortName}}Client extends BaseAPI {
authorization_model_id: authorizationModelId,
};
if (writes?.length) {
apiBody.writes = { tuple_keys: writes };
apiBody.writes = {
tuple_keys: writes,
on_duplicate: conflict?.onDuplicateWrites
};
}
if (deletes?.length) {
apiBody.deletes = { tuple_keys: deletes };
apiBody.deletes = {
tuple_keys: deletes,
on_missing: conflict?.onMissingDeletes
};
}
await this.api.write(this.getStoreId(options)!, apiBody, options);
return {
Expand Down Expand Up @@ -553,8 +607,10 @@ export class {{appShortName}}Client extends BaseAPI {
/**
* WriteTuples - Utility method to write tuples, wraps Write
* @param {TupleKey[]} tuples
* @param {ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts} [options]
* @param {ClientRequestOptsWithAuthZModelId & ClientWriteTuplesRequestOpts} [options]
* @param {string} [options.authorizationModelId] - Overrides the authorization model id in the configuration
* @param {object} [options.conflict] - Conflict handling options
* @param {OnDuplicateWrites} [options.conflict.onDuplicateWrites] - Controls behavior when writing duplicate tuples. Defaults to `OnDuplicateWrites.Error`
* @param {object} [options.transaction]
* @param {boolean} [options.transaction.disable] - Disables running the write in a transaction mode. Defaults to `false`
* @param {number} [options.transaction.maxPerChunk] - Max number of items to send in a single transaction chunk. Defaults to `1`
Expand All @@ -564,7 +620,7 @@ export class {{appShortName}}Client extends BaseAPI {
* @param {number} [options.retryParams.maxRetry] - Override the max number of retries on each API request
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
*/
async writeTuples(tuples: TupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise<ClientWriteResponse> {
async writeTuples(tuples: TupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteTuplesRequestOpts = {}): Promise<ClientWriteResponse> {
const { headers = {} } = options;
setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "WriteTuples");
return this.write({ writes: tuples }, { ...options, headers });
Expand All @@ -573,8 +629,10 @@ export class {{appShortName}}Client extends BaseAPI {
/**
* DeleteTuples - Utility method to delete tuples, wraps Write
* @param {TupleKeyWithoutCondition[]} tuples
* @param {ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts} [options]
* @param {ClientRequestOptsWithAuthZModelId & ClientDeleteTuplesRequestOpts} [options]
* @param {string} [options.authorizationModelId] - Overrides the authorization model id in the configuration
* @param {object} [options.conflict] - Conflict handling options
* @param {OnMissingDeletes} [options.conflict.onMissingDeletes] - Controls behavior when deleting non-existent tuples. Defaults to `OnMissingDeletes.Error`
* @param {object} [options.transaction]
* @param {boolean} [options.transaction.disable] - Disables running the write in a transaction mode. Defaults to `false`
* @param {number} [options.transaction.maxPerChunk] - Max number of items to send in a single transaction chunk. Defaults to `1`
Expand All @@ -584,7 +642,7 @@ export class {{appShortName}}Client extends BaseAPI {
* @param {number} [options.retryParams.maxRetry] - Override the max number of retries on each API request
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
*/
async deleteTuples(tuples: TupleKeyWithoutCondition[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise<ClientWriteResponse> {
async deleteTuples(tuples: TupleKeyWithoutCondition[], options: ClientRequestOptsWithAuthZModelId & ClientDeleteTuplesRequestOpts = {}): Promise<ClientWriteResponse> {
const { headers = {} } = options;
setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "DeleteTuples");
return this.write({ deletes: tuples }, { ...options, headers });
Expand Down
5 changes: 4 additions & 1 deletion config/clients/js/template/example/example1/example1.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ async function main () {
object: "document:7772ab2a-d83f-756d-9397-c5ed9f3cb69a"
}
]
}, { authorizationModelId });
}, {
authorizationModelId,
conflict: { onDuplicateWrites: 'ignore' }
});
console.log("Done Writing Tuples");

// Set the model ID
Expand Down
Loading
Loading