From a785145c57d1dcb462cab273afc733a6ea1c7735 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Fri, 23 Aug 2024 16:35:25 +0100 Subject: [PATCH] fix(js-sdk): set the consistency parameter correctly in OpenFGAClient --- config/clients/js/template/client.mustache | 7 +++- .../js/template/tests/client.test.ts.mustache | 39 +++++++++--------- .../template/tests/helpers/nocks.ts.mustache | 40 +++++++++++++++---- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/config/clients/js/template/client.mustache b/config/clients/js/template/client.mustache index 651ae2c0..4d8816d9 100644 --- a/config/clients/js/template/client.mustache +++ b/config/clients/js/template/client.mustache @@ -406,6 +406,7 @@ export class {{appShortName}}Client extends BaseAPI { const readRequest: ReadRequest = { page_size: options.pageSize, continuation_token: options.continuationToken, + consistency: options.consistency }; if (body.user || body.object || body.relation) { readRequest.tuple_key = body; @@ -569,7 +570,8 @@ export class {{appShortName}}Client extends BaseAPI { }, context: body.context, contextual_tuples: { tuple_keys: body.contextualTuples || [] }, - authorization_model_id: this.getAuthorizationModelId(options) + authorization_model_id: this.getAuthorizationModelId(options), + consistency: options.consistency }, options); } @@ -630,6 +632,7 @@ export class {{appShortName}}Client extends BaseAPI { return this.api.expand(this.getStoreId(options)!, { authorization_model_id: this.getAuthorizationModelId(options), tuple_key: body, + consistency: options.consistency }, options); } @@ -652,6 +655,7 @@ export class {{appShortName}}Client extends BaseAPI { type: body.type, context: body.context, contextual_tuples: { tuple_keys: body.contextualTuples || [] }, + consistency: options.consistency }, options); } @@ -710,6 +714,7 @@ export class {{appShortName}}Client extends BaseAPI { user_filters: body.user_filters, context: body.context, contextual_tuples: body.contextualTuples || [], + consistency: options.consistency }, options); } diff --git a/config/clients/js/template/tests/client.test.ts.mustache b/config/clients/js/template/tests/client.test.ts.mustache index a8edb23b..ef21ae6e 100644 --- a/config/clients/js/template/tests/client.test.ts.mustache +++ b/config/clients/js/template/tests/client.test.ts.mustache @@ -10,6 +10,7 @@ import { FgaValidationError, {{appShortName}}Client, ListUsersResponse, + ConsistencyPreference, } from "../index"; import { baseConfig, defaultConfiguration, getNocks } from "./helpers"; @@ -280,10 +281,10 @@ describe("{{appTitleCaseName}} Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.read(baseConfig.storeId!, tuple); + const scope = nocks.read(baseConfig.storeId!, tuple, undefined, ConsistencyPreference.HigherConsistency); expect(scope.isDone()).toBe(false); - const data = await fgaClient.read(tuple); + const data = await fgaClient.read(tuple, { consistency: ConsistencyPreference.HigherConsistency}); expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); @@ -487,10 +488,10 @@ describe("{{appTitleCaseName}} Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.check(baseConfig.storeId!, tuple); + const scope = nocks.check(baseConfig.storeId!, tuple, undefined, undefined, undefined, ConsistencyPreference.HigherConsistency); expect(scope.isDone()).toBe(false); - const data = await fgaClient.check(tuple); + const data = await fgaClient.check(tuple, { consistency: ConsistencyPreference.HigherConsistency }); expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ allowed: expect.any(Boolean) }); @@ -512,12 +513,12 @@ describe("{{appTitleCaseName}} Client", () => { relation: "reader", object: "workspace:3", }]; - const scope0 = nocks.check(defaultConfiguration.storeId!, tuples[0], defaultConfiguration.getBasePath(), { allowed: true }, 200).matchHeader("{{clientMethodHeader}}", "BatchCheck"); - const scope1 = nocks.check(defaultConfiguration.storeId!, tuples[1], defaultConfiguration.getBasePath(), { allowed: false }, 200).matchHeader("{{clientMethodHeader}}", "BatchCheck"); + const scope0 = nocks.check(defaultConfiguration.storeId!, tuples[0], defaultConfiguration.getBasePath(), { allowed: true }, 200, ConsistencyPreference.HigherConsistency).matchHeader("X-OpenFGA-Client-Method", "BatchCheck"); + const scope1 = nocks.check(defaultConfiguration.storeId!, tuples[1], defaultConfiguration.getBasePath(), { allowed: false }, 200, ConsistencyPreference.HigherConsistency).matchHeader("X-OpenFGA-Client-Method", "BatchCheck"); const scope2 = nocks.check(defaultConfiguration.storeId!, tuples[2], defaultConfiguration.getBasePath(), { "code": "validation_error", "message": "relation 'workspace#reader' not found" - }, 400).matchHeader("{{clientMethodHeader}}", "BatchCheck"); + }, 400, ConsistencyPreference.HigherConsistency).matchHeader("X-OpenFGA-Client-Method", "BatchCheck"); const scope3 = nock(defaultConfiguration.getBasePath()) .get(`/stores/${defaultConfiguration.storeId!}/authorization-models`) .query({ page_size: 1 }) @@ -528,7 +529,7 @@ describe("{{appTitleCaseName}} Client", () => { expect(scope0.isDone()).toBe(false); expect(scope1.isDone()).toBe(false); expect(scope2.isDone()).toBe(false); - const response = await fgaClient.batchCheck([tuples[0], tuples[1], tuples[2]]); + const response = await fgaClient.batchCheck([tuples[0], tuples[1], tuples[2]], { consistency: ConsistencyPreference.HigherConsistency }); expect(scope0.isDone()).toBe(true); expect(scope1.isDone()).toBe(true); @@ -551,10 +552,10 @@ describe("{{appTitleCaseName}} Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.expand(baseConfig.storeId!, tuple); + const scope = nocks.expand(baseConfig.storeId!, tuple, undefined, ConsistencyPreference.HigherConsistency); expect(scope.isDone()).toBe(false); - const data = await fgaClient.expand(tuple, { authorizationModelId: "01GXSA8YR785C4FYS3C0RTG7B1" }); + const data = await fgaClient.expand(tuple, { authorizationModelId: "01GXSA8YR785C4FYS3C0RTG7B1", consistency: ConsistencyPreference.HigherConsistency }); expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); @@ -564,7 +565,7 @@ describe("{{appTitleCaseName}} Client", () => { describe("ListObjects", () => { it("should call the api and return the response", async () => { const mockedResponse = { objects: ["document:roadmap"] }; - const scope = nocks.listObjects(baseConfig.storeId!, mockedResponse); + const scope = nocks.listObjects(baseConfig.storeId!, mockedResponse, undefined, ConsistencyPreference.HigherConsistency); expect(scope.isDone()).toBe(false); const response = await fgaClient.listObjects({ @@ -583,6 +584,7 @@ describe("{{appTitleCaseName}} Client", () => { }] }, { authorizationModelId: "01GAHCE4YVKPQEKZQHT2R89MQV", + consistency: ConsistencyPreference.HigherConsistency, }); expect(scope.isDone()).toBe(true); @@ -614,11 +616,11 @@ describe("{{appTitleCaseName}} Client", () => { relation: "can_read", object: "workspace:1", }]; - const scope0 = nocks.check(defaultConfiguration.storeId!, tuples[0], defaultConfiguration.getBasePath(), { allowed: true }).matchHeader("{{clientMethodHeader}}", "ListRelations"); - const scope1 = nocks.check(defaultConfiguration.storeId!, tuples[1], defaultConfiguration.getBasePath(), { allowed: false }).matchHeader("{{clientMethodHeader}}", "ListRelations"); - const scope2 = nocks.check(defaultConfiguration.storeId!, tuples[2], defaultConfiguration.getBasePath(), { allowed: true }).matchHeader("{{clientMethodHeader}}", "ListRelations"); - const scope3 = nocks.check(defaultConfiguration.storeId!, tuples[3], defaultConfiguration.getBasePath(), { allowed: false }).matchHeader("{{clientMethodHeader}}", "ListRelations"); - const scope4 = nocks.check(defaultConfiguration.storeId!, tuples[4], defaultConfiguration.getBasePath(), { allowed: false }).matchHeader("{{clientMethodHeader}}", "ListRelations"); + const scope0 = nocks.check(defaultConfiguration.storeId!, tuples[0], defaultConfiguration.getBasePath(), { allowed: true }, undefined, ConsistencyPreference.HigherConsistency).matchHeader("X-OpenFGA-Client-Method", "ListRelations"); + const scope1 = nocks.check(defaultConfiguration.storeId!, tuples[1], defaultConfiguration.getBasePath(), { allowed: false }, undefined, ConsistencyPreference.HigherConsistency).matchHeader("X-OpenFGA-Client-Method", "ListRelations"); + const scope2 = nocks.check(defaultConfiguration.storeId!, tuples[2], defaultConfiguration.getBasePath(), { allowed: true }, undefined, ConsistencyPreference.HigherConsistency).matchHeader("X-OpenFGA-Client-Method", "ListRelations"); + const scope3 = nocks.check(defaultConfiguration.storeId!, tuples[3], defaultConfiguration.getBasePath(), { allowed: false }, undefined, ConsistencyPreference.HigherConsistency).matchHeader("X-OpenFGA-Client-Method", "ListRelations"); + const scope4 = nocks.check(defaultConfiguration.storeId!, tuples[4], defaultConfiguration.getBasePath(), { allowed: false }, undefined, ConsistencyPreference.HigherConsistency).matchHeader("X-OpenFGA-Client-Method", "ListRelations"); const scope5 = nock(defaultConfiguration.getBasePath()) .get(`/stores/${defaultConfiguration.storeId!}/authorization-models`) .query({ page_size: 1 }) @@ -636,7 +638,7 @@ describe("{{appTitleCaseName}} Client", () => { user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", object: "workspace:1", relations: ["admin", "guest", "reader", "viewer"], - }); + }, { consistency: ConsistencyPreference.HigherConsistency }); expect(scope0.isDone()).toBe(true); expect(scope1.isDone()).toBe(true); @@ -771,7 +773,7 @@ describe("{{appTitleCaseName}} Client", () => { } }] }; - const scope = nocks.listUsers(baseConfig.storeId!, mockedResponse); + const scope = nocks.listUsers(baseConfig.storeId!, mockedResponse, undefined, ConsistencyPreference.HigherConsistency); expect(scope.isDone()).toBe(false); const response = await fgaClient.listUsers({ @@ -799,6 +801,7 @@ describe("{{appTitleCaseName}} Client", () => { }] }, { authorizationModelId: "01GAHCE4YVKPQEKZQHT2R89MQV", + consistency: ConsistencyPreference.HigherConsistency }); expect(scope.isDone()).toBe(true); diff --git a/config/clients/js/template/tests/helpers/nocks.ts.mustache b/config/clients/js/template/tests/helpers/nocks.ts.mustache index 27ff9aee..af85cf85 100644 --- a/config/clients/js/template/tests/helpers/nocks.ts.mustache +++ b/config/clients/js/template/tests/helpers/nocks.ts.mustache @@ -6,16 +6,20 @@ import { AuthorizationModel, CheckRequest, CheckResponse, + ConsistencyPreference, CreateStoreResponse, + ExpandRequest, ExpandResponse, GetStoreResponse, ListObjectsResponse, ListStoresResponse, + ListUsersRequest, ListUsersResponse, ReadAssertionsResponse, ReadAuthorizationModelResponse, ReadAuthorizationModelsResponse, ReadChangesResponse, + ReadRequest, ReadResponse, TupleKey, TupleOperation, @@ -153,9 +157,12 @@ export const getNocks = ((nock: typeof Nock) => ({ storeId: string, tuple: TupleKey, basePath = defaultConfiguration.getBasePath(), + consistency: ConsistencyPreference|undefined = undefined, ) => { return nock(basePath) - .post(`/stores/${storeId}/read`) + .post(`/stores/${storeId}/read`, (body: ReadRequest) => + body.consistency === consistency + ) .reply(200, { tuples: [], continuation_token: "" } as ReadResponse); }, write: ( @@ -183,12 +190,14 @@ export const getNocks = ((nock: typeof Nock) => ({ basePath = defaultConfiguration.getBasePath(), response: { allowed: boolean } | { code: string, message: string } = { allowed: true }, statusCode = 200, + consistency: ConsistencyPreference|undefined = undefined, ) => { return nock(basePath) .post(`/stores/${storeId}/check`, (body: CheckRequest) => body.tuple_key.user === tuple.user && body.tuple_key.relation === tuple.relation && - body.tuple_key.object === tuple.object + body.tuple_key.object === tuple.object && + body.consistency === consistency ) .reply(statusCode, response as CheckResponse); }, @@ -196,19 +205,36 @@ export const getNocks = ((nock: typeof Nock) => ({ storeId: string, tuple: TupleKey, basePath = defaultConfiguration.getBasePath(), + consistency: ConsistencyPreference|undefined = undefined, ) => { return nock(basePath) - .post(`/stores/${storeId}/expand`) + .post(`/stores/${storeId}/expand`, (body: ExpandRequest) => + body.consistency === consistency + ) .reply(200, { tree: {} } as ExpandResponse); }, - listObjects: (storeId: string, responseBody: ListObjectsResponse, basePath = defaultConfiguration.getBasePath()) => { + listObjects: ( + storeId: string, + responseBody: ListObjectsResponse, + basePath = defaultConfiguration.getBasePath(), + consistency: ConsistencyPreference|undefined = undefined, + ) => { return nock(basePath) - .post(`/stores/${storeId}/list-objects`) + .post(`/stores/${storeId}/list-objects`, (body: ListUsersRequest) => + body.consistency === consistency + ) .reply(200, responseBody); }, - listUsers: (storeId: string, responseBody: ListUsersResponse, basePath = defaultConfiguration.getBasePath()) => { + listUsers: ( + storeId: string, + responseBody: ListUsersResponse, + basePath = defaultConfiguration.getBasePath(), + consistency: ConsistencyPreference|undefined = undefined + ) => { return nock(basePath) - .post(`/stores/${storeId}/list-users`) + .post(`/stores/${storeId}/list-users`, (body: ListUsersRequest) => + body.consistency === consistency + ) .reply(200, responseBody); }, readAssertions: (storeId: string, modelId: string, assertions: ReadAssertionsResponse["assertions"] = [], basePath = defaultConfiguration.getBasePath()) => {