feat: implement mailer support
feat: basic structure
This commit is contained in:
parent
60532e83cf
commit
ca8ee728fb
26 changed files with 2410 additions and 285 deletions
92
README.md
92
README.md
|
@ -1,73 +1,19 @@
|
|||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||
</p>
|
||||
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
|
||||
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ pnpm run start
|
||||
|
||||
# watch mode
|
||||
$ pnpm run start:dev
|
||||
|
||||
# production mode
|
||||
$ pnpm run start:prod
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ pnpm run test
|
||||
|
||||
# e2e tests
|
||||
$ pnpm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ pnpm run test:cov
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](LICENSE).
|
||||
## Waterwolf Identity Solution
|
||||
|
||||
This is a re-imagination on how BOOP works with an optimized OAUTH 2.0 flow. This will hook into the existing user database to read usernames and passwords.
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Use pnmp to install the required packages.
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
|
65
package.json
65
package.json
|
@ -17,37 +17,52 @@
|
|||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"prepare": "ts-patch install && typia patch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
"@atech/postal": "^1.0.0",
|
||||
"@nestjs/common": "^10.3.10",
|
||||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^10.3.10",
|
||||
"@nestjs/platform-express": "^10.3.10",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"mysql2": "^3.10.2",
|
||||
"nanoid": "^5.0.7",
|
||||
"nestjs-postal-client": "^0.0.5",
|
||||
"oidc-provider": "^8.5.1",
|
||||
"pug": "^3.0.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sequelize": "^6.37.3",
|
||||
"sequelize-typescript": "^2.1.6",
|
||||
"typia": "^6.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"prettier": "^3.0.0",
|
||||
"@nestjs/cli": "^10.4.2",
|
||||
"@nestjs/schematics": "^10.1.2",
|
||||
"@nestjs/testing": "^10.3.10",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/sequelize": "^4.28.20",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-jest": "^29.2.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-patch": "^3.2.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
@ -66,4 +81,4 @@
|
|||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
}
|
2160
pnpm-lock.yaml
2160
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,30 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { PostalClientService } from 'nestjs-postal-client';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
constructor(
|
||||
private readonly appService: AppService,
|
||||
private readonly postal: PostalClientService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('email_test')
|
||||
async getEmailTest(): Promise<any> {
|
||||
await this.postal.sendMessage({
|
||||
to: ['kakious@kakio.us'],
|
||||
subject: 'Test email',
|
||||
from: 'kakious@kakio.us',
|
||||
plain_body: 'This is a test email',
|
||||
});
|
||||
|
||||
return {
|
||||
message: 'Email sent',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import config from './config/config';
|
||||
import { MailModule } from './mail/mail.module';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
cache: true,
|
||||
isGlobal: true,
|
||||
load: [config],
|
||||
}),
|
||||
MailModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
|
|
10
src/auth/auth.module.ts
Normal file
10
src/auth/auth.module.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [ConfigService],
|
||||
exports: [],
|
||||
})
|
||||
export class AuthModule {}
|
0
src/auth/controllers/auth.controller.ts
Normal file
0
src/auth/controllers/auth.controller.ts
Normal file
0
src/auth/controllers/oidc.controller.ts
Normal file
0
src/auth/controllers/oidc.controller.ts
Normal file
0
src/auth/mailers/postal.service.ts
Normal file
0
src/auth/mailers/postal.service.ts
Normal file
0
src/auth/oidc/adapter.ts
Normal file
0
src/auth/oidc/adapter.ts
Normal file
26
src/auth/oidc/helper/nanoid.helper.ts
Normal file
26
src/auth/oidc/helper/nanoid.helper.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
let nanoidImport: typeof import('nanoid') | null = null;
|
||||
|
||||
const getNanoId = async () => {
|
||||
if (!nanoidImport) {
|
||||
// This is an async import as it's only esm and we need to use it in a commonjs environment because
|
||||
// nestjs doesn't work very well with esm yet.
|
||||
nanoidImport = await (eval(
|
||||
"import('nanoid')",
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
) as Promise<typeof import('nanoid')>);
|
||||
}
|
||||
|
||||
return nanoidImport;
|
||||
};
|
||||
|
||||
const generateId = async (length = 21, charset?: string): Promise<string> => {
|
||||
const nanoid = await getNanoId();
|
||||
|
||||
if (charset) {
|
||||
return nanoid.customAlphabet(charset, length)();
|
||||
}
|
||||
|
||||
return nanoid.nanoid(length);
|
||||
};
|
||||
|
||||
export default generateId;
|
13
src/auth/oidc/types/accessToken.type.ts
Normal file
13
src/auth/oidc/types/accessToken.type.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export interface AccessToken {
|
||||
iat: number;
|
||||
exp: number;
|
||||
accountId?: string;
|
||||
expiresWithSession: boolean;
|
||||
grantId: string;
|
||||
gty: string;
|
||||
sessionUid: string;
|
||||
clientId: string;
|
||||
scope: string;
|
||||
kind: string;
|
||||
jti: string;
|
||||
}
|
15
src/auth/oidc/types/adapter.type.ts
Normal file
15
src/auth/oidc/types/adapter.type.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export enum adapterTypes {
|
||||
Session = 'Session',
|
||||
AccessToken = 'AccessToken',
|
||||
AuthorizationCode = 'AuthorizationCode',
|
||||
RefreshToken = 'RefreshToken',
|
||||
DeviceCode = 'DeviceCode',
|
||||
ClientCredentials = 'ClientCredentials',
|
||||
Client = 'Client',
|
||||
InitalAccessToken = 'InitalAccessToken',
|
||||
RegistrationAccessToken = 'RegistrationAccessToken',
|
||||
Interaction = 'Interaction',
|
||||
ReplayDetection = 'ReplayDetection',
|
||||
PushedAuthorizationRequest = 'PushedAuthorizationRequest',
|
||||
Grant = 'Grant',
|
||||
}
|
77
src/auth/oidc/types/interaction.type.ts
Normal file
77
src/auth/oidc/types/interaction.type.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
export interface Interaction {
|
||||
readonly kind: string;
|
||||
|
||||
iat: number;
|
||||
exp: number;
|
||||
|
||||
session?: InteractionSession | undefined;
|
||||
|
||||
params: InteractionParams;
|
||||
prompt: PromptDetail;
|
||||
result?: InteractionResults | undefined;
|
||||
returnTo: string;
|
||||
deviceCode?: string | undefined;
|
||||
|
||||
trusted?: string[] | undefined;
|
||||
uid: string;
|
||||
lastSubmission?: InteractionResults | undefined;
|
||||
grantId?: string | undefined;
|
||||
|
||||
save(ttl: number): Promise<string>;
|
||||
persist(): Promise<string>;
|
||||
}
|
||||
|
||||
export interface InteractionSession {
|
||||
accountId: string;
|
||||
uid: string;
|
||||
cookie: string;
|
||||
acr?: string | undefined;
|
||||
amr?: string[] | undefined;
|
||||
}
|
||||
|
||||
export type UnknownObject = Record<string, unknown>;
|
||||
|
||||
export interface PromptDetail {
|
||||
name: Prompts | string;
|
||||
reasons: string[];
|
||||
details: InteractionDetails;
|
||||
}
|
||||
|
||||
export enum Prompts {
|
||||
login = 'login',
|
||||
consent = 'consent',
|
||||
select_account = 'select_account',
|
||||
}
|
||||
|
||||
export interface InteractionResults {
|
||||
login?:
|
||||
| {
|
||||
remember?: boolean | undefined;
|
||||
accountId: string;
|
||||
ts?: number | undefined;
|
||||
amr?: string[] | undefined;
|
||||
acr?: string | undefined;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
consent?:
|
||||
| {
|
||||
grantId?: string | undefined;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface InteractionParams extends UnknownObject {
|
||||
scope?: string | undefined;
|
||||
client_id?: string | undefined;
|
||||
}
|
||||
|
||||
export interface InteractionDetails extends UnknownObject {
|
||||
missingOIDCScope?: string[] | undefined;
|
||||
missingOIDCClaims?: string[] | undefined;
|
||||
missingResourceScopes?: UnknownObject;
|
||||
}
|
59
src/auth/oidc/types/session.type.ts
Normal file
59
src/auth/oidc/types/session.type.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
export interface VerifiedSessionFromRequest {
|
||||
session: Session;
|
||||
//USER: User;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
iat: number;
|
||||
exp: number;
|
||||
uid: string;
|
||||
jti: string;
|
||||
|
||||
accountId?: string | undefined;
|
||||
acr?: string | undefined;
|
||||
amr?: string[] | undefined;
|
||||
loginTs?: number | undefined;
|
||||
transient?: boolean | undefined;
|
||||
state?: UnknownObject | undefined;
|
||||
authorizations?: Record<string, ClientAuthorizationState> | undefined;
|
||||
clientId?: string | undefined;
|
||||
|
||||
authTime(): string | void;
|
||||
past(age: number): boolean;
|
||||
|
||||
ensureClientContainer(clientId: string): void;
|
||||
loginAccount(details: {
|
||||
accountId: string;
|
||||
acr?: string | undefined;
|
||||
amr?: string[] | undefined;
|
||||
loginTs?: number | undefined;
|
||||
transient?: boolean | undefined;
|
||||
}): void;
|
||||
authorizationFor(clientId: string): ClientAuthorizationState | void;
|
||||
sidFor(clientId: string): string;
|
||||
sidFor(clientId: string, value: string): void;
|
||||
grantIdFor(clientId: string): string;
|
||||
grantIdFor(clientId: string, value: string): void;
|
||||
|
||||
save(ttl: number): Promise<string>;
|
||||
persist(): Promise<string>;
|
||||
destroy(): Promise<void>;
|
||||
resetIdentifier(): void;
|
||||
}
|
||||
|
||||
export type UnknownObject = Record<string, unknown>;
|
||||
|
||||
export interface ClientAuthorizationState {
|
||||
persistsLogout?: boolean | undefined;
|
||||
sid?: string | undefined;
|
||||
grantId?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* interface BaseModel {
|
||||
jti: string;
|
||||
kind: string;
|
||||
iat?: number | undefined;
|
||||
exp?: number | undefined;
|
||||
}
|
||||
*/
|
0
src/auth/resources/emails/registration.pug
Normal file
0
src/auth/resources/emails/registration.pug
Normal file
0
src/auth/resources/views/layout.pug
Normal file
0
src/auth/resources/views/layout.pug
Normal file
0
src/auth/resources/views/login.pug
Normal file
0
src/auth/resources/views/login.pug
Normal file
0
src/auth/services/auth.service.ts
Normal file
0
src/auth/services/auth.service.ts
Normal file
0
src/auth/services/core.service.ts
Normal file
0
src/auth/services/core.service.ts
Normal file
6
src/config/config.ts
Normal file
6
src/config/config.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default async () => ({
|
||||
mail: {
|
||||
baseUrl: process.env.MAIL_BASE_URL,
|
||||
apiKey: process.env.MAIL_API_KEY,
|
||||
},
|
||||
});
|
19
src/mail/mail.module.ts
Normal file
19
src/mail/mail.module.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { PostalClientModule } from 'nestjs-postal-client';
|
||||
import postalConfig from './postal.config';
|
||||
import { PostalConfigService } from './postal_config.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PostalClientModule.registerAsync({
|
||||
imports: [ConfigModule.forFeature(postalConfig)],
|
||||
useClass: PostalConfigService,
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [ConfigService],
|
||||
exports: [PostalClientModule],
|
||||
})
|
||||
export class MailModule {}
|
23
src/mail/postal.config.ts
Normal file
23
src/mail/postal.config.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { registerAs } from '@nestjs/config';
|
||||
import type { tags } from 'typia';
|
||||
import typia from 'typia';
|
||||
|
||||
import { validateOrThrow } from '../util/validation.util';
|
||||
|
||||
export const POSTAL_CONFIG_KEY = 'postal';
|
||||
|
||||
const validateEqualsPostalConfig = typia.createValidateEquals<PostalConfig>();
|
||||
|
||||
export interface PostalConfig {
|
||||
POSTAL_BASE_URL: string & tags.Format<'url'>;
|
||||
POSTAL_API_KEY: string & tags.MinLength<1>;
|
||||
}
|
||||
|
||||
export default registerAs(
|
||||
POSTAL_CONFIG_KEY,
|
||||
(): PostalConfig =>
|
||||
validateOrThrow(validateEqualsPostalConfig, {
|
||||
POSTAL_BASE_URL: process.env['POSTAL_BASE_URL'],
|
||||
POSTAL_API_KEY: process.env['POSTAL_API_KEY'],
|
||||
}),
|
||||
);
|
32
src/mail/postal_config.service.ts
Normal file
32
src/mail/postal_config.service.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import type {
|
||||
PostalModuleOptions,
|
||||
PostalModuleOptionsFactory,
|
||||
} from 'nestjs-postal-client';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import type { PostalConfig } from './postal.config';
|
||||
import { POSTAL_CONFIG_KEY } from './postal.config';
|
||||
|
||||
@Injectable()
|
||||
export class PostalConfigService implements PostalModuleOptionsFactory {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
createPostalOptions(): Promise<PostalModuleOptions> | PostalModuleOptions {
|
||||
const postalConfig =
|
||||
this.configService.getOrThrow<PostalConfig>(POSTAL_CONFIG_KEY);
|
||||
|
||||
return {
|
||||
http: {
|
||||
headers: {
|
||||
'User-Agent': 'OIDC-Client',
|
||||
},
|
||||
},
|
||||
postal: {
|
||||
baseURL: postalConfig.POSTAL_BASE_URL,
|
||||
apiKey: postalConfig.POSTAL_API_KEY,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
55
src/util/validation.util.ts
Normal file
55
src/util/validation.util.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import type {
|
||||
ClassConstructor,
|
||||
ClassTransformOptions,
|
||||
} from 'class-transformer';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import type { ValidatorOptions } from 'class-validator';
|
||||
import { validate } from 'class-validator';
|
||||
import type { IValidation } from 'typia';
|
||||
|
||||
export const VALIDATOR_OPTIONS: ValidatorOptions = {
|
||||
forbidNonWhitelisted: true,
|
||||
forbidUnknownValues: true,
|
||||
whitelist: true,
|
||||
};
|
||||
|
||||
export const TRANSFORMER_OPTIONS: ClassTransformOptions = {
|
||||
excludeExtraneousValues: true,
|
||||
exposeDefaultValues: true,
|
||||
};
|
||||
|
||||
export async function transformAndValidate<T extends object>(
|
||||
cls: ClassConstructor<T>,
|
||||
data: unknown,
|
||||
): Promise<T> {
|
||||
const instance = plainToClass(cls, data, TRANSFORMER_OPTIONS);
|
||||
const errors = await validate(instance, VALIDATOR_OPTIONS);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.toString());
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
function stringifyIError(error: IValidation.IError) {
|
||||
return ` - Invalid type on ${error.path}, expect to be ${error.expected}, was ${JSON.stringify(
|
||||
error.value,
|
||||
)}`;
|
||||
}
|
||||
|
||||
type PartialUndefined<T> = {
|
||||
[P in keyof T]?: T[P] | undefined;
|
||||
};
|
||||
|
||||
export function validateOrThrow<T>(
|
||||
validator: (input: unknown) => IValidation<T>,
|
||||
input: PartialUndefined<T>,
|
||||
) {
|
||||
const result = validator(input);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
const body = result.errors.map(stringifyIError).join('\n');
|
||||
throw new Error(`Validation errors:\n${body}`);
|
||||
}
|
|
@ -12,10 +12,15 @@
|
|||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"plugins": [
|
||||
{
|
||||
"transform": "typia/lib/transform"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue