Skip to content

Commit

Permalink
feat: add option to OrGuard to customize the thrown error
Browse files Browse the repository at this point in the history
  • Loading branch information
p-dim-popov authored and jmcdo29 committed Dec 10, 2024
1 parent c050d97 commit f6b8016
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 9 deletions.
3 changes: 3 additions & 0 deletions packages/or-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ OrGuard(guards: Array<Type<CanActivate> | InjectionToken>, orGuardOptions?: OrGu
interface OrGuardOptions {
throwOnFirstError?: boolean;
throwLastError?: boolean;
throwError?: object | ((errors: unknown[]) => unknown);
}
```

Expand All @@ -61,6 +62,8 @@ interface OrGuardOptions {
handled with `return false` or just thrown. The default value is `false`. If
this is set to `true`, the **last** error encountered will lead to the same
error being thrown.
- `throwError`: provide a custom error to throw if all guards fail or provide a function
to receive all encountered errors and return a custom error to throw.

> **Note**: guards are ran in a non-deterministic order. All guard returns are
> transformed into Observables and ran concurrently to ensure the fastest
Expand Down
23 changes: 18 additions & 5 deletions packages/or-guard/src/lib/or.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import {
of,
OperatorFunction,
throwError,
pipe,
pipe, tap
} from 'rxjs';
import { catchError, last, mergeMap, takeWhile } from 'rxjs/operators';

interface OrGuardOptions {
throwOnFirstError?: boolean;
throwLastError?: boolean;
throwError?: object | ((errors: unknown[]) => unknown)
}

export function OrGuard(
Expand All @@ -39,13 +40,25 @@ export function OrGuard(
const canActivateReturns: Array<Observable<boolean>> = this.guards.map(
(guard) => this.deferGuard(guard, context)
);
const errors: unknown[] = [];
return from(canActivateReturns).pipe(
mergeMap((obs) => {
return obs.pipe(this.handleError());
}),
mergeMap((obs) => obs.pipe(this.handleError())),
tap(({ error }) => errors.push(error)),
takeWhile(({ result }) => result === false, true),
last(),
concatMap(({ result, error }) => result === false && orGuardOptions?.throwLastError && error ? throwError(() => error) : of(result))
concatMap(({ result }) => {
if (result === false) {
if (orGuardOptions?.throwLastError) {
return throwError(() => errors.at(-1))
}

if (orGuardOptions?.throwError) {
return throwError(() => typeof orGuardOptions.throwError === 'function' ? orGuardOptions.throwError(errors) : orGuardOptions.throwError)
}
}

return of(result);
})
);
}

Expand Down
14 changes: 13 additions & 1 deletion packages/or-guard/test/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Controller, Get, UnauthorizedException, UseGuards } from '@nestjs/common';

import { AndGuard, OrGuard } from '../src';
import { ObsGuard } from './obs.guard';
Expand Down Expand Up @@ -35,6 +35,18 @@ export class AppController {
return this.message;
}

@UseGuards(OrGuard([ThrowGuard, ThrowGuard], { throwError: new UnauthorizedException('Should provide either "x-api-key" header or query') }))
@Get('throw-custom')
getThrowGuardThrowCustom() {
return this.message;
}

@UseGuards(OrGuard([ThrowGuard, ThrowGuard], { throwError: (errors) => new UnauthorizedException((errors as { message?: string }[]).filter(error => error.message).join(', ')) }))
@Get('throw-custom-narrow')
getThrowGuardThrowCustomNarrow() {
return this.message;
}

@UseGuards(OrGuard(['SyncAndProm', ObsGuard]))
@Get('logical-and')
getLogicalAnd() {
Expand Down
46 changes: 43 additions & 3 deletions packages/or-guard/test/or.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,12 @@ describe('OrGuard and AndGuard Integration Test', () => {
});
describe('throw-last', () => {
/**
* OrGuard([SyncGuard, ThrowGuard], { throwLastError: true})
* OrGuard([SyncGuard, ThrowGuard], { throwLastError: true })
*
* | Sync | Throw | Final |
* | - | - | - |
* | true | UnauthorizedException | false |
* | false | UnauthorizedException | false |
* | true | UnauthorizedException | true |
* | false | UnauthorizedException | UnauthorizedException |
*/
it('should throw the last error', async () => {
return supertest(app.getHttpServer())
Expand All @@ -204,6 +204,46 @@ describe('OrGuard and AndGuard Integration Test', () => {
});
});
});
describe('throw-custom', () => {
/**
* OrGuard([ThrowGuard, ThrowGuard], { throwError: new UnauthorizedException('Should provide either "x-api-key" header or query') })
*
* | Throw | Throw | Final |
* | - | - | - |
* | UnauthorizedException | UnauthorizedException | object |
* | UnauthorizedException | UnauthorizedException | object |
*/
it('should throw the custom error', async () => {
return supertest(app.getHttpServer())
.get('/throw-custom')
.expect(401)
.expect(({ body }) => {
expect(body).toEqual(
expect.objectContaining({ message: 'Should provide either "x-api-key" header or query' })
);
});
});
});
describe('throw-custom-narrow', () => {
/**
* OrGuard([ThrowGuard, ThrowGuard], { throwError: (errors) => new UnauthorizedException((errors as { message?: string }[]).filter(error => error.message).join(', ')) })
*
* | Throw | Throw | Final |
* | - | - | - |
* | UnauthorizedException | UnauthorizedException | UnauthorizedException |
* | UnauthorizedException | UnauthorizedException | unknown |
*/
it('should throw the custom error', async () => {
return supertest(app.getHttpServer())
.get('/throw-custom-narrow')
.expect(401)
.expect(({ body }) => {
expect(body).toEqual(
expect.objectContaining({ message: 'UnauthorizedException: ThrowGuard, UnauthorizedException: ThrowGuard' })
);
});
});
});
});
}
);
Expand Down

0 comments on commit f6b8016

Please sign in to comment.