From 7e34b9f211f24e2a017eb5d0c9f6dab08dd42481 Mon Sep 17 00:00:00 2001 From: Jacek Tomaszewski Date: Wed, 22 Apr 2026 00:15:15 +0200 Subject: [PATCH] feat(query-graphql): expose computeAuthorizationFilter on Authorizer Add a public `computeAuthorizationFilter(ctx, authorizationContext)` method on the `Authorizer` interface as a thin, intention-revealing alias of `authorize()`. DefaultAuthorizer and the internal relation authorizer delegate to authorize(). This lets custom resolvers and domain services reuse the exact filter the `@Authorize` pipeline produces without duplicating operator mapping or reaching into the resolver internals. Co-Authored-By: Claude Opus 4.7 --- .../__fixtures__/test-resolver.authorizer.ts | 4 +++ .../auth/default-crud-auth.service.spec.ts | 15 +++++++++ packages/query-graphql/src/auth/authorizer.ts | 12 +++++++ .../src/auth/default-crud.authorizer.ts | 31 ++++++++++++++----- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/packages/query-graphql/__tests__/__fixtures__/test-resolver.authorizer.ts b/packages/query-graphql/__tests__/__fixtures__/test-resolver.authorizer.ts index cefb102ea..8f4758621 100644 --- a/packages/query-graphql/__tests__/__fixtures__/test-resolver.authorizer.ts +++ b/packages/query-graphql/__tests__/__fixtures__/test-resolver.authorizer.ts @@ -13,4 +13,8 @@ export class TestResolverAuthorizer implements Authorizer { authorizeRelation(): Promise> { return Promise.reject(new Error('authorizeRelation Not Implemented')) } + + computeAuthorizationFilter(): Promise> { + return Promise.reject(new Error('computeAuthorizationFilter Not Implemented')) + } } diff --git a/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts b/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts index 3580dbe56..d2e9081ee 100644 --- a/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts +++ b/packages/query-graphql/__tests__/auth/default-crud-auth.service.spec.ts @@ -126,6 +126,21 @@ describe('createDefaultAuthorizer', () => { expect(filter).toEqual({ ownerId: { eq: 2 } }) }) + it('should expose computeAuthorizationFilter as a public alias of authorize', async () => { + const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)) + const context = { user: { id: 7 } } + const authzCtx: AuthorizationContext = { + operationName: 'queryMany', + operationGroup: OperationGroup.READ, + readonly: true, + many: true + } + const viaAuthorize = await authorizer.authorize(context, authzCtx) + const viaCompute = await authorizer.computeAuthorizationFilter(context, authzCtx) + expect(viaCompute).toEqual(viaAuthorize) + expect(viaCompute).toEqual({ ownerId: { eq: 7 } }) + }) + it('should create an auth filter that depends on the passed operation name', async () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)) const filter = await authorizer.authorize( diff --git a/packages/query-graphql/src/auth/authorizer.ts b/packages/query-graphql/src/auth/authorizer.ts index 06f57dcc7..d79b237d8 100644 --- a/packages/query-graphql/src/auth/authorizer.ts +++ b/packages/query-graphql/src/auth/authorizer.ts @@ -44,4 +44,16 @@ export interface Authorizer extends CustomAuthorizer { context: any, authorizerContext: AuthorizationContext ): Promise | undefined> + + /** + * Compute the authorization filter the `@Authorize` pipeline would apply for + * the given context and operation. Intended for consumers outside the + * resolver pipeline (custom resolvers, domain services) so they can reuse + * the same `Filter` the generated CRUD resolvers use. + */ + computeAuthorizationFilter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: any, + authorizerContext: AuthorizationContext + ): Promise> } diff --git a/packages/query-graphql/src/auth/default-crud.authorizer.ts b/packages/query-graphql/src/auth/default-crud.authorizer.ts index 468f77d37..a4f55edf7 100644 --- a/packages/query-graphql/src/auth/default-crud.authorizer.ts +++ b/packages/query-graphql/src/auth/default-crud.authorizer.ts @@ -12,15 +12,22 @@ export interface AuthorizerOptions { authorize: (context: any, authorizationContext: AuthorizationContext) => Filter | Promise> } -const createRelationAuthorizer = (opts: AuthorizerOptions): Authorizer => ({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async authorize(context: any, authorizationContext: AuthorizationContext): Promise> { - return opts.authorize(context, authorizationContext) ?? {} - }, - authorizeRelation(): Promise> { - return Promise.reject(new Error('Not implemented')) +const createRelationAuthorizer = (opts: AuthorizerOptions): Authorizer => { + const relationAuthorizer: Authorizer = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async authorize(context: any, authorizationContext: AuthorizationContext): Promise> { + return opts.authorize(context, authorizationContext) ?? {} + }, + authorizeRelation(): Promise> { + return Promise.reject(new Error('Not implemented')) + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + computeAuthorizationFilter(context: any, authorizationContext: AuthorizationContext): Promise> { + return relationAuthorizer.authorize(context, authorizationContext) + } } -}) + return relationAuthorizer +} export function createDefaultAuthorizer( DTOClass: Class, @@ -51,6 +58,14 @@ export function createDefaultAuthorizer( ) } + computeAuthorizationFilter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: any, + authorizationContext: AuthorizationContext + ): Promise> { + return this.authorize(context, authorizationContext) + } + async authorizeRelation( relationName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any