From e3cc55357ca306f615608c1a06b4815a1b23eb44 Mon Sep 17 00:00:00 2001 From: Michael Gagliardo Date: Thu, 27 Oct 2022 11:05:21 -0400 Subject: [PATCH] fix: set correct consistency and rework to class-based proxy handlers --- .github/workflows/authzed-node.yaml | 2 +- src/v1-promise.test.ts | 12 ++ src/v1.test.ts | 12 ++ src/v1.ts | 206 ++++++++++++++++------------ 4 files changed, 143 insertions(+), 89 deletions(-) diff --git a/.github/workflows/authzed-node.yaml b/.github/workflows/authzed-node.yaml index f7577c7..4fd6215 100644 --- a/.github/workflows/authzed-node.yaml +++ b/.github/workflows/authzed-node.yaml @@ -85,7 +85,7 @@ jobs: with: working-directory: ./js-dist - name: Run Yarn tests - run: CI=true only-run-tests + run: CI=true yarn only-run-tests working-directory: ./js-dist - uses: actions/upload-artifact@v2 with: diff --git a/src/v1-promise.test.ts b/src/v1-promise.test.ts index 7b89bf0..ca1cf4e 100644 --- a/src/v1-promise.test.ts +++ b/src/v1-promise.test.ts @@ -197,6 +197,12 @@ describe('Lookup APIs', () => { }), permission: 'view', subjectObjectType: 'test/user', + consistency: Consistency.create({ + requirement: { + oneofKind: 'fullyConsistent', + fullyConsistent: true, + }, + }), }); const result = await client.lookupSubjects(request) @@ -219,6 +225,12 @@ describe('Lookup APIs', () => { }), permission: 'view', resourceObjectType: 'test/document', + consistency: Consistency.create({ + requirement: { + oneofKind: 'fullyConsistent', + fullyConsistent: true, + }, + }), }); const resStream = await client.lookupResources(request) diff --git a/src/v1.test.ts b/src/v1.test.ts index 40a8983..daad0c1 100644 --- a/src/v1.test.ts +++ b/src/v1.test.ts @@ -243,6 +243,12 @@ describe('Lookup APIs', () => { }), permission: 'view', subjectObjectType: 'test/user', + consistency: Consistency.create({ + requirement: { + oneofKind: 'fullyConsistent', + fullyConsistent: true, + }, + }), }); const resStream = client.lookupSubjects(request); @@ -280,6 +286,12 @@ describe('Lookup APIs', () => { }), permission: 'view', resourceObjectType: 'test/document', + consistency: Consistency.create({ + requirement: { + oneofKind: 'fullyConsistent', + fullyConsistent: true, + }, + }), }); const resStream = client.lookupResources(request); diff --git a/src/v1.ts b/src/v1.ts index 36bf208..380f194 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -27,11 +27,117 @@ type ZedPromiseClientInterface = { type ICombinedClient = ZedClientInterface & { promises: ZedPromiseClientInterface} -const streamMethods = new Set([ - 'readRelationships', - 'lookupResources', - 'lookupSubjects' -]) +/** + * A standard client_grpc1 (via @grpc/grpc-js) that will correctly + * proxy the namespaced methods to the correct service client. + */ +class ZedClient implements ProxyHandler { + private acl: PermissionsServiceClient + private ns: SchemaServiceClient + private watch: WatchServiceClient + + constructor(endpoint: string, creds: grpc.ChannelCredentials) { + this.acl = new PermissionsServiceClient(endpoint, creds); + this.ns = new SchemaServiceClient(endpoint, creds); + this.watch = new WatchServiceClient(endpoint, creds); + } + + static create(endpoint: string, creds: grpc.ChannelCredentials) { + return new Proxy({} as any, new ZedClient(endpoint, creds)) + } + + get(_target: object, name: string | symbol) { + if ((this.acl as any)[name as string]) { + return (this.acl as any)[name as string]; + } + + if ((this.ns as any)[name as string]) { + return (this.ns as any)[name as string]; + } + + if ((this.watch as any)[name as string]) { + return (this.watch as any)[name as string]; + } + + return undefined; + } +} + +/** + * Proxies all methods from the {@link ZedClientInterface} to return promises + * in order to support async/await for {@link ClientUnaryCall} and {@link ClientReadableStream} + * responses. Methods that normally return an instance of stream, will instead return an + * array of objects collected while the stream was open. + * + * @param client The default grpc1 client + * @returns A promise-wrapped grpc1 client + */ +class ZedPromiseClient implements ProxyHandler{ + private client: ZedClientInterface + private promiseCache: Record = {} + private streamMethods = new Set([ + 'readRelationships', + 'lookupResources', + 'lookupSubjects' + ]) + + constructor(client: ZedClientInterface) { + this.client = client + } + + static create(client: ZedClientInterface) { + return new Proxy({} as any, new ZedPromiseClient(client)) + } + + get(_target: Record, name: string | symbol) { + if (!(name in this.promiseCache)) { + const clientMethod = (this.client as any)[name as string] + if (clientMethod !== undefined) { + if (this.streamMethods.has(name as string)) { + this.promiseCache[name as string] = promisifyStream(clientMethod, this.client) + } else if (typeof clientMethod === 'function') { + this.promiseCache[name as string] = promisify((this.client as any)[name as string]).bind(this.client) + } else { + return clientMethod + } + } + } + + return this.promiseCache[name as string] + } +} + +/** + * The {@link ZedCombinedClient} proxies both callback/promise-style methods to the underlying + * {@link ZedClient} and {@link ZedPromiseClient} instances. Direct method calls on the combined + * client will result in calling the underlying callback methods (the generated gRPC methods) while + * the same methods accessed at a sub-path `.promises.` will result in the promise-wrapped + * methods. + */ +class ZedCombinedClient implements ProxyHandler{ + private client: ZedClientInterface + private promiseClient: ZedPromiseClientInterface + + constructor(client: ZedClientInterface, promiseClient: ZedPromiseClientInterface) { + this.client = client + this.promiseClient = promiseClient + } + + static create(endpoint: string, creds: grpc.ChannelCredentials) { + const client = ZedClient.create(endpoint, creds) + const promiseClient = ZedPromiseClient.create(client) + + return new Proxy({} as any, new ZedCombinedClient(client, promiseClient)) + } + + get(_target: object, name: string | symbol) { + if (name === 'promises') { + return this.promiseClient + } + + return (this.client as any)[name as string] + } +} /** * NewClient creates a new client for calling Authzed APIs. @@ -68,10 +174,12 @@ export function NewClientWithCustomCert( /** * NewClientWithChannelCredentials creates a new client for calling Authzed APIs using custom grpc ChannelCredentials. * - * The combined client exposes all service-level methods at the root which directly call grpc-generated methods - * while also exposing a `promises` object containing all promise-wrapped methods. For all methods that - * return a {@link ClientReadableStream}, the promise-wrapped method will return an array of the resulting responses - * after the stream has been closed. + The {@link ZedCombinedClient} proxies both callback/promise-style methods to the underlying + * {@link ZedClient} and {@link ZedPromiseClient} instances. Direct method calls on the combined + * client will result in calling the underlying callback methods (the generated gRPC methods) while + * the same methods accessed at a sub-path `.promises.` will result in the promise-wrapped + * methods. For all methods that return a {@link ClientReadableStream}, the promise-wrapped method + * will return an array of the resulting responses after the stream has been closed. * * @param endpoint Uri for communicating with Authzed. * @param creds ChannelCredentials used for grpc. @@ -81,85 +189,7 @@ export function NewClientWithChannelCredentials( endpoint = util.authzedEndpoint, creds: grpc.ChannelCredentials ): ICombinedClient { - const proxy = createClient(endpoint, creds) - const promiseClient = createPromiseClient(proxy) - - const fullHandler = { - get(target: object, name: string | symbol) { - if (name === 'promises') { - return promiseClient - } - - return (proxy as any)[name as string] - } - } - - return new Proxy({}, fullHandler) as ICombinedClient -} - -/** - * Creates the standard client_grpc1 (via @grpc/grpc-js) that will correctly - * proxy the namespaced methods to the correct service client. - * - * @param endpoint The grpc endpoing - * @param creds The channel credentials - * @returns A default grpc1 client - */ -function createClient(endpoint: string, creds: grpc.ChannelCredentials): ZedClientInterface { - const acl = new PermissionsServiceClient(endpoint, creds); - const ns = new SchemaServiceClient(endpoint, creds); - const watch = new WatchServiceClient(endpoint, creds); - - - const handler = { - get(target: object, name: string | symbol) { - if ((acl as any)[name as string]) { - return (acl as any)[name as string]; - } - - if ((ns as any)[name as string]) { - return (ns as any)[name as string]; - } - - if ((watch as any)[name as string]) { - return (watch as any)[name as string]; - } - - return undefined; - }, - }; - - return new Proxy({} as ZedClientInterface, handler) -} - -/** - * Proxies all methods from the {@link ZedClientInterface} to return promises - * in order to support async/await for {@link ClientUnaryCall} and {@link ClientReadableStream} - * responses. - * - * @param client The default grpc1 client - * @returns A promise-wrapped grpc1 client - */ -function createPromiseClient(client: ZedClientInterface): ZedPromiseClientInterface { - const handler = { - get(target: object, name: string | symbol) { - if ((client as any)[name as string]) { - if (streamMethods.has(name as string)) { - return promisifyStream((client as any)[name as string], client) - } - - if (typeof (client as any)[name as string] === 'function') { - return promisify((client as any)[name as string]).bind(client); - } - - return (client as any)[name as string] - } - - return undefined - }, - }; - - return new Proxy({} as any, handler); + return ZedCombinedClient.create(endpoint, creds) } export * from "./authzedapi/authzed/api/v1/core";