Skip to content

Commit

Permalink
feat: new and guard (#32)
Browse files Browse the repository at this point in the history
With the new`AndGuard`, devs can now set up cases where they want to use
an `OrGuard` to handle an `(A && B) || C` kind of condition, or any
other complex logical approach they'd like. Devs are also now able to
correctly pass injection tokens to the `AndGuard` and `OrGuard` and use
custom providers for their guards that are registered as providers
  • Loading branch information
jmcdo29 authored Dec 14, 2023
2 parents 8c65811 + bc9bbdf commit f1fe0cb
Show file tree
Hide file tree
Showing 12 changed files with 2,141 additions and 1,940 deletions.
9 changes: 9 additions & 0 deletions .changeset/tame-eels-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@nest-lab/or-guard': minor
---

Add a new AndGuard to handle complex logical cases

With the new `AndGuard` it is now possible to create logical cases like
`(A && B) || C` using the `OrGuard` and a composite guard approach. Check out
the docs for more info
6 changes: 4 additions & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"singleQuote": true
}
"printWidth": 80,
"singleQuote": true,
"proseWrap": "always"
}
95 changes: 81 additions & 14 deletions packages/or-guard/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# or-guard
# @nest-lab/or-guard

This library contains a single guard that allows for checking multiple guards and if **any one of them passes** the entire request will be considered authenticated.
This library contains a two guards that allows for checking multiple guards and
creating complex logical statements based on the results of those guards for if
the request should be completed or not.

## Installation

Expand All @@ -12,39 +14,104 @@ yarn add @nest-lab/or-guard
pnpm i @nest-lab/or-guard
```

## Usage
## OrGuard

To use the `OrGuard`, there are a couple of things that need to happen, due to how the guard resolves the guards it's going to be using.
To use the `OrGuard`, there are a couple of things that need to happen, due to
how the guard resolves the guards it's going to be using.

First, make sure to add all the guards the `OrGuard` will be using to the current module's `providers` array. Enhancer in Nest are just specialized providers after all. This will allow the `OrGuard` to use a `ModuleRef` to get these guards.
First, make sure to add all the guards the `OrGuard` will be using to the
current module's `providers` array. Enhancer in Nest are just specialized
providers after all. This will allow the `OrGuard` to use a `ModuleRef` to get
these guards. The guards can either be registered directly as providers, or set
up as custom providers and you may use an injection token reference. Make sure,
that if you use a custom provider, the _instance_ of the guard is what is tied
to the token, not the reference to the class.

Second, make sure **none** of these guards are `REQUEST` or `TRANSIENT` scoped, as this **will** make the `OrGuard` throw an error.
Second, make sure **none** of these guards are `REQUEST` or `TRANSIENT` scoped,
as this **will** make the `OrGuard` throw an error.

Third, make use of it! The `OrGuard` takes in an array of guard to use for the first parameter, and an optional second parameter for options as described below.
Third, make use of it! The `OrGuard` takes in an array of guard to use for the
first parameter, and an optional second parameter for options as described
below.

> **important**: for Nest v7, use `@nest-lab/[email protected]`, for Nest v8, please use v2
> **important**: for Nest v7, use `@nest-lab/[email protected]`, for Nest v8,
> please use v2
```ts
OrGuard(guards: CanActivate[], orGuardOptions?: OrGuardOptions): CanActivate
OrGuard(guards: Array<Type<CanActivate> | InjectionToken>, orGuardOptions?: OrGuardOptions): CanActivate
```

- `guards`: an array of guards for the `OrGuard` to resolve and test
- `orGuardOptions`: an optional object with properties to modify how the `OrGuard` functions
- `guards`: an array of guards or injection tokens for the `OrGuard` to resolve
and test
- `orGuardOptions`: an optional object with properties to modify how the
`OrGuard` functions

```ts
interface OrGuardOptions {
throwOnFirstError?: boolean;
}
```

- `throwOnFirstError`: a boolean to tell the `OrGuard` whether to throw if an error is encountered or if the error should be considered a `return false`. The default value is `false`. If this is set to `true`, the **first** error encountered will lead to the same error being thrown.
- `throwOnFirstError`: a boolean to tell the `OrGuard` whether to throw if an
error is encountered or if the error should be considered a `return false`.
The default value is `false`. If this is set to `true`, the **first** error
encountered will lead to the same error being thrown.

> **Note**: guards are ran in a non-deterministic order. All guard returns are
> transformed into Observables and ran concurrently to ensure the fastest
> response time possible.
## AndGuard

Just like the `OrGuard`, you can create a logic grouping of situations that
should pass. This is Nest's default when there are multiple guards passed to the
`@UseGuards()` decorator; however, there are situations where it would be useful
to use an `AndGuard` inside of an `OrGuard` to be able to create logic like
`(A && B) || C`. With using an `AndGuard` inside of an `OrGuard`, you'll most
likely want to create a dedicated [custom provider][customprov] for the guard
like so:

```typescript
{
provide: AndGuardToken,
useClass: AndGuard([GuardA, GuardB])
}
```

With this added to the module's providers where you plan to use the related
`OrGuard` you can use the following in a controller or resolve:

```typescript
@UseGuards(OrGuard([AndGuardToken, GuardC]))
```

And this library will set up the handling of the logic for
`(GuardA && GuardB) || GuardC` without having to worry about the complexities
under the hood.

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

> **Note**: guards are ran in a non-deterministic order. All guard returns are transformed into Observables and ran concurrently to ensure the fastest response time possible.
```ts
interface OrGuardOptions {
throwOnFirstError?: boolean;
}
```

## Local Development

Feel free to pull down the repository and work locally. If any changes are made, please make sure tests are added to ensure the functionality works and nothing is broken.
Feel free to pull down the repository and work locally. If any changes are made,
please make sure tests are added to ensure the functionality works and nothing
is broken.

### Running unit tests

Run `nx test or-guard` to execute the unit tests via [Jest](https://jestjs.io).

[customprov]: https://docs.nestjs.com/fundamentals/custom-providers
4 changes: 4 additions & 0 deletions packages/or-guard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
"publishConfig": {
"access": "public"
},
"main": "src/index.js",
"files": [
"src"
],
"repository": {
"type": "github",
"url": "https://github.com/jmcdo29/nest-lab",
Expand Down
1 change: 1 addition & 0 deletions packages/or-guard/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './lib/and.guard';
export * from './lib/or.guard';
85 changes: 85 additions & 0 deletions packages/or-guard/src/lib/and.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
CanActivate,
ExecutionContext,
Inject,
InjectionToken,
mixin,
Type,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import {
defer,
from,
Observable,
of,
OperatorFunction,
throwError,
} from 'rxjs';
import { catchError, last, mergeMap, every } from 'rxjs/operators';

interface OrGuardOptions {
throwOnFirstError?: boolean;
}

export function AndGuard(
guards: Array<Type<CanActivate> | InjectionToken>,
orGuardOptions?: OrGuardOptions
) {
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)
);
return from(canActivateReturns).pipe(
mergeMap((obs) => {
return obs.pipe(this.handleError());
}),
every((val) => val === true),
last()
);
}

private deferGuard(
guard: CanActivate,
context: ExecutionContext
): Observable<boolean> {
return defer(() => {
const guardVal = guard.canActivate(context);
if (this.guardIsPromise(guardVal)) {
return from(guardVal);
}
if (this.guardIsObservable(guardVal)) {
return guardVal;
}
return of(guardVal);
});
}

private handleError(): OperatorFunction<boolean, boolean> {
return catchError((err) => {
if (orGuardOptions?.throwOnFirstError) {
return throwError(() => err);
}
return of(false);
});
}

private guardIsPromise(
guard: boolean | Promise<boolean> | Observable<boolean>
): guard is Promise<boolean> {
return !!(guard as Promise<boolean>).then;
}

private guardIsObservable(
guard: boolean | Observable<boolean>
): guard is Observable<boolean> {
return !!(guard as Observable<boolean>).pipe;
}
}

const Guard = mixin(AndMixinGuard);
return Guard as Type<CanActivate>;
}
7 changes: 4 additions & 3 deletions packages/or-guard/src/lib/or.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CanActivate,
ExecutionContext,
Inject,
InjectionToken,
mixin,
Type,
} from '@nestjs/common';
Expand All @@ -14,14 +15,14 @@ import {
OperatorFunction,
throwError,
} from 'rxjs';
import { catchError, last, mergeMap, takeWhile} from 'rxjs/operators';
import { catchError, last, mergeMap, takeWhile } from 'rxjs/operators';

interface OrGuardOptions {
throwOnFirstError?: boolean;
}

export function OrGuard(
guards: Type<CanActivate>[],
guards: Array<Type<CanActivate> | InjectionToken>,
orGuardOptions?: OrGuardOptions
) {
class OrMixinGuard implements CanActivate {
Expand Down Expand Up @@ -60,7 +61,7 @@ export function OrGuard(
private handleError(): OperatorFunction<boolean, boolean> {
return catchError((err) => {
if (orGuardOptions?.throwOnFirstError) {
return throwError(err);
return throwError(() => err);
}
return of(false);
});
Expand Down
6 changes: 6 additions & 0 deletions packages/or-guard/test/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ export class AppController {
getThrowGuardThrow() {
return this.message;
}

@UseGuards(OrGuard(['SyncAndProm', ObsGuard]))
@Get('logical-and')
getLogicalAnd() {
return this.message;
}
}
12 changes: 11 additions & 1 deletion packages/or-guard/test/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';

import { AndGuard } from '../src/';
import { AppController } from './app.controller';
import { ObsGuard } from './obs.guard';
import { PromGuard } from './prom.guard';
Expand All @@ -8,6 +9,15 @@ import { ThrowGuard } from './throw.guard';

@Module({
controllers: [AppController],
providers: [ObsGuard, SyncGuard, PromGuard, ThrowGuard],
providers: [
ObsGuard,
SyncGuard,
PromGuard,
ThrowGuard,
{
provide: 'SyncAndProm',
useClass: AndGuard([SyncGuard, PromGuard]),
},
],
})
export class AppModule {}
55 changes: 55 additions & 0 deletions packages/or-guard/test/or.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,61 @@ describe('Or Guard Integration Test', () => {
`(
'sync val $sync syncExpect $syncExpect',
({ sync, syncExpect }: { sync: boolean; syncExpect: boolean }) => {
describe.each`
prom | promExpect
${true} | ${true}
${false} | ${false}
`(
'prom val $prom promExpect $promExpect',
({ prom, promExpect }: { prom: boolean; promExpect: boolean }) => {
describe.each`
obs | obsExpect
${true} | ${true}
${false} | ${syncExpect && promExpect}
`(
'obs val $obs final expect $obsExpect',
({ obs, obsExpect }: { obs: boolean; obsExpect: boolean }) => {
let app: INestApplication;
beforeEach(async () => {
const testMod = await moduleConfig
.overrideProvider(SyncGuard)
.useValue({ canActivate: () => sync })
.overrideProvider(PromGuard)
.useValue({ canActivate: async () => prom })
.overrideProvider(ObsGuard)
.useValue({ canActivate: () => of(obs) })
.compile();
app = testMod.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
/**
* OrGuard([SyncGuard, PromGuard, ObsGuard])
*
* | Sync | Prom | Obs | Final |
* | - | - | - | - |
* | true | true | true | true |
* | true | true | false | true |
* | true | false | true | true |
* | true | false | false | false |
* | false | true | true | true |
* | false | true | false | false |
* | false | false | true | true |
* | false | false | false | false |
*/
it(`should make a request to the server and${
obsExpect ? ' ' : ' not '
}succeed`, async () => {
return supertest(app.getHttpServer())
.get('/logical-and')
.expect(obsExpect ? 200 : 403);
});
}
);
}
);
describe.each`
prom | promExpect
${true} | ${true}
Expand Down
2 changes: 1 addition & 1 deletion packages/or-guard/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"target": "es2021"
},
"exclude": ["**/*.spec.ts", "jest.config.ts"],
"include": ["**/*.ts"]
"include": ["src/**/*.ts"]
}
Loading

0 comments on commit f1fe0cb

Please sign in to comment.