diff --git a/documentation/docs/graphql/authorization.mdx b/documentation/docs/graphql/authorization.mdx index 0a13944c0..62c246ac4 100644 --- a/documentation/docs/graphql/authorization.mdx +++ b/documentation/docs/graphql/authorization.mdx @@ -428,12 +428,20 @@ export class SubTaskAuthorizer implements Authorizer { The `AuthorizationContext` has the following shape: ```ts title='authorizer.ts' +export enum OperationGroup { + READ = 'read', + AGGREGATE = 'aggregate', + CREATE = 'create', + UPDATE = 'update', + DELETE = 'delete', +} + interface AuthorizationContext { /** The name of the method that uses the @AuthorizeFilter decorator */ operationName: string; /** The group this operation belongs to */ - operationGroup: 'read' | 'aggregate' | 'create' | 'update' | 'delete'; + operationGroup: OperationGroup; /** If the operation does not modify any entities */ readonly: boolean; @@ -444,26 +452,27 @@ interface AuthorizationContext { ``` This context is automatially created for you when using the built-in resolvers. -If you authorize custom methods by using the `@AuthorizerFilter()`, you have three options: - -- Pass your own `AuthorizationContext` as argument to the decorator: - ```ts - @AuthorizerFilter({ - operationName: 'completedTodoItems', - operationGroup: 'read', - readonly: true, - many: true - }) - ``` -- Pass a custom operation name as argument and let the context be inferred from the name (the nae should follow the conventions below): `@AuthorizerFilter('queryCompletedTodoItems')`. -- Don't pass a custom name and let the decorator use the name of the decorated method: `@AuthorizerFilter()` - -The `operationName` is the name of the method that makes use of the `@AuthorizerFilter()` or the argument passed to this decorator (`@AuthorizerFilter('customMethodName')`). -The names of the generated CRUD resolver methods are similar to the ones of the [QueryService](../concepts/services.mdx): - -- `query` +If you authorize custom methods by using the `@AuthorizerFilter()`, you should pass the context as argument to the decorator: + +```ts +@AuthorizerFilter({ + operationName: 'completedTodoItems', + operationGroup: OperationGroup.READ, + readonly: true, + many: true +}) +``` + +You can leave out the `operationName` to let the context use the name of the decorated Method. +If you leave out the `readonly` property, it's inferred from the `operationGroup`. + +The `operationNames` of the generated CRUD resolver methods are similar to the ones of the [QueryService](../concepts/services.mdx): + +- `queryMany` - `findById` - `aggregate` +- `createOne` +- `createMany` - `updateOne` - `updateMany` - `deleteOne` @@ -474,8 +483,10 @@ The names of the generated CRUD resolver methods are similar to the ones of the - `query{PluralRelationName}` (e.g. querySubTasks) - `find{SingularRelationName}` (e.g. findTodoItem) - `aggregate{PluralRelationName}` (e.g. aggregateSubTasks) -- `remove{RelationName}from{SingularParentName}` (e.g. removeSubTaskFromTodoItem) -- `set{RelationName}On{SingularParentName}` (e.g. setSubTaskOnTodoItem) +- `remove{SingularRelationName}from{SingularParentName}` (e.g. removeSubTaskFromTodoItem) +- `remove{PluralRelationName}from{SingularParentName}` (e.g. removeSubTasksFromTodoItem) +- `set{SingularRelationName}On{SingularParentName}` (e.g. setSubTaskOnTodoItem) +- `add{PluralRelationName}On{SingularParentName}` (e.g. addSubTasksOnTodoItem) ## Complete Example diff --git a/examples/auth/src/todo-item/todo-item.resolver.ts b/examples/auth/src/todo-item/todo-item.resolver.ts index 071461860..14b7ebb81 100644 --- a/examples/auth/src/todo-item/todo-item.resolver.ts +++ b/examples/auth/src/todo-item/todo-item.resolver.ts @@ -1,5 +1,10 @@ import { Filter, InjectAssemblerQueryService, mergeFilter, mergeQuery, QueryService } from '@nestjs-query/core'; -import { AuthorizerInterceptor, AuthorizerFilter, ConnectionType } from '@nestjs-query/query-graphql'; +import { + AuthorizerInterceptor, + AuthorizerFilter, + ConnectionType, + AuthorizationOperationGroup, +} from '@nestjs-query/query-graphql'; import { Args, Query, Resolver } from '@nestjs/graphql'; import { UseGuards, UseInterceptors } from '@nestjs/common'; import { TodoItemDTO } from './dto/todo-item.dto'; @@ -17,7 +22,11 @@ export class TodoItemResolver { @Query(() => TodoItemConnection) async completedTodoItems( @Args() query: TodoItemQuery, - @AuthorizerFilter('queryCompletedTodoItems') authFilter: Filter, + @AuthorizerFilter({ + operationGroup: AuthorizationOperationGroup.READ, + many: true, + }) + authFilter: Filter, ): Promise> { // add the completed filter the user provided filter const filter: Filter = mergeFilter(query.filter ?? {}, { completed: { is: true } }); @@ -34,9 +43,7 @@ export class TodoItemResolver { async uncompletedTodoItems( @Args() query: TodoItemQuery, @AuthorizerFilter({ - operationName: 'queryUncompletedTodoItems', - operationGroup: 'read', - readonly: true, + operationGroup: AuthorizationOperationGroup.READ, many: true, }) authFilter: Filter, 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 85d2aec62..81fed7af4 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 @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Filter } from '@nestjs-query/core'; import { Injectable } from '@nestjs/common'; import { Authorizer, Relation, Authorize, UnPagedRelation } from '../../src'; -import { AuthorizationContext, getAuthorizerToken } from '../../src/auth'; +import { AuthorizationContext, OperationGroup, getAuthorizerToken } from '../../src/auth'; import { createAuthorizerProviders } from '../../src/providers'; describe('createDefaultAuthorizer', () => { @@ -85,7 +85,7 @@ describe('createDefaultAuthorizer', () => { const authorizer = testingModule.get>(getAuthorizerToken(TestDTO)); const filter = await authorizer.authorize( { user: { id: 2 } }, - { operationName: 'other', operationGroup: 'read', readonly: true, many: true }, + { operationName: 'other', operationGroup: OperationGroup.READ, readonly: true, many: true }, ); expect(filter).toEqual({ ownerId: { neq: 2 } }); }); @@ -113,7 +113,7 @@ describe('createDefaultAuthorizer', () => { const filter = await authorizer.authorizeRelation( 'relations', { user: { id: 2 } }, - { operationName: 'other', operationGroup: 'read', readonly: true, many: true }, + { operationName: 'other', operationGroup: OperationGroup.READ, readonly: true, many: true }, ); expect(filter).toEqual({ relationOwnerId: { neq: 2 } }); }); diff --git a/packages/query-graphql/src/auth/authorizer.ts b/packages/query-graphql/src/auth/authorizer.ts index b9c402af8..017a15125 100644 --- a/packages/query-graphql/src/auth/authorizer.ts +++ b/packages/query-graphql/src/auth/authorizer.ts @@ -1,13 +1,19 @@ import { Filter } from '@nestjs-query/core'; -export type AuthorizationOperationGroup = 'read' | 'aggregate' | 'create' | 'update' | 'delete'; +export enum OperationGroup { + READ = 'read', + AGGREGATE = 'aggregate', + CREATE = 'create', + UPDATE = 'update', + DELETE = 'delete', +} export interface AuthorizationContext { /** The name of the method that uses the @AuthorizeFilter decorator */ operationName: string; /** The group this operation belongs to */ - operationGroup: AuthorizationOperationGroup; + operationGroup: OperationGroup; /** If the operation does not modify any entities */ readonly: boolean; diff --git a/packages/query-graphql/src/decorators/authorize-filter.decorator.ts b/packages/query-graphql/src/decorators/authorize-filter.decorator.ts index 6ffc09756..14d1bcc11 100644 --- a/packages/query-graphql/src/decorators/authorize-filter.decorator.ts +++ b/packages/query-graphql/src/decorators/authorize-filter.decorator.ts @@ -1,9 +1,12 @@ import { ModifyRelationOptions } from '@nestjs-query/core'; import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; -import { AuthorizationContext, AuthorizationOperationGroup } from '../auth'; +import { AuthorizationContext, OperationGroup } from '../auth'; import { AuthorizerContext } from '../interceptors'; +type PartialAuthorizationContext = Partial & + Pick; + function getContext(executionContext: ExecutionContext): C { const gqlExecutionContext = GqlExecutionContext.create(executionContext); return gqlExecutionContext.getContext(); @@ -11,7 +14,7 @@ function getContext(executionContext: ExecutionContext): C { function getAuthorizerFilter>( context: C, - authorizationContext: AuthorizationContext, + authorizationContext?: AuthorizationContext, ) { if (!context.authorizer) { return undefined; @@ -22,7 +25,7 @@ function getAuthorizerFilter>( function getRelationAuthFilter>( context: C, relationName: string, - authorizationContext: AuthorizationContext, + authorizationContext?: AuthorizationContext, ) { if (!context.authorizer) { return undefined; @@ -30,80 +33,51 @@ function getRelationAuthFilter>( return context.authorizer.authorizeRelation(relationName, context, authorizationContext); } -function getAuthorizationContext(operationNameOrContext: string | AuthorizationContext): AuthorizationContext { - if (typeof operationNameOrContext !== 'string') { - return operationNameOrContext; - } - - const lcMethodName = operationNameOrContext.toLowerCase(); - - const isCreate = lcMethodName.startsWith('create'); - const isUpdate = lcMethodName.startsWith('update') || lcMethodName.startsWith('set'); - const isDelete = lcMethodName.startsWith('delete') || lcMethodName.startsWith('remove'); - const isAggregate = lcMethodName.startsWith('aggregate'); - const isQuery = lcMethodName.startsWith('query'); - const isFind = lcMethodName.startsWith('find'); - const isMany = lcMethodName.endsWith('many'); - - let operationGroup: AuthorizationOperationGroup = 'read'; - - if (isCreate) { - operationGroup = 'create'; - } else if (isDelete) { - operationGroup = 'delete'; - } else if (isUpdate) { - operationGroup = 'update'; - } else if (isAggregate) { - operationGroup = 'aggregate'; - } +function getAuthorizationContext( + methodName: string | symbol, + partialAuthContext?: PartialAuthorizationContext, +): AuthorizationContext | undefined { + if (!partialAuthContext) return undefined; return { - operationName: operationNameOrContext, - operationGroup, - readonly: isQuery || isFind || isAggregate, - many: isMany || isQuery || isAggregate, + operationName: methodName.toString(), + readonly: + partialAuthContext.operationGroup === OperationGroup.READ || + partialAuthContext.operationGroup === OperationGroup.AGGREGATE, + ...partialAuthContext, }; } -export function AuthorizerFilter(): ParameterDecorator; -export function AuthorizerFilter(context: AuthorizationContext): ParameterDecorator; -export function AuthorizerFilter(operationName: string): ParameterDecorator; -export function AuthorizerFilter(operationNameOrContext?: string | AuthorizationContext): ParameterDecorator { +export function AuthorizerFilter(partialAuthContext?: PartialAuthorizationContext): ParameterDecorator { // eslint-disable-next-line @typescript-eslint/ban-types return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { - const authorizationContext = getAuthorizationContext(operationNameOrContext ?? propertyKey.toString()); + const authorizationContext = getAuthorizationContext(propertyKey, partialAuthContext); return createParamDecorator((data: unknown, executionContext: ExecutionContext) => getAuthorizerFilter(getContext>(executionContext), authorizationContext), )()(target, propertyKey, parameterIndex); }; } -export function RelationAuthorizerFilter(relationName: string): ParameterDecorator; -export function RelationAuthorizerFilter(relationName: string, operationName: string): ParameterDecorator; -export function RelationAuthorizerFilter(relationName: string, context: AuthorizationContext): ParameterDecorator; export function RelationAuthorizerFilter( relationName: string, - operationNameOrContext?: string | AuthorizationContext, + partialAuthContext?: PartialAuthorizationContext, ): ParameterDecorator { // eslint-disable-next-line @typescript-eslint/ban-types return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { - const authorizationContext = getAuthorizationContext(operationNameOrContext ?? propertyKey.toString()); + const authorizationContext = getAuthorizationContext(propertyKey, partialAuthContext); return createParamDecorator((data: unknown, executionContext: ExecutionContext) => getRelationAuthFilter(getContext>(executionContext), relationName, authorizationContext), )()(target, propertyKey, parameterIndex); }; } -export function ModifyRelationAuthorizerFilter(relationName: string): ParameterDecorator; -export function ModifyRelationAuthorizerFilter(relationName: string, operationName: string): ParameterDecorator; -export function ModifyRelationAuthorizerFilter(relationName: string, context: AuthorizationContext): ParameterDecorator; export function ModifyRelationAuthorizerFilter( relationName: string, - operationNameOrContext?: string | AuthorizationContext, + partialAuthContext?: PartialAuthorizationContext, ): ParameterDecorator { // eslint-disable-next-line @typescript-eslint/ban-types return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { - const authorizationContext = getAuthorizationContext(operationNameOrContext ?? propertyKey.toString()); + const authorizationContext = getAuthorizationContext(propertyKey, partialAuthContext); return createParamDecorator( async (data: unknown, executionContext: ExecutionContext): Promise> => { const context = getContext>(executionContext); diff --git a/packages/query-graphql/src/index.ts b/packages/query-graphql/src/index.ts index 44b3f5307..454761b7f 100644 --- a/packages/query-graphql/src/index.ts +++ b/packages/query-graphql/src/index.ts @@ -40,7 +40,12 @@ export { DTONamesOpts } from './common'; export { NestjsQueryGraphQLModule } from './module'; export { AutoResolverOpts } from './providers'; export { pubSubToken, GraphQLPubSub } from './subscription'; -export { Authorizer, AuthorizerOptions, AuthorizationContext } from './auth'; +export { + Authorizer, + AuthorizerOptions, + AuthorizationContext, + OperationGroup as AuthorizationOperationGroup, +} from './auth'; export { Hook, HookTypes, diff --git a/packages/query-graphql/src/resolvers/aggregate.resolver.ts b/packages/query-graphql/src/resolvers/aggregate.resolver.ts index e30d4911b..1f1f3d891 100644 --- a/packages/query-graphql/src/resolvers/aggregate.resolver.ts +++ b/packages/query-graphql/src/resolvers/aggregate.resolver.ts @@ -7,6 +7,7 @@ import { AggregateQueryParam, AuthorizerFilter, ResolverMethodOpts, ResolverQuer import { AggregateArgsType, AggregateResponseType } from '../types'; import { transformAndValidate } from './helpers'; import { BaseServiceResolver, ResolverClass, ServiceResolver } from './resolver.interface'; +import { OperationGroup } from '../auth'; export type AggregateResolverOpts = { enabled?: boolean; @@ -51,7 +52,11 @@ export const Aggregateable = , - @AuthorizerFilter() authFilter?: Filter, + @AuthorizerFilter({ + operationGroup: OperationGroup.AGGREGATE, + many: true, + }) + authFilter?: Filter, ): Promise[]> { const qa = await transformAndValidate(AA, args); return this.service.aggregate(mergeFilter(qa.filter || {}, authFilter ?? {}), query); diff --git a/packages/query-graphql/src/resolvers/create.resolver.ts b/packages/query-graphql/src/resolvers/create.resolver.ts index af2b83fe4..1c5396eac 100644 --- a/packages/query-graphql/src/resolvers/create.resolver.ts +++ b/packages/query-graphql/src/resolvers/create.resolver.ts @@ -20,6 +20,7 @@ import { } from '../types'; import { createSubscriptionFilter } from './helpers'; import { BaseServiceResolver, ResolverClass, ServiceResolver, SubscriptionResolverOpts } from './resolver.interface'; +import { OperationGroup } from '../auth'; export type CreatedEvent = { [eventName: string]: DTO }; @@ -135,8 +136,14 @@ export const Creatable = >( }, opts.one ?? {}, ) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async createOne(@MutationHookArgs() input: CO, @AuthorizerFilter() authorizeFilter?: Filter): Promise { + async createOne( + @MutationHookArgs() input: CO, + @AuthorizerFilter({ + operationGroup: OperationGroup.CREATE, + many: false, + }) // eslint-disable-next-line @typescript-eslint/no-unused-vars + authorizeFilter?: Filter, + ): Promise { // Ignore `authorizeFilter` for now but give users the ability to throw an UnauthorizedException const created = await this.service.createOne(input.input.input); if (enableOneSubscriptions) { @@ -157,8 +164,15 @@ export const Creatable = >( }, opts.many ?? {}, ) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async createMany(@MutationHookArgs() input: CM, @AuthorizerFilter() authorizeFilter?: Filter): Promise { + async createMany( + @MutationHookArgs() input: CM, + + @AuthorizerFilter({ + operationGroup: OperationGroup.CREATE, + many: true, + }) // eslint-disable-next-line @typescript-eslint/no-unused-vars + authorizeFilter?: Filter, + ): Promise { // Ignore `authorizeFilter` for now but give users the ability to throw an UnauthorizedException const created = await this.service.createMany(input.input.input); if (enableManySubscriptions) { diff --git a/packages/query-graphql/src/resolvers/delete.resolver.ts b/packages/query-graphql/src/resolvers/delete.resolver.ts index 5b3b6bbd5..7f741d400 100644 --- a/packages/query-graphql/src/resolvers/delete.resolver.ts +++ b/packages/query-graphql/src/resolvers/delete.resolver.ts @@ -17,6 +17,7 @@ import { import { MutationHookArgs, ResolverMutation, ResolverSubscription, AuthorizerFilter } from '../decorators'; import { createSubscriptionFilter } from './helpers'; import { AuthorizerInterceptor, HookInterceptor } from '../interceptors'; +import { OperationGroup } from '../auth'; export type DeletedEvent = { [eventName: string]: DTO }; export interface DeleteResolverOpts extends SubscriptionResolverOpts { @@ -104,7 +105,11 @@ export const Deletable = >( ) async deleteOne( @MutationHookArgs() input: DO, - @AuthorizerFilter() authorizeFilter?: Filter, + @AuthorizerFilter({ + operationGroup: OperationGroup.DELETE, + many: false, + }) + authorizeFilter?: Filter, ): Promise> { const deletedResponse = await this.service.deleteOne(input.input.id, { filter: authorizeFilter ?? {} }); if (enableOneSubscriptions) { @@ -122,7 +127,11 @@ export const Deletable = >( ) async deleteMany( @MutationHookArgs() input: DM, - @AuthorizerFilter() authorizeFilter?: Filter, + @AuthorizerFilter({ + operationGroup: OperationGroup.DELETE, + many: false, + }) + authorizeFilter?: Filter, ): Promise { const deleteManyResponse = await this.service.deleteMany(mergeFilter(input.input.filter, authorizeFilter ?? {})); if (enableManySubscriptions) { diff --git a/packages/query-graphql/src/resolvers/read.resolver.ts b/packages/query-graphql/src/resolvers/read.resolver.ts index 6a5994834..ea6fac56a 100644 --- a/packages/query-graphql/src/resolvers/read.resolver.ts +++ b/packages/query-graphql/src/resolvers/read.resolver.ts @@ -21,6 +21,7 @@ import { } from './resolver.interface'; import { AuthorizerInterceptor, HookInterceptor } from '../interceptors'; import { HookTypes } from '../hooks'; +import { OperationGroup } from '../auth'; export type ReadResolverFromOpts< DTO, @@ -73,7 +74,14 @@ export const Readable = , QS extends { interceptors: [HookInterceptor(HookTypes.BEFORE_FIND_ONE, DTOClass), AuthorizerInterceptor(DTOClass)] }, opts.one ?? {}, ) - async findById(@HookArgs() input: FO, @AuthorizerFilter() authorizeFilter?: Filter): Promise { + async findById( + @HookArgs() input: FO, + @AuthorizerFilter({ + operationGroup: OperationGroup.READ, + many: false, + }) + authorizeFilter?: Filter, + ): Promise { return this.service.findById(input.id, { filter: authorizeFilter }); } @@ -86,7 +94,11 @@ export const Readable = , QS extends ) async queryMany( @HookArgs() query: QA, - @AuthorizerFilter('query') authorizeFilter?: Filter, + @AuthorizerFilter({ + operationGroup: OperationGroup.READ, + many: true, + }) + authorizeFilter?: Filter, ): Promise> { return ConnectionType.createFromPromise( (q) => this.service.query(q), diff --git a/packages/query-graphql/src/resolvers/relations/aggregate-relations.resolver.ts b/packages/query-graphql/src/resolvers/relations/aggregate-relations.resolver.ts index c2f138491..af6bc5bea 100644 --- a/packages/query-graphql/src/resolvers/relations/aggregate-relations.resolver.ts +++ b/packages/query-graphql/src/resolvers/relations/aggregate-relations.resolver.ts @@ -1,6 +1,7 @@ import { AggregateQuery, AggregateResponse, Class, Filter, mergeFilter, QueryService } from '@nestjs-query/core'; import { ExecutionContext } from '@nestjs/common'; import { Args, ArgsType, Context, Parent, Resolver } from '@nestjs/graphql'; +import { OperationGroup } from '../../auth'; import { getDTONames } from '../../common'; import { AggregateQueryParam, RelationAuthorizerFilter, ResolverField } from '../../decorators'; import { AuthorizerInterceptor } from '../../interceptors'; @@ -53,7 +54,11 @@ const AggregateRelationMixin = (DTOClass: Class, relation: A @Args() q: RelationQA, @AggregateQueryParam() aggregateQuery: AggregateQuery, @Context() context: ExecutionContext, - @RelationAuthorizerFilter(baseNameLower) relationFilter?: Filter, + @RelationAuthorizerFilter(baseNameLower, { + operationGroup: OperationGroup.AGGREGATE, + many: true, + }) + relationFilter?: Filter, ): Promise> { const qa = await transformAndValidate(RelationQA, q); const loader = DataLoaderFactory.getOrCreateLoader( diff --git a/packages/query-graphql/src/resolvers/relations/read-relations.resolver.ts b/packages/query-graphql/src/resolvers/relations/read-relations.resolver.ts index bd828f6fa..96df38cd7 100644 --- a/packages/query-graphql/src/resolvers/relations/read-relations.resolver.ts +++ b/packages/query-graphql/src/resolvers/relations/read-relations.resolver.ts @@ -1,6 +1,7 @@ import { Class, Filter, mergeQuery, QueryService } from '@nestjs-query/core'; import { ExecutionContext } from '@nestjs/common'; import { Args, ArgsType, Context, Parent, Resolver } from '@nestjs/graphql'; +import { OperationGroup } from '../../auth'; import { getDTONames } from '../../common'; import { RelationAuthorizerFilter, ResolverField } from '../../decorators'; import { AuthorizerInterceptor } from '../../interceptors'; @@ -42,7 +43,11 @@ const ReadOneRelationMixin = (DTOClass: Class, relation: Res async [`find${baseName}`]( @Parent() dto: DTO, @Context() context: ExecutionContext, - @RelationAuthorizerFilter(baseNameLower) authFilter?: Filter, + @RelationAuthorizerFilter(baseNameLower, { + operationGroup: OperationGroup.READ, + many: false, + }) + authFilter?: Filter, ): Promise { return DataLoaderFactory.getOrCreateLoader(context, loaderName, findLoader.createLoader(this.service)).load({ dto, @@ -89,7 +94,11 @@ const ReadManyRelationMixin = (DTOClass: Class, relation: Re @Parent() dto: DTO, @Args() q: RelationQA, @Context() context: ExecutionContext, - @RelationAuthorizerFilter(pluralBaseNameLower) relationFilter?: Filter, + @RelationAuthorizerFilter(pluralBaseNameLower, { + operationGroup: OperationGroup.READ, + many: true, + }) + relationFilter?: Filter, ): Promise> { const qa = await transformAndValidate(RelationQA, q); const relationLoader = DataLoaderFactory.getOrCreateLoader( diff --git a/packages/query-graphql/src/resolvers/relations/remove-relations.resolver.ts b/packages/query-graphql/src/resolvers/relations/remove-relations.resolver.ts index 974e39ed6..4559cd948 100644 --- a/packages/query-graphql/src/resolvers/relations/remove-relations.resolver.ts +++ b/packages/query-graphql/src/resolvers/relations/remove-relations.resolver.ts @@ -1,5 +1,6 @@ import { Class, ModifyRelationOptions, QueryService } from '@nestjs-query/core'; import { Resolver, ArgsType, Args } from '@nestjs/graphql'; +import { OperationGroup } from '../../auth'; import { getDTONames } from '../../common'; import { ModifyRelationAuthorizerFilter, ResolverMutation } from '../../decorators'; import { AuthorizerInterceptor } from '../../interceptors'; @@ -30,7 +31,11 @@ const RemoveOneRelationMixin = (DTOClass: Class, relation: R @ResolverMutation(() => DTOClass, {}, commonResolverOpts, { interceptors: [AuthorizerInterceptor(DTOClass)] }) async [`remove${baseName}From${dtoNames.baseName}`]( @Args() setArgs: SetArgs, - @ModifyRelationAuthorizerFilter(baseNameLower) modifyRelationsFilter?: ModifyRelationOptions, + @ModifyRelationAuthorizerFilter(baseNameLower, { + operationGroup: OperationGroup.UPDATE, + many: false, + }) + modifyRelationsFilter?: ModifyRelationOptions, ): Promise { const { input } = await transformAndValidate(SetArgs, setArgs); return this.service.removeRelation(relationName, input.id, input.relationId, modifyRelationsFilter); @@ -60,7 +65,11 @@ const RemoveManyRelationsMixin = (DTOClass: Class, relation: @ResolverMutation(() => DTOClass, {}, commonResolverOpts, { interceptors: [AuthorizerInterceptor(DTOClass)] }) async [`remove${pluralBaseName}From${dtoNames.baseName}`]( @Args() addArgs: AddArgs, - @ModifyRelationAuthorizerFilter(pluralBaseNameLower) modifyRelationsFilter?: ModifyRelationOptions, + @ModifyRelationAuthorizerFilter(pluralBaseNameLower, { + operationGroup: OperationGroup.UPDATE, + many: true, + }) + modifyRelationsFilter?: ModifyRelationOptions, ): Promise { const { input } = await transformAndValidate(AddArgs, addArgs); return this.service.removeRelations(relationName, input.id, input.relationIds, modifyRelationsFilter); diff --git a/packages/query-graphql/src/resolvers/relations/update-relations.resolver.ts b/packages/query-graphql/src/resolvers/relations/update-relations.resolver.ts index 3509efdf8..dfefb1cee 100644 --- a/packages/query-graphql/src/resolvers/relations/update-relations.resolver.ts +++ b/packages/query-graphql/src/resolvers/relations/update-relations.resolver.ts @@ -8,6 +8,7 @@ import { transformAndValidate } from '../helpers'; import { ServiceResolver, BaseServiceResolver } from '../resolver.interface'; import { flattenRelations, removeRelationOpts } from './helpers'; import { RelationsOpts, ResolverRelation } from './relations.interface'; +import { OperationGroup } from '../../auth'; const UpdateOneRelationMixin = (DTOClass: Class, relation: ResolverRelation) => < B extends Class>> @@ -32,7 +33,11 @@ const UpdateOneRelationMixin = (DTOClass: Class, relation: R }) async [`set${baseName}On${dtoNames.baseName}`]( @Args() setArgs: SetArgs, - @ModifyRelationAuthorizerFilter(baseNameLower) modifyRelationsFilter?: ModifyRelationOptions, + @ModifyRelationAuthorizerFilter(baseNameLower, { + operationGroup: OperationGroup.UPDATE, + many: false, + }) + modifyRelationsFilter?: ModifyRelationOptions, ): Promise { const { input } = await transformAndValidate(SetArgs, setArgs); return this.service.setRelation(relationName, input.id, input.relationId, modifyRelationsFilter); @@ -64,7 +69,11 @@ const UpdateManyRelationMixin = (DTOClass: Class, relation: }) async [`add${pluralBaseName}To${dtoNames.baseName}`]( @Args() addArgs: AddArgs, - @ModifyRelationAuthorizerFilter(pluralBaseNameLower) modifyRelationsFilter?: ModifyRelationOptions, + @ModifyRelationAuthorizerFilter(pluralBaseNameLower, { + operationGroup: OperationGroup.UPDATE, + many: true, + }) + modifyRelationsFilter?: ModifyRelationOptions, ): Promise { const { input } = await transformAndValidate(AddArgs, addArgs); return this.service.addRelations(relationName, input.id, input.relationIds, modifyRelationsFilter); diff --git a/packages/query-graphql/src/resolvers/update.resolver.ts b/packages/query-graphql/src/resolvers/update.resolver.ts index 080fc0fba..019f834d3 100644 --- a/packages/query-graphql/src/resolvers/update.resolver.ts +++ b/packages/query-graphql/src/resolvers/update.resolver.ts @@ -25,6 +25,7 @@ import { BaseServiceResolver, ResolverClass, ServiceResolver, SubscriptionResolv import { AuthorizerFilter, MutationHookArgs, ResolverMutation, ResolverSubscription } from '../decorators'; import { createSubscriptionFilter } from './helpers'; import { AuthorizerInterceptor, HookInterceptor } from '../interceptors'; +import { OperationGroup } from '../auth'; export type UpdatedEvent = { [eventName: string]: DTO }; export interface UpdateResolverOpts> extends SubscriptionResolverOpts { @@ -142,7 +143,14 @@ export const Updateable = >( commonResolverOpts, opts.one ?? {}, ) - async updateOne(@MutationHookArgs() input: UO, @AuthorizerFilter() authorizeFilter?: Filter): Promise { + async updateOne( + @MutationHookArgs() input: UO, + @AuthorizerFilter({ + operationGroup: OperationGroup.UPDATE, + many: false, + }) + authorizeFilter?: Filter, + ): Promise { const { id, update } = input.input; const updateResult = await this.service.updateOne(id, update, { filter: authorizeFilter ?? {} }); if (enableOneSubscriptions) { @@ -165,7 +173,11 @@ export const Updateable = >( ) async updateMany( @MutationHookArgs() input: UM, - @AuthorizerFilter() authorizeFilter?: Filter, + @AuthorizerFilter({ + operationGroup: OperationGroup.UPDATE, + many: true, + }) + authorizeFilter?: Filter, ): Promise { const { update, filter } = input.input; const updateManyResponse = await this.service.updateMany(update, mergeFilter(filter, authorizeFilter ?? {}));