Skip to content

Commit d8146f3

Browse files
committed
fix(client): fix read and add sleep on batch calls
1 parent 9382142 commit d8146f3

File tree

4 files changed

+84
-41
lines changed

4 files changed

+84
-41
lines changed

.openapi-generator/FILES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ utils/generate-random-id.ts
4444
utils/index.ts
4545
utils/set-header-if-not-set.ts
4646
utils/set-not-enumerable-property.ts
47+
utils/sleep.ts
4748
validation.ts

client.ts

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,32 @@ import {
2828
ReadAuthorizationModelResponse,
2929
ReadAuthorizationModelsResponse,
3030
ReadChangesResponse,
31+
ReadRequest,
3132
ReadResponse,
3233
TupleKey as ApiTupleKey,
3334
WriteAuthorizationModelRequest,
3435
WriteAuthorizationModelResponse,
36+
WriteRequest,
3537
} from "./apiModel";
3638
import { BaseAPI } from "./base";
3739
import { CallResult, PromiseResult } from "./common";
3840
import { Configuration, RetryParams, UserConfigurationParams } from "./configuration";
39-
import { FgaError } from "./errors";
41+
import { FgaError, FgaRequiredParamError } from "./errors";
4042
import {
4143
chunkSequentialCall,
4244
generateRandomIdWithNonUniqueFallback,
4345
setHeaderIfNotSet,
4446
setNotEnumerableProperty,
4547
} from "./utils";
4648

47-
export type OpenFgaClientConfig = (UserConfigurationParams | Configuration) & {
49+
export type ClientConfiguration = (UserConfigurationParams | Configuration) & {
4850
authorizationModelId?: string;
4951
}
5052

51-
type TupleKey = Required<ApiTupleKey>;
53+
export type ClientTupleKey = Required<ApiTupleKey>;
5254

5355
const DEFAULT_MAX_METHOD_PARALLEL_REQS = 10;
56+
const DEFAULT_WAIT_TIME_BETWEEN_CHUNKS_IN_MS = 100;
5457
const DEFAULT_MAX_RETRY_OVERRIDE = 15;
5558
const CLIENT_METHOD_HEADER = "X-OpenFGA-Client-Method";
5659
const CLIENT_BULK_REQUEST_ID_HEADER = "X-OpenFGA-Client-Bulk-Request-Id";
@@ -68,7 +71,7 @@ export type ClientRequestOptsWithAuthZModelId = ClientRequestOpts & Authorizati
6871

6972
export type PaginationOptions = { pageSize?: number, continuationToken?: string; };
7073

71-
export type ClientCheckRequest = TupleKey & { contextualTuples?: TupleKey[] };
74+
export type ClientCheckRequest = ClientTupleKey & { contextualTuples?: ClientTupleKey[] };
7275

7376
export type ClientBatchCheckRequest = ClientCheckRequest[];
7477

@@ -90,16 +93,18 @@ export interface ClientWriteRequestOpts {
9093
transaction?: {
9194
disable?: boolean;
9295
maxPerChunk?: number;
96+
waitTimeBetweenChunksInMs?: number;
9397
}
9498
}
9599

96100
export interface BatchCheckRequestOpts {
97101
maxParallelRequests?: number;
102+
waitTimeBetweenChunksInMs?: number;
98103
}
99104

100105
export interface ClientWriteRequest {
101-
writes?: TupleKey[];
102-
deletes?: TupleKey[];
106+
writes?: ClientTupleKey[];
107+
deletes?: ClientTupleKey[];
103108
}
104109

105110
export enum ClientWriteStatus {
@@ -108,18 +113,18 @@ export enum ClientWriteStatus {
108113
}
109114

110115
export interface ClientWriteResponse {
111-
writes: { tuple_key: TupleKey, status: ClientWriteStatus, err?: Error }[];
112-
deletes: { tuple_key: TupleKey, status: ClientWriteStatus, err?: Error }[];
116+
writes: { tuple_key: ClientTupleKey, status: ClientWriteStatus, err?: Error }[];
117+
deletes: { tuple_key: ClientTupleKey, status: ClientWriteStatus, err?: Error }[];
113118
}
114119

115120
export interface ClientReadChangesRequest {
116121
type: string;
117122
}
118123

119-
export type ClientExpandRequest = Pick<TupleKey, "relation" | "object">;
124+
export type ClientExpandRequest = Pick<ClientTupleKey, "relation" | "object">;
120125
export type ClientReadRequest = ApiTupleKey;
121-
export type ClientListObjectsRequest = Omit<ListObjectsRequest, "authorization_model_id" | "contextual_tuples"> & { contextualTuples?: TupleKey[] };
122-
export type ClientWriteAssertionsRequest = (TupleKey & Pick<Assertion, "expectation">)[];
126+
export type ClientListObjectsRequest = Omit<ListObjectsRequest, "authorization_model_id" | "contextual_tuples"> & { contextualTuples?: ClientTupleKey[] };
127+
export type ClientWriteAssertionsRequest = (ClientTupleKey & Pick<Assertion, "expectation">)[];
123128

124129
function getObjectFromString(objectString: string): { type: string; id: string } {
125130
const [type, id] = objectString.split(":");
@@ -130,14 +135,14 @@ export class OpenFgaClient extends BaseAPI {
130135
public api: OpenFgaApi;
131136
public authorizationModelId?: string;
132137

133-
constructor(configuration: OpenFgaClientConfig, protected axios?: AxiosStatic) {
138+
constructor(configuration: ClientConfiguration, protected axios?: AxiosStatic) {
134139
super(configuration, axios);
135140

136141
this.api = new OpenFgaApi(this.configuration);
137142
this.authorizationModelId = configuration.authorizationModelId;
138143
}
139144

140-
private getAuthorizationModelId(options: AuthorizationModelIdOpts = {}) {
145+
protected getAuthorizationModelId(options: AuthorizationModelIdOpts = {}) {
141146
return options?.authorizationModelId || this.authorizationModelId;
142147
}
143148

@@ -241,7 +246,7 @@ export class OpenFgaClient extends BaseAPI {
241246
async readAuthorizationModel(options: ClientRequestOptsWithAuthZModelId = {}): PromiseResult<ReadAuthorizationModelResponse> {
242247
const authorizationModelId = this.getAuthorizationModelId(options);
243248
if (!authorizationModelId) {
244-
throw new Error("authorization_model_id_required");
249+
throw new FgaRequiredParamError("ClientConfiguration", "authorizationModelId");
245250
}
246251
return this.api.readAuthorizationModel(authorizationModelId, options);
247252
}
@@ -294,8 +299,15 @@ export class OpenFgaClient extends BaseAPI {
294299
* @param {number} [options.retryParams.maxRetry] - Override the max number of retries on each API request
295300
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
296301
*/
297-
async read(body: ClientReadRequest, options: ClientRequestOpts = {}): PromiseResult<ReadResponse> {
298-
return this.api.read({ tuple_key: body }, options);
302+
async read(body: ClientReadRequest = {}, options: ClientRequestOpts & PaginationOptions = {}): PromiseResult<ReadResponse> {
303+
const readRequest: ReadRequest = {
304+
page_size: options.pageSize,
305+
continuation_token: options.continuationToken,
306+
};
307+
if (body.user || body.object || body.relation) {
308+
readRequest.tuple_key = body;
309+
}
310+
return this.api.read(readRequest, options);
299311
}
300312

301313
/**
@@ -306,23 +318,32 @@ export class OpenFgaClient extends BaseAPI {
306318
* @param {object} [options.transaction]
307319
* @param {boolean} [options.transaction.disable] - Disables running the write in a transaction mode. Defaults to `false`
308320
* @param {number} [options.transaction.maxPerChunk] - Max number of items to send in a single transaction chunk. Defaults to `1`
321+
* @param {number} [options.transaction.waitTimeBetweenChunksInMs] - Time to wait between chunks. Defaults to `100`
309322
* @param {object} [options.headers] - Custom headers to send alongside the request
310323
* @param {object} [options.retryParams] - Override the retry parameters for this request
311324
* @param {number} [options.retryParams.maxRetry] - Override the max number of retries on each API request
312325
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
313326
*/
314327
async write(body: ClientWriteRequest, options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise<ClientWriteResponse> {
315328
const { transaction = {}, headers = {} } = options;
316-
const { maxPerChunk = 1 } = transaction; // 1 has to be the default otherwise the chunks will be sent in transactions
329+
const {
330+
maxPerChunk = 1, // 1 has to be the default otherwise the chunks will be sent in transactions
331+
waitTimeBetweenChunksInMs = DEFAULT_WAIT_TIME_BETWEEN_CHUNKS_IN_MS,
332+
} = transaction;
317333
const { writes, deletes } = body;
318334
const authorizationModelId = this.getAuthorizationModelId(options);
319335

320336
if (!transaction?.disable) {
321-
await this.api.write({
322-
writes: { tuple_keys: writes || [] },
323-
deletes: { tuple_keys: deletes || [] },
337+
const apiBody: WriteRequest = {
324338
authorization_model_id: authorizationModelId,
325-
}, options);
339+
};
340+
if (writes?.length) {
341+
apiBody.writes = { tuple_keys: writes };
342+
}
343+
if (deletes?.length) {
344+
apiBody.deletes = { tuple_keys: deletes };
345+
}
346+
await this.api.write(apiBody, options);
326347
return {
327348
writes: writes?.map(tuple => ({
328349
tuple_key: tuple,
@@ -338,61 +359,63 @@ export class OpenFgaClient extends BaseAPI {
338359
setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "Write");
339360
setHeaderIfNotSet(headers, CLIENT_BULK_REQUEST_ID_HEADER, generateRandomIdWithNonUniqueFallback());
340361
const results: ClientWriteResponse = { writes: [], deletes: [] };
341-
await chunkSequentialCall<TupleKey, void>(
362+
await chunkSequentialCall<ClientTupleKey, void>(
342363
(chunk) => this.api.write(
343364
{ writes: { tuple_keys: chunk}, authorization_model_id: authorizationModelId },
344365
{ retryParams: { maxRetry: DEFAULT_MAX_RETRY_OVERRIDE }, headers })
345366
.then(() => { results.writes.push(...chunk.map(tuple => ({ tuple_key: tuple, status: ClientWriteStatus.SUCCESS }))); })
346367
.catch((err) => { results.writes.push(...chunk.map(tuple => ({ tuple_key: tuple, status: ClientWriteStatus.FAILURE, err }))); }),
347368
writes || [],
348-
maxPerChunk,
369+
{ maxPerChunk, waitTimeBetweenChunksInMs },
349370
);
350-
await chunkSequentialCall<TupleKey, void>(
371+
await chunkSequentialCall<ClientTupleKey, void>(
351372
(chunk) => this.api.write(
352373
{ deletes: { tuple_keys: chunk }, authorization_model_id: authorizationModelId },
353374
{ retryParams: { maxRetry: DEFAULT_MAX_RETRY_OVERRIDE }, headers })
354375
.then(() => { results.deletes.push(...chunk.map(tuple => ({ tuple_key: tuple, status: ClientWriteStatus.SUCCESS }))); })
355376
.catch((err) => { results.deletes.push(...chunk.map(tuple => ({ tuple_key: tuple, status: ClientWriteStatus.FAILURE, err }))); }),
356377
deletes || [],
357-
maxPerChunk,
378+
{ maxPerChunk, waitTimeBetweenChunksInMs },
358379
);
359380

360381
return results;
361382
}
362383

363384
/**
364385
* WriteTuples - Utility method to write tuples, wraps Write
365-
* @param {TupleKey} tuples
386+
* @param {ClientTupleKey[]} tuples
366387
* @param {ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts} [options]
367388
* @param {string} [options.authorizationModelId] - Overrides the authorization model id in the configuration
368389
* @param {object} [options.transaction]
369390
* @param {boolean} [options.transaction.disable] - Disables running the write in a transaction mode. Defaults to `false`
370391
* @param {number} [options.transaction.maxPerChunk] - Max number of items to send in a single transaction chunk. Defaults to `1`
392+
* @param {number} [options.transaction.waitTimeBetweenChunksInMs] - Time to wait between chunks. Defaults to `100`
371393
* @param {object} [options.headers] - Custom headers to send alongside the request
372394
* @param {object} [options.retryParams] - Override the retry parameters for this request
373395
* @param {number} [options.retryParams.maxRetry] - Override the max number of retries on each API request
374396
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
375397
*/
376-
async writeTuples(tuples: TupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise<ClientWriteResponse> {
398+
async writeTuples(tuples: ClientTupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise<ClientWriteResponse> {
377399
const { headers = {} } = options;
378400
setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "WriteTuples");
379401
return this.write({ writes: tuples }, { ...options, headers });
380402
}
381403

382404
/**
383405
* DeleteTuples - Utility method to delete tuples, wraps Write
384-
* @param {TupleKey} tuples
406+
* @param {ClientTupleKey[]} tuples
385407
* @param {ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts} [options]
386408
* @param {string} [options.authorizationModelId] - Overrides the authorization model id in the configuration
387409
* @param {object} [options.transaction]
388410
* @param {boolean} [options.transaction.disable] - Disables running the write in a transaction mode. Defaults to `false`
389411
* @param {number} [options.transaction.maxPerChunk] - Max number of items to send in a single transaction chunk. Defaults to `1`
412+
* @param {number} [options.transaction.waitTimeBetweenChunksInMs] - Time to wait between chunks. Defaults to `100`
390413
* @param {object} [options.headers] - Custom headers to send alongside the request
391414
* @param {object} [options.retryParams] - Override the retry parameters for this request
392415
* @param {number} [options.retryParams.maxRetry] - Override the max number of retries on each API request
393416
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
394417
*/
395-
async deleteTuples(tuples: TupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise<ClientWriteResponse> {
418+
async deleteTuples(tuples: ClientTupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise<ClientWriteResponse> {
396419
const { headers = {} } = options;
397420
setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "DeleteTuples");
398421
return this.write({ deletes: tuples }, { ...options, headers });
@@ -431,19 +454,20 @@ export class OpenFgaClient extends BaseAPI {
431454
* BatchCheck - Run a set of checks (evaluates)
432455
* @param {ClientBatchCheckRequest} body
433456
* @param {ClientRequestOptsWithAuthZModelId & BatchCheckRequestOpts} [options]
434-
* @param {number} [options.maxParallelRequests] - Max number of requests to issue in parallel
457+
* @param {number} [options.maxParallelRequests] - Max number of requests to issue in parallel. Defaults to `10`
458+
* @param {number} [options.waitTimeBetweenChunksInMs] - Time to wait between chunks. Defaults to `100`
435459
* @param {string} [options.authorizationModelId] - Overrides the authorization model id in the configuration
436460
* @param {object} [options.headers] - Custom headers to send alongside the request
437461
* @param {object} [options.retryParams] - Override the retry parameters for this request
438462
* @param {number} [options.retryParams.maxRetry] - Override the max number of retries on each API request
439463
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
440464
*/
441465
async batchCheck(body: ClientBatchCheckRequest, options: ClientRequestOptsWithAuthZModelId & BatchCheckRequestOpts = {}): Promise<ClientBatchCheckResponse> {
442-
const { headers = {}, maxParallelRequests = DEFAULT_MAX_METHOD_PARALLEL_REQS } = options;
466+
const { headers = {}, maxParallelRequests = DEFAULT_MAX_METHOD_PARALLEL_REQS, waitTimeBetweenChunksInMs = DEFAULT_WAIT_TIME_BETWEEN_CHUNKS_IN_MS } = options;
443467
setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "BatchCheck");
444468
setHeaderIfNotSet(headers, CLIENT_BULK_REQUEST_ID_HEADER, generateRandomIdWithNonUniqueFallback());
445469

446-
const responses = (await chunkSequentialCall<TupleKey, any>(async (tuples) =>
470+
const responses = (await chunkSequentialCall<ClientTupleKey, any>(async (tuples) =>
447471
Promise.all(tuples.map(tuple => this.check(tuple, { ...options, retryParams: { maxRetry: DEFAULT_MAX_RETRY_OVERRIDE }, headers })
448472
.then(({ allowed, $response: response }) => {
449473
const result = {
@@ -457,7 +481,7 @@ export class OpenFgaClient extends BaseAPI {
457481
error: err,
458482
_request: tuple,
459483
}))
460-
)), body, maxParallelRequests).then(results => results.flat())) as ClientBatchCheckSingleResponse[];
484+
)), body, { maxPerChunk: maxParallelRequests, waitTimeBetweenChunksInMs }).then(results => results.flat())) as ClientBatchCheckSingleResponse[];
461485

462486
return { responses };
463487
}
@@ -492,9 +516,8 @@ export class OpenFgaClient extends BaseAPI {
492516
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
493517
*/
494518
async listObjects(body: ClientListObjectsRequest, options: ClientRequestOptsWithAuthZModelId = {}): PromiseResult<ListObjectsResponse> {
495-
const authorizationModelId = this.getAuthorizationModelId(options);
496519
return this.api.listObjects({
497-
authorization_model_id: authorizationModelId,
520+
authorization_model_id: this.getAuthorizationModelId(options),
498521
user: body.user,
499522
relation: body.relation,
500523
type: body.type,
@@ -516,9 +539,7 @@ export class OpenFgaClient extends BaseAPI {
516539
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
517540
*/
518541
async readAssertions(options: ClientRequestOptsWithAuthZModelId = {}): PromiseResult<ReadAssertionsResponse> {
519-
const authorizationModelId = this.getAuthorizationModelId(options);
520-
// Note: authorization model id is validated later
521-
return this.api.readAssertions(authorizationModelId!, options);
542+
return this.api.readAssertions(this.getAuthorizationModelId(options)!, options);
522543
}
523544

524545
/**
@@ -532,8 +553,7 @@ export class OpenFgaClient extends BaseAPI {
532553
* @param {number} [options.retryParams.minWaitInMs] - Override the minimum wait before a retry is initiated
533554
*/
534555
async writeAssertions(assertions: ClientWriteAssertionsRequest, options: ClientRequestOptsWithAuthZModelId = {}): PromiseResult<void> {
535-
const authorizationModelId = this.getAuthorizationModelId(options);
536-
return this.api.writeAssertions(authorizationModelId!, {
556+
return this.api.writeAssertions(this.getAuthorizationModelId(options)!, {
537557
assertions: assertions.map(assertion => ({
538558
tuple_key: {
539559
user: assertion.user,

utils/chunk-call.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,22 @@
1212

1313

1414
import chunkArray from "./chunk-array";
15+
import sleep from "./sleep";
1516

1617
export async function chunkSequentialCall<T = any, W = any>(
1718
fnToCall: (chunk: T[]) => Promise<W>,
1819
dataArray: T[],
19-
maxPerChunk: number,
20+
{ maxPerChunk, waitTimeBetweenChunksInMs }: { maxPerChunk: number, waitTimeBetweenChunksInMs: number },
2021
): Promise<Awaited<W>[]> {
2122
const chunkedSet = chunkArray<T>(dataArray, maxPerChunk);
2223
const results = [];
24+
let chunkIndex = 0;
2325
for (const chunk of chunkedSet) {
2426
results.push(await fnToCall(chunk));
27+
chunkIndex++;
28+
if (chunkIndex < chunkedSet.length) {
29+
await sleep(waitTimeBetweenChunksInMs);
30+
}
2531
}
2632
return results;
2733
}

utils/sleep.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* JavaScript and Node.js SDK for OpenFGA
3+
*
4+
* API version: 0.1
5+
* Website: https://openfga.dev
6+
* Documentation: https://openfga.dev/docs
7+
* Support: https://discord.gg/8naAwJfWN6
8+
* License: [Apache-2.0](https://github.com/openfga/js-sdk/blob/main/LICENSE)
9+
*
10+
* NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT.
11+
*/
12+
13+
14+
export default function sleep(ms: number): Promise<unknown> {
15+
return new Promise(resolve => setTimeout(resolve, ms));
16+
}

0 commit comments

Comments
 (0)