From e4d3aec54538e2e50ec6fa7c72467c89d679bbf3 Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Thu, 2 Nov 2023 11:07:16 -0400 Subject: [PATCH] feat!: support for conditions --- CHANGELOG.md | 6 + api.ts | 19 ++- apiModel.ts | 362 +++++++++++++++++++++++++++++++++-------- client.ts | 52 +++--- tests/client.test.ts | 24 ++- tests/helpers/nocks.ts | 27 ++- tests/index.test.ts | 21 ++- 7 files changed, 405 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a8e0f..959e5fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.3.0 + +### [0.3.0](https://github.com/openfga/js-sdk/compare/v0.2.10...v0.3.0) (2023-11-01) + +- feat!: support for conditions + ## v0.2.10 ### [0.2.10](https://github.com/openfga/js-sdk/compare/v0.2.9...v0.2.10) (2023-11-01) diff --git a/api.ts b/api.ts index 5ed9952..69de424 100644 --- a/api.ts +++ b/api.ts @@ -32,14 +32,18 @@ import { Assertion, AuthorizationModel, CheckRequest, + CheckRequestTupleKey, CheckResponse, Computed, + Condition, + ConditionParamTypeRef, ContextualTupleKeys, CreateStoreRequest, CreateStoreResponse, Difference, ErrorCode, ExpandRequest, + ExpandRequestTupleKey, ExpandResponse, GetStoreResponse, InternalErrorCode, @@ -52,6 +56,7 @@ import { Node, Nodes, NotFoundErrorCode, + NullValue, ObjectRelation, PathUnknownErrorMessageResponse, ReadAssertionsResponse, @@ -59,18 +64,20 @@ import { ReadAuthorizationModelsResponse, ReadChangesResponse, ReadRequest, + ReadRequestTupleKey, ReadResponse, RelationMetadata, RelationReference, + RelationshipCondition, Status, Store, Tuple, TupleChange, TupleKey, - TupleKeys, TupleOperation, TupleToUserset, TypeDefinition, + TypeName, Users, Userset, UsersetTree, @@ -82,6 +89,8 @@ import { WriteAuthorizationModelRequest, WriteAuthorizationModelResponse, WriteRequest, + WriteRequestTupleKey, + WriteRequestTupleKeys, } from "./apiModel"; @@ -268,7 +277,7 @@ export const OpenFgaApiAxiosParamCreator = function (configuration: Configuratio }; }, /** - * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To achieve this, both the store tuples and the authorization model are used. An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. + * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To achieve this, both the store tuples and the authorization model are used. An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. * @summary List all objects of the given type that the user has a relation with * @param {ListObjectsRequest} body * @param {*} [options] Override http request option. @@ -725,7 +734,7 @@ export const OpenFgaApiFp = function(configuration: Configuration, credentials: return createRequestFunction(localVarAxiosArgs, globalAxios, configuration, credentials); }, /** - * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To achieve this, both the store tuples and the authorization model are used. An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. + * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To achieve this, both the store tuples and the authorization model are used. An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. * @summary List all objects of the given type that the user has a relation with * @param {ListObjectsRequest} body * @param {*} [options] Override http request option. @@ -898,7 +907,7 @@ export const OpenFgaApiFactory = function (configuration: Configuration, credent return localVarFp.getStore(options).then((request) => request(axios)); }, /** - * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To achieve this, both the store tuples and the authorization model are used. An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. + * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To achieve this, both the store tuples and the authorization model are used. An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. * @summary List all objects of the given type that the user has a relation with * @param {ListObjectsRequest} body * @param {*} [options] Override http request option. @@ -1071,7 +1080,7 @@ export class OpenFgaApi extends BaseAPI { } /** - * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To achieve this, both the store tuples and the authorization model are used. An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. + * The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To achieve this, both the store tuples and the authorization model are used. An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. * @summary List all objects of the given type that the user has a relation with * @param {ListObjectsRequest} body * @param {*} [options] Override http request option. diff --git a/apiModel.ts b/apiModel.ts index c1e4970..a2bc50d 100644 --- a/apiModel.ts +++ b/apiModel.ts @@ -36,10 +36,10 @@ export interface Any { export interface Assertion { /** * - * @type {TupleKey} + * @type {CheckRequestTupleKey} * @memberof Assertion */ - tuple_key: TupleKey; + tuple_key: CheckRequestTupleKey; /** * * @type {boolean} @@ -70,7 +70,13 @@ export interface AuthorizationModel { * @type {Array} * @memberof AuthorizationModel */ - type_definitions?: Array; + type_definitions: Array; + /** + * + * @type {{ [key: string]: Condition; }} + * @memberof AuthorizationModel + */ + conditions?: { [key: string]: Condition; }; } /** * @@ -80,10 +86,10 @@ export interface AuthorizationModel { export interface CheckRequest { /** * - * @type {TupleKey} + * @type {CheckRequestTupleKey} * @memberof CheckRequest */ - tuple_key: TupleKey; + tuple_key: CheckRequestTupleKey; /** * * @type {ContextualTupleKeys} @@ -102,6 +108,37 @@ export interface CheckRequest { * @memberof CheckRequest */ trace?: boolean; + /** + * Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. + * @type {object} + * @memberof CheckRequest + */ + context?: object; +} +/** + * + * @export + * @interface CheckRequestTupleKey + */ +export interface CheckRequestTupleKey { + /** + * + * @type {string} + * @memberof CheckRequestTupleKey + */ + user: string; + /** + * + * @type {string} + * @memberof CheckRequestTupleKey + */ + relation: string; + /** + * + * @type {string} + * @memberof CheckRequestTupleKey + */ + object: string; } /** * @@ -133,8 +170,54 @@ export interface Computed { * @type {string} * @memberof Computed */ - userset?: string; + userset: string; +} +/** + * + * @export + * @interface Condition + */ +export interface Condition { + /** + * + * @type {string} + * @memberof Condition + */ + name: string; + /** + * A Google CEL expression, expressed as a string. + * @type {string} + * @memberof Condition + */ + expression: string; + /** + * A map of parameter names to the parameter\'s defined type reference. + * @type {{ [key: string]: ConditionParamTypeRef; }} + * @memberof Condition + */ + parameters?: { [key: string]: ConditionParamTypeRef; }; } +/** + * + * @export + * @interface ConditionParamTypeRef + */ +export interface ConditionParamTypeRef { + /** + * + * @type {TypeName} + * @memberof ConditionParamTypeRef + */ + type_name: TypeName; + /** + * + * @type {Array} + * @memberof ConditionParamTypeRef + */ + generic_types?: Array; +} + + /** * * @export @@ -172,25 +255,25 @@ export interface CreateStoreResponse { * @type {string} * @memberof CreateStoreResponse */ - id?: string; + id: string; /** * * @type {string} * @memberof CreateStoreResponse */ - name?: string; + name: string; /** * * @type {string} * @memberof CreateStoreResponse */ - created_at?: string; + created_at: string; /** * * @type {string} * @memberof CreateStoreResponse */ - updated_at?: string; + updated_at: string; } /** * @@ -276,10 +359,10 @@ export enum ErrorCode { export interface ExpandRequest { /** * - * @type {TupleKey} + * @type {ExpandRequestTupleKey} * @memberof ExpandRequest */ - tuple_key: TupleKey; + tuple_key: ExpandRequestTupleKey; /** * * @type {string} @@ -287,6 +370,25 @@ export interface ExpandRequest { */ authorization_model_id?: string; } +/** + * + * @export + * @interface ExpandRequestTupleKey + */ +export interface ExpandRequestTupleKey { + /** + * + * @type {string} + * @memberof ExpandRequestTupleKey + */ + relation: string; + /** + * + * @type {string} + * @memberof ExpandRequestTupleKey + */ + object: string; +} /** * * @export @@ -311,25 +413,25 @@ export interface GetStoreResponse { * @type {string} * @memberof GetStoreResponse */ - id?: string; + id: string; /** * * @type {string} * @memberof GetStoreResponse */ - name?: string; + name: string; /** * * @type {string} * @memberof GetStoreResponse */ - created_at?: string; + created_at: string; /** * * @type {string} * @memberof GetStoreResponse */ - updated_at?: string; + updated_at: string; } /** * @@ -433,6 +535,12 @@ export interface ListObjectsRequest { * @memberof ListObjectsRequest */ contextual_tuples?: ContextualTupleKeys; + /** + * Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. + * @type {object} + * @memberof ListObjectsRequest + */ + context?: object; } /** * @@ -445,7 +553,7 @@ export interface ListObjectsResponse { * @type {Array} * @memberof ListObjectsResponse */ - objects?: Array; + objects: Array; } /** * @@ -458,13 +566,13 @@ export interface ListStoresResponse { * @type {Array} * @memberof ListStoresResponse */ - stores?: Array; + stores: Array; /** * The continuation token will be empty if there are no more stores. * @type {string} * @memberof ListStoresResponse */ - continuation_token?: string; + continuation_token: string; } /** * @@ -490,7 +598,7 @@ export interface Node { * @type {string} * @memberof Node */ - name?: string; + name: string; /** * * @type {Leaf} @@ -527,7 +635,7 @@ export interface Nodes { * @type {Array} * @memberof Nodes */ - nodes?: Array; + nodes: Array; } /** * @@ -542,6 +650,16 @@ export enum NotFoundErrorCode { Unimplemented = 'unimplemented' } +/** + * `NullValue` is a singleton enumeration to represent the null value for the `Value` type union. The JSON representation for `NullValue` is JSON `null`. - NULL_VALUE: Null value. + * @export + * @enum {string} + */ + +export enum NullValue { + NullValue = 'NULL_VALUE' +} + /** * * @export @@ -593,7 +711,7 @@ export interface ReadAssertionsResponse { * @type {string} * @memberof ReadAssertionsResponse */ - authorization_model_id?: string; + authorization_model_id: string; /** * * @type {Array} @@ -625,7 +743,7 @@ export interface ReadAuthorizationModelsResponse { * @type {Array} * @memberof ReadAuthorizationModelsResponse */ - authorization_models?: Array; + authorization_models: Array; /** * The continuation token will be empty if there are no more models. * @type {string} @@ -644,7 +762,7 @@ export interface ReadChangesResponse { * @type {Array} * @memberof ReadChangesResponse */ - changes?: Array; + changes: Array; /** * The continuation token will be identical if there are no new changes. * @type {string} @@ -660,10 +778,10 @@ export interface ReadChangesResponse { export interface ReadRequest { /** * - * @type {TupleKey} + * @type {ReadRequestTupleKey} * @memberof ReadRequest */ - tuple_key?: TupleKey; + tuple_key?: ReadRequestTupleKey; /** * * @type {number} @@ -677,6 +795,31 @@ export interface ReadRequest { */ continuation_token?: string; } +/** + * + * @export + * @interface ReadRequestTupleKey + */ +export interface ReadRequestTupleKey { + /** + * + * @type {string} + * @memberof ReadRequestTupleKey + */ + user?: string; + /** + * + * @type {string} + * @memberof ReadRequestTupleKey + */ + relation?: string; + /** + * + * @type {string} + * @memberof ReadRequestTupleKey + */ + object?: string; +} /** * * @export @@ -688,13 +831,13 @@ export interface ReadResponse { * @type {Array} * @memberof ReadResponse */ - tuples?: Array; + tuples: Array; /** * The continuation token will be empty if there are no more tuples. * @type {string} * @memberof ReadResponse */ - continuation_token?: string; + continuation_token: string; } /** * @@ -733,6 +876,31 @@ export interface RelationReference { * @memberof RelationReference */ wildcard?: object; + /** + * The name of a condition that is enforced over the allowed relation. + * @type {string} + * @memberof RelationReference + */ + condition?: string; +} +/** + * + * @export + * @interface RelationshipCondition + */ +export interface RelationshipCondition { + /** + * A reference (by name) of the relationship condition defined in the authorization model. + * @type {string} + * @memberof RelationshipCondition + */ + name: string; + /** + * Additional context/data to persist along with the condition. The keys must match the parameters defined by the condition, and the value types must match the parameter type definitions. + * @type {object} + * @memberof RelationshipCondition + */ + context: object; } /** * @@ -770,31 +938,31 @@ export interface Store { * @type {string} * @memberof Store */ - id?: string; + id: string; /** * * @type {string} * @memberof Store */ - name?: string; + name: string; /** * * @type {string} * @memberof Store */ - created_at?: string; + created_at: string; /** * * @type {string} * @memberof Store */ - updated_at?: string; + updated_at: string; /** * * @type {string} * @memberof Store */ - deleted_at?: string; + deleted_at: string; } /** * @@ -807,13 +975,13 @@ export interface Tuple { * @type {TupleKey} * @memberof Tuple */ - key?: TupleKey; + key: TupleKey; /** * * @type {string} * @memberof Tuple */ - timestamp?: string; + timestamp: string; } /** * @@ -826,19 +994,19 @@ export interface TupleChange { * @type {TupleKey} * @memberof TupleChange */ - tuple_key?: TupleKey; + tuple_key: TupleKey; /** * * @type {TupleOperation} * @memberof TupleChange */ - operation?: TupleOperation; + operation: TupleOperation; /** * * @type {string} * @memberof TupleChange */ - timestamp?: string; + timestamp: string; } @@ -853,32 +1021,25 @@ export interface TupleKey { * @type {string} * @memberof TupleKey */ - object?: string; + user: string; /** * * @type {string} * @memberof TupleKey */ - relation?: string; + relation: string; /** * * @type {string} * @memberof TupleKey */ - user?: string; -} -/** - * - * @export - * @interface TupleKeys - */ -export interface TupleKeys { + object: string; /** * - * @type {Array} - * @memberof TupleKeys + * @type {RelationshipCondition} + * @memberof TupleKey */ - tuple_keys: Array; + condition?: RelationshipCondition; } /** * @@ -902,13 +1063,13 @@ export interface TupleToUserset { * @type {ObjectRelation} * @memberof TupleToUserset */ - tupleset?: ObjectRelation; + tupleset: ObjectRelation; /** * * @type {ObjectRelation} * @memberof TupleToUserset */ - computedUserset?: ObjectRelation; + computedUserset: ObjectRelation; } /** * @@ -935,6 +1096,27 @@ export interface TypeDefinition { */ metadata?: Metadata; } +/** + * + * @export + * @enum {string} + */ + +export enum TypeName { + Unspecified = 'TYPE_NAME_UNSPECIFIED', + Any = 'TYPE_NAME_ANY', + Bool = 'TYPE_NAME_BOOL', + String = 'TYPE_NAME_STRING', + Int = 'TYPE_NAME_INT', + Uint = 'TYPE_NAME_UINT', + Double = 'TYPE_NAME_DOUBLE', + Duration = 'TYPE_NAME_DURATION', + Timestamp = 'TYPE_NAME_TIMESTAMP', + Map = 'TYPE_NAME_MAP', + List = 'TYPE_NAME_LIST', + Ipaddress = 'TYPE_NAME_IPADDRESS' +} + /** * * @export @@ -946,7 +1128,7 @@ export interface Users { * @type {Array} * @memberof Users */ - users?: Array; + users: Array; } /** * @@ -1015,13 +1197,13 @@ export interface UsersetTreeDifference { * @type {Node} * @memberof UsersetTreeDifference */ - base?: Node; + base: Node; /** * * @type {Node} * @memberof UsersetTreeDifference */ - subtract?: Node; + subtract: Node; } /** * @@ -1034,13 +1216,13 @@ export interface UsersetTreeTupleToUserset { * @type {string} * @memberof UsersetTreeTupleToUserset */ - tupleset?: string; + tupleset: string; /** * * @type {Array} * @memberof UsersetTreeTupleToUserset */ - computed?: Array; + computed: Array; } /** * @@ -1053,7 +1235,7 @@ export interface Usersets { * @type {Array} * @memberof Usersets */ - child?: Array; + child: Array; } /** * @@ -1106,7 +1288,13 @@ export interface WriteAuthorizationModelRequest { * @type {string} * @memberof WriteAuthorizationModelRequest */ - schema_version?: string; + schema_version: string; + /** + * + * @type {{ [key: string]: Condition; }} + * @memberof WriteAuthorizationModelRequest + */ + conditions?: { [key: string]: Condition; }; } /** * @@ -1119,7 +1307,7 @@ export interface WriteAuthorizationModelResponse { * @type {string} * @memberof WriteAuthorizationModelResponse */ - authorization_model_id?: string; + authorization_model_id: string; } /** * @@ -1129,16 +1317,16 @@ export interface WriteAuthorizationModelResponse { export interface WriteRequest { /** * - * @type {TupleKeys} + * @type {WriteRequestTupleKeys} * @memberof WriteRequest */ - writes?: TupleKeys; + writes?: WriteRequestTupleKeys; /** * - * @type {TupleKeys} + * @type {WriteRequestTupleKeys} * @memberof WriteRequest */ - deletes?: TupleKeys; + deletes?: WriteRequestTupleKeys; /** * * @type {string} @@ -1146,4 +1334,48 @@ export interface WriteRequest { */ authorization_model_id?: string; } +/** + * + * @export + * @interface WriteRequestTupleKey + */ +export interface WriteRequestTupleKey { + /** + * + * @type {string} + * @memberof WriteRequestTupleKey + */ + user: string; + /** + * + * @type {string} + * @memberof WriteRequestTupleKey + */ + relation: string; + /** + * + * @type {string} + * @memberof WriteRequestTupleKey + */ + object: string; + /** + * + * @type {RelationshipCondition} + * @memberof WriteRequestTupleKey + */ + condition?: RelationshipCondition; +} +/** + * + * @export + * @interface WriteRequestTupleKeys + */ +export interface WriteRequestTupleKeys { + /** + * + * @type {Array} + * @memberof WriteRequestTupleKeys + */ + tuple_keys: Array; +} diff --git a/client.ts b/client.ts index a30c6cb..4d3198c 100644 --- a/client.ts +++ b/client.ts @@ -17,9 +17,12 @@ import asyncPool = require("tiny-async-pool"); import { OpenFgaApi } from "./api"; import { Assertion, + CheckRequest, + CheckRequestTupleKey, CheckResponse, CreateStoreRequest, CreateStoreResponse, + ExpandRequestTupleKey, ExpandResponse, GetStoreResponse, ListObjectsRequest, @@ -30,11 +33,13 @@ import { ReadAuthorizationModelsResponse, ReadChangesResponse, ReadRequest, + ReadRequestTupleKey, ReadResponse, - TupleKey as ApiTupleKey, + TupleKey, WriteAuthorizationModelRequest, WriteAuthorizationModelResponse, WriteRequest, + WriteRequestTupleKey, } from "./apiModel"; import { BaseAPI } from "./base"; import { CallResult, PromiseResult } from "./common"; @@ -44,7 +49,6 @@ import { chunkArray, generateRandomIdWithNonUniqueFallback, setHeaderIfNotSet, - setNotEnumerableProperty, } from "./utils"; import { isWellFormedUlidString } from "./validation"; @@ -52,8 +56,6 @@ export type ClientConfiguration = (UserConfigurationParams | Configuration) & { authorizationModelId?: string; } -export type ClientTupleKey = Required; - const DEFAULT_MAX_METHOD_PARALLEL_REQS = 10; const CLIENT_METHOD_HEADER = "X-OpenFGA-Client-Method"; const CLIENT_BULK_REQUEST_ID_HEADER = "X-OpenFGA-Client-Bulk-Request-Id"; @@ -67,11 +69,13 @@ export interface AuthorizationModelIdOpts { authorizationModelId?: string; } -export type ClientRequestOptsWithAuthZModelId = ClientRequestOpts & AuthorizationModelIdOpts; +export type ClientRequestOptsWithAuthZModelId = ClientRequestOpts & AuthorizationModelIdOpts; export type PaginationOptions = { pageSize?: number, continuationToken?: string; }; -export type ClientCheckRequest = ClientTupleKey & { contextualTuples?: ClientTupleKey[] }; +export type ClientCheckRequest = CheckRequestTupleKey & + Pick & + { contextualTuples?: Array }; export type ClientBatchCheckRequest = ClientCheckRequest[]; @@ -102,8 +106,8 @@ export interface BatchCheckRequestOpts { } export interface ClientWriteRequest { - writes?: ClientTupleKey[]; - deletes?: ClientTupleKey[]; + writes?: WriteRequestTupleKey[]; + deletes?: WriteRequestTupleKey[]; } export enum ClientWriteStatus { @@ -112,7 +116,7 @@ export enum ClientWriteStatus { } export interface ClientWriteSingleResponse { - tuple_key: ClientTupleKey; + tuple_key: WriteRequestTupleKey; status: ClientWriteStatus; err?: Error; } @@ -130,11 +134,15 @@ export interface ClientReadChangesRequest { type: string; } -export type ClientExpandRequest = Pick; -export type ClientReadRequest = ApiTupleKey; -export type ClientListObjectsRequest = Omit & { contextualTuples?: ClientTupleKey[] }; -export type ClientListRelationsRequest = Pick & { relations?: string[] }; -export type ClientWriteAssertionsRequest = (ClientTupleKey & Pick)[]; +export type ClientExpandRequest = ExpandRequestTupleKey; +export type ClientReadRequest = ReadRequestTupleKey; +export type ClientListObjectsRequest = Omit & { + contextualTuples?: Array +}; +export type ClientListRelationsRequest = Omit & { + relations?: string[], +}; +export type ClientWriteAssertionsRequest = (CheckRequestTupleKey & Pick)[]; export class OpenFgaClient extends BaseAPI { public api: OpenFgaApi; @@ -433,7 +441,7 @@ export class OpenFgaClient extends BaseAPI { /** * WriteTuples - Utility method to write tuples, wraps Write - * @param {ClientTupleKey[]} tuples + * @param {WriteRequestTupleKey[]} tuples * @param {ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts} [options] * @param {string} [options.authorizationModelId] - Overrides the authorization model id in the configuration * @param {object} [options.transaction] @@ -445,7 +453,7 @@ export class OpenFgaClient 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: ClientTupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise { + async writeTuples(tuples: WriteRequestTupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise { const { headers = {} } = options; setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "WriteTuples"); return this.write({ writes: tuples }, { ...options, headers }); @@ -453,7 +461,7 @@ export class OpenFgaClient extends BaseAPI { /** * DeleteTuples - Utility method to delete tuples, wraps Write - * @param {ClientTupleKey[]} tuples + * @param {WriteRequestTupleKey[]} tuples * @param {ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts} [options] * @param {string} [options.authorizationModelId] - Overrides the authorization model id in the configuration * @param {object} [options.transaction] @@ -465,7 +473,7 @@ export class OpenFgaClient 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: ClientTupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise { + async deleteTuples(tuples: WriteRequestTupleKey[], options: ClientRequestOptsWithAuthZModelId & ClientWriteRequestOpts = {}): Promise { const { headers = {} } = options; setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "DeleteTuples"); return this.write({ deletes: tuples }, { ...options, headers }); @@ -586,11 +594,12 @@ export class OpenFgaClient extends BaseAPI { * @param {string} listRelationsRequest.user The user object, must be of the form: `:` * @param {string} listRelationsRequest.object The object, must be of the form: `:` * @param {string[]} listRelationsRequest.relations The list of relations to check + * @param {TupleKey[]} listRelationsRequest.contextualTuples The contextual tuples to send + * @param {object} listRelationsRequest.context The contextual tuples to send * @param options */ async listRelations(listRelationsRequest: ClientListRelationsRequest, options: ClientRequestOptsWithAuthZModelId & BatchCheckRequestOpts = {}): Promise { - const { user, object } = listRelationsRequest; - const { relations } = listRelationsRequest; + const { user, object, relations, contextualTuples, context } = listRelationsRequest; const { headers = {}, maxParallelRequests = DEFAULT_MAX_METHOD_PARALLEL_REQS } = options; setHeaderIfNotSet(headers, CLIENT_METHOD_HEADER, "ListRelations"); setHeaderIfNotSet(headers, CLIENT_BULK_REQUEST_ID_HEADER, generateRandomIdWithNonUniqueFallback()); @@ -603,7 +612,8 @@ export class OpenFgaClient extends BaseAPI { user, relation, object, - contextualTuples: listRelationsRequest.contextualTuples, + contextualTuples, + context, })), { ...options, headers, maxParallelRequests }); const firstErrorResponse = batchCheckResults.responses.find(response => (response as any).error); diff --git a/tests/client.test.ts b/tests/client.test.ts index c526d25..352b483 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -44,7 +44,15 @@ describe("OpenFGA Client", () => { describe("ListStores", () => { it("should properly call the OpenFga API", async () => { const store = { id: "some-id", name: "some-name" }; - const scope = nocks.listStores(defaultConfiguration.getBasePath(), { stores: [store] }); + const scope = nocks.listStores(defaultConfiguration.getBasePath(), { + continuation_token: "", + stores: [{ + ...store, + created_at: "2023-11-02T15:27:47.951Z", + updated_at: "2023-11-02T15:27:47.951Z", + deleted_at: "2023-11-02T15:27:47.951Z", + }], + }); expect(scope.isDone()).toBe(false); const response = await fgaClient.listStores(); @@ -56,9 +64,13 @@ describe("OpenFGA Client", () => { }); describe("CreateStore", () => { - it("should properly call the OpenFga API", async () => { + it("should create a store", async () => { const store = { id: "some-id", name: "some-name" }; - const scope = nocks.createStore(defaultConfiguration.getBasePath(), store); + const scope = nocks.createStore(defaultConfiguration.getBasePath(), { + ...store, + created_at: "2023-11-02T15:27:47.951Z", + updated_at: "2023-11-02T15:27:47.951Z", + }); expect(scope.isDone()).toBe(false); const response = await fgaClient.createStore(store); @@ -71,7 +83,11 @@ describe("OpenFGA Client", () => { describe("GetStore", () => { it("should properly call the OpenFga API", async () => { const store = { id: defaultConfiguration.storeId, name: "some-name" }; - const scope = nocks.getStore(store.id, defaultConfiguration.getBasePath(), store); + const scope = nocks.getStore(store.id, defaultConfiguration.getBasePath(), { + ...store, + created_at: "2023-11-02T15:27:47.951Z", + updated_at: "2023-11-02T15:27:47.951Z", + }); expect(scope.isDone()).toBe(false); const response = await fgaClient.getStore(); diff --git a/tests/helpers/nocks.ts b/tests/helpers/nocks.ts index 889aadc..b1e8a27 100644 --- a/tests/helpers/nocks.ts +++ b/tests/helpers/nocks.ts @@ -47,7 +47,16 @@ export const getNocks = ((nock: typeof Nock) => ({ }, listStores: ( basePath = defaultConfiguration.getBasePath(), - response: ListStoresResponse = { stores: [{ id: "some-id", name: "some-name" }] }, + response: ListStoresResponse = { + continuation_token: "...", + stores: [{ + id: "some-id", + name: "some-name", + created_at: "2023-11-02T15:27:47.951Z", + updated_at: "2023-11-02T15:27:47.951Z", + deleted_at: "2023-11-02T15:27:47.951Z", + }] + }, responseCode = 200, ) => { return nock(basePath) @@ -56,7 +65,12 @@ export const getNocks = ((nock: typeof Nock) => ({ }, createStore: ( basePath = defaultConfiguration.getBasePath(), - response: CreateStoreResponse = { id: "some-id", name: "some-name" }, + response: CreateStoreResponse = { + id: "some-id", + name: "some-name", + created_at: "2023-11-02T15:27:47.951Z", + updated_at: "2023-11-02T15:27:47.951Z", + }, responseCode = 200, ) => { return nock(basePath) @@ -66,7 +80,12 @@ export const getNocks = ((nock: typeof Nock) => ({ getStore: ( storeId: string, basePath = defaultConfiguration.getBasePath(), - response: GetStoreResponse = { id: "some-id", name: "some-name" }, + response: GetStoreResponse = { + id: "some-id", + name: "some-name", + created_at: "2023-11-02T15:27:47.951Z", + updated_at: "2023-11-02T15:27:47.951Z", + }, responseCode = 200, ) => { return nock(basePath) @@ -145,7 +164,7 @@ export const getNocks = ((nock: typeof Nock) => ({ ) => { return nock(basePath) .post(`/stores/${storeId}/read`) - .reply(200, { tuples: [] } as ReadResponse); + .reply(200, { tuples: [], continuation_token: "" } as ReadResponse); }, write: ( storeId: string, diff --git a/tests/index.test.ts b/tests/index.test.ts index 006c307..c7ffba0 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -278,8 +278,9 @@ describe("OpenFGA SDK", function () { describe("400 level error should result in FgaApiValidationError", () => { const tupleKey = { - object: "foobar:x", user: "user:xyz", + relation: "viewer", + object: "foobar:x", }; beforeEach(async () => { @@ -330,8 +331,9 @@ describe("OpenFGA SDK", function () { describe("429 level error should result in FgaApiRateLimitExceededError", () => { const tupleKey = { - object: "foobar:x", user: "user:xyz", + relation: "viewer", + object: "foobar:x", }; beforeEach(async () => { @@ -370,8 +372,9 @@ describe("OpenFGA SDK", function () { describe("429 with retry in config and retry is successful", () => { const tupleKey = { - object: "foobar:x", user: "user:xyz", + relation: "viewer", + object: "foobar:x", }; beforeEach(async () => { @@ -413,8 +416,9 @@ describe("OpenFGA SDK", function () { describe("429 with retry in call and retry is successful", () => { const tupleKey = { - object: "foobar:x", user: "user:xyz", + relation: "viewer", + object: "foobar:x", }; beforeEach(async () => { @@ -453,8 +457,9 @@ describe("OpenFGA SDK", function () { describe("500 level error should result in FgaApiInternalError", () => { const tupleKey = { - object: "foobar:x", user: "user:xyz", + relation: "viewer", + object: "foobar:x", }; beforeEach(async () => { @@ -517,8 +522,9 @@ describe("OpenFGA SDK", function () { describe("404 level error should result in FgaApiNotFoundError", () => { const tupleKey = { - object: "foobar:x", user: "user:xyz", + relation: "viewer", + object: "foobar:x", }; beforeEach(async () => { @@ -551,8 +557,9 @@ describe("OpenFGA SDK", function () { describe("401 during authentication should result in FgaApiAuthenticationError", () => { const tupleKey = { - object: "foobar:x", user: "user:xyz", + relation: "viewer", + object: "foobar:x", }; beforeEach(async () => {