Skip to content

Commit

Permalink
feat: allow for and guard guards to be ran sequentially (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcdo29 authored Feb 7, 2024
2 parents e0951a8 + 3a767b5 commit b210cba
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/late-nails-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@nest-lab/or-guard': minor
---

Allow for AndGuard guards to be ran in sequential order
3 changes: 1 addition & 2 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"firsttris.vscode-jest-runner"
"dbaeumer.vscode-eslint"
]
}
11 changes: 7 additions & 4 deletions packages/or-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,20 @@ And this library will set up the handling of the logic for
under the hood.

```ts
AndGuard(guards: Array<Type<CanActivate> | InjectionToken>, orGuardOptions?: OrGuardOptions): CanActivate
AndGuard(guards: Array<Type<CanActivate> | 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;
}
```

Expand Down
17 changes: 9 additions & 8 deletions packages/or-guard/src/lib/and.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type<CanActivate> | InjectionToken>,
orGuardOptions?: OrGuardOptions
orGuardOptions?: AndGuardOptions
) {
class AndMixinGuard implements CanActivate {
private guards: CanActivate[] = [];
constructor(@Inject(ModuleRef) private readonly modRef: ModuleRef) {}
canActivate(context: ExecutionContext): Observable<boolean> {
this.guards = guards.map((guard) => this.modRef.get(guard));
const canActivateReturns: Array<Observable<boolean>> = this.guards.map(
(guard) => this.deferGuard(guard, context)
);
const canActivateReturns: Array<() => Observable<boolean>> =
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()
Expand Down
16 changes: 15 additions & 1 deletion packages/or-guard/test/app.controller.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
}
}
4 changes: 4 additions & 0 deletions packages/or-guard/test/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -14,6 +16,8 @@ import { ThrowGuard } from './throw.guard';
SyncGuard,
PromGuard,
ThrowGuard,
SetUserGuard,
ReadUserGuard,
{
provide: 'SyncAndProm',
useClass: AndGuard([SyncGuard, PromGuard]),
Expand Down
22 changes: 20 additions & 2 deletions packages/or-guard/test/or.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 |
* | - | - | - | - |
Expand Down Expand Up @@ -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);
});
});
});
12 changes: 12 additions & 0 deletions packages/or-guard/test/read-user.guard.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> | Observable<boolean> {
const req = context.switchToHttp().getRequest();
return !!req.user;
}
}
12 changes: 12 additions & 0 deletions packages/or-guard/test/set-user.guard.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
await setTimeout(500);
const req = context.switchToHttp().getRequest();
req.user = 'set';
return true;
}
}

0 comments on commit b210cba

Please sign in to comment.