diff --git a/.changeset/late-nails-brush.md b/.changeset/late-nails-brush.md new file mode 100644 index 0000000..664d4d8 --- /dev/null +++ b/.changeset/late-nails-brush.md @@ -0,0 +1,5 @@ +--- +'@nest-lab/or-guard': minor +--- + +Allow for AndGuard guards to be ran in sequential order diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6a302fe..462e29b 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,7 +2,6 @@ "recommendations": [ "nrwl.angular-console", "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", - "firsttris.vscode-jest-runner" + "dbaeumer.vscode-eslint" ] } diff --git a/packages/or-guard/README.md b/packages/or-guard/README.md index f4376c9..60b5139 100644 --- a/packages/or-guard/README.md +++ b/packages/or-guard/README.md @@ -90,17 +90,20 @@ And this library will set up the handling of the logic for under the hood. ```ts -AndGuard(guards: Array | InjectionToken>, orGuardOptions?: OrGuardOptions): CanActivate +AndGuard(guards: Array | InjectionToken>, andGuardOptions?: AndGuardOptions): CanActivate ``` - `guards`: an array of guards or injection tokens for the `AndGuard` to resolve and test -- `orGuardOptions`: an optional object with properties to modify how the - `OrGuard` functions +- `andGuardOptions`: an optional object with properties to modify how the + `AndGuard` functions ```ts -interface OrGuardOptions { +interface AndGuardOptions { + // immediately stop all other guards and throw an error throwOnFirstError?: boolean; + // run the guards in order they are declared in the array rather than in parallel + sequential?: boolean; } ``` diff --git a/packages/or-guard/src/lib/and.guard.ts b/packages/or-guard/src/lib/and.guard.ts index 23aa477..c1cc699 100644 --- a/packages/or-guard/src/lib/and.guard.ts +++ b/packages/or-guard/src/lib/and.guard.ts @@ -15,27 +15,28 @@ import { OperatorFunction, throwError, } from 'rxjs'; -import { catchError, last, mergeMap, every } from 'rxjs/operators'; +import { catchError, last, mergeMap, every, concatMap } from 'rxjs/operators'; -interface OrGuardOptions { +interface AndGuardOptions { throwOnFirstError?: boolean; + sequential?: boolean; } export function AndGuard( guards: Array | InjectionToken>, - orGuardOptions?: OrGuardOptions + orGuardOptions?: AndGuardOptions ) { class AndMixinGuard implements CanActivate { private guards: CanActivate[] = []; constructor(@Inject(ModuleRef) private readonly modRef: ModuleRef) {} canActivate(context: ExecutionContext): Observable { this.guards = guards.map((guard) => this.modRef.get(guard)); - const canActivateReturns: Array> = this.guards.map( - (guard) => this.deferGuard(guard, context) - ); + const canActivateReturns: Array<() => Observable> = + this.guards.map((guard) => () => this.deferGuard(guard, context)); + const mapOperator = orGuardOptions?.sequential ? concatMap : mergeMap; return from(canActivateReturns).pipe( - mergeMap((obs) => { - return obs.pipe(this.handleError()); + mapOperator((obs) => { + return obs().pipe(this.handleError()); }), every((val) => val === true), last() diff --git a/packages/or-guard/test/app.controller.ts b/packages/or-guard/test/app.controller.ts index 0acfe5e..87fe4dd 100644 --- a/packages/or-guard/test/app.controller.ts +++ b/packages/or-guard/test/app.controller.ts @@ -1,8 +1,10 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; -import { OrGuard } from '../src'; +import { AndGuard, OrGuard } from '../src'; import { ObsGuard } from './obs.guard'; import { PromGuard } from './prom.guard'; +import { ReadUserGuard } from './read-user.guard'; +import { SetUserGuard } from './set-user.guard'; import { SyncGuard } from './sync.guard'; import { ThrowGuard } from './throw.guard'; @@ -32,4 +34,16 @@ export class AppController { getLogicalAnd() { return this.message; } + + @UseGuards(AndGuard([SetUserGuard, ReadUserGuard])) + @Get('set-user-fail') + getSetUserFail() { + return this.message; + } + + @UseGuards(AndGuard([SetUserGuard, ReadUserGuard], { sequential: true })) + @Get('set-user-pass') + getSetUserPass() { + return this.message; + } } diff --git a/packages/or-guard/test/app.module.ts b/packages/or-guard/test/app.module.ts index 861ee34..2a91b5d 100644 --- a/packages/or-guard/test/app.module.ts +++ b/packages/or-guard/test/app.module.ts @@ -6,6 +6,8 @@ import { ObsGuard } from './obs.guard'; import { PromGuard } from './prom.guard'; import { SyncGuard } from './sync.guard'; import { ThrowGuard } from './throw.guard'; +import { SetUserGuard } from './set-user.guard'; +import { ReadUserGuard } from './read-user.guard'; @Module({ controllers: [AppController], @@ -14,6 +16,8 @@ import { ThrowGuard } from './throw.guard'; SyncGuard, PromGuard, ThrowGuard, + SetUserGuard, + ReadUserGuard, { provide: 'SyncAndProm', useClass: AndGuard([SyncGuard, PromGuard]), diff --git a/packages/or-guard/test/or.guard.spec.ts b/packages/or-guard/test/or.guard.spec.ts index 69ee16a..50a0666 100644 --- a/packages/or-guard/test/or.guard.spec.ts +++ b/packages/or-guard/test/or.guard.spec.ts @@ -8,7 +8,7 @@ import { ObsGuard } from './obs.guard'; import { PromGuard } from './prom.guard'; import { SyncGuard } from './sync.guard'; -describe('Or Guard Integration Test', () => { +describe('OrGuard and AndGuard Integration Test', () => { let moduleConfig: TestingModuleBuilder; beforeEach(() => { @@ -55,7 +55,7 @@ describe('Or Guard Integration Test', () => { await app.close(); }); /** - * OrGuard([SyncGuard, PromGuard, ObsGuard]) + * OrGuard([AndGuard([SyncGuard, PromGuard]), ObsGuard]) * * | Sync | Prom | Obs | Final | * | - | - | - | - | @@ -185,4 +185,22 @@ describe('Or Guard Integration Test', () => { }); } ); + + describe('AndGuard with options', () => { + let app: INestApplication; + beforeAll(async () => { + const testMod = await moduleConfig.compile(); + app = testMod.createNestApplication(); + await app.init(); + }); + afterAll(async () => { + await app.close(); + }); + it('should throw an error if not ran sequentially', async () => { + return supertest(app.getHttpServer()).get('/set-user-fail').expect(403); + }); + it('should not throw an error if ran sequentially', async () => { + return supertest(app.getHttpServer()).get('/set-user-pass').expect(200); + }); + }); }); diff --git a/packages/or-guard/test/read-user.guard.ts b/packages/or-guard/test/read-user.guard.ts new file mode 100644 index 0000000..5a13ddf --- /dev/null +++ b/packages/or-guard/test/read-user.guard.ts @@ -0,0 +1,12 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class ReadUserGuard implements CanActivate { + canActivate( + context: ExecutionContext + ): boolean | Promise | Observable { + const req = context.switchToHttp().getRequest(); + return !!req.user; + } +} diff --git a/packages/or-guard/test/set-user.guard.ts b/packages/or-guard/test/set-user.guard.ts new file mode 100644 index 0000000..c9b7425 --- /dev/null +++ b/packages/or-guard/test/set-user.guard.ts @@ -0,0 +1,12 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { setTimeout } from 'timers/promises'; + +@Injectable() +export class SetUserGuard implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + await setTimeout(500); + const req = context.switchToHttp().getRequest(); + req.user = 'set'; + return true; + } +}