Skip to content

Commit d58c628

Browse files
committed
feat(validation): migrate to Zod in @nbw/validation; drop class-validator
- Add env, Discord strategy, and song form schemas to @nbw/validation - Wire ConfigModule to validateEnv; parse user/song/query payloads with Zod - Replace DTO class helpers with mappers in song.util; re-export validation from @nbw/database for consumers - Frontend: import song form schemas from @nbw/validation; remove local SongForm.zod - Remove class-validator/class-transformer and global ValidationPipe from backend - Update specs; note bun test may need workspace resolution fixes for @nbw/config
1 parent 144a9eb commit d58c628

63 files changed

Lines changed: 887 additions & 702 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@encode42/nbs.js": "^5.0.2",
2828
"@nbw/config": "workspace:*",
2929
"@nbw/database": "workspace:*",
30+
"@nbw/validation": "workspace:*",
3031
"@nbw/song": "workspace:*",
3132
"@nbw/sounds": "workspace:*",
3233
"@nbw/thumbnail": "workspace:*",
@@ -44,10 +45,10 @@
4445
"axios": "^1.13.2",
4546
"bcryptjs": "^3.0.3",
4647
"class-transformer": "^0.5.1",
47-
"class-validator": "^0.14.3",
4848
"esm": "^3.2.25",
4949
"express": "^5.2.1",
5050
"mongoose": "^9.0.1",
51+
"nestjs-zod": "^5.0.1",
5152
"multer": "2.1.1",
5253
"nanoid": "^5.1.6",
5354
"passport": "^0.7.0",

apps/backend/scripts/build.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,6 @@ const build = async () => {
5151
await Bun.$`rm -rf dist`;
5252

5353
const optionalRequirePackages = [
54-
'class-transformer',
55-
'class-transformer/storage',
56-
'class-validator',
5754
'@nestjs/microservices',
5855
'@nestjs/websockets',
5956
'@fastify/static',
@@ -76,8 +73,11 @@ const build = async () => {
7673
}),
7774
'@nbw/config',
7875
'@nbw/database',
76+
'@nbw/validation',
7977
'@nbw/song',
8078
'@nbw/sounds',
79+
// @nestjs/swagger → @nestjs/mapped-types requires class-transformer metadata storage; bundler mis-resolves subpaths
80+
'class-transformer',
8181
],
8282
splitting: true,
8383
});

apps/backend/src/app.module.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Logger, Module } from '@nestjs/common';
22
import { ConfigModule, ConfigService } from '@nestjs/config';
3-
import { APP_GUARD } from '@nestjs/core';
3+
import { APP_GUARD, APP_PIPE } from '@nestjs/core';
44
import { MongooseModule, MongooseModuleFactoryOptions } from '@nestjs/mongoose';
55
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
6+
import { ZodValidationPipe } from 'nestjs-zod';
67
import { MailerModule } from '@nestjs-modules/mailer';
78
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
89

910
import { AuthModule } from './auth/auth.module';
10-
import { validate } from './config/EnvironmentVariables';
11+
import { validateEnv } from '@nbw/validation';
1112
import { EmailLoginModule } from './email-login/email-login.module';
1213
import { FileModule } from './file/file.module';
1314
import { ParseTokenPipe } from './lib/parseToken';
@@ -21,7 +22,7 @@ import { UserModule } from './user/user.module';
2122
ConfigModule.forRoot({
2223
isGlobal: true,
2324
envFilePath: ['.env.test', '.env.development', '.env.production'],
24-
validate,
25+
validate: validateEnv,
2526
}),
2627
//DatabaseModule,
2728
MongooseModule.forRootAsync({
@@ -82,6 +83,10 @@ import { UserModule } from './user/user.module';
8283
controllers: [],
8384
providers: [
8485
ParseTokenPipe,
86+
{
87+
provide: APP_PIPE,
88+
useClass: ZodValidationPipe,
89+
},
8590
{
8691
provide: APP_GUARD,
8792
useClass: ThrottlerGuard,

apps/backend/src/auth/auth.service.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ describe('AuthService', () => {
278278
profileImage: 'http://example.com/photo.jpg',
279279
};
280280

281+
mockUserService.generateUsername.mockResolvedValue('testuser');
281282
mockUserService.findByEmail.mockResolvedValue(null);
282283
mockUserService.create.mockResolvedValue({ id: 'new-user-id' });
283284

apps/backend/src/auth/auth.service.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import axios from 'axios';
44
import type { CookieOptions, Request, Response } from 'express';
55
import ms from 'ms';
66

7-
import { CreateUser } from '@nbw/database';
87
import type { UserDocument } from '@nbw/database';
8+
import { createUserSchema } from '@nbw/validation';
99
import { UserService } from '@server/user/user.service';
1010

1111
import { DiscordUser } from './types/discordProfile';
@@ -90,10 +90,9 @@ export class AuthService {
9090

9191
private async createNewUser(user: Profile) {
9292
const { username, email, profileImage } = user;
93-
const baseUsername = username;
94-
const newUsername = await this.userService.generateUsername(baseUsername);
93+
const newUsername = await this.userService.generateUsername(username);
9594

96-
const newUser = new CreateUser({
95+
const newUser = createUserSchema.parse({
9796
username: newUsername,
9897
email: email,
9998
profileImage: profileImage,
@@ -220,8 +219,6 @@ export class AuthService {
220219
return null;
221220
}
222221

223-
const user = await this.userService.findByID(decoded.id);
224-
225-
return user;
222+
return await this.userService.findByID(decoded.id);
226223
}
227224
}

apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.

apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
import type { DiscordStrategyConfig } from '@nbw/validation';
12
import { VerifyFunction } from 'passport-oauth2';
2-
3-
import { DiscordStrategyConfig } from './DiscordStrategyConfig';
43
import DiscordStrategy from './Strategy';
54
import { DiscordPermissionScope, Profile } from './types';
65

@@ -42,16 +41,15 @@ describe('DiscordStrategy', () => {
4241
prompt: 'consent',
4342
};
4443

45-
await expect(strategy['validateConfig'](config)).resolves.toBeUndefined();
44+
expect(() => strategy['validateConfig'](config)).not.toThrow();
4645
});
4746

4847
it('should make API request', async () => {
49-
const mockGet = jest.fn((url, accessToken, callback) => {
50-
callback(null, JSON.stringify({ id: '123' }));
48+
strategy['_oauth2'].get = jest.fn((url, accessToken, callback) => {
49+
// oauth2 `dataCallback` typings omit `null`; runtime passes null on success.
50+
callback(null as never, JSON.stringify({ id: '123' }));
5151
});
5252

53-
strategy['_oauth2'].get = mockGet;
54-
5553
const result = await strategy['makeApiRequest']<{ id: string }>(
5654
'https://discord.com/api/users/@me',
5755
'test-access-token',

apps/backend/src/auth/strategies/discord.strategy/Strategy.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { Logger } from '@nestjs/common';
2-
import { plainToClass } from 'class-transformer';
3-
import { validateOrReject } from 'class-validator';
2+
import {
3+
discordStrategyConfigSchema,
4+
type DiscordStrategyConfig,
5+
} from '@nbw/validation';
46
import {
57
InternalOAuthError,
68
Strategy as OAuth2Strategy,
79
StrategyOptions as OAuth2StrategyOptions,
810
VerifyCallback,
911
VerifyFunction,
1012
} from 'passport-oauth2';
11-
12-
import { DiscordStrategyConfig } from './DiscordStrategyConfig';
1313
import {
1414
Profile,
1515
ProfileConnection,
@@ -47,20 +47,19 @@ export default class Strategy extends OAuth2Strategy {
4747
);
4848

4949
this.validateConfig(options);
50-
this.scope = options.scope;
50+
this.scope = options.scope as ScopeType;
5151
this.scopeDelay = options.scopeDelay ?? 0;
5252
this.fetchScopeEnabled = options.fetchScope ?? true;
5353
this._oauth2.useAuthorizationHeaderforGET(true);
5454
this.prompt = options.prompt;
5555
}
5656

57-
private async validateConfig(config: DiscordStrategyConfig): Promise<void> {
57+
private validateConfig(config: DiscordStrategyConfig): void {
5858
try {
59-
const validatedConfig = plainToClass(DiscordStrategyConfig, config);
60-
await validateOrReject(validatedConfig);
61-
} catch (errors) {
62-
this.logger.error(errors);
63-
throw new Error(`Configuration validation failed: ${errors}`);
59+
discordStrategyConfigSchema.parse(config);
60+
} catch (error) {
61+
this.logger.error(error);
62+
throw new Error(`Configuration validation failed: ${String(error)}`);
6463
}
6564
}
6665

0 commit comments

Comments
 (0)