From f61524bda1ac3c896c39bc762bccbc46532ce2de Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 10:33:18 -0300 Subject: [PATCH 01/21] refactor: update ESLint configuration to use TypeScript ESLint and integrate new plugins for improved linting and code quality --- eslint.config.js | 166 +++++++++++++++++++++++++---------------------- 1 file changed, 90 insertions(+), 76 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 5c33c8c8..41122f00 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,15 +1,11 @@ import js from '@eslint/js'; -import tseslint from '@typescript-eslint/eslint-plugin'; -import tsparser from '@typescript-eslint/parser'; -import prettierConfig from 'eslint-config-prettier'; -import prettierPlugin from 'eslint-plugin-prettier'; +import tseslint from 'typescript-eslint'; import globals from 'globals'; +import importPlugin from 'eslint-plugin-import'; +import unusedImportsPlugin from 'eslint-plugin-unused-imports'; -export default [ - // Base JavaScript configuration - js.configs.recommended, - - // Global ignore patterns +export default tseslint.config( + // Global ignores. { ignores: [ '**/node_modules/**', @@ -21,91 +17,109 @@ export default [ '**/*.config.ts', '**/generated/**', '.eslintrc.js', - '**/*.spec.ts', - '**/*.test.ts', ], }, + + // Apply base recommended configurations. + js.configs.recommended, + ...tseslint.configs.recommended, - // Universal TypeScript configuration for the entire monorepo + // A single, unified object for all custom rules and plugin configurations. { - files: ['**/*.ts', '**/*.tsx'], languageOptions: { - parser: tsparser, - parserOptions: { - ecmaVersion: 2021, - sourceType: 'module', - // Don't use project-based parsing to avoid config issues - ecmaFeatures: { - jsx: true, - }, - }, globals: { - // Universal globals that work everywhere ...globals.node, - ...globals.browser, - ...globals.es2021, - console: 'readonly', - process: 'readonly', - Buffer: 'readonly', - __dirname: 'readonly', - __filename: 'readonly', - global: 'readonly', - React: 'readonly', - JSX: 'readonly', + ...globals.es2021, // A modern equivalent of the 'es6' env }, }, plugins: { - '@typescript-eslint': tseslint, - prettier: prettierPlugin, + 'import': importPlugin, + 'unused-imports': unusedImportsPlugin, + }, + settings: { + 'import/resolver': { + typescript: { + // Point to all tsconfig.json files in your workspaces + project: [ + 'apps/*/tsconfig.json', + 'packages/*/tsconfig.json', + './tsconfig.json', // Also include the root tsconfig as a fallback + ], + }, + node: true, + }, + // Allow Bun built-in modules + 'import/core-modules': ['bun:test', 'bun:sqlite', 'bun'], }, rules: { - // Turn off rules that conflict with TypeScript - 'no-undef': 'off', // TypeScript handles this - 'no-unused-vars': 'off', // Use TypeScript version instead - 'no-redeclare': 'off', // TypeScript handles this better - - // Turn off rules that don't exist or cause issues - 'react-hooks/exhaustive-deps': 'off', - '@next/next/no-sync-scripts': 'off', - 'no-shadow-restricted-names': 'off', - - // TypeScript specific rules - simplified and lenient - '@typescript-eslint/no-unused-vars': [ + // Manually include rules from the import plugin's recommended configs. + ...importPlugin.configs.recommended.rules, + ...importPlugin.configs.typescript.rules, + + // Your custom rules from the original file. + 'no-console': 'warn', + 'max-len': ['error', { + code: 1024, + ignoreComments: true, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + }], + 'key-spacing': ['error', { + align: { + beforeColon: false, + afterColon: true, + on: 'colon', + }, + }], + '@typescript-eslint/no-unused-vars': 'off', // Disabled to allow unused-imports plugin to handle it. + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-require-imports': 'warn', + '@typescript-eslint/ban-ts-comment': 'warn', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ 'warn', { - argsIgnorePattern: '^_', + vars: 'all', varsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_', - ignoreRestSiblings: true, + args: 'after-used', + argsIgnorePattern: '^_', }, ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - - // Prettier integration - 'prettier/prettier': [ - 'warn', - { - endOfLine: 'auto', - trailingComma: 'all', + 'import/order': ['error', { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'unknown', + ], + 'pathGroups': [{ + pattern: '@/**', + group: 'internal', + }], + pathGroupsExcludedImportTypes: ['builtin'], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, }, - ], + }], + 'import/newline-after-import': 'error', + 'import/no-duplicates': 'error', - // Relaxed rules for monorepo compatibility - 'no-console': 'off', - 'prefer-const': 'warn', - 'no-constant-condition': 'warn', - 'no-constant-binary-expression': 'warn', - }, - settings: { - react: { - version: 'detect', - }, + // Spacing rules for consistency + 'space-infix-ops': 'error', // Enforces spaces around operators like +, =, etc. + 'keyword-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around keywords like if, else. + 'arrow-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around arrow in arrow functions. + 'space-before-blocks': 'error', // Enforces a space before opening curly braces. + 'object-curly-spacing': ['error', 'always'], // Enforces spaces inside curly braces: { foo } not {foo}. + 'comma-spacing': ['error', { 'before': false, 'after': true }], // Enforces space after a comma, not before. + 'space-before-function-paren': ['error', { 'anonymous': 'always', 'named': 'never', 'asyncArrow': 'always' }], // Controls space before function parentheses. + 'comma-dangle': ['error', 'never'], // Disallows trailing commas }, }, - - // Prettier config (must be last to override conflicting rules) - prettierConfig, -]; +); \ No newline at end of file From 64847d492209a7b9f8252244215618f968f3044b Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 10:38:28 -0300 Subject: [PATCH 02/21] lint --- apps/backend/scripts/build.ts | 16 +- apps/backend/src/app.module.ts | 50 +- apps/backend/src/auth/auth.controller.spec.ts | 34 +- apps/backend/src/auth/auth.controller.ts | 38 +- apps/backend/src/auth/auth.module.ts | 74 +- apps/backend/src/auth/auth.service.spec.ts | 157 ++-- apps/backend/src/auth/auth.service.ts | 60 +- .../src/auth/strategies/JWT.strategy.spec.ts | 28 +- .../src/auth/strategies/JWT.strategy.ts | 8 +- .../discord.strategy/DiscordStrategyConfig.ts | 4 +- .../discord.strategy/Strategy.spec.ts | 92 +-- .../strategies/discord.strategy/Strategy.ts | 74 +- .../discord.strategy/discord.strategy.spec.ts | 14 +- .../auth/strategies/discord.strategy/index.ts | 16 +- .../auth/strategies/github.strategy.spec.ts | 14 +- .../src/auth/strategies/github.strategy.ts | 10 +- .../auth/strategies/google.strategy.spec.ts | 12 +- .../src/auth/strategies/google.strategy.ts | 12 +- .../magicLinkEmail.strategy.spec.ts | 48 +- .../strategies/magicLinkEmail.strategy.ts | 36 +- .../src/config/EnvironmentVariables.ts | 4 +- .../email-login.controller.spec.ts | 2 +- .../src/email-login/email-login.module.ts | 2 +- .../email-login/email-login.service.spec.ts | 2 +- apps/backend/src/file/file.module.ts | 32 +- apps/backend/src/file/file.service.spec.ts | 79 +- apps/backend/src/file/file.service.ts | 62 +- apps/backend/src/lib/GetRequestUser.spec.ts | 14 +- apps/backend/src/lib/GetRequestUser.ts | 10 +- .../backend/src/lib/initializeSwagger.spec.ts | 23 +- apps/backend/src/lib/initializeSwagger.ts | 6 +- apps/backend/src/lib/parseToken.spec.ts | 28 +- apps/backend/src/lib/parseToken.ts | 4 +- .../src/mailing/mailing.controller.spec.ts | 10 +- apps/backend/src/mailing/mailing.module.ts | 4 +- .../src/mailing/mailing.service.spec.ts | 26 +- apps/backend/src/mailing/mailing.service.ts | 18 +- apps/backend/src/main.ts | 12 +- apps/backend/src/seed/seed.controller.spec.ts | 10 +- apps/backend/src/seed/seed.controller.ts | 4 +- apps/backend/src/seed/seed.module.ts | 14 +- apps/backend/src/seed/seed.service.spec.ts | 22 +- apps/backend/src/seed/seed.service.ts | 92 +-- .../song-browser.controller.spec.ts | 24 +- .../song-browser/song-browser.controller.ts | 10 +- .../src/song-browser/song-browser.module.ts | 4 +- .../song-browser/song-browser.service.spec.ts | 40 +- .../src/song-browser/song-browser.service.ts | 46 +- .../song/my-songs/my-songs.controller.spec.ts | 23 +- .../src/song/my-songs/my-songs.controller.ts | 6 +- .../song-upload/song-upload.service.spec.ts | 189 ++--- .../song/song-upload/song-upload.service.ts | 120 +-- .../song-webhook/song-webhook.service.spec.ts | 98 +-- .../song/song-webhook/song-webhook.service.ts | 18 +- apps/backend/src/song/song.controller.spec.ts | 114 +-- apps/backend/src/song/song.controller.ts | 42 +- apps/backend/src/song/song.module.ts | 12 +- apps/backend/src/song/song.service.spec.ts | 702 +++++++++--------- apps/backend/src/song/song.service.ts | 120 +-- apps/backend/src/song/song.util.ts | 56 +- apps/backend/src/user/user.controller.spec.ts | 18 +- apps/backend/src/user/user.controller.ts | 4 +- apps/backend/src/user/user.module.ts | 6 +- apps/backend/src/user/user.service.spec.ts | 112 +-- apps/backend/src/user/user.service.ts | 24 +- apps/frontend/mdx-components.tsx | 4 +- .../src/app/(content)/(info)/about/page.tsx | 2 +- .../app/(content)/(info)/blog/[id]/page.tsx | 24 +- .../src/app/(content)/(info)/blog/page.tsx | 2 +- .../src/app/(content)/(info)/contact/page.tsx | 2 +- .../app/(content)/(info)/help/[id]/page.tsx | 18 +- .../src/app/(content)/(info)/help/page.tsx | 2 +- apps/frontend/src/app/(content)/layout.tsx | 2 +- .../src/app/(content)/my-songs/page.tsx | 2 +- apps/frontend/src/app/(content)/page.tsx | 24 +- .../src/app/(content)/song/[id]/page.tsx | 24 +- .../src/app/(content)/upload/layout.tsx | 2 +- .../src/app/(content)/upload/page.tsx | 2 +- .../(external)/(auth)/login/email/page.tsx | 2 +- .../src/app/(external)/(auth)/login/page.tsx | 2 +- .../(external)/(legal)/guidelines/page.tsx | 2 +- .../app/(external)/(legal)/privacy/page.tsx | 2 +- .../src/app/(external)/(legal)/terms/page.tsx | 2 +- .../app/(external)/[...not-found]/page.tsx | 2 +- apps/frontend/src/app/(external)/layout.tsx | 4 +- apps/frontend/src/app/layout.tsx | 30 +- apps/frontend/src/app/not-found.tsx | 4 +- apps/frontend/src/lib/axios/ClientAxios.ts | 6 +- apps/frontend/src/lib/axios/index.ts | 4 +- apps/frontend/src/lib/posts.ts | 8 +- .../auth/components/client/LoginFrom.tsx | 16 +- .../src/modules/auth/components/loginPage.tsx | 2 +- .../auth/components/loginWithEmailPage.tsx | 3 +- .../src/modules/auth/features/auth.utils.ts | 8 +- .../src/modules/browse/WelcomeBanner.tsx | 6 +- .../browse/components/HomePageComponent.tsx | 27 +- .../modules/browse/components/SongCard.tsx | 2 +- .../components/client/CategoryButton.tsx | 12 +- .../components/client/TimespanButton.tsx | 4 +- .../client/context/FeaturedSongs.context.tsx | 23 +- .../client/context/HomePage.context.tsx | 6 +- .../client/context/RecentSongs.context.tsx | 24 +- .../my-songs/components/MySongsPage.tsx | 25 +- .../components/client/DeleteConfirmDialog.tsx | 2 +- .../components/client/MySongsButtons.tsx | 8 +- .../components/client/MySongsTable.tsx | 6 +- .../my-songs/components/client/SongRow.tsx | 11 +- .../client/context/MySongs.context.tsx | 34 +- .../shared/components/CustomMarkdown.tsx | 4 +- .../shared/components/NoteBlockWorldLogo.tsx | 4 +- .../shared/components/TeamMemberCard.tsx | 2 +- .../shared/components/client/BackButton.tsx | 2 +- .../shared/components/client/Carousel.tsx | 36 +- .../shared/components/client/Command.tsx | 12 +- .../shared/components/client/FormElements.tsx | 10 +- .../shared/components/client/GenericModal.tsx | 2 +- .../shared/components/client/Popover.tsx | 2 +- .../shared/components/client/ads/AdSlots.tsx | 18 +- .../components/client/ads/DetectAdBlock.tsx | 3 +- .../shared/components/layout/BlockTab.tsx | 4 +- .../components/layout/DocumentLayout.tsx | 3 +- .../shared/components/layout/Footer.tsx | 2 +- .../shared/components/layout/Header.tsx | 4 +- .../shared/components/layout/MusicalNote.tsx | 30 +- .../shared/components/layout/NavLinks.tsx | 2 +- .../shared/components/layout/NavbarLayout.tsx | 5 +- .../components/layout/RandomSongButton.tsx | 6 +- .../shared/components/layout/SettingsMenu.tsx | 4 +- .../components/layout/SignOutButton.tsx | 2 +- .../components/layout/SongThumbnail.tsx | 2 +- .../shared/components/layout/UserMenu.tsx | 25 +- .../shared/components/layout/UserMenuLink.tsx | 4 +- .../shared/components/layout/popover.tsx | 2 +- .../src/modules/shared/components/tooltip.tsx | 4 +- .../components/client/EditSongPage.tsx | 10 +- .../components/client/SongEditForm.tsx | 4 +- .../client/context/EditSong.context.tsx | 114 ++- .../components/client/SongUploadForm.tsx | 7 +- .../components/client/UploadCompleteModal.tsx | 8 +- .../components/client/UploadSongPage.tsx | 11 +- .../client/context/UploadSong.context.tsx | 55 +- .../modules/song/components/SongDetails.tsx | 2 +- .../src/modules/song/components/SongPage.tsx | 23 +- .../song/components/SongPageButtons.tsx | 25 +- .../components/client/DownloadSongModal.tsx | 2 +- .../song/components/client/FileDisplay.tsx | 8 +- .../components/client/InstrumentPicker.tsx | 17 +- .../song/components/client/LicenseInfo.tsx | 2 +- .../song/components/client/ShareModal.tsx | 4 +- .../song/components/client/SongCanvas.tsx | 30 +- .../song/components/client/SongForm.tsx | 17 +- .../song/components/client/SongForm.zod.ts | 24 +- .../components/client/SongSearchCombo.tsx | 14 +- .../song/components/client/SongSelector.tsx | 14 +- .../components/client/SongThumbnailInput.tsx | 18 +- .../components/client/ThumbnailRenderer.tsx | 12 +- .../client/context/Song.context.tsx | 6 +- .../src/modules/song/util/downloadSong.ts | 12 +- .../modules/user/components/UserProfile.tsx | 2 +- .../src/modules/user/features/song.util.ts | 4 +- .../src/modules/user/features/user.util.ts | 2 +- packages/configs/src/colors.ts | 110 +-- packages/configs/src/song.ts | 118 +-- packages/configs/src/user.ts | 2 +- .../database/src/common/dto/PageQuery.dto.ts | 26 +- .../src/song/dto/FeaturedSongsDto.dto.ts | 10 +- .../database/src/song/dto/SongPage.dto.ts | 18 +- .../database/src/song/dto/SongPreview.dto.ts | 22 +- packages/database/src/song/dto/SongStats.ts | 2 +- .../database/src/song/dto/SongView.dto.ts | 34 +- .../src/song/dto/ThumbnailData.dto.ts | 16 +- .../src/song/dto/UploadSongDto.dto.ts | 52 +- .../src/song/dto/UploadSongResponseDto.dto.ts | 22 +- .../database/src/song/entity/song.entity.ts | 8 +- .../database/src/user/dto/CreateUser.dto.ts | 8 +- packages/database/src/user/dto/GetUser.dto.ts | 8 +- .../database/src/user/dto/NewEmailUser.dto.ts | 6 +- .../src/user/dto/UpdateUsername.dto.ts | 2 +- packages/database/src/user/dto/user.dto.ts | 4 +- .../database/src/user/entity/user.entity.ts | 12 +- packages/song/src/injectMetadata.ts | 2 +- packages/song/src/notes.ts | 20 +- packages/song/src/obfuscate.ts | 16 +- packages/song/src/pack.ts | 8 +- packages/song/src/parse.ts | 34 +- packages/song/src/stats.ts | 22 +- packages/song/src/util.ts | 6 +- packages/song/tests/song/index.spec.ts | 31 +- packages/sounds/src/fetchSoundList.ts | 4 +- packages/thumbnail/src/canvasFactory.ts | 22 +- packages/thumbnail/src/index.ts | 8 +- packages/thumbnail/src/utils.spec.ts | 2 +- packages/thumbnail/src/utils.ts | 4 +- 193 files changed, 2376 insertions(+), 2386 deletions(-) diff --git a/apps/backend/scripts/build.ts b/apps/backend/scripts/build.ts index 5417739e..894a3240 100644 --- a/apps/backend/scripts/build.ts +++ b/apps/backend/scripts/build.ts @@ -8,7 +8,7 @@ const writeSoundList = async () => { function writeJSONFile( dir: string, fileName: string, - data: Record | string[], + data: Record | string[] ) { const path = resolve(dir, fileName); const jsonString = JSON.stringify(data, null, 0); @@ -56,16 +56,16 @@ const build = async () => { 'class-validator', '@nestjs/microservices', '@nestjs/websockets', - '@fastify/static', + '@fastify/static' ]; const result = await Bun.build({ entrypoints: ['./src/main.ts'], - outdir: './dist', - target: 'bun', - minify: false, - sourcemap: 'linked', - external: optionalRequirePackages.filter((pkg) => { + outdir : './dist', + target : 'bun', + minify : false, + sourcemap : 'linked', + external : optionalRequirePackages.filter((pkg) => { try { require(pkg); return false; @@ -73,7 +73,7 @@ const build = async () => { return true; } }), - splitting: true, + splitting: true }); if (!result.success) { diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index b3f2e3bd..c7d84293 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -20,30 +20,30 @@ import { UserModule } from './user/user.module'; @Module({ imports: [ ConfigModule.forRoot({ - isGlobal: true, + isGlobal : true, envFilePath: ['.env.development', '.env.production'], - validate, + validate }), //DatabaseModule, MongooseModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], + imports : [ConfigModule], + inject : [ConfigService], useFactory: ( - configService: ConfigService, + configService: ConfigService ): MongooseModuleFactoryOptions => { const url = configService.getOrThrow('MONGO_URL'); Logger.debug(`Connecting to ${url}`); return { - uri: url, + uri : url, retryAttempts: 10, - retryDelay: 3000, + retryDelay : 3000 }; - }, + } }), // Mailing MailerModule.forRootAsync({ - imports: [ConfigModule], + imports : [ConfigModule], useFactory: (configService: ConfigService) => { const transport = configService.getOrThrow('MAIL_TRANSPORT'); const from = configService.getOrThrow('MAIL_FROM'); @@ -51,26 +51,26 @@ import { UserModule } from './user/user.module'; AppModule.logger.debug(`MAIL_FROM: ${from}`); return { transport: transport, - defaults: { - from: from, + defaults : { + from: from }, template: { - dir: __dirname + '/mailing/templates', + dir : __dirname + '/mailing/templates', adapter: new HandlebarsAdapter(), options: { - strict: true, - }, - }, + strict: true + } + } }; }, - inject: [ConfigService], + inject: [ConfigService] }), // Throttler ThrottlerModule.forRoot([ { - ttl: 60, - limit: 256, // 256 requests per minute - }, + ttl : 60, + limit: 256 // 256 requests per minute + } ]), SongModule, UserModule, @@ -79,17 +79,17 @@ import { UserModule } from './user/user.module'; SongBrowserModule, SeedModule.forRoot(), EmailLoginModule, - MailingModule, + MailingModule ], controllers: [], - providers: [ + providers : [ ParseTokenPipe, { - provide: APP_GUARD, - useClass: ThrottlerGuard, - }, + provide : APP_GUARD, + useClass: ThrottlerGuard + } ], - exports: [ParseTokenPipe], + exports: [ParseTokenPipe] }) export class AppModule { static readonly logger = new Logger(AppModule.name); diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index d6bae4fa..fc863d90 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -6,15 +6,15 @@ import { AuthService } from './auth.service'; import { MagicLinkEmailStrategy } from './strategies/magicLinkEmail.strategy'; const mockAuthService = { - githubLogin: jest.fn(), - googleLogin: jest.fn(), - discordLogin: jest.fn(), - verifyToken: jest.fn(), - loginWithEmail: jest.fn(), + githubLogin : jest.fn(), + googleLogin : jest.fn(), + discordLogin : jest.fn(), + verifyToken : jest.fn(), + loginWithEmail: jest.fn() }; const mockMagicLinkEmailStrategy = { - send: jest.fn(), + send: jest.fn() }; describe('AuthController', () => { @@ -24,16 +24,16 @@ describe('AuthController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [AuthController], - providers: [ + providers : [ { - provide: AuthService, - useValue: mockAuthService, + provide : AuthService, + useValue: mockAuthService }, { - provide: MagicLinkEmailStrategy, - useValue: mockMagicLinkEmailStrategy, - }, - ], + provide : MagicLinkEmailStrategy, + useValue: mockMagicLinkEmailStrategy + } + ] }).compile(); controller = module.get(AuthController); @@ -61,7 +61,7 @@ describe('AuthController', () => { (authService.githubLogin as jest.Mock).mockRejectedValueOnce(error); await expect(controller.githubRedirect(req, res)).rejects.toThrow( - 'Test error', + 'Test error' ); }); }); @@ -90,7 +90,7 @@ describe('AuthController', () => { (authService.googleLogin as jest.Mock).mockRejectedValueOnce(error); await expect(controller.googleRedirect(req, res)).rejects.toThrow( - 'Test error', + 'Test error' ); }); }); @@ -119,7 +119,7 @@ describe('AuthController', () => { (authService.discordLogin as jest.Mock).mockRejectedValueOnce(error); await expect(controller.discordRedirect(req, res)).rejects.toThrow( - 'Test error', + 'Test error' ); }); }); @@ -148,7 +148,7 @@ describe('AuthController', () => { (authService.loginWithEmail as jest.Mock).mockRejectedValueOnce(error); await expect(controller.magicLinkRedirect(req, res)).rejects.toThrow( - 'Test error', + 'Test error' ); }); }); diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index 66e29486..ed44cfb9 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -8,7 +8,7 @@ import { Post, Req, Res, - UseGuards, + UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; @@ -26,15 +26,15 @@ export class AuthController { @Inject(AuthService) private readonly authService: AuthService, @Inject(MagicLinkEmailStrategy) - private readonly magicLinkEmailStrategy: MagicLinkEmailStrategy, + private readonly magicLinkEmailStrategy: MagicLinkEmailStrategy ) {} @Throttle({ default: { // one every 1 hour - ttl: 60 * 60 * 1000, - limit: 1, - }, + ttl : 60 * 60 * 1000, + limit: 1 + } }) @Post('login/magic-link') @ApiOperation({ @@ -44,21 +44,21 @@ export class AuthController { content: { 'application/json': { schema: { - type: 'object', + type : 'object', properties: { destination: { - type: 'string', - example: 'vycasnicolas@gmail.com', - description: 'Email address to send the magic link to', - }, + type : 'string', + example : 'vycasnicolas@gmail.com', + description: 'Email address to send the magic link to' + } }, - required: ['destination'], - }, - }, - }, - }, + required: ['destination'] + } + } + } + } }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async magicLinkLogin(@Req() req: Request, @Res() res: Response) { throw new HttpException('Not implemented', HttpStatus.NOT_IMPLEMENTED); // TODO: uncomment this line to enable magic link login @@ -67,7 +67,7 @@ export class AuthController { @Get('magic-link/callback') @ApiOperation({ - summary: 'Will send the user a email with a single use login link', + summary: 'Will send the user a email with a single use login link' }) @UseGuards(AuthGuard('magic-link')) public async magicLinkRedirect(@Req() req: Request, @Res() res: Response) { @@ -127,9 +127,9 @@ export class AuthController { public verify( @Req() req: Request, @Res({ - passthrough: true, + passthrough: true }) - res: Response, + res: Response ) { this.authService.verifyToken(req, res); } diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index e266b0bf..c4343ea8 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -17,14 +17,14 @@ import { MagicLinkEmailStrategy } from './strategies/magicLinkEmail.strategy'; export class AuthModule { static forRootAsync(): DynamicModule { return { - module: AuthModule, + module : AuthModule, imports: [ UserModule, ConfigModule.forRoot(), MailingModule, JwtModule.registerAsync({ - inject: [ConfigService], - imports: [ConfigModule], + inject : [ConfigService], + imports : [ConfigModule], useFactory: async (config: ConfigService) => { const JWT_SECRET = config.get('JWT_SECRET'); const JWT_EXPIRES_IN = config.get('JWT_EXPIRES_IN'); @@ -39,14 +39,14 @@ export class AuthModule { } return { - secret: JWT_SECRET, - signOptions: { expiresIn: JWT_EXPIRES_IN || '60s' }, + secret : JWT_SECRET, + signOptions: { expiresIn: JWT_EXPIRES_IN || '60s' } }; - }, - }), + } + }) ], controllers: [AuthController], - providers: [ + providers : [ AuthService, ConfigService, GoogleStrategy, @@ -55,61 +55,61 @@ export class AuthModule { MagicLinkEmailStrategy, JwtStrategy, { - inject: [ConfigService], - provide: 'COOKIE_EXPIRES_IN', + inject : [ConfigService], + provide : 'COOKIE_EXPIRES_IN', useFactory: (configService: ConfigService) => - configService.getOrThrow('COOKIE_EXPIRES_IN'), + configService.getOrThrow('COOKIE_EXPIRES_IN') }, { - inject: [ConfigService], - provide: 'SERVER_URL', + inject : [ConfigService], + provide : 'SERVER_URL', useFactory: (configService: ConfigService) => - configService.getOrThrow('SERVER_URL'), + configService.getOrThrow('SERVER_URL') }, { - inject: [ConfigService], - provide: 'FRONTEND_URL', + inject : [ConfigService], + provide : 'FRONTEND_URL', useFactory: (configService: ConfigService) => - configService.getOrThrow('FRONTEND_URL'), + configService.getOrThrow('FRONTEND_URL') }, { - inject: [ConfigService], - provide: 'JWT_SECRET', + inject : [ConfigService], + provide : 'JWT_SECRET', useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_SECRET'), + configService.getOrThrow('JWT_SECRET') }, { - inject: [ConfigService], - provide: 'JWT_EXPIRES_IN', + inject : [ConfigService], + provide : 'JWT_EXPIRES_IN', useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_EXPIRES_IN'), + configService.getOrThrow('JWT_EXPIRES_IN') }, { - inject: [ConfigService], - provide: 'JWT_REFRESH_SECRET', + inject : [ConfigService], + provide : 'JWT_REFRESH_SECRET', useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_REFRESH_SECRET'), + configService.getOrThrow('JWT_REFRESH_SECRET') }, { - inject: [ConfigService], - provide: 'JWT_REFRESH_EXPIRES_IN', + inject : [ConfigService], + provide : 'JWT_REFRESH_EXPIRES_IN', useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_REFRESH_EXPIRES_IN'), + configService.getOrThrow('JWT_REFRESH_EXPIRES_IN') }, { - inject: [ConfigService], - provide: 'MAGIC_LINK_SECRET', + inject : [ConfigService], + provide : 'MAGIC_LINK_SECRET', useFactory: (configService: ConfigService) => - configService.getOrThrow('MAGIC_LINK_SECRET'), + configService.getOrThrow('MAGIC_LINK_SECRET') }, { - inject: [ConfigService], - provide: 'APP_DOMAIN', + inject : [ConfigService], + provide : 'APP_DOMAIN', useFactory: (configService: ConfigService) => - configService.get('APP_DOMAIN'), - }, + configService.get('APP_DOMAIN') + } ], - exports: [AuthService], + exports: [AuthService] }; } } diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts index 311fece9..a70d6d2b 100644 --- a/apps/backend/src/auth/auth.service.spec.ts +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -1,7 +1,8 @@ +import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; + import type { UserDocument } from '@nbw/database'; import { JwtService } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; -import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; import type { Request, Response } from 'express'; import { UserService } from '@server/user/user.service'; @@ -10,26 +11,26 @@ import { AuthService } from './auth.service'; import { Profile } from './types/profile'; const mockAxios = { - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), + get : jest.fn(), + post : jest.fn(), + put : jest.fn(), delete: jest.fn(), - create: jest.fn(), + create: jest.fn() }; mock.module('axios', () => mockAxios); const mockUserService = { generateUsername: jest.fn(), - findByEmail: jest.fn(), - findByID: jest.fn(), - create: jest.fn(), + findByEmail : jest.fn(), + findByID : jest.fn(), + create : jest.fn() }; const mockJwtService = { - decode: jest.fn(), + decode : jest.fn(), signAsync: jest.fn(), - verify: jest.fn(), + verify : jest.fn() }; describe('AuthService', () => { @@ -42,50 +43,50 @@ describe('AuthService', () => { providers: [ AuthService, { - provide: UserService, - useValue: mockUserService, + provide : UserService, + useValue: mockUserService }, { - provide: JwtService, - useValue: mockJwtService, + provide : JwtService, + useValue: mockJwtService }, { - provide: 'COOKIE_EXPIRES_IN', - useValue: '3600', + provide : 'COOKIE_EXPIRES_IN', + useValue: '3600' }, { - provide: 'FRONTEND_URL', - useValue: 'http://frontend.test.com', + provide : 'FRONTEND_URL', + useValue: 'http://frontend.test.com' }, { - provide: 'COOKIE_EXPIRES_IN', - useValue: '3600', + provide : 'COOKIE_EXPIRES_IN', + useValue: '3600' }, { - provide: 'JWT_SECRET', - useValue: 'test-jwt-secret', + provide : 'JWT_SECRET', + useValue: 'test-jwt-secret' }, { - provide: 'JWT_EXPIRES_IN', - useValue: '1d', + provide : 'JWT_EXPIRES_IN', + useValue: '1d' }, { - provide: 'JWT_REFRESH_SECRET', - useValue: 'test-jwt-refresh-secret', + provide : 'JWT_REFRESH_SECRET', + useValue: 'test-jwt-refresh-secret' }, { - provide: 'JWT_REFRESH_EXPIRES_IN', - useValue: '7d', + provide : 'JWT_REFRESH_EXPIRES_IN', + useValue: '7d' }, { - provide: 'WHITELISTED_USERS', - useValue: 'tomast1337,bentroen,testuser', + provide : 'WHITELISTED_USERS', + useValue: 'tomast1337,bentroen,testuser' }, { - provide: 'APP_DOMAIN', - useValue: '.test.com', - }, - ], + provide : 'APP_DOMAIN', + useValue: '.test.com' + } + ] }).compile(); authService = module.get(AuthService); @@ -103,7 +104,7 @@ describe('AuthService', () => { const res = { status: jest.fn().mockReturnThis(), - json: jest.fn(), + json : jest.fn() } as any; await authService.verifyToken(req, res); @@ -111,7 +112,7 @@ describe('AuthService', () => { expect(res.status).toHaveBeenCalledWith(401); expect(res.json).toHaveBeenCalledWith({ - message: 'No authorization header', + message: 'No authorization header' }); }); @@ -120,7 +121,7 @@ describe('AuthService', () => { const res = { status: jest.fn().mockReturnThis(), - json: jest.fn(), + json : jest.fn() } as any; await authService.verifyToken(req, res); @@ -131,12 +132,12 @@ describe('AuthService', () => { it('should throw an error if user is not found', async () => { const req = { - headers: { authorization: 'Bearer test-token' }, + headers: { authorization: 'Bearer test-token' } } as Request; const res = { status: jest.fn().mockReturnThis(), - json: jest.fn(), + json : jest.fn() } as any; mockJwtService.verify.mockReturnValueOnce({ id: 'test-id' }); @@ -150,12 +151,12 @@ describe('AuthService', () => { it('should return decoded token if user is found', async () => { const req = { - headers: { authorization: 'Bearer test-token' }, + headers: { authorization: 'Bearer test-token' } } as Request; const res = { status: jest.fn().mockReturnThis(), - json: jest.fn(), + json : jest.fn() } as any; const decodedToken = { id: 'test-id' }; @@ -200,24 +201,24 @@ describe('AuthService', () => { } return Promise.reject(new Error('Invalid secret')); - }, + } ); const tokens = await (authService as any).createJwtPayload(payload); expect(tokens).toEqual({ - access_token: accessToken, - refresh_token: refreshToken, + access_token : accessToken, + refresh_token: refreshToken }); expect(jwtService.signAsync).toHaveBeenCalledWith(payload, { - secret: 'test-jwt-secret', - expiresIn: '1d', + secret : 'test-jwt-secret', + expiresIn: '1d' }); expect(jwtService.signAsync).toHaveBeenCalledWith(payload, { - secret: 'test-jwt-refresh-secret', - expiresIn: '7d', + secret : 'test-jwt-refresh-secret', + expiresIn: '7d' }); }); }); @@ -225,19 +226,19 @@ describe('AuthService', () => { describe('GenTokenRedirect', () => { it('should set cookies and redirect to the frontend URL', async () => { const user_registered = { - _id: 'user-id', - email: 'test@example.com', - username: 'testuser', + _id : 'user-id', + email : 'test@example.com', + username: 'testuser' } as unknown as UserDocument; const res = { - cookie: jest.fn(), - redirect: jest.fn(), + cookie : jest.fn(), + redirect: jest.fn() } as unknown as Response; const tokens = { - access_token: 'access-token', - refresh_token: 'refresh-token', + access_token : 'access-token', + refresh_token: 'refresh-token' }; spyOn(authService as any, 'createJwtPayload').mockResolvedValue(tokens); @@ -245,14 +246,14 @@ describe('AuthService', () => { await (authService as any).GenTokenRedirect(user_registered, res); expect((authService as any).createJwtPayload).toHaveBeenCalledWith({ - id: 'user-id', - email: 'test@example.com', - username: 'testuser', + id : 'user-id', + email : 'test@example.com', + username: 'testuser' }); expect(res.cookie).toHaveBeenCalledWith('token', 'access-token', { domain: '.test.com', - maxAge: 3600000, + maxAge: 3600000 }); expect(res.cookie).toHaveBeenCalledWith( @@ -260,8 +261,8 @@ describe('AuthService', () => { 'refresh-token', { domain: '.test.com', - maxAge: 3600000, - }, + maxAge: 3600000 + } ); expect(res.redirect).toHaveBeenCalledWith('http://frontend.test.com/'); @@ -271,9 +272,9 @@ describe('AuthService', () => { describe('verifyAndGetUser', () => { it('should create a new user if the user is not registered', async () => { const user: Profile = { - username: 'testuser', - email: 'test@example.com', - profileImage: 'http://example.com/photo.jpg', + username : 'testuser', + email : 'test@example.com', + profileImage: 'http://example.com/photo.jpg' }; mockUserService.findByEmail.mockResolvedValue(null); @@ -285,9 +286,9 @@ describe('AuthService', () => { expect(userService.create).toHaveBeenCalledWith( expect.objectContaining({ - email: 'test@example.com', - profileImage: 'http://example.com/photo.jpg', - }), + email : 'test@example.com', + profileImage: 'http://example.com/photo.jpg' + }) ); expect(result).toEqual({ id: 'new-user-id' }); @@ -295,14 +296,14 @@ describe('AuthService', () => { it('should return the registered user if the user is already registered', async () => { const user: Profile = { - username: 'testuser', - email: 'test@example.com', - profileImage: 'http://example.com/photo.jpg', + username : 'testuser', + email : 'test@example.com', + profileImage: 'http://example.com/photo.jpg' }; const registeredUser = { - id: 'registered-user-id', - profileImage: 'http://example.com/photo.jpg', + id : 'registered-user-id', + profileImage: 'http://example.com/photo.jpg' }; mockUserService.findByEmail.mockResolvedValue(registeredUser); @@ -315,15 +316,15 @@ describe('AuthService', () => { it('should update the profile image if it has changed', async () => { const user: Profile = { - username: 'testuser', - email: 'test@example.com', - profileImage: 'http://example.com/new-photo.jpg', + username : 'testuser', + email : 'test@example.com', + profileImage: 'http://example.com/new-photo.jpg' }; const registeredUser = { - id: 'registered-user-id', + id : 'registered-user-id', profileImage: 'http://example.com/old-photo.jpg', - save: jest.fn(), + save : jest.fn() }; mockUserService.findByEmail.mockResolvedValue(registeredUser); @@ -333,7 +334,7 @@ describe('AuthService', () => { expect(userService.findByEmail).toHaveBeenCalledWith('test@example.com'); expect(registeredUser.profileImage).toEqual( - 'http://example.com/new-photo.jpg', + 'http://example.com/new-photo.jpg' ); expect(registeredUser.save).toHaveBeenCalled(); diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index b590e003..4be506fc 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -35,7 +35,7 @@ export class AuthService { @Inject('JWT_REFRESH_EXPIRES_IN') private readonly JWT_REFRESH_EXPIRES_IN: string, @Inject('APP_DOMAIN') - private readonly APP_DOMAIN?: string, + private readonly APP_DOMAIN?: string ) {} public async verifyToken(req: Request, res: Response) { @@ -54,7 +54,7 @@ export class AuthService { try { const decoded = this.jwtService.verify(token, { - secret: this.JWT_SECRET, + secret: this.JWT_SECRET }); // verify if user exists @@ -76,9 +76,9 @@ export class AuthService { const profile = { // Generate username from display name - username: email.split('@')[0], - email: email, - profileImage: user.photos[0].value, + username : email.split('@')[0], + email : email, + profileImage: user.photos[0].value }; // verify if user exists @@ -93,9 +93,9 @@ export class AuthService { const newUsername = await this.userService.generateUsername(baseUsername); const newUser = new CreateUser({ - username: newUsername, - email: email, - profileImage: profileImage, + username : newUsername, + email : email, + profileImage: profileImage }); return await this.userService.create(newUser); @@ -126,17 +126,17 @@ export class AuthService { 'https://api.github.com/user/emails', { headers: { - Authorization: `token ${user.accessToken}`, - }, - }, + Authorization: `token ${user.accessToken}` + } + } ); const email = response.data.filter((email) => email.primary)[0].email; const user_registered = await this.verifyAndGetUser({ - username: profile.username, - email: email, - profileImage: profile.photos[0].value, + username : profile.username, + email : email, + profileImage: profile.photos[0].value }); return this.GenTokenRedirect(user_registered, res); @@ -148,9 +148,9 @@ export class AuthService { const profile = { // Generate username from display name - username: user.username, - email: user.email, - profileImage: profilePictureUrl, + username : user.username, + email : user.email, + profileImage: profilePictureUrl }; // verify if user exists @@ -172,29 +172,29 @@ export class AuthService { public async createJwtPayload(payload: TokenPayload): Promise { const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync(payload, { - secret: this.JWT_SECRET, - expiresIn: this.JWT_EXPIRES_IN, + secret : this.JWT_SECRET, + expiresIn: this.JWT_EXPIRES_IN }), this.jwtService.signAsync(payload, { - secret: this.JWT_REFRESH_SECRET, - expiresIn: this.JWT_REFRESH_EXPIRES_IN, - }), + secret : this.JWT_REFRESH_SECRET, + expiresIn: this.JWT_REFRESH_EXPIRES_IN + }) ]); return { - access_token: accessToken, - refresh_token: refreshToken, + access_token : accessToken, + refresh_token: refreshToken }; } private async GenTokenRedirect( user_registered: UserDocument, - res: Response>, + res: Response> ): Promise { const token = await this.createJwtPayload({ - id: user_registered._id.toString(), - email: user_registered.email, - username: user_registered.username, + id : user_registered._id.toString(), + email : user_registered.email, + username: user_registered.username }); const frontEndURL = this.FRONTEND_URL; @@ -203,12 +203,12 @@ export class AuthService { res.cookie('token', token.access_token, { domain: domain, - maxAge: maxAge, + maxAge: maxAge }); res.cookie('refresh_token', token.refresh_token, { domain: domain, - maxAge: maxAge, + maxAge: maxAge }); res.redirect(frontEndURL + '/'); diff --git a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts index 052cae9e..c269c23e 100644 --- a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts @@ -13,12 +13,12 @@ describe('JwtStrategy', () => { providers: [ JwtStrategy, { - provide: ConfigService, + provide : ConfigService, useValue: { - getOrThrow: jest.fn().mockReturnValue('test-secret'), - }, - }, - ], + getOrThrow: jest.fn().mockReturnValue('test-secret') + } + } + ] }).compile(); jwtStrategy = module.get(JwtStrategy); @@ -34,7 +34,7 @@ describe('JwtStrategy', () => { jest.spyOn(configService, 'getOrThrow').mockReturnValue(null); expect(() => new JwtStrategy(configService)).toThrowError( - 'JwtStrategy requires a secret or key', + 'JwtStrategy requires a secret or key' ); }); }); @@ -43,9 +43,9 @@ describe('JwtStrategy', () => { it('should return payload with refreshToken from header', () => { const req = { headers: { - authorization: 'Bearer test-refresh-token', + authorization: 'Bearer test-refresh-token' }, - cookies: {}, + cookies: {} } as unknown as Request; const payload = { userId: 'test-user-id' }; @@ -54,7 +54,7 @@ describe('JwtStrategy', () => { expect(result).toEqual({ ...payload, - refreshToken: 'test-refresh-token', + refreshToken: 'test-refresh-token' }); }); @@ -62,8 +62,8 @@ describe('JwtStrategy', () => { const req = { headers: {}, cookies: { - refresh_token: 'test-refresh-token', - }, + refresh_token: 'test-refresh-token' + } } as unknown as Request; const payload = { userId: 'test-user-id' }; @@ -72,20 +72,20 @@ describe('JwtStrategy', () => { expect(result).toEqual({ ...payload, - refreshToken: 'test-refresh-token', + refreshToken: 'test-refresh-token' }); }); it('should throw an error if no refresh token is provided', () => { const req = { headers: {}, - cookies: {}, + cookies: {} } as unknown as Request; const payload = { userId: 'test-user-id' }; expect(() => jwtStrategy.validate(req, payload)).toThrowError( - 'No refresh token', + 'No refresh token' ); }); }); diff --git a/apps/backend/src/auth/strategies/JWT.strategy.ts b/apps/backend/src/auth/strategies/JWT.strategy.ts index 6311d4e4..aa037f28 100644 --- a/apps/backend/src/auth/strategies/JWT.strategy.ts +++ b/apps/backend/src/auth/strategies/JWT.strategy.ts @@ -11,9 +11,9 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { const JWT_SECRET = config.getOrThrow('JWT_SECRET'); super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: JWT_SECRET, - passReqToCallback: true, + jwtFromRequest : ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey : JWT_SECRET, + passReqToCallback: true }); } @@ -31,7 +31,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { return { ...payload, - refreshToken, + refreshToken }; } } diff --git a/apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts b/apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts index af8b44f2..02f6d42d 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts @@ -4,11 +4,11 @@ import { IsEnum, IsNumber, IsOptional, - IsString, + IsString } from 'class-validator'; import { StrategyOptions as OAuth2StrategyOptions, - StrategyOptionsWithRequest as OAuth2StrategyOptionsWithRequest, + StrategyOptionsWithRequest as OAuth2StrategyOptionsWithRequest } from 'passport-oauth2'; import type { ScopeType } from './types'; diff --git a/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts b/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts index e075f778..0943fe9a 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts @@ -10,16 +10,16 @@ describe('DiscordStrategy', () => { beforeEach(() => { const config: DiscordStrategyConfig = { - clientID: 'test-client-id', + clientID : 'test-client-id', clientSecret: 'test-client-secret', - callbackUrl: 'http://localhost:3000/callback', - scope: [ + callbackUrl : 'http://localhost:3000/callback', + scope : [ DiscordPermissionScope.Email, DiscordPermissionScope.Identify, - DiscordPermissionScope.Connections, + DiscordPermissionScope.Connections // DiscordPermissionScope.Bot, // Not allowed scope ], - prompt: 'consent', + prompt: 'consent' }; strategy = new DiscordStrategy(config, verify); @@ -35,11 +35,11 @@ describe('DiscordStrategy', () => { it('should validate config', async () => { const config: DiscordStrategyConfig = { - clientID: 'test-client-id', + clientID : 'test-client-id', clientSecret: 'test-client-secret', - callbackUrl: 'http://localhost:3000/callback', - scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], - prompt: 'consent', + callbackUrl : 'http://localhost:3000/callback', + scope : [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], + prompt : 'consent' }; await expect(strategy['validateConfig'](config)).resolves.toBeUndefined(); @@ -54,7 +54,7 @@ describe('DiscordStrategy', () => { const result = await strategy['makeApiRequest']<{ id: string }>( 'https://discord.com/api/users/@me', - 'test-access-token', + 'test-access-token' ); expect(result).toEqual({ id: '123' }); @@ -71,47 +71,47 @@ describe('DiscordStrategy', () => { it('should build profile', () => { const profileData = { - id: '123', - username: 'testuser', - displayName: 'Test User', - avatar: 'avatar.png', - banner: 'banner.png', - email: 'test@example.com', - verified: true, - mfa_enabled: true, + id : '123', + username : 'testuser', + displayName : 'Test User', + avatar : 'avatar.png', + banner : 'banner.png', + email : 'test@example.com', + verified : true, + mfa_enabled : true, public_flags: 1, - flags: 1, - locale: 'en-US', - global_name: 'testuser#1234', + flags : 1, + locale : 'en-US', + global_name : 'testuser#1234', premium_type: 1, - connections: [], - guilds: [], + connections : [], + guilds : [] } as unknown as Profile; const profile = strategy['buildProfile'](profileData, 'test-access-token'); expect(profile).toMatchObject({ - provider: 'discord', - id: '123', - username: 'testuser', - displayName: 'Test User', - avatar: 'avatar.png', - banner: 'banner.png', - email: 'test@example.com', - verified: true, - mfa_enabled: true, + provider : 'discord', + id : '123', + username : 'testuser', + displayName : 'Test User', + avatar : 'avatar.png', + banner : 'banner.png', + email : 'test@example.com', + verified : true, + mfa_enabled : true, public_flags: 1, - flags: 1, - locale: 'en-US', - global_name: 'testuser#1234', + flags : 1, + locale : 'en-US', + global_name : 'testuser#1234', premium_type: 1, - connections: [], - guilds: [], + connections : [], + guilds : [], access_token: 'test-access-token', - fetchedAt: expect.any(Date), - createdAt: expect.any(Date), - _raw: JSON.stringify(profileData), - _json: profileData, + fetchedAt : expect.any(Date), + createdAt : expect.any(Date), + _raw : JSON.stringify(profileData), + _json : profileData }); }); @@ -121,7 +121,7 @@ describe('DiscordStrategy', () => { const result = await strategy['fetchScopeData']( DiscordPermissionScope.Connections, - 'test-access-token', + 'test-access-token' ); expect(result).toEqual([{ id: '123' }]); @@ -133,7 +133,7 @@ describe('DiscordStrategy', () => { const result = await strategy['fetchScopeData']( DiscordPermissionScope.Bot, - 'test-access-token', + 'test-access-token' ); expect(result).toEqual(null); @@ -141,9 +141,9 @@ describe('DiscordStrategy', () => { it('should enrich profile with scopes', async () => { const profile = { - id: '123', + id : '123', connections: [], - guilds: [], + guilds : [] } as unknown as Profile; const mockFetchScopeData = jest @@ -172,7 +172,7 @@ describe('DiscordStrategy', () => { const params = strategy.authorizationParams(options); expect(params).toMatchObject({ - prompt: 'consent', + prompt: 'consent' }); }); }); diff --git a/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts b/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts index ebc6d905..31b5b37c 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts @@ -6,7 +6,7 @@ import { Strategy as OAuth2Strategy, StrategyOptions as OAuth2StrategyOptions, VerifyCallback, - VerifyFunction, + VerifyFunction } from 'passport-oauth2'; import { DiscordStrategyConfig } from './DiscordStrategyConfig'; @@ -15,7 +15,7 @@ import { ProfileConnection, ProfileGuild, ScopeType, - SingleScopeType, + SingleScopeType } from './types'; interface AuthorizationParams { @@ -38,12 +38,12 @@ export default class Strategy extends OAuth2Strategy { public constructor(options: DiscordStrategyConfig, verify: VerifyFunction) { super( { - scopeSeparator: ' ', + scopeSeparator : ' ', ...options, authorizationURL: 'https://discord.com/api/oauth2/authorize', - tokenURL: 'https://discord.com/api/oauth2/token', + tokenURL : 'https://discord.com/api/oauth2/token' } as OAuth2StrategyOptions, - verify, + verify ); this.validateConfig(options); @@ -66,7 +66,7 @@ export default class Strategy extends OAuth2Strategy { private async makeApiRequest( url: string, - accessToken: string, + accessToken: string ): Promise { return new Promise((resolve, reject) => { this._oauth2.get(url, accessToken, (err, body) => { @@ -87,7 +87,7 @@ export default class Strategy extends OAuth2Strategy { private async fetchUserData(accessToken: string): Promise { return this.makeApiRequest( `${Strategy.DISCORD_API_BASE}/users/@me`, - accessToken, + accessToken ); } @@ -109,15 +109,15 @@ export default class Strategy extends OAuth2Strategy { private async enrichProfileWithScopes( profile: Profile, - accessToken: string, + accessToken: string ): Promise { await Promise.all([ this.fetchScopeData('connections', accessToken).then( - (data) => (profile.connections = data as ProfileConnection[]), + (data) => (profile.connections = data as ProfileConnection[]) ), this.fetchScopeData('guilds', accessToken).then( - (data) => (profile.guilds = data as ProfileGuild[]), - ), + (data) => (profile.guilds = data as ProfileGuild[]) + ) ]); profile.fetchedAt = new Date(); @@ -125,7 +125,7 @@ export default class Strategy extends OAuth2Strategy { private async fetchScopeData( scope: SingleScopeType, - accessToken: string, + accessToken: string ): Promise { if (!this.scope.includes(scope)) { return null; @@ -137,7 +137,7 @@ export default class Strategy extends OAuth2Strategy { return this.makeApiRequest( `${Strategy.DISCORD_API_BASE}/users/@me/${scope}`, - accessToken, + accessToken ); } @@ -148,34 +148,34 @@ export default class Strategy extends OAuth2Strategy { private buildProfile(data: Profile, accessToken: string): Profile { const { id } = data; return { - provider: 'discord', - id: id, - username: data.username, - displayName: data.displayName, - avatar: data.avatar, - banner: data.banner, - email: data.email, - verified: data.verified, - mfa_enabled: data.mfa_enabled, + provider : 'discord', + id : id, + username : data.username, + displayName : data.displayName, + avatar : data.avatar, + banner : data.banner, + email : data.email, + verified : data.verified, + mfa_enabled : data.mfa_enabled, public_flags: data.public_flags, - flags: data.flags, - locale: data.locale, - global_name: data.global_name, + flags : data.flags, + locale : data.locale, + global_name : data.global_name, premium_type: data.premium_type, - connections: data.connections, - guilds: data.guilds, + connections : data.connections, + guilds : data.guilds, access_token: accessToken, - fetchedAt: new Date(), - createdAt: this.calculateCreationDate(id), - _raw: JSON.stringify(data), - _json: data as unknown as Record, + fetchedAt : new Date(), + createdAt : this.calculateCreationDate(id), + _raw : JSON.stringify(data), + _json : data as unknown as Record }; } public fetchScope( scope: SingleScopeType, accessToken: string, - callback: (err: Error | null, data: Record | null) => void, + callback: (err: Error | null, data: Record | null) => void ): void { // Early return if scope is not included if (!this.scope.includes(scope)) { @@ -185,7 +185,7 @@ export default class Strategy extends OAuth2Strategy { // Handle scope delay const delayPromise = new Promise((resolve) => - setTimeout(resolve, this.scopeDelay ?? 0), + setTimeout(resolve, this.scopeDelay ?? 0) ); delayPromise @@ -199,7 +199,7 @@ export default class Strategy extends OAuth2Strategy { callback( new InternalOAuthError(`Failed to fetch scope: ${scope}`, err), - null, + null ); return; @@ -208,7 +208,7 @@ export default class Strategy extends OAuth2Strategy { try { if (typeof body !== 'string') { const error = new Error( - `Invalid response type for scope: ${scope}`, + `Invalid response type for scope: ${scope}` ); this.logger.error(error.message); @@ -227,7 +227,7 @@ export default class Strategy extends OAuth2Strategy { this.logger.error('Parse error:', error); callback(error, null); } - }, + } ); }) .catch((error) => { @@ -237,7 +237,7 @@ export default class Strategy extends OAuth2Strategy { } public override authorizationParams( - options: AuthorizationParams, + options: AuthorizationParams ): AuthorizationParams & Record { const params: AuthorizationParams & Record = super.authorizationParams(options) as Record; diff --git a/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts b/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts index 0dbc8608..8aeec7d3 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts @@ -12,7 +12,7 @@ describe('DiscordStrategy', () => { providers: [ DiscordStrategy, { - provide: ConfigService, + provide : ConfigService, useValue: { getOrThrow: jest.fn((key: string) => { switch (key) { @@ -25,10 +25,10 @@ describe('DiscordStrategy', () => { default: return null; } - }), - }, - }, - ], + }) + } + } + ] }).compile(); discordStrategy = module.get(DiscordStrategy); @@ -44,7 +44,7 @@ describe('DiscordStrategy', () => { jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); expect(() => new DiscordStrategy(configService)).toThrowError( - 'OAuth2Strategy requires a clientID option', + 'OAuth2Strategy requires a clientID option' ); }); }); @@ -58,7 +58,7 @@ describe('DiscordStrategy', () => { const result = await discordStrategy.validate( accessToken, refreshToken, - profile, + profile ); expect(result).toEqual({ accessToken, refreshToken, profile }); diff --git a/apps/backend/src/auth/strategies/discord.strategy/index.ts b/apps/backend/src/auth/strategies/discord.strategy/index.ts index 61dc578a..9a4d3e53 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/index.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/index.ts @@ -10,24 +10,24 @@ export class DiscordStrategy extends PassportStrategy(strategy, 'discord') { private static logger = new Logger(DiscordStrategy.name); constructor( @Inject(ConfigService) - configService: ConfigService, + configService: ConfigService ) { const DISCORD_CLIENT_ID = configService.getOrThrow('DISCORD_CLIENT_ID'); const DISCORD_CLIENT_SECRET = configService.getOrThrow( - 'DISCORD_CLIENT_SECRET', + 'DISCORD_CLIENT_SECRET' ); const SERVER_URL = configService.getOrThrow('SERVER_URL'); const config = { - clientID: DISCORD_CLIENT_ID, + clientID : DISCORD_CLIENT_ID, clientSecret: DISCORD_CLIENT_SECRET, - callbackUrl: `${SERVER_URL}/api/v1/auth/discord/callback`, - scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], - fetchScope: true, - prompt: 'none', + callbackUrl : `${SERVER_URL}/api/v1/auth/discord/callback`, + scope : [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], + fetchScope : true, + prompt : 'none' }; super(config); @@ -35,7 +35,7 @@ export class DiscordStrategy extends PassportStrategy(strategy, 'discord') { async validate(accessToken: string, refreshToken: string, profile: any) { DiscordStrategy.logger.debug( - `Discord Login Data ${JSON.stringify(profile)}`, + `Discord Login Data ${JSON.stringify(profile)}` ); return { accessToken, refreshToken, profile }; diff --git a/apps/backend/src/auth/strategies/github.strategy.spec.ts b/apps/backend/src/auth/strategies/github.strategy.spec.ts index c8793e00..7101ed7b 100644 --- a/apps/backend/src/auth/strategies/github.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/github.strategy.spec.ts @@ -12,7 +12,7 @@ describe('GithubStrategy', () => { providers: [ GithubStrategy, { - provide: ConfigService, + provide : ConfigService, useValue: { getOrThrow: jest.fn((key: string) => { switch (key) { @@ -25,10 +25,10 @@ describe('GithubStrategy', () => { default: return null; } - }), - }, - }, - ], + }) + } + } + ] }).compile(); githubStrategy = module.get(GithubStrategy); @@ -44,7 +44,7 @@ describe('GithubStrategy', () => { jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); expect(() => new GithubStrategy(configService)).toThrowError( - 'OAuth2Strategy requires a clientID option', + 'OAuth2Strategy requires a clientID option' ); }); }); @@ -58,7 +58,7 @@ describe('GithubStrategy', () => { const result = await githubStrategy.validate( accessToken, refreshToken, - profile, + profile ); expect(result).toEqual({ accessToken, refreshToken, profile }); diff --git a/apps/backend/src/auth/strategies/github.strategy.ts b/apps/backend/src/auth/strategies/github.strategy.ts index 27293151..c991b1cd 100644 --- a/apps/backend/src/auth/strategies/github.strategy.ts +++ b/apps/backend/src/auth/strategies/github.strategy.ts @@ -8,23 +8,23 @@ export class GithubStrategy extends PassportStrategy(strategy, 'github') { private static logger = new Logger(GithubStrategy.name); constructor( @Inject(ConfigService) - configService: ConfigService, + configService: ConfigService ) { const GITHUB_CLIENT_ID = configService.getOrThrow('GITHUB_CLIENT_ID'); const GITHUB_CLIENT_SECRET = configService.getOrThrow( - 'GITHUB_CLIENT_SECRET', + 'GITHUB_CLIENT_SECRET' ); const SERVER_URL = configService.getOrThrow('SERVER_URL'); super({ - clientID: GITHUB_CLIENT_ID, + clientID : GITHUB_CLIENT_ID, clientSecret: GITHUB_CLIENT_SECRET, redirect_uri: `${SERVER_URL}/api/v1/auth/github/callback`, - scope: 'user:read,user:email', - state: false, + scope : 'user:read,user:email', + state : false }); } diff --git a/apps/backend/src/auth/strategies/google.strategy.spec.ts b/apps/backend/src/auth/strategies/google.strategy.spec.ts index c1f1233e..4e563b79 100644 --- a/apps/backend/src/auth/strategies/google.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/google.strategy.spec.ts @@ -13,7 +13,7 @@ describe('GoogleStrategy', () => { providers: [ GoogleStrategy, { - provide: ConfigService, + provide : ConfigService, useValue: { getOrThrow: jest.fn((key: string) => { switch (key) { @@ -26,10 +26,10 @@ describe('GoogleStrategy', () => { default: return null; } - }), - }, - }, - ], + }) + } + } + ] }).compile(); googleStrategy = module.get(GoogleStrategy); @@ -45,7 +45,7 @@ describe('GoogleStrategy', () => { jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); expect(() => new GoogleStrategy(configService)).toThrowError( - 'OAuth2Strategy requires a clientID option', + 'OAuth2Strategy requires a clientID option' ); }); }); diff --git a/apps/backend/src/auth/strategies/google.strategy.ts b/apps/backend/src/auth/strategies/google.strategy.ts index a19e1789..f751f734 100644 --- a/apps/backend/src/auth/strategies/google.strategy.ts +++ b/apps/backend/src/auth/strategies/google.strategy.ts @@ -8,13 +8,13 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { private static logger = new Logger(GoogleStrategy.name); constructor( @Inject(ConfigService) - configService: ConfigService, + configService: ConfigService ) { const GOOGLE_CLIENT_ID = configService.getOrThrow('GOOGLE_CLIENT_ID'); const GOOGLE_CLIENT_SECRET = configService.getOrThrow( - 'GOOGLE_CLIENT_SECRET', + 'GOOGLE_CLIENT_SECRET' ); const SERVER_URL = configService.getOrThrow('SERVER_URL'); @@ -23,10 +23,10 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { GoogleStrategy.logger.debug(`Google Login callbackURL ${callbackURL}`); super({ - clientID: GOOGLE_CLIENT_ID, + clientID : GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET, - callbackURL: callbackURL, - scope: ['email', 'profile'], + callbackURL : callbackURL, + scope : ['email', 'profile'] }); } @@ -34,7 +34,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { accessToken: string, refreshToken: string, profile: any, - done: VerifyCallback, + done: VerifyCallback ): any { GoogleStrategy.logger.debug(`Google Login Data ${JSON.stringify(profile)}`); done(null, profile); diff --git a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts index a77f26c6..ede8cb2e 100644 --- a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts @@ -13,12 +13,12 @@ describe('MagicLinkEmailStrategy', () => { let _configService: ConfigService; const mockUserService = { - findByEmail: jest.fn(), - createWithEmail: jest.fn(), + findByEmail : jest.fn(), + createWithEmail: jest.fn() }; const mockMailingService = { - sendEmail: jest.fn(), + sendEmail: jest.fn() }; const mockConfigService = { @@ -26,7 +26,7 @@ describe('MagicLinkEmailStrategy', () => { if (key === 'MAGIC_LINK_SECRET') return 'test_secret'; if (key === 'SERVER_URL') return 'http://localhost:3000'; return null; - }), + }) }; beforeEach(async () => { @@ -37,14 +37,14 @@ describe('MagicLinkEmailStrategy', () => { { provide: MailingService, useValue: mockMailingService }, { provide: ConfigService, useValue: mockConfigService }, { - provide: 'MAGIC_LINK_SECRET', - useValue: 'test_secret', + provide : 'MAGIC_LINK_SECRET', + useValue: 'test_secret' }, { - provide: 'SERVER_URL', - useValue: 'http://localhost:3000', - }, - ], + provide : 'SERVER_URL', + useValue: 'http://localhost:3000' + } + ] }).compile(); strategy = module.get(MagicLinkEmailStrategy); @@ -71,20 +71,20 @@ describe('MagicLinkEmailStrategy', () => { await MagicLinkEmailStrategy.sendMagicLink( 'http://localhost:3000', userService, - mailingService, + mailingService )(email, magicLink); expect(mockUserService.findByEmail).toHaveBeenCalledWith(email); expect(mockMailingService.sendEmail).toHaveBeenCalledWith({ - to: email, + to : email, context: { magicLink: 'http://localhost/api/v1/auth/magic-link/callback?token=test_token', - username: 'testuser', + username: 'testuser' }, - subject: 'Noteblock Magic Link', - template: 'magic-link', + subject : 'Noteblock Magic Link', + template: 'magic-link' }); }); @@ -102,20 +102,20 @@ describe('MagicLinkEmailStrategy', () => { await MagicLinkEmailStrategy.sendMagicLink( 'http://localhost:3000', userService, - mailingService, + mailingService )(email, magicLink); expect(mockUserService.findByEmail).toHaveBeenCalledWith(email); expect(mockMailingService.sendEmail).toHaveBeenCalledWith({ - to: email, + to : email, context: { magicLink: 'http://localhost/api/v1/auth/magic-link/callback?token=test_token', - username: 'testuser', + username: 'testuser' }, - subject: 'Welcome to Noteblock.world', - template: 'magic-link-new-account', + subject : 'Welcome to Noteblock.world', + template: 'magic-link-new-account' }); }); }); @@ -138,15 +138,15 @@ describe('MagicLinkEmailStrategy', () => { mockUserService.findByEmail.mockResolvedValue(null); mockUserService.createWithEmail.mockResolvedValue({ - email: 'test@example.com', - username: 'test', + email : 'test@example.com', + username: 'test' }); const result = await strategy.validate(payload); expect(result).toEqual({ - email: 'test@example.com', - username: 'test', + email : 'test@example.com', + username: 'test' }); }); }); diff --git a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts index eb528158..92b7aacc 100644 --- a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts +++ b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts @@ -14,7 +14,7 @@ type magicLinkCallback = (error: any, user: any) => void; @Injectable() export class MagicLinkEmailStrategy extends PassportStrategy( strategy, - 'magic-link', + 'magic-link' ) { static logger = new Logger(MagicLinkEmailStrategy.name); @@ -26,23 +26,23 @@ export class MagicLinkEmailStrategy extends PassportStrategy( @Inject(UserService) private readonly userService: UserService, @Inject(MailingService) - private readonly mailingService: MailingService, + private readonly mailingService: MailingService ) { super({ - secret: MAGIC_LINK_SECRET, - confirmUrl: `${SERVER_URL}/api/v1/auth/magic-link/confirm`, - callbackUrl: `${SERVER_URL}/api/v1/auth/magic-link/callback`, + secret : MAGIC_LINK_SECRET, + confirmUrl : `${SERVER_URL}/api/v1/auth/magic-link/confirm`, + callbackUrl : `${SERVER_URL}/api/v1/auth/magic-link/callback`, sendMagicLink: MagicLinkEmailStrategy.sendMagicLink( SERVER_URL, userService, - mailingService, + mailingService ), verify: ( payload: authenticationLinkPayload, - callback: magicLinkCallback, + callback: magicLinkCallback ) => { callback(null, this.validate(payload)); - }, + } }); } @@ -50,37 +50,37 @@ export class MagicLinkEmailStrategy extends PassportStrategy( ( SERVER_URL: string, userService: UserService, - mailingService: MailingService, + mailingService: MailingService ) => async (email: string, magicLink: string) => { const user = await userService.findByEmail(email); if (!user) { mailingService.sendEmail({ - to: email, + to : email, context: { magicLink: magicLink, - username: email.split('@')[0], + username : email.split('@')[0] }, - subject: 'Welcome to Noteblock.world', - template: 'magic-link-new-account', + subject : 'Welcome to Noteblock.world', + template: 'magic-link-new-account' }); } else { mailingService.sendEmail({ - to: email, + to : email, context: { magicLink: magicLink, - username: user.username, + username : user.username }, - subject: 'Noteblock Magic Link', - template: 'magic-link', + subject : 'Noteblock Magic Link', + template: 'magic-link' }); } }; async validate(payload: authenticationLinkPayload) { MagicLinkEmailStrategy.logger.debug( - `Validating payload: ${JSON.stringify(payload)}`, + `Validating payload: ${JSON.stringify(payload)}` ); const user = await this.userService.findByEmail(payload.destination); diff --git a/apps/backend/src/config/EnvironmentVariables.ts b/apps/backend/src/config/EnvironmentVariables.ts index cbb15109..05b667ea 100644 --- a/apps/backend/src/config/EnvironmentVariables.ts +++ b/apps/backend/src/config/EnvironmentVariables.ts @@ -97,11 +97,11 @@ export class EnvironmentVariables { export function validate(config: Record) { const validatedConfig = plainToInstance(EnvironmentVariables, config, { - enableImplicitConversion: true, + enableImplicitConversion: true }); const errors = validateSync(validatedConfig, { - skipMissingProperties: false, + skipMissingProperties: false }); if (errors.length > 0) { diff --git a/apps/backend/src/email-login/email-login.controller.spec.ts b/apps/backend/src/email-login/email-login.controller.spec.ts index 1e8e48a3..d80d3fe4 100644 --- a/apps/backend/src/email-login/email-login.controller.spec.ts +++ b/apps/backend/src/email-login/email-login.controller.spec.ts @@ -9,7 +9,7 @@ describe('EmailLoginController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [EmailLoginController], - providers: [EmailLoginService], + providers : [EmailLoginService] }).compile(); controller = module.get(EmailLoginController); diff --git a/apps/backend/src/email-login/email-login.module.ts b/apps/backend/src/email-login/email-login.module.ts index 47414fa8..7f624fa2 100644 --- a/apps/backend/src/email-login/email-login.module.ts +++ b/apps/backend/src/email-login/email-login.module.ts @@ -5,6 +5,6 @@ import { EmailLoginService } from './email-login.service'; @Module({ controllers: [EmailLoginController], - providers: [EmailLoginService], + providers : [EmailLoginService] }) export class EmailLoginModule {} diff --git a/apps/backend/src/email-login/email-login.service.spec.ts b/apps/backend/src/email-login/email-login.service.spec.ts index 1424b132..86361700 100644 --- a/apps/backend/src/email-login/email-login.service.spec.ts +++ b/apps/backend/src/email-login/email-login.service.spec.ts @@ -7,7 +7,7 @@ describe('EmailLoginService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [EmailLoginService], + providers: [EmailLoginService] }).compile(); service = module.get(EmailLoginService); diff --git a/apps/backend/src/file/file.module.ts b/apps/backend/src/file/file.module.ts index 33303b61..4ac68551 100644 --- a/apps/backend/src/file/file.module.ts +++ b/apps/backend/src/file/file.module.ts @@ -7,49 +7,49 @@ import { FileService } from './file.service'; export class FileModule { static forRootAsync(): DynamicModule { return { - module: FileModule, - imports: [ConfigModule.forRoot()], + module : FileModule, + imports : [ConfigModule.forRoot()], providers: [ { - provide: 'S3_BUCKET_SONGS', + provide : 'S3_BUCKET_SONGS', useFactory: (configService: ConfigService) => configService.getOrThrow('S3_BUCKET_SONGS'), - inject: [ConfigService], + inject: [ConfigService] }, { - provide: 'S3_BUCKET_THUMBS', + provide : 'S3_BUCKET_THUMBS', useFactory: (configService: ConfigService) => configService.getOrThrow('S3_BUCKET_THUMBS'), - inject: [ConfigService], + inject: [ConfigService] }, { - provide: 'S3_KEY', + provide : 'S3_KEY', useFactory: (configService: ConfigService) => configService.getOrThrow('S3_KEY'), - inject: [ConfigService], + inject: [ConfigService] }, { - provide: 'S3_SECRET', + provide : 'S3_SECRET', useFactory: (configService: ConfigService) => configService.getOrThrow('S3_SECRET'), - inject: [ConfigService], + inject: [ConfigService] }, { - provide: 'S3_ENDPOINT', + provide : 'S3_ENDPOINT', useFactory: (configService: ConfigService) => configService.getOrThrow('S3_ENDPOINT'), - inject: [ConfigService], + inject: [ConfigService] }, { - provide: 'S3_REGION', + provide : 'S3_REGION', useFactory: (configService: ConfigService) => configService.getOrThrow('S3_REGION'), - inject: [ConfigService], + inject: [ConfigService] }, - FileService, + FileService ], - exports: [FileService], + exports: [FileService] }; } } diff --git a/apps/backend/src/file/file.service.spec.ts b/apps/backend/src/file/file.service.spec.ts index 8d5e9e64..bf14fb02 100644 --- a/apps/backend/src/file/file.service.spec.ts +++ b/apps/backend/src/file/file.service.spec.ts @@ -1,29 +1,30 @@ +import { beforeEach, describe, expect, it, jest, mock } from 'bun:test'; + import { S3Client } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { Test, TestingModule } from '@nestjs/testing'; -import { beforeEach, describe, expect, it, jest, mock } from 'bun:test'; import { FileService } from './file.service'; mock.module('@aws-sdk/client-s3', () => { const mS3Client = { - send: jest.fn(), + send: jest.fn() }; return { - S3Client: jest.fn(() => mS3Client), - GetObjectCommand: jest.fn(), - PutObjectCommand: jest.fn(), + S3Client : jest.fn(() => mS3Client), + GetObjectCommand : jest.fn(), + PutObjectCommand : jest.fn(), HeadBucketCommand: jest.fn(), - ObjectCannedACL: { - private: 'private', - public_read: 'public-read', - }, + ObjectCannedACL : { + private : 'private', + public_read: 'public-read' + } }; }); mock.module('@aws-sdk/s3-request-presigner', () => ({ - getSignedUrl: jest.fn(), + getSignedUrl: jest.fn() })); describe('FileService', () => { @@ -35,30 +36,30 @@ describe('FileService', () => { providers: [ FileService, { - provide: 'S3_BUCKET_THUMBS', - useValue: 'test-bucket-thumbs', + provide : 'S3_BUCKET_THUMBS', + useValue: 'test-bucket-thumbs' }, { - provide: 'S3_BUCKET_SONGS', - useValue: 'test-bucket-songs', + provide : 'S3_BUCKET_SONGS', + useValue: 'test-bucket-songs' }, { - provide: 'S3_KEY', - useValue: 'test-key', + provide : 'S3_KEY', + useValue: 'test-key' }, { - provide: 'S3_SECRET', - useValue: 'test-secret', + provide : 'S3_SECRET', + useValue: 'test-secret' }, { - provide: 'S3_ENDPOINT', - useValue: 'test-endpoint', + provide : 'S3_ENDPOINT', + useValue: 'test-endpoint' }, { - provide: 'S3_REGION', - useValue: 'test-region', - }, - ], + provide : 'S3_REGION', + useValue: 'test-region' + } + ] }).compile(); fileService = module.get(FileService); @@ -105,11 +106,11 @@ describe('FileService', () => { const publicId = 'test-id'; (s3Client.send as jest.Mock).mockRejectedValueOnce( - new Error('Upload failed'), + new Error('Upload failed') ); await expect(fileService.uploadSong(buffer, publicId)).rejects.toThrow( - 'Upload failed', + 'Upload failed' ); }); @@ -128,11 +129,11 @@ describe('FileService', () => { const filename = 'test-file.nbs'; (getSignedUrl as jest.Mock).mockRejectedValueOnce( - new Error('Signed URL generation failed'), + new Error('Signed URL generation failed') ); await expect(fileService.getSongDownloadUrl(key, filename)).rejects.toThrow( - 'Signed URL generation failed', + 'Signed URL generation failed' ); }); @@ -145,7 +146,7 @@ describe('FileService', () => { const result = await fileService.uploadThumbnail(buffer, publicId); expect(result).toBe( - 'https://test-bucket-thumbs.s3.test-region.backblazeb2.com/thumbs/test-id.png', + 'https://test-bucket-thumbs.s3.test-region.backblazeb2.com/thumbs/test-id.png' ); }); @@ -162,11 +163,11 @@ describe('FileService', () => { const nbsFileUrl = 'test-file.nbs'; (s3Client.send as jest.Mock).mockRejectedValueOnce( - new Error('Deletion failed'), + new Error('Deletion failed') ); await expect(fileService.deleteSong(nbsFileUrl)).rejects.toThrow( - 'Deletion failed', + 'Deletion failed' ); }); @@ -177,8 +178,8 @@ describe('FileService', () => { Body: { transformToByteArray: jest .fn() - .mockResolvedValueOnce(new Uint8Array([1, 2, 3])), - }, + .mockResolvedValueOnce(new Uint8Array([1, 2, 3])) + } }; (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); @@ -191,7 +192,7 @@ describe('FileService', () => { expect(arrayBufferResult).toBeInstanceOf(ArrayBuffer); expect(new Uint8Array(arrayBufferResult)).toEqual( - new Uint8Array([1, 2, 3]), + new Uint8Array([1, 2, 3]) ); }); @@ -199,11 +200,11 @@ describe('FileService', () => { const nbsFileUrl = 'test-file.nbs'; (s3Client.send as jest.Mock).mockRejectedValueOnce( - new Error('Retrieval failed'), + new Error('Retrieval failed') ); await expect(fileService.getSongFile(nbsFileUrl)).rejects.toThrow( - 'Retrieval failed', + 'Retrieval failed' ); }); @@ -212,14 +213,14 @@ describe('FileService', () => { const mockResponse = { Body: { - transformToByteArray: jest.fn().mockResolvedValueOnce(null), - }, + transformToByteArray: jest.fn().mockResolvedValueOnce(null) + } }; (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); await expect(fileService.getSongFile(nbsFileUrl)).rejects.toThrow( - 'Error getting file', + 'Error getting file' ); }); diff --git a/apps/backend/src/file/file.service.ts b/apps/backend/src/file/file.service.ts index 1eda743e..b8c7c82f 100644 --- a/apps/backend/src/file/file.service.ts +++ b/apps/backend/src/file/file.service.ts @@ -5,7 +5,7 @@ import { HeadBucketCommand, ObjectCannedACL, PutObjectCommand, - S3Client, + S3Client } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { Inject, Injectable, Logger } from '@nestjs/common'; @@ -29,7 +29,7 @@ export class FileService { @Inject('S3_ENDPOINT') private readonly S3_ENDPOINT: string, @Inject('S3_REGION') - private readonly S3_REGION: string, + private readonly S3_REGION: string ) { this.s3Client = this.getS3Client(); // verify that the bucket exists @@ -40,23 +40,23 @@ export class FileService { private async verifyBucket() { try { this.logger.debug( - `Verifying buckets ${this.S3_BUCKET_SONGS} and ${this.S3_BUCKET_THUMBS}`, + `Verifying buckets ${this.S3_BUCKET_SONGS} and ${this.S3_BUCKET_THUMBS}` ); await Promise.all([ this.s3Client.send( - new HeadBucketCommand({ Bucket: this.S3_BUCKET_SONGS }), + new HeadBucketCommand({ Bucket: this.S3_BUCKET_SONGS }) ), this.s3Client.send( - new HeadBucketCommand({ Bucket: this.S3_BUCKET_THUMBS }), - ), + new HeadBucketCommand({ Bucket: this.S3_BUCKET_THUMBS }) + ) ]); this.logger.debug('Buckets verification successful'); } catch (error) { this.logger.error( `Error verifying buckets ${this.S3_BUCKET_SONGS} and ${this.S3_BUCKET_THUMBS}`, - error, + error ); throw error; @@ -74,13 +74,13 @@ export class FileService { // Create S3 client const s3Client = new S3Client({ - region: region, - endpoint: endpoint, + region : region, + endpoint : endpoint, credentials: { - accessKeyId: key, - secretAccessKey: secret, + accessKeyId : key, + secretAccessKey: secret }, - forcePathStyle: endpoint.includes('localhost') ? true : false, + forcePathStyle: endpoint.includes('localhost') ? true : false }); return s3Client; @@ -100,7 +100,7 @@ export class FileService { bucket, fileName, mimetype, - ObjectCannedACL.private, + ObjectCannedACL.private ); return fileName; @@ -119,7 +119,7 @@ export class FileService { bucket, fileName, mimetype, - ObjectCannedACL.private, + ObjectCannedACL.private ); return fileName; @@ -129,16 +129,16 @@ export class FileService { const bucket = this.S3_BUCKET_SONGS; const command = new GetObjectCommand({ - Bucket: bucket, - Key: key, + Bucket : bucket, + Key : key, ResponseContentDisposition: `attachment; filename="${filename.replace( /[/"]/g, - '_', - )}"`, + '_' + )}"` }); const signedUrl = await getSignedUrl(this.s3Client, command, { - expiresIn: 2 * 60, // 2 minutes + expiresIn: 2 * 60 // 2 minutes }); return signedUrl; @@ -157,7 +157,7 @@ export class FileService { bucket, fileName, mimetype, - ObjectCannedACL.public_read, + ObjectCannedACL.public_read ); return this.getThumbnailUrl(fileName); @@ -183,7 +183,7 @@ export class FileService { const command = new GetObjectCommand({ Bucket: bucket, - Key: nbsFileUrl, + Key : nbsFileUrl }); try { @@ -201,18 +201,18 @@ export class FileService { bucket: string, name: string, mimetype: string, - accessControl: ObjectCannedACL = ObjectCannedACL.public_read, + accessControl: ObjectCannedACL = ObjectCannedACL.public_read ) { const params = { - Bucket: bucket, - Key: String(name), - Body: file, - ACL: accessControl, - ContentType: mimetype, - ContentDisposition: `attachment; filename=${name.split('/').pop()}`, + Bucket : bucket, + Key : String(name), + Body : file, + ACL : accessControl, + ContentType : mimetype, + ContentDisposition : `attachment; filename=${name.split('/').pop()}`, CreateBucketConfiguration: { - LocationConstraint: 'ap-south-1', - }, + LocationConstraint: 'ap-south-1' + } }; const command = new PutObjectCommand(params); @@ -231,7 +231,7 @@ export class FileService { const command = new GetObjectCommand({ Bucket: bucket, - Key: nbsFileUrl, + Key : nbsFileUrl }); try { diff --git a/apps/backend/src/lib/GetRequestUser.spec.ts b/apps/backend/src/lib/GetRequestUser.spec.ts index ebc2e65f..116b4d18 100644 --- a/apps/backend/src/lib/GetRequestUser.spec.ts +++ b/apps/backend/src/lib/GetRequestUser.spec.ts @@ -6,7 +6,7 @@ import { GetRequestToken, validateUser } from './GetRequestUser'; describe('GetRequestToken', () => { it('should be a defined decorator', () => { const mockExecutionContext = { - switchToHttp: jest.fn().mockReturnThis(), + switchToHttp: jest.fn().mockReturnThis() } as unknown as ExecutionContext; const result = GetRequestToken(null, mockExecutionContext); @@ -18,8 +18,8 @@ describe('GetRequestToken', () => { describe('validateUser', () => { it('should return the user if the user exists', () => { const mockUser = { - _id: 'test-id', - username: 'testuser', + _id : 'test-id', + username: 'testuser' } as unknown as UserDocument; const result = validateUser(mockUser); @@ -32,11 +32,11 @@ describe('validateUser', () => { new HttpException( { error: { - user: 'User not found', - }, + user: 'User not found' + } }, - HttpStatus.UNAUTHORIZED, - ), + HttpStatus.UNAUTHORIZED + ) ); }); }); diff --git a/apps/backend/src/lib/GetRequestUser.ts b/apps/backend/src/lib/GetRequestUser.ts index ab2d581c..13d37d84 100644 --- a/apps/backend/src/lib/GetRequestUser.ts +++ b/apps/backend/src/lib/GetRequestUser.ts @@ -3,7 +3,7 @@ import { ExecutionContext, HttpException, HttpStatus, - createParamDecorator, + createParamDecorator } from '@nestjs/common'; import type { Request } from 'express'; @@ -16,7 +16,7 @@ export const GetRequestToken = createParamDecorator( const user = req.existingUser; return user; - }, + } ); export const validateUser = (user: UserDocument | null) => { @@ -24,10 +24,10 @@ export const validateUser = (user: UserDocument | null) => { throw new HttpException( { error: { - user: 'User not found', - }, + user: 'User not found' + } }, - HttpStatus.UNAUTHORIZED, + HttpStatus.UNAUTHORIZED ); } diff --git a/apps/backend/src/lib/initializeSwagger.spec.ts b/apps/backend/src/lib/initializeSwagger.spec.ts index 8792226b..319fbf17 100644 --- a/apps/backend/src/lib/initializeSwagger.spec.ts +++ b/apps/backend/src/lib/initializeSwagger.spec.ts @@ -1,21 +1,22 @@ +import { beforeEach, describe, expect, it, jest, mock } from 'bun:test'; + import { INestApplication } from '@nestjs/common'; import { SwaggerModule } from '@nestjs/swagger'; -import { beforeEach, describe, expect, it, jest, mock } from 'bun:test'; import { initializeSwagger } from './initializeSwagger'; mock.module('@nestjs/swagger', () => ({ DocumentBuilder: jest.fn().mockImplementation(() => ({ - setTitle: jest.fn().mockReturnThis(), + setTitle : jest.fn().mockReturnThis(), setDescription: jest.fn().mockReturnThis(), - setVersion: jest.fn().mockReturnThis(), - addBearerAuth: jest.fn().mockReturnThis(), - build: jest.fn().mockReturnValue({}), + setVersion : jest.fn().mockReturnThis(), + addBearerAuth : jest.fn().mockReturnThis(), + build : jest.fn().mockReturnValue({}) })), SwaggerModule: { createDocument: jest.fn().mockReturnValue({}), - setup: jest.fn(), - }, + setup : jest.fn() + } })); describe('initializeSwagger', () => { @@ -30,7 +31,7 @@ describe('initializeSwagger', () => { expect(SwaggerModule.createDocument).toHaveBeenCalledWith( app, - expect.any(Object), + expect.any(Object) ); expect(SwaggerModule.setup).toHaveBeenCalledWith( @@ -39,9 +40,9 @@ describe('initializeSwagger', () => { expect.any(Object), { swaggerOptions: { - persistAuthorization: true, - }, - }, + persistAuthorization: true + } + } ); }); }); diff --git a/apps/backend/src/lib/initializeSwagger.ts b/apps/backend/src/lib/initializeSwagger.ts index 2e45498c..06c0871d 100644 --- a/apps/backend/src/lib/initializeSwagger.ts +++ b/apps/backend/src/lib/initializeSwagger.ts @@ -2,7 +2,7 @@ import { INestApplication } from '@nestjs/common'; import { DocumentBuilder, SwaggerCustomOptions, - SwaggerModule, + SwaggerModule } from '@nestjs/swagger'; export function initializeSwagger(app: INestApplication) { @@ -17,8 +17,8 @@ export function initializeSwagger(app: INestApplication) { const swaggerOptions: SwaggerCustomOptions = { swaggerOptions: { - persistAuthorization: true, - }, + persistAuthorization: true + } }; SwaggerModule.setup('docs', app, document, swaggerOptions); diff --git a/apps/backend/src/lib/parseToken.spec.ts b/apps/backend/src/lib/parseToken.spec.ts index 8db755f3..90408751 100644 --- a/apps/backend/src/lib/parseToken.spec.ts +++ b/apps/backend/src/lib/parseToken.spec.ts @@ -14,12 +14,12 @@ describe('ParseTokenPipe', () => { providers: [ ParseTokenPipe, { - provide: AuthService, + provide : AuthService, useValue: { - getUserFromToken: jest.fn(), - }, - }, - ], + getUserFromToken: jest.fn() + } + } + ] }).compile(); parseTokenPipe = module.get(ParseTokenPipe); @@ -34,7 +34,7 @@ describe('ParseTokenPipe', () => { it('should return true if no authorization header is present', async () => { const mockExecutionContext = { switchToHttp: jest.fn().mockReturnThis(), - getRequest: jest.fn().mockReturnValue({ headers: {} }), + getRequest : jest.fn().mockReturnValue({ headers: {} }) } as unknown as ExecutionContext; const result = await parseTokenPipe.canActivate(mockExecutionContext); @@ -45,9 +45,9 @@ describe('ParseTokenPipe', () => { it('should return true if user is not found from token', async () => { const mockExecutionContext = { switchToHttp: jest.fn().mockReturnThis(), - getRequest: jest.fn().mockReturnValue({ - headers: { authorization: 'Bearer test-token' }, - }), + getRequest : jest.fn().mockReturnValue({ + headers: { authorization: 'Bearer test-token' } + }) } as unknown as ExecutionContext; jest.spyOn(authService, 'getUserFromToken').mockResolvedValue(null); @@ -63,10 +63,10 @@ describe('ParseTokenPipe', () => { const mockExecutionContext = { switchToHttp: jest.fn().mockReturnThis(), - getRequest: jest.fn().mockReturnValue({ - headers: { authorization: 'Bearer test-token' }, - existingUser: null, - }), + getRequest : jest.fn().mockReturnValue({ + headers : { authorization: 'Bearer test-token' }, + existingUser: null + }) } as unknown as ExecutionContext; jest.spyOn(authService, 'getUserFromToken').mockResolvedValue(mockUser); @@ -77,7 +77,7 @@ describe('ParseTokenPipe', () => { expect(authService.getUserFromToken).toHaveBeenCalledWith('test-token'); expect( - mockExecutionContext.switchToHttp().getRequest().existingUser, + mockExecutionContext.switchToHttp().getRequest().existingUser ).toEqual(mockUser); }); }); diff --git a/apps/backend/src/lib/parseToken.ts b/apps/backend/src/lib/parseToken.ts index 0b6d090b..5f2d0ba6 100644 --- a/apps/backend/src/lib/parseToken.ts +++ b/apps/backend/src/lib/parseToken.ts @@ -3,7 +3,7 @@ import { ExecutionContext, Inject, Injectable, - Logger, + Logger } from '@nestjs/common'; import { AuthService } from '@server/auth/auth.service'; @@ -14,7 +14,7 @@ export class ParseTokenPipe implements CanActivate { constructor( @Inject(AuthService) - private readonly authService: AuthService, + private readonly authService: AuthService ) {} async canActivate(context: ExecutionContext): Promise { diff --git a/apps/backend/src/mailing/mailing.controller.spec.ts b/apps/backend/src/mailing/mailing.controller.spec.ts index cebfd371..584f022f 100644 --- a/apps/backend/src/mailing/mailing.controller.spec.ts +++ b/apps/backend/src/mailing/mailing.controller.spec.ts @@ -9,12 +9,12 @@ describe('MailingController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [MailingController], - providers: [ + providers : [ { - provide: MailingService, - useValue: {}, - }, - ], + provide : MailingService, + useValue: {} + } + ] }).compile(); controller = module.get(MailingController); diff --git a/apps/backend/src/mailing/mailing.module.ts b/apps/backend/src/mailing/mailing.module.ts index fb737e0e..f732bf1e 100644 --- a/apps/backend/src/mailing/mailing.module.ts +++ b/apps/backend/src/mailing/mailing.module.ts @@ -5,7 +5,7 @@ import { MailingService } from './mailing.service'; @Module({ controllers: [MailingController], - providers: [MailingService], - exports: [MailingService], + providers : [MailingService], + exports : [MailingService] }) export class MailingModule {} diff --git a/apps/backend/src/mailing/mailing.service.spec.ts b/apps/backend/src/mailing/mailing.service.spec.ts index 4918150e..bcbd8763 100644 --- a/apps/backend/src/mailing/mailing.service.spec.ts +++ b/apps/backend/src/mailing/mailing.service.spec.ts @@ -4,7 +4,7 @@ import { MailerService } from '@nestjs-modules/mailer'; import { MailingService } from './mailing.service'; const MockedMailerService = { - sendMail: jest.fn(), + sendMail: jest.fn() }; describe('MailingService', () => { @@ -16,10 +16,10 @@ describe('MailingService', () => { providers: [ MailingService, { - provide: MailerService, - useValue: MockedMailerService, - }, - ], + provide : MailerService, + useValue: MockedMailerService + } + ] }).compile(); service = module.get(MailingService); @@ -40,8 +40,8 @@ describe('MailingService', () => { const template = 'hello'; const context = { - name: 'John Doe', - message: 'Hello, this is a test email!', + name : 'John Doe', + message: 'Hello, this is a test email!' }; await service.sendEmail({ to, subject, template, context }); @@ -54,15 +54,15 @@ describe('MailingService', () => { attachments: [ { filename: 'background-image.png', - cid: 'background-image', - path: `${__dirname}/templates/img/background-image.png`, + cid : 'background-image', + path : `${__dirname}/templates/img/background-image.png` }, { filename: 'logo.png', - cid: 'logo', - path: `${__dirname}/templates/img/logo.png`, - }, - ], + cid : 'logo', + path : `${__dirname}/templates/img/logo.png` + } + ] }); }); }); diff --git a/apps/backend/src/mailing/mailing.service.ts b/apps/backend/src/mailing/mailing.service.ts index 8fd5b447..e73a53d7 100644 --- a/apps/backend/src/mailing/mailing.service.ts +++ b/apps/backend/src/mailing/mailing.service.ts @@ -15,33 +15,33 @@ export class MailingService { private readonly logger = new Logger(MailingService.name); constructor( @Inject(MailerService) - private readonly mailerService: MailerService, + private readonly mailerService: MailerService ) {} async sendEmail({ to, subject, template, - context, + context }: EmailOptions): Promise { try { await this.mailerService.sendMail({ to, subject, - template: `${template}`, // The template file name (without extension) + template : `${template}`, // The template file name (without extension) context, // The context to be passed to the template attachments: [ { filename: 'background-image.png', - cid: 'background-image', - path: `${__dirname}/templates/img/background-image.png`, + cid : 'background-image', + path : `${__dirname}/templates/img/background-image.png` }, { filename: 'logo.png', - cid: 'logo', - path: `${__dirname}/templates/img/logo.png`, - }, - ], + cid : 'logo', + path : `${__dirname}/templates/img/logo.png` + } + ] }); this.logger.debug(`Email sent to ${to}`); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 389ee3c0..7f1f60ab 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -18,11 +18,11 @@ async function bootstrap() { app.useGlobalPipes( new ValidationPipe({ - transform: true, + transform : true, transformOptions: { - enableImplicitConversion: true, - }, - }), + enableImplicitConversion: true + } + }) ); app.use(express.json({ limit: '50mb' })); @@ -36,8 +36,8 @@ async function bootstrap() { app.enableCors({ allowedHeaders: ['content-type', 'authorization', 'src'], exposedHeaders: ['Content-Disposition'], - origin: [process.env.FRONTEND_URL || '', 'https://bentroen.github.io'], - credentials: true, + origin : [process.env.FRONTEND_URL || '', 'https://bentroen.github.io'], + credentials : true }); app.use('/v1', express.static('public')); diff --git a/apps/backend/src/seed/seed.controller.spec.ts b/apps/backend/src/seed/seed.controller.spec.ts index 63cc91ea..9dfbd229 100644 --- a/apps/backend/src/seed/seed.controller.spec.ts +++ b/apps/backend/src/seed/seed.controller.spec.ts @@ -9,12 +9,12 @@ describe('SeedController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [SeedController], - providers: [ + providers : [ { - provide: SeedService, - useValue: {}, - }, - ], + provide : SeedService, + useValue: {} + } + ] }).compile(); controller = module.get(SeedController); diff --git a/apps/backend/src/seed/seed.controller.ts b/apps/backend/src/seed/seed.controller.ts index ae82f980..5e9b1ed5 100644 --- a/apps/backend/src/seed/seed.controller.ts +++ b/apps/backend/src/seed/seed.controller.ts @@ -10,12 +10,12 @@ export class SeedController { @Get('seed-dev') @ApiOperation({ - summary: 'Seed the database with development data', + summary: 'Seed the database with development data' }) async seed() { this.seedService.seedDev(); return { - message: 'Seeding in progress', + message: 'Seeding in progress' }; } } diff --git a/apps/backend/src/seed/seed.module.ts b/apps/backend/src/seed/seed.module.ts index bf4ded79..5d8f01d7 100644 --- a/apps/backend/src/seed/seed.module.ts +++ b/apps/backend/src/seed/seed.module.ts @@ -16,24 +16,24 @@ export class SeedModule { if (env.NODE_ENV !== 'development') { SeedModule.logger.warn('Seeding is only allowed in development mode'); return { - module: SeedModule, + module: SeedModule }; } else { SeedModule.logger.warn('Seeding is allowed in development mode'); return { - module: SeedModule, - imports: [UserModule, SongModule, ConfigModule.forRoot()], + module : SeedModule, + imports : [UserModule, SongModule, ConfigModule.forRoot()], providers: [ ConfigService, SeedService, { - provide: 'NODE_ENV', + provide : 'NODE_ENV', useFactory: (configService: ConfigService) => configService.get('NODE_ENV'), - inject: [ConfigService], - }, + inject: [ConfigService] + } ], - controllers: [SeedController], + controllers: [SeedController] }; } } diff --git a/apps/backend/src/seed/seed.service.spec.ts b/apps/backend/src/seed/seed.service.spec.ts index 7577e9c4..93fd575a 100644 --- a/apps/backend/src/seed/seed.service.spec.ts +++ b/apps/backend/src/seed/seed.service.spec.ts @@ -13,23 +13,23 @@ describe('SeedService', () => { providers: [ SeedService, { - provide: 'NODE_ENV', - useValue: 'development', + provide : 'NODE_ENV', + useValue: 'development' }, { - provide: UserService, + provide : UserService, useValue: { - createWithPassword: jest.fn(), - }, + createWithPassword: jest.fn() + } }, { - provide: SongService, + provide : SongService, useValue: { - uploadSong: jest.fn(), - getSongById: jest.fn(), - }, - }, - ], + uploadSong : jest.fn(), + getSongById: jest.fn() + } + } + ] }).compile(); service = module.get(SeedService); diff --git a/apps/backend/src/seed/seed.service.ts b/apps/backend/src/seed/seed.service.ts index af3798eb..3882ec17 100644 --- a/apps/backend/src/seed/seed.service.ts +++ b/apps/backend/src/seed/seed.service.ts @@ -7,14 +7,14 @@ import { SongDocument, UploadSongDto, UserDocument, - VisibilityType, + VisibilityType } from '@nbw/database'; import { HttpException, HttpStatus, Inject, Injectable, - Logger, + Logger } from '@nestjs/common'; import { SongService } from '@server/song/song.service'; @@ -31,7 +31,7 @@ export class SeedService { private readonly userService: UserService, @Inject(SongService) - private readonly songService: SongService, + private readonly songService: SongService ) {} public async seedDev() { @@ -51,13 +51,13 @@ export class SeedService { for (let i = 0; i < 100; i++) { const user = await this.userService.createWithEmail( - faker.internet.email(), + faker.internet.email() ); //change user creation date (user as any).createdAt = this.generateRandomDate( new Date(2020, 0, 1), - new Date(), + new Date() ); user.loginCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); @@ -65,21 +65,21 @@ export class SeedService { user.description = faker.lorem.paragraph(); user.socialLinks = { - youtube: faker.internet.url(), - x: faker.internet.url(), - discord: faker.internet.url(), - instagram: faker.internet.url(), - twitch: faker.internet.url(), - bandcamp: faker.internet.url(), - facebook: faker.internet.url(), - github: faker.internet.url(), - reddit: faker.internet.url(), - snapchat: faker.internet.url(), + youtube : faker.internet.url(), + x : faker.internet.url(), + discord : faker.internet.url(), + instagram : faker.internet.url(), + twitch : faker.internet.url(), + bandcamp : faker.internet.url(), + facebook : faker.internet.url(), + github : faker.internet.url(), + reddit : faker.internet.url(), + snapchat : faker.internet.url(), soundcloud: faker.internet.url(), - spotify: faker.internet.url(), - steam: faker.internet.url(), - telegram: faker.internet.url(), - tiktok: faker.internet.url(), + spotify : faker.internet.url(), + steam : faker.internet.url(), + telegram : faker.internet.url(), + tiktok : faker.internet.url() }; // remove some social links randomly to simulate users not having all of them or having none @@ -109,37 +109,37 @@ export class SeedService { const body: UploadSongDto = { file: { - buffer: fileData, - size: fileBuffer.length, - mimetype: 'application/octet-stream', - originalname: `${faker.music.songName()}.nbs`, + buffer : fileData, + size : fileBuffer.length, + mimetype : 'application/octet-stream', + originalname: `${faker.music.songName()}.nbs` }, allowDownload: faker.datatype.boolean(), - visibility: faker.helpers.arrayElement( - visibilities, + visibility : faker.helpers.arrayElement( + visibilities ) as VisibilityType, - title: faker.music.songName(), - originalAuthor: faker.music.artist(), - description: faker.lorem.paragraph(), - license: faker.helpers.arrayElement(licenses) as LicenseType, - category: faker.helpers.arrayElement(categories) as CategoryType, + title : faker.music.songName(), + originalAuthor : faker.music.artist(), + description : faker.lorem.paragraph(), + license : faker.helpers.arrayElement(licenses) as LicenseType, + category : faker.helpers.arrayElement(categories) as CategoryType, customInstruments: [], - thumbnailData: { + thumbnailData : { backgroundColor: faker.internet.color(), - startLayer: faker.helpers.rangeToNumber({ min: 0, max: 4 }), - startTick: faker.helpers.rangeToNumber({ min: 0, max: 100 }), - zoomLevel: faker.helpers.rangeToNumber({ min: 1, max: 5 }), - }, + startLayer : faker.helpers.rangeToNumber({ min: 0, max: 4 }), + startTick : faker.helpers.rangeToNumber({ min: 0, max: 100 }), + zoomLevel : faker.helpers.rangeToNumber({ min: 1, max: 5 }) + } }; const uploadSongResponse = await this.songService.uploadSong({ user, body, - file: body.file, + file: body.file }); const song = await this.songService.getSongById( - uploadSongResponse.publicId, + uploadSongResponse.publicId ); if (!song) continue; @@ -147,7 +147,7 @@ export class SeedService { //change song creation date (song as any).createdAt = this.generateRandomDate( new Date(2020, 0, 1), - new Date(), + new Date() ); song.playCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); @@ -166,7 +166,7 @@ export class SeedService { start = 1, stepScale = 2, stepProbability = 0.5, - limit = Number.MAX_SAFE_INTEGER, + limit = Number.MAX_SAFE_INTEGER ) { let max = start; @@ -193,15 +193,15 @@ export class SeedService { layer.meta.name = instrument.meta.name; const notes = Array.from({ - length: faker.helpers.rangeToNumber({ min: 20, max: 120 }), + length: faker.helpers.rangeToNumber({ min: 20, max: 120 }) }).map( () => new Note(instrument, { - key: faker.helpers.rangeToNumber({ min: 0, max: 127 }), + key : faker.helpers.rangeToNumber({ min: 0, max: 127 }), velocity: faker.helpers.rangeToNumber({ min: 0, max: 127 }), - panning: faker.helpers.rangeToNumber({ min: -1, max: 1 }), - pitch: faker.helpers.rangeToNumber({ min: -1, max: 1 }), - }), + panning : faker.helpers.rangeToNumber({ min: -1, max: 1 }), + pitch : faker.helpers.rangeToNumber({ min: -1, max: 1 }) + }) ); for (let i = 0; i < notes.length; i++) @@ -216,8 +216,8 @@ export class SeedService { return new Date( faker.date.between({ from: from.getTime(), - to: to.getTime(), - }), + to : to.getTime() + }) ); } } diff --git a/apps/backend/src/song-browser/song-browser.controller.spec.ts b/apps/backend/src/song-browser/song-browser.controller.spec.ts index 0e95d2ff..1e16f027 100644 --- a/apps/backend/src/song-browser/song-browser.controller.spec.ts +++ b/apps/backend/src/song-browser/song-browser.controller.spec.ts @@ -5,10 +5,10 @@ import { SongBrowserController } from './song-browser.controller'; import { SongBrowserService } from './song-browser.service'; const mockSongBrowserService = { - getFeaturedSongs: jest.fn(), - getRecentSongs: jest.fn(), - getCategories: jest.fn(), - getSongsByCategory: jest.fn(), + getFeaturedSongs : jest.fn(), + getRecentSongs : jest.fn(), + getCategories : jest.fn(), + getSongsByCategory: jest.fn() }; describe('SongBrowserController', () => { @@ -18,12 +18,12 @@ describe('SongBrowserController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [SongBrowserController], - providers: [ + providers : [ { - provide: SongBrowserService, - useValue: mockSongBrowserService, - }, - ], + provide : SongBrowserService, + useValue: mockSongBrowserService + } + ] }).compile(); controller = module.get(SongBrowserController); @@ -39,7 +39,7 @@ describe('SongBrowserController', () => { const featuredSongs: FeaturedSongsDto = {} as FeaturedSongsDto; mockSongBrowserService.getFeaturedSongs.mockResolvedValueOnce( - featuredSongs, + featuredSongs ); const result = await controller.getFeaturedSongs(); @@ -67,7 +67,7 @@ describe('SongBrowserController', () => { it('should return a list of song categories and song counts', async () => { const categories: Record = { category1: 10, - category2: 5, + category2: 5 }; mockSongBrowserService.getCategories.mockResolvedValueOnce(categories); @@ -93,7 +93,7 @@ describe('SongBrowserController', () => { expect(songBrowserService.getSongsByCategory).toHaveBeenCalledWith( id, - query, + query ); }); }); diff --git a/apps/backend/src/song-browser/song-browser.controller.ts b/apps/backend/src/song-browser/song-browser.controller.ts index f9746d1e..1f2aaf8f 100644 --- a/apps/backend/src/song-browser/song-browser.controller.ts +++ b/apps/backend/src/song-browser/song-browser.controller.ts @@ -4,7 +4,7 @@ import { Controller, Get, Param, - Query, + Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; @@ -24,10 +24,10 @@ export class SongBrowserController { @Get('/recent') @ApiOperation({ - summary: 'Get a filtered/sorted list of recent songs with pagination', + summary: 'Get a filtered/sorted list of recent songs with pagination' }) public async getSongList( - @Query() query: PageQueryDTO, + @Query() query: PageQueryDTO ): Promise { return await this.songBrowserService.getRecentSongs(query); } @@ -42,7 +42,7 @@ export class SongBrowserController { @ApiOperation({ summary: 'Get a list of song categories and song counts' }) public async getSongsByCategory( @Param('id') id: string, - @Query() query: PageQueryDTO, + @Query() query: PageQueryDTO ): Promise { return await this.songBrowserService.getSongsByCategory(id, query); } @@ -51,7 +51,7 @@ export class SongBrowserController { @ApiOperation({ summary: 'Get a list of songs at random' }) public async getRandomSongs( @Query('count') count: string, - @Query('category') category: string, + @Query('category') category: string ): Promise { const countInt = parseInt(count); diff --git a/apps/backend/src/song-browser/song-browser.module.ts b/apps/backend/src/song-browser/song-browser.module.ts index f1eb25e0..22600971 100644 --- a/apps/backend/src/song-browser/song-browser.module.ts +++ b/apps/backend/src/song-browser/song-browser.module.ts @@ -6,8 +6,8 @@ import { SongBrowserController } from './song-browser.controller'; import { SongBrowserService } from './song-browser.service'; @Module({ - providers: [SongBrowserService], + providers : [SongBrowserService], controllers: [SongBrowserController], - imports: [SongModule], + imports : [SongModule] }) export class SongBrowserModule {} diff --git a/apps/backend/src/song-browser/song-browser.service.spec.ts b/apps/backend/src/song-browser/song-browser.service.spec.ts index f46f98d6..a2bc2e3e 100644 --- a/apps/backend/src/song-browser/song-browser.service.spec.ts +++ b/apps/backend/src/song-browser/song-browser.service.spec.ts @@ -7,11 +7,11 @@ import { SongService } from '@server/song/song.service'; import { SongBrowserService } from './song-browser.service'; const mockSongService = { - getSongsForTimespan: jest.fn(), + getSongsForTimespan : jest.fn(), getSongsBeforeTimespan: jest.fn(), - getRecentSongs: jest.fn(), - getCategories: jest.fn(), - getSongsByCategory: jest.fn(), + getRecentSongs : jest.fn(), + getCategories : jest.fn(), + getSongsByCategory : jest.fn() }; describe('SongBrowserService', () => { @@ -23,10 +23,10 @@ describe('SongBrowserService', () => { providers: [ SongBrowserService, { - provide: SongService, - useValue: mockSongService, - }, - ], + provide : SongService, + useValue: mockSongService + } + ] }).compile(); service = module.get(SongBrowserService); @@ -40,12 +40,12 @@ describe('SongBrowserService', () => { describe('getFeaturedSongs', () => { it('should return featured songs', async () => { const songWithUser: SongWithUser = { - title: 'Test Song', + title : 'Test Song', uploader: { username: 'testuser', profileImage: 'testimage' }, - stats: { - duration: 100, - noteCount: 100, - }, + stats : { + duration : 100, + noteCount: 100 + } } as any; jest @@ -68,8 +68,8 @@ describe('SongBrowserService', () => { const query: PageQueryDTO = { page: 1, limit: 10 }; const songPreviewDto: SongPreviewDto = { - title: 'Test Song', - uploader: { username: 'testuser', profileImage: 'testimage' }, + title : 'Test Song', + uploader: { username: 'testuser', profileImage: 'testimage' } } as any; jest @@ -82,7 +82,7 @@ describe('SongBrowserService', () => { expect(songService.getRecentSongs).toHaveBeenCalledWith( query.page, - query.limit, + query.limit ); }); @@ -90,7 +90,7 @@ describe('SongBrowserService', () => { const query: PageQueryDTO = { page: undefined, limit: undefined }; await expect(service.getRecentSongs(query)).rejects.toThrow( - HttpException, + HttpException ); }); }); @@ -114,8 +114,8 @@ describe('SongBrowserService', () => { const query: PageQueryDTO = { page: 1, limit: 10 }; const songPreviewDto: SongPreviewDto = { - title: 'Test Song', - uploader: { username: 'testuser', profileImage: 'testimage' }, + title : 'Test Song', + uploader: { username: 'testuser', profileImage: 'testimage' } } as any; jest @@ -129,7 +129,7 @@ describe('SongBrowserService', () => { expect(songService.getSongsByCategory).toHaveBeenCalledWith( category, query.page, - query.limit, + query.limit ); }); }); diff --git a/apps/backend/src/song-browser/song-browser.service.ts b/apps/backend/src/song-browser/song-browser.service.ts index 0739d5c0..1da8635e 100644 --- a/apps/backend/src/song-browser/song-browser.service.ts +++ b/apps/backend/src/song-browser/song-browser.service.ts @@ -4,7 +4,7 @@ import { PageQueryDTO, SongPreviewDto, SongWithUser, - TimespanType, + TimespanType } from '@nbw/database'; import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; @@ -14,28 +14,28 @@ import { SongService } from '@server/song/song.service'; export class SongBrowserService { constructor( @Inject(SongService) - private songService: SongService, + private songService: SongService ) {} public async getFeaturedSongs(): Promise { const now = new Date(Date.now()); const times: Record = { - hour: new Date(Date.now()).setHours(now.getHours() - 1), - day: new Date(Date.now()).setDate(now.getDate() - 1), - week: new Date(Date.now()).setDate(now.getDate() - 7), + hour : new Date(Date.now()).setHours(now.getHours() - 1), + day : new Date(Date.now()).setDate(now.getDate() - 1), + week : new Date(Date.now()).setDate(now.getDate() - 7), month: new Date(Date.now()).setMonth(now.getMonth() - 1), - year: new Date(Date.now()).setFullYear(now.getFullYear() - 1), - all: new Date(0).getTime(), + year : new Date(Date.now()).setFullYear(now.getFullYear() - 1), + all : new Date(0).getTime() }; const songs: Record = { - hour: [], - day: [], - week: [], + hour : [], + day : [], + week : [], month: [], - year: [], - all: [], + year : [], + all : [] }; for (const [timespan, time] of Object.entries(times)) { @@ -51,7 +51,7 @@ export class SongBrowserService { const missing = BROWSER_SONGS.paddedFeaturedPageSize - songPage.length; const additionalSongs = await this.songService.getSongsBeforeTimespan( - time, + time ); songPage.push(...additionalSongs.slice(0, missing)); @@ -63,27 +63,27 @@ export class SongBrowserService { const featuredSongs = FeaturedSongsDto.create(); featuredSongs.hour = songs.hour.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song), + SongPreviewDto.fromSongDocumentWithUser(song) ); featuredSongs.day = songs.day.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song), + SongPreviewDto.fromSongDocumentWithUser(song) ); featuredSongs.week = songs.week.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song), + SongPreviewDto.fromSongDocumentWithUser(song) ); featuredSongs.month = songs.month.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song), + SongPreviewDto.fromSongDocumentWithUser(song) ); featuredSongs.year = songs.year.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song), + SongPreviewDto.fromSongDocumentWithUser(song) ); featuredSongs.all = songs.all.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song), + SongPreviewDto.fromSongDocumentWithUser(song) ); return featuredSongs; @@ -95,7 +95,7 @@ export class SongBrowserService { if (!page || !limit) { throw new HttpException( 'Invalid query parameters', - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } @@ -108,18 +108,18 @@ export class SongBrowserService { public async getSongsByCategory( category: string, - query: PageQueryDTO, + query: PageQueryDTO ): Promise { return await this.songService.getSongsByCategory( category, query.page ?? 1, - query.limit ?? 10, + query.limit ?? 10 ); } public async getRandomSongs( count: number, - category: string, + category: string ): Promise { return await this.songService.getRandomSongs(count, category); } diff --git a/apps/backend/src/song/my-songs/my-songs.controller.spec.ts b/apps/backend/src/song/my-songs/my-songs.controller.spec.ts index 25b3d33f..8e67c8a4 100644 --- a/apps/backend/src/song/my-songs/my-songs.controller.spec.ts +++ b/apps/backend/src/song/my-songs/my-songs.controller.spec.ts @@ -5,10 +5,11 @@ import { AuthGuard } from '@nestjs/passport'; import { Test, TestingModule } from '@nestjs/testing'; import { SongService } from '../song.service'; + import { MySongsController } from './my-songs.controller'; const mockSongService = { - getMySongsPage: jest.fn(), + getMySongsPage: jest.fn() }; describe('MySongsController', () => { @@ -18,12 +19,12 @@ describe('MySongsController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [MySongsController], - providers: [ + providers : [ { - provide: SongService, - useValue: mockSongService, - }, - ], + provide : SongService, + useValue: mockSongService + } + ] }) .overrideGuard(AuthGuard('jwt-refresh')) .useValue({ canActivate: jest.fn(() => true) }) @@ -44,9 +45,9 @@ describe('MySongsController', () => { const songPageDto: SongPageDto = { content: [], - page: 0, - limit: 0, - total: 0, + page : 0, + limit : 0, + total : 0 }; mockSongService.getMySongsPage.mockResolvedValueOnce(songPageDto); @@ -62,7 +63,7 @@ describe('MySongsController', () => { const user = null; await expect(controller.getMySongsPage(query, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -74,7 +75,7 @@ describe('MySongsController', () => { mockSongService.getMySongsPage.mockRejectedValueOnce(error); await expect(controller.getMySongsPage(query, user)).rejects.toThrow( - 'Test error', + 'Test error' ); }); }); diff --git a/apps/backend/src/song/my-songs/my-songs.controller.ts b/apps/backend/src/song/my-songs/my-songs.controller.ts index 986ac5f3..1f263b84 100644 --- a/apps/backend/src/song/my-songs/my-songs.controller.ts +++ b/apps/backend/src/song/my-songs/my-songs.controller.ts @@ -16,18 +16,18 @@ export class MySongsController { @Get('/') @ApiOperation({ - summary: 'Get a list of songs uploaded by the current authenticated user', + summary: 'Get a list of songs uploaded by the current authenticated user' }) @ApiBearerAuth() @UseGuards(AuthGuard('jwt-refresh')) public async getMySongsPage( @Query() query: PageQueryDTO, - @GetRequestToken() user: UserDocument | null, + @GetRequestToken() user: UserDocument | null ): Promise { user = validateUser(user); return await this.songService.getMySongsPage({ query, - user, + user }); } } diff --git a/apps/backend/src/song/song-upload/song-upload.service.spec.ts b/apps/backend/src/song/song-upload/song-upload.service.spec.ts index c58c0d22..56758886 100644 --- a/apps/backend/src/song/song-upload/song-upload.service.spec.ts +++ b/apps/backend/src/song/song-upload/song-upload.service.spec.ts @@ -1,14 +1,15 @@ +import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; + import { Instrument, Layer, Note, Song } from '@encode42/nbs.js'; import type { UserDocument } from '@nbw/database'; import { SongDocument, Song as SongEntity, ThumbnailData, - UploadSongDto, + UploadSongDto } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; import { Types } from 'mongoose'; import { FileService } from '@server/file/file.service'; @@ -18,18 +19,18 @@ import { SongUploadService } from './song-upload.service'; // mock drawToImage function mock.module('@nbw/thumbnail', () => ({ - drawToImage: jest.fn().mockResolvedValue(Buffer.from('test-image-buffer')), + drawToImage: jest.fn().mockResolvedValue(Buffer.from('test-image-buffer')) })); const mockFileService = { - uploadSong: jest.fn(), + uploadSong : jest.fn(), uploadPackedSong: jest.fn(), - uploadThumbnail: jest.fn(), - getSongFile: jest.fn(), + uploadThumbnail : jest.fn(), + getSongFile : jest.fn() }; const mockUserService = { - findByID: jest.fn(), + findByID: jest.fn() }; describe('SongUploadService', () => { @@ -42,14 +43,14 @@ describe('SongUploadService', () => { providers: [ SongUploadService, { - provide: FileService, - useValue: mockFileService, + provide : FileService, + useValue: mockFileService }, { - provide: UserService, - useValue: mockUserService, - }, - ], + provide : UserService, + useValue: mockUserService + } + ] }).compile(); songUploadService = module.get(SongUploadService); @@ -66,94 +67,94 @@ describe('SongUploadService', () => { const file = { buffer: Buffer.from('test') } as Express.Multer.File; const user: UserDocument = { - _id: new Types.ObjectId(), - username: 'testuser', + _id : new Types.ObjectId(), + username: 'testuser' } as UserDocument; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: true, - file: 'somebytes', + file : 'somebytes' }; const songEntity = new SongEntity(); songEntity.uploader = user._id; spyOn(songUploadService as any, 'checkIsFileValid').mockImplementation( - (_file: Express.Multer.File) => undefined, + (_file: Express.Multer.File) => undefined ); spyOn(songUploadService as any, 'prepareSongForUpload').mockReturnValue({ - nbsSong: new Song(), - songBuffer: Buffer.from('test'), + nbsSong : new Song(), + songBuffer: Buffer.from('test') }); spyOn( songUploadService as any, - 'preparePackedSongForUpload', + 'preparePackedSongForUpload' ).mockResolvedValue(Buffer.from('test')); spyOn(songUploadService as any, 'generateSongDocument').mockResolvedValue( - songEntity, + songEntity ); spyOn(songUploadService, 'generateAndUploadThumbnail').mockResolvedValue( - 'http://test.com/thumbnail.png', + 'http://test.com/thumbnail.png' ); spyOn(songUploadService as any, 'uploadSongFile').mockResolvedValue( - 'http://test.com/file.nbs', + 'http://test.com/file.nbs' ); spyOn(songUploadService as any, 'uploadPackedSongFile').mockResolvedValue( - 'http://test.com/packed-file.nbs', + 'http://test.com/packed-file.nbs' ); const result = await songUploadService.processUploadedSong({ file, user, - body, + body }); expect(result).toEqual(songEntity); expect((songUploadService as any).checkIsFileValid).toHaveBeenCalledWith( - file, + file ); expect( - (songUploadService as any).prepareSongForUpload, + (songUploadService as any).prepareSongForUpload ).toHaveBeenCalledWith(file.buffer, body, user); expect( - (songUploadService as any).preparePackedSongForUpload, + (songUploadService as any).preparePackedSongForUpload ).toHaveBeenCalledWith(expect.any(Song), body.customInstruments); expect(songUploadService.generateAndUploadThumbnail).toHaveBeenCalledWith( body.thumbnailData, expect.any(Song), - expect.any(String), + expect.any(String) ); expect((songUploadService as any).uploadSongFile).toHaveBeenCalledWith( expect.any(Buffer), - expect.any(String), + expect.any(String) ); expect( - (songUploadService as any).uploadPackedSongFile, + (songUploadService as any).uploadPackedSongFile ).toHaveBeenCalledWith(expect.any(Buffer), expect.any(String)); }); }); @@ -161,60 +162,60 @@ describe('SongUploadService', () => { describe('processSongPatch', () => { it('should process and patch a song', async () => { const user: UserDocument = { - _id: new Types.ObjectId(), - username: 'testuser', + _id : new Types.ObjectId(), + username: 'testuser' } as UserDocument; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: true, - file: 'somebytes', + file : 'somebytes' }; const songDocument: SongDocument = { ...body, - publicId: 'test-id', - uploader: user._id, + publicId : 'test-id', + uploader : user._id, customInstruments: [], - thumbnailData: body.thumbnailData, - nbsFileUrl: 'http://test.com/file.nbs', - save: jest.fn().mockResolvedValue({}), + thumbnailData : body.thumbnailData, + nbsFileUrl : 'http://test.com/file.nbs', + save : jest.fn().mockResolvedValue({}) } as any; spyOn(fileService, 'getSongFile').mockResolvedValue(new ArrayBuffer(0)); spyOn(songUploadService as any, 'prepareSongForUpload').mockReturnValue({ - nbsSong: new Song(), - songBuffer: Buffer.from('test'), + nbsSong : new Song(), + songBuffer: Buffer.from('test') }); spyOn( songUploadService as any, - 'preparePackedSongForUpload', + 'preparePackedSongForUpload' ).mockResolvedValue(Buffer.from('test')); spyOn(songUploadService, 'generateAndUploadThumbnail').mockResolvedValue( - 'http://test.com/thumbnail.png', + 'http://test.com/thumbnail.png' ); spyOn(songUploadService as any, 'uploadSongFile').mockResolvedValue( - 'http://test.com/file.nbs', + 'http://test.com/file.nbs' ); spyOn(songUploadService as any, 'uploadPackedSongFile').mockResolvedValue( - 'http://test.com/packed-file.nbs', + 'http://test.com/packed-file.nbs' ); await songUploadService.processSongPatch(songDocument, body, user); @@ -224,10 +225,10 @@ describe('SongUploadService', () => { describe('generateAndUploadThumbnail', () => { it('should generate and upload a thumbnail', async () => { const thumbnailData: ThumbnailData = { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }; const nbsSong = new Song(); @@ -236,29 +237,29 @@ describe('SongUploadService', () => { const publicId = 'test-id'; spyOn(fileService, 'uploadThumbnail').mockResolvedValue( - 'http://test.com/thumbnail.png', + 'http://test.com/thumbnail.png' ); const result = await songUploadService.generateAndUploadThumbnail( thumbnailData, nbsSong, - publicId, + publicId ); expect(result).toBe('http://test.com/thumbnail.png'); expect(fileService.uploadThumbnail).toHaveBeenCalledWith( expect.any(Buffer), - publicId, + publicId ); }); it('should throw an error if the thumbnail is invalid', async () => { const thumbnailData: ThumbnailData = { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }; const nbsSong = new Song(); @@ -272,7 +273,7 @@ describe('SongUploadService', () => { await songUploadService.generateAndUploadThumbnail( thumbnailData, nbsSong, - publicId, + publicId ); } catch (error) { expect(error).toBeInstanceOf(HttpException); @@ -286,12 +287,12 @@ describe('SongUploadService', () => { const publicId = 'test-id'; spyOn(fileService, 'uploadSong').mockResolvedValue( - 'http://test.com/file.nbs', + 'http://test.com/file.nbs' ); const result = await (songUploadService as any).uploadSongFile( file, - publicId, + publicId ); expect(result).toBe('http://test.com/file.nbs'); @@ -303,7 +304,7 @@ describe('SongUploadService', () => { const publicId = 'test-id'; spyOn(fileService, 'uploadSong').mockRejectedValue( - new Error('test error'), + new Error('test error') ); try { @@ -320,12 +321,12 @@ describe('SongUploadService', () => { const publicId = 'test-id'; spyOn(fileService, 'uploadPackedSong').mockResolvedValue( - 'http://test.com/packed-file.nbs', + 'http://test.com/packed-file.nbs' ); const result = await (songUploadService as any).uploadPackedSongFile( file, - publicId, + publicId ); expect(result).toBe('http://test.com/packed-file.nbs'); @@ -337,7 +338,7 @@ describe('SongUploadService', () => { const publicId = 'test-id'; spyOn(fileService, 'uploadPackedSong').mockRejectedValue( - new Error('test error'), + new Error('test error') ); try { @@ -353,11 +354,11 @@ describe('SongUploadService', () => { const songTest = new Song(); songTest.meta = { - author: 'Nicolas Vycas', - description: 'super cool song', - importName: 'test', - name: 'Cool Test Song', - originalAuthor: 'Nicolas Vycas', + author : 'Nicolas Vycas', + description : 'super cool song', + importName : 'test', + name : 'Cool Test Song', + originalAuthor: 'Nicolas Vycas' }; songTest.tempo = 20; @@ -375,7 +376,7 @@ describe('SongUploadService', () => { new Note(instrument, { key: 45 }), new Note(instrument, { key: 50 }), new Note(instrument, { key: 45 }), - new Note(instrument, { key: 57 }), + new Note(instrument, { key: 57 }) ]; // Place the notes @@ -395,7 +396,7 @@ describe('SongUploadService', () => { const buffer = new ArrayBuffer(0); expect(() => songUploadService.getSongObject(buffer)).toThrow( - HttpException, + HttpException ); }); }); @@ -403,7 +404,7 @@ describe('SongUploadService', () => { describe('checkIsFileValid', () => { it('should throw an error if the file is not provided', () => { expect(() => (songUploadService as any).checkIsFileValid(null)).toThrow( - HttpException, + HttpException ); }); @@ -411,7 +412,7 @@ describe('SongUploadService', () => { const file = { buffer: Buffer.from('test') } as Express.Multer.File; expect(() => - (songUploadService as any).checkIsFileValid(file), + (songUploadService as any).checkIsFileValid(file) ).not.toThrow(); }); }); diff --git a/apps/backend/src/song/song-upload/song-upload.service.ts b/apps/backend/src/song/song-upload/song-upload.service.ts index bbf37b9d..cfb9a51d 100644 --- a/apps/backend/src/song/song-upload/song-upload.service.ts +++ b/apps/backend/src/song/song-upload/song-upload.service.ts @@ -5,13 +5,13 @@ import { SongStats, ThumbnailData, UploadSongDto, - UserDocument, + UserDocument } from '@nbw/database'; import { NoteQuadTree, SongStatsGenerator, injectSongFileMetadata, - obfuscateAndPackSong, + obfuscateAndPackSong } from '@nbw/song'; import { drawToImage } from '@nbw/thumbnail'; import { @@ -19,7 +19,7 @@ import { HttpStatus, Inject, Injectable, - Logger, + Logger } from '@nestjs/common'; import { Types } from 'mongoose'; @@ -41,7 +41,7 @@ export class SongUploadService { private fileService: FileService, @Inject(UserService) - private userService: UserService, + private userService: UserService ) {} private async getSoundsMapping(): Promise> { @@ -49,7 +49,7 @@ export class SongUploadService { if (!this.soundsMapping) { const response = await fetch( - process.env.SERVER_URL + '/api/v1/data/soundList.json', + process.env.SERVER_URL + '/api/v1/data/soundList.json' ); this.soundsMapping = (await response.json()) as Record; @@ -64,7 +64,7 @@ export class SongUploadService { if (!this.soundsSubset) { try { const response = await fetch( - process.env.SERVER_URL + '/api/v1/data/soundList.json', + process.env.SERVER_URL + '/api/v1/data/soundList.json' ); const soundMapping = (await response.json()) as Record; @@ -74,10 +74,10 @@ export class SongUploadService { throw new HttpException( { error: { - file: 'An error occurred while retrieving sound list', - }, + file: 'An error occurred while retrieving sound list' + } }, - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR ); } } @@ -91,7 +91,7 @@ export class SongUploadService { if (!uploader) { throw new HttpException( 'user not found, contact an administrator', - HttpStatus.UNAUTHORIZED, + HttpStatus.UNAUTHORIZED ); } @@ -106,7 +106,7 @@ export class SongUploadService { fileKey: string, packedFileKey: string, songStats: SongStats, - file: Express.Multer.File, + file: Express.Multer.File ): Promise { const song = new SongEntity(); song.uploader = await this.validateUploader(user); @@ -115,7 +115,7 @@ export class SongUploadService { song.originalAuthor = removeExtraSpaces(body.originalAuthor); song.description = removeExtraSpaces(body.description); song.category = body.category; - song.allowDownload = true || body.allowDownload; //TODO: implement allowDownload; + song.allowDownload = true;// || body.allowDownload; //TODO: implement allowDownload; song.visibility = body.visibility; song.license = body.license; @@ -125,7 +125,7 @@ export class SongUploadService { songStats.firstCustomInstrumentIndex; const paddedInstruments = body.customInstruments.concat( - Array(customInstrumentCount - body.customInstruments.length).fill(''), + Array(customInstrumentCount - body.customInstruments.length).fill('') ); song.customInstruments = paddedInstruments; @@ -142,7 +142,7 @@ export class SongUploadService { public async processUploadedSong({ file, user, - body, + body }: { body: UploadSongDto; file: Express.Multer.File; @@ -155,14 +155,14 @@ export class SongUploadService { const { nbsSong, songBuffer } = this.prepareSongForUpload( file.buffer, body, - user, + user ); // Prepare packed song for upload // This can generate a client error if the custom instruments are invalid, so it's done before the song is uploaded const packedSongBuffer = await this.preparePackedSongForUpload( nbsSong, - body.customInstruments, + body.customInstruments ); // Generate song public ID @@ -174,7 +174,7 @@ export class SongUploadService { // Upload packed song file const packedFileKey = await this.uploadPackedSongFile( packedSongBuffer, - publicId, + publicId ); // PROCESS UPLOADED SONG @@ -187,7 +187,7 @@ export class SongUploadService { const thumbUrl: string = await this.generateAndUploadThumbnail( body.thumbnailData, nbsSong, - publicId, + publicId ); // Create song document @@ -199,7 +199,7 @@ export class SongUploadService { fileKey, packedFileKey, // TODO: should be packedFileUrl songStats, - file, + file ); return song; @@ -208,7 +208,7 @@ export class SongUploadService { public async processSongPatch( songDocument: SongDocument, body: UploadSongDto, - user: UserDocument, + user: UserDocument ): Promise { // Compare arrays of custom instruments including order const customInstrumentsChanged = @@ -234,7 +234,7 @@ export class SongUploadService { // and/or regenerate and reupload the thumbnail const songFile = await this.fileService.getSongFile( - songDocument.nbsFileUrl, + songDocument.nbsFileUrl ); const originalSongBuffer = Buffer.from(songFile); @@ -246,13 +246,13 @@ export class SongUploadService { const { nbsSong, songBuffer } = this.prepareSongForUpload( originalSongBuffer, body, - user, + user ); // Obfuscate and pack song with updated custom instruments const packedSongBuffer = await this.preparePackedSongForUpload( nbsSong, - body.customInstruments, + body.customInstruments ); // Re-upload song file @@ -261,7 +261,7 @@ export class SongUploadService { // Re-upload packed song file await this.uploadPackedSongFile( packedSongBuffer, - songDocument.publicId, + songDocument.publicId ); } @@ -273,7 +273,7 @@ export class SongUploadService { await this.generateAndUploadThumbnail( body.thumbnailData, nbsSong, - songDocument.publicId, + songDocument.publicId ); } } @@ -282,11 +282,11 @@ export class SongUploadService { private prepareSongForUpload( songFileBuffer: Buffer, body: UploadSongDto, - user: UserDocument, + user: UserDocument ): { nbsSong: Song; songBuffer: Buffer } { const songFileArrayBuffer = songFileBuffer.buffer.slice( songFileBuffer.byteOffset, - songFileBuffer.byteOffset + songFileBuffer.byteLength, + songFileBuffer.byteOffset + songFileBuffer.byteLength ) as ArrayBuffer; // Is the uploaded file a valid .nbs file? @@ -299,7 +299,7 @@ export class SongUploadService { removeExtraSpaces(user.username), removeExtraSpaces(body.originalAuthor), removeExtraSpaces(body.description), - body.customInstruments, + body.customInstruments ); const updatedSongArrayBuffer = toArrayBuffer(nbsSong); @@ -310,7 +310,7 @@ export class SongUploadService { private async preparePackedSongForUpload( nbsSong: Song, - soundsArray: string[], + soundsArray: string[] ): Promise { const soundsMapping = await this.getSoundsMapping(); const validSoundsSubset = await this.getValidSoundsSubset(); @@ -320,7 +320,7 @@ export class SongUploadService { const packedSongBuffer = await obfuscateAndPackSong( nbsSong, soundsArray, - soundsMapping, + soundsMapping ); return packedSongBuffer; @@ -328,13 +328,13 @@ export class SongUploadService { private validateCustomInstruments( soundsArray: string[], - validSounds: Set, + validSounds: Set ): void { const isInstrumentValid = (sound: string) => sound === '' || validSounds.has(sound); const areAllInstrumentsValid = soundsArray.every((sound) => - isInstrumentValid(sound), + isInstrumentValid(sound) ); if (!areAllInstrumentsValid) { @@ -342,10 +342,10 @@ export class SongUploadService { { error: { customInstruments: - 'One or more invalid custom instruments have been set', - }, + 'One or more invalid custom instruments have been set' + } }, - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } } @@ -353,20 +353,20 @@ export class SongUploadService { public async generateAndUploadThumbnail( thumbnailData: ThumbnailData, nbsSong: Song, - publicId: string, + publicId: string ): Promise { const { startTick, startLayer, zoomLevel, backgroundColor } = thumbnailData; const quadTree = new NoteQuadTree(nbsSong); const thumbBuffer = await drawToImage({ - notes: quadTree, - startTick: startTick, - startLayer: startLayer, - zoomLevel: zoomLevel, + notes : quadTree, + startTick : startTick, + startLayer : startLayer, + zoomLevel : zoomLevel, backgroundColor: backgroundColor, - imgWidth: 1280, - imgHeight: 768, + imgWidth : 1280, + imgHeight : 768 }); // Upload thumbnail @@ -378,10 +378,10 @@ export class SongUploadService { throw new HttpException( { error: { - file: "An error occurred while creating the song's thumbnail", - }, + file: "An error occurred while creating the song's thumbnail" + } }, - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR ); } @@ -392,7 +392,7 @@ export class SongUploadService { private async uploadSongFile( file: Buffer, - publicId: string, + publicId: string ): Promise { let fileKey: string; @@ -402,10 +402,10 @@ export class SongUploadService { throw new HttpException( { error: { - file: 'An error occurred while uploading the packed song file', - }, + file: 'An error occurred while uploading the packed song file' + } }, - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR ); } @@ -416,7 +416,7 @@ export class SongUploadService { private async uploadPackedSongFile( file: Buffer, - publicId: string, + publicId: string ): Promise { let fileKey: string; @@ -426,10 +426,10 @@ export class SongUploadService { throw new HttpException( { error: { - file: 'An error occurred while uploading the song file', - }, + file: 'An error occurred while uploading the song file' + } }, - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR ); } @@ -446,11 +446,11 @@ export class SongUploadService { throw new HttpException( { error: { - file: 'Invalid NBS file', - errors: nbsSong.errors, - }, + file : 'Invalid NBS file', + errors: nbsSong.errors + } }, - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } @@ -462,10 +462,10 @@ export class SongUploadService { throw new HttpException( { error: { - file: 'File not found', - }, + file: 'File not found' + } }, - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } } diff --git a/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts b/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts index 8b09fef1..3f259a25 100644 --- a/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts +++ b/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts @@ -1,22 +1,24 @@ +import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; + import { Song as SongEntity, SongWithUser } from '@nbw/database'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; -import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; import { Model } from 'mongoose'; import { getUploadDiscordEmbed } from '../song.util'; + import { SongWebhookService } from './song-webhook.service'; mock.module('../song.util', () => ({ - getUploadDiscordEmbed: jest.fn(), + getUploadDiscordEmbed: jest.fn() })); const mockSongModel = { - find: jest.fn().mockReturnThis(), - sort: jest.fn().mockReturnThis(), + find : jest.fn().mockReturnThis(), + sort : jest.fn().mockReturnThis(), populate: jest.fn().mockReturnThis(), - save: jest.fn(), + save : jest.fn() }; describe('SongWebhookService', () => { @@ -26,18 +28,18 @@ describe('SongWebhookService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ConfigModule.forRoot()], + imports : [ConfigModule.forRoot()], providers: [ SongWebhookService, { - provide: getModelToken(SongEntity.name), - useValue: mockSongModel, + provide : getModelToken(SongEntity.name), + useValue: mockSongModel }, { - provide: 'DISCORD_WEBHOOK_URL', - useValue: 'http://localhost/webhook', - }, - ], + provide : 'DISCORD_WEBHOOK_URL', + useValue: 'http://localhost/webhook' + } + ] }).compile(); service = module.get(SongWebhookService); @@ -53,13 +55,13 @@ describe('SongWebhookService', () => { it('should post a new webhook message for a song', async () => { const song: SongWithUser = { publicId: '123', - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader: { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); (global as any).fetch = jest.fn().mockResolvedValue({ - json: jest.fn().mockResolvedValue({ id: 'message-id' }), + json: jest.fn().mockResolvedValue({ id: 'message-id' }) }); const result = await service.postSongWebhook(song); @@ -67,18 +69,18 @@ describe('SongWebhookService', () => { expect(result).toBe('message-id'); expect(fetch).toHaveBeenCalledWith('http://localhost/webhook?wait=true', { - method: 'POST', + method : 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify({}), + body: JSON.stringify({}) }); }); it('should return null if there is an error', async () => { const song: SongWithUser = { publicId: '123', - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader: { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); @@ -94,9 +96,9 @@ describe('SongWebhookService', () => { describe('updateSongWebhook', () => { it('should update the webhook message for a song', async () => { const song: SongWithUser = { - publicId: '123', + publicId : '123', webhookMessageId: 'message-id', - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); @@ -108,20 +110,20 @@ describe('SongWebhookService', () => { expect(fetch).toHaveBeenCalledWith( 'http://localhost/webhook/messages/message-id', { - method: 'PATCH', + method : 'PATCH', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify({}), - }, + body: JSON.stringify({}) + } ); }); it('should log an error if there is an error', async () => { const song: SongWithUser = { - publicId: '123', + publicId : '123', webhookMessageId: 'message-id', - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); @@ -134,7 +136,7 @@ describe('SongWebhookService', () => { expect(loggerSpy).toHaveBeenCalledWith( 'Error updating Discord webhook', - expect.any(Error), + expect.any(Error) ); }); }); @@ -142,9 +144,9 @@ describe('SongWebhookService', () => { describe('deleteSongWebhook', () => { it('should delete the webhook message for a song', async () => { const song: SongWithUser = { - publicId: '123', + publicId : '123', webhookMessageId: 'message-id', - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; (global as any).fetch = jest.fn().mockResolvedValue({}); @@ -154,16 +156,16 @@ describe('SongWebhookService', () => { expect(fetch).toHaveBeenCalledWith( 'http://localhost/webhook/messages/message-id', { - method: 'DELETE', - }, + method: 'DELETE' + } ); }); it('should log an error if there is an error', async () => { const song: SongWithUser = { - publicId: '123', + publicId : '123', webhookMessageId: 'message-id', - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; (global as any).fetch = jest.fn().mockRejectedValue(new Error('Error')); @@ -174,7 +176,7 @@ describe('SongWebhookService', () => { expect(loggerSpy).toHaveBeenCalledWith( 'Error deleting Discord webhook', - expect.any(Error), + expect.any(Error) ); }); }); @@ -182,10 +184,10 @@ describe('SongWebhookService', () => { describe('syncSongWebhook', () => { it('should update the webhook message if the song is public', async () => { const song: SongWithUser = { - publicId: '123', + publicId : '123', webhookMessageId: 'message-id', - visibility: 'public', - uploader: { username: 'testuser', profileImage: 'testimage' }, + visibility : 'public', + uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; const updateSpy = spyOn(service, 'updateSongWebhook'); @@ -197,10 +199,10 @@ describe('SongWebhookService', () => { it('should delete the webhook message if the song is not public', async () => { const song: SongWithUser = { - publicId: '123', + publicId : '123', webhookMessageId: 'message-id', - visibility: 'private', - uploader: { username: 'testuser', profileImage: 'testimage' }, + visibility : 'private', + uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; const deleteSpy = spyOn(service, 'deleteSongWebhook'); @@ -212,9 +214,9 @@ describe('SongWebhookService', () => { it('should post a new webhook message if the song is public and does not have a message', async () => { const song: SongWithUser = { - publicId: '123', + publicId : '123', visibility: 'public', - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; const postSpy = spyOn(service, 'postSongWebhook'); @@ -226,9 +228,9 @@ describe('SongWebhookService', () => { it('should return null if the song is not public and does not have a message', async () => { const song: SongWithUser = { - publicId: '123', + publicId : '123', visibility: 'private', - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; const result = await service.syncSongWebhook(song); @@ -243,13 +245,13 @@ describe('SongWebhookService', () => { { publicId: '123', uploader: { username: 'testuser', profileImage: 'testimage' }, - save: jest.fn(), - } as unknown as SongWithUser, + save : jest.fn() + } as unknown as SongWithUser ]; mockSongModel.find.mockReturnValue({ - sort: jest.fn().mockReturnThis(), - populate: jest.fn().mockResolvedValue(songs), + sort : jest.fn().mockReturnThis(), + populate: jest.fn().mockResolvedValue(songs) }); const syncSpy = spyOn(service, 'syncSongWebhook'); diff --git a/apps/backend/src/song/song-webhook/song-webhook.service.ts b/apps/backend/src/song/song-webhook/song-webhook.service.ts index 90e16745..e5788372 100644 --- a/apps/backend/src/song/song-webhook/song-webhook.service.ts +++ b/apps/backend/src/song/song-webhook/song-webhook.service.ts @@ -13,7 +13,7 @@ export class SongWebhookService implements OnModuleInit { @InjectModel(SongEntity.name) private songModel: Model, @Inject('DISCORD_WEBHOOK_URL') - private readonly discordWebhookUrl: string | undefined, + private readonly discordWebhookUrl: string | undefined ) {} async onModuleInit() { @@ -41,11 +41,11 @@ export class SongWebhookService implements OnModuleInit { try { const response = await fetch(`${webhookUrl}?wait=true`, { - method: 'POST', + method : 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify(webhookData), + body: JSON.stringify(webhookData) }); const data = (await response.json()) as { id: string }; @@ -84,11 +84,11 @@ export class SongWebhookService implements OnModuleInit { try { await fetch(`${webhookUrl}/messages/${song.webhookMessageId}`, { - method: 'PATCH', + method : 'PATCH', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, - body: JSON.stringify(webhookData), + body: JSON.stringify(webhookData) }); this.logger.log(`Updated webhook message for song ${song.publicId}`); @@ -121,7 +121,7 @@ export class SongWebhookService implements OnModuleInit { try { await fetch(`${webhookUrl}/messages/${song.webhookMessageId}`, { - method: 'DELETE', + method: 'DELETE' }); this.logger.log(`Deleted webhook message for song ${song.publicId}`); @@ -179,7 +179,7 @@ export class SongWebhookService implements OnModuleInit { for (const songDocument of await songQuery) { const webhookMessageId = await this.syncSongWebhook( - songDocument as unknown as SongWithUser, + songDocument as unknown as SongWithUser ); songDocument.webhookMessageId = webhookMessageId; diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts index f4fc3ac2..611aee31 100644 --- a/apps/backend/src/song/song.controller.spec.ts +++ b/apps/backend/src/song/song.controller.spec.ts @@ -4,7 +4,7 @@ import { SongPreviewDto, SongViewDto, UploadSongDto, - UploadSongResponseDto, + UploadSongResponseDto } from '@nbw/database'; import { HttpStatus, UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @@ -17,13 +17,13 @@ import { SongController } from './song.controller'; import { SongService } from './song.service'; const mockSongService = { - getSongByPage: jest.fn(), - getSong: jest.fn(), - getSongEdit: jest.fn(), - patchSong: jest.fn(), + getSongByPage : jest.fn(), + getSong : jest.fn(), + getSongEdit : jest.fn(), + patchSong : jest.fn(), getSongDownloadUrl: jest.fn(), - deleteSong: jest.fn(), - uploadSong: jest.fn(), + deleteSong : jest.fn(), + uploadSong : jest.fn() }; const mockFileService = {}; @@ -35,16 +35,16 @@ describe('SongController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [SongController], - providers: [ + providers : [ { - provide: SongService, - useValue: mockSongService, + provide : SongService, + useValue: mockSongService }, { - provide: FileService, - useValue: mockFileService, - }, - ], + provide : FileService, + useValue: mockFileService + } + ] }) .overrideGuard(AuthGuard('jwt-refresh')) .useValue({ canActivate: jest.fn(() => true) }) @@ -125,7 +125,7 @@ describe('SongController', () => { mockSongService.getSongEdit.mockRejectedValueOnce(new Error('Error')); await expect(songController.getEditSong(id, user)).rejects.toThrow( - 'Error', + 'Error' ); }); }); @@ -153,7 +153,7 @@ describe('SongController', () => { mockSongService.patchSong.mockRejectedValueOnce(new Error('Error')); await expect(songController.patchSong(id, req, user)).rejects.toThrow( - 'Error', + 'Error' ); }); }); @@ -165,8 +165,8 @@ describe('SongController', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const res = { - set: jest.fn(), - redirect: jest.fn(), + set : jest.fn(), + redirect: jest.fn() } as unknown as Response; const url = 'test-url'; @@ -176,8 +176,8 @@ describe('SongController', () => { await songController.getSongFile(id, src, user, res); expect(res.set).toHaveBeenCalledWith({ - 'Content-Disposition': 'attachment; filename="song.nbs"', - 'Access-Control-Expose-Headers': 'Content-Disposition', + 'Content-Disposition' : 'attachment; filename="song.nbs"', + 'Access-Control-Expose-Headers': 'Content-Disposition' }); expect(res.redirect).toHaveBeenCalledWith(HttpStatus.FOUND, url); @@ -186,7 +186,7 @@ describe('SongController', () => { id, user, src, - false, + false ); }); @@ -196,16 +196,16 @@ describe('SongController', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const res = { - set: jest.fn(), - redirect: jest.fn(), + set : jest.fn(), + redirect: jest.fn() } as unknown as Response; mockSongService.getSongDownloadUrl.mockRejectedValueOnce( - new Error('Error'), + new Error('Error') ); await expect( - songController.getSongFile(id, src, user, res), + songController.getSongFile(id, src, user, res) ).rejects.toThrow('Error'); }); }); @@ -227,7 +227,7 @@ describe('SongController', () => { id, user, 'open', - true, + true ); }); @@ -237,7 +237,7 @@ describe('SongController', () => { const src = 'invalid-src'; await expect( - songController.getSongOpenUrl(id, user, src), + songController.getSongOpenUrl(id, user, src) ).rejects.toThrow(UnauthorizedException); }); @@ -247,11 +247,11 @@ describe('SongController', () => { const src = 'downloadButton'; mockSongService.getSongDownloadUrl.mockRejectedValueOnce( - new Error('Error'), + new Error('Error') ); await expect( - songController.getSongOpenUrl(id, user, src), + songController.getSongOpenUrl(id, user, src) ).rejects.toThrow('Error'); }); }); @@ -275,7 +275,7 @@ describe('SongController', () => { mockSongService.deleteSong.mockRejectedValueOnce(new Error('Error')); await expect(songController.deleteSong(id, user)).rejects.toThrow( - 'Error', + 'Error' ); }); }); @@ -285,21 +285,21 @@ describe('SongController', () => { const file = { buffer: Buffer.from('test') } as Express.Multer.File; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'cc_by_sa', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'cc_by_sa', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, - file: undefined, - allowDownload: false, + file : undefined, + allowDownload: false }; const user: UserDocument = { _id: 'test-user-id' } as UserDocument; @@ -317,21 +317,21 @@ describe('SongController', () => { const file = { buffer: Buffer.from('test') } as Express.Multer.File; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'cc_by_sa', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'cc_by_sa', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, - file: undefined, - allowDownload: false, + file : undefined, + allowDownload: false }; const user: UserDocument = { _id: 'test-user-id' } as UserDocument; @@ -339,7 +339,7 @@ describe('SongController', () => { mockSongService.uploadSong.mockRejectedValueOnce(new Error('Error')); await expect(songController.createSong(file, body, user)).rejects.toThrow( - 'Error', + 'Error' ); }); }); diff --git a/apps/backend/src/song/song.controller.ts b/apps/backend/src/song/song.controller.ts index d6711cee..e64bca9e 100644 --- a/apps/backend/src/song/song.controller.ts +++ b/apps/backend/src/song/song.controller.ts @@ -5,7 +5,7 @@ import { SongPreviewDto, SongViewDto, UploadSongDto, - UploadSongResponseDto, + UploadSongResponseDto } from '@nbw/database'; import type { RawBodyRequest } from '@nestjs/common'; import { @@ -24,7 +24,7 @@ import { UnauthorizedException, UploadedFile, UseGuards, - UseInterceptors, + UseInterceptors } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { FileInterceptor } from '@nestjs/platform-express'; @@ -34,7 +34,7 @@ import { ApiBody, ApiConsumes, ApiOperation, - ApiTags, + ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; @@ -50,7 +50,7 @@ import { SongService } from './song.service'; export class SongController { static multerConfig: MulterOptions = { limits: { - fileSize: UPLOAD_CONSTANTS.file.maxSize, + fileSize: UPLOAD_CONSTANTS.file.maxSize }, fileFilter: (req, file, cb) => { if (!file.originalname.match(/\.(nbs)$/)) { @@ -58,20 +58,20 @@ export class SongController { } cb(null, true); - }, + } }; constructor( public readonly songService: SongService, - public readonly fileService: FileService, + public readonly fileService: FileService ) {} @Get('/') @ApiOperation({ - summary: 'Get a filtered/sorted list of songs with pagination', + summary: 'Get a filtered/sorted list of songs with pagination' }) public async getSongList( - @Query() query: PageQueryDTO, + @Query() query: PageQueryDTO ): Promise { return await this.songService.getSongByPage(query); } @@ -80,7 +80,7 @@ export class SongController { @ApiOperation({ summary: 'Get song info by ID' }) public async getSong( @Param('id') id: string, - @GetRequestToken() user: UserDocument | null, + @GetRequestToken() user: UserDocument | null ): Promise { return await this.songService.getSong(id, user); } @@ -91,7 +91,7 @@ export class SongController { @ApiBearerAuth() public async getEditSong( @Param('id') id: string, - @GetRequestToken() user: UserDocument | null, + @GetRequestToken() user: UserDocument | null ): Promise { user = validateUser(user); return await this.songService.getSongEdit(id, user); @@ -103,12 +103,12 @@ export class SongController { @ApiOperation({ summary: 'Edit song info by ID' }) @ApiBody({ description: 'Upload Song', - type: UploadSongResponseDto, + type : UploadSongResponseDto }) public async patchSong( @Param('id') id: string, @Req() req: RawBodyRequest, - @GetRequestToken() user: UserDocument | null, + @GetRequestToken() user: UserDocument | null ): Promise { user = validateUser(user); //TODO: Fix this weird type casting and raw body access @@ -122,15 +122,15 @@ export class SongController { @Param('id') id: string, @Query('src') src: string, @GetRequestToken() user: UserDocument | null, - @Res() res: Response, + @Res() res: Response ): Promise { user = validateUser(user); // TODO: no longer used res.set({ - 'Content-Disposition': 'attachment; filename="song.nbs"', + 'Content-Disposition' : 'attachment; filename="song.nbs"', // Expose the Content-Disposition header to the client - 'Access-Control-Expose-Headers': 'Content-Disposition', + 'Access-Control-Expose-Headers': 'Content-Disposition' }); const url = await this.songService.getSongDownloadUrl(id, user, src, false); @@ -142,7 +142,7 @@ export class SongController { public async getSongOpenUrl( @Param('id') id: string, @GetRequestToken() user: UserDocument | null, - @Headers('src') src: string, + @Headers('src') src: string ): Promise { if (src != 'downloadButton') { throw new UnauthorizedException('Invalid source'); @@ -152,7 +152,7 @@ export class SongController { id, user, 'open', - true, + true ); return url; @@ -164,7 +164,7 @@ export class SongController { @ApiOperation({ summary: 'Delete a song' }) public async deleteSong( @Param('id') id: string, - @GetRequestToken() user: UserDocument | null, + @GetRequestToken() user: UserDocument | null ): Promise { user = validateUser(user); await this.songService.deleteSong(id, user); @@ -176,16 +176,16 @@ export class SongController { @ApiConsumes('multipart/form-data') @ApiBody({ description: 'Upload Song', - type: UploadSongResponseDto, + type : UploadSongResponseDto }) @UseInterceptors(FileInterceptor('file', SongController.multerConfig)) @ApiOperation({ - summary: 'Upload a .nbs file and send the song data, creating a new song', + summary: 'Upload a .nbs file and send the song data, creating a new song' }) public async createSong( @UploadedFile() file: Express.Multer.File, @Body() body: UploadSongDto, - @GetRequestToken() user: UserDocument | null, + @GetRequestToken() user: UserDocument | null ): Promise { user = validateUser(user); return await this.songService.uploadSong({ body, file, user }); diff --git a/apps/backend/src/song/song.module.ts b/apps/backend/src/song/song.module.ts index 52218af7..7c62b565 100644 --- a/apps/backend/src/song/song.module.ts +++ b/apps/backend/src/song/song.module.ts @@ -18,20 +18,20 @@ import { SongService } from './song.service'; MongooseModule.forFeature([{ name: Song.name, schema: SongSchema }]), AuthModule, UserModule, - FileModule.forRootAsync(), + FileModule.forRootAsync() ], providers: [ SongService, SongUploadService, SongWebhookService, { - inject: [ConfigService], - provide: 'DISCORD_WEBHOOK_URL', + inject : [ConfigService], + provide : 'DISCORD_WEBHOOK_URL', useFactory: (configService: ConfigService) => - configService.getOrThrow('DISCORD_WEBHOOK_URL'), - }, + configService.getOrThrow('DISCORD_WEBHOOK_URL') + } ], controllers: [SongController, MySongsController], - exports: [SongService], + exports : [SongService] }) export class SongModule {} diff --git a/apps/backend/src/song/song.service.spec.ts b/apps/backend/src/song/song.service.spec.ts index b5445461..365efcdf 100644 --- a/apps/backend/src/song/song.service.spec.ts +++ b/apps/backend/src/song/song.service.spec.ts @@ -8,7 +8,7 @@ import { SongViewDto, SongWithUser, UploadSongDto, - UploadSongResponseDto, + UploadSongResponseDto } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; @@ -22,21 +22,21 @@ import { SongWebhookService } from './song-webhook/song-webhook.service'; import { SongService } from './song.service'; const mockFileService = { - deleteSong: jest.fn(), - getSongDownloadUrl: jest.fn(), + deleteSong : jest.fn(), + getSongDownloadUrl: jest.fn() }; const mockSongUploadService = { processUploadedSong: jest.fn(), - processSongPatch: jest.fn(), + processSongPatch : jest.fn() }; const mockSongWebhookService = { syncAllSongsWebhook: jest.fn(), - postSongWebhook: jest.fn(), - updateSongWebhook: jest.fn(), - deleteSongWebhook: jest.fn(), - syncSongWebhook: jest.fn(), + postSongWebhook : jest.fn(), + updateSongWebhook : jest.fn(), + deleteSongWebhook : jest.fn(), + syncSongWebhook : jest.fn() }; describe('SongService', () => { @@ -50,22 +50,22 @@ describe('SongService', () => { providers: [ SongService, { - provide: SongWebhookService, - useValue: mockSongWebhookService, + provide : SongWebhookService, + useValue: mockSongWebhookService }, { - provide: getModelToken(SongEntity.name), - useValue: mongoose.model(SongEntity.name, SongSchema), + provide : getModelToken(SongEntity.name), + useValue: mongoose.model(SongEntity.name, SongSchema) }, { - provide: FileService, - useValue: mockFileService, + provide : FileService, + useValue: mockFileService }, { - provide: SongUploadService, - useValue: mockSongUploadService, - }, - ], + provide : SongUploadService, + useValue: mockSongUploadService + } + ] }).compile(); service = module.get(SongService); @@ -84,53 +84,53 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: true, - file: 'somebytes', + file : 'somebytes' }; const commonData = { - publicId: 'public-song-id', + publicId : 'public-song-id', createdAt: new Date(), - stats: { - midiFileName: 'test.mid', - noteCount: 100, - tickCount: 1000, - layerCount: 10, - tempo: 120, - tempoRange: [100, 150], - timeSignature: 4, - duration: 60, - loop: true, - loopStartTick: 0, - minutesSpent: 10, - vanillaInstrumentCount: 10, - customInstrumentCount: 0, + stats : { + midiFileName : 'test.mid', + noteCount : 100, + tickCount : 1000, + layerCount : 10, + tempo : 120, + tempoRange : [100, 150], + timeSignature : 4, + duration : 60, + loop : true, + loopStartTick : 0, + minutesSpent : 10, + vanillaInstrumentCount : 10, + customInstrumentCount : 0, firstCustomInstrumentIndex: 0, - outOfRangeNoteCount: 0, - detunedNoteCount: 0, - customInstrumentNoteCount: 0, - incompatibleNoteCount: 0, - compatible: true, - instrumentNoteCounts: [10], + outOfRangeNoteCount : 0, + detunedNoteCount : 0, + customInstrumentNoteCount : 0, + incompatibleNoteCount : 0, + compatible : true, + instrumentNoteCounts : [10] }, - fileSize: 424242, + fileSize : 424242, packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl: 'http://test.com/file.nbs', - thumbnailUrl: 'http://test.com/thumbnail.nbs', - uploader: user._id, + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + uploader : user._id }; const songEntity = new SongEntity(); @@ -138,13 +138,13 @@ describe('SongService', () => { const songDocument: SongDocument = { ...songEntity, - ...commonData, + ...commonData } as any; songDocument.populate = jest.fn().mockResolvedValue({ ...songEntity, ...commonData, - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader: { username: 'testuser', profileImage: 'testimage' } } as unknown as SongWithUser); songDocument.save = jest.fn().mockResolvedValue(songDocument); @@ -152,7 +152,7 @@ describe('SongService', () => { const populatedSong = { ...songEntity, ...commonData, - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader: { username: 'testuser', profileImage: 'testimage' } } as unknown as SongWithUser; jest @@ -164,13 +164,13 @@ describe('SongService', () => { const result = await service.uploadSong({ file, user, body }); expect(result).toEqual( - UploadSongResponseDto.fromSongWithUserDocument(populatedSong), + UploadSongResponseDto.fromSongWithUserDocument(populatedSong) ); expect(songUploadService.processUploadedSong).toHaveBeenCalledWith({ file, user, - body, + body }); expect(songModel.create).toHaveBeenCalledWith(songEntity); @@ -178,7 +178,7 @@ describe('SongService', () => { expect(songDocument.populate).toHaveBeenCalledWith( 'uploader', - 'username profileImage -_id', + 'username profileImage -_id' ); }); }); @@ -189,47 +189,47 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const songEntity = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: true, - file: 'somebytes', - publicId: 'public-song-id', - createdAt: new Date(), - stats: {} as SongStats, - fileSize: 424242, + file : 'somebytes', + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl: 'http://test.com/file.nbs', - thumbnailUrl: 'http://test.com/thumbnail.nbs', - uploader: user._id, + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + uploader : user._id } as unknown as SongEntity; const populatedSong = { ...songEntity, - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader: { username: 'testuser', profileImage: 'testimage' } } as unknown as SongWithUser; const mockFindOne = { exec: jest.fn().mockResolvedValue({ ...songEntity, - populate: jest.fn().mockResolvedValue(populatedSong), - }), + populate: jest.fn().mockResolvedValue(populatedSong) + }) }; jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); jest.spyOn(songModel, 'deleteOne').mockReturnValue({ - exec: jest.fn().mockResolvedValue({}), + exec: jest.fn().mockResolvedValue({}) } as any); jest.spyOn(fileService, 'deleteSong').mockResolvedValue(undefined); @@ -237,14 +237,14 @@ describe('SongService', () => { const result = await service.deleteSong(publicId, user); expect(result).toEqual( - UploadSongResponseDto.fromSongWithUserDocument(populatedSong), + UploadSongResponseDto.fromSongWithUserDocument(populatedSong) ); expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); expect(songModel.deleteOne).toHaveBeenCalledWith({ publicId }); expect(fileService.deleteSong).toHaveBeenCalledWith( - songEntity.nbsFileUrl, + songEntity.nbsFileUrl ); }); @@ -254,13 +254,13 @@ describe('SongService', () => { const mockFindOne = { findOne: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(null), + exec : jest.fn().mockResolvedValue(null) }; jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); await expect(service.deleteSong(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -271,13 +271,13 @@ describe('SongService', () => { songEntity.uploader = new mongoose.Types.ObjectId(); // Different uploader const mockFindOne = { - exec: jest.fn().mockResolvedValue(songEntity), + exec: jest.fn().mockResolvedValue(songEntity) }; jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); await expect(service.deleteSong(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -288,13 +288,13 @@ describe('SongService', () => { songEntity.uploader = new mongoose.Types.ObjectId(); // Different uploader const mockFindOne = { - exec: jest.fn().mockResolvedValue(songEntity), + exec: jest.fn().mockResolvedValue(songEntity) }; jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); await expect(service.deleteSong(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); }); @@ -305,69 +305,69 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: true, - file: 'somebytes', + file : 'somebytes' }; const missingData = { - publicId: 'public-song-id', + publicId : 'public-song-id', createdAt: new Date(), - stats: { - midiFileName: 'test.mid', - noteCount: 100, - tickCount: 1000, - layerCount: 10, - tempo: 120, - tempoRange: [100, 150], - timeSignature: 4, - duration: 60, - loop: true, - loopStartTick: 0, - minutesSpent: 10, - vanillaInstrumentCount: 10, - customInstrumentCount: 0, + stats : { + midiFileName : 'test.mid', + noteCount : 100, + tickCount : 1000, + layerCount : 10, + tempo : 120, + tempoRange : [100, 150], + timeSignature : 4, + duration : 60, + loop : true, + loopStartTick : 0, + minutesSpent : 10, + vanillaInstrumentCount : 10, + customInstrumentCount : 0, firstCustomInstrumentIndex: 0, - outOfRangeNoteCount: 0, - detunedNoteCount: 0, - customInstrumentNoteCount: 0, - incompatibleNoteCount: 0, - compatible: true, - instrumentNoteCounts: [10], + outOfRangeNoteCount : 0, + detunedNoteCount : 0, + customInstrumentNoteCount : 0, + incompatibleNoteCount : 0, + compatible : true, + instrumentNoteCounts : [10] }, - fileSize: 424242, + fileSize : 424242, packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl: 'http://test.com/file.nbs', - thumbnailUrl: 'http://test.com/thumbnail.nbs', - uploader: user._id, + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + uploader : user._id }; const songDocument: SongDocument = { - ...missingData, + ...missingData } as any; songDocument.save = jest.fn().mockResolvedValue(songDocument); songDocument.populate = jest.fn().mockResolvedValue({ ...missingData, - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader: { username: 'testuser', profileImage: 'testimage' } }); const populatedSong = { ...missingData, - uploader: { username: 'testuser', profileImage: 'testimage' }, + uploader: { username: 'testuser', profileImage: 'testimage' } }; jest.spyOn(songModel, 'findOne').mockResolvedValue(songDocument); @@ -379,7 +379,7 @@ describe('SongService', () => { const result = await service.patchSong(publicId, body, user); expect(result).toEqual( - UploadSongResponseDto.fromSongWithUserDocument(populatedSong as any), + UploadSongResponseDto.fromSongWithUserDocument(populatedSong as any) ); expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); @@ -387,14 +387,14 @@ describe('SongService', () => { expect(songUploadService.processSongPatch).toHaveBeenCalledWith( songDocument, body, - user, + user ); expect(songDocument.save).toHaveBeenCalled(); expect(songDocument.populate).toHaveBeenCalledWith( 'uploader', - 'username profileImage -_id', + 'username profileImage -_id' ); }, 10000); // Increase the timeout to 10000 ms @@ -403,27 +403,27 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, - file: 'somebytes', - allowDownload: false, + file : 'somebytes', + allowDownload: false }; jest.spyOn(songModel, 'findOne').mockReturnValue(null as any); await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -432,31 +432,31 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, - file: 'somebytes', - allowDownload: false, + file : 'somebytes', + allowDownload: false }; const songEntity = { - uploader: 'different-user-id', + uploader: 'different-user-id' } as any; jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -465,31 +465,31 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const body: UploadSongDto = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, - file: 'somebytes', - allowDownload: false, + file : 'somebytes', + allowDownload: false }; const songEntity = { - uploader: 'different-user-id', + uploader: 'different-user-id' } as any; jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity); await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -498,46 +498,46 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const body: UploadSongDto = { - file: undefined, - allowDownload: false, - visibility: 'public', - title: '', + file : undefined, + allowDownload : false, + visibility : 'public', + title : '', originalAuthor: '', - description: '', - category: 'pop', - thumbnailData: { + description : '', + category : 'pop', + thumbnailData : { backgroundColor: '#000000', - startLayer: 0, - startTick: 0, - zoomLevel: 1, + startLayer : 0, + startTick : 0, + zoomLevel : 1 }, - license: 'standard', - customInstruments: [], + license : 'standard', + customInstruments: [] }; const songEntity = { - uploader: user._id, - file: undefined, - allowDownload: false, - visibility: 'public', - title: '', + uploader : user._id, + file : undefined, + allowDownload : false, + visibility : 'public', + title : '', originalAuthor: '', - description: '', - category: 'pop', - thumbnailData: { + description : '', + category : 'pop', + thumbnailData : { backgroundColor: '#000000', - startLayer: 0, - startTick: 0, - zoomLevel: 1, + startLayer : 0, + startTick : 0, + zoomLevel : 1 }, - license: 'standard', - customInstruments: [], + license : 'standard', + customInstruments: [] } as any; jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException, + HttpException ); }); }); @@ -545,20 +545,20 @@ describe('SongService', () => { describe('getSongByPage', () => { it('should return a list of songs by page', async () => { const query = { - page: 1, + page : 1, limit: 10, - sort: 'createdAt', - order: true, + sort : 'createdAt', + order: true }; const songList: SongWithUser[] = []; const mockFind = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit : jest.fn().mockReturnThis(), populate: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(songList), + exec : jest.fn().mockResolvedValue(songList) }; jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); @@ -566,14 +566,14 @@ describe('SongService', () => { const result = await service.getSongByPage(query); expect(result).toEqual( - songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)), + songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)) ); expect(songModel.find).toHaveBeenCalledWith({ visibility: 'public' }); expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: 1 }); expect(mockFind.skip).toHaveBeenCalledWith( - query.page * query.limit - query.limit, + query.page * query.limit - query.limit ); expect(mockFind.limit).toHaveBeenCalledWith(query.limit); @@ -582,20 +582,20 @@ describe('SongService', () => { it('should throw an error if the query is invalid', async () => { const query = { - page: undefined, + page : undefined, limit: undefined, - sort: undefined, - order: true, + sort : undefined, + order: true }; const songList: SongWithUser[] = []; const mockFind = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit : jest.fn().mockReturnThis(), populate: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(songList), + exec : jest.fn().mockResolvedValue(songList) }; jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); @@ -610,30 +610,30 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const songDocument = { - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, - file: 'somebytes', + file : 'somebytes', allowDownload: false, - uploader: {}, - save: jest.fn(), + uploader : {}, + save : jest.fn() } as any; songDocument.save = jest.fn().mockResolvedValue(songDocument); const mockFindOne = { populate: jest.fn().mockResolvedValue(songDocument), - ...songDocument, + ...songDocument }; jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); @@ -648,7 +648,7 @@ describe('SongService', () => { const publicId = 'test-id'; const user: UserDocument = { - _id: 'test-user-id', + _id: 'test-user-id' } as unknown as UserDocument; const mockFindOne = null; @@ -656,7 +656,7 @@ describe('SongService', () => { jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); await expect(service.getSong(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -665,9 +665,9 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const songEntity = { - publicId: 'test-public-id', + publicId : 'test-public-id', visibility: 'private', - uploader: 'different-user-id', + uploader : 'different-user-id' }; jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); @@ -680,9 +680,9 @@ describe('SongService', () => { const user: UserDocument = null as any; const songEntity = { - publicId: 'test-public-id', + publicId : 'test-public-id', visibility: 'private', - uploader: 'different-user-id', + uploader : 'different-user-id' }; jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); @@ -696,29 +696,29 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const songEntity = { - visibility: 'public', - uploader: 'test-user-id', - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - license: 'standard', + visibility : 'public', + uploader : 'test-user-id', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: true, - publicId: 'public-song-id', - createdAt: new Date(), - stats: {} as SongStats, - fileSize: 424242, + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl: 'http://test.com/file.nbs', - thumbnailUrl: 'http://test.com/thumbnail.nbs', - save: jest.fn(), + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + save : jest.fn() }; const url = 'http://test.com/song.nbs'; @@ -733,7 +733,7 @@ describe('SongService', () => { expect(fileService.getSongDownloadUrl).toHaveBeenCalledWith( songEntity.nbsFileUrl, - `${songEntity.title}.nbs`, + `${songEntity.title}.nbs` ); }); @@ -744,7 +744,7 @@ describe('SongService', () => { jest.spyOn(songModel, 'findOne').mockResolvedValue(null); await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -754,13 +754,13 @@ describe('SongService', () => { const songEntity = { visibility: 'private', - uploader: 'different-user-id', + uploader : 'different-user-id' }; jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -769,35 +769,35 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const songEntity = { - visibility: 'public', - uploader: 'test-user-id', - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - license: 'standard', + visibility : 'public', + uploader : 'test-user-id', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: false, - publicId: 'public-song-id', - createdAt: new Date(), - stats: {} as SongStats, - fileSize: 424242, + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, packedSongUrl: undefined, - nbsFileUrl: 'http://test.com/file.nbs', - thumbnailUrl: 'http://test.com/thumbnail.nbs', - save: jest.fn(), + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + save : jest.fn() }; jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -812,7 +812,7 @@ describe('SongService', () => { }); await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -821,31 +821,31 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const songEntity = { - visibility: 'public', - uploader: 'test-user-id', - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - license: 'standard', + visibility : 'public', + uploader : 'test-user-id', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: true, - publicId: 'public-song-id', - createdAt: new Date(), - stats: {} as SongStats, - fileSize: 424242, + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl: 'http://test.com/file.nbs', - thumbnailUrl: 'http://test.com/thumbnail.nbs', - save: jest.fn().mockImplementationOnce(() => { + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + save : jest.fn().mockImplementationOnce(() => { throw new Error('Error saving song'); - }), + }) }; jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); @@ -855,7 +855,7 @@ describe('SongService', () => { .mockResolvedValue('http://test.com/song.nbs'); await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); }); @@ -863,19 +863,19 @@ describe('SongService', () => { describe('getMySongsPage', () => { it('should return a list of songs uploaded by the user', async () => { const query = { - page: 1, + page : 1, limit: 10, - sort: 'createdAt', - order: true, + sort : 'createdAt', + order: true }; const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const songList: SongWithUser[] = []; const mockFind = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockResolvedValue(songList), + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit: jest.fn().mockResolvedValue(songList) }; jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); @@ -885,24 +885,24 @@ describe('SongService', () => { expect(result).toEqual({ content: songList.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song), + SongPreviewDto.fromSongDocumentWithUser(song) ), - page: 1, + page : 1, limit: 10, - total: 0, + total: 0 }); expect(songModel.find).toHaveBeenCalledWith({ uploader: user._id }); expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: 1 }); expect(mockFind.skip).toHaveBeenCalledWith( - query.page * query.limit - query.limit, + query.page * query.limit - query.limit ); expect(mockFind.limit).toHaveBeenCalledWith(query.limit); expect(songModel.countDocuments).toHaveBeenCalledWith({ - uploader: user._id, + uploader: user._id }); }); }); @@ -915,8 +915,8 @@ describe('SongService', () => { songEntity.uploader = user._id; // Ensure uploader is set const mockFindOne = { - exec: jest.fn().mockResolvedValue(songEntity), - populate: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(songEntity), + populate: jest.fn().mockReturnThis() }; jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); @@ -934,13 +934,13 @@ describe('SongService', () => { const findOneMock = { findOne: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(null), + exec : jest.fn().mockResolvedValue(null) }; jest.spyOn(songModel, 'findOne').mockReturnValue(findOneMock as any); await expect(service.getSongEdit(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); @@ -949,39 +949,39 @@ describe('SongService', () => { const user: UserDocument = { _id: 'test-user-id' } as UserDocument; const songEntity = { - uploader: 'different-user-id', - title: 'Test Song', - originalAuthor: 'Test Author', - description: 'Test Description', - category: 'alternative', - visibility: 'public', - license: 'standard', + uploader : 'different-user-id', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', customInstruments: [], - thumbnailData: { - startTick: 0, - startLayer: 0, - zoomLevel: 1, - backgroundColor: '#000000', + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' }, allowDownload: true, - publicId: 'public-song-id', - createdAt: new Date(), - stats: {} as SongStats, - fileSize: 424242, + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl: 'http://test.com/file.nbs', - thumbnailUrl: 'http://test.com/thumbnail.nbs', + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs' } as unknown as SongEntity; const findOneMock = { findOne: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(songEntity), + exec : jest.fn().mockResolvedValue(songEntity) }; jest.spyOn(songModel, 'findOne').mockReturnValue(findOneMock as any); await expect(service.getSongEdit(publicId, user)).rejects.toThrow( - HttpException, + HttpException ); }); }); @@ -990,7 +990,7 @@ describe('SongService', () => { it('should return a list of song categories and their counts', async () => { const categories = [ { _id: 'category1', count: 10 }, - { _id: 'category2', count: 5 }, + { _id: 'category2', count: 5 } ]; jest.spyOn(songModel, 'aggregate').mockResolvedValue(categories); @@ -1002,7 +1002,7 @@ describe('SongService', () => { expect(songModel.aggregate).toHaveBeenCalledWith([ { $match: { visibility: 'public' } }, { $group: { _id: '$category', count: { $sum: 1 } } }, - { $sort: { count: -1 } }, + { $sort: { count: -1 } } ]); }); }); @@ -1015,11 +1015,11 @@ describe('SongService', () => { const songList: SongWithUser[] = []; const mockFind = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit : jest.fn().mockReturnThis(), populate: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(songList), + exec : jest.fn().mockResolvedValue(songList) }; jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); @@ -1027,12 +1027,12 @@ describe('SongService', () => { const result = await service.getSongsByCategory(category, page, limit); expect(result).toEqual( - songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)), + songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)) ); expect(songModel.find).toHaveBeenCalledWith({ category, - visibility: 'public', + visibility: 'public' }); expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: -1 }); @@ -1041,7 +1041,7 @@ describe('SongService', () => { expect(mockFind.populate).toHaveBeenCalledWith( 'uploader', - 'username profileImage -_id', + 'username profileImage -_id' ); expect(mockFind.exec).toHaveBeenCalled(); diff --git a/apps/backend/src/song/song.service.ts b/apps/backend/src/song/song.service.ts index 1da06c08..2529849b 100644 --- a/apps/backend/src/song/song.service.ts +++ b/apps/backend/src/song/song.service.ts @@ -8,14 +8,14 @@ import { SongViewDto, SongWithUser, UploadSongDto, - UploadSongResponseDto, + UploadSongResponseDto } from '@nbw/database'; import { HttpException, HttpStatus, Inject, Injectable, - Logger, + Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; @@ -40,19 +40,19 @@ export class SongService { private songUploadService: SongUploadService, @Inject(SongWebhookService) - private songWebhookService: SongWebhookService, + private songWebhookService: SongWebhookService ) {} public async getSongById(publicId: string) { return this.songModel.findOne({ - publicId, + publicId }); } public async uploadSong({ file, user, - body, + body }: { body: UploadSongDto; file: Express.Multer.File; @@ -61,7 +61,7 @@ export class SongService { const song = await this.songUploadService.processUploadedSong({ file, user, - body, + body }); // Create song document @@ -70,11 +70,11 @@ export class SongService { // Post Discord webhook const populatedSong = (await songDocument.populate( 'uploader', - 'username profileImage -_id', + 'username profileImage -_id' )) as unknown as SongWithUser; const webhookMessageId = await this.songWebhookService.syncSongWebhook( - populatedSong, + populatedSong ); songDocument.webhookMessageId = webhookMessageId; @@ -87,7 +87,7 @@ export class SongService { public async deleteSong( publicId: string, - user: UserDocument, + user: UserDocument ): Promise { const foundSong = await this.songModel .findOne({ publicId: publicId }) @@ -107,7 +107,7 @@ export class SongService { const populatedSong = (await foundSong.populate( 'uploader', - 'username profileImage -_id', + 'username profileImage -_id' )) as unknown as SongWithUser; await this.songWebhookService.deleteSongWebhook(populatedSong); @@ -118,10 +118,10 @@ export class SongService { public async patchSong( publicId: string, body: UploadSongDto, - user: UserDocument, + user: UserDocument ): Promise { const foundSong = await this.songModel.findOne({ - publicId: publicId, + publicId: publicId }); if (!foundSong) { @@ -168,11 +168,11 @@ export class SongService { const populatedSong = (await foundSong.populate( 'uploader', - 'username profileImage -_id', + 'username profileImage -_id' )) as unknown as SongWithUser; const webhookMessageId = await this.songWebhookService.syncSongWebhook( - populatedSong, + populatedSong ); foundSong.webhookMessageId = webhookMessageId; @@ -189,16 +189,16 @@ export class SongService { if (!page || !limit || !sort) { throw new HttpException( 'Invalid query parameters', - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } const songs = (await this.songModel .find({ - visibility: 'public', + visibility: 'public' }) .sort({ - [sort]: order ? 1 : -1, + [sort]: order ? 1 : -1 }) .skip(page * limit - limit) .limit(limit) @@ -210,16 +210,16 @@ export class SongService { public async getRecentSongs( page: number, - limit: number, + limit: number ): Promise { const queryObject: any = { - visibility: 'public', + visibility: 'public' }; const data = (await this.songModel .find(queryObject) .sort({ - createdAt: -1, + createdAt: -1 }) .skip(page * limit - limit) .limit(limit) @@ -233,9 +233,9 @@ export class SongService { return this.songModel .find({ visibility: 'public', - createdAt: { - $gte: timespan, - }, + createdAt : { + $gte: timespan + } }) .sort({ playCount: -1 }) .limit(BROWSER_SONGS.featuredPageSize) @@ -244,14 +244,14 @@ export class SongService { } public async getSongsBeforeTimespan( - timespan: number, + timespan: number ): Promise { return this.songModel .find({ visibility: 'public', - createdAt: { - $lt: timespan, - }, + createdAt : { + $lt: timespan + } }) .sort({ createdAt: -1 }) .limit(BROWSER_SONGS.featuredPageSize) @@ -261,7 +261,7 @@ export class SongService { public async getSong( publicId: string, - user: UserDocument | null, + user: UserDocument | null ): Promise { const foundSong = await this.songModel.findOne({ publicId: publicId }); @@ -285,7 +285,7 @@ export class SongService { const populatedSong = await foundSong.populate( 'uploader', - 'username profileImage -_id', + 'username profileImage -_id' ); return SongViewDto.fromSongDocument(populatedSong); @@ -296,7 +296,7 @@ export class SongService { publicId: string, user: UserDocument | null, src?: string, - packed: boolean = false, + packed: boolean = false ): Promise { const foundSong = await this.songModel.findOne({ publicId: publicId }); @@ -308,7 +308,7 @@ export class SongService { if (!user || foundSong.uploader.toString() !== user._id.toString()) { throw new HttpException( 'This song is private', - HttpStatus.UNAUTHORIZED, + HttpStatus.UNAUTHORIZED ); } } @@ -316,7 +316,7 @@ export class SongService { if (!packed && !foundSong.allowDownload) { throw new HttpException( 'The uploader has disabled downloads of this song', - HttpStatus.UNAUTHORIZED, + HttpStatus.UNAUTHORIZED ); } @@ -337,14 +337,14 @@ export class SongService { this.logger.error('Error getting song file', e); throw new HttpException( 'An error occurred while retrieving the song file', - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR ); } } public async getMySongsPage({ query, - user, + user }: { query: PageQueryDTO; user: UserDocument; @@ -356,31 +356,31 @@ export class SongService { const songData = (await this.songModel .find({ - uploader: user._id, + uploader: user._id }) .sort({ - [sort]: order ? 1 : -1, + [sort]: order ? 1 : -1 }) .skip(limit * (page - 1)) .limit(limit)) as unknown as SongWithUser[]; const total = await this.songModel.countDocuments({ - uploader: user._id, + uploader: user._id }); return { content: songData.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song), + SongPreviewDto.fromSongDocumentWithUser(song) ), - page: page, + page : page, limit: limit, - total: total, + total: total }; } public async getSongEdit( publicId: string, - user: UserDocument, + user: UserDocument ): Promise { const foundSong = await this.songModel .findOne({ publicId: publicId }) @@ -403,20 +403,20 @@ export class SongService { const categories = (await this.songModel.aggregate([ { $match: { - visibility: 'public', - }, + visibility: 'public' + } }, { $group: { - _id: '$category', - count: { $sum: 1 }, - }, + _id : '$category', + count: { $sum: 1 } + } }, { $sort: { - count: -1, - }, - }, + count: -1 + } + } ])) as unknown as { _id: string; count: number }[]; // Return object with category names as keys and counts as values @@ -432,12 +432,12 @@ export class SongService { public async getSongsByCategory( category: string, page: number, - limit: number, + limit: number ): Promise { const songs = (await this.songModel .find({ - category: category, - visibility: 'public', + category : category, + visibility: 'public' }) .sort({ createdAt: -1 }) .skip(page * limit - limit) @@ -450,26 +450,26 @@ export class SongService { public async getRandomSongs( count: number, - category: string, + category: string ): Promise { const songs = (await this.songModel .aggregate([ { $match: { - visibility: 'public', - }, + visibility: 'public' + } }, { $sample: { - size: count, - }, - }, + size: count + } + } ]) .exec()) as unknown as SongWithUser[]; await this.songModel.populate(songs, { - path: 'uploader', - select: 'username profileImage -_id', + path : 'uploader', + select: 'username profileImage -_id' }); return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); diff --git a/apps/backend/src/song/song.util.ts b/apps/backend/src/song/song.util.ts index 50168171..bf098caa 100644 --- a/apps/backend/src/song/song.util.ts +++ b/apps/backend/src/song/song.util.ts @@ -40,62 +40,62 @@ export function getUploadDiscordEmbed({ originalAuthor, category, license, - stats, + stats }: SongWithUser) { let fieldsArray = []; if (originalAuthor) { fieldsArray.push({ - name: 'Original Author', - value: originalAuthor, - inline: false, + name : 'Original Author', + value : originalAuthor, + inline: false }); } fieldsArray = fieldsArray.concat([ { - name: 'Category', - value: UPLOAD_CONSTANTS.categories[category], - inline: true, + name : 'Category', + value : UPLOAD_CONSTANTS.categories[category], + inline: true }, { - name: 'Notes', - value: stats.noteCount.toLocaleString('en-US'), - inline: true, + name : 'Notes', + value : stats.noteCount.toLocaleString('en-US'), + inline: true }, { - name: 'Length', - value: formatDuration(stats.duration), - inline: true, - }, + name : 'Length', + value : formatDuration(stats.duration), + inline: true + } ]); return { embeds: [ { - title: title, + title : title, description: description, - color: Number('0x' + thumbnailData.backgroundColor.replace('#', '')), - timestamp: createdAt.toISOString(), - footer: { + color : Number('0x' + thumbnailData.backgroundColor.replace('#', '')), + timestamp : createdAt.toISOString(), + footer : { text: UPLOAD_CONSTANTS.licenses[license] ? UPLOAD_CONSTANTS.licenses[license].shortName - : 'Unknown License', + : 'Unknown License' }, author: { - name: uploader.username, - icon_url: uploader.profileImage, + name : uploader.username, + icon_url: uploader.profileImage //url: 'https://noteblock.world/user/${uploaderName}', }, fields: fieldsArray, - url: `https://noteblock.world/song/${publicId}`, - image: { - url: thumbnailUrl, + url : `https://noteblock.world/song/${publicId}`, + image : { + url: thumbnailUrl }, thumbnail: { - url: 'https://noteblock.world/nbw-color.png', - }, - }, - ], + url: 'https://noteblock.world/nbw-color.png' + } + } + ] }; } diff --git a/apps/backend/src/user/user.controller.spec.ts b/apps/backend/src/user/user.controller.spec.ts index b8b94b96..ebc5f0d8 100644 --- a/apps/backend/src/user/user.controller.spec.ts +++ b/apps/backend/src/user/user.controller.spec.ts @@ -8,8 +8,8 @@ import { UserService } from './user.service'; const mockUserService = { getUserByEmailOrId: jest.fn(), - getUserPaginated: jest.fn(), - getSelfUserData: jest.fn(), + getUserPaginated : jest.fn(), + getSelfUserData : jest.fn() }; describe('UserController', () => { @@ -19,12 +19,12 @@ describe('UserController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UserController], - providers: [ + providers : [ { - provide: UserService, - useValue: mockUserService, - }, - ], + provide : UserService, + useValue: mockUserService + } + ] }).compile(); userController = module.get(UserController); @@ -38,9 +38,9 @@ describe('UserController', () => { describe('getUser', () => { it('should return user data by email or ID', async () => { const query: GetUser = { - email: 'test@email.com', + email : 'test@email.com', username: 'test-username', - id: 'test-id', + id : 'test-id' }; const user = { email: 'test@example.com' }; diff --git a/apps/backend/src/user/user.controller.ts b/apps/backend/src/user/user.controller.ts index 193551fa..c1197fec 100644 --- a/apps/backend/src/user/user.controller.ts +++ b/apps/backend/src/user/user.controller.ts @@ -11,7 +11,7 @@ import { UserService } from './user.service'; export class UserController { constructor( @Inject(UserService) - private readonly userService: UserService, + private readonly userService: UserService ) {} @Get() @@ -43,7 +43,7 @@ export class UserController { @ApiOperation({ summary: 'Update the username' }) async updateUsername( @GetRequestToken() user: UserDocument | null, - @Body() body: UpdateUsernameDto, + @Body() body: UpdateUsernameDto ) { user = validateUser(user); return await this.userService.updateUsername(user, body); diff --git a/apps/backend/src/user/user.module.ts b/apps/backend/src/user/user.module.ts index 53a58c90..c8f9225e 100644 --- a/apps/backend/src/user/user.module.ts +++ b/apps/backend/src/user/user.module.ts @@ -7,10 +7,10 @@ import { UserService } from './user.service'; @Module({ imports: [ - MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), + MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]) ], - providers: [UserService], + providers : [UserService], controllers: [UserController], - exports: [UserService], + exports : [UserService] }) export class UserModule {} diff --git a/apps/backend/src/user/user.service.spec.ts b/apps/backend/src/user/user.service.spec.ts index 56e07464..b397f1bb 100644 --- a/apps/backend/src/user/user.service.spec.ts +++ b/apps/backend/src/user/user.service.spec.ts @@ -3,7 +3,7 @@ import { GetUser, PageQueryDTO, User, - UserDocument, + UserDocument } from '@nbw/database'; import { HttpException, HttpStatus } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; @@ -13,14 +13,14 @@ import { Model } from 'mongoose'; import { UserService } from './user.service'; const mockUserModel = { - create: jest.fn(), - findOne: jest.fn(), - findById: jest.fn(), - find: jest.fn(), - save: jest.fn(), - exec: jest.fn(), - select: jest.fn(), - countDocuments: jest.fn(), + create : jest.fn(), + findOne : jest.fn(), + findById : jest.fn(), + find : jest.fn(), + save : jest.fn(), + exec : jest.fn(), + select : jest.fn(), + countDocuments: jest.fn() }; describe('UserService', () => { @@ -32,10 +32,10 @@ describe('UserService', () => { providers: [ UserService, { - provide: getModelToken(User.name), - useValue: mockUserModel, - }, - ], + provide : getModelToken(User.name), + useValue: mockUserModel + } + ] }).compile(); service = module.get(UserService); @@ -49,14 +49,14 @@ describe('UserService', () => { describe('create', () => { it('should create a new user', async () => { const createUserDto: CreateUser = { - username: 'testuser', - email: 'test@example.com', - profileImage: 'testimage.png', + username : 'testuser', + email : 'test@example.com', + profileImage: 'testimage.png' }; const user = { ...createUserDto, - save: jest.fn().mockReturnThis(), + save: jest.fn().mockReturnThis() } as any; jest.spyOn(userModel, 'create').mockReturnValue(user); @@ -75,7 +75,7 @@ describe('UserService', () => { const user = { email } as UserDocument; jest.spyOn(userModel, 'findOne').mockReturnValue({ - exec: jest.fn().mockResolvedValue(user), + exec: jest.fn().mockResolvedValue(user) } as any); const result = await service.findByEmail(email); @@ -91,7 +91,7 @@ describe('UserService', () => { const user = { _id: id } as UserDocument; jest.spyOn(userModel, 'findById').mockReturnValue({ - exec: jest.fn().mockResolvedValue(user), + exec: jest.fn().mockResolvedValue(user) } as any); const result = await service.findByID(id); @@ -109,14 +109,14 @@ describe('UserService', () => { const usersPage = { users, total: 1, - page: 1, - limit: 10, + page : 1, + limit: 10 }; const mockFind = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockResolvedValue(users), + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit: jest.fn().mockResolvedValue(users) }; jest.spyOn(userModel, 'find').mockReturnValue(mockFind as any); @@ -160,8 +160,8 @@ describe('UserService', () => { await expect(service.getUserByEmailOrId(query)).rejects.toThrow( new HttpException( 'Username is not supported yet', - HttpStatus.BAD_REQUEST, - ), + HttpStatus.BAD_REQUEST + ) ); }); @@ -171,8 +171,8 @@ describe('UserService', () => { await expect(service.getUserByEmailOrId(query)).rejects.toThrow( new HttpException( 'You must provide an email or an id', - HttpStatus.BAD_REQUEST, - ), + HttpStatus.BAD_REQUEST + ) ); }); }); @@ -184,7 +184,7 @@ describe('UserService', () => { jest.spyOn(userModel, 'findById').mockReturnValue({ populate: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(hydratedUser), + exec : jest.fn().mockResolvedValue(hydratedUser) } as any); const result = await service.getHydratedUser(user); @@ -193,7 +193,7 @@ describe('UserService', () => { expect(userModel.findById).toHaveBeenCalledWith(user._id); expect(userModel.findById(user._id).populate).toHaveBeenCalledWith( - 'songs', + 'songs' ); }); }); @@ -217,7 +217,7 @@ describe('UserService', () => { jest.spyOn(service, 'findByID').mockResolvedValue(null); await expect(service.getSelfUserData(user)).rejects.toThrow( - new HttpException('user not found', HttpStatus.NOT_FOUND), + new HttpException('user not found', HttpStatus.NOT_FOUND) ); }); @@ -229,9 +229,9 @@ describe('UserService', () => { const userData = { ...user, - lastSeen: yesterday, + lastSeen : yesterday, loginStreak: 1, - save: jest.fn().mockResolvedValue(true), + save : jest.fn().mockResolvedValue(true) } as unknown as UserDocument; jest.spyOn(service, 'findByID').mockResolvedValue(userData); @@ -250,9 +250,9 @@ describe('UserService', () => { const userData = { ...user, - lastSeen: today, + lastSeen : today, loginStreak: 1, - save: jest.fn().mockResolvedValue(true), + save : jest.fn().mockResolvedValue(true) } as unknown as UserDocument; jest.spyOn(service, 'findByID').mockResolvedValue(userData); @@ -272,9 +272,9 @@ describe('UserService', () => { const userData = { ...user, - lastSeen: twoDaysAgo, + lastSeen : twoDaysAgo, loginStreak: 5, - save: jest.fn().mockResolvedValue(true), + save : jest.fn().mockResolvedValue(true) } as unknown as UserDocument; jest.spyOn(service, 'findByID').mockResolvedValue(userData); @@ -294,9 +294,9 @@ describe('UserService', () => { const userData = { ...user, - lastSeen: yesterday, + lastSeen : yesterday, loginCount: 5, - save: jest.fn().mockResolvedValue(true), + save : jest.fn().mockResolvedValue(true) } as unknown as UserDocument; jest.spyOn(service, 'findByID').mockResolvedValue(userData); @@ -316,9 +316,9 @@ describe('UserService', () => { const userData = { ...user, - lastSeen: today, + lastSeen : today, loginCount: 5, - save: jest.fn().mockResolvedValue(true), + save : jest.fn().mockResolvedValue(true) } as unknown as UserDocument; jest.spyOn(service, 'findByID').mockResolvedValue(userData); @@ -339,10 +339,10 @@ describe('UserService', () => { const userData = { ...user, - lastSeen: yesterday, - loginStreak: 8, + lastSeen : yesterday, + loginStreak : 8, maxLoginStreak: 8, - save: jest.fn().mockResolvedValue(true), + save : jest.fn().mockResolvedValue(true) } as unknown as UserDocument; jest.spyOn(service, 'findByID').mockResolvedValue(userData); @@ -362,10 +362,10 @@ describe('UserService', () => { const userData = { ...user, - lastSeen: yesterday, - loginStreak: 4, + lastSeen : yesterday, + loginStreak : 4, maxLoginStreak: 8, - save: jest.fn().mockResolvedValue(true), + save : jest.fn().mockResolvedValue(true) } as unknown as UserDocument; jest.spyOn(service, 'findByID').mockResolvedValue(userData); @@ -384,7 +384,7 @@ describe('UserService', () => { jest.spyOn(userModel, 'findOne').mockReturnValue({ select: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(user), + exec : jest.fn().mockResolvedValue(user) } as any); const result = await service.usernameExists(username); @@ -393,7 +393,7 @@ describe('UserService', () => { expect(userModel.findOne).toHaveBeenCalledWith({ username }); expect(userModel.findOne({ username }).select).toHaveBeenCalledWith( - 'username', + 'username' ); }); @@ -402,7 +402,7 @@ describe('UserService', () => { jest.spyOn(userModel, 'findOne').mockReturnValue({ select: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(null), + exec : jest.fn().mockResolvedValue(null) } as any); const result = await service.usernameExists(username); @@ -411,7 +411,7 @@ describe('UserService', () => { expect(userModel.findOne).toHaveBeenCalledWith({ username }); expect(userModel.findOne({ username }).select).toHaveBeenCalledWith( - 'username', + 'username' ); }); }); @@ -488,7 +488,7 @@ describe('UserService', () => { it('should update a user username', async () => { const user = { username: 'testuser', - save: jest.fn().mockReturnThis(), + save : jest.fn().mockReturnThis() } as unknown as UserDocument; const body = { username: 'newuser' }; @@ -498,9 +498,9 @@ describe('UserService', () => { const result = await service.updateUsername(user, body); expect(result).toEqual({ - username: 'newuser', + username : 'newuser', publicName: undefined, - email: undefined, + email : undefined }); expect(user.username).toBe(body.username); @@ -510,7 +510,7 @@ describe('UserService', () => { it('should throw an error if username already exists', async () => { const user = { username: 'testuser', - save: jest.fn().mockReturnThis(), + save : jest.fn().mockReturnThis() } as unknown as UserDocument; const body = { username: 'newuser' }; @@ -518,7 +518,7 @@ describe('UserService', () => { jest.spyOn(service, 'usernameExists').mockResolvedValue(true); await expect(service.updateUsername(user, body)).rejects.toThrow( - new HttpException('Username already exists', HttpStatus.BAD_REQUEST), + new HttpException('Username already exists', HttpStatus.BAD_REQUEST) ); }); }); diff --git a/apps/backend/src/user/user.service.ts b/apps/backend/src/user/user.service.ts index 051da77e..6a509fa3 100644 --- a/apps/backend/src/user/user.service.ts +++ b/apps/backend/src/user/user.service.ts @@ -5,7 +5,7 @@ import { UpdateUsernameDto, User, UserDocument, - UserDto, + UserDto } from '@nbw/database'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; @@ -29,7 +29,7 @@ export class UserService { public async update(user: UserDocument): Promise { try { return (await this.userModel.findByIdAndUpdate(user._id, user, { - new: true, // return the updated document + new: true // return the updated document })) as UserDocument; } catch (error) { if (error instanceof Error) { @@ -47,18 +47,18 @@ export class UserService { if (userByEmail) { throw new HttpException( 'Email already registered', - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } const emailPrefixUsername = await this.generateUsername( - email.split('@')[0], + email.split('@')[0] ); const user = await this.userModel.create({ - email: email, - username: emailPrefixUsername, - publicName: emailPrefixUsername, + email : email, + username : emailPrefixUsername, + publicName: emailPrefixUsername }); return user; @@ -77,7 +77,7 @@ export class UserService { } public async findByPublicName( - publicName: string, + publicName: string ): Promise { const user = await this.userModel.findOne({ publicName }); @@ -108,7 +108,7 @@ export class UserService { users, total, page, - limit, + limit }; } @@ -126,13 +126,13 @@ export class UserService { if (username) { throw new HttpException( 'Username is not supported yet', - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } throw new HttpException( 'You must provide an email or an id', - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } @@ -217,7 +217,7 @@ export class UserService { if (await this.usernameExists(username)) { throw new HttpException( 'Username already exists', - HttpStatus.BAD_REQUEST, + HttpStatus.BAD_REQUEST ); } diff --git a/apps/frontend/mdx-components.tsx b/apps/frontend/mdx-components.tsx index 92af1be8..0f99736c 100644 --- a/apps/frontend/mdx-components.tsx +++ b/apps/frontend/mdx-components.tsx @@ -15,7 +15,7 @@ import { ol, p, pre, - ul, + ul } from '@web/modules/shared/components/CustomMarkdown'; export function useMDXComponents(components: MDXComponents): MDXComponents { @@ -35,6 +35,6 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { pre, code, a, - ...components, + ...components }; } diff --git a/apps/frontend/src/app/(content)/(info)/about/page.tsx b/apps/frontend/src/app/(content)/(info)/about/page.tsx index 37916161..bddc8ab8 100644 --- a/apps/frontend/src/app/(content)/(info)/about/page.tsx +++ b/apps/frontend/src/app/(content)/(info)/about/page.tsx @@ -6,7 +6,7 @@ import { NoteBlockWorldLogo } from '@web/modules/shared/components/NoteBlockWorl import About from './about.mdx'; export const metadata: Metadata = { - title: 'About', + title: 'About' }; const AboutPage = () => { diff --git a/apps/frontend/src/app/(content)/(info)/blog/[id]/page.tsx b/apps/frontend/src/app/(content)/(info)/blog/[id]/page.tsx index c0d63d05..736b1e23 100644 --- a/apps/frontend/src/app/(content)/(info)/blog/[id]/page.tsx +++ b/apps/frontend/src/app/(content)/(info)/blog/[id]/page.tsx @@ -17,18 +17,18 @@ export function generateMetadata({ params }: BlogPageProps): Metadata { const publicUrl = process.env.NEXT_PUBLIC_URL; return { - title: post.title, - authors: [{ name: post.author }], + title : post.title, + authors : [{ name: post.author }], openGraph: { - url: publicUrl + '/blog/' + id, - title: post.title, + url : publicUrl + '/blog/' + id, + title : post.title, siteName: 'Note Block World', - images: [ + images : [ { - url: publicUrl + post.image, - }, - ], - }, + url: publicUrl + post.image + } + ] + } }; } @@ -80,11 +80,11 @@ const BlogPost = ({ params }: BlogPageProps) => {

{/* Add 12 hours to the date to display at noon UTC */} {new Date( - new Date(post.date).getTime() + 12 * 60 * 60 * 1000, + new Date(post.date).getTime() + 12 * 60 * 60 * 1000 ).toLocaleDateString('en-UK', { - day: 'numeric', + day : 'numeric', month: 'short', - year: 'numeric', + year : 'numeric' })}

diff --git a/apps/frontend/src/app/(content)/(info)/blog/page.tsx b/apps/frontend/src/app/(content)/(info)/blog/page.tsx index bcadb92c..bbe24879 100644 --- a/apps/frontend/src/app/(content)/(info)/blog/page.tsx +++ b/apps/frontend/src/app/(content)/(info)/blog/page.tsx @@ -8,7 +8,7 @@ import { getSortedPostsData } from '@web/lib/posts'; import type { PostType } from '@web/lib/posts'; export const metadata: Metadata = { - title: 'Blog', + title: 'Blog' }; async function BlogPage() { diff --git a/apps/frontend/src/app/(content)/(info)/contact/page.tsx b/apps/frontend/src/app/(content)/(info)/contact/page.tsx index ebd20a1a..7cac16df 100644 --- a/apps/frontend/src/app/(content)/(info)/contact/page.tsx +++ b/apps/frontend/src/app/(content)/(info)/contact/page.tsx @@ -5,7 +5,7 @@ import BackButton from '@web/modules/shared/components/client/BackButton'; import Contact from './contact.mdx'; export const metadata: Metadata = { - title: 'Contact', + title: 'Contact' }; const AboutPage = () => { diff --git a/apps/frontend/src/app/(content)/(info)/help/[id]/page.tsx b/apps/frontend/src/app/(content)/(info)/help/[id]/page.tsx index 2507590c..f975a79a 100644 --- a/apps/frontend/src/app/(content)/(info)/help/[id]/page.tsx +++ b/apps/frontend/src/app/(content)/(info)/help/[id]/page.tsx @@ -17,18 +17,18 @@ export function generateMetadata({ params }: HelpPageProps): Metadata { const publicUrl = process.env.NEXT_PUBLIC_URL; return { - title: post.title, - authors: [{ name: post.author }], + title : post.title, + authors : [{ name: post.author }], openGraph: { - url: publicUrl + '/help/' + id, - title: post.title, + url : publicUrl + '/help/' + id, + title : post.title, siteName: 'Note Block World', - images: [ + images : [ { - url: publicUrl + post.image, - }, - ], - }, + url: publicUrl + post.image + } + ] + } }; } diff --git a/apps/frontend/src/app/(content)/(info)/help/page.tsx b/apps/frontend/src/app/(content)/(info)/help/page.tsx index f0984c24..72624227 100644 --- a/apps/frontend/src/app/(content)/(info)/help/page.tsx +++ b/apps/frontend/src/app/(content)/(info)/help/page.tsx @@ -8,7 +8,7 @@ import { getSortedPostsData } from '@web/lib/posts'; import type { PostType } from '@web/lib/posts'; export const metadata: Metadata = { - title: 'Help Center', + title: 'Help Center' }; async function HelpPage() { diff --git a/apps/frontend/src/app/(content)/layout.tsx b/apps/frontend/src/app/(content)/layout.tsx index 223b1086..5015d184 100644 --- a/apps/frontend/src/app/(content)/layout.tsx +++ b/apps/frontend/src/app/(content)/layout.tsx @@ -2,7 +2,7 @@ import '@web/app/globals.css'; import NavbarLayout from '@web/modules/shared/components/layout/NavbarLayout'; export default async function ContentLayout({ - children, + children }: { children: React.ReactNode; }) { diff --git a/apps/frontend/src/app/(content)/my-songs/page.tsx b/apps/frontend/src/app/(content)/my-songs/page.tsx index 110df923..68f94fa9 100644 --- a/apps/frontend/src/app/(content)/my-songs/page.tsx +++ b/apps/frontend/src/app/(content)/my-songs/page.tsx @@ -5,7 +5,7 @@ import { checkLogin } from '@web/modules/auth/features/auth.utils'; import Page from '@web/modules/my-songs/components/MySongsPage'; export const metadata: Metadata = { - title: 'My songs', + title: 'My songs' }; const MySongsPage = async () => { diff --git a/apps/frontend/src/app/(content)/page.tsx b/apps/frontend/src/app/(content)/page.tsx index ac9ee47c..53850a40 100644 --- a/apps/frontend/src/app/(content)/page.tsx +++ b/apps/frontend/src/app/(content)/page.tsx @@ -11,12 +11,12 @@ async function fetchRecentSongs() { '/song-browser/recent', { params: { - page: 1, // TODO: fiz constants + page : 1, // TODO: fiz constants limit: 16, // TODO: change 'limit' parameter to 'skip' and load 12 songs initially, then load 8 more songs on each pagination - sort: 'recent', - order: false, - }, - }, + sort : 'recent', + order: false + } + } ); return response.data; @@ -28,24 +28,24 @@ async function fetchRecentSongs() { async function fetchFeaturedSongs(): Promise { try { const response = await axiosInstance.get( - '/song-browser/featured', + '/song-browser/featured' ); return response.data; } catch (error) { return { - hour: [], - day: [], - week: [], + hour : [], + day : [], + week : [], month: [], - year: [], - all: [], + year : [], + all : [] }; } } export const metadata: Metadata = { - title: 'Songs', + title: 'Songs' }; async function Home() { diff --git a/apps/frontend/src/app/(content)/song/[id]/page.tsx b/apps/frontend/src/app/(content)/song/[id]/page.tsx index 198a9c96..85c1cb1b 100644 --- a/apps/frontend/src/app/(content)/song/[id]/page.tsx +++ b/apps/frontend/src/app/(content)/song/[id]/page.tsx @@ -12,7 +12,7 @@ interface SongPage { } export async function generateMetadata({ - params, + params }: SongPage): Promise { let song; const publicUrl = process.env.NEXT_PUBLIC_URL; @@ -28,30 +28,30 @@ export async function generateMetadata({ try { const response = await axios.get(`/song/${params.id}`, { - headers, + headers }); song = await response.data; } catch { return { - title: 'Song not found', + title: 'Song not found' }; } return { - title: song.title, + title : song.title, description: song.description, - authors: [{ name: song.uploader.username }], - openGraph: { - url: publicUrl + '/song/' + song.publicId, - title: song.title, + authors : [{ name: song.uploader.username }], + openGraph : { + url : publicUrl + '/song/' + song.publicId, + title : song.title, description: song.description, - siteName: 'Note Block World', - images: [song.thumbnailUrl], + siteName : 'Note Block World', + images : [song.thumbnailUrl] }, twitter: { - card: 'summary_large_image', - }, + card: 'summary_large_image' + } }; } diff --git a/apps/frontend/src/app/(content)/upload/layout.tsx b/apps/frontend/src/app/(content)/upload/layout.tsx index 39086489..6d1261c9 100644 --- a/apps/frontend/src/app/(content)/upload/layout.tsx +++ b/apps/frontend/src/app/(content)/upload/layout.tsx @@ -1,7 +1,7 @@ import '@web/app/enableRecaptchaBadge.css'; export default async function UploadLayout({ - children, + children }: { children: React.ReactNode; }) { diff --git a/apps/frontend/src/app/(content)/upload/page.tsx b/apps/frontend/src/app/(content)/upload/page.tsx index bccf0c9d..997918e2 100644 --- a/apps/frontend/src/app/(content)/upload/page.tsx +++ b/apps/frontend/src/app/(content)/upload/page.tsx @@ -5,7 +5,7 @@ import { checkLogin, getUserData } from '@web/modules/auth/features/auth.utils'; import { UploadSongPage } from '@web/modules/song-upload/components/client/UploadSongPage'; export const metadata: Metadata = { - title: 'Upload song', + title: 'Upload song' }; async function UploadPage() { diff --git a/apps/frontend/src/app/(external)/(auth)/login/email/page.tsx b/apps/frontend/src/app/(external)/(auth)/login/email/page.tsx index be8b80b9..e1d174b1 100644 --- a/apps/frontend/src/app/(external)/(auth)/login/email/page.tsx +++ b/apps/frontend/src/app/(external)/(auth)/login/email/page.tsx @@ -5,7 +5,7 @@ import { LoginWithEmailPage } from '@web/modules/auth/components/loginWithEmailP import { checkLogin } from '@web/modules/auth/features/auth.utils'; export const metadata: Metadata = { - title: 'Sign in', + title: 'Sign in' }; const Login = async () => { diff --git a/apps/frontend/src/app/(external)/(auth)/login/page.tsx b/apps/frontend/src/app/(external)/(auth)/login/page.tsx index b8c1de65..0859da86 100644 --- a/apps/frontend/src/app/(external)/(auth)/login/page.tsx +++ b/apps/frontend/src/app/(external)/(auth)/login/page.tsx @@ -5,7 +5,7 @@ import { LoginPage } from '@web/modules/auth/components/loginPage'; import { checkLogin } from '@web/modules/auth/features/auth.utils'; export const metadata: Metadata = { - title: 'Sign in', + title: 'Sign in' }; const Login = async () => { diff --git a/apps/frontend/src/app/(external)/(legal)/guidelines/page.tsx b/apps/frontend/src/app/(external)/(legal)/guidelines/page.tsx index 5ba331fc..931afc43 100644 --- a/apps/frontend/src/app/(external)/(legal)/guidelines/page.tsx +++ b/apps/frontend/src/app/(external)/(legal)/guidelines/page.tsx @@ -6,7 +6,7 @@ import { Metadata } from 'next'; import { CustomMarkdown } from '@web/modules/shared/components/CustomMarkdown'; export const metadata: Metadata = { - title: 'Community Guidelines', + title: 'Community Guidelines' }; async function TermsOfServicePage() { diff --git a/apps/frontend/src/app/(external)/(legal)/privacy/page.tsx b/apps/frontend/src/app/(external)/(legal)/privacy/page.tsx index 4ef11c59..26321593 100644 --- a/apps/frontend/src/app/(external)/(legal)/privacy/page.tsx +++ b/apps/frontend/src/app/(external)/(legal)/privacy/page.tsx @@ -6,7 +6,7 @@ import { Metadata } from 'next'; import { CustomMarkdown } from '@web/modules/shared/components/CustomMarkdown'; export const metadata: Metadata = { - title: 'Privacy Policy', + title: 'Privacy Policy' }; async function PrivacyPolicyPage() { diff --git a/apps/frontend/src/app/(external)/(legal)/terms/page.tsx b/apps/frontend/src/app/(external)/(legal)/terms/page.tsx index 40290108..4bd4812b 100644 --- a/apps/frontend/src/app/(external)/(legal)/terms/page.tsx +++ b/apps/frontend/src/app/(external)/(legal)/terms/page.tsx @@ -6,7 +6,7 @@ import { Metadata } from 'next'; import { CustomMarkdown } from '@web/modules/shared/components/CustomMarkdown'; export const metadata: Metadata = { - title: 'Terms of Service', + title: 'Terms of Service' }; async function TermsOfServicePage() { diff --git a/apps/frontend/src/app/(external)/[...not-found]/page.tsx b/apps/frontend/src/app/(external)/[...not-found]/page.tsx index ac4b13ea..8d68f3a2 100644 --- a/apps/frontend/src/app/(external)/[...not-found]/page.tsx +++ b/apps/frontend/src/app/(external)/[...not-found]/page.tsx @@ -5,7 +5,7 @@ import { notFound } from 'next/navigation'; // See: https://github.com/vercel/next.js/discussions/50034 export const metadata: Metadata = { - title: 'Page not found', + title: 'Page not found' }; export default function NotFound() { diff --git a/apps/frontend/src/app/(external)/layout.tsx b/apps/frontend/src/app/(external)/layout.tsx index aa9d1eb1..2cc6f442 100644 --- a/apps/frontend/src/app/(external)/layout.tsx +++ b/apps/frontend/src/app/(external)/layout.tsx @@ -3,7 +3,7 @@ import '@web/app/enableRecaptchaBadge.css'; import '@web/app/hideScrollbar.css'; export default async function LoginLayout({ - children, + children }: { children: React.ReactNode; }) { @@ -12,7 +12,7 @@ export default async function LoginLayout({
{children} diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index 76ddeaff..e4ae21a7 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -15,7 +15,7 @@ import { TooltipProvider } from '../modules/shared/components/tooltip'; const lato = Lato({ subsets: ['latin'], - weight: ['100', '300', '400', '700', '900'], + weight : ['100', '300', '400', '700', '900'] }); export const metadata: Metadata = { @@ -23,27 +23,27 @@ export const metadata: Metadata = { description: 'Discover, share and listen to note block music from all around the world', applicationName: 'Note Block World', - keywords: ['note block', 'music', 'minecraft', 'nbs', 'note block studio'], - openGraph: { - type: 'website', - images: `${process.env.NEXT_PUBLIC_URL}/nbw-header.png`, - locale: 'en_US', - url: process.env.NEXT_PUBLIC_URL, - siteName: 'Note Block World', - }, + keywords : ['note block', 'music', 'minecraft', 'nbs', 'note block studio'], + openGraph : { + type : 'website', + images : `${process.env.NEXT_PUBLIC_URL}/nbw-header.png`, + locale : 'en_US', + url : process.env.NEXT_PUBLIC_URL, + siteName: 'Note Block World' + } }; const jsonLd: WithContext = { '@context': 'https://schema.org', - '@type': 'WebSite', - name: 'Note Block World', - url: process.env.NEXT_PUBLIC_URL, + '@type' : 'WebSite', + name : 'Note Block World', + url : process.env.NEXT_PUBLIC_URL, description: - 'Discover, share and listen to note block music from all around the world', + 'Discover, share and listen to note block music from all around the world' }; export default function RootLayout({ - children, + children }: { children: React.ReactNode; }) { @@ -100,7 +100,7 @@ export default function RootLayout({ position='bottom-center' toastOptions={{ className: '!bg-zinc-700 !text-white !max-w-fit', - duration: 4000, + duration : 4000 }} /> diff --git a/apps/frontend/src/lib/axios/ClientAxios.ts b/apps/frontend/src/lib/axios/ClientAxios.ts index bdb14b2c..cf78f749 100644 --- a/apps/frontend/src/lib/axios/ClientAxios.ts +++ b/apps/frontend/src/lib/axios/ClientAxios.ts @@ -5,8 +5,8 @@ import { getTokenLocal } from './token.utils'; export const baseApiURL = process.env.NEXT_PUBLIC_API_URL; const ClientAxios = axios.create({ - baseURL: baseApiURL, - withCredentials: true, + baseURL : baseApiURL, + withCredentials: true }); // Add a request interceptor to add the token to the request @@ -24,7 +24,7 @@ ClientAxios.interceptors.request.use( }, (error) => { return Promise.reject(error); - }, + } ); export default ClientAxios; diff --git a/apps/frontend/src/lib/axios/index.ts b/apps/frontend/src/lib/axios/index.ts index 6b5ec67a..b2905b6d 100644 --- a/apps/frontend/src/lib/axios/index.ts +++ b/apps/frontend/src/lib/axios/index.ts @@ -3,8 +3,8 @@ import axios from 'axios'; export const baseApiURL = process.env.NEXT_PUBLIC_API_URL; const axiosInstance = axios.create({ - baseURL: baseApiURL, - withCredentials: true, + baseURL : baseApiURL, + withCredentials: true }); export default axiosInstance; diff --git a/apps/frontend/src/lib/posts.ts b/apps/frontend/src/lib/posts.ts index 109edefb..ace540c1 100644 --- a/apps/frontend/src/lib/posts.ts +++ b/apps/frontend/src/lib/posts.ts @@ -34,7 +34,7 @@ const helpPostIds = fs export function getSortedPostsData( postsPath: 'help' | 'blog', - sortBy: 'id' | 'date', + sortBy: 'id' | 'date' ) { const postsDirectory = path.join(process.cwd(), 'posts', postsPath); @@ -73,7 +73,7 @@ export function getSortedPostsData( export function getPostData( postsPath: 'help' | 'blog', - postId: string, + postId: string ): PostType { // Look for the file in the posts directory that contains postId as suffix const fileName = @@ -93,8 +93,8 @@ export function getPostData( // Combine the data with the id return { - id: postId, + id : postId, ...(matterResult.data as Omit), - content: matterResult.content, + content: matterResult.content }; } diff --git a/apps/frontend/src/modules/auth/components/client/LoginFrom.tsx b/apps/frontend/src/modules/auth/components/client/LoginFrom.tsx index ab4a701c..4c989fcf 100644 --- a/apps/frontend/src/modules/auth/components/client/LoginFrom.tsx +++ b/apps/frontend/src/modules/auth/components/client/LoginFrom.tsx @@ -10,7 +10,7 @@ import ClientAxios from '@web/lib/axios/ClientAxios'; import { Input, - SubmitButton, + SubmitButton } from '../../../shared/components/client/FormElements'; type LoginFormData = { @@ -27,7 +27,7 @@ export const LoginForm: FC = () => { const { register, handleSubmit, - formState: { errors }, + formState: { errors } } = useForm(); const onSubmit = async ({ email }: LoginFormData) => { @@ -36,19 +36,19 @@ export const LoginForm: FC = () => { const url = `${backendURL}/auth/login/magic-link`; const response = await ClientAxios.post(url, { - destination: email, + destination: email }); console.log(response.data); toast.success(`A magic link has been sent to ${email}!`, { position: 'top-center', - duration: 20_000, // 20 seconds + duration: 20_000 // 20 seconds }); toast.success('It will stay valid for one hour!', { position: 'top-center', - duration: 20_000, // 20 seconds + duration: 20_000 // 20 seconds }); } catch (error) { if ((error as any).isAxiosError) { @@ -60,19 +60,19 @@ export const LoginForm: FC = () => { if (status === 429) { toast.error('Too many requests. Please try again later.', { position: 'top-center', - duration: 20_000, // 20 seconds + duration: 20_000 // 20 seconds }); } else { toast.error('An unexpected error occurred', { position: 'top-center', - duration: 20_000, // 20 seconds + duration: 20_000 // 20 seconds }); } } } else { toast.error('An unexpected error occurred', { position: 'top-center', - duration: 20_000, // 20 seconds + duration: 20_000 // 20 seconds }); } } finally { diff --git a/apps/frontend/src/modules/auth/components/loginPage.tsx b/apps/frontend/src/modules/auth/components/loginPage.tsx index 5551d6d3..933ed66b 100644 --- a/apps/frontend/src/modules/auth/components/loginPage.tsx +++ b/apps/frontend/src/modules/auth/components/loginPage.tsx @@ -1,7 +1,7 @@ import { faDiscord, faGithub, - faGoogle, + faGoogle } from '@fortawesome/free-brands-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Link from 'next/link'; diff --git a/apps/frontend/src/modules/auth/components/loginWithEmailPage.tsx b/apps/frontend/src/modules/auth/components/loginWithEmailPage.tsx index 9ba806f1..06367fd3 100644 --- a/apps/frontend/src/modules/auth/components/loginWithEmailPage.tsx +++ b/apps/frontend/src/modules/auth/components/loginWithEmailPage.tsx @@ -1,7 +1,8 @@ -import { LoginForm } from './client/LoginFrom'; import { CopyrightFooter } from '../../shared/components/layout/CopyrightFooter'; import { NoteBlockWorldLogo } from '../../shared/components/NoteBlockWorldLogo'; +import { LoginForm } from './client/LoginFrom'; + export const LoginWithEmailPage = () => { return (
{ // verify the token with the server const res = await axiosInstance.get('/auth/verify', { headers: { - authorization: `Bearer ${token.value}`, - }, + authorization: `Bearer ${token.value}` + } }); // if the token is valid, redirect to home page @@ -44,8 +44,8 @@ export const getUserData = async (): Promise => { // verify the token with the server const res = await axiosInstance.get('/user/me', { headers: { - authorization: `Bearer ${token.value}`, - }, + authorization: `Bearer ${token.value}` + } }); // if the token is valid, redirect to home page diff --git a/apps/frontend/src/modules/browse/WelcomeBanner.tsx b/apps/frontend/src/modules/browse/WelcomeBanner.tsx index 89c64d8a..00fbb28a 100644 --- a/apps/frontend/src/modules/browse/WelcomeBanner.tsx +++ b/apps/frontend/src/modules/browse/WelcomeBanner.tsx @@ -7,9 +7,9 @@ export const WelcomeBanner = () => {
diff --git a/apps/frontend/src/modules/browse/components/HomePageComponent.tsx b/apps/frontend/src/modules/browse/components/HomePageComponent.tsx index 4ea9ecb0..127582af 100644 --- a/apps/frontend/src/modules/browse/components/HomePageComponent.tsx +++ b/apps/frontend/src/modules/browse/components/HomePageComponent.tsx @@ -3,16 +3,9 @@ import { faEllipsis } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { CategoryButtonGroup } from './client/CategoryButton'; -import { useFeaturedSongsProvider } from './client/context/FeaturedSongs.context'; -import { useRecentSongsProvider } from './client/context/RecentSongs.context'; -import LoadMoreButton from './client/LoadMoreButton'; -import { TimespanButtonGroup } from './client/TimespanButton'; -import SongCard from './SongCard'; -import SongCardGroup from './SongCardGroup'; import { InterSectionAdSlot, - SongCardAdSlot, + SongCardAdSlot } from '../../shared/components/client/ads/AdSlots'; import { Carousel, @@ -20,10 +13,18 @@ import { CarouselDots, CarouselItem, CarouselNext, - CarouselPrevious, + CarouselPrevious } from '../../shared/components/client/Carousel'; import { WelcomeBanner } from '../WelcomeBanner'; +import { CategoryButtonGroup } from './client/CategoryButton'; +import { useFeaturedSongsProvider } from './client/context/FeaturedSongs.context'; +import { useRecentSongsProvider } from './client/context/RecentSongs.context'; +import LoadMoreButton from './client/LoadMoreButton'; +import { TimespanButtonGroup } from './client/TimespanButton'; +import SongCard from './SongCard'; +import SongCardGroup from './SongCardGroup'; + export const HomePageComponent = () => { const { featuredSongsPage } = useFeaturedSongsProvider(); @@ -46,9 +47,9 @@ export const HomePageComponent = () => { @@ -84,7 +85,7 @@ export const HomePageComponent = () => { ) : ( - ), + ) )}
diff --git a/apps/frontend/src/modules/browse/components/SongCard.tsx b/apps/frontend/src/modules/browse/components/SongCard.tsx index d5025023..315bf7a6 100644 --- a/apps/frontend/src/modules/browse/components/SongCard.tsx +++ b/apps/frontend/src/modules/browse/components/SongCard.tsx @@ -46,7 +46,7 @@ const SongDataDisplay = ({ song }: { song: SongPreviewDtoType | null }) => { ) : ( `${song.uploader.username} • ${formatTimeAgo( - new Date(song.createdAt), + new Date(song.createdAt) )}` )}

diff --git a/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx b/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx index 0de6bb2c..2de1c892 100644 --- a/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx +++ b/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx @@ -7,7 +7,7 @@ import { CarouselContent, CarouselItem, CarouselNextSmall, - CarouselPreviousSmall, + CarouselPreviousSmall } from '@web/modules/shared/components/client/Carousel'; import { useRecentSongsProvider } from './context/RecentSongs.context'; @@ -28,11 +28,11 @@ export const CategoryButtonGroup = () => { @@ -71,7 +71,7 @@ export const CategoryButton = ({ isDisabled, onClick, children, - id, + id }: CategoryButtonProps) => { return ( diff --git a/apps/frontend/src/modules/browse/components/client/TimespanButton.tsx b/apps/frontend/src/modules/browse/components/client/TimespanButton.tsx index bc50d568..15625493 100644 --- a/apps/frontend/src/modules/browse/components/client/TimespanButton.tsx +++ b/apps/frontend/src/modules/browse/components/client/TimespanButton.tsx @@ -3,7 +3,7 @@ import { Tooltip, TooltipContent, - TooltipTrigger, + TooltipTrigger } from '@web/modules/shared/components/tooltip'; import { useFeaturedSongsProvider } from './context/FeaturedSongs.context'; @@ -75,7 +75,7 @@ export const TimespanButton = ({ isDisabled, onClick, children, - id, + id }: TimespanButtonProps) => { return ( diff --git a/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx b/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx index 8b32d8a1..46ecc74b 100644 --- a/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx +++ b/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx @@ -1,10 +1,6 @@ 'use client'; -import { - FeaturedSongsDtoType, - SongPreviewDtoType, - TimespanType, -} from '@nbw/database'; +import { FeaturedSongsDtoType, SongPreviewDtoType, TimespanType } from '@nbw/database'; import { createContext, useContext, useEffect, useState } from 'react'; type FeaturedSongsContextType = { @@ -15,16 +11,10 @@ type FeaturedSongsContextType = { }; const FeaturedSongsContext = createContext( - {} as FeaturedSongsContextType, + {} as FeaturedSongsContextType ); -export function FeaturedSongsProvider({ - children, - initialFeaturedSongs, -}: { - children: React.ReactNode; - initialFeaturedSongs: FeaturedSongsDtoType; -}) { +export function FeaturedSongsProvider({ children, initialFeaturedSongs }: { children: React.ReactNode; initialFeaturedSongs: FeaturedSongsDtoType;}) { // Featured songs const [featuredSongs] = useState(initialFeaturedSongs); @@ -39,11 +29,10 @@ export function FeaturedSongsProvider({ result[timespan] = featuredSongs[timespan as TimespanType].length === 0; return result; }, - {} as Record, + {} as Record ); useEffect(() => { - // eslint-disable-next-line react-hooks/exhaustive-deps setFeaturedSongsPage(featuredSongs[timespan]); }, [featuredSongs, timespan]); @@ -53,7 +42,7 @@ export function FeaturedSongsProvider({ featuredSongsPage, timespan, setTimespan, - timespanEmpty, + timespanEmpty }} > {children} @@ -66,7 +55,7 @@ export function useFeaturedSongsProvider() { if (context === undefined || context === null) { throw new Error( - 'useFeaturedSongsProvider must be used within a FeaturedSongsProvider', + 'useFeaturedSongsProvider must be used within a FeaturedSongsProvider' ); } diff --git a/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx b/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx index c65e7ab1..abad6bb3 100644 --- a/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx +++ b/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx @@ -9,13 +9,13 @@ import { RecentSongsProvider } from './RecentSongs.context'; type HomePageContextType = null; const HomePageContext = createContext( - null as HomePageContextType, + null as HomePageContextType ); export function HomePageProvider({ children, initialRecentSongs, - initialFeaturedSongs, + initialFeaturedSongs }: { children: React.ReactNode; initialRecentSongs: SongPreviewDtoType[]; @@ -37,7 +37,7 @@ export function useHomePageProvider() { if (context === undefined || context === null) { throw new Error( - 'useHomePageProvider must be used within a HomepageProvider', + 'useHomePageProvider must be used within a HomepageProvider' ); } diff --git a/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx b/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx index 8d7fe9b7..d2650af4 100644 --- a/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx +++ b/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx @@ -6,7 +6,7 @@ import { useCallback, useContext, useEffect, - useState, + useState } from 'react'; import axiosInstance from '@web/lib/axios'; @@ -23,12 +23,12 @@ type RecentSongsContextType = { }; const RecentSongsContext = createContext( - {} as RecentSongsContextType, + {} as RecentSongsContextType ); export function RecentSongsProvider({ children, - initialRecentSongs, + initialRecentSongs }: { children: React.ReactNode; initialRecentSongs: SongPreviewDtoType[]; @@ -61,9 +61,9 @@ export function RecentSongsProvider({ params: { page, limit: fetchCount, // TODO: fix constants - order: false, - }, - }, + order: false + } + } ); const newSongs: Array = response.data; @@ -75,7 +75,7 @@ export function RecentSongsProvider({ setRecentSongs((prevSongs) => [ ...prevSongs.filter((song) => song !== null), - ...response.data, + ...response.data ]); if (response.data.length < fetchCount) { @@ -83,7 +83,7 @@ export function RecentSongsProvider({ } } catch (error) { setRecentSongs((prevSongs) => - prevSongs.filter((song) => song !== null), + prevSongs.filter((song) => song !== null) ); setRecentError('Error loading recent songs'); @@ -91,13 +91,13 @@ export function RecentSongsProvider({ setLoading(false); } }, - [page, endpoint], + [page, endpoint] ); const fetchCategories = useCallback(async function () { try { const response = await axiosInstance.get>( - '/song-browser/categories', + '/song-browser/categories' ); return response.data; @@ -149,7 +149,7 @@ export function RecentSongsProvider({ categories, selectedCategory, setSelectedCategory, - increasePageRecent, + increasePageRecent }} > {children} @@ -162,7 +162,7 @@ export function useRecentSongsProvider() { if (context === undefined || context === null) { throw new Error( - 'useRecentSongsProvider must be used within a RecentSongsProvider', + 'useRecentSongsProvider must be used within a RecentSongsProvider' ); } diff --git a/apps/frontend/src/modules/my-songs/components/MySongsPage.tsx b/apps/frontend/src/modules/my-songs/components/MySongsPage.tsx index 93ee1b15..524592e2 100644 --- a/apps/frontend/src/modules/my-songs/components/MySongsPage.tsx +++ b/apps/frontend/src/modules/my-songs/components/MySongsPage.tsx @@ -3,26 +3,27 @@ import type { SongPageDtoType, SongsFolder } from '@nbw/database'; import axiosInstance from '@web/lib/axios'; +import { getTokenServer } from '../../auth/features/auth.utils'; + import { MySongProvider } from './client/context/MySongs.context'; import { MySongsPageComponent } from './client/MySongsTable'; -import { getTokenServer } from '../../auth/features/auth.utils'; async function fetchSongsPage( page: number, pageSize: number, - token: string, + token: string ): Promise { const response = await axiosInstance .get('/my-songs', { headers: { - authorization: `Bearer ${token}`, + authorization: `Bearer ${token}` }, params: { - page: page + 1, + page : page + 1, limit: pageSize, - sort: 'createdAt', - order: false, - }, + sort : 'createdAt', + order: false + } }) .then((res) => { return res.data; @@ -52,7 +53,7 @@ async function fetchSongsFolder(): Promise { const firstPage = await fetchSongsPage(currentPage, pageSize, token.value); const data: SongsFolder = { - [currentPage]: firstPage, + [currentPage]: firstPage }; return data; @@ -62,10 +63,10 @@ async function fetchSongsFolder(): Promise { return { 0: { content: [], - total: 0, - page: 0, - limit: 0, - }, + total : 0, + page : 0, + limit : 0 + } }; } } diff --git a/apps/frontend/src/modules/my-songs/components/client/DeleteConfirmDialog.tsx b/apps/frontend/src/modules/my-songs/components/client/DeleteConfirmDialog.tsx index e740bf35..bf53b3cf 100644 --- a/apps/frontend/src/modules/my-songs/components/client/DeleteConfirmDialog.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/DeleteConfirmDialog.tsx @@ -4,7 +4,7 @@ export default function DeleteConfirmDialog({ isOpen, setIsOpen, songTitle, - onConfirm, + onConfirm }: { isOpen: boolean; setIsOpen: (value: boolean) => void; diff --git a/apps/frontend/src/modules/my-songs/components/client/MySongsButtons.tsx b/apps/frontend/src/modules/my-songs/components/client/MySongsButtons.tsx index 5a609f89..efdb4ca1 100644 --- a/apps/frontend/src/modules/my-songs/components/client/MySongsButtons.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/MySongsButtons.tsx @@ -3,7 +3,7 @@ import { faDownload, faPencil, - faTrash, + faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Link from 'next/link'; @@ -11,12 +11,12 @@ import Link from 'next/link'; import { Tooltip, TooltipContent, - TooltipTrigger, + TooltipTrigger } from '@web/modules/shared/components/tooltip'; import { downloadSongFile } from '@web/modules/song/util/downloadSong'; export const DownloadSongButton = ({ - song, + song }: { song: { publicId: string; @@ -33,7 +33,7 @@ export const DownloadSongButton = ({ }; const DownloadButton = ({ - handleClick, + handleClick }: { handleClick: React.MouseEventHandler; }) => { diff --git a/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx b/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx index 2140824d..bc0ec47a 100644 --- a/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx @@ -1,7 +1,7 @@ 'use client'; import { faChevronLeft, - faChevronRight, + faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { MY_SONGS } from '@nbw/config'; @@ -40,7 +40,7 @@ const NoSongs = () => ( const SongRows = ({ page, - pageSize, + pageSize }: { page: SongPageDtoType | null; pageSize: number; @@ -139,7 +139,7 @@ export const MySongsPageComponent = () => { isDeleteDialogOpen, setIsDeleteDialogOpen, songToDelete, - deleteSong, + deleteSong } = useMySongsProvider(); return ( diff --git a/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx b/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx index 7bc60297..69de8873 100644 --- a/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx @@ -2,7 +2,7 @@ import { faCirclePlay, faEye, faEyeSlash, - faPlay, + faPlay } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SongPreviewDtoType } from '@nbw/database'; @@ -12,13 +12,14 @@ import Skeleton from 'react-loading-skeleton'; import SongThumbnail from '@web/modules/shared/components/layout/SongThumbnail'; import { formatDuration } from '@web/modules/shared/util/format'; -import { useMySongsProvider } from './context/MySongs.context'; import { DeleteButton, DownloadSongButton, - EditButton, + EditButton } from '../client/MySongsButtons'; +import { useMySongsProvider } from './context/MySongs.context'; + export const SongRow = ({ song }: { song?: SongPreviewDtoType | null }) => { const { setIsDeleteDialogOpen, setSongToDelete } = useMySongsProvider(); @@ -123,9 +124,9 @@ export const SongRow = ({ song }: { song?: SongPreviewDtoType | null }) => { ) : ( <> {new Date(song.createdAt).toLocaleDateString('en-US', { - day: 'numeric', + day : 'numeric', month: 'short', - year: 'numeric', + year : 'numeric' })} )} diff --git a/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx b/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx index 72737afd..19943ced 100644 --- a/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx @@ -4,14 +4,14 @@ import { MY_SONGS } from '@nbw/config'; import type { SongPageDtoType, SongPreviewDtoType, - SongsFolder, + SongsFolder } from '@nbw/database'; import { createContext, useCallback, useContext, useEffect, - useState, + useState } from 'react'; import { toast } from 'react-hot-toast'; @@ -37,7 +37,7 @@ type MySongsContextType = { }; const MySongsContext = createContext( - {} as MySongsContextType, + {} as MySongsContextType ); type MySongProviderProps = { @@ -53,7 +53,7 @@ export const MySongProvider = ({ children, totalPagesInit = 0, currentPageInit = 0, - pageSizeInit = MY_SONGS.PAGE_SIZE, + pageSizeInit = MY_SONGS.PAGE_SIZE }: MySongProviderProps) => { const [loadedSongs, setLoadedSongs] = useState(InitialsongsFolder); @@ -69,14 +69,14 @@ export const MySongProvider = ({ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [songToDelete, setSongToDelete] = useState( - null, + null ); const putPage = useCallback( async ({ key, page }: { key: number; page: SongPageDtoType }) => { setLoadedSongs({ ...loadedSongs, [key]: page }); }, - [loadedSongs], + [loadedSongs] ); const fetchSongsPage = useCallback(async (): Promise => { @@ -86,22 +86,22 @@ export const MySongProvider = ({ try { const response = await axiosInstance.get('/my-songs', { params: { - page: currentPage, + page : currentPage, limit: pageSize, - sort: 'createdAt', - order: 'false', + sort : 'createdAt', + order: 'false' }, headers: { - authorization: `Bearer ${token}`, - }, + authorization: `Bearer ${token}` + } }); const data = response.data as SongPageDtoType; // TODO: total, page and pageSize are stored in every page, when it should be stored in the folder (what matters is 'content') putPage({ - key: currentPage, - page: data, + key : currentPage, + page: data }); setTotalSongs(data.total); @@ -144,7 +144,7 @@ export const MySongProvider = ({ setCurrentPage(page); } }, - [totalPages], + [totalPages] ); const nextpage = useCallback(() => { @@ -169,8 +169,8 @@ export const MySongProvider = ({ try { await axiosInstance.delete(`/song/${songToDelete.publicId}`, { headers: { - authorization: `Bearer ${token}`, - }, + authorization: `Bearer ${token}` + } }); setIsDeleteDialogOpen(false); @@ -208,7 +208,7 @@ export const MySongProvider = ({ setIsDeleteDialogOpen, songToDelete, setSongToDelete, - deleteSong, + deleteSong }} > {children} diff --git a/apps/frontend/src/modules/shared/components/CustomMarkdown.tsx b/apps/frontend/src/modules/shared/components/CustomMarkdown.tsx index 8fb45917..4f17aa89 100644 --- a/apps/frontend/src/modules/shared/components/CustomMarkdown.tsx +++ b/apps/frontend/src/modules/shared/components/CustomMarkdown.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import Markdown, { ExtraProps } from 'react-markdown'; export const CustomMarkdown = ({ - MarkdownContent, + MarkdownContent }: { MarkdownContent: string; }) => { @@ -23,7 +23,7 @@ export const CustomMarkdown = ({ blockquote, pre, code, - a, + a }} > {MarkdownContent} diff --git a/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx b/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx index 83c97fd2..a38c373a 100644 --- a/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx +++ b/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx @@ -6,7 +6,7 @@ export const NoteBlockWorldLogo = ({ size, orientation = 'adaptive', glow, - className, + className }: { size: number; orientation: 'horizontal' | 'vertical' | 'adaptive'; @@ -32,7 +32,7 @@ export const NoteBlockWorldLogo = ({ flexConfig, 'flex items-center justify-center gap-2 min-w-fit max-w-full', glow ? 'animate-[nbw-glow_3s_ease-in-out_infinite]' : '', - className, + className )} > ) { diff --git a/apps/frontend/src/modules/shared/components/client/Carousel.tsx b/apps/frontend/src/modules/shared/components/client/Carousel.tsx index 521b57ef..99e6254e 100644 --- a/apps/frontend/src/modules/shared/components/client/Carousel.tsx +++ b/apps/frontend/src/modules/shared/components/client/Carousel.tsx @@ -2,11 +2,11 @@ import { faChevronLeft, - faChevronRight, + faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import useEmblaCarousel, { - type UseEmblaCarouselType, + type UseEmblaCarouselType } from 'embla-carousel-react'; import { ButtonHTMLAttributes, @@ -17,7 +17,7 @@ import { useCallback, useContext, useEffect, - useState, + useState } from 'react'; import { cn } from '../../../../lib/tailwind.utils'; @@ -69,14 +69,14 @@ export const Carousel = forwardRef< children, ...props }, - ref, + ref ) => { const [carouselRef, api] = useEmblaCarousel( { ...opts, - axis: orientation === 'horizontal' ? 'x' : 'y', + axis: orientation === 'horizontal' ? 'x' : 'y' }, - plugins, + plugins ); const [canScrollPrev, setCanScrollPrev] = useState(false); @@ -117,7 +117,7 @@ export const Carousel = forwardRef< scrollNext(); } }, - [scrollPrev, scrollNext], + [scrollPrev, scrollNext] ); useEffect(() => { @@ -155,7 +155,7 @@ export const Carousel = forwardRef< scrollPrev, scrollNext, canScrollPrev, - canScrollNext, + canScrollNext }} >
); - }, + } ); Carousel.displayName = 'Carousel'; @@ -213,7 +213,7 @@ const CarouselButton = forwardRef< ref={ref} className={cn( 'absolute h-12 w-12 rounded-full bg-zinc-800 hover:bg-zinc-700 hover:scale-110 transition-all duration-200 ease-in-out cursor-pointer', - className, + className )} {...props} /> @@ -235,7 +235,7 @@ export const CarouselPrevious = forwardRef< orientation === 'horizontal' ? '-left-6 top-1/2 -translate-y-1/2' : '-top-12 left-1/2 -translate-x-1/2 rotate-90', - className, + className )} onClick={scrollPrev} {...props} @@ -260,7 +260,7 @@ export const CarouselNext = forwardRef< orientation === 'horizontal' ? '-right-6 top-1/2 -translate-y-1/2' : 'bottom-12 left-1/2 -translate-x-1/2 rotate-90', - className, + className )} onClick={scrollNext} {...props} @@ -281,7 +281,7 @@ const CarouselButtonSmall = forwardRef< ref={ref} className={cn( 'absolute h-10 w-10 rounded-full bg-zinc-900 hover:bg-zinc-800 transition-all duration-200 ease-in-out cursor-pointer', - className, + className )} {...props} /> @@ -308,7 +308,7 @@ export const CarouselPreviousSmall = forwardRef< ? '-left-5 top-1/2 -translate-y-1/2' : '-top-12 left-1/2 -translate-x-1/2 rotate-90', 'shadow-[15px_0_20px_20px_rgb(24,24,27)]', - className, + className )} onClick={scrollPrev} {...props} @@ -338,7 +338,7 @@ export const CarouselNextSmall = forwardRef< ? '-right-5 top-1/2 -translate-y-1/2' : 'bottom-12 left-1/2 -translate-x-1/2 rotate-90', 'shadow-[-15px_0_20px_20px_rgb(24,24,27)]', - className, + className )} onClick={scrollNext} {...props} @@ -366,7 +366,7 @@ export const useDotButton = (): UseDotButtonType => { if (!api) return; api.scrollTo(index); }, - [api], + [api] ); const onInit = useCallback(() => { @@ -389,7 +389,7 @@ export const useDotButton = (): UseDotButtonType => { return { selectedIndex, scrollSnaps, - onDotButtonClick, + onDotButtonClick }; }; @@ -404,7 +404,7 @@ export const CarouselDots = () => { onClick={() => onDotButtonClick(index)} className={cn( 'h-2.5 w-2.5 rounded-full bg-zinc-800 hover:bg-zinc-700 transition-all duration-200 ease-in-out', - index === selectedIndex && 'bg-zinc-700', + index === selectedIndex && 'bg-zinc-700' )} /> ))} diff --git a/apps/frontend/src/modules/shared/components/client/Command.tsx b/apps/frontend/src/modules/shared/components/client/Command.tsx index 68fc65d2..b170787c 100644 --- a/apps/frontend/src/modules/shared/components/client/Command.tsx +++ b/apps/frontend/src/modules/shared/components/client/Command.tsx @@ -15,7 +15,7 @@ const Command = React.forwardRef< ref={ref} className={cn( 'flex h-full w-full min-w-72 flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', - className, + className )} {...props} /> @@ -40,7 +40,7 @@ const CommandInput = React.forwardRef< ref={ref} className={cn( 'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', - className, + className )} {...props} /> @@ -83,7 +83,7 @@ const CommandGroup = React.forwardRef< ref={ref} className={cn( 'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', - className, + className )} {...props} /> @@ -112,7 +112,7 @@ const CommandItem = React.forwardRef< ref={ref} className={cn( 'relative flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm outline-none hover:bg-blue-600 transition-colors duration-100 data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50', - className, + className )} {...props} /> @@ -128,7 +128,7 @@ const CommandShortcut = ({ @@ -145,5 +145,5 @@ export { CommandGroup, CommandItem, CommandShortcut, - CommandSeparator, + CommandSeparator }; diff --git a/apps/frontend/src/modules/shared/components/client/FormElements.tsx b/apps/frontend/src/modules/shared/components/client/FormElements.tsx index 5d453377..d77d2a43 100644 --- a/apps/frontend/src/modules/shared/components/client/FormElements.tsx +++ b/apps/frontend/src/modules/shared/components/client/FormElements.tsx @@ -32,7 +32,7 @@ export const Area = ({ tooltip, isLoading, className, - children, + children }: { label?: string; tooltip?: React.ReactNode; @@ -50,7 +50,7 @@ export const Area = ({
{children} @@ -181,7 +181,7 @@ export const Select = forwardRef< ? 'border-red-500 focus:outline-red-500' : 'border-zinc-500' } disabled:border-zinc-700 disabled:cursor-not-allowed disabled:text-zinc-500 p-2`, - props.className, + props.className )} /> )} @@ -238,7 +238,7 @@ export const Slider = forwardRef< ref={ref} className={cn( 'relative flex w-full touch-none select-none items-center', - className, + className )} {...props} > @@ -257,7 +257,7 @@ export const UploadButton = ({ isDisabled }: { isDisabled: boolean }) => { 'transform motion-reduce:transform-none transition duration-150 ease-in-back', isDisabled ? '' - : 'hover:scale-[115%] hover:rotate-6 active:scale-[85%] active:rotate-6', + : 'hover:scale-[115%] hover:rotate-6 active:scale-[85%] active:rotate-6' )} > @@ -98,7 +98,7 @@ export function SongSearchCombo({ size='sm' className={cn( 'mr-2 h-4 w-4', - value === '' ? 'opacity-100' : 'opacity-0', + value === '' ? 'opacity-100' : 'opacity-0' )} /> No sound @@ -117,7 +117,7 @@ export function SongSearchCombo({ size='sm' className={cn( 'mr-2 h-4 w-4', - value === currentSound ? 'opacity-100' : 'opacity-0', + value === currentSound ? 'opacity-100' : 'opacity-0' )} /> {currentSound diff --git a/apps/frontend/src/modules/song/components/client/SongSelector.tsx b/apps/frontend/src/modules/song/components/client/SongSelector.tsx index ed815d1f..e28aea59 100644 --- a/apps/frontend/src/modules/song/components/client/SongSelector.tsx +++ b/apps/frontend/src/modules/song/components/client/SongSelector.tsx @@ -16,30 +16,30 @@ export const SongSelector = () => { const file = acceptedFiles[0]; setFile(file); }, - [setFile], + [setFile] ); const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ - onDrop: handleFileDrop, + onDrop : handleFileDrop, onDropRejected: (fileRejections) => { const error = fileRejections[0].errors[0].code; if (error === 'file-invalid-type') { toast.error("Oops! This doesn't look like a valid NBS file.", { - position: 'bottom-center', + position: 'bottom-center' }); } else if (error === 'file-too-large') { toast.error('This file is too large! (Max size: 3 MB)', { - position: 'bottom-center', + position: 'bottom-center' }); } }, accept: { - 'application/nbs': ['.nbs'], + 'application/nbs': ['.nbs'] }, - maxSize: UPLOAD_CONSTANTS.file.maxSize, + maxSize : UPLOAD_CONSTANTS.file.maxSize, multiple: false, - noClick: true, + noClick : true }); return ( diff --git a/apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx b/apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx index c5a762a4..cad8a4c3 100644 --- a/apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx +++ b/apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx @@ -7,7 +7,7 @@ import { cn } from '@web/lib/tailwind.utils'; import { Tooltip, TooltipContent, - TooltipTrigger, + TooltipTrigger } from '@web/modules/shared/components/tooltip'; import { useSongProvider } from './context/Song.context'; @@ -23,7 +23,7 @@ function ThumbnailSliders({ formMethods, isLocked, maxTick, - maxLayer, + maxLayer }: { formMethods: UseFormReturn & UseFormReturn; isLocked: boolean; @@ -35,7 +35,7 @@ function ThumbnailSliders({ const [zoomLevel, startTick, startLayer] = formMethods.watch([ 'thumbnailData.zoomLevel', 'thumbnailData.startTick', - 'thumbnailData.startLayer', + 'thumbnailData.startLayer' ]); return ( @@ -49,7 +49,7 @@ function ThumbnailSliders({ id='zoom-level' className='w-full disabled:cursor-not-allowed' {...register('thumbnailData.zoomLevel', { - valueAsNumber: true, + valueAsNumber: true })} disabled={isLocked} min={THUMBNAIL_CONSTANTS.zoomLevel.min} @@ -67,7 +67,7 @@ function ThumbnailSliders({ className='w-full disabled:cursor-not-allowed' {...register('thumbnailData.startTick', { valueAsNumber: true, - max: maxTick, + max : maxTick })} disabled={isLocked} min={THUMBNAIL_CONSTANTS.startTick.default} @@ -85,7 +85,7 @@ function ThumbnailSliders({ className='w-full disabled:cursor-not-allowed' {...register('thumbnailData.startLayer', { valueAsNumber: true, - max: maxLayer, + max : maxLayer })} disabled={isLocked} min={THUMBNAIL_CONSTANTS.startLayer.default} @@ -102,7 +102,7 @@ const ColorButton = ({ tooltip, active, onClick, - disabled, + disabled }: { color: string; tooltip: string; @@ -117,7 +117,7 @@ const ColorButton = ({ type='button' className={cn( 'w-6 h-6 rounded-full flex-none border-2 border-zinc-200 border-opacity-30 disabled:opacity-30', - active && 'outline outline-2 outline-zinc-200', + active && 'outline outline-2 outline-zinc-200' )} style={{ backgroundColor: color }} disabled={disabled} @@ -130,7 +130,7 @@ const ColorButton = ({ export const SongThumbnailInput = ({ type, - isLocked, + isLocked }: { type: 'upload' | 'edit'; isLocked: boolean; diff --git a/apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx b/apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx index 77fc2ee9..093376f2 100644 --- a/apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx +++ b/apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx @@ -12,7 +12,7 @@ type ThumbnailRendererCanvasProps = { export const ThumbnailRendererCanvas = ({ notes, - formMethods, + formMethods }: ThumbnailRendererCanvasProps) => { const canvasRef = useRef(null); const drawRequest = useRef(null); @@ -23,8 +23,8 @@ export const ThumbnailRendererCanvas = ({ 'thumbnailData.zoomLevel', 'thumbnailData.startTick', 'thumbnailData.startLayer', - 'thumbnailData.backgroundColor', - ], + 'thumbnailData.backgroundColor' + ] ); useEffect(() => { @@ -60,10 +60,10 @@ export const ThumbnailRendererCanvas = ({ startLayer, zoomLevel, backgroundColor, - canvasWidth: canvas.width, + canvasWidth : canvas.width, canvasHeight: canvas.height, - imgWidth: 1280, - imgHeight: 768, + imgWidth : 1280, + imgHeight : 768 }); swap(output, canvas); diff --git a/apps/frontend/src/modules/song/components/client/context/Song.context.tsx b/apps/frontend/src/modules/song/components/client/context/Song.context.tsx index 258ab120..4f43eb34 100644 --- a/apps/frontend/src/modules/song/components/client/context/Song.context.tsx +++ b/apps/frontend/src/modules/song/components/client/context/Song.context.tsx @@ -5,12 +5,12 @@ import { useContext } from 'react'; import { EditSongContext, EditSongProvider, - useEditSongProviderType, + useEditSongProviderType } from '@web/modules/song-edit/components/client/context/EditSong.context'; import { UploadSongContext, UploadSongProvider, - useUploadSongProviderType, + useUploadSongProviderType } from '@web/modules/song-upload/components/client/context/UploadSong.context'; export const SongProvider = ({ children }: { children: React.ReactNode }) => { @@ -24,7 +24,7 @@ export const SongProvider = ({ children }: { children: React.ReactNode }) => { type ContextType = 'upload' | 'edit'; export const useSongProvider = ( - type: ContextType, + type: ContextType ): useUploadSongProviderType & useEditSongProviderType => { const uploadContext = useContext(UploadSongContext); const editContext = useContext(EditSongContext); diff --git a/apps/frontend/src/modules/song/util/downloadSong.ts b/apps/frontend/src/modules/song/util/downloadSong.ts index da241576..5badbd48 100644 --- a/apps/frontend/src/modules/song/util/downloadSong.ts +++ b/apps/frontend/src/modules/song/util/downloadSong.ts @@ -14,13 +14,13 @@ export const downloadSongFile = async (song: { axios .get(`/song/${song.publicId}/download`, { params: { - src: 'downloadButton', + src: 'downloadButton' }, headers: { - authorization: `Bearer ${token}`, + authorization: `Bearer ${token}` }, - responseType: 'blob', - withCredentials: true, + responseType : 'blob', + withCredentials: true }) .then((res) => { const url = window.URL.createObjectURL(res.data); @@ -41,8 +41,8 @@ export const openSongInNBS = async (song: { publicId: string }) => { axios .get(`/song/${song.publicId}/open`, { headers: { - src: 'downloadButton', - }, + src: 'downloadButton' + } }) .then((response) => { const responseUrl = response.data; diff --git a/apps/frontend/src/modules/user/components/UserProfile.tsx b/apps/frontend/src/modules/user/components/UserProfile.tsx index 4ac515af..3f0f5708 100644 --- a/apps/frontend/src/modules/user/components/UserProfile.tsx +++ b/apps/frontend/src/modules/user/components/UserProfile.tsx @@ -14,7 +14,7 @@ const UserProfile = ({ userData }: UserProfileProps) => { publicName, description, profileImage, - socialLinks, + socialLinks } = userData; return ( diff --git a/apps/frontend/src/modules/user/features/song.util.ts b/apps/frontend/src/modules/user/features/song.util.ts index de418f61..8552abd9 100644 --- a/apps/frontend/src/modules/user/features/song.util.ts +++ b/apps/frontend/src/modules/user/features/song.util.ts @@ -3,8 +3,8 @@ import axiosInstance from '@web/lib/axios'; export const getUserSongs = async (userId: string) => { const res = await axiosInstance.get('/song/user', { params: { - id: userId, - }, + id: userId + } }); return res.data; diff --git a/apps/frontend/src/modules/user/features/user.util.ts b/apps/frontend/src/modules/user/features/user.util.ts index 70e8dd00..cfd083d9 100644 --- a/apps/frontend/src/modules/user/features/user.util.ts +++ b/apps/frontend/src/modules/user/features/user.util.ts @@ -2,7 +2,7 @@ import axiosInstance from '../../../lib/axios'; import { UserProfileData } from '../../auth/types/User'; export const getUserProfileData = async ( - id: string, + id: string ): Promise => { try { const res = await axiosInstance.get(`/user/?id=${id}`); diff --git a/packages/configs/src/colors.ts b/packages/configs/src/colors.ts index 188d9a85..1cdab6f5 100644 --- a/packages/configs/src/colors.ts +++ b/packages/configs/src/colors.ts @@ -26,113 +26,113 @@ import colors from 'tailwindcss/colors'; export const BG_COLORS = { red: { - key: 'red', - name: 'Red', + key : 'red', + name : 'Red', light: colors.red[400], - dark: colors.red[900], + dark : colors.red[900] }, orange: { - key: 'orange', - name: 'Orange', + key : 'orange', + name : 'Orange', light: colors.orange[400], - dark: colors.orange[900], + dark : colors.orange[900] }, amber: { - key: 'amber', - name: 'Amber', + key : 'amber', + name : 'Amber', light: colors.amber[400], - dark: colors.amber[900], + dark : colors.amber[900] }, yellow: { - key: 'yellow', - name: 'Yellow', + key : 'yellow', + name : 'Yellow', light: colors.yellow[400], - dark: colors.yellow[900], + dark : colors.yellow[900] }, lime: { - key: 'lime', - name: 'Lime', + key : 'lime', + name : 'Lime', light: colors.lime[400], - dark: colors.lime[900], + dark : colors.lime[900] }, green: { - key: 'green', - name: 'Green', + key : 'green', + name : 'Green', light: colors.green[400], - dark: colors.green[900], + dark : colors.green[900] }, emerald: { - key: 'emerald', - name: 'Emerald', + key : 'emerald', + name : 'Emerald', light: colors.emerald[400], - dark: colors.emerald[900], + dark : colors.emerald[900] }, teal: { - key: 'teal', - name: 'Teal', + key : 'teal', + name : 'Teal', light: colors.teal[400], - dark: colors.teal[900], + dark : colors.teal[900] }, cyan: { - key: 'cyan', - name: 'Cyan', + key : 'cyan', + name : 'Cyan', light: colors.cyan[400], - dark: colors.cyan[900], + dark : colors.cyan[900] }, sky: { - key: 'sky', - name: 'Sky', + key : 'sky', + name : 'Sky', light: colors.sky[400], - dark: colors.sky[900], + dark : colors.sky[900] }, blue: { - key: 'blue', - name: 'Blue', + key : 'blue', + name : 'Blue', light: colors.blue[400], - dark: colors.blue[900], + dark : colors.blue[900] }, indigo: { - key: 'indigo', - name: 'Indigo', + key : 'indigo', + name : 'Indigo', light: colors.indigo[400], - dark: colors.indigo[900], + dark : colors.indigo[900] }, violet: { - key: 'violet', - name: 'Violet', + key : 'violet', + name : 'Violet', light: colors.violet[400], - dark: colors.violet[900], + dark : colors.violet[900] }, purple: { - key: 'purple', - name: 'Purple', + key : 'purple', + name : 'Purple', light: colors.purple[400], - dark: colors.purple[900], + dark : colors.purple[900] }, fuchsia: { - key: 'fuchsia', - name: 'Fuchsia', + key : 'fuchsia', + name : 'Fuchsia', light: colors.fuchsia[400], - dark: colors.fuchsia[900], + dark : colors.fuchsia[900] }, pink: { - key: 'pink', - name: 'Pink', + key : 'pink', + name : 'Pink', light: colors.pink[400], - dark: colors.pink[900], + dark : colors.pink[900] }, rose: { - key: 'rose', - name: 'Rose', + key : 'rose', + name : 'Rose', light: colors.rose[400], - dark: colors.rose[900], + dark : colors.rose[900] }, gray: { - key: 'gray', - name: 'Gray', + key : 'gray', + name : 'Gray', light: colors.zinc[200], - dark: colors.zinc[800], - }, + dark : colors.zinc[800] + } } as const; export const bgColorsArray = Object.values(BG_COLORS); diff --git a/packages/configs/src/song.ts b/packages/configs/src/song.ts index 1fbe40d6..68cc327f 100644 --- a/packages/configs/src/song.ts +++ b/packages/configs/src/song.ts @@ -2,113 +2,113 @@ import { BG_COLORS } from './colors'; export const THUMBNAIL_CONSTANTS = { zoomLevel: { - min: 1, - max: 5, - default: 3, + min : 1, + max : 5, + default: 3 }, startTick: { - default: 0, + default: 0 }, startLayer: { - default: 0, + default: 0 }, backgroundColor: { - default: BG_COLORS.gray.dark, - }, + default: BG_COLORS.gray.dark + } } as const; export const MIMETYPE_NBS = 'application/octet-stream' as const; export const UPLOAD_CONSTANTS = { file: { - maxSize: 1024 * 1024 * 3, // 3 MB + maxSize: 1024 * 1024 * 3 // 3 MB }, title: { minLength: 3, - maxLength: 100, + maxLength: 100 }, description: { - maxLength: 1000, + maxLength: 1000 }, originalAuthor: { - maxLength: 50, + maxLength: 50 }, category: { - default: 'none', + default: 'none' }, license: { - default: 'none', + default: 'none' }, customInstruments: { - maxCount: 240, + maxCount: 240 }, categories: { - none: 'No category', - rock: 'Rock', - pop: 'Pop', - jazz: 'Jazz', - blues: 'Blues', - country: 'Country', - metal: 'Metal', - hiphop: 'Hip-Hop', - rap: 'Rap', - reggae: 'Reggae', - classical: 'Classical', - electronic: 'Electronic', - dance: 'Dance', - rnb: 'R&B', - soul: 'Soul', - funk: 'Funk', - punk: 'Punk', - alternative: 'Alternative', - indie: 'Indie', - folk: 'Folk', - latin: 'Latin', - world: 'World', - other: 'Other', - vocaloid: 'Vocaloid', - soundtrack: 'Soundtrack', + none : 'No category', + rock : 'Rock', + pop : 'Pop', + jazz : 'Jazz', + blues : 'Blues', + country : 'Country', + metal : 'Metal', + hiphop : 'Hip-Hop', + rap : 'Rap', + reggae : 'Reggae', + classical : 'Classical', + electronic : 'Electronic', + dance : 'Dance', + rnb : 'R&B', + soul : 'Soul', + funk : 'Funk', + punk : 'Punk', + alternative : 'Alternative', + indie : 'Indie', + folk : 'Folk', + latin : 'Latin', + world : 'World', + other : 'Other', + vocaloid : 'Vocaloid', + soundtrack : 'Soundtrack', instrumental: 'Instrumental', - ambient: 'Ambient', - gaming: 'Gaming', - anime: 'Anime', - movies_tv: 'Movies & TV', - chiptune: 'Chiptune', - lofi: 'Lofi', - kpop: 'K-pop', - jpop: 'J-pop', + ambient : 'Ambient', + gaming : 'Gaming', + anime : 'Anime', + movies_tv : 'Movies & TV', + chiptune : 'Chiptune', + lofi : 'Lofi', + kpop : 'K-pop', + jpop : 'J-pop' }, licenses: { standard: { - name: 'Standard License', + name : 'Standard License', shortName: 'Standard License', description: "The author reserves all rights. You may not use this song without the author's permission.", uploadDescription: - 'You allow us to distribute your song on Note Block World. Other users can listen to it, but they cannot use the song without your permission.', + 'You allow us to distribute your song on Note Block World. Other users can listen to it, but they cannot use the song without your permission.' }, cc_by_sa: { - name: 'Creative Commons - Attribution-ShareAlike 4.0', + name : 'Creative Commons - Attribution-ShareAlike 4.0', shortName: 'CC BY-SA 4.0', description: 'You can copy, modify, and distribute this song, even for commercial purposes, as long as you credit the author and provide a link to the song. If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.\n\nFor more information, please visit the [Creative Commons website](https://creativecommons.org/licenses/by-sa/4.0/).', uploadDescription: - 'Anyone can copy, modify, remix, adapt and distribute this song, even for commercial purposes, as long as attribution is provided and the modifications are distributed under the same license.\nFor more information, visit the [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) website.', - }, + 'Anyone can copy, modify, remix, adapt and distribute this song, even for commercial purposes, as long as attribution is provided and the modifications are distributed under the same license.\nFor more information, visit the [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) website.' + } }, visibility: { - public: 'Public', - private: 'Private', - }, + public : 'Public', + private: 'Private' + } } as const; export const TIMESPANS = [ @@ -117,15 +117,15 @@ export const TIMESPANS = [ 'week', 'month', 'year', - 'all', + 'all' ] as const; export const MY_SONGS = { PAGE_SIZE: 10, - SORT: 'createdAt', + SORT : 'createdAt' } as const; export const BROWSER_SONGS = { - featuredPageSize: 10, - paddedFeaturedPageSize: 5, + featuredPageSize : 10, + paddedFeaturedPageSize: 5 } as const; diff --git a/packages/configs/src/user.ts b/packages/configs/src/user.ts index 4a0e7f37..9daf496e 100644 --- a/packages/configs/src/user.ts +++ b/packages/configs/src/user.ts @@ -1,5 +1,5 @@ export const USER_CONSTANTS = { USERNAME_MIN_LENGTH: 3, USERNAME_MAX_LENGTH: 32, - ALLOWED_REGEXP: /^[a-zA-Z0-9-_.]*$/, + ALLOWED_REGEXP : /^[a-zA-Z0-9-_.]*$/ } as const; diff --git a/packages/database/src/common/dto/PageQuery.dto.ts b/packages/database/src/common/dto/PageQuery.dto.ts index 76dbd225..66963c4c 100644 --- a/packages/database/src/common/dto/PageQuery.dto.ts +++ b/packages/database/src/common/dto/PageQuery.dto.ts @@ -9,7 +9,7 @@ import { IsOptional, IsString, Max, - Min, + Min } from 'class-validator'; import type { TimespanType } from '@database/song/dto/types'; @@ -17,31 +17,31 @@ import type { TimespanType } from '@database/song/dto/types'; export class PageQueryDTO { @Min(1) @ApiProperty({ - example: 1, - description: 'page', + example : 1, + description: 'page' }) page?: number = 1; @IsNotEmpty() @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, + allowNaN : false, + allowInfinity : false, + maxDecimalPlaces: 0 }) @Min(1) @Max(100) @ApiProperty({ - example: 20, - description: 'limit', + example : 20, + description: 'limit' }) limit?: number; @IsString() @IsOptional() @ApiProperty({ - example: 'field', + example : 'field', description: 'Sorts the results by the specified field.', - required: false, + required : false }) sort?: string = 'createdAt'; @@ -51,16 +51,16 @@ export class PageQueryDTO { example: false, description: 'Sorts the results in ascending order if true; in descending order if false.', - required: false, + required: false }) order?: boolean = false; @IsEnum(TIMESPANS) @IsOptional() @ApiProperty({ - example: 'hour', + example : 'hour', description: 'Filters the results by the specified timespan.', - required: false, + required : false }) timespan?: TimespanType; diff --git a/packages/database/src/song/dto/FeaturedSongsDto.dto.ts b/packages/database/src/song/dto/FeaturedSongsDto.dto.ts index 65d6eff7..e9b8c221 100644 --- a/packages/database/src/song/dto/FeaturedSongsDto.dto.ts +++ b/packages/database/src/song/dto/FeaturedSongsDto.dto.ts @@ -10,12 +10,12 @@ export class FeaturedSongsDto { public static create(): FeaturedSongsDto { return { - hour: [], - day: [], - week: [], + hour : [], + day : [], + week : [], month: [], - year: [], - all: [], + year : [], + all : [] }; } } diff --git a/packages/database/src/song/dto/SongPage.dto.ts b/packages/database/src/song/dto/SongPage.dto.ts index 4e1e0a70..c83af5d9 100644 --- a/packages/database/src/song/dto/SongPage.dto.ts +++ b/packages/database/src/song/dto/SongPage.dto.ts @@ -10,25 +10,25 @@ export class SongPageDto { @IsNotEmpty() @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, + allowNaN : false, + allowInfinity : false, + maxDecimalPlaces: 0 }) page: number; @IsNotEmpty() @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, + allowNaN : false, + allowInfinity : false, + maxDecimalPlaces: 0 }) limit: number; @IsNotEmpty() @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, + allowNaN : false, + allowInfinity : false, + maxDecimalPlaces: 0 }) total: number; } diff --git a/packages/database/src/song/dto/SongPreview.dto.ts b/packages/database/src/song/dto/SongPreview.dto.ts index 97acff3c..6eb8a056 100644 --- a/packages/database/src/song/dto/SongPreview.dto.ts +++ b/packages/database/src/song/dto/SongPreview.dto.ts @@ -58,18 +58,18 @@ export class SongPreviewDto { public static fromSongDocumentWithUser(song: SongWithUser): SongPreviewDto { return new SongPreviewDto({ - publicId: song.publicId, - uploader: song.uploader, - title: song.title, - description: song.description, + publicId : song.publicId, + uploader : song.uploader, + title : song.title, + description : song.description, originalAuthor: song.originalAuthor, - duration: song.stats.duration, - noteCount: song.stats.noteCount, - thumbnailUrl: song.thumbnailUrl, - createdAt: song.createdAt, - updatedAt: song.updatedAt, - playCount: song.playCount, - visibility: song.visibility, + duration : song.stats.duration, + noteCount : song.stats.noteCount, + thumbnailUrl : song.thumbnailUrl, + createdAt : song.createdAt, + updatedAt : song.updatedAt, + playCount : song.playCount, + visibility : song.visibility }); } } diff --git a/packages/database/src/song/dto/SongStats.ts b/packages/database/src/song/dto/SongStats.ts index 49cb712b..db904fa7 100644 --- a/packages/database/src/song/dto/SongStats.ts +++ b/packages/database/src/song/dto/SongStats.ts @@ -3,7 +3,7 @@ import { IsInt, IsNumber, IsString, - ValidateIf, + ValidateIf } from 'class-validator'; export class SongStats { diff --git a/packages/database/src/song/dto/SongView.dto.ts b/packages/database/src/song/dto/SongView.dto.ts index b7b5ed6f..0349468d 100644 --- a/packages/database/src/song/dto/SongView.dto.ts +++ b/packages/database/src/song/dto/SongView.dto.ts @@ -4,7 +4,7 @@ import { IsNotEmpty, IsNumber, IsString, - IsUrl, + IsUrl } from 'class-validator'; import { SongStats } from '@database/song/dto/SongStats'; @@ -82,23 +82,23 @@ export class SongViewDto { public static fromSongDocument(song: SongDocument): SongViewDto { return new SongViewDto({ - publicId: song.publicId, - createdAt: song.createdAt, - uploader: song.uploader as unknown as SongViewUploader, - thumbnailUrl: song.thumbnailUrl, - playCount: song.playCount, - downloadCount: song.downloadCount, - likeCount: song.likeCount, - allowDownload: song.allowDownload, - title: song.title, - originalAuthor: song.originalAuthor, - description: song.description, - category: song.category, - visibility: song.visibility, - license: song.license, + publicId : song.publicId, + createdAt : song.createdAt, + uploader : song.uploader as unknown as SongViewUploader, + thumbnailUrl : song.thumbnailUrl, + playCount : song.playCount, + downloadCount : song.downloadCount, + likeCount : song.likeCount, + allowDownload : song.allowDownload, + title : song.title, + originalAuthor : song.originalAuthor, + description : song.description, + category : song.category, + visibility : song.visibility, + license : song.license, customInstruments: song.customInstruments, - fileSize: song.fileSize, - stats: song.stats, + fileSize : song.fileSize, + stats : song.stats }); } diff --git a/packages/database/src/song/dto/ThumbnailData.dto.ts b/packages/database/src/song/dto/ThumbnailData.dto.ts index 09456c14..82f11669 100644 --- a/packages/database/src/song/dto/ThumbnailData.dto.ts +++ b/packages/database/src/song/dto/ThumbnailData.dto.ts @@ -9,7 +9,7 @@ export class ThumbnailData { @IsInt() @ApiProperty({ description: 'Zoom level of the cover image', - example: THUMBNAIL_CONSTANTS.zoomLevel.default, + example : THUMBNAIL_CONSTANTS.zoomLevel.default }) zoomLevel: number; @@ -18,7 +18,7 @@ export class ThumbnailData { @IsInt() @ApiProperty({ description: 'X position of the cover image', - example: THUMBNAIL_CONSTANTS.startTick.default, + example : THUMBNAIL_CONSTANTS.startTick.default }) startTick: number; @@ -26,7 +26,7 @@ export class ThumbnailData { @Min(0) @ApiProperty({ description: 'Y position of the cover image', - example: THUMBNAIL_CONSTANTS.startLayer.default, + example : THUMBNAIL_CONSTANTS.startLayer.default }) startLayer: number; @@ -34,16 +34,16 @@ export class ThumbnailData { @IsHexColor() @ApiProperty({ description: 'Background color of the cover image', - example: THUMBNAIL_CONSTANTS.backgroundColor.default, + example : THUMBNAIL_CONSTANTS.backgroundColor.default }) backgroundColor: string; static getApiExample(): ThumbnailData { return { - zoomLevel: 3, - startTick: 0, - startLayer: 0, - backgroundColor: '#F0F0F0', + zoomLevel : 3, + startTick : 0, + startLayer : 0, + backgroundColor: '#F0F0F0' }; } } diff --git a/packages/database/src/song/dto/UploadSongDto.dto.ts b/packages/database/src/song/dto/UploadSongDto.dto.ts index dfe47049..59038a13 100644 --- a/packages/database/src/song/dto/UploadSongDto.dto.ts +++ b/packages/database/src/song/dto/UploadSongDto.dto.ts @@ -8,7 +8,7 @@ import { IsNotEmpty, IsString, MaxLength, - ValidateNested, + ValidateNested } from 'class-validator'; import type { SongDocument } from '@database/song/entity/song.entity'; @@ -31,7 +31,7 @@ export class UploadSongDto { description: 'The file to upload', // @ts-ignore //TODO: fix this - type: 'file', + type: 'file' }) file: any; //TODO: Express.Multer.File; @@ -39,9 +39,9 @@ export class UploadSongDto { @IsBoolean() @Type(() => Boolean) @ApiProperty({ - default: true, + default : true, description: 'Whether the song can be downloaded by other users', - example: true, + example : true }) allowDownload: boolean; @@ -49,10 +49,10 @@ export class UploadSongDto { @IsString() @IsIn(visibility) @ApiProperty({ - enum: visibility, - default: visibility[0], + enum : visibility, + default : visibility[0], description: 'The visibility of the song', - example: visibility[0], + example : visibility[0] }) visibility: VisibilityType; @@ -61,7 +61,7 @@ export class UploadSongDto { @MaxLength(UPLOAD_CONSTANTS.title.maxLength) @ApiProperty({ description: 'Title of the song', - example: 'My Song', + example : 'My Song' }) title: string; @@ -69,7 +69,7 @@ export class UploadSongDto { @MaxLength(UPLOAD_CONSTANTS.originalAuthor.maxLength) @ApiProperty({ description: 'Original author of the song', - example: 'Myself', + example : 'Myself' }) originalAuthor: string; @@ -77,7 +77,7 @@ export class UploadSongDto { @MaxLength(UPLOAD_CONSTANTS.description.maxLength) @ApiProperty({ description: 'Description of the song', - example: 'This is my song', + example : 'This is my song' }) description: string; @@ -85,9 +85,9 @@ export class UploadSongDto { @IsString() @IsIn(categories) @ApiProperty({ - enum: categories, + enum : categories, description: 'Category of the song', - example: categories[0], + example : categories[0] }) category: CategoryType; @@ -97,7 +97,7 @@ export class UploadSongDto { @Transform(({ value }) => JSON.parse(value)) @ApiProperty({ description: 'Thumbnail data of the song', - example: ThumbnailData.getApiExample(), + example : ThumbnailData.getApiExample() }) thumbnailData: ThumbnailData; @@ -105,10 +105,10 @@ export class UploadSongDto { @IsString() @IsIn(licenses) @ApiProperty({ - enum: licenses, - default: licenses[0], + enum : licenses, + default : licenses[0], description: 'The visibility of the song', - example: licenses[0], + example : licenses[0] }) license: LicenseType; @@ -116,7 +116,7 @@ export class UploadSongDto { @MaxLength(UPLOAD_CONSTANTS.customInstruments.maxCount, { each: true }) @ApiProperty({ description: - 'List of custom instrument paths, one for each custom instrument in the song, relative to the assets/minecraft/sounds folder', + 'List of custom instrument paths, one for each custom instrument in the song, relative to the assets/minecraft/sounds folder' }) @Transform(({ value }) => JSON.parse(value)) customInstruments: string[]; @@ -127,15 +127,15 @@ export class UploadSongDto { public static fromSongDocument(song: SongDocument): UploadSongDto { return new UploadSongDto({ - allowDownload: song.allowDownload, - visibility: song.visibility, - title: song.title, - originalAuthor: song.originalAuthor, - description: song.description, - category: song.category, - thumbnailData: song.thumbnailData, - license: song.license, - customInstruments: song.customInstruments ?? [], + allowDownload : song.allowDownload, + visibility : song.visibility, + title : song.title, + originalAuthor : song.originalAuthor, + description : song.description, + category : song.category, + thumbnailData : song.thumbnailData, + license : song.license, + customInstruments: song.customInstruments ?? [] }); } } diff --git a/packages/database/src/song/dto/UploadSongResponseDto.dto.ts b/packages/database/src/song/dto/UploadSongResponseDto.dto.ts index 2fedb3e1..f892e200 100644 --- a/packages/database/src/song/dto/UploadSongResponseDto.dto.ts +++ b/packages/database/src/song/dto/UploadSongResponseDto.dto.ts @@ -4,7 +4,7 @@ import { IsNotEmpty, IsString, MaxLength, - ValidateNested, + ValidateNested } from 'class-validator'; import type { SongWithUser } from '@database/song/entity/song.entity'; @@ -17,7 +17,7 @@ export class UploadSongResponseDto { @IsNotEmpty() @ApiProperty({ description: 'ID of the song', - example: '1234567890abcdef12345678', + example : '1234567890abcdef12345678' }) publicId: string; @@ -26,7 +26,7 @@ export class UploadSongResponseDto { @MaxLength(128) @ApiProperty({ description: 'Title of the song', - example: 'My Song', + example : 'My Song' }) title: string; @@ -34,7 +34,7 @@ export class UploadSongResponseDto { @MaxLength(64) @ApiProperty({ description: 'Original author of the song', - example: 'Myself', + example : 'Myself' }) uploader: SongViewDto.SongViewUploader; @@ -44,7 +44,7 @@ export class UploadSongResponseDto { @Transform(({ value }) => JSON.parse(value)) @ApiProperty({ description: 'Thumbnail data of the song', - example: ThumbnailData.getApiExample(), + example : ThumbnailData.getApiExample() }) thumbnailUrl: string; @@ -59,15 +59,15 @@ export class UploadSongResponseDto { } public static fromSongWithUserDocument( - song: SongWithUser, + song: SongWithUser ): UploadSongResponseDto { return new UploadSongResponseDto({ - publicId: song.publicId, - title: song.title, - uploader: song.uploader, - duration: song.stats.duration, + publicId : song.publicId, + title : song.title, + uploader : song.uploader, + duration : song.stats.duration, thumbnailUrl: song.thumbnailUrl, - noteCount: song.stats.noteCount, + noteCount : song.stats.noteCount }); } } diff --git a/packages/database/src/song/entity/song.entity.ts b/packages/database/src/song/entity/song.entity.ts index 8f099e2d..5ee97e94 100644 --- a/packages/database/src/song/entity/song.entity.ts +++ b/packages/database/src/song/entity/song.entity.ts @@ -12,12 +12,12 @@ import type { CategoryType, LicenseType, VisibilityType } from '../dto/types'; @Schema({ timestamps: true, versionKey: false, - toJSON: { - virtuals: true, + toJSON : { + virtuals : true, transform: (doc, ret) => { delete ret._id; - }, - }, + } + } }) export class Song { @Prop({ type: String, required: true, unique: true }) diff --git a/packages/database/src/user/dto/CreateUser.dto.ts b/packages/database/src/user/dto/CreateUser.dto.ts index ec6ca8f8..9a590c29 100644 --- a/packages/database/src/user/dto/CreateUser.dto.ts +++ b/packages/database/src/user/dto/CreateUser.dto.ts @@ -4,7 +4,7 @@ import { IsNotEmpty, IsString, IsUrl, - MaxLength, + MaxLength } from 'class-validator'; export class CreateUser { @@ -14,7 +14,7 @@ export class CreateUser { @IsEmail() @ApiProperty({ description: 'Email of the user', - example: 'vycasnicolas@gmailcom', + example : 'vycasnicolas@gmailcom' }) email: string; @@ -23,7 +23,7 @@ export class CreateUser { @MaxLength(64) @ApiProperty({ description: 'Username of the user', - example: 'tomast1137', + example : 'tomast1137' }) username: string; @@ -31,7 +31,7 @@ export class CreateUser { @IsUrl() @ApiProperty({ description: 'Profile image of the user', - example: 'https://example.com/image.png', + example : 'https://example.com/image.png' }) profileImage: string; diff --git a/packages/database/src/user/dto/GetUser.dto.ts b/packages/database/src/user/dto/GetUser.dto.ts index 3feb46a3..7ff170db 100644 --- a/packages/database/src/user/dto/GetUser.dto.ts +++ b/packages/database/src/user/dto/GetUser.dto.ts @@ -5,7 +5,7 @@ import { IsOptional, IsString, MaxLength, - MinLength, + MinLength } from 'class-validator'; export class GetUser { @@ -15,7 +15,7 @@ export class GetUser { @IsEmail() @ApiProperty({ description: 'Email of the user', - example: 'vycasnicolas@gmailcom', + example : 'vycasnicolas@gmailcom' }) email?: string; @@ -24,7 +24,7 @@ export class GetUser { @MaxLength(64) @ApiProperty({ description: 'Username of the user', - example: 'tomast1137', + example : 'tomast1137' }) username?: string; @@ -35,7 +35,7 @@ export class GetUser { @IsMongoId() @ApiProperty({ description: 'ID of the user', - example: 'replace0me6b5f0a8c1a6d8c', + example : 'replace0me6b5f0a8c1a6d8c' }) id?: string; diff --git a/packages/database/src/user/dto/NewEmailUser.dto.ts b/packages/database/src/user/dto/NewEmailUser.dto.ts index 33be8301..11ecbb22 100644 --- a/packages/database/src/user/dto/NewEmailUser.dto.ts +++ b/packages/database/src/user/dto/NewEmailUser.dto.ts @@ -4,13 +4,13 @@ import { IsNotEmpty, IsString, MaxLength, - MinLength, + MinLength } from 'class-validator'; export class NewEmailUserDto { @ApiProperty({ description: 'User name', - example: 'Tomast1337', + example : 'Tomast1337' }) @IsString() @IsNotEmpty() @@ -20,7 +20,7 @@ export class NewEmailUserDto { @ApiProperty({ description: 'User email', - example: 'vycasnicolas@gmail.com', + example : 'vycasnicolas@gmail.com' }) @IsString() @IsNotEmpty() diff --git a/packages/database/src/user/dto/UpdateUsername.dto.ts b/packages/database/src/user/dto/UpdateUsername.dto.ts index c13cfb28..622824b1 100644 --- a/packages/database/src/user/dto/UpdateUsername.dto.ts +++ b/packages/database/src/user/dto/UpdateUsername.dto.ts @@ -9,7 +9,7 @@ export class UpdateUsernameDto { @Matches(USER_CONSTANTS.ALLOWED_REGEXP) @ApiProperty({ description: 'Username of the user', - example: 'tomast1137', + example : 'tomast1137' }) username: string; } diff --git a/packages/database/src/user/dto/user.dto.ts b/packages/database/src/user/dto/user.dto.ts index a611c20f..4d6596c7 100644 --- a/packages/database/src/user/dto/user.dto.ts +++ b/packages/database/src/user/dto/user.dto.ts @@ -6,9 +6,9 @@ export class UserDto { email: string; static fromEntity(user: User): UserDto { const userDto: UserDto = { - username: user.username, + username : user.username, publicName: user.publicName, - email: user.email, + email : user.email }; return userDto; diff --git a/packages/database/src/user/entity/user.entity.ts b/packages/database/src/user/entity/user.entity.ts index e480e133..19a19c7d 100644 --- a/packages/database/src/user/entity/user.entity.ts +++ b/packages/database/src/user/entity/user.entity.ts @@ -24,13 +24,13 @@ class SocialLinks { @Schema({ timestamps: true, - toJSON: { - virtuals: true, + toJSON : { + virtuals : true, transform: (doc, ret) => { delete ret._id; delete ret.__v; - }, - }, + } + } }) export class User { @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) @@ -73,9 +73,9 @@ export class User { description: string; @Prop({ - type: String, + type : String, required: true, - default: '/img/note-block-pfp.jpg', + default : '/img/note-block-pfp.jpg' }) profileImage: string; diff --git a/packages/song/src/injectMetadata.ts b/packages/song/src/injectMetadata.ts index 0b755a8d..bec26cf2 100644 --- a/packages/song/src/injectMetadata.ts +++ b/packages/song/src/injectMetadata.ts @@ -7,7 +7,7 @@ export function injectSongFileMetadata( author: string, originalAuthor: string, description: string, - soundPaths: string[], + soundPaths: string[] ) { if (description != '') description += '\n\n'; description += 'Uploaded to Note Block World'; diff --git a/packages/song/src/notes.ts b/packages/song/src/notes.ts index 757e0da7..d5a0e10f 100644 --- a/packages/song/src/notes.ts +++ b/packages/song/src/notes.ts @@ -25,11 +25,11 @@ export class NoteQuadTree { const tick = parseInt(tickStr); const treeItem = new Rectangle({ - x: tick, - y: layerId, - width: 1, + x : tick, + y : layerId, + width : 1, height: 1, - data: { ...note, tick: tick, layer: layerId }, + data : { ...note, tick: tick, layer: layerId } }); // @ts-ignore //TODO: fix this @@ -45,7 +45,7 @@ export class NoteQuadTree { x1, y1, x2, - y2, + y2 }: { x1: number; y1: number; @@ -53,10 +53,10 @@ export class NoteQuadTree { y2: number; }): Note[] { const rect = new Rectangle({ - x: Math.min(x1, x2), - y: Math.min(y1, y2), - width: Math.abs(x2 - x1), - height: Math.abs(y2 - y1), + x : Math.min(x1, x2), + y : Math.min(y1, y2), + width : Math.abs(x2 - x1), + height: Math.abs(y2 - y1) }); return this.quadtree @@ -69,7 +69,7 @@ export class NoteQuadTree { note.tick >= x1 && note.tick <= x2 && note.layer >= y1 && - note.layer <= y2, + note.layer <= y2 ); } } diff --git a/packages/song/src/obfuscate.ts b/packages/song/src/obfuscate.ts index 490bcb80..fedfe9b4 100644 --- a/packages/song/src/obfuscate.ts +++ b/packages/song/src/obfuscate.ts @@ -55,7 +55,7 @@ export class SongObfuscator { for (const [ instrumentId, - instrument, + instrument ] of song.instruments.loaded.entries()) { if (instrument.builtIn) { instrumentMapping[instrumentId] = instrumentId; @@ -74,7 +74,7 @@ export class SongObfuscator { ) { console.log( `Skipping instrument '${instrumentName}' with ${noteCountPerInstrument[instrumentId]}`, - `notes and sound file '${soundFilePath}' (custom ID: ${customId})`, + `notes and sound file '${soundFilePath}' (custom ID: ${customId})` ); continue; @@ -87,14 +87,14 @@ export class SongObfuscator { console.log( `Keeping instrument '${instrumentName}' with`, `${noteCountPerInstrument[instrumentId]} notes and sound file`, - `'${this.soundPaths[customId]}' (custom ID: ${customId} -> ${newCustomId})`, + `'${this.soundPaths[customId]}' (custom ID: ${customId} -> ${newCustomId})` ); const newInstrument = new Instrument(newInstrumentId, { - name: instrumentName === 'Tempo Changer' ? 'Tempo Changer' : '', + name : instrumentName === 'Tempo Changer' ? 'Tempo Changer' : '', soundFile: soundFilePath, - key: instrument.key, - pressKey: false, + key : instrument.key, + pressKey : false }); output.instruments.loaded.push(newInstrument); @@ -107,7 +107,7 @@ export class SongObfuscator { private resolveNotes( song: Song, output: Song, - instrumentMapping: Record, + instrumentMapping: Record ) { // ✅ Pile notes at the top // ✅ Bake layer volume into note velocity @@ -172,7 +172,7 @@ export class SongObfuscator { key, velocity, panning, - pitch, + pitch }); return newNote; diff --git a/packages/song/src/pack.ts b/packages/song/src/pack.ts index b46df6cc..566397be 100644 --- a/packages/song/src/pack.ts +++ b/packages/song/src/pack.ts @@ -6,7 +6,7 @@ import { SongObfuscator } from './obfuscate'; export async function obfuscateAndPackSong( nbsSong: Song, soundsArray: string[], - soundsMapping: Record, + soundsMapping: Record ) { // Create a ZIP file with the obfuscated song in the root as 'song.nbs' // and the sounds in a 'sounds' folder. Return the ZIP file as a buffer. @@ -37,7 +37,7 @@ export async function obfuscateAndPackSong( // Download the sound from Mojang servers const soundFileUrl = `https://resources.download.minecraft.net/${hash.slice( 0, - 2, + 2 )}/${hash}`; let soundFileBuffer: ArrayBuffer; @@ -65,9 +65,9 @@ export async function obfuscateAndPackSong( // Generate the ZIP file as a buffer const zipBuffer = await zip.generateAsync({ - type: 'nodebuffer', + type : 'nodebuffer', mimeType: 'application/zip', // default - comment: 'Uploaded to Note Block World', + comment : 'Uploaded to Note Block World' // TODO: explore adding a password to the ZIP file // https://github.com/Stuk/jszip/issues/115 // https://github.com/Stuk/jszip/pull/696 diff --git a/packages/song/src/parse.ts b/packages/song/src/parse.ts index 202daf8d..7d721eda 100644 --- a/packages/song/src/parse.ts +++ b/packages/song/src/parse.ts @@ -8,7 +8,7 @@ async function getVanillaSoundList() { // Object that maps sound paths to their respective hashes const response = await fetch( - process.env.NEXT_PUBLIC_API_URL + '/data/soundList.json', + process.env.NEXT_PUBLIC_API_URL + '/data/soundList.json' ); const soundsMapping = (await response.json()) as Record; @@ -18,7 +18,7 @@ async function getVanillaSoundList() { } export async function parseSongFromBuffer( - buffer: ArrayBuffer, + buffer: ArrayBuffer ): Promise { const song = fromArrayBuffer(buffer); @@ -31,28 +31,28 @@ export async function parseSongFromBuffer( const vanillaSoundList = await getVanillaSoundList(); return { - title: song.meta.name, - author: song.meta.author, + title : song.meta.name, + author : song.meta.author, originalAuthor: song.meta.originalAuthor, - description: song.meta.description, - length: quadTree.width, - height: quadTree.height, - arrayBuffer: buffer, - notes: quadTree, - instruments: getInstruments(song, vanillaSoundList), + description : song.meta.description, + length : quadTree.width, + height : quadTree.height, + arrayBuffer : buffer, + notes : quadTree, + instruments : getInstruments(song, vanillaSoundList) }; } const getInstruments = ( song: Song, - vanillaSoundList: string[], + vanillaSoundList: string[] ): InstrumentArray => { const blockCounts = getInstrumentNoteCounts(song); const firstCustomIndex = song.instruments.firstCustomIndex; const customInstruments = song.instruments.loaded.filter( - (instrument) => instrument.builtIn === false, + (instrument) => instrument.builtIn === false ); return customInstruments.map((instrument, id) => { @@ -60,7 +60,7 @@ const getInstruments = ( const fullSoundPath = instrument.meta.soundFile.replace( 'minecraft/', - 'minecraft/sounds/', + 'minecraft/sounds/' ); if (vanillaSoundList.includes(fullSoundPath)) { @@ -68,10 +68,10 @@ const getInstruments = ( } return { - id: id, - name: instrument.meta.name || '', - file: soundFile, - count: blockCounts[id + firstCustomIndex] || 0, + id : id, + name : instrument.meta.name || '', + file : soundFile, + count: blockCounts[id + firstCustomIndex] || 0 }; }); }; diff --git a/packages/song/src/stats.ts b/packages/song/src/stats.ts index 4d53a0d4..25db6a7e 100644 --- a/packages/song/src/stats.ts +++ b/packages/song/src/stats.ts @@ -29,7 +29,7 @@ export class SongStatsGenerator { detunedNoteCount, customInstrumentNoteCount, incompatibleNoteCount, - instrumentNoteCounts, + instrumentNoteCounts } = this.getCounts(tempoChangerInstrumentIds); const tempoSegments = this.getTempoSegments(tempoChangerInstrumentIds); @@ -45,7 +45,7 @@ export class SongStatsGenerator { const { vanillaInstrumentCount, customInstrumentCount } = this.getVanillaAndCustomUsedInstrumentCounts( instrumentNoteCounts, - tempoChangerInstrumentIds, + tempoChangerInstrumentIds ); const firstCustomInstrumentIndex = this.getFirstCustomInstrumentIndex(); @@ -72,7 +72,7 @@ export class SongStatsGenerator { outOfRangeNoteCount, detunedNoteCount, incompatibleNoteCount, - compatible, + compatible }; } @@ -103,7 +103,7 @@ export class SongStatsGenerator { let incompatibleNoteCount = 0; const instrumentNoteCounts = Array( - this.song.instruments.loaded.length, + this.song.instruments.loaded.length ).fill(0); for (const [layerId, layer] of this.song.layers.entries()) { @@ -159,7 +159,7 @@ export class SongStatsGenerator { // Don't consider tempo changers as detuned notes or custom instruments const isTempoChanger = tempoChangerInstruments.includes( // @ts-ignore //TODO: fix this - note.instrument, + note.instrument ); // @ts-ignore //TODO: fix this @@ -194,7 +194,7 @@ export class SongStatsGenerator { detunedNoteCount, customInstrumentNoteCount, incompatibleNoteCount, - instrumentNoteCounts, + instrumentNoteCounts }; } @@ -203,7 +203,7 @@ export class SongStatsGenerator { } private getTempoRange( - tempoSegments: Record, + tempoSegments: Record ): number[] | null { const tempoValues = Object.values(tempoSegments); // If song has only the tempo set at the beginning, we have no tempo changes (indicated as null) @@ -216,7 +216,7 @@ export class SongStatsGenerator { } private getTempoSegments( - tempoChangerInstruments: number[], + tempoChangerInstruments: number[] ): Record { const tempoSegments: Record = {}; @@ -254,7 +254,7 @@ export class SongStatsGenerator { private getDuration(tempoSegments: Record): number { const tempoChangeTicks = Object.keys(tempoSegments).map((tick) => - parseInt(tick), + parseInt(tick) ); tempoChangeTicks.sort((a, b) => a - b); @@ -300,7 +300,7 @@ export class SongStatsGenerator { private getVanillaAndCustomUsedInstrumentCounts( noteCountsPerInstrument: number[], - tempoChangerInstruments: number[], + tempoChangerInstruments: number[] ): { vanillaInstrumentCount: number; customInstrumentCount: number; @@ -321,7 +321,7 @@ export class SongStatsGenerator { return { vanillaInstrumentCount, - customInstrumentCount, + customInstrumentCount }; } diff --git a/packages/song/src/util.ts b/packages/song/src/util.ts index 00e96dbd..ba9e722d 100644 --- a/packages/song/src/util.ts +++ b/packages/song/src/util.ts @@ -2,7 +2,7 @@ import { Song } from '@encode42/nbs.js'; export function getTempoChangerInstrumentIds(song: Song): number[] { return song.instruments.loaded.flatMap((instrument, id) => - instrument.meta.name === 'Tempo Changer' ? [id] : [], + instrument.meta.name === 'Tempo Changer' ? [id] : [] ); } @@ -10,8 +10,8 @@ export function getInstrumentNoteCounts(song: Song): Record { const blockCounts = Object.fromEntries( Object.keys(song.instruments.loaded).map((instrumentId) => [ instrumentId, - 0, - ]), + 0 + ]) ); for (const layer of song.layers) { diff --git a/packages/song/tests/song/index.spec.ts b/packages/song/tests/song/index.spec.ts index 08b8d48c..ea6cd645 100644 --- a/packages/song/tests/song/index.spec.ts +++ b/packages/song/tests/song/index.spec.ts @@ -1,9 +1,10 @@ // @ts-nocheck // TODO: fix this import assert from 'assert'; -import { openSongFromPath } from './util'; import { SongStatsGenerator } from '../../src/stats'; +import { openSongFromPath } from './util'; + // TO RUN: // // From the root of the 'shared' package, run: @@ -12,26 +13,26 @@ import { SongStatsGenerator } from '../../src/stats'; // TODO: refactor to use a proper test runner (e.g. jest) const testSongPaths = { - simple: 'files/testSimple.nbs', - extraPopulatedLayer: 'files/testExtraPopulatedLayer.nbs', - loop: 'files/testLoop.nbs', - detune: 'files/testDetune.nbs', - outOfRange: 'files/testOutOfRange.nbs', - outOfRangeCustomPitch: 'files/testOutOfRangeCustomPitch.nbs', - customInstrumentNoUsage: 'files/testCustomInstrumentNoUsage.nbs', - customInstrumentUsage: 'files/testCustomInstrumentUsage.nbs', - tempoChangerWithStart: 'files/testTempoChangerWithStart.nbs', - tempoChangerNoStart: 'files/testTempoChangerNoStart.nbs', + simple : 'files/testSimple.nbs', + extraPopulatedLayer : 'files/testExtraPopulatedLayer.nbs', + loop : 'files/testLoop.nbs', + detune : 'files/testDetune.nbs', + outOfRange : 'files/testOutOfRange.nbs', + outOfRangeCustomPitch : 'files/testOutOfRangeCustomPitch.nbs', + customInstrumentNoUsage : 'files/testCustomInstrumentNoUsage.nbs', + customInstrumentUsage : 'files/testCustomInstrumentUsage.nbs', + tempoChangerWithStart : 'files/testTempoChangerWithStart.nbs', + tempoChangerNoStart : 'files/testTempoChangerNoStart.nbs', tempoChangerDifferentStart: 'files/testTempoChangerDifferentStart.nbs', - tempoChangerOverlap: 'files/testTempoChangerOverlap.nbs', + tempoChangerOverlap : 'files/testTempoChangerOverlap.nbs', tempoChangerMultipleInstruments: - 'files/testTempoChangerMultipleInstruments.nbs', + 'files/testTempoChangerMultipleInstruments.nbs' }; const testSongStats = Object.fromEntries( Object.entries(testSongPaths).map(([name, path]) => { return [name, SongStatsGenerator.getSongStats(openSongFromPath(path))]; - }), + }) ); describe('SongStatsGenerator', () => { @@ -60,7 +61,7 @@ describe('SongStatsGenerator', () => { assert( stats.instrumentNoteCounts.toString() === - [5, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 2].toString(), + [5, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 2].toString() ); }); diff --git a/packages/sounds/src/fetchSoundList.ts b/packages/sounds/src/fetchSoundList.ts index e1d07d7c..c1881c13 100644 --- a/packages/sounds/src/fetchSoundList.ts +++ b/packages/sounds/src/fetchSoundList.ts @@ -57,7 +57,7 @@ async function getAssetIndexSounds(version: string) { // Get a new record with only keys that end with '.ogg' const sounds = Object.entries(objects).filter(([key]) => - key.endsWith('.ogg'), + key.endsWith('.ogg') ); return sounds; @@ -68,7 +68,7 @@ async function getSoundList(version: string) { // Return an object with sound names as keys and hashes as values const soundList = Object.fromEntries( - sounds.map(([key, { hash }]) => [key, hash]), + sounds.map(([key, { hash }]) => [key, hash]) ); return soundList; diff --git a/packages/thumbnail/src/canvasFactory.ts b/packages/thumbnail/src/canvasFactory.ts index d1c32fb6..3efc0d25 100644 --- a/packages/thumbnail/src/canvasFactory.ts +++ b/packages/thumbnail/src/canvasFactory.ts @@ -15,7 +15,7 @@ if (typeof document === 'undefined') { const { createCanvas: nodeCreateCanvas, loadImage: nodeLoadImage, - GlobalFonts, + GlobalFonts } = canvasModule; const getPath = (filename: string): string => { @@ -30,8 +30,8 @@ if (typeof document === 'undefined') { workingDir, 'packages', 'thumbnail', - filename.split('/').join(path.sep), - ), + filename.split('/').join(path.sep) + ) ]; // Check which path exists @@ -68,14 +68,14 @@ if (typeof document === 'undefined') { } canvasUtils = { - createCanvas: nodeCreateCanvas, - loadImage: nodeLoadImage, + createCanvas : nodeCreateCanvas, + loadImage : nodeLoadImage, getPath, useFont, saveToImage, noteBlockImage, DrawingCanvas: canvasModule.Canvas, - RenderedImage: canvasModule.Image, + RenderedImage: canvasModule.Image }; } catch (error) { // Fallback for when @napi-rs/canvas is not available (e.g., in browser build) @@ -91,7 +91,7 @@ if (typeof document === 'undefined') { const loadImage = (src: string): Promise => { return Promise.reject( - new Error('loadImage not available in this environment'), + new Error('loadImage not available in this environment') ); }; @@ -106,7 +106,7 @@ if (typeof document === 'undefined') { }; const noteBlockImage = Promise.reject( - new Error('noteBlockImage not available in this environment'), + new Error('noteBlockImage not available in this environment') ); canvasUtils = { @@ -117,7 +117,7 @@ if (typeof document === 'undefined') { saveToImage, noteBlockImage, DrawingCanvas: HTMLCanvasElement as any, - RenderedImage: HTMLImageElement as any, + RenderedImage: HTMLImageElement as any }; } } else { @@ -162,7 +162,7 @@ if (typeof document === 'undefined') { saveToImage, noteBlockImage, DrawingCanvas: HTMLCanvasElement, - RenderedImage: HTMLImageElement, + RenderedImage: HTMLImageElement }; } @@ -174,5 +174,5 @@ export const { saveToImage, noteBlockImage, DrawingCanvas, - RenderedImage, + RenderedImage } = canvasUtils; diff --git a/packages/thumbnail/src/index.ts b/packages/thumbnail/src/index.ts index 588e7c9e..9461ee3b 100644 --- a/packages/thumbnail/src/index.ts +++ b/packages/thumbnail/src/index.ts @@ -7,7 +7,7 @@ import { createCanvas, noteBlockImage, saveToImage, - useFont, + useFont } from './canvasFactory'; import type { Canvas, DrawParams } from './types'; import { getKeyText, instrumentColors, isDarkColor, tintImage } from './utils'; @@ -43,7 +43,7 @@ export const drawNotesOffscreen = async ({ canvasWidth, //canvasHeight, imgWidth = 1280, - imgHeight = 768, + imgHeight = 768 }: DrawParams) => { // Create new offscreen canvas const canvas = createCanvas(imgWidth, imgHeight); @@ -98,7 +98,7 @@ export const drawNotesOffscreen = async ({ x1: startTick, y1: startLayer, x2: endTick, - y2: endLayer, + y2: endLayer }); visibleNotes.forEach(async (note: Note) => { @@ -117,7 +117,7 @@ export const drawNotesOffscreen = async ({ x, y, 8 * zoomFactor, - 8 * zoomFactor, + 8 * zoomFactor ); // Draw the key text diff --git a/packages/thumbnail/src/utils.spec.ts b/packages/thumbnail/src/utils.spec.ts index 802d38ba..b96a1573 100644 --- a/packages/thumbnail/src/utils.spec.ts +++ b/packages/thumbnail/src/utils.spec.ts @@ -101,7 +101,7 @@ describe('getKeyText', () => { { key: 84, expected: 'A7' }, { key: 85, expected: 'A#7' }, { key: 86, expected: 'B7' }, - { key: 87, expected: 'C8' }, + { key: 87, expected: 'C8' } ]; testCases.forEach(({ key, expected }) => { diff --git a/packages/thumbnail/src/utils.ts b/packages/thumbnail/src/utils.ts index 52ad53af..4f2a7c96 100644 --- a/packages/thumbnail/src/utils.ts +++ b/packages/thumbnail/src/utils.ts @@ -17,7 +17,7 @@ export const instrumentColors = [ '#be5728', '#19be19', '#be1957', - '#575757', + '#575757' ]; const tintedImages: Record = {}; @@ -69,7 +69,7 @@ export const getKeyText = (key: number): string => { 'G#', 'A', 'A#', - 'B', + 'B' ]; const note = notes[(key + 9) % 12]; From 35275925aece8d6049ab5fcbf6627529987f3c8a Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 10:42:24 -0300 Subject: [PATCH 03/21] chore: simplify lint scripts in package.json for improved maintainability --- package.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/package.json b/package.json index 66570f4a..24dd07bb 100644 --- a/package.json +++ b/package.json @@ -45,15 +45,7 @@ "dev": "concurrently --success first -n \"server,web\" --prefix-colors \"cyan,magenta\" --prefix \"{name} {time}\" \"cd ./apps/backend && bun run start:dev\" \"cd ./apps/frontend && bun run start\"", "dev:server": "cd ./apps/backend && bun run start:dev", "dev:web": "cd ./apps/frontend && bun run start", - "lint": "eslint \"**/*.{ts,tsx}\" --fix --max-warnings 20", - "lint:server": "cd ./apps/backend && eslint \"src/**/*.ts\" --ignore-pattern \"**/*.spec.ts\" --ignore-pattern \"**/*.test.ts\" --fix --max-warnings 10", - "lint:web": "cd ./apps/frontend && eslint \"src/**/*.{ts,tsx}\" --fix --max-warnings 10", - "lint:packages": "cd ./packages/configs && eslint \"src/**/*.ts\" --fix && cd ../database && eslint \"src/**/*.ts\" --fix && cd ../song && eslint \"src/**/*.ts\" --fix && cd ../sounds && eslint \"src/**/*.ts\" --fix && cd ../thumbnail && eslint \"src/**/*.ts\" --fix", - "lint:database": "cd ./packages/database && bun run lint", - "lint:song": "cd ./packages/song && bun run lint", - "lint:sounds": "cd ./packages/sounds && bun run lint", - "lint:thumbnail": "cd ./packages/thumbnail && bun run lint", - "lint:components": "cd ./packages/components && bun run lint", + "lint": "eslint \"**/*.{ts,tsx}\" --fix", "test": "cd ./apps/backend && bun test", "cy:open": "bun run test:cy", "test:cy": "cd ./tests && bun run cy:open", From 28b4e4f0cbeb2156bcd56aaa07701e3fb1bc857e Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 10:47:18 -0300 Subject: [PATCH 04/21] chore: integrate @stylistic/eslint-plugin and update ESLint configuration for improved code style enforcement --- bun.lock | 28 +++++++++++++++++++++------- eslint.config.js | 41 ++++++++++++++++++++--------------------- package.json | 2 +- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index 5cfbf9a5..239c74c1 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ }, "devDependencies": { "@eslint/js": "^9.35.0", + "@stylistic/eslint-plugin": "^5.4.0", "@types/bun": "^1.2.10", "@types/node": "^20.3.1", "@typescript-eslint/eslint-plugin": "^8.43.0", @@ -19,7 +20,6 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-mdx": "^3.6.2", "eslint-plugin-prettier": "^4.2.5", - "eslint-plugin-unused-imports": "^2.0.0", "globals": "^16.4.0", "prettier": "^2.8.8", }, @@ -864,6 +864,8 @@ "@smithy/util-waiter": ["@smithy/util-waiter@3.2.0", "", { "dependencies": { "@smithy/abort-controller": "^3.1.9", "@smithy/types": "^3.7.2", "tslib": "^2.6.2" } }, "sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg=="], + "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.44.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-UG8hdElzuBDzIbjG1QDwnYH0MQ73YLXDFHgZzB4Zh/YJfnw8XNsloVtytqzx0I2Qky9THSdpTmi8Vjn/pf/Lew=="], + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], @@ -1030,7 +1032,7 @@ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/utils": "8.43.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="], @@ -1532,10 +1534,6 @@ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw=="], - "eslint-plugin-unused-imports": ["eslint-plugin-unused-imports@2.0.0", "", { "dependencies": { "eslint-rule-composer": "^0.3.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^5.0.0", "eslint": "^8.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin"] }, "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A=="], - - "eslint-rule-composer": ["eslint-rule-composer@0.3.0", "", {}, "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg=="], - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], @@ -2430,7 +2428,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.1", "", {}, "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -3026,6 +3024,8 @@ "@angular-devkit/core/jsonc-parser": ["jsonc-parser@3.2.1", "", {}, "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA=="], + "@angular-devkit/core/picomatch": ["picomatch@4.0.1", "", {}, "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg=="], + "@angular-devkit/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="], "@angular-devkit/core/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -3178,10 +3178,24 @@ "@types/whatwg-url/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + + "@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + + "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], + "ajv-formats/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], "alce/esprima": ["esprima@1.2.5", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ=="], diff --git a/eslint.config.js b/eslint.config.js index 41122f00..aca82b3e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,7 +2,7 @@ import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import globals from 'globals'; import importPlugin from 'eslint-plugin-import'; -import unusedImportsPlugin from 'eslint-plugin-unused-imports'; +import stylistic from '@stylistic/eslint-plugin'; export default tseslint.config( // Global ignores. @@ -34,7 +34,7 @@ export default tseslint.config( }, plugins: { 'import': importPlugin, - 'unused-imports': unusedImportsPlugin, + '@stylistic': stylistic, }, settings: { 'import/resolver': { @@ -65,19 +65,10 @@ export default tseslint.config( ignoreStrings: true, ignoreTemplateLiterals: true, }], - 'key-spacing': ['error', { - align: { - beforeColon: false, - afterColon: true, - on: 'colon', - }, - }], - '@typescript-eslint/no-unused-vars': 'off', // Disabled to allow unused-imports plugin to handle it. '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-require-imports': 'warn', '@typescript-eslint/ban-ts-comment': 'warn', - 'unused-imports/no-unused-imports': 'error', - 'unused-imports/no-unused-vars': [ + '@typescript-eslint/no-unused-vars': [ 'warn', { vars: 'all', @@ -112,14 +103,22 @@ export default tseslint.config( 'import/no-duplicates': 'error', // Spacing rules for consistency - 'space-infix-ops': 'error', // Enforces spaces around operators like +, =, etc. - 'keyword-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around keywords like if, else. - 'arrow-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around arrow in arrow functions. - 'space-before-blocks': 'error', // Enforces a space before opening curly braces. - 'object-curly-spacing': ['error', 'always'], // Enforces spaces inside curly braces: { foo } not {foo}. - 'comma-spacing': ['error', { 'before': false, 'after': true }], // Enforces space after a comma, not before. - 'space-before-function-paren': ['error', { 'anonymous': 'always', 'named': 'never', 'asyncArrow': 'always' }], // Controls space before function parentheses. - 'comma-dangle': ['error', 'never'], // Disallows trailing commas + '@stylistic/space-infix-ops': 'error', // Enforces spaces around operators like +, =, etc. + '@stylistic/keyword-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around keywords like if, else. + '@stylistic/arrow-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around arrow in arrow functions. + '@stylistic/space-before-blocks': 'error', // Enforces a space before opening curly braces. + '@stylistic/object-curly-spacing': ['error', 'always'], // Enforces spaces inside curly braces: { foo } not {foo}. + '@stylistic/comma-spacing': ['error', { 'before': false, 'after': true }], // Enforces space after a comma, not before. + '@stylistic/space-before-function-paren': ['error', { 'anonymous': 'always', 'named': 'never', 'asyncArrow': 'always' }], // Controls space before function parentheses. + '@stylistic/comma-dangle': ['error', 'never'], // Disallows trailing commas + '@stylistic/key-spacing': ['error', { + align: { + beforeColon: false, + afterColon: true, + on: 'colon', + }, + }], }, }, -); \ No newline at end of file +); + diff --git a/package.json b/package.json index 24dd07bb..c023723e 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ ], "devDependencies": { "@eslint/js": "^9.35.0", + "@stylistic/eslint-plugin": "^5.4.0", "@types/bun": "^1.2.10", "@types/node": "^20.3.1", "@typescript-eslint/eslint-plugin": "^8.43.0", @@ -70,7 +71,6 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-mdx": "^3.6.2", "eslint-plugin-prettier": "^4.2.5", - "eslint-plugin-unused-imports": "^2.0.0", "globals": "^16.4.0", "prettier": "^2.8.8" }, From ea0dc10a16c172704a8dad67307f26b29f6c5816 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 10:47:45 -0300 Subject: [PATCH 05/21] lint commit --- apps/backend/src/auth/auth.service.spec.ts | 2 +- .../strategies/discord.strategy/Strategy.ts | 6 +- .../auth/strategies/discord.strategy/types.ts | 248 +++++++++--------- .../magicLinkEmail.strategy.spec.ts | 1 - apps/backend/src/auth/types/discordProfile.ts | 42 +-- apps/backend/src/auth/types/githubProfile.ts | 84 +++--- apps/backend/src/auth/types/googleProfile.ts | 28 +- apps/backend/src/auth/types/profile.ts | 4 +- apps/backend/src/auth/types/token.ts | 6 +- .../src/config/EnvironmentVariables.ts | 2 +- apps/backend/src/file/file.service.ts | 2 +- apps/backend/src/lib/parseToken.spec.ts | 1 - apps/backend/src/mailing/mailing.service.ts | 4 +- apps/backend/src/seed/seed.service.spec.ts | 1 - .../song-browser/song-browser.service.spec.ts | 1 - .../song-upload/song-upload.service.spec.ts | 3 +- .../song/song-upload/song-upload.service.ts | 2 +- apps/backend/src/song/song.controller.spec.ts | 9 +- apps/backend/src/song/song.service.ts | 2 +- apps/frontend/src/lib/posts.ts | 14 +- apps/frontend/src/modules/auth/types/User.ts | 48 ++-- .../components/client/CategoryButton.tsx | 8 +- .../components/client/TimespanButton.tsx | 8 +- .../client/context/FeaturedSongs.context.tsx | 8 +- .../client/context/HomePage.context.tsx | 4 +- .../client/context/RecentSongs.context.tsx | 16 +- .../components/client/DeleteConfirmDialog.tsx | 4 +- .../components/client/MySongsButtons.tsx | 2 +- .../components/client/MySongsTable.tsx | 2 +- .../client/context/MySongs.context.tsx | 36 +-- .../shared/components/NoteBlockWorldLogo.tsx | 6 +- .../shared/components/TeamMemberCard.tsx | 6 +- .../shared/components/client/Carousel.tsx | 18 +- .../shared/components/client/FormElements.tsx | 38 +-- .../shared/components/client/GenericModal.tsx | 6 +- .../shared/components/client/ads/AdSlots.tsx | 12 +- .../shared/components/layout/BlockTab.tsx | 6 +- .../shared/components/layout/NavLinks.tsx | 2 +- .../components/layout/SongThumbnail.tsx | 2 +- .../shared/components/layout/UserMenuLink.tsx | 8 +- .../components/client/SongEditForm.tsx | 2 +- .../client/context/EditSong.context.tsx | 20 +- .../client/context/UploadSong.context.tsx | 30 +-- .../song/components/SongPageButtons.tsx | 4 +- .../components/client/DownloadSongModal.tsx | 4 +- .../song/components/client/FileDisplay.tsx | 6 +- .../components/client/InstrumentPicker.tsx | 10 +- .../song/components/client/ShareModal.tsx | 4 +- .../song/components/client/SongForm.tsx | 4 +- .../components/client/SongSearchCombo.tsx | 6 +- .../components/client/SongThumbnailInput.tsx | 14 +- .../components/client/ThumbnailRenderer.tsx | 2 +- .../src/modules/song/util/downloadSong.ts | 2 +- 53 files changed, 398 insertions(+), 412 deletions(-) diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts index a70d6d2b..67b10197 100644 --- a/apps/backend/src/auth/auth.service.spec.ts +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -3,9 +3,9 @@ import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; import type { UserDocument } from '@nbw/database'; import { JwtService } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from '@server/user/user.service'; import type { Request, Response } from 'express'; -import { UserService } from '@server/user/user.service'; import { AuthService } from './auth.service'; import { Profile } from './types/profile'; diff --git a/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts b/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts index 31b5b37c..eeb8111c 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts @@ -30,11 +30,11 @@ export default class Strategy extends OAuth2Strategy { public static DISCORD_API_BASE = 'https://discord.com/api'; private readonly logger = new Logger('DiscordStrategy'); - private scope: ScopeType; - private scopeDelay: number; + private scope : ScopeType; + private scopeDelay : number; private fetchScopeEnabled: boolean; public override name = 'discord'; - prompt?: string; + prompt? : string; public constructor(options: DiscordStrategyConfig, verify: VerifyFunction) { super( { diff --git a/apps/backend/src/auth/strategies/discord.strategy/types.ts b/apps/backend/src/auth/strategies/discord.strategy/types.ts index cb276741..80f4e3a6 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/types.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/types.ts @@ -31,7 +31,7 @@ export enum DiscordPermissionScope { RpcVoiceRead = 'rpc.voice.read', RpcVoiceWrite = 'rpc.voice.write', Voice = 'voice', - WebhookIncoming = 'webhook.incoming', + WebhookIncoming = 'webhook.incoming' } export type SingleScopeType = `${DiscordPermissionScope}`; @@ -42,188 +42,188 @@ export type ScopeType = SingleScopeType[]; * https://discord.com/developers/docs/resources/user#user-object */ export interface DiscordUser { - id: string; - username: string; - global_name?: string | undefined; - avatar: string; - bot?: string | undefined; - system?: boolean | undefined; - mfa_enabled?: boolean | undefined; - banner?: string | undefined; - accent_color?: number | undefined; - locale?: string | undefined; - verified?: boolean | undefined; - email?: string | undefined; - flags?: number | undefined; - premium_type?: number | undefined; - public_flags?: number | undefined; + id : string; + username : string; + global_name? : string | undefined; + avatar : string; + bot? : string | undefined; + system? : boolean | undefined; + mfa_enabled? : boolean | undefined; + banner? : string | undefined; + accent_color? : number | undefined; + locale? : string | undefined; + verified? : boolean | undefined; + email? : string | undefined; + flags? : number | undefined; + premium_type? : number | undefined; + public_flags? : number | undefined; avatar_decoration_data?: AvatarDecorationData | undefined; } export interface AvatarDecorationData { - asset: string; + asset : string; sku_id: string; } export interface DiscordAccount { - id: string; + id : string; name: string; } export interface DiscordApplication { - id: string; - name: string; - icon?: string | undefined; + id : string; + name : string; + icon? : string | undefined; description: string; - bot?: DiscordUser; + bot? : DiscordUser; } export interface DiscordIntegration { - id: string; - name: string; - type: string; - enabled: boolean; - syncing?: boolean | undefined; - role_id?: string | undefined; - enable_emoticons?: boolean | undefined; - expire_behavior?: number | undefined; + id : string; + name : string; + type : string; + enabled : boolean; + syncing? : boolean | undefined; + role_id? : string | undefined; + enable_emoticons? : boolean | undefined; + expire_behavior? : number | undefined; expire_grace_period?: number | undefined; - user?: DiscordUser | undefined; - account: DiscordAccount; - synced_at?: Date | undefined; - subscriber_count?: number | undefined; - revoked?: boolean | undefined; - application?: DiscordApplication | undefined; - scopes?: ScopeType | undefined; + user? : DiscordUser | undefined; + account : DiscordAccount; + synced_at? : Date | undefined; + subscriber_count? : number | undefined; + revoked? : boolean | undefined; + application? : DiscordApplication | undefined; + scopes? : ScopeType | undefined; } export interface ProfileConnection { - id: string; - name: string; - type: string; - revoked?: boolean | undefined; + id : string; + name : string; + type : string; + revoked? : boolean | undefined; integrations?: DiscordIntegration[] | undefined; - verified: boolean; - friend_sync: boolean; + verified : boolean; + friend_sync : boolean; show_activity: boolean; - two_way_link: boolean; - visibility: number; + two_way_link : boolean; + visibility : number; } export interface DiscordRoleTag { - bot_id?: string | undefined; - integration_id?: string | undefined; - premium_subscriber?: null | undefined; + bot_id? : string | undefined; + integration_id? : string | undefined; + premium_subscriber? : null | undefined; subscription_listing_id?: string | undefined; - available_for_purchase?: null | undefined; - guild_connections?: null | undefined; + available_for_purchase? : null | undefined; + guild_connections? : null | undefined; } export interface DiscordRole { - id: string; - name: string; - color: number; - hoist: boolean; - icon?: string | undefined; + id : string; + name : string; + color : number; + hoist : boolean; + icon? : string | undefined; unicode_emoji?: string | undefined; - position: number; - permissions: string; - managed: boolean; - tags?: DiscordRoleTag | undefined; - flags: number; + position : number; + permissions : string; + managed : boolean; + tags? : DiscordRoleTag | undefined; + flags : number; } export interface DiscordEmoji { - id?: string | undefined; - name: string | undefined; - roles?: string[]; - user?: DiscordUser; + id? : string | undefined; + name : string | undefined; + roles? : string[]; + user? : DiscordUser; require_colons?: boolean | undefined; - managed?: boolean | undefined; - animated?: boolean | undefined; - available?: boolean | undefined; + managed? : boolean | undefined; + animated? : boolean | undefined; + available? : boolean | undefined; } export interface DiscordWelcomeScreenChannel { - channel_id: string; + channel_id : string; description: string; - emoji_id?: string | undefined; + emoji_id? : string | undefined; emoji_name?: string | undefined; } export interface DiscordWelcomeScreen { - description?: string | undefined; + description? : string | undefined; welcome_channels: DiscordWelcomeScreenChannel[]; } export interface DiscordSticker { - id: string; - pack_id?: string | undefined; - name: string; + id : string; + pack_id? : string | undefined; + name : string; description: string; - tags: string; - type: number; + tags : string; + type : number; format_type: number; - available?: boolean | undefined; - guild_id?: string | undefined; - user?: DiscordUser | undefined; + available? : boolean | undefined; + guild_id? : string | undefined; + user? : DiscordUser | undefined; sort_value?: number | undefined; } export interface ProfileGuild { - id: string; - name: string; - icon?: string | undefined; - icon_hash?: string | undefined; - splash?: string | undefined; - discovery_splash?: string | undefined; - owner?: boolean | string; - owner_id: string; - permissions?: string | undefined; - afk_channel_id?: string | undefined; - afk_timeout?: number | undefined; - widget_enabled: boolean | undefined; - widget_channel_id?: string | undefined; - verification_level?: number | undefined; + id : string; + name : string; + icon? : string | undefined; + icon_hash? : string | undefined; + splash? : string | undefined; + discovery_splash? : string | undefined; + owner? : boolean | string; + owner_id : string; + permissions? : string | undefined; + afk_channel_id? : string | undefined; + afk_timeout? : number | undefined; + widget_enabled : boolean | undefined; + widget_channel_id? : string | undefined; + verification_level? : number | undefined; default_message_notifications?: number | undefined; - explicit_content_filter?: number | undefined; - roles: DiscordRole[]; - emojis: DiscordEmoji[]; - features: string[]; - mfa_level?: number | undefined; - application_id?: string | undefined; - system_channel_id?: string | undefined; - system_channel_flags?: number | undefined; - rules_channel_id?: string | undefined; - max_presences?: number | undefined; - max_members?: number | undefined; - vanity_url_code?: string | undefined; - description?: string | undefined; - banner?: string | undefined; - premium_tier?: number | undefined; - premium_subscription_count?: number | undefined; - preferred_locale?: string | undefined; - public_updates_channel_id?: string | undefined; - max_video_channel_users?: number | undefined; + explicit_content_filter? : number | undefined; + roles : DiscordRole[]; + emojis : DiscordEmoji[]; + features : string[]; + mfa_level? : number | undefined; + application_id? : string | undefined; + system_channel_id? : string | undefined; + system_channel_flags? : number | undefined; + rules_channel_id? : string | undefined; + max_presences? : number | undefined; + max_members? : number | undefined; + vanity_url_code? : string | undefined; + description? : string | undefined; + banner? : string | undefined; + premium_tier? : number | undefined; + premium_subscription_count? : number | undefined; + preferred_locale? : string | undefined; + public_updates_channel_id? : string | undefined; + max_video_channel_users? : number | undefined; max_stage_video_channel_users?: number | undefined; - approximate_member_count?: number | undefined; - approximate_presence_count?: number | undefined; - welcome_screen?: DiscordWelcomeScreen | undefined; - nsfw_level?: number | undefined; - stickers?: DiscordSticker[] | undefined; - premium_progress_bar_enabled?: boolean | undefined; - safety_alerts_channel_id?: string | undefined; + approximate_member_count? : number | undefined; + approximate_presence_count? : number | undefined; + welcome_screen? : DiscordWelcomeScreen | undefined; + nsfw_level? : number | undefined; + stickers? : DiscordSticker[] | undefined; + premium_progress_bar_enabled? : boolean | undefined; + safety_alerts_channel_id? : string | undefined; } export interface Profile extends Omit, DiscordUser { - provider: string; + provider : string; connections?: ProfileConnection[] | undefined; - guilds?: ProfileGuild[] | undefined; + guilds? : ProfileGuild[] | undefined; access_token: string; - fetchedAt: Date; - createdAt: Date; - _raw: unknown; - _json: Record; + fetchedAt : Date; + createdAt : Date; + _raw : unknown; + _json : Record; } diff --git a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts index ede8cb2e..073bd5f3 100644 --- a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts @@ -1,6 +1,5 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; - import { MailingService } from '@server/mailing/mailing.service'; import { UserService } from '@server/user/user.service'; diff --git a/apps/backend/src/auth/types/discordProfile.ts b/apps/backend/src/auth/types/discordProfile.ts index 18835e6b..30d4d82e 100644 --- a/apps/backend/src/auth/types/discordProfile.ts +++ b/apps/backend/src/auth/types/discordProfile.ts @@ -1,28 +1,28 @@ export type DiscordUser = { - access_token: string; + access_token : string; refresh_token: string; - profile: DiscordProfile; + profile : DiscordProfile; }; export type DiscordProfile = { - id: string; - username: string; - avatar: string; - discriminator: string; - public_flags: number; - flags: number; - banner: string; - accent_color: string; - global_name: string; + id : string; + username : string; + avatar : string; + discriminator : string; + public_flags : number; + flags : number; + banner : string; + accent_color : string; + global_name : string; avatar_decoration_data: string | null; - banner_color: string | null; - clan: string | null; - mfa_enabled: boolean; - locale: string; - premium_type: number; - email: string; - verified: boolean; - provider: string; - accessToken: string; - fetchedAt: string; + banner_color : string | null; + clan : string | null; + mfa_enabled : boolean; + locale : string; + premium_type : number; + email : string; + verified : boolean; + provider : string; + accessToken : string; + fetchedAt : string; }; diff --git a/apps/backend/src/auth/types/githubProfile.ts b/apps/backend/src/auth/types/githubProfile.ts index 754737e8..2e2bc2b5 100644 --- a/apps/backend/src/auth/types/githubProfile.ts +++ b/apps/backend/src/auth/types/githubProfile.ts @@ -7,59 +7,59 @@ type Photo = { }; type ProfileJson = { - login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; + login : string; + id : number; + node_id : string; + avatar_url : string; + gravatar_id : string; + url : string; + html_url : string; + followers_url : string; + following_url : string; + gists_url : string; + starred_url : string; + subscriptions_url : string; + organizations_url : string; + repos_url : string; + events_url : string; received_events_url: string; - type: string; - site_admin: boolean; - name: string; - company: string; - blog: string; - location: string; - email: string; - hireable: boolean; - bio: string; - twitter_username: string; - public_repos: number; - public_gists: number; - followers: number; - following: number; - created_at: string; - updated_at: string; + type : string; + site_admin : boolean; + name : string; + company : string; + blog : string; + location : string; + email : string; + hireable : boolean; + bio : string; + twitter_username : string; + public_repos : number; + public_gists : number; + followers : number; + following : number; + created_at : string; + updated_at : string; }; type Profile = { - id: string; + id : string; displayName: string; - username: string; - profileUrl: string; - emails: Email[]; - photos: Photo[]; - provider: string; - _raw: string; - _json: ProfileJson; + username : string; + profileUrl : string; + emails : Email[]; + photos : Photo[]; + provider : string; + _raw : string; + _json : ProfileJson; }; export type GithubAccessToken = { accessToken: string; - profile: Profile; + profile : Profile; }; export type GithubEmailList = Array<{ - email: string; - primary: string; + email : string; + primary : string; visibility: string; }>; diff --git a/apps/backend/src/auth/types/googleProfile.ts b/apps/backend/src/auth/types/googleProfile.ts index 8b571c64..9d29c495 100644 --- a/apps/backend/src/auth/types/googleProfile.ts +++ b/apps/backend/src/auth/types/googleProfile.ts @@ -1,5 +1,5 @@ type Email = { - value: string; + value : string; verified: boolean; }; @@ -8,27 +8,27 @@ type Photo = { }; type ProfileJson = { - sub: string; - name: string; - given_name: string; - family_name?: string; - picture: string; - email: string; + sub : string; + name : string; + given_name : string; + family_name? : string; + picture : string; + email : string; email_verified: boolean; - locale: string; + locale : string; }; // TODO: this is a uniform profile type standardized by passport for all providers export type GoogleProfile = { - id: string; + id : string; displayName: string; name: { familyName?: string; - givenName: string; + givenName : string; }; - emails: Email[]; - photos: Photo[]; + emails : Email[]; + photos : Photo[]; provider: string; - _raw: string; - _json: ProfileJson; + _raw : string; + _json : ProfileJson; }; diff --git a/apps/backend/src/auth/types/profile.ts b/apps/backend/src/auth/types/profile.ts index edce9d7b..9e6f6f74 100644 --- a/apps/backend/src/auth/types/profile.ts +++ b/apps/backend/src/auth/types/profile.ts @@ -1,5 +1,5 @@ export type Profile = { - username: string; - email: string; + username : string; + email : string; profileImage: string; }; diff --git a/apps/backend/src/auth/types/token.ts b/apps/backend/src/auth/types/token.ts index 104354ce..c5025df7 100644 --- a/apps/backend/src/auth/types/token.ts +++ b/apps/backend/src/auth/types/token.ts @@ -1,10 +1,10 @@ export type TokenPayload = { - id: string; - email: string; + id : string; + email : string; username: string; }; export type Tokens = { - access_token: string; + access_token : string; refresh_token: string; }; diff --git a/apps/backend/src/config/EnvironmentVariables.ts b/apps/backend/src/config/EnvironmentVariables.ts index 05b667ea..43555591 100644 --- a/apps/backend/src/config/EnvironmentVariables.ts +++ b/apps/backend/src/config/EnvironmentVariables.ts @@ -3,7 +3,7 @@ import { IsEnum, IsOptional, IsString, validateSync } from 'class-validator'; enum Environment { Development = 'development', - Production = 'production', + Production = 'production' } export class EnvironmentVariables { diff --git a/apps/backend/src/file/file.service.ts b/apps/backend/src/file/file.service.ts index b8c7c82f..8385a57e 100644 --- a/apps/backend/src/file/file.service.ts +++ b/apps/backend/src/file/file.service.ts @@ -14,7 +14,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; export class FileService { private readonly logger = new Logger(FileService.name); private s3Client: S3Client; - private region: string; + private region : string; constructor( @Inject('S3_BUCKET_SONGS') diff --git a/apps/backend/src/lib/parseToken.spec.ts b/apps/backend/src/lib/parseToken.spec.ts index 90408751..83e4fd7d 100644 --- a/apps/backend/src/lib/parseToken.spec.ts +++ b/apps/backend/src/lib/parseToken.spec.ts @@ -1,6 +1,5 @@ import { ExecutionContext } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; - import { AuthService } from '@server/auth/auth.service'; import { ParseTokenPipe } from './parseToken'; diff --git a/apps/backend/src/mailing/mailing.service.ts b/apps/backend/src/mailing/mailing.service.ts index e73a53d7..b8ebdf19 100644 --- a/apps/backend/src/mailing/mailing.service.ts +++ b/apps/backend/src/mailing/mailing.service.ts @@ -2,8 +2,8 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { MailerService } from '@nestjs-modules/mailer'; interface EmailOptions { - to: string; - subject: string; + to : string; + subject : string; template: string; context: { [name: string]: any; diff --git a/apps/backend/src/seed/seed.service.spec.ts b/apps/backend/src/seed/seed.service.spec.ts index 93fd575a..3f5bd236 100644 --- a/apps/backend/src/seed/seed.service.spec.ts +++ b/apps/backend/src/seed/seed.service.spec.ts @@ -1,5 +1,4 @@ import { Test, TestingModule } from '@nestjs/testing'; - import { SongService } from '@server/song/song.service'; import { UserService } from '@server/user/user.service'; diff --git a/apps/backend/src/song-browser/song-browser.service.spec.ts b/apps/backend/src/song-browser/song-browser.service.spec.ts index a2bc2e3e..98a50ad4 100644 --- a/apps/backend/src/song-browser/song-browser.service.spec.ts +++ b/apps/backend/src/song-browser/song-browser.service.spec.ts @@ -1,7 +1,6 @@ import { PageQueryDTO, SongPreviewDto, SongWithUser } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; - import { SongService } from '@server/song/song.service'; import { SongBrowserService } from './song-browser.service'; diff --git a/apps/backend/src/song/song-upload/song-upload.service.spec.ts b/apps/backend/src/song/song-upload/song-upload.service.spec.ts index 56758886..72c8bdf0 100644 --- a/apps/backend/src/song/song-upload/song-upload.service.spec.ts +++ b/apps/backend/src/song/song-upload/song-upload.service.spec.ts @@ -10,10 +10,9 @@ import { } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Types } from 'mongoose'; - import { FileService } from '@server/file/file.service'; import { UserService } from '@server/user/user.service'; +import { Types } from 'mongoose'; import { SongUploadService } from './song-upload.service'; diff --git a/apps/backend/src/song/song-upload/song-upload.service.ts b/apps/backend/src/song/song-upload/song-upload.service.ts index cfb9a51d..4d354f0b 100644 --- a/apps/backend/src/song/song-upload/song-upload.service.ts +++ b/apps/backend/src/song/song-upload/song-upload.service.ts @@ -31,7 +31,7 @@ import { generateSongId, removeExtraSpaces } from '../song.util'; @Injectable() export class SongUploadService { private soundsMapping: Record; - private soundsSubset: Set; + private soundsSubset : Set; // TODO: move all upload auxiliary methods to new UploadSongService private readonly logger = new Logger(SongUploadService.name); diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts index 611aee31..20e80154 100644 --- a/apps/backend/src/song/song.controller.spec.ts +++ b/apps/backend/src/song/song.controller.spec.ts @@ -1,11 +1,5 @@ import type { UserDocument } from '@nbw/database'; -import { - PageQueryDTO, - SongPreviewDto, - SongViewDto, - UploadSongDto, - UploadSongResponseDto -} from '@nbw/database'; +import { PageQueryDTO, SongPreviewDto, SongViewDto, UploadSongDto, UploadSongResponseDto } from '@nbw/database'; import { HttpStatus, UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Test, TestingModule } from '@nestjs/testing'; @@ -13,6 +7,7 @@ import { Response } from 'express'; import { FileService } from '@server/file/file.service'; + import { SongController } from './song.controller'; import { SongService } from './song.service'; diff --git a/apps/backend/src/song/song.service.ts b/apps/backend/src/song/song.service.ts index 2529849b..c8ad1aee 100644 --- a/apps/backend/src/song/song.service.ts +++ b/apps/backend/src/song/song.service.ts @@ -347,7 +347,7 @@ export class SongService { user }: { query: PageQueryDTO; - user: UserDocument; + user : UserDocument; }): Promise { const page = parseInt(query.page?.toString() ?? '1'); const limit = parseInt(query.limit?.toString() ?? '10'); diff --git a/apps/frontend/src/lib/posts.ts b/apps/frontend/src/lib/posts.ts index ace540c1..10e9485c 100644 --- a/apps/frontend/src/lib/posts.ts +++ b/apps/frontend/src/lib/posts.ts @@ -4,13 +4,13 @@ import path from 'path'; import matter from 'gray-matter'; export type PostType = { - id: string; - title: string; - shortTitle?: string; - date: Date; - image: string; - content: string; - author?: string; + id : string; + title : string; + shortTitle? : string; + date : Date; + image : string; + content : string; + author? : string; authorImage?: string; }; diff --git a/apps/frontend/src/modules/auth/types/User.ts b/apps/frontend/src/modules/auth/types/User.ts index 9ed24892..ef7c5418 100644 --- a/apps/frontend/src/modules/auth/types/User.ts +++ b/apps/frontend/src/modules/auth/types/User.ts @@ -1,26 +1,26 @@ export type UserTokenData = { - id: string; - email: string; + id : string; + email : string; username: string; }; export type LoggedUserData = { - loginStreak: number; - loginCount: number; - playCount: number; - username: string; - publicName: string; - email: string; - description: string; - profileImage: string; - socialLinks: SocialLinks; + loginStreak : number; + loginCount : number; + playCount : number; + username : string; + publicName : string; + email : string; + description : string; + profileImage : string; + socialLinks : SocialLinks; prefersDarkTheme: boolean; - creationDate: string; - lastEdited: string; - lastLogin: string; - createdAt: string; - updatedAt: string; - id: string; + creationDate : string; + lastEdited : string; + lastLogin : string; + createdAt : string; + updatedAt : string; + id : string; }; export enum SocialLinksTypes { @@ -39,7 +39,7 @@ export enum SocialLinksTypes { THREADS = 'threads', TWITCH = 'twitch', X = 'x', - YOUTUBE = 'youtube', + YOUTUBE = 'youtube' } export type SocialLinks = { @@ -47,11 +47,11 @@ export type SocialLinks = { }; export type UserProfileData = { - lastLogin: Date; - loginStreak: number; - playCount: number; - publicName: string; - description: string; + lastLogin : Date; + loginStreak : number; + playCount : number; + publicName : string; + description : string; profileImage: string; - socialLinks: SocialLinks; + socialLinks : SocialLinks; }; diff --git a/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx b/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx index 2de1c892..1b83897e 100644 --- a/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx +++ b/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx @@ -13,11 +13,11 @@ import { import { useRecentSongsProvider } from './context/RecentSongs.context'; type CategoryButtonProps = { - children: React.ReactNode; - isActive: boolean; + children : React.ReactNode; + isActive : boolean; isDisabled?: boolean; - onClick: (e: React.MouseEvent) => void; - id: string; + onClick : (e: React.MouseEvent) => void; + id : string; }; export const CategoryButtonGroup = () => { diff --git a/apps/frontend/src/modules/browse/components/client/TimespanButton.tsx b/apps/frontend/src/modules/browse/components/client/TimespanButton.tsx index 15625493..e92202aa 100644 --- a/apps/frontend/src/modules/browse/components/client/TimespanButton.tsx +++ b/apps/frontend/src/modules/browse/components/client/TimespanButton.tsx @@ -9,11 +9,11 @@ import { import { useFeaturedSongsProvider } from './context/FeaturedSongs.context'; interface TimespanButtonProps { - children: React.ReactNode; - isActive: boolean; + children : React.ReactNode; + isActive : boolean; isDisabled: boolean; - onClick: (e: React.MouseEvent) => void; - id: string; + onClick : (e: React.MouseEvent) => void; + id : string; } export const TimespanButtonGroup = () => { diff --git a/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx b/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx index 46ecc74b..79988d52 100644 --- a/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx +++ b/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx @@ -5,16 +5,16 @@ import { createContext, useContext, useEffect, useState } from 'react'; type FeaturedSongsContextType = { featuredSongsPage: SongPreviewDtoType[]; - timespan: TimespanType; - setTimespan: (timespan: TimespanType) => void; - timespanEmpty: Record; + timespan : TimespanType; + setTimespan : (timespan: TimespanType) => void; + timespanEmpty : Record; }; const FeaturedSongsContext = createContext( {} as FeaturedSongsContextType ); -export function FeaturedSongsProvider({ children, initialFeaturedSongs }: { children: React.ReactNode; initialFeaturedSongs: FeaturedSongsDtoType;}) { +export function FeaturedSongsProvider({ children, initialFeaturedSongs }: { children: React.ReactNode; initialFeaturedSongs: FeaturedSongsDtoType; }) { // Featured songs const [featuredSongs] = useState(initialFeaturedSongs); diff --git a/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx b/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx index abad6bb3..04effa29 100644 --- a/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx +++ b/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx @@ -17,8 +17,8 @@ export function HomePageProvider({ initialRecentSongs, initialFeaturedSongs }: { - children: React.ReactNode; - initialRecentSongs: SongPreviewDtoType[]; + children : React.ReactNode; + initialRecentSongs : SongPreviewDtoType[]; initialFeaturedSongs: FeaturedSongsDtoType; }) { return ( diff --git a/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx b/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx index d2650af4..a4643ce2 100644 --- a/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx +++ b/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx @@ -12,13 +12,13 @@ import { import axiosInstance from '@web/lib/axios'; type RecentSongsContextType = { - recentSongs: (SongPreviewDtoType | null)[]; - recentError: string; - increasePageRecent: () => Promise; - isLoading: boolean; - hasMore: boolean; - selectedCategory: string; - categories: Record; + recentSongs : (SongPreviewDtoType | null)[]; + recentError : string; + increasePageRecent : () => Promise; + isLoading : boolean; + hasMore : boolean; + selectedCategory : string; + categories : Record; setSelectedCategory: (category: string) => void; }; @@ -30,7 +30,7 @@ export function RecentSongsProvider({ children, initialRecentSongs }: { - children: React.ReactNode; + children : React.ReactNode; initialRecentSongs: SongPreviewDtoType[]; }) { // Recent songs diff --git a/apps/frontend/src/modules/my-songs/components/client/DeleteConfirmDialog.tsx b/apps/frontend/src/modules/my-songs/components/client/DeleteConfirmDialog.tsx index bf53b3cf..cf42afcc 100644 --- a/apps/frontend/src/modules/my-songs/components/client/DeleteConfirmDialog.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/DeleteConfirmDialog.tsx @@ -6,9 +6,9 @@ export default function DeleteConfirmDialog({ songTitle, onConfirm }: { - isOpen: boolean; + isOpen : boolean; setIsOpen: (value: boolean) => void; - songId: string; + songId : string; songTitle: string; onConfirm: () => void; }) { diff --git a/apps/frontend/src/modules/my-songs/components/client/MySongsButtons.tsx b/apps/frontend/src/modules/my-songs/components/client/MySongsButtons.tsx index efdb4ca1..c2476ab5 100644 --- a/apps/frontend/src/modules/my-songs/components/client/MySongsButtons.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/MySongsButtons.tsx @@ -20,7 +20,7 @@ export const DownloadSongButton = ({ }: { song: { publicId: string; - title: string; + title : string; }; }) => { return ( diff --git a/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx b/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx index bc0ec47a..6862be9e 100644 --- a/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx @@ -42,7 +42,7 @@ const SongRows = ({ page, pageSize }: { - page: SongPageDtoType | null; + page : SongPageDtoType | null; pageSize: number; }) => { const maxPage = MY_SONGS.PAGE_SIZE; diff --git a/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx b/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx index 19943ced..e2cef96d 100644 --- a/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx @@ -19,21 +19,21 @@ import axiosInstance from '@web/lib/axios'; import { getTokenLocal } from '@web/lib/axios/token.utils'; type MySongsContextType = { - page: SongPageDtoType | null; - nextpage: () => void; - prevpage: () => void; - gotoPage: (page: number) => void; - totalSongs: number; - totalPages: number; - currentPage: number; - pageSize: number; - isLoading: boolean; - error: string | null; - isDeleteDialogOpen: boolean; + page : SongPageDtoType | null; + nextpage : () => void; + prevpage : () => void; + gotoPage : (page: number) => void; + totalSongs : number; + totalPages : number; + currentPage : number; + pageSize : number; + isLoading : boolean; + error : string | null; + isDeleteDialogOpen : boolean; setIsDeleteDialogOpen: (isOpen: boolean) => void; - songToDelete: SongPreviewDtoType | null; - setSongToDelete: (song: SongPreviewDtoType) => void; - deleteSong: () => void; + songToDelete : SongPreviewDtoType | null; + setSongToDelete : (song: SongPreviewDtoType) => void; + deleteSong : () => void; }; const MySongsContext = createContext( @@ -42,10 +42,10 @@ const MySongsContext = createContext( type MySongProviderProps = { InitialsongsFolder?: SongsFolder; - children?: React.ReactNode; - totalPagesInit?: number; - currentPageInit?: number; - pageSizeInit?: number; + children? : React.ReactNode; + totalPagesInit? : number; + currentPageInit? : number; + pageSizeInit? : number; }; export const MySongProvider = ({ diff --git a/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx b/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx index a38c373a..554c195d 100644 --- a/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx +++ b/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx @@ -8,10 +8,10 @@ export const NoteBlockWorldLogo = ({ glow, className }: { - size: number; + size : number; orientation: 'horizontal' | 'vertical' | 'adaptive'; - glow?: boolean; - className?: string; + glow? : boolean; + className? : string; }) => { let flexConfig, marginConfig; diff --git a/apps/frontend/src/modules/shared/components/TeamMemberCard.tsx b/apps/frontend/src/modules/shared/components/TeamMemberCard.tsx index 75f32fce..9b284f54 100644 --- a/apps/frontend/src/modules/shared/components/TeamMemberCard.tsx +++ b/apps/frontend/src/modules/shared/components/TeamMemberCard.tsx @@ -9,9 +9,9 @@ export const TeamMemberCard = ({ img, children }: { - name: string; - github: string; - img: string; + name : string; + github : string; + img : string; children: string; }) => { return ( diff --git a/apps/frontend/src/modules/shared/components/client/Carousel.tsx b/apps/frontend/src/modules/shared/components/client/Carousel.tsx index 99e6254e..9d209a84 100644 --- a/apps/frontend/src/modules/shared/components/client/Carousel.tsx +++ b/apps/frontend/src/modules/shared/components/client/Carousel.tsx @@ -28,17 +28,17 @@ type CarouselOptions = UseCarouselParameters[0]; type CarouselPlugin = UseCarouselParameters[1]; type CarouselProps = { - opts?: CarouselOptions; - plugins?: CarouselPlugin; + opts? : CarouselOptions; + plugins? : CarouselPlugin; orientation?: 'horizontal' | 'vertical'; - setApi?: (api: CarouselApi) => void; + setApi? : (api: CarouselApi) => void; }; type CarouselContextProps = { - carouselRef: ReturnType[0]; - api: ReturnType[1]; - scrollPrev: () => void; - scrollNext: () => void; + carouselRef : ReturnType[0]; + api : ReturnType[1]; + scrollPrev : () => void; + scrollNext : () => void; canScrollPrev: boolean; canScrollNext: boolean; } & CarouselProps; @@ -351,8 +351,8 @@ export const CarouselNextSmall = forwardRef< CarouselNextSmall.displayName = 'CarouselNextSmall'; type UseDotButtonType = { - selectedIndex: number; - scrollSnaps: number[]; + selectedIndex : number; + scrollSnaps : number[]; onDotButtonClick: (index: number) => void; }; diff --git a/apps/frontend/src/modules/shared/components/client/FormElements.tsx b/apps/frontend/src/modules/shared/components/client/FormElements.tsx index d77d2a43..48ca3f7e 100644 --- a/apps/frontend/src/modules/shared/components/client/FormElements.tsx +++ b/apps/frontend/src/modules/shared/components/client/FormElements.tsx @@ -13,7 +13,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; export const Label = forwardRef< HTMLLabelElement, React.InputHTMLAttributes & { - id?: string; + id? : string; label: string; } >((props, ref) => { @@ -34,11 +34,11 @@ export const Area = ({ className, children }: { - label?: string; - tooltip?: React.ReactNode; + label? : string; + tooltip? : React.ReactNode; isLoading?: boolean; className?: string; - children: React.ReactNode; + children : React.ReactNode; }) => { return ( <> @@ -81,11 +81,11 @@ const InfoTooltip = ({ children }: { children: React.ReactNode }) => { export const Input = forwardRef< HTMLInputElement, React.InputHTMLAttributes & { - id: string; - label: string; - tooltip?: React.ReactNode; - description?: string; - isLoading?: boolean; + id : string; + label : string; + tooltip? : React.ReactNode; + description? : string; + isLoading? : boolean; errorMessage?: string; } >((props, ref) => { @@ -121,10 +121,10 @@ Input.displayName = 'Input'; export const TextArea = forwardRef< HTMLTextAreaElement, React.InputHTMLAttributes & { - id: string; - label: string; - tooltip?: React.ReactNode; - isLoading?: boolean; + id : string; + label : string; + tooltip? : React.ReactNode; + isLoading? : boolean; errorMessage?: string; } >((props, ref) => { @@ -155,12 +155,12 @@ TextArea.displayName = 'TextArea'; export const Select = forwardRef< HTMLSelectElement, React.SelectHTMLAttributes & { - id: string; - label?: string; - tooltip?: React.ReactNode; - isLoading?: boolean; + id : string; + label? : string; + tooltip? : React.ReactNode; + isLoading? : boolean; errorMessage?: string; - description?: string; + description? : string; } >((props, ref) => { const { id, label, isLoading, errorMessage, description, ...rest } = props; @@ -200,7 +200,7 @@ export const Checkbox = forwardRef< HTMLInputElement, React.InputHTMLAttributes & { errorMessage?: string; - tooltip?: React.ReactNode; + tooltip? : React.ReactNode; } >((props, ref) => { const { errorMessage, tooltip, ...rest } = props; diff --git a/apps/frontend/src/modules/shared/components/client/GenericModal.tsx b/apps/frontend/src/modules/shared/components/client/GenericModal.tsx index 3ed69dc4..6169d467 100644 --- a/apps/frontend/src/modules/shared/components/client/GenericModal.tsx +++ b/apps/frontend/src/modules/shared/components/client/GenericModal.tsx @@ -6,10 +6,10 @@ import { Dialog, Transition } from '@headlessui/react'; import { Fragment } from 'react'; interface GenericModalProps { - isOpen: boolean; + isOpen : boolean; setIsOpen?: (isOpen: boolean) => void; - title: string; - children?: React.ReactNode | React.ReactNode[] | string; + title : string; + children? : React.ReactNode | React.ReactNode[] | string; } const GenericModal = ({ diff --git a/apps/frontend/src/modules/shared/components/client/ads/AdSlots.tsx b/apps/frontend/src/modules/shared/components/client/ads/AdSlots.tsx index 7ab4f52a..92c4c9f9 100644 --- a/apps/frontend/src/modules/shared/components/client/ads/AdSlots.tsx +++ b/apps/frontend/src/modules/shared/components/client/ads/AdSlots.tsx @@ -42,13 +42,13 @@ const AdTemplate = ({ hiddenClassName = 'hidden', showCloseButton = true }: { - className: string; - adSlot: string; - adFormat: string; - adLayoutKey?: string; + className : string; + adSlot : string; + adFormat : string; + adLayoutKey? : string; fullWidthResponsive?: string; - hiddenClassName?: string; - showCloseButton?: boolean; + hiddenClassName? : string; + showCloseButton? : boolean; }) => { const pubId = useAdSenseClient(); diff --git a/apps/frontend/src/modules/shared/components/layout/BlockTab.tsx b/apps/frontend/src/modules/shared/components/layout/BlockTab.tsx index 18ee4f31..e4847565 100644 --- a/apps/frontend/src/modules/shared/components/layout/BlockTab.tsx +++ b/apps/frontend/src/modules/shared/components/layout/BlockTab.tsx @@ -12,9 +12,9 @@ export const BlockTab = ({ label, className }: { - href: string; - icon: IconDefinition; - label: string; + href : string; + icon : IconDefinition; + label : string; className?: string; }) => { return ( diff --git a/apps/frontend/src/modules/shared/components/layout/NavLinks.tsx b/apps/frontend/src/modules/shared/components/layout/NavLinks.tsx index 2b7d3e23..ef54a47c 100644 --- a/apps/frontend/src/modules/shared/components/layout/NavLinks.tsx +++ b/apps/frontend/src/modules/shared/components/layout/NavLinks.tsx @@ -9,7 +9,7 @@ export function NavLinks({ userData }: { isUserLoggedIn: boolean; - userData?: LoggedUserData; + userData? : LoggedUserData; }) { return (
diff --git a/apps/frontend/src/modules/shared/components/layout/SongThumbnail.tsx b/apps/frontend/src/modules/shared/components/layout/SongThumbnail.tsx index f12ac316..3fa46fae 100644 --- a/apps/frontend/src/modules/shared/components/layout/SongThumbnail.tsx +++ b/apps/frontend/src/modules/shared/components/layout/SongThumbnail.tsx @@ -6,7 +6,7 @@ const SongThumbnail = ({ src, fallbackSrc = '/demo.png' }: { - src: string; + src : string; fallbackSrc?: string; }) => { return ( diff --git a/apps/frontend/src/modules/shared/components/layout/UserMenuLink.tsx b/apps/frontend/src/modules/shared/components/layout/UserMenuLink.tsx index a7eb628b..9b8da74a 100644 --- a/apps/frontend/src/modules/shared/components/layout/UserMenuLink.tsx +++ b/apps/frontend/src/modules/shared/components/layout/UserMenuLink.tsx @@ -18,10 +18,10 @@ const UserMenuLink = ({ external = false, textColor = 'text-white' }: { - icon: IconDefinition; - href: string; - label: string; - external?: boolean; + icon : IconDefinition; + href : string; + label : string; + external? : boolean; textColor?: string; }) => ( diff --git a/apps/frontend/src/modules/song-edit/components/client/SongEditForm.tsx b/apps/frontend/src/modules/song-edit/components/client/SongEditForm.tsx index 4eadba9d..11490975 100644 --- a/apps/frontend/src/modules/song-edit/components/client/SongEditForm.tsx +++ b/apps/frontend/src/modules/song-edit/components/client/SongEditForm.tsx @@ -10,7 +10,7 @@ import { useEditSongProviderType } from './context/EditSong.context'; type SongEditFormProps = { songData: UploadSongDtoType; - songId: string; + songId : string; username: string; }; diff --git a/apps/frontend/src/modules/song-edit/components/client/context/EditSong.context.tsx b/apps/frontend/src/modules/song-edit/components/client/context/EditSong.context.tsx index 7ee841a2..f1569410 100644 --- a/apps/frontend/src/modules/song-edit/components/client/context/EditSong.context.tsx +++ b/apps/frontend/src/modules/song-edit/components/client/context/EditSong.context.tsx @@ -14,17 +14,17 @@ import { getTokenLocal } from '@web/lib/axios/token.utils'; import { EditSongForm, editSongFormSchema } from '../../../../song/components/client/SongForm.zod'; export type useEditSongProviderType = { - formMethods: UseFormReturn; - submitSong: () => void; - register: UseFormRegister; - errors: FieldErrors; - song: SongFileType | null; - instrumentSounds: string[]; + formMethods : UseFormReturn; + submitSong : () => void; + register : UseFormRegister; + errors : FieldErrors; + song : SongFileType | null; + instrumentSounds : string[]; setInstrumentSound: (index: number, value: string) => void; - sendError: string | null; - isSubmitting: boolean; - loadSong: (id: string, username: string, song: UploadSongDtoType) => void; - setSongId: (id: string) => void; + sendError : string | null; + isSubmitting : boolean; + loadSong : (id: string, username: string, song: UploadSongDtoType) => void; + setSongId : (id: string) => void; }; export const EditSongContext = createContext( diff --git a/apps/frontend/src/modules/song-upload/components/client/context/UploadSong.context.tsx b/apps/frontend/src/modules/song-upload/components/client/context/UploadSong.context.tsx index aa0f13fd..e9637f61 100644 --- a/apps/frontend/src/modules/song-upload/components/client/context/UploadSong.context.tsx +++ b/apps/frontend/src/modules/song-upload/components/client/context/UploadSong.context.tsx @@ -14,30 +14,26 @@ import { UploadSongForm, uploadSongFormSchema } from '../../../../song/component import UploadCompleteModal from '../UploadCompleteModal'; export type useUploadSongProviderType = { - song: SongFileType | null; - filename: string | null; - setFile: (file: File | null) => void; - instrumentSounds: string[]; + song : SongFileType | null; + filename : string | null; + setFile : (file: File | null) => void; + instrumentSounds : string[]; setInstrumentSound: (index: number, value: string) => void; - formMethods: UseFormReturn; - submitSong: () => void; - register: UseFormRegister; - errors: FieldErrors; - sendError: string | null; - isSubmitting: boolean; - isUploadComplete: boolean; - uploadedSongId: string | null; + formMethods : UseFormReturn; + submitSong : () => void; + register : UseFormRegister; + errors : FieldErrors; + sendError : string | null; + isSubmitting : boolean; + isUploadComplete : boolean; + uploadedSongId : string | null; }; export const UploadSongContext = createContext( null as unknown as useUploadSongProviderType ); -export const UploadSongProvider = ({ - children -}: { - children: React.ReactNode; -}) => { +export const UploadSongProvider = ({ children }: { children: React.ReactNode; }) => { const [song, setSong] = useState(null); const [filename, setFilename] = useState(null); const [instrumentSounds, setInstrumentSounds] = useState([]); diff --git a/apps/frontend/src/modules/song/components/SongPageButtons.tsx b/apps/frontend/src/modules/song/components/SongPageButtons.tsx index beb53acd..4223ef20 100644 --- a/apps/frontend/src/modules/song/components/SongPageButtons.tsx +++ b/apps/frontend/src/modules/song/components/SongPageButtons.tsx @@ -161,7 +161,7 @@ const OpenInNBSButton = ({ isLoading, handleClick }: { - isLoading: boolean; + isLoading : boolean; handleClick: React.MouseEventHandler; }) => { return ( @@ -247,7 +247,7 @@ const DownloadButton = ({ handleClick }: { downloadCount: number; - handleClick: React.MouseEventHandler; + handleClick : React.MouseEventHandler; }) => { const [isLoggedIn, setIsLoggedIn] = useState(true); diff --git a/apps/frontend/src/modules/song/components/client/DownloadSongModal.tsx b/apps/frontend/src/modules/song/components/client/DownloadSongModal.tsx index fa5af11d..40495a13 100644 --- a/apps/frontend/src/modules/song/components/client/DownloadSongModal.tsx +++ b/apps/frontend/src/modules/song/components/client/DownloadSongModal.tsx @@ -11,9 +11,9 @@ export default function DownloadSongModal({ setIsOpen, song }: { - isOpen: boolean; + isOpen : boolean; setIsOpen: (isOpen: boolean) => void; - song: SongViewDtoType; + song : SongViewDtoType; }) { const [isCopied, setIsCopied] = useState(false); diff --git a/apps/frontend/src/modules/song/components/client/FileDisplay.tsx b/apps/frontend/src/modules/song/components/client/FileDisplay.tsx index b72e9352..1e91ab5b 100644 --- a/apps/frontend/src/modules/song/components/client/FileDisplay.tsx +++ b/apps/frontend/src/modules/song/components/client/FileDisplay.tsx @@ -12,8 +12,8 @@ export const FileDisplay = ({ children, className }: { - fileName: string; - children: React.ReactNode; + fileName : string; + children : React.ReactNode; className?: string; }) => (
{ return ( diff --git a/apps/frontend/src/modules/song/components/client/InstrumentPicker.tsx b/apps/frontend/src/modules/song/components/client/InstrumentPicker.tsx index 4df8f42a..3f807bb9 100644 --- a/apps/frontend/src/modules/song/components/client/InstrumentPicker.tsx +++ b/apps/frontend/src/modules/song/components/client/InstrumentPicker.tsx @@ -14,7 +14,7 @@ const InstrumentTableHeader = ({ children }: { className?: string; - children: React.ReactNode; + children : React.ReactNode; }) => { return (
{ return (
{ return (
diff --git a/apps/frontend/src/modules/song/components/client/ShareModal.tsx b/apps/frontend/src/modules/song/components/client/ShareModal.tsx index 8d06cb29..f0ca3990 100644 --- a/apps/frontend/src/modules/song/components/client/ShareModal.tsx +++ b/apps/frontend/src/modules/song/components/client/ShareModal.tsx @@ -23,9 +23,9 @@ export default function ShareModal({ setIsOpen, songId }: { - isOpen: boolean; + isOpen : boolean; setIsOpen: (isOpen: boolean) => void; - songId: string; + songId : string; }) { const [isCopied, setIsCopied] = useState(false); diff --git a/apps/frontend/src/modules/song/components/client/SongForm.tsx b/apps/frontend/src/modules/song/components/client/SongForm.tsx index cfc3bbd5..6b9d1a7d 100644 --- a/apps/frontend/src/modules/song/components/client/SongForm.tsx +++ b/apps/frontend/src/modules/song/components/client/SongForm.tsx @@ -27,9 +27,9 @@ import InstrumentPicker from './InstrumentPicker'; import { SongThumbnailInput } from './SongThumbnailInput'; type SongFormProps = { - type: 'upload' | 'edit'; + type : 'upload' | 'edit'; isLoading?: boolean; - isLocked?: boolean; + isLocked? : boolean; }; export const SongForm = ({ diff --git a/apps/frontend/src/modules/song/components/client/SongSearchCombo.tsx b/apps/frontend/src/modules/song/components/client/SongSearchCombo.tsx index 2c928bc9..b9c1ec3a 100644 --- a/apps/frontend/src/modules/song/components/client/SongSearchCombo.tsx +++ b/apps/frontend/src/modules/song/components/client/SongSearchCombo.tsx @@ -25,10 +25,10 @@ export function SongSearchCombo({ sounds, locked }: { - value: string; + value : string; setValue: (value: string) => void; - sounds: string[]; - locked: boolean; + sounds : string[]; + locked : boolean; }) { const [open, setOpen] = React.useState(false); const [searchTerm, setSearchTerm] = React.useState(''); diff --git a/apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx b/apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx index cad8a4c3..f92ed22e 100644 --- a/apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx +++ b/apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx @@ -26,9 +26,9 @@ function ThumbnailSliders({ maxLayer }: { formMethods: UseFormReturn & UseFormReturn; - isLocked: boolean; - maxTick: number; - maxLayer: number; + isLocked : boolean; + maxTick : number; + maxLayer : number; }) { const { register } = formMethods; @@ -104,11 +104,11 @@ const ColorButton = ({ onClick, disabled }: { - color: string; + color : string; tooltip: string; - active: boolean; + active : boolean; - onClick: (e: React.MouseEvent) => void; + onClick : (e: React.MouseEvent) => void; disabled: boolean; }) => ( @@ -132,7 +132,7 @@ export const SongThumbnailInput = ({ type, isLocked }: { - type: 'upload' | 'edit'; + type : 'upload' | 'edit'; isLocked: boolean; }) => { const { song, formMethods } = useSongProvider(type); diff --git a/apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx b/apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx index 093376f2..3f95c5e2 100644 --- a/apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx +++ b/apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx @@ -6,7 +6,7 @@ import { UseFormReturn } from 'react-hook-form'; import { UploadSongForm } from './SongForm.zod'; type ThumbnailRendererCanvasProps = { - notes: NoteQuadTree; + notes : NoteQuadTree; formMethods: UseFormReturn; }; diff --git a/apps/frontend/src/modules/song/util/downloadSong.ts b/apps/frontend/src/modules/song/util/downloadSong.ts index 5badbd48..54840deb 100644 --- a/apps/frontend/src/modules/song/util/downloadSong.ts +++ b/apps/frontend/src/modules/song/util/downloadSong.ts @@ -7,7 +7,7 @@ import { getTokenLocal } from '@web/lib/axios/token.utils'; export const downloadSongFile = async (song: { publicId: string; - title: string; + title : string; }) => { const token = getTokenLocal(); From 8a108599e0b9264426580f3a907b7f2259fae162 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 10:47:53 -0300 Subject: [PATCH 06/21] lint commit --- .../src/song/dto/FeaturedSongsDto.dto.ts | 10 ++-- .../database/src/song/dto/SongPreview.dto.ts | 2 +- .../database/src/song/dto/SongView.dto.ts | 2 +- packages/database/src/user/dto/user.dto.ts | 4 +- .../database/src/user/entity/user.entity.ts | 30 +++++----- packages/song/src/obfuscate.ts | 4 +- packages/song/src/stats.ts | 18 +++--- packages/song/src/types.ts | 28 ++++----- packages/sounds/src/types.ts | 60 +++++++++---------- packages/sounds/src/util.ts | 2 +- packages/thumbnail/src/types.ts | 20 +++---- 11 files changed, 90 insertions(+), 90 deletions(-) diff --git a/packages/database/src/song/dto/FeaturedSongsDto.dto.ts b/packages/database/src/song/dto/FeaturedSongsDto.dto.ts index e9b8c221..346df4db 100644 --- a/packages/database/src/song/dto/FeaturedSongsDto.dto.ts +++ b/packages/database/src/song/dto/FeaturedSongsDto.dto.ts @@ -1,12 +1,12 @@ import { SongPreviewDto } from './SongPreview.dto'; export class FeaturedSongsDto { - hour: SongPreviewDto[]; - day: SongPreviewDto[]; - week: SongPreviewDto[]; + hour : SongPreviewDto[]; + day : SongPreviewDto[]; + week : SongPreviewDto[]; month: SongPreviewDto[]; - year: SongPreviewDto[]; - all: SongPreviewDto[]; + year : SongPreviewDto[]; + all : SongPreviewDto[]; public static create(): FeaturedSongsDto { return { diff --git a/packages/database/src/song/dto/SongPreview.dto.ts b/packages/database/src/song/dto/SongPreview.dto.ts index 6eb8a056..6cdea5d1 100644 --- a/packages/database/src/song/dto/SongPreview.dto.ts +++ b/packages/database/src/song/dto/SongPreview.dto.ts @@ -3,7 +3,7 @@ import { IsNotEmpty, IsString, IsUrl, MaxLength } from 'class-validator'; import type { SongWithUser } from '@database/song/entity/song.entity'; type SongPreviewUploader = { - username: string; + username : string; profileImage: string; }; diff --git a/packages/database/src/song/dto/SongView.dto.ts b/packages/database/src/song/dto/SongView.dto.ts index 0349468d..fb3438e3 100644 --- a/packages/database/src/song/dto/SongView.dto.ts +++ b/packages/database/src/song/dto/SongView.dto.ts @@ -13,7 +13,7 @@ import type { SongDocument } from '@database/song/entity/song.entity'; import type { CategoryType, LicenseType, VisibilityType } from './types'; export type SongViewUploader = { - username: string; + username : string; profileImage: string; }; diff --git a/packages/database/src/user/dto/user.dto.ts b/packages/database/src/user/dto/user.dto.ts index 4d6596c7..413ddede 100644 --- a/packages/database/src/user/dto/user.dto.ts +++ b/packages/database/src/user/dto/user.dto.ts @@ -1,9 +1,9 @@ import { User } from '../entity/user.entity'; export class UserDto { - username: string; + username : string; publicName: string; - email: string; + email : string; static fromEntity(user: User): UserDto { const userDto: UserDto = { username : user.username, diff --git a/packages/database/src/user/entity/user.entity.ts b/packages/database/src/user/entity/user.entity.ts index 19a19c7d..20e58e9e 100644 --- a/packages/database/src/user/entity/user.entity.ts +++ b/packages/database/src/user/entity/user.entity.ts @@ -4,22 +4,22 @@ import { Schema as MongooseSchema } from 'mongoose'; @Schema({}) class SocialLinks { - bandcamp?: string; - discord?: string; - facebook?: string; - github?: string; - instagram?: string; - reddit?: string; - snapchat?: string; + bandcamp? : string; + discord? : string; + facebook? : string; + github? : string; + instagram? : string; + reddit? : string; + snapchat? : string; soundcloud?: string; - spotify?: string; - steam?: string; - telegram?: string; - tiktok?: string; - threads?: string; - twitch?: string; - x?: string; - youtube?: string; + spotify? : string; + steam? : string; + telegram? : string; + tiktok? : string; + threads? : string; + twitch? : string; + x? : string; + youtube? : string; } @Schema({ diff --git a/packages/song/src/obfuscate.ts b/packages/song/src/obfuscate.ts index fedfe9b4..a13f05a2 100644 --- a/packages/song/src/obfuscate.ts +++ b/packages/song/src/obfuscate.ts @@ -3,9 +3,9 @@ import { Instrument, Layer, Note, Song } from '@encode42/nbs.js'; import { getInstrumentNoteCounts, getTempoChangerInstrumentIds } from './util'; export class SongObfuscator { - private song: Song; + private song : Song; private soundPaths: string[]; - private output: Song; + private output : Song; public static obfuscateSong(song: Song, soundPaths: string[]) { return new SongObfuscator(song, soundPaths).output; diff --git a/packages/song/src/stats.ts b/packages/song/src/stats.ts index 25db6a7e..93224557 100644 --- a/packages/song/src/stats.ts +++ b/packages/song/src/stats.ts @@ -11,7 +11,7 @@ export class SongStatsGenerator { return new SongStatsGenerator(song).toObject(); } - private song: Song; + private song : Song; private stats: SongStatsType; private constructor(song: Song) { @@ -85,14 +85,14 @@ export class SongStatsGenerator { } private getCounts(tempoChangerInstruments: number[]): { - noteCount: number; - tickCount: number; - layerCount: number; - outOfRangeNoteCount: number; - detunedNoteCount: number; + noteCount : number; + tickCount : number; + layerCount : number; + outOfRangeNoteCount : number; + detunedNoteCount : number; customInstrumentNoteCount: number; - incompatibleNoteCount: number; - instrumentNoteCounts: number[]; + incompatibleNoteCount : number; + instrumentNoteCounts : number[]; } { let noteCount = 0; let tickCount = 0; @@ -303,7 +303,7 @@ export class SongStatsGenerator { tempoChangerInstruments: number[] ): { vanillaInstrumentCount: number; - customInstrumentCount: number; + customInstrumentCount : number; } { const firstCustomIndex = this.song.instruments.firstCustomIndex; diff --git a/packages/song/src/types.ts b/packages/song/src/types.ts index 1de71613..d141c1d4 100644 --- a/packages/song/src/types.ts +++ b/packages/song/src/types.ts @@ -3,30 +3,30 @@ import { SongStats } from '@nbw/database'; import { NoteQuadTree } from './notes'; export type SongFileType = { - title: string; - description: string; - author: string; + title : string; + description : string; + author : string; originalAuthor: string; - length: number; - height: number; - arrayBuffer: ArrayBuffer; - notes: NoteQuadTree; - instruments: InstrumentArray; + length : number; + height : number; + arrayBuffer : ArrayBuffer; + notes : NoteQuadTree; + instruments : InstrumentArray; }; export type InstrumentArray = Instrument[]; export type Note = { - tick: number; - layer: number; - key: number; + tick : number; + layer : number; + key : number; instrument: number; }; export type Instrument = { - id: number; - name: string; - file: string; + id : number; + name : string; + file : string; count: number; }; diff --git a/packages/sounds/src/types.ts b/packages/sounds/src/types.ts index 73776a5b..cad1dc49 100644 --- a/packages/sounds/src/types.ts +++ b/packages/sounds/src/types.ts @@ -1,16 +1,16 @@ export type VersionSummary = { - id: string; - type: 'release' | 'snapshot' | 'old_beta' | 'old_alpha'; - url: string; - time: string; - releaseTime: string; - sha1: string; + id : string; + type : 'release' | 'snapshot' | 'old_beta' | 'old_alpha'; + url : string; + time : string; + releaseTime : string; + sha1 : string; complianceLevel: number; }; export type VersionManifest = { latest: { - release: string; + release : string; snapshot: string; }; versions: VersionSummary[]; @@ -24,14 +24,14 @@ type OS = { type Rule = { action: Action; - os: OS; + os : OS; }; type Artifact = { path: string; sha1: string; size: number; - url: string; + url : string; }; type Downloads = { @@ -40,21 +40,21 @@ type Downloads = { type Library = { downloads: Downloads; - name: string; - rules?: Rule[]; + name : string; + rules? : Rule[]; }; type File = { - id: string; + id : string; sha1: string; size: number; - url: string; + url : string; }; type Client = { argument: string; - file: File; - type: string; + file : File; + type : string; }; type Logging = { @@ -64,31 +64,31 @@ type Logging = { export type VersionData = { arguments: { game: (string | { rules: Rule[]; value: string })[]; - jvm: string[]; + jvm : string[]; }; assetIndex: { - id: string; - sha1: string; - size: number; + id : string; + sha1 : string; + size : number; totalSize: number; - url: string; + url : string; }; - assets: string; + assets : string; complianceLevel: number; downloads: { - client: Artifact; + client : Artifact; client_mappings: Artifact; - server: Artifact; + server : Artifact; server_mappings: Artifact; }; - id: string; - libraries: Library[]; - logging: Logging; - mainClass: string; + id : string; + libraries : Library[]; + logging : Logging; + mainClass : string; minimumLauncherVersion: number; - releaseTime: string; - time: string; - type: string; + releaseTime : string; + time : string; + type : string; }; export type AssetIndexRecord = Record; diff --git a/packages/sounds/src/util.ts b/packages/sounds/src/util.ts index 75cac05d..a8d82da0 100644 --- a/packages/sounds/src/util.ts +++ b/packages/sounds/src/util.ts @@ -3,7 +3,7 @@ export type RecordKey = string | number | symbol; export class TwoWayMap { - private map: Map; + private map : Map; private reverseMap: Map; constructor(map: Map) { diff --git a/packages/thumbnail/src/types.ts b/packages/thumbnail/src/types.ts index 69e0c061..0329c9f2 100644 --- a/packages/thumbnail/src/types.ts +++ b/packages/thumbnail/src/types.ts @@ -4,15 +4,15 @@ import type { NoteQuadTree } from '@nbw/song'; import { DrawingCanvas, RenderedImage } from './canvasFactory'; export interface DrawParams { - notes: NoteQuadTree; - startTick: number; - startLayer: number; - zoomLevel: number; + notes : NoteQuadTree; + startTick : number; + startLayer : number; + zoomLevel : number; backgroundColor: string; - canvasWidth?: number; - canvasHeight?: number; - imgWidth: number; - imgHeight: number; + canvasWidth? : number; + canvasHeight? : number; + imgWidth : number; + imgHeight : number; } export type Canvas = typeof DrawingCanvas; @@ -32,6 +32,6 @@ export interface CanvasUtils { useFont(): void; saveToImage(canvas: HTMLCanvasElement | NapiRs.Canvas): Promise; noteBlockImage: Promise | any; - DrawingCanvas: any; - RenderedImage: any; + DrawingCanvas : any; + RenderedImage : any; } From d367a4bb8513ee5f5d95d1a6961a186e8c6c975d Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 11:13:12 -0300 Subject: [PATCH 07/21] chore: update dependencies in package.json and bun.lock, add TypeScript ESLint support, and adjust test file inclusion in tsconfig.json --- apps/backend/package.json | 2 +- apps/backend/tsconfig.json | 6 ++-- bun.lock | 74 +++++++++++++++++++++++++++++++------- eslint.config.js | 1 - package.json | 3 +- packages/song/package.json | 5 +-- 6 files changed, 70 insertions(+), 21 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index f3861fb6..7ac45435 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -63,12 +63,12 @@ "@nbw/sounds": "workspace:*" }, "devDependencies": { + "@types/bun": "latest", "@faker-js/faker": "^9.3.0", "@nestjs/cli": "^10.4.9", "@nestjs/schematics": "^10.2.3", "@nestjs/testing": "^10.4.15", "@types/bcryptjs": "^2.4.6", - "@types/bun": "^1.2.10", "@types/express": "^4.17.21", "@types/multer": "^1.4.12", "@types/node": "^20.17.10", diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index bc4e305d..9e98024b 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -24,13 +24,13 @@ } }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts" ], "exclude": [ "node_modules", "dist", - "**/*.spec.ts", - "**/*.test.ts", "e2e/**/*", "test/**/*" ] diff --git a/bun.lock b/bun.lock index 239c74c1..3cf4b2b0 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,7 @@ "eslint-plugin-prettier": "^4.2.5", "globals": "^16.4.0", "prettier": "^2.8.8", + "typescript-eslint": "^8.44.1", }, }, "apps/backend": { @@ -74,7 +75,7 @@ "@nestjs/schematics": "^10.2.3", "@nestjs/testing": "^10.4.15", "@types/bcryptjs": "^2.4.6", - "@types/bun": "^1.2.10", + "@types/bun": "latest", "@types/express": "^4.17.21", "@types/multer": "^1.4.12", "@types/node": "^20.17.10", @@ -187,6 +188,7 @@ "@encode42/nbs.js": "^5.0.2", "@nbw/database": "workspace:*", "@timohausmann/quadtree-ts": "^2.2.2", + "jszip": "^3.10.1", "unidecode": "^1.1.0", }, "devDependencies": { @@ -1756,6 +1758,8 @@ "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], @@ -1980,6 +1984,8 @@ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + "juice": ["juice@10.0.1", "", { "dependencies": { "cheerio": "1.0.0-rc.12", "commander": "^6.1.0", "mensch": "^0.3.4", "slick": "^1.12.2", "web-resource-inliner": "^6.0.1" }, "bin": { "juice": "bin/juice" } }, "sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA=="], "jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="], @@ -2012,6 +2018,8 @@ "libqp": ["libqp@2.1.1", "", {}, "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -2374,6 +2382,8 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "param-case": ["param-case@2.1.1", "", { "dependencies": { "no-case": "^2.2.0" } }, "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -2654,6 +2664,8 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], @@ -2868,6 +2880,8 @@ "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + "typescript-eslint": ["typescript-eslint@8.44.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.44.1", "@typescript-eslint/parser": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/utils": "8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg=="], + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], @@ -3120,19 +3134,19 @@ "@mdx-js/mdx/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], - "@nbw/backend/@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@nbw/backend/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], - "@nbw/config/@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@nbw/config/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], - "@nbw/database/@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@nbw/database/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], "@nbw/frontend/eslint-plugin-mdx": ["eslint-plugin-mdx@3.1.5", "", { "dependencies": { "eslint-mdx": "^3.1.5", "eslint-plugin-markdown": "^3.0.1", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "tslib": "^2.6.2", "unified": "^11.0.4", "vfile": "^6.0.1" }, "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-lUE7tP7IrIRHU3gTtASDe5u4YM2SvQveYVJfuo82yn3MLh/B/v05FNySURCK4aIxIYF1QYo3IRemQG/lyQzpAg=="], - "@nbw/song/@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@nbw/song/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], - "@nbw/sounds/@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@nbw/sounds/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], - "@nbw/thumbnail/@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], + "@nbw/thumbnail/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], "@nbw/thumbnail/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], @@ -3902,6 +3916,14 @@ "typed-array-buffer/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.44.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/type-utils": "8.44.1", "@typescript-eslint/utils": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw=="], + + "typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw=="], + + "typescript-eslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.44.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.44.1", "@typescript-eslint/tsconfig-utils": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A=="], + + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.44.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg=="], + "unbox-primitive/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], "unified-engine/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], @@ -3988,19 +4010,19 @@ "@jest/types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], - "@nbw/backend/@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + "@nbw/backend/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], - "@nbw/config/@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + "@nbw/config/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], - "@nbw/database/@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + "@nbw/database/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], "@nbw/frontend/eslint-plugin-mdx/eslint-mdx": ["eslint-mdx@3.1.5", "", { "dependencies": { "acorn": "^8.11.3", "acorn-jsx": "^5.3.2", "espree": "^9.6.1", "estree-util-visit": "^2.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "synckit": "^0.9.0", "tslib": "^2.6.2", "unified": "^11.0.4", "unified-engine": "^11.2.0", "unist-util-visit": "^5.0.0", "uvu": "^0.5.6", "vfile": "^6.0.1" }, "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-ynztX0k7CQ3iDL7fDEIeg3g0O/d6QPv7IBI9fdYLhXp5fAp0fi8X22xF/D3+Pk0f90R27uwqa1clHpay6t0l8Q=="], - "@nbw/song/@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + "@nbw/song/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], - "@nbw/sounds/@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + "@nbw/sounds/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], - "@nbw/thumbnail/@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], + "@nbw/thumbnail/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], "@nestjs-modules/mailer/glob/jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="], @@ -4206,6 +4228,28 @@ "terser-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1" } }, "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg=="], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/typescript-estree": "8.44.1", "@typescript-eslint/utils": "8.44.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g=="], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], + + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1" } }, "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg=="], + + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.44.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.44.1", "@typescript-eslint/types": "^8.44.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1" } }, "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg=="], + "unified-engine/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "unified-engine/concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -4342,6 +4386,10 @@ "terser-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], + "vfile-reporter/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "web-resource-inliner/htmlparser2/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], diff --git a/eslint.config.js b/eslint.config.js index aca82b3e..d29d3b41 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -121,4 +121,3 @@ export default tseslint.config( }, }, ); - diff --git a/package.json b/package.json index c023723e..aec7215d 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,8 @@ "eslint-plugin-mdx": "^3.6.2", "eslint-plugin-prettier": "^4.2.5", "globals": "^16.4.0", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "typescript-eslint": "^8.44.1" }, "dependencies": { "@nbw/sounds": "workspace:*", diff --git a/packages/song/package.json b/packages/song/package.json index 549da60b..e7d40a85 100644 --- a/packages/song/package.json +++ b/packages/song/package.json @@ -30,9 +30,10 @@ }, "dependencies": { "@encode42/nbs.js": "^5.0.2", - "unidecode": "^1.1.0", + "@nbw/database": "workspace:*", "@timohausmann/quadtree-ts": "^2.2.2", - "@nbw/database": "workspace:*" + "jszip": "^3.10.1", + "unidecode": "^1.1.0" }, "peerDependencies": { "typescript": "^5" From ed455762aeba8eae34cde2de2263ec8ac836ac4c Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 11:13:25 -0300 Subject: [PATCH 08/21] Lint fixes --- apps/backend/src/auth/auth.controller.spec.ts | 2 ++ apps/backend/src/auth/auth.service.spec.ts | 3 ++- .../src/auth/strategies/JWT.strategy.spec.ts | 2 ++ .../src/auth/strategies/github.strategy.spec.ts | 2 ++ .../src/auth/strategies/google.strategy.spec.ts | 2 ++ .../strategies/magicLinkEmail.strategy.spec.ts | 8 ++++++-- apps/backend/src/lib/parseToken.spec.ts | 1 + apps/backend/src/seed/seed.service.spec.ts | 1 + .../song-browser/song-browser.service.spec.ts | 1 + .../song/song-upload/song-upload.service.spec.ts | 3 ++- .../song-webhook/song-webhook.service.spec.ts | 16 ++++++++-------- apps/backend/src/song/song.controller.spec.ts | 1 + .../database/src/common/dto/PageQuery.dto.ts | 2 +- .../database/src/song/dto/SongPreview.dto.ts | 2 +- packages/database/src/song/dto/SongView.dto.ts | 4 ++-- .../database/src/song/dto/UploadSongDto.dto.ts | 2 +- .../src/song/dto/UploadSongResponseDto.dto.ts | 2 +- packages/database/src/song/entity/song.entity.ts | 3 +-- 18 files changed, 37 insertions(+), 20 deletions(-) diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index fc863d90..06a2eb15 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -1,3 +1,5 @@ +import { describe, beforeEach, it, expect, jest } from 'bun:test'; + import { Test, TestingModule } from '@nestjs/testing'; import type { Request, Response } from 'express'; diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts index 67b10197..ee0e24e3 100644 --- a/apps/backend/src/auth/auth.service.spec.ts +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -3,9 +3,10 @@ import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; import type { UserDocument } from '@nbw/database'; import { JwtService } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; -import { UserService } from '@server/user/user.service'; import type { Request, Response } from 'express'; +import { UserService } from '@server/user/user.service'; + import { AuthService } from './auth.service'; import { Profile } from './types/profile'; diff --git a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts index c269c23e..1a9de82b 100644 --- a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts @@ -1,3 +1,5 @@ +import { describe, beforeEach, it, expect, jest } from 'bun:test'; + import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import type { Request } from 'express'; diff --git a/apps/backend/src/auth/strategies/github.strategy.spec.ts b/apps/backend/src/auth/strategies/github.strategy.spec.ts index 7101ed7b..44ecd0fb 100644 --- a/apps/backend/src/auth/strategies/github.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/github.strategy.spec.ts @@ -1,3 +1,5 @@ +import { describe, beforeEach, it, expect, jest } from 'bun:test'; + import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; diff --git a/apps/backend/src/auth/strategies/google.strategy.spec.ts b/apps/backend/src/auth/strategies/google.strategy.spec.ts index 4e563b79..6d20647c 100644 --- a/apps/backend/src/auth/strategies/google.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/google.strategy.spec.ts @@ -1,3 +1,5 @@ +import { describe, beforeEach, it, expect, jest } from 'bun:test'; + import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { VerifyCallback } from 'passport-google-oauth20'; diff --git a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts index 073bd5f3..27c7d867 100644 --- a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts @@ -1,5 +1,9 @@ +import { describe, beforeEach, it, expect, jest } from 'bun:test'; + +import { UserDocument } from '@nbw/database'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; + import { MailingService } from '@server/mailing/mailing.service'; import { UserService } from '@server/user/user.service'; @@ -128,7 +132,7 @@ describe('MagicLinkEmailStrategy', () => { const result = await strategy.validate(payload); - expect(result).toEqual(user); + expect(result).toEqual(user as UserDocument); }); it('should create a new user if not found and return the user', async () => { @@ -146,7 +150,7 @@ describe('MagicLinkEmailStrategy', () => { expect(result).toEqual({ email : 'test@example.com', username: 'test' - }); + } as UserDocument); }); }); }); diff --git a/apps/backend/src/lib/parseToken.spec.ts b/apps/backend/src/lib/parseToken.spec.ts index 83e4fd7d..90408751 100644 --- a/apps/backend/src/lib/parseToken.spec.ts +++ b/apps/backend/src/lib/parseToken.spec.ts @@ -1,5 +1,6 @@ import { ExecutionContext } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; + import { AuthService } from '@server/auth/auth.service'; import { ParseTokenPipe } from './parseToken'; diff --git a/apps/backend/src/seed/seed.service.spec.ts b/apps/backend/src/seed/seed.service.spec.ts index 3f5bd236..93fd575a 100644 --- a/apps/backend/src/seed/seed.service.spec.ts +++ b/apps/backend/src/seed/seed.service.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; + import { SongService } from '@server/song/song.service'; import { UserService } from '@server/user/user.service'; diff --git a/apps/backend/src/song-browser/song-browser.service.spec.ts b/apps/backend/src/song-browser/song-browser.service.spec.ts index 98a50ad4..a2bc2e3e 100644 --- a/apps/backend/src/song-browser/song-browser.service.spec.ts +++ b/apps/backend/src/song-browser/song-browser.service.spec.ts @@ -1,6 +1,7 @@ import { PageQueryDTO, SongPreviewDto, SongWithUser } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; + import { SongService } from '@server/song/song.service'; import { SongBrowserService } from './song-browser.service'; diff --git a/apps/backend/src/song/song-upload/song-upload.service.spec.ts b/apps/backend/src/song/song-upload/song-upload.service.spec.ts index 72c8bdf0..56758886 100644 --- a/apps/backend/src/song/song-upload/song-upload.service.spec.ts +++ b/apps/backend/src/song/song-upload/song-upload.service.spec.ts @@ -10,9 +10,10 @@ import { } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { Types } from 'mongoose'; + import { FileService } from '@server/file/file.service'; import { UserService } from '@server/user/user.service'; -import { Types } from 'mongoose'; import { SongUploadService } from './song-upload.service'; diff --git a/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts b/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts index 3f259a25..33754c14 100644 --- a/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts +++ b/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts @@ -60,9 +60,9 @@ describe('SongWebhookService', () => { (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - (global as any).fetch = jest.fn().mockResolvedValue({ + global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ id: 'message-id' }) - }); + } as unknown as Response) as unknown as typeof global.fetch; const result = await service.postSongWebhook(song); @@ -85,7 +85,7 @@ describe('SongWebhookService', () => { (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - (global as any).fetch = jest.fn().mockRejectedValue(new Error('Error')); + global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; const result = await service.postSongWebhook(song); @@ -103,7 +103,7 @@ describe('SongWebhookService', () => { (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - (global as any).fetch = jest.fn().mockResolvedValue({}); + global.fetch = jest.fn().mockResolvedValue({} as Response) as unknown as typeof global.fetch; await service.updateSongWebhook(song); @@ -128,7 +128,7 @@ describe('SongWebhookService', () => { (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - (global as any).fetch = jest.fn().mockRejectedValue(new Error('Error')); + global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; const loggerSpy = spyOn(service['logger'], 'error'); @@ -149,7 +149,7 @@ describe('SongWebhookService', () => { uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; - (global as any).fetch = jest.fn().mockResolvedValue({}); + global.fetch = jest.fn().mockResolvedValue({} as Response) as unknown as typeof global.fetch; await service.deleteSongWebhook(song); @@ -168,7 +168,7 @@ describe('SongWebhookService', () => { uploader : { username: 'testuser', profileImage: 'testimage' } } as SongWithUser; - (global as any).fetch = jest.fn().mockRejectedValue(new Error('Error')); + global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; const loggerSpy = spyOn(service['logger'], 'error'); @@ -256,7 +256,7 @@ describe('SongWebhookService', () => { const syncSpy = spyOn(service, 'syncSongWebhook'); - await (service as any).syncAllSongsWebhook(); + await service['syncAllSongsWebhook'](); expect(syncSpy).toHaveBeenCalledWith(songs[0]); }); diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts index 20e80154..5fdb156f 100644 --- a/apps/backend/src/song/song.controller.spec.ts +++ b/apps/backend/src/song/song.controller.spec.ts @@ -8,6 +8,7 @@ import { Response } from 'express'; import { FileService } from '@server/file/file.service'; + import { SongController } from './song.controller'; import { SongService } from './song.service'; diff --git a/packages/database/src/common/dto/PageQuery.dto.ts b/packages/database/src/common/dto/PageQuery.dto.ts index 66963c4c..1fd7ed6a 100644 --- a/packages/database/src/common/dto/PageQuery.dto.ts +++ b/packages/database/src/common/dto/PageQuery.dto.ts @@ -12,7 +12,7 @@ import { Min } from 'class-validator'; -import type { TimespanType } from '@database/song/dto/types'; +import type { TimespanType } from '../../song/dto/types'; export class PageQueryDTO { @Min(1) diff --git a/packages/database/src/song/dto/SongPreview.dto.ts b/packages/database/src/song/dto/SongPreview.dto.ts index 6cdea5d1..a11c4a46 100644 --- a/packages/database/src/song/dto/SongPreview.dto.ts +++ b/packages/database/src/song/dto/SongPreview.dto.ts @@ -1,6 +1,6 @@ import { IsNotEmpty, IsString, IsUrl, MaxLength } from 'class-validator'; -import type { SongWithUser } from '@database/song/entity/song.entity'; +import type { SongWithUser } from '../entity/song.entity'; type SongPreviewUploader = { username : string; diff --git a/packages/database/src/song/dto/SongView.dto.ts b/packages/database/src/song/dto/SongView.dto.ts index fb3438e3..c33f5a77 100644 --- a/packages/database/src/song/dto/SongView.dto.ts +++ b/packages/database/src/song/dto/SongView.dto.ts @@ -7,9 +7,9 @@ import { IsUrl } from 'class-validator'; -import { SongStats } from '@database/song/dto/SongStats'; -import type { SongDocument } from '@database/song/entity/song.entity'; +import type { SongDocument } from '../entity/song.entity'; +import { SongStats } from './SongStats'; import type { CategoryType, LicenseType, VisibilityType } from './types'; export type SongViewUploader = { diff --git a/packages/database/src/song/dto/UploadSongDto.dto.ts b/packages/database/src/song/dto/UploadSongDto.dto.ts index 59038a13..5cb0c83f 100644 --- a/packages/database/src/song/dto/UploadSongDto.dto.ts +++ b/packages/database/src/song/dto/UploadSongDto.dto.ts @@ -11,7 +11,7 @@ import { ValidateNested } from 'class-validator'; -import type { SongDocument } from '@database/song/entity/song.entity'; +import type { SongDocument } from '../entity/song.entity'; import { ThumbnailData } from './ThumbnailData.dto'; import type { CategoryType, LicenseType, VisibilityType } from './types'; diff --git a/packages/database/src/song/dto/UploadSongResponseDto.dto.ts b/packages/database/src/song/dto/UploadSongResponseDto.dto.ts index f892e200..f6d49cbd 100644 --- a/packages/database/src/song/dto/UploadSongResponseDto.dto.ts +++ b/packages/database/src/song/dto/UploadSongResponseDto.dto.ts @@ -7,7 +7,7 @@ import { ValidateNested } from 'class-validator'; -import type { SongWithUser } from '@database/song/entity/song.entity'; +import type { SongWithUser } from '../entity/song.entity'; import * as SongViewDto from './SongView.dto'; import { ThumbnailData } from './ThumbnailData.dto'; diff --git a/packages/database/src/song/entity/song.entity.ts b/packages/database/src/song/entity/song.entity.ts index 5ee97e94..f76a30c8 100644 --- a/packages/database/src/song/entity/song.entity.ts +++ b/packages/database/src/song/entity/song.entity.ts @@ -2,8 +2,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import type { HydratedDocument } from 'mongoose'; import { Schema as MongooseSchema, Types } from 'mongoose'; -import { User } from '@database/user/entity/user.entity'; - +import { User } from '../../user/entity/user.entity'; import { SongStats } from '../dto/SongStats'; import type { SongViewUploader } from '../dto/SongView.dto'; import { ThumbnailData } from '../dto/ThumbnailData.dto'; From fd2d4a7df3439e15aa5a264c89c9997b119bc707 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 11:20:21 -0300 Subject: [PATCH 09/21] test fixes --- apps/backend/src/email-login/email-login.controller.spec.ts | 2 ++ apps/backend/src/email-login/email-login.service.spec.ts | 2 ++ apps/backend/src/lib/GetRequestUser.spec.ts | 2 ++ apps/backend/src/lib/parseToken.spec.ts | 2 ++ apps/backend/src/mailing/mailing.controller.spec.ts | 2 ++ apps/backend/src/mailing/mailing.service.spec.ts | 2 ++ apps/backend/src/seed/seed.controller.spec.ts | 2 ++ apps/backend/src/seed/seed.service.spec.ts | 2 ++ apps/backend/src/song-browser/song-browser.controller.spec.ts | 2 ++ apps/backend/src/song-browser/song-browser.service.spec.ts | 2 ++ apps/backend/src/song/my-songs/my-songs.controller.spec.ts | 2 ++ apps/backend/src/song/song.controller.spec.ts | 2 ++ apps/backend/src/song/song.service.spec.ts | 2 ++ packages/thumbnail/src/utils.spec.ts | 2 ++ 14 files changed, 28 insertions(+) diff --git a/apps/backend/src/email-login/email-login.controller.spec.ts b/apps/backend/src/email-login/email-login.controller.spec.ts index d80d3fe4..ecd86d63 100644 --- a/apps/backend/src/email-login/email-login.controller.spec.ts +++ b/apps/backend/src/email-login/email-login.controller.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { Test, TestingModule } from '@nestjs/testing'; import { EmailLoginController } from './email-login.controller'; diff --git a/apps/backend/src/email-login/email-login.service.spec.ts b/apps/backend/src/email-login/email-login.service.spec.ts index 86361700..3f063d56 100644 --- a/apps/backend/src/email-login/email-login.service.spec.ts +++ b/apps/backend/src/email-login/email-login.service.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { Test, TestingModule } from '@nestjs/testing'; import { EmailLoginService } from './email-login.service'; diff --git a/apps/backend/src/lib/GetRequestUser.spec.ts b/apps/backend/src/lib/GetRequestUser.spec.ts index 116b4d18..ec7a9e9f 100644 --- a/apps/backend/src/lib/GetRequestUser.spec.ts +++ b/apps/backend/src/lib/GetRequestUser.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import type { UserDocument } from '@nbw/database'; import { ExecutionContext, HttpException, HttpStatus } from '@nestjs/common'; diff --git a/apps/backend/src/lib/parseToken.spec.ts b/apps/backend/src/lib/parseToken.spec.ts index 90408751..274c69ba 100644 --- a/apps/backend/src/lib/parseToken.spec.ts +++ b/apps/backend/src/lib/parseToken.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { ExecutionContext } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; diff --git a/apps/backend/src/mailing/mailing.controller.spec.ts b/apps/backend/src/mailing/mailing.controller.spec.ts index 584f022f..5962a32b 100644 --- a/apps/backend/src/mailing/mailing.controller.spec.ts +++ b/apps/backend/src/mailing/mailing.controller.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { Test, TestingModule } from '@nestjs/testing'; import { MailingController } from './mailing.controller'; diff --git a/apps/backend/src/mailing/mailing.service.spec.ts b/apps/backend/src/mailing/mailing.service.spec.ts index bcbd8763..0dce3220 100644 --- a/apps/backend/src/mailing/mailing.service.spec.ts +++ b/apps/backend/src/mailing/mailing.service.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { Test, TestingModule } from '@nestjs/testing'; import { MailerService } from '@nestjs-modules/mailer'; diff --git a/apps/backend/src/seed/seed.controller.spec.ts b/apps/backend/src/seed/seed.controller.spec.ts index 9dfbd229..f5a725b1 100644 --- a/apps/backend/src/seed/seed.controller.spec.ts +++ b/apps/backend/src/seed/seed.controller.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { Test, TestingModule } from '@nestjs/testing'; import { SeedController } from './seed.controller'; diff --git a/apps/backend/src/seed/seed.service.spec.ts b/apps/backend/src/seed/seed.service.spec.ts index 93fd575a..83d23979 100644 --- a/apps/backend/src/seed/seed.service.spec.ts +++ b/apps/backend/src/seed/seed.service.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { Test, TestingModule } from '@nestjs/testing'; import { SongService } from '@server/song/song.service'; diff --git a/apps/backend/src/song-browser/song-browser.controller.spec.ts b/apps/backend/src/song-browser/song-browser.controller.spec.ts index 1e16f027..3c2bf06c 100644 --- a/apps/backend/src/song-browser/song-browser.controller.spec.ts +++ b/apps/backend/src/song-browser/song-browser.controller.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { FeaturedSongsDto, PageQueryDTO, SongPreviewDto } from '@nbw/database'; import { Test, TestingModule } from '@nestjs/testing'; diff --git a/apps/backend/src/song-browser/song-browser.service.spec.ts b/apps/backend/src/song-browser/song-browser.service.spec.ts index a2bc2e3e..fbb24898 100644 --- a/apps/backend/src/song-browser/song-browser.service.spec.ts +++ b/apps/backend/src/song-browser/song-browser.service.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import { PageQueryDTO, SongPreviewDto, SongWithUser } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; diff --git a/apps/backend/src/song/my-songs/my-songs.controller.spec.ts b/apps/backend/src/song/my-songs/my-songs.controller.spec.ts index 8e67c8a4..43f3af29 100644 --- a/apps/backend/src/song/my-songs/my-songs.controller.spec.ts +++ b/apps/backend/src/song/my-songs/my-songs.controller.spec.ts @@ -1,3 +1,5 @@ +import { beforeEach, describe, expect, it, jest } from 'bun:test'; + import type { UserDocument } from '@nbw/database'; import { PageQueryDTO, SongPageDto } from '@nbw/database'; import { HttpException } from '@nestjs/common'; diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts index 5fdb156f..7fe8317f 100644 --- a/apps/backend/src/song/song.controller.spec.ts +++ b/apps/backend/src/song/song.controller.spec.ts @@ -1,3 +1,5 @@ +import { beforeEach, describe, expect, it, jest } from 'bun:test'; + import type { UserDocument } from '@nbw/database'; import { PageQueryDTO, SongPreviewDto, SongViewDto, UploadSongDto, UploadSongResponseDto } from '@nbw/database'; import { HttpStatus, UnauthorizedException } from '@nestjs/common'; diff --git a/apps/backend/src/song/song.service.spec.ts b/apps/backend/src/song/song.service.spec.ts index 365efcdf..6108457e 100644 --- a/apps/backend/src/song/song.service.spec.ts +++ b/apps/backend/src/song/song.service.spec.ts @@ -1,3 +1,5 @@ +import { jest, describe, beforeEach, expect, it } from 'bun:test'; + import type { UserDocument } from '@nbw/database'; import { SongDocument, diff --git a/packages/thumbnail/src/utils.spec.ts b/packages/thumbnail/src/utils.spec.ts index b96a1573..ed3fa582 100644 --- a/packages/thumbnail/src/utils.spec.ts +++ b/packages/thumbnail/src/utils.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'bun:test'; + import { getKeyText, instrumentColors, isDarkColor } from './utils'; describe('instrumentColors', () => { From 2d1301bad9b6e3da8c8f0207f4b9ce08e37742fd Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 11:22:37 -0300 Subject: [PATCH 10/21] chore: update ESLint configuration to enforce 4-space indentation by default and 2-space indentation for JSX files --- eslint.config.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index d29d3b41..b726cb34 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -103,6 +103,7 @@ export default tseslint.config( 'import/no-duplicates': 'error', // Spacing rules for consistency + '@stylistic/indent': ['error', 4], // Set default indentation to 4 spaces. '@stylistic/space-infix-ops': 'error', // Enforces spaces around operators like +, =, etc. '@stylistic/keyword-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around keywords like if, else. '@stylistic/arrow-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around arrow in arrow functions. @@ -120,4 +121,12 @@ export default tseslint.config( }], }, }, + // Override for JSX files + { + files: ['**/*.jsx', '**/*.tsx'], + rules: { + '@stylistic/indent': ['error', 2], // Set indentation to 2 spaces for JSX files. + }, + }, ); + From 649e94f7306dcef309abe2dd54bac373fd13805b Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 11:22:46 -0300 Subject: [PATCH 11/21] linting --- apps/backend/scripts/build.ts | 138 +- apps/backend/src/app.module.ts | 142 +- apps/backend/src/auth/auth.controller.spec.ts | 258 +-- apps/backend/src/auth/auth.controller.ts | 210 +- apps/backend/src/auth/auth.module.ts | 188 +- apps/backend/src/auth/auth.service.spec.ts | 558 ++--- apps/backend/src/auth/auth.service.ts | 360 ++-- .../src/auth/strategies/JWT.strategy.spec.ts | 152 +- .../src/auth/strategies/JWT.strategy.ts | 46 +- .../discord.strategy/DiscordStrategyConfig.ts | 86 +- .../discord.strategy/Strategy.spec.ts | 302 +-- .../strategies/discord.strategy/Strategy.ts | 438 ++-- .../discord.strategy/discord.strategy.spec.ts | 100 +- .../auth/strategies/discord.strategy/index.ts | 66 +- .../auth/strategies/discord.strategy/types.ts | 344 +-- .../auth/strategies/github.strategy.spec.ts | 100 +- .../src/auth/strategies/github.strategy.ts | 46 +- .../auth/strategies/google.strategy.spec.ts | 94 +- .../src/auth/strategies/google.strategy.ts | 58 +- .../magicLinkEmail.strategy.spec.ts | 246 +-- .../strategies/magicLinkEmail.strategy.ts | 144 +- apps/backend/src/auth/types/discordProfile.ts | 46 +- apps/backend/src/auth/types/githubProfile.ts | 96 +- apps/backend/src/auth/types/googleProfile.ts | 44 +- apps/backend/src/auth/types/profile.ts | 6 +- apps/backend/src/auth/types/token.ts | 10 +- .../src/config/EnvironmentVariables.ts | 146 +- .../email-login.controller.spec.ts | 22 +- .../src/email-login/email-login.controller.ts | 2 +- .../src/email-login/email-login.module.ts | 4 +- .../email-login/email-login.service.spec.ts | 20 +- apps/backend/src/file/file.module.ts | 92 +- apps/backend/src/file/file.service.spec.ts | 392 ++-- apps/backend/src/file/file.service.ts | 468 ++-- apps/backend/src/lib/GetRequestUser.spec.ts | 60 +- apps/backend/src/lib/GetRequestUser.ts | 44 +- .../backend/src/lib/initializeSwagger.spec.ts | 72 +- apps/backend/src/lib/initializeSwagger.ts | 32 +- apps/backend/src/lib/parseToken.spec.ts | 126 +- apps/backend/src/lib/parseToken.ts | 48 +- .../src/mailing/mailing.controller.spec.ts | 32 +- .../backend/src/mailing/mailing.controller.ts | 2 +- apps/backend/src/mailing/mailing.module.ts | 6 +- .../src/mailing/mailing.service.spec.ts | 98 +- apps/backend/src/mailing/mailing.service.ts | 78 +- apps/backend/src/main.ts | 76 +- apps/backend/src/seed/seed.controller.spec.ts | 32 +- apps/backend/src/seed/seed.controller.ts | 22 +- apps/backend/src/seed/seed.module.ts | 50 +- apps/backend/src/seed/seed.service.spec.ts | 58 +- apps/backend/src/seed/seed.service.ts | 394 ++-- .../song-browser.controller.spec.ts | 144 +- .../song-browser/song-browser.controller.ts | 90 +- .../src/song-browser/song-browser.module.ts | 6 +- .../song-browser/song-browser.service.spec.ts | 202 +- .../src/song-browser/song-browser.service.ts | 200 +- .../song/my-songs/my-songs.controller.spec.ts | 120 +- .../src/song/my-songs/my-songs.controller.ts | 34 +- .../song-upload/song-upload.service.spec.ts | 740 +++---- .../song/song-upload/song-upload.service.ts | 796 +++---- .../song-webhook/song-webhook.service.spec.ts | 404 ++-- .../song/song-webhook/song-webhook.service.ts | 232 +- apps/backend/src/song/song.controller.spec.ts | 524 ++--- apps/backend/src/song/song.controller.ts | 330 +-- apps/backend/src/song/song.module.ts | 38 +- apps/backend/src/song/song.service.spec.ts | 1888 ++++++++--------- apps/backend/src/song/song.service.ts | 822 +++---- apps/backend/src/song/song.util.ts | 146 +- apps/backend/src/user/user.controller.spec.ts | 126 +- apps/backend/src/user/user.controller.ts | 70 +- apps/backend/src/user/user.module.ts | 12 +- apps/backend/src/user/user.service.spec.ts | 770 +++---- apps/backend/src/user/user.service.ts | 352 +-- apps/frontend/src/app/layout.tsx | 4 +- apps/frontend/src/global.d.ts | 4 +- apps/frontend/src/lib/axios/ClientAxios.ts | 26 +- apps/frontend/src/lib/axios/index.ts | 4 +- apps/frontend/src/lib/axios/token.utils.ts | 28 +- apps/frontend/src/lib/posts.ts | 142 +- apps/frontend/src/lib/tailwind.utils.ts | 2 +- .../auth/components/client/login.util.ts | 40 +- .../src/modules/auth/features/auth.utils.ts | 78 +- apps/frontend/src/modules/auth/types/User.ts | 86 +- .../shared/components/client/GenericModal.tsx | 4 +- .../shared/components/client/ads/AdSlots.tsx | 14 +- .../src/modules/shared/util/format.ts | 122 +- .../song/components/client/SongForm.tsx | 4 +- .../song/components/client/SongForm.zod.ts | 106 +- .../src/modules/song/util/downloadSong.ts | 98 +- .../src/modules/user/features/song.util.ts | 12 +- .../src/modules/user/features/user.util.ts | 16 +- packages/configs/src/colors.ts | 216 +- packages/configs/src/song.ts | 200 +- packages/configs/src/user.ts | 6 +- .../database/src/common/dto/PageQuery.dto.ts | 108 +- .../src/song/dto/CustomInstrumentData.dto.ts | 4 +- .../src/song/dto/FeaturedSongsDto.dto.ts | 32 +- .../database/src/song/dto/SongPage.dto.ts | 50 +- .../database/src/song/dto/SongPreview.dto.ts | 132 +- packages/database/src/song/dto/SongStats.ts | 90 +- .../database/src/song/dto/SongView.dto.ts | 190 +- .../src/song/dto/ThumbnailData.dto.ts | 78 +- .../src/song/dto/UploadSongDto.dto.ts | 238 +-- .../src/song/dto/UploadSongResponseDto.dto.ts | 108 +- .../database/src/song/entity/song.entity.ts | 112 +- .../database/src/user/dto/CreateUser.dto.ts | 64 +- packages/database/src/user/dto/GetUser.dto.ts | 72 +- .../database/src/user/dto/Login.dto copy.ts | 8 +- .../src/user/dto/LoginWithEmail.dto.ts | 8 +- .../database/src/user/dto/NewEmailUser.dto.ts | 46 +- .../src/user/dto/SingleUsePass.dto.ts | 16 +- .../src/user/dto/UpdateUsername.dto.ts | 18 +- packages/database/src/user/dto/user.dto.ts | 22 +- .../database/src/user/entity/user.entity.ts | 118 +- packages/song/src/injectMetadata.ts | 40 +- packages/song/src/notes.ts | 102 +- packages/song/src/obfuscate.ts | 362 ++-- packages/song/src/pack.ts | 106 +- packages/song/src/parse.ts | 108 +- packages/song/src/stats.ts | 590 +++--- packages/song/src/types.ts | 34 +- packages/song/src/util.ts | 34 +- packages/song/tests/song/index.spec.ts | 262 +-- packages/song/tests/song/util.ts | 24 +- packages/sounds/src/fetchSoundList.ts | 86 +- packages/sounds/src/types.ts | 118 +- packages/sounds/src/util.ts | 28 +- packages/thumbnail/src/canvasFactory.ts | 280 +-- packages/thumbnail/src/index.ts | 202 +- packages/thumbnail/src/types.ts | 34 +- packages/thumbnail/src/utils.spec.ts | 262 +-- packages/thumbnail/src/utils.ts | 130 +- 132 files changed, 10114 insertions(+), 10114 deletions(-) diff --git a/apps/backend/scripts/build.ts b/apps/backend/scripts/build.ts index 894a3240..21128cd9 100644 --- a/apps/backend/scripts/build.ts +++ b/apps/backend/scripts/build.ts @@ -5,90 +5,90 @@ import { resolve } from 'path'; import { getLatestVersionSoundList } from '@nbw/sounds'; const writeSoundList = async () => { - function writeJSONFile( - dir: string, - fileName: string, - data: Record | string[] - ) { - const path = resolve(dir, fileName); - const jsonString = JSON.stringify(data, null, 0); - writeFileSync(path, jsonString); - } + function writeJSONFile( + dir: string, + fileName: string, + data: Record | string[] + ) { + const path = resolve(dir, fileName); + const jsonString = JSON.stringify(data, null, 0); + writeFileSync(path, jsonString); + } - const dataDir = resolve(__dirname, '..', 'public', 'data'); + const dataDir = resolve(__dirname, '..', 'public', 'data'); - const soundListPath = 'soundList.json'; + const soundListPath = 'soundList.json'; - // Try to create the output directory if it doesn't exist - try { - mkdirSync(dataDir, { recursive: true }); - } catch (err) { - if (err instanceof Error && err.message.includes('EEXIST')) { - console.log('Sound data files already exist; skipping generation.'); - process.exit(0); + // Try to create the output directory if it doesn't exist + try { + mkdirSync(dataDir, { recursive: true }); + } catch (err) { + if (err instanceof Error && err.message.includes('EEXIST')) { + console.log('Sound data files already exist; skipping generation.'); + process.exit(0); + } } - } - // If the files already exist, exit early - const files = [soundListPath].map((fileName) => resolve(dataDir, fileName)); + // If the files already exist, exit early + const files = [soundListPath].map((fileName) => resolve(dataDir, fileName)); - if (files.every((file) => existsSync(file))) { - console.log('Sound data files already exist; skipping generation.'); - process.exit(0); - } + if (files.every((file) => existsSync(file))) { + console.log('Sound data files already exist; skipping generation.'); + process.exit(0); + } - console.log('Generating sound data files...'); + console.log('Generating sound data files...'); - // Write list of sounds in the latest MC version to a JSON file - // Filter the list to only include sounds that match the chosen patterns - // (defined in the shared/ module) - getLatestVersionSoundList().then((soundList) => { - writeJSONFile(dataDir, soundListPath, soundList); - }); + // Write list of sounds in the latest MC version to a JSON file + // Filter the list to only include sounds that match the chosen patterns + // (defined in the shared/ module) + getLatestVersionSoundList().then((soundList) => { + writeJSONFile(dataDir, soundListPath, soundList); + }); }; const build = async () => { - await Bun.$`rm -rf dist`; + await Bun.$`rm -rf dist`; - const optionalRequirePackages = [ - 'class-transformer', - 'class-transformer/storage', - 'class-validator', - '@nestjs/microservices', - '@nestjs/websockets', - '@fastify/static' - ]; + const optionalRequirePackages = [ + 'class-transformer', + 'class-transformer/storage', + 'class-validator', + '@nestjs/microservices', + '@nestjs/websockets', + '@fastify/static' + ]; - const result = await Bun.build({ - entrypoints: ['./src/main.ts'], - outdir : './dist', - target : 'bun', - minify : false, - sourcemap : 'linked', - external : optionalRequirePackages.filter((pkg) => { - try { - require(pkg); - return false; - } catch (_) { - return true; - } - }), - splitting: true - }); + const result = await Bun.build({ + entrypoints: ['./src/main.ts'], + outdir : './dist', + target : 'bun', + minify : false, + sourcemap : 'linked', + external : optionalRequirePackages.filter((pkg) => { + try { + require(pkg); + return false; + } catch (_) { + return true; + } + }), + splitting: true + }); - if (!result.success) { - console.log(result.logs[0]); - process.exit(1); - } + if (!result.success) { + console.log(result.logs[0]); + process.exit(1); + } - console.log('Built successfully!'); + console.log('Built successfully!'); }; build() - .then(() => { - writeSoundList(); - }) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => { + writeSoundList(); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index c7d84293..048a9b38 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -18,79 +18,79 @@ import { SongBrowserModule } from './song-browser/song-browser.module'; import { UserModule } from './user/user.module'; @Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal : true, - envFilePath: ['.env.development', '.env.production'], - validate - }), - //DatabaseModule, - MongooseModule.forRootAsync({ - imports : [ConfigModule], - inject : [ConfigService], - useFactory: ( - configService: ConfigService - ): MongooseModuleFactoryOptions => { - const url = configService.getOrThrow('MONGO_URL'); - Logger.debug(`Connecting to ${url}`); + imports: [ + ConfigModule.forRoot({ + isGlobal : true, + envFilePath: ['.env.development', '.env.production'], + validate + }), + //DatabaseModule, + MongooseModule.forRootAsync({ + imports : [ConfigModule], + inject : [ConfigService], + useFactory: ( + configService: ConfigService + ): MongooseModuleFactoryOptions => { + const url = configService.getOrThrow('MONGO_URL'); + Logger.debug(`Connecting to ${url}`); - return { - uri : url, - retryAttempts: 10, - retryDelay : 3000 - }; - } - }), - // Mailing - MailerModule.forRootAsync({ - imports : [ConfigModule], - useFactory: (configService: ConfigService) => { - const transport = configService.getOrThrow('MAIL_TRANSPORT'); - const from = configService.getOrThrow('MAIL_FROM'); - AppModule.logger.debug(`MAIL_TRANSPORT: ${transport}`); - AppModule.logger.debug(`MAIL_FROM: ${from}`); - return { - transport: transport, - defaults : { - from: from - }, - template: { - dir : __dirname + '/mailing/templates', - adapter: new HandlebarsAdapter(), - options: { - strict: true + return { + uri : url, + retryAttempts: 10, + retryDelay : 3000 + }; } - } - }; - }, - inject: [ConfigService] - }), - // Throttler - ThrottlerModule.forRoot([ - { - ttl : 60, - limit: 256 // 256 requests per minute - } - ]), - SongModule, - UserModule, - AuthModule.forRootAsync(), - FileModule.forRootAsync(), - SongBrowserModule, - SeedModule.forRoot(), - EmailLoginModule, - MailingModule - ], - controllers: [], - providers : [ - ParseTokenPipe, - { - provide : APP_GUARD, - useClass: ThrottlerGuard - } - ], - exports: [ParseTokenPipe] + }), + // Mailing + MailerModule.forRootAsync({ + imports : [ConfigModule], + useFactory: (configService: ConfigService) => { + const transport = configService.getOrThrow('MAIL_TRANSPORT'); + const from = configService.getOrThrow('MAIL_FROM'); + AppModule.logger.debug(`MAIL_TRANSPORT: ${transport}`); + AppModule.logger.debug(`MAIL_FROM: ${from}`); + return { + transport: transport, + defaults : { + from: from + }, + template: { + dir : __dirname + '/mailing/templates', + adapter: new HandlebarsAdapter(), + options: { + strict: true + } + } + }; + }, + inject: [ConfigService] + }), + // Throttler + ThrottlerModule.forRoot([ + { + ttl : 60, + limit: 256 // 256 requests per minute + } + ]), + SongModule, + UserModule, + AuthModule.forRootAsync(), + FileModule.forRootAsync(), + SongBrowserModule, + SeedModule.forRoot(), + EmailLoginModule, + MailingModule + ], + controllers: [], + providers : [ + ParseTokenPipe, + { + provide : APP_GUARD, + useClass: ThrottlerGuard + } + ], + exports: [ParseTokenPipe] }) export class AppModule { - static readonly logger = new Logger(AppModule.name); + static readonly logger = new Logger(AppModule.name); } diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts index 06a2eb15..299d1d83 100644 --- a/apps/backend/src/auth/auth.controller.spec.ts +++ b/apps/backend/src/auth/auth.controller.spec.ts @@ -8,172 +8,172 @@ import { AuthService } from './auth.service'; import { MagicLinkEmailStrategy } from './strategies/magicLinkEmail.strategy'; const mockAuthService = { - githubLogin : jest.fn(), - googleLogin : jest.fn(), - discordLogin : jest.fn(), - verifyToken : jest.fn(), - loginWithEmail: jest.fn() + githubLogin : jest.fn(), + googleLogin : jest.fn(), + discordLogin : jest.fn(), + verifyToken : jest.fn(), + loginWithEmail: jest.fn() }; const mockMagicLinkEmailStrategy = { - send: jest.fn() + send: jest.fn() }; describe('AuthController', () => { - let controller: AuthController; - let authService: AuthService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AuthController], - providers : [ - { - provide : AuthService, - useValue: mockAuthService - }, - { - provide : MagicLinkEmailStrategy, - useValue: mockMagicLinkEmailStrategy - } - ] - }).compile(); - - controller = module.get(AuthController); - authService = module.get(AuthService); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('githubRedirect', () => { - it('should call AuthService.githubLogin', async () => { - const req = {} as Request; - const res = {} as Response; - - await controller.githubRedirect(req, res); - - expect(authService.githubLogin).toHaveBeenCalledWith(req, res); + let controller: AuthController; + let authService: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers : [ + { + provide : AuthService, + useValue: mockAuthService + }, + { + provide : MagicLinkEmailStrategy, + useValue: mockMagicLinkEmailStrategy + } + ] + }).compile(); + + controller = module.get(AuthController); + authService = module.get(AuthService); }); - it('should handle exceptions', async () => { - const req = {} as Request; - const res = {} as Response; - const error = new Error('Test error'); - (authService.githubLogin as jest.Mock).mockRejectedValueOnce(error); + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('githubRedirect', () => { + it('should call AuthService.githubLogin', async () => { + const req = {} as Request; + const res = {} as Response; + + await controller.githubRedirect(req, res); + + expect(authService.githubLogin).toHaveBeenCalledWith(req, res); + }); + + it('should handle exceptions', async () => { + const req = {} as Request; + const res = {} as Response; + const error = new Error('Test error'); + (authService.githubLogin as jest.Mock).mockRejectedValueOnce(error); - await expect(controller.githubRedirect(req, res)).rejects.toThrow( - 'Test error' - ); + await expect(controller.githubRedirect(req, res)).rejects.toThrow( + 'Test error' + ); + }); }); - }); - describe('githubLogin', () => { - it('should call AuthService.githubLogin', async () => { - await controller.githubLogin(); - expect(authService.githubLogin).toHaveBeenCalled(); + describe('githubLogin', () => { + it('should call AuthService.githubLogin', async () => { + await controller.githubLogin(); + expect(authService.githubLogin).toHaveBeenCalled(); + }); }); - }); - describe('googleRedirect', () => { - it('should call AuthService.googleLogin', async () => { - const req = {} as Request; - const res = {} as Response; + describe('googleRedirect', () => { + it('should call AuthService.googleLogin', async () => { + const req = {} as Request; + const res = {} as Response; - await controller.googleRedirect(req, res); + await controller.googleRedirect(req, res); - expect(authService.googleLogin).toHaveBeenCalledWith(req, res); - }); + expect(authService.googleLogin).toHaveBeenCalledWith(req, res); + }); - it('should handle exceptions', async () => { - const req = {} as Request; - const res = {} as Response; - const error = new Error('Test error'); - (authService.googleLogin as jest.Mock).mockRejectedValueOnce(error); + it('should handle exceptions', async () => { + const req = {} as Request; + const res = {} as Response; + const error = new Error('Test error'); + (authService.googleLogin as jest.Mock).mockRejectedValueOnce(error); - await expect(controller.googleRedirect(req, res)).rejects.toThrow( - 'Test error' - ); + await expect(controller.googleRedirect(req, res)).rejects.toThrow( + 'Test error' + ); + }); }); - }); - describe('googleLogin', () => { - it('should call AuthService.googleLogin', async () => { - await controller.googleLogin(); - expect(authService.googleLogin).toHaveBeenCalled(); + describe('googleLogin', () => { + it('should call AuthService.googleLogin', async () => { + await controller.googleLogin(); + expect(authService.googleLogin).toHaveBeenCalled(); + }); }); - }); - describe('discordRedirect', () => { - it('should call AuthService.discordLogin', async () => { - const req = {} as Request; - const res = {} as Response; + describe('discordRedirect', () => { + it('should call AuthService.discordLogin', async () => { + const req = {} as Request; + const res = {} as Response; - await controller.discordRedirect(req, res); + await controller.discordRedirect(req, res); - expect(authService.discordLogin).toHaveBeenCalledWith(req, res); - }); + expect(authService.discordLogin).toHaveBeenCalledWith(req, res); + }); - it('should handle exceptions', async () => { - const req = {} as Request; - const res = {} as Response; - const error = new Error('Test error'); - (authService.discordLogin as jest.Mock).mockRejectedValueOnce(error); + it('should handle exceptions', async () => { + const req = {} as Request; + const res = {} as Response; + const error = new Error('Test error'); + (authService.discordLogin as jest.Mock).mockRejectedValueOnce(error); - await expect(controller.discordRedirect(req, res)).rejects.toThrow( - 'Test error' - ); + await expect(controller.discordRedirect(req, res)).rejects.toThrow( + 'Test error' + ); + }); }); - }); - describe('discordLogin', () => { - it('should call AuthService.discordLogin', async () => { - await controller.discordLogin(); - expect(authService.discordLogin).toHaveBeenCalled(); + describe('discordLogin', () => { + it('should call AuthService.discordLogin', async () => { + await controller.discordLogin(); + expect(authService.discordLogin).toHaveBeenCalled(); + }); }); - }); - describe('magicLinkRedirect', () => { - it('should call AuthService.loginWithEmail', async () => { - const req = {} as Request; - const res = {} as Response; + describe('magicLinkRedirect', () => { + it('should call AuthService.loginWithEmail', async () => { + const req = {} as Request; + const res = {} as Response; - await controller.magicLinkRedirect(req, res); + await controller.magicLinkRedirect(req, res); - expect(authService.loginWithEmail).toHaveBeenCalledWith(req, res); - }); + expect(authService.loginWithEmail).toHaveBeenCalledWith(req, res); + }); - it('should handle exceptions', async () => { - const req = {} as Request; - const res = {} as Response; - const error = new Error('Test error'); - (authService.loginWithEmail as jest.Mock).mockRejectedValueOnce(error); + it('should handle exceptions', async () => { + const req = {} as Request; + const res = {} as Response; + const error = new Error('Test error'); + (authService.loginWithEmail as jest.Mock).mockRejectedValueOnce(error); - await expect(controller.magicLinkRedirect(req, res)).rejects.toThrow( - 'Test error' - ); + await expect(controller.magicLinkRedirect(req, res)).rejects.toThrow( + 'Test error' + ); + }); }); - }); - - describe('magicLinkLogin', () => { - it('should call AuthService.discordLogin', async () => { - //const req = {} as Request; - //const res = {} as Response; - // TODO: Implement this test - //await controller.magicLinkLogin(req, res); - // - //expect(mockMagicLinkEmailStrategy.send).toHaveBeenCalledWith(req, res); + + describe('magicLinkLogin', () => { + it('should call AuthService.discordLogin', async () => { + //const req = {} as Request; + //const res = {} as Response; + // TODO: Implement this test + //await controller.magicLinkLogin(req, res); + // + //expect(mockMagicLinkEmailStrategy.send).toHaveBeenCalledWith(req, res); + }); }); - }); - describe('verify', () => { - it('should call AuthService.verifyToken', async () => { - const req = {} as Request; - const res = {} as Response; + describe('verify', () => { + it('should call AuthService.verifyToken', async () => { + const req = {} as Request; + const res = {} as Response; - await controller.verify(req, res); + await controller.verify(req, res); - expect(authService.verifyToken).toHaveBeenCalledWith(req, res); + expect(authService.verifyToken).toHaveBeenCalledWith(req, res); + }); }); - }); }); diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index ed44cfb9..883a9dba 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -1,14 +1,14 @@ import { - Controller, - Get, - HttpException, - HttpStatus, - Inject, - Logger, - Post, - Req, - Res, - UseGuards + Controller, + Get, + HttpException, + HttpStatus, + Inject, + Logger, + Post, + Req, + Res, + UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; @@ -21,116 +21,116 @@ import { MagicLinkEmailStrategy } from './strategies/magicLinkEmail.strategy'; @Controller('auth') @ApiTags('auth') export class AuthController { - private readonly logger = new Logger(AuthController.name); - constructor( - @Inject(AuthService) - private readonly authService: AuthService, - @Inject(MagicLinkEmailStrategy) - private readonly magicLinkEmailStrategy: MagicLinkEmailStrategy - ) {} + private readonly logger = new Logger(AuthController.name); + constructor( + @Inject(AuthService) + private readonly authService: AuthService, + @Inject(MagicLinkEmailStrategy) + private readonly magicLinkEmailStrategy: MagicLinkEmailStrategy + ) {} - @Throttle({ - default: { - // one every 1 hour - ttl : 60 * 60 * 1000, - limit: 1 - } - }) - @Post('login/magic-link') - @ApiOperation({ - summary: + @Throttle({ + default: { + // one every 1 hour + ttl : 60 * 60 * 1000, + limit: 1 + } + }) + @Post('login/magic-link') + @ApiOperation({ + summary: 'Will send the user a email with a single use login link, if the user does not exist it will create a new user', - requestBody: { - content: { - 'application/json': { - schema: { - type : 'object', - properties: { - destination: { - type : 'string', - example : 'vycasnicolas@gmail.com', - description: 'Email address to send the magic link to' - } - }, - required: ['destination'] - } + requestBody: { + content: { + 'application/json': { + schema: { + type : 'object', + properties: { + destination: { + type : 'string', + example : 'vycasnicolas@gmail.com', + description: 'Email address to send the magic link to' + } + }, + required: ['destination'] + } + } + } } - } - } - }) + }) - public async magicLinkLogin(@Req() req: Request, @Res() res: Response) { - throw new HttpException('Not implemented', HttpStatus.NOT_IMPLEMENTED); + public async magicLinkLogin(@Req() req: Request, @Res() res: Response) { + throw new HttpException('Not implemented', HttpStatus.NOT_IMPLEMENTED); // TODO: uncomment this line to enable magic link login //return this.magicLinkEmailStrategy.send(req, res); - } + } - @Get('magic-link/callback') - @ApiOperation({ - summary: 'Will send the user a email with a single use login link' - }) - @UseGuards(AuthGuard('magic-link')) - public async magicLinkRedirect(@Req() req: Request, @Res() res: Response) { - return this.authService.loginWithEmail(req, res); - } + @Get('magic-link/callback') + @ApiOperation({ + summary: 'Will send the user a email with a single use login link' + }) + @UseGuards(AuthGuard('magic-link')) + public async magicLinkRedirect(@Req() req: Request, @Res() res: Response) { + return this.authService.loginWithEmail(req, res); + } - @Get('login/github') - @UseGuards(AuthGuard('github')) - @ApiOperation({ summary: 'Login with github' }) - public githubLogin() { + @Get('login/github') + @UseGuards(AuthGuard('github')) + @ApiOperation({ summary: 'Login with github' }) + public githubLogin() { // Not need for implementation, its handled by passport - this.logger.debug('GitHub login'); - } + this.logger.debug('GitHub login'); + } - @Get('github/callback') - @UseGuards(AuthGuard('github')) - @ApiOperation({ summary: 'GitHub login callback' }) - public githubRedirect(@Req() req: Request, @Res() res: Response) { - return this.authService.githubLogin(req, res); - } + @Get('github/callback') + @UseGuards(AuthGuard('github')) + @ApiOperation({ summary: 'GitHub login callback' }) + public githubRedirect(@Req() req: Request, @Res() res: Response) { + return this.authService.githubLogin(req, res); + } - @Get('login/google') - @ApiOperation({ summary: 'Login with google' }) - @UseGuards(AuthGuard('google')) - public googleLogin() { + @Get('login/google') + @ApiOperation({ summary: 'Login with google' }) + @UseGuards(AuthGuard('google')) + public googleLogin() { // Not need for implementation, its handled by passport - this.logger.debug('Google login'); - } + this.logger.debug('Google login'); + } - @Get('google/callback') - @ApiOperation({ summary: 'Google login callback' }) - @UseGuards(AuthGuard('google')) - public googleRedirect(@Req() req: Request, @Res() res: Response) { - return this.authService.googleLogin(req, res); - } + @Get('google/callback') + @ApiOperation({ summary: 'Google login callback' }) + @UseGuards(AuthGuard('google')) + public googleRedirect(@Req() req: Request, @Res() res: Response) { + return this.authService.googleLogin(req, res); + } - @Get('login/discord') - @ApiOperation({ summary: 'Login with discord' }) - @UseGuards(AuthGuard('discord')) - public discordLogin() { + @Get('login/discord') + @ApiOperation({ summary: 'Login with discord' }) + @UseGuards(AuthGuard('discord')) + public discordLogin() { // Not need for implementation, its handled by passport - this.logger.debug('Discord login'); - } + this.logger.debug('Discord login'); + } - @Get('discord/callback') - @ApiOperation({ summary: 'Discord login callback' }) - @UseGuards(AuthGuard('discord')) - public discordRedirect(@Req() req: Request, @Res() res: Response) { - return this.authService.discordLogin(req, res); - } + @Get('discord/callback') + @ApiOperation({ summary: 'Discord login callback' }) + @UseGuards(AuthGuard('discord')) + public discordRedirect(@Req() req: Request, @Res() res: Response) { + return this.authService.discordLogin(req, res); + } - @Get('verify') - @ApiOperation({ summary: 'Verify user token' }) - @ApiResponse({ status: 200, description: 'User token verified' }) - @ApiResponse({ status: 401, description: 'User token not verified' }) - @UseGuards(AuthGuard('jwt-refresh')) - public verify( - @Req() req: Request, - @Res({ - passthrough: true - }) - res: Response - ) { - this.authService.verifyToken(req, res); - } + @Get('verify') + @ApiOperation({ summary: 'Verify user token' }) + @ApiResponse({ status: 200, description: 'User token verified' }) + @ApiResponse({ status: 401, description: 'User token not verified' }) + @UseGuards(AuthGuard('jwt-refresh')) + public verify( + @Req() req: Request, + @Res({ + passthrough: true + }) + res: Response + ) { + this.authService.verifyToken(req, res); + } } diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index c4343ea8..5649c1c1 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -15,101 +15,101 @@ import { MagicLinkEmailStrategy } from './strategies/magicLinkEmail.strategy'; @Module({}) export class AuthModule { - static forRootAsync(): DynamicModule { - return { - module : AuthModule, - imports: [ - UserModule, - ConfigModule.forRoot(), - MailingModule, - JwtModule.registerAsync({ - inject : [ConfigService], - imports : [ConfigModule], - useFactory: async (config: ConfigService) => { - const JWT_SECRET = config.get('JWT_SECRET'); - const JWT_EXPIRES_IN = config.get('JWT_EXPIRES_IN'); + static forRootAsync(): DynamicModule { + return { + module : AuthModule, + imports: [ + UserModule, + ConfigModule.forRoot(), + MailingModule, + JwtModule.registerAsync({ + inject : [ConfigService], + imports : [ConfigModule], + useFactory: async (config: ConfigService) => { + const JWT_SECRET = config.get('JWT_SECRET'); + const JWT_EXPIRES_IN = config.get('JWT_EXPIRES_IN'); - if (!JWT_SECRET) { - Logger.error('JWT_SECRET is not set'); - throw new Error('JWT_SECRET is not set'); - } + if (!JWT_SECRET) { + Logger.error('JWT_SECRET is not set'); + throw new Error('JWT_SECRET is not set'); + } - if (!JWT_EXPIRES_IN) { - Logger.warn('JWT_EXPIRES_IN is not set, using default of 60s'); - } + if (!JWT_EXPIRES_IN) { + Logger.warn('JWT_EXPIRES_IN is not set, using default of 60s'); + } - return { - secret : JWT_SECRET, - signOptions: { expiresIn: JWT_EXPIRES_IN || '60s' } - }; - } - }) - ], - controllers: [AuthController], - providers : [ - AuthService, - ConfigService, - GoogleStrategy, - GithubStrategy, - DiscordStrategy, - MagicLinkEmailStrategy, - JwtStrategy, - { - inject : [ConfigService], - provide : 'COOKIE_EXPIRES_IN', - useFactory: (configService: ConfigService) => - configService.getOrThrow('COOKIE_EXPIRES_IN') - }, - { - inject : [ConfigService], - provide : 'SERVER_URL', - useFactory: (configService: ConfigService) => - configService.getOrThrow('SERVER_URL') - }, - { - inject : [ConfigService], - provide : 'FRONTEND_URL', - useFactory: (configService: ConfigService) => - configService.getOrThrow('FRONTEND_URL') - }, - { - inject : [ConfigService], - provide : 'JWT_SECRET', - useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_SECRET') - }, - { - inject : [ConfigService], - provide : 'JWT_EXPIRES_IN', - useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_EXPIRES_IN') - }, - { - inject : [ConfigService], - provide : 'JWT_REFRESH_SECRET', - useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_REFRESH_SECRET') - }, - { - inject : [ConfigService], - provide : 'JWT_REFRESH_EXPIRES_IN', - useFactory: (configService: ConfigService) => - configService.getOrThrow('JWT_REFRESH_EXPIRES_IN') - }, - { - inject : [ConfigService], - provide : 'MAGIC_LINK_SECRET', - useFactory: (configService: ConfigService) => - configService.getOrThrow('MAGIC_LINK_SECRET') - }, - { - inject : [ConfigService], - provide : 'APP_DOMAIN', - useFactory: (configService: ConfigService) => - configService.get('APP_DOMAIN') - } - ], - exports: [AuthService] - }; - } + return { + secret : JWT_SECRET, + signOptions: { expiresIn: JWT_EXPIRES_IN || '60s' } + }; + } + }) + ], + controllers: [AuthController], + providers : [ + AuthService, + ConfigService, + GoogleStrategy, + GithubStrategy, + DiscordStrategy, + MagicLinkEmailStrategy, + JwtStrategy, + { + inject : [ConfigService], + provide : 'COOKIE_EXPIRES_IN', + useFactory: (configService: ConfigService) => + configService.getOrThrow('COOKIE_EXPIRES_IN') + }, + { + inject : [ConfigService], + provide : 'SERVER_URL', + useFactory: (configService: ConfigService) => + configService.getOrThrow('SERVER_URL') + }, + { + inject : [ConfigService], + provide : 'FRONTEND_URL', + useFactory: (configService: ConfigService) => + configService.getOrThrow('FRONTEND_URL') + }, + { + inject : [ConfigService], + provide : 'JWT_SECRET', + useFactory: (configService: ConfigService) => + configService.getOrThrow('JWT_SECRET') + }, + { + inject : [ConfigService], + provide : 'JWT_EXPIRES_IN', + useFactory: (configService: ConfigService) => + configService.getOrThrow('JWT_EXPIRES_IN') + }, + { + inject : [ConfigService], + provide : 'JWT_REFRESH_SECRET', + useFactory: (configService: ConfigService) => + configService.getOrThrow('JWT_REFRESH_SECRET') + }, + { + inject : [ConfigService], + provide : 'JWT_REFRESH_EXPIRES_IN', + useFactory: (configService: ConfigService) => + configService.getOrThrow('JWT_REFRESH_EXPIRES_IN') + }, + { + inject : [ConfigService], + provide : 'MAGIC_LINK_SECRET', + useFactory: (configService: ConfigService) => + configService.getOrThrow('MAGIC_LINK_SECRET') + }, + { + inject : [ConfigService], + provide : 'APP_DOMAIN', + useFactory: (configService: ConfigService) => + configService.get('APP_DOMAIN') + } + ], + exports: [AuthService] + }; + } } diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts index ee0e24e3..a688c448 100644 --- a/apps/backend/src/auth/auth.service.spec.ts +++ b/apps/backend/src/auth/auth.service.spec.ts @@ -12,336 +12,336 @@ import { AuthService } from './auth.service'; import { Profile } from './types/profile'; const mockAxios = { - get : jest.fn(), - post : jest.fn(), - put : jest.fn(), - delete: jest.fn(), - create: jest.fn() + get : jest.fn(), + post : jest.fn(), + put : jest.fn(), + delete: jest.fn(), + create: jest.fn() }; mock.module('axios', () => mockAxios); const mockUserService = { - generateUsername: jest.fn(), - findByEmail : jest.fn(), - findByID : jest.fn(), - create : jest.fn() + generateUsername: jest.fn(), + findByEmail : jest.fn(), + findByID : jest.fn(), + create : jest.fn() }; const mockJwtService = { - decode : jest.fn(), - signAsync: jest.fn(), - verify : jest.fn() + decode : jest.fn(), + signAsync: jest.fn(), + verify : jest.fn() }; describe('AuthService', () => { - let authService: AuthService; - let userService: UserService; - let jwtService: JwtService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AuthService, - { - provide : UserService, - useValue: mockUserService - }, - { - provide : JwtService, - useValue: mockJwtService - }, - { - provide : 'COOKIE_EXPIRES_IN', - useValue: '3600' - }, - { - provide : 'FRONTEND_URL', - useValue: 'http://frontend.test.com' - }, - { - provide : 'COOKIE_EXPIRES_IN', - useValue: '3600' - }, - { - provide : 'JWT_SECRET', - useValue: 'test-jwt-secret' - }, - { - provide : 'JWT_EXPIRES_IN', - useValue: '1d' - }, - { - provide : 'JWT_REFRESH_SECRET', - useValue: 'test-jwt-refresh-secret' - }, - { - provide : 'JWT_REFRESH_EXPIRES_IN', - useValue: '7d' - }, - { - provide : 'WHITELISTED_USERS', - useValue: 'tomast1337,bentroen,testuser' - }, - { - provide : 'APP_DOMAIN', - useValue: '.test.com' - } - ] - }).compile(); - - authService = module.get(AuthService); - userService = module.get(UserService); - jwtService = module.get(JwtService); - }); - - it('should be defined', () => { - expect(authService).toBeDefined(); - }); - - describe('verifyToken', () => { - it('should throw an error if no authorization header is provided', async () => { - const req = { headers: {} } as Request; - - const res = { - status: jest.fn().mockReturnThis(), - json : jest.fn() - } as any; - - await authService.verifyToken(req, res); - - expect(res.status).toHaveBeenCalledWith(401); - - expect(res.json).toHaveBeenCalledWith({ - message: 'No authorization header' - }); + let authService: AuthService; + let userService: UserService; + let jwtService: JwtService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { + provide : UserService, + useValue: mockUserService + }, + { + provide : JwtService, + useValue: mockJwtService + }, + { + provide : 'COOKIE_EXPIRES_IN', + useValue: '3600' + }, + { + provide : 'FRONTEND_URL', + useValue: 'http://frontend.test.com' + }, + { + provide : 'COOKIE_EXPIRES_IN', + useValue: '3600' + }, + { + provide : 'JWT_SECRET', + useValue: 'test-jwt-secret' + }, + { + provide : 'JWT_EXPIRES_IN', + useValue: '1d' + }, + { + provide : 'JWT_REFRESH_SECRET', + useValue: 'test-jwt-refresh-secret' + }, + { + provide : 'JWT_REFRESH_EXPIRES_IN', + useValue: '7d' + }, + { + provide : 'WHITELISTED_USERS', + useValue: 'tomast1337,bentroen,testuser' + }, + { + provide : 'APP_DOMAIN', + useValue: '.test.com' + } + ] + }).compile(); + + authService = module.get(AuthService); + userService = module.get(UserService); + jwtService = module.get(JwtService); }); - it('should throw an error if no token is provided', async () => { - const req = { headers: { authorization: 'Bearer ' } } as Request; + it('should be defined', () => { + expect(authService).toBeDefined(); + }); - const res = { - status: jest.fn().mockReturnThis(), - json : jest.fn() - } as any; + describe('verifyToken', () => { + it('should throw an error if no authorization header is provided', async () => { + const req = { headers: {} } as Request; - await authService.verifyToken(req, res); + const res = { + status: jest.fn().mockReturnThis(), + json : jest.fn() + } as any; - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ message: 'No token provided' }); - }); + await authService.verifyToken(req, res); - it('should throw an error if user is not found', async () => { - const req = { - headers: { authorization: 'Bearer test-token' } - } as Request; + expect(res.status).toHaveBeenCalledWith(401); - const res = { - status: jest.fn().mockReturnThis(), - json : jest.fn() - } as any; + expect(res.json).toHaveBeenCalledWith({ + message: 'No authorization header' + }); + }); - mockJwtService.verify.mockReturnValueOnce({ id: 'test-id' }); - mockUserService.findByID.mockResolvedValueOnce(null); + it('should throw an error if no token is provided', async () => { + const req = { headers: { authorization: 'Bearer ' } } as Request; - await authService.verifyToken(req, res); + const res = { + status: jest.fn().mockReturnThis(), + json : jest.fn() + } as any; - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ message: 'Unauthorized' }); - }); + await authService.verifyToken(req, res); - it('should return decoded token if user is found', async () => { - const req = { - headers: { authorization: 'Bearer test-token' } - } as Request; + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: 'No token provided' }); + }); - const res = { - status: jest.fn().mockReturnThis(), - json : jest.fn() - } as any; + it('should throw an error if user is not found', async () => { + const req = { + headers: { authorization: 'Bearer test-token' } + } as Request; - const decodedToken = { id: 'test-id' }; - mockJwtService.verify.mockReturnValueOnce(decodedToken); - mockUserService.findByID.mockResolvedValueOnce({ id: 'test-id' }); + const res = { + status: jest.fn().mockReturnThis(), + json : jest.fn() + } as any; - const result = await authService.verifyToken(req, res); - expect(result).toEqual(decodedToken); - }); - }); + mockJwtService.verify.mockReturnValueOnce({ id: 'test-id' }); + mockUserService.findByID.mockResolvedValueOnce(null); + + await authService.verifyToken(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: 'Unauthorized' }); + }); + + it('should return decoded token if user is found', async () => { + const req = { + headers: { authorization: 'Bearer test-token' } + } as Request; - describe('getUserFromToken', () => { - it('should return null if token is invalid', async () => { - mockJwtService.decode.mockReturnValueOnce(null); + const res = { + status: jest.fn().mockReturnThis(), + json : jest.fn() + } as any; - const result = await authService.getUserFromToken('invalid-token'); - expect(result).toBeNull(); + const decodedToken = { id: 'test-id' }; + mockJwtService.verify.mockReturnValueOnce(decodedToken); + mockUserService.findByID.mockResolvedValueOnce({ id: 'test-id' }); + + const result = await authService.verifyToken(req, res); + expect(result).toEqual(decodedToken); + }); }); - it('should return user if token is valid', async () => { - const decodedToken = { id: 'test-id' }; - mockJwtService.decode.mockReturnValueOnce(decodedToken); - mockUserService.findByID.mockResolvedValueOnce({ id: 'test-id' }); + describe('getUserFromToken', () => { + it('should return null if token is invalid', async () => { + mockJwtService.decode.mockReturnValueOnce(null); + + const result = await authService.getUserFromToken('invalid-token'); + expect(result).toBeNull(); + }); - const result = await authService.getUserFromToken('valid-token'); - expect(result).toEqual({ id: 'test-id' } as UserDocument); + it('should return user if token is valid', async () => { + const decodedToken = { id: 'test-id' }; + mockJwtService.decode.mockReturnValueOnce(decodedToken); + mockUserService.findByID.mockResolvedValueOnce({ id: 'test-id' }); + + const result = await authService.getUserFromToken('valid-token'); + expect(result).toEqual({ id: 'test-id' } as UserDocument); + }); }); - }); - - describe('createJwtPayload', () => { - it('should create access and refresh tokens', async () => { - const payload = { id: 'user-id', username: 'testuser' }; - const accessToken = 'access-token'; - const refreshToken = 'refresh-token'; - - spyOn(jwtService, 'signAsync').mockImplementation( - (payload, options: any) => { - if (options.secret === 'test-jwt-secret') { - return Promise.resolve(accessToken); - } else if (options.secret === 'test-jwt-refresh-secret') { - return Promise.resolve(refreshToken); - } - - return Promise.reject(new Error('Invalid secret')); - } - ); - - const tokens = await (authService as any).createJwtPayload(payload); - - expect(tokens).toEqual({ - access_token : accessToken, - refresh_token: refreshToken - }); - - expect(jwtService.signAsync).toHaveBeenCalledWith(payload, { - secret : 'test-jwt-secret', - expiresIn: '1d' - }); - - expect(jwtService.signAsync).toHaveBeenCalledWith(payload, { - secret : 'test-jwt-refresh-secret', - expiresIn: '7d' - }); + + describe('createJwtPayload', () => { + it('should create access and refresh tokens', async () => { + const payload = { id: 'user-id', username: 'testuser' }; + const accessToken = 'access-token'; + const refreshToken = 'refresh-token'; + + spyOn(jwtService, 'signAsync').mockImplementation( + (payload, options: any) => { + if (options.secret === 'test-jwt-secret') { + return Promise.resolve(accessToken); + } else if (options.secret === 'test-jwt-refresh-secret') { + return Promise.resolve(refreshToken); + } + + return Promise.reject(new Error('Invalid secret')); + } + ); + + const tokens = await (authService as any).createJwtPayload(payload); + + expect(tokens).toEqual({ + access_token : accessToken, + refresh_token: refreshToken + }); + + expect(jwtService.signAsync).toHaveBeenCalledWith(payload, { + secret : 'test-jwt-secret', + expiresIn: '1d' + }); + + expect(jwtService.signAsync).toHaveBeenCalledWith(payload, { + secret : 'test-jwt-refresh-secret', + expiresIn: '7d' + }); + }); }); - }); - - describe('GenTokenRedirect', () => { - it('should set cookies and redirect to the frontend URL', async () => { - const user_registered = { - _id : 'user-id', - email : 'test@example.com', - username: 'testuser' - } as unknown as UserDocument; - - const res = { - cookie : jest.fn(), - redirect: jest.fn() - } as unknown as Response; - - const tokens = { - access_token : 'access-token', - refresh_token: 'refresh-token' - }; - - spyOn(authService as any, 'createJwtPayload').mockResolvedValue(tokens); - - await (authService as any).GenTokenRedirect(user_registered, res); - - expect((authService as any).createJwtPayload).toHaveBeenCalledWith({ - id : 'user-id', - email : 'test@example.com', - username: 'testuser' - }); - - expect(res.cookie).toHaveBeenCalledWith('token', 'access-token', { - domain: '.test.com', - maxAge: 3600000 - }); - - expect(res.cookie).toHaveBeenCalledWith( - 'refresh_token', - 'refresh-token', - { - domain: '.test.com', - maxAge: 3600000 - } - ); - - expect(res.redirect).toHaveBeenCalledWith('http://frontend.test.com/'); + + describe('GenTokenRedirect', () => { + it('should set cookies and redirect to the frontend URL', async () => { + const user_registered = { + _id : 'user-id', + email : 'test@example.com', + username: 'testuser' + } as unknown as UserDocument; + + const res = { + cookie : jest.fn(), + redirect: jest.fn() + } as unknown as Response; + + const tokens = { + access_token : 'access-token', + refresh_token: 'refresh-token' + }; + + spyOn(authService as any, 'createJwtPayload').mockResolvedValue(tokens); + + await (authService as any).GenTokenRedirect(user_registered, res); + + expect((authService as any).createJwtPayload).toHaveBeenCalledWith({ + id : 'user-id', + email : 'test@example.com', + username: 'testuser' + }); + + expect(res.cookie).toHaveBeenCalledWith('token', 'access-token', { + domain: '.test.com', + maxAge: 3600000 + }); + + expect(res.cookie).toHaveBeenCalledWith( + 'refresh_token', + 'refresh-token', + { + domain: '.test.com', + maxAge: 3600000 + } + ); + + expect(res.redirect).toHaveBeenCalledWith('http://frontend.test.com/'); + }); }); - }); - describe('verifyAndGetUser', () => { - it('should create a new user if the user is not registered', async () => { - const user: Profile = { - username : 'testuser', - email : 'test@example.com', - profileImage: 'http://example.com/photo.jpg' - }; + describe('verifyAndGetUser', () => { + it('should create a new user if the user is not registered', async () => { + const user: Profile = { + username : 'testuser', + email : 'test@example.com', + profileImage: 'http://example.com/photo.jpg' + }; - mockUserService.findByEmail.mockResolvedValue(null); - mockUserService.create.mockResolvedValue({ id: 'new-user-id' }); + mockUserService.findByEmail.mockResolvedValue(null); + mockUserService.create.mockResolvedValue({ id: 'new-user-id' }); - const result = await (authService as any).verifyAndGetUser(user); + const result = await (authService as any).verifyAndGetUser(user); - expect(userService.findByEmail).toHaveBeenCalledWith('test@example.com'); + expect(userService.findByEmail).toHaveBeenCalledWith('test@example.com'); - expect(userService.create).toHaveBeenCalledWith( - expect.objectContaining({ - email : 'test@example.com', - profileImage: 'http://example.com/photo.jpg' - }) - ); + expect(userService.create).toHaveBeenCalledWith( + expect.objectContaining({ + email : 'test@example.com', + profileImage: 'http://example.com/photo.jpg' + }) + ); - expect(result).toEqual({ id: 'new-user-id' }); - }); + expect(result).toEqual({ id: 'new-user-id' }); + }); - it('should return the registered user if the user is already registered', async () => { - const user: Profile = { - username : 'testuser', - email : 'test@example.com', - profileImage: 'http://example.com/photo.jpg' - }; + it('should return the registered user if the user is already registered', async () => { + const user: Profile = { + username : 'testuser', + email : 'test@example.com', + profileImage: 'http://example.com/photo.jpg' + }; - const registeredUser = { - id : 'registered-user-id', - profileImage: 'http://example.com/photo.jpg' - }; + const registeredUser = { + id : 'registered-user-id', + profileImage: 'http://example.com/photo.jpg' + }; - mockUserService.findByEmail.mockResolvedValue(registeredUser); + mockUserService.findByEmail.mockResolvedValue(registeredUser); - const result = await (authService as any).verifyAndGetUser(user); + const result = await (authService as any).verifyAndGetUser(user); - expect(userService.findByEmail).toHaveBeenCalledWith('test@example.com'); - expect(result).toEqual(registeredUser); - }); + expect(userService.findByEmail).toHaveBeenCalledWith('test@example.com'); + expect(result).toEqual(registeredUser); + }); - it('should update the profile image if it has changed', async () => { - const user: Profile = { - username : 'testuser', - email : 'test@example.com', - profileImage: 'http://example.com/new-photo.jpg' - }; + it('should update the profile image if it has changed', async () => { + const user: Profile = { + username : 'testuser', + email : 'test@example.com', + profileImage: 'http://example.com/new-photo.jpg' + }; - const registeredUser = { - id : 'registered-user-id', - profileImage: 'http://example.com/old-photo.jpg', - save : jest.fn() - }; + const registeredUser = { + id : 'registered-user-id', + profileImage: 'http://example.com/old-photo.jpg', + save : jest.fn() + }; - mockUserService.findByEmail.mockResolvedValue(registeredUser); + mockUserService.findByEmail.mockResolvedValue(registeredUser); - const result = await (authService as any).verifyAndGetUser(user); + const result = await (authService as any).verifyAndGetUser(user); - expect(userService.findByEmail).toHaveBeenCalledWith('test@example.com'); + expect(userService.findByEmail).toHaveBeenCalledWith('test@example.com'); - expect(registeredUser.profileImage).toEqual( - 'http://example.com/new-photo.jpg' - ); + expect(registeredUser.profileImage).toEqual( + 'http://example.com/new-photo.jpg' + ); - expect(registeredUser.save).toHaveBeenCalled(); - expect(result).toEqual(registeredUser); + expect(registeredUser.save).toHaveBeenCalled(); + expect(result).toEqual(registeredUser); + }); }); - }); - describe('createNewUser', () => undefined); + describe('createNewUser', () => undefined); }); diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index 4be506fc..cccc2e27 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -15,214 +15,214 @@ import { TokenPayload, Tokens } from './types/token'; @Injectable() export class AuthService { - private readonly logger = new Logger(AuthService.name); - constructor( - @Inject(UserService) - private readonly userService: UserService, - @Inject(JwtService) - private readonly jwtService: JwtService, - @Inject('COOKIE_EXPIRES_IN') - private readonly COOKIE_EXPIRES_IN: string, - @Inject('FRONTEND_URL') - private readonly FRONTEND_URL: string, - - @Inject('JWT_SECRET') - private readonly JWT_SECRET: string, - @Inject('JWT_EXPIRES_IN') - private readonly JWT_EXPIRES_IN: string, - @Inject('JWT_REFRESH_SECRET') - private readonly JWT_REFRESH_SECRET: string, - @Inject('JWT_REFRESH_EXPIRES_IN') - private readonly JWT_REFRESH_EXPIRES_IN: string, - @Inject('APP_DOMAIN') - private readonly APP_DOMAIN?: string - ) {} - - public async verifyToken(req: Request, res: Response) { - const headers = req.headers; - const authorizationHeader = headers.authorization; - - if (!authorizationHeader) { - return res.status(401).json({ message: 'No authorization header' }); - } + private readonly logger = new Logger(AuthService.name); + constructor( + @Inject(UserService) + private readonly userService: UserService, + @Inject(JwtService) + private readonly jwtService: JwtService, + @Inject('COOKIE_EXPIRES_IN') + private readonly COOKIE_EXPIRES_IN: string, + @Inject('FRONTEND_URL') + private readonly FRONTEND_URL: string, + + @Inject('JWT_SECRET') + private readonly JWT_SECRET: string, + @Inject('JWT_EXPIRES_IN') + private readonly JWT_EXPIRES_IN: string, + @Inject('JWT_REFRESH_SECRET') + private readonly JWT_REFRESH_SECRET: string, + @Inject('JWT_REFRESH_EXPIRES_IN') + private readonly JWT_REFRESH_EXPIRES_IN: string, + @Inject('APP_DOMAIN') + private readonly APP_DOMAIN?: string + ) {} + + public async verifyToken(req: Request, res: Response) { + const headers = req.headers; + const authorizationHeader = headers.authorization; + + if (!authorizationHeader) { + return res.status(401).json({ message: 'No authorization header' }); + } - const token = authorizationHeader.split(' ')[1]; + const token = authorizationHeader.split(' ')[1]; - if (!token) { - return res.status(401).json({ message: 'No token provided' }); - } + if (!token) { + return res.status(401).json({ message: 'No token provided' }); + } - try { - const decoded = this.jwtService.verify(token, { - secret: this.JWT_SECRET - }); - - // verify if user exists - const user_registered = await this.userService.findByID(decoded.id); - - if (!user_registered) { - return res.status(401).json({ message: 'Unauthorized' }); - } else { - return decoded; - } - } catch (error) { - return res.status(401).json({ message: 'Unauthorized' }); + try { + const decoded = this.jwtService.verify(token, { + secret: this.JWT_SECRET + }); + + // verify if user exists + const user_registered = await this.userService.findByID(decoded.id); + + if (!user_registered) { + return res.status(401).json({ message: 'Unauthorized' }); + } else { + return decoded; + } + } catch (error) { + return res.status(401).json({ message: 'Unauthorized' }); + } } - } - - public async googleLogin(req: Request, res: Response) { - const user = req.user as GoogleProfile; - const email = user.emails[0].value; - const profile = { - // Generate username from display name - username : email.split('@')[0], - email : email, - profileImage: user.photos[0].value - }; + public async googleLogin(req: Request, res: Response) { + const user = req.user as GoogleProfile; + const email = user.emails[0].value; - // verify if user exists - const user_registered = await this.verifyAndGetUser(profile); + const profile = { + // Generate username from display name + username : email.split('@')[0], + email : email, + profileImage: user.photos[0].value + }; - return this.GenTokenRedirect(user_registered, res); - } + // verify if user exists + const user_registered = await this.verifyAndGetUser(profile); - private async createNewUser(user: Profile) { - const { username, email, profileImage } = user; - const baseUsername = username; - const newUsername = await this.userService.generateUsername(baseUsername); - - const newUser = new CreateUser({ - username : newUsername, - email : email, - profileImage: profileImage - }); + return this.GenTokenRedirect(user_registered, res); + } - return await this.userService.create(newUser); - } + private async createNewUser(user: Profile) { + const { username, email, profileImage } = user; + const baseUsername = username; + const newUsername = await this.userService.generateUsername(baseUsername); - private async verifyAndGetUser(user: Profile) { - const user_registered = await this.userService.findByEmail(user.email); + const newUser = new CreateUser({ + username : newUsername, + email : email, + profileImage: profileImage + }); - if (!user_registered) { - return await this.createNewUser(user); + return await this.userService.create(newUser); } - // Update profile picture if it has changed - if (user_registered.profileImage !== user.profileImage) { - user_registered.profileImage = user.profileImage; - await user_registered.save(); - } + private async verifyAndGetUser(user: Profile) { + const user_registered = await this.userService.findByEmail(user.email); - return user_registered; - } - - public async githubLogin(req: Request, res: Response) { - const user = req.user as GithubAccessToken; - const { profile } = user; + if (!user_registered) { + return await this.createNewUser(user); + } - // verify if user exists - const response = await axios.get( - 'https://api.github.com/user/emails', - { - headers: { - Authorization: `token ${user.accessToken}` + // Update profile picture if it has changed + if (user_registered.profileImage !== user.profileImage) { + user_registered.profileImage = user.profileImage; + await user_registered.save(); } - } - ); - const email = response.data.filter((email) => email.primary)[0].email; + return user_registered; + } - const user_registered = await this.verifyAndGetUser({ - username : profile.username, - email : email, - profileImage: profile.photos[0].value - }); + public async githubLogin(req: Request, res: Response) { + const user = req.user as GithubAccessToken; + const { profile } = user; + + // verify if user exists + const response = await axios.get( + 'https://api.github.com/user/emails', + { + headers: { + Authorization: `token ${user.accessToken}` + } + } + ); + + const email = response.data.filter((email) => email.primary)[0].email; + + const user_registered = await this.verifyAndGetUser({ + username : profile.username, + email : email, + profileImage: profile.photos[0].value + }); + + return this.GenTokenRedirect(user_registered, res); + } - return this.GenTokenRedirect(user_registered, res); - } + public async discordLogin(req: Request, res: Response) { + const user = (req.user as DiscordUser).profile; + const profilePictureUrl = `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`; - public async discordLogin(req: Request, res: Response) { - const user = (req.user as DiscordUser).profile; - const profilePictureUrl = `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`; + const profile = { + // Generate username from display name + username : user.username, + email : user.email, + profileImage: profilePictureUrl + }; - const profile = { - // Generate username from display name - username : user.username, - email : user.email, - profileImage: profilePictureUrl - }; + // verify if user exists + const user_registered = await this.verifyAndGetUser(profile); - // verify if user exists - const user_registered = await this.verifyAndGetUser(profile); + return this.GenTokenRedirect(user_registered, res); + } - return this.GenTokenRedirect(user_registered, res); - } + public async loginWithEmail(req: Request, res: Response) { + const user = req.user as UserDocument; + + if (!user) { + return res.redirect(this.FRONTEND_URL + '/login'); + } - public async loginWithEmail(req: Request, res: Response) { - const user = req.user as UserDocument; + return this.GenTokenRedirect(user, res); + } - if (!user) { - return res.redirect(this.FRONTEND_URL + '/login'); + public async createJwtPayload(payload: TokenPayload): Promise { + const [accessToken, refreshToken] = await Promise.all([ + this.jwtService.signAsync(payload, { + secret : this.JWT_SECRET, + expiresIn: this.JWT_EXPIRES_IN + }), + this.jwtService.signAsync(payload, { + secret : this.JWT_REFRESH_SECRET, + expiresIn: this.JWT_REFRESH_EXPIRES_IN + }) + ]); + + return { + access_token : accessToken, + refresh_token: refreshToken + }; } - return this.GenTokenRedirect(user, res); - } - - public async createJwtPayload(payload: TokenPayload): Promise { - const [accessToken, refreshToken] = await Promise.all([ - this.jwtService.signAsync(payload, { - secret : this.JWT_SECRET, - expiresIn: this.JWT_EXPIRES_IN - }), - this.jwtService.signAsync(payload, { - secret : this.JWT_REFRESH_SECRET, - expiresIn: this.JWT_REFRESH_EXPIRES_IN - }) - ]); - - return { - access_token : accessToken, - refresh_token: refreshToken - }; - } - - private async GenTokenRedirect( - user_registered: UserDocument, - res: Response> - ): Promise { - const token = await this.createJwtPayload({ - id : user_registered._id.toString(), - email : user_registered.email, - username: user_registered.username - }); - - const frontEndURL = this.FRONTEND_URL; - const domain = this.APP_DOMAIN; - const maxAge = parseInt(this.COOKIE_EXPIRES_IN) * 1000; - - res.cookie('token', token.access_token, { - domain: domain, - maxAge: maxAge - }); - - res.cookie('refresh_token', token.refresh_token, { - domain: domain, - maxAge: maxAge - }); - - res.redirect(frontEndURL + '/'); - } - - public async getUserFromToken(token: string): Promise { - const decoded = this.jwtService.decode(token) as TokenPayload; - - if (!decoded) { - return null; + private async GenTokenRedirect( + user_registered: UserDocument, + res: Response> + ): Promise { + const token = await this.createJwtPayload({ + id : user_registered._id.toString(), + email : user_registered.email, + username: user_registered.username + }); + + const frontEndURL = this.FRONTEND_URL; + const domain = this.APP_DOMAIN; + const maxAge = parseInt(this.COOKIE_EXPIRES_IN) * 1000; + + res.cookie('token', token.access_token, { + domain: domain, + maxAge: maxAge + }); + + res.cookie('refresh_token', token.refresh_token, { + domain: domain, + maxAge: maxAge + }); + + res.redirect(frontEndURL + '/'); } - const user = await this.userService.findByID(decoded.id); + public async getUserFromToken(token: string): Promise { + const decoded = this.jwtService.decode(token) as TokenPayload; + + if (!decoded) { + return null; + } - return user; - } + const user = await this.userService.findByID(decoded.id); + + return user; + } } diff --git a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts index 1a9de82b..9290a530 100644 --- a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts @@ -7,88 +7,88 @@ import type { Request } from 'express'; import { JwtStrategy } from './JWT.strategy'; describe('JwtStrategy', () => { - let jwtStrategy: JwtStrategy; - let configService: ConfigService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - JwtStrategy, - { - provide : ConfigService, - useValue: { - getOrThrow: jest.fn().mockReturnValue('test-secret') - } - } - ] - }).compile(); - - jwtStrategy = module.get(JwtStrategy); - configService = module.get(ConfigService); - }); - - it('should be defined', () => { - expect(jwtStrategy).toBeDefined(); - }); - - describe('constructor', () => { - it('should throw an error if JWT_SECRET is not set', () => { - jest.spyOn(configService, 'getOrThrow').mockReturnValue(null); - - expect(() => new JwtStrategy(configService)).toThrowError( - 'JwtStrategy requires a secret or key' - ); + let jwtStrategy: JwtStrategy; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + JwtStrategy, + { + provide : ConfigService, + useValue: { + getOrThrow: jest.fn().mockReturnValue('test-secret') + } + } + ] + }).compile(); + + jwtStrategy = module.get(JwtStrategy); + configService = module.get(ConfigService); }); - }); - describe('validate', () => { - it('should return payload with refreshToken from header', () => { - const req = { - headers: { - authorization: 'Bearer test-refresh-token' - }, - cookies: {} - } as unknown as Request; - - const payload = { userId: 'test-user-id' }; - - const result = jwtStrategy.validate(req, payload); - - expect(result).toEqual({ - ...payload, - refreshToken: 'test-refresh-token' - }); + it('should be defined', () => { + expect(jwtStrategy).toBeDefined(); }); - it('should return payload with refreshToken from cookie', () => { - const req = { - headers: {}, - cookies: { - refresh_token: 'test-refresh-token' - } - } as unknown as Request; - - const payload = { userId: 'test-user-id' }; + describe('constructor', () => { + it('should throw an error if JWT_SECRET is not set', () => { + jest.spyOn(configService, 'getOrThrow').mockReturnValue(null); - const result = jwtStrategy.validate(req, payload); - - expect(result).toEqual({ - ...payload, - refreshToken: 'test-refresh-token' - }); + expect(() => new JwtStrategy(configService)).toThrowError( + 'JwtStrategy requires a secret or key' + ); + }); }); - it('should throw an error if no refresh token is provided', () => { - const req = { - headers: {}, - cookies: {} - } as unknown as Request; - - const payload = { userId: 'test-user-id' }; - - expect(() => jwtStrategy.validate(req, payload)).toThrowError( - 'No refresh token' - ); + describe('validate', () => { + it('should return payload with refreshToken from header', () => { + const req = { + headers: { + authorization: 'Bearer test-refresh-token' + }, + cookies: {} + } as unknown as Request; + + const payload = { userId: 'test-user-id' }; + + const result = jwtStrategy.validate(req, payload); + + expect(result).toEqual({ + ...payload, + refreshToken: 'test-refresh-token' + }); + }); + + it('should return payload with refreshToken from cookie', () => { + const req = { + headers: {}, + cookies: { + refresh_token: 'test-refresh-token' + } + } as unknown as Request; + + const payload = { userId: 'test-user-id' }; + + const result = jwtStrategy.validate(req, payload); + + expect(result).toEqual({ + ...payload, + refreshToken: 'test-refresh-token' + }); + }); + + it('should throw an error if no refresh token is provided', () => { + const req = { + headers: {}, + cookies: {} + } as unknown as Request; + + const payload = { userId: 'test-user-id' }; + + expect(() => jwtStrategy.validate(req, payload)).toThrowError( + 'No refresh token' + ); + }); }); - }); }); diff --git a/apps/backend/src/auth/strategies/JWT.strategy.ts b/apps/backend/src/auth/strategies/JWT.strategy.ts index aa037f28..87695398 100644 --- a/apps/backend/src/auth/strategies/JWT.strategy.ts +++ b/apps/backend/src/auth/strategies/JWT.strategy.ts @@ -6,32 +6,32 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { - private static logger = new Logger(JwtStrategy.name); - constructor(@Inject(ConfigService) config: ConfigService) { - const JWT_SECRET = config.getOrThrow('JWT_SECRET'); + private static logger = new Logger(JwtStrategy.name); + constructor(@Inject(ConfigService) config: ConfigService) { + const JWT_SECRET = config.getOrThrow('JWT_SECRET'); - super({ - jwtFromRequest : ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey : JWT_SECRET, - passReqToCallback: true - }); - } + super({ + jwtFromRequest : ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey : JWT_SECRET, + passReqToCallback: true + }); + } - public validate(req: Request, payload: any) { - const refreshTokenHeader = req.headers?.authorization; - const refreshTokenCookie = req.cookies?.refresh_token; + public validate(req: Request, payload: any) { + const refreshTokenHeader = req.headers?.authorization; + const refreshTokenCookie = req.cookies?.refresh_token; - const refreshToken = refreshTokenHeader - ? refreshTokenHeader.split(' ')[1] - : refreshTokenCookie; + const refreshToken = refreshTokenHeader + ? refreshTokenHeader.split(' ')[1] + : refreshTokenCookie; - if (!refreshToken) { - throw new Error('No refresh token'); - } + if (!refreshToken) { + throw new Error('No refresh token'); + } - return { - ...payload, - refreshToken - }; - } + return { + ...payload, + refreshToken + }; + } } diff --git a/apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts b/apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts index 02f6d42d..9ad4cc0f 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/DiscordStrategyConfig.ts @@ -1,14 +1,14 @@ import { - IsArray, - IsBoolean, - IsEnum, - IsNumber, - IsOptional, - IsString + IsArray, + IsBoolean, + IsEnum, + IsNumber, + IsOptional, + IsString } from 'class-validator'; import { - StrategyOptions as OAuth2StrategyOptions, - StrategyOptionsWithRequest as OAuth2StrategyOptionsWithRequest + StrategyOptions as OAuth2StrategyOptions, + StrategyOptionsWithRequest as OAuth2StrategyOptionsWithRequest } from 'passport-oauth2'; import type { ScopeType } from './types'; @@ -18,43 +18,43 @@ type MergedOAuth2StrategyOptions = | OAuth2StrategyOptionsWithRequest; type DiscordStrategyOptions = Pick< - MergedOAuth2StrategyOptions, + MergedOAuth2StrategyOptions, 'clientID' | 'clientSecret' | 'scope' >; export class DiscordStrategyConfig implements DiscordStrategyOptions { - // The client ID assigned by Discord. - @IsString() - clientID: string; - - // The client secret assigned by Discord. - @IsString() - clientSecret: string; - - // The URL to which Discord will redirect the user after granting authorization. - @IsString() - callbackUrl: string; - - // An array of permission scopes to request. - @IsArray() - @IsString({ each: true }) - scope: ScopeType; - - // The delay in milliseconds between requests for the same scope. - @IsOptional() - @IsNumber() - scopeDelay?: number; - - // Whether to fetch data for the specified scope. - @IsOptional() - @IsBoolean() - fetchScope?: boolean; - - @IsEnum(['none', 'consent']) - prompt: 'consent' | 'none'; - - // The separator for the scope values. - @IsOptional() - @IsString() - scopeSeparator?: string; + // The client ID assigned by Discord. + @IsString() + clientID: string; + + // The client secret assigned by Discord. + @IsString() + clientSecret: string; + + // The URL to which Discord will redirect the user after granting authorization. + @IsString() + callbackUrl: string; + + // An array of permission scopes to request. + @IsArray() + @IsString({ each: true }) + scope: ScopeType; + + // The delay in milliseconds between requests for the same scope. + @IsOptional() + @IsNumber() + scopeDelay?: number; + + // Whether to fetch data for the specified scope. + @IsOptional() + @IsBoolean() + fetchScope?: boolean; + + @IsEnum(['none', 'consent']) + prompt: 'consent' | 'none'; + + // The separator for the scope values. + @IsOptional() + @IsString() + scopeSeparator?: string; } diff --git a/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts b/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts index 0943fe9a..d64e2150 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts @@ -5,174 +5,174 @@ import DiscordStrategy from './Strategy'; import { DiscordPermissionScope, Profile } from './types'; describe('DiscordStrategy', () => { - let strategy: DiscordStrategy; - const verify: VerifyFunction = jest.fn(); - - beforeEach(() => { - const config: DiscordStrategyConfig = { - clientID : 'test-client-id', - clientSecret: 'test-client-secret', - callbackUrl : 'http://localhost:3000/callback', - scope : [ - DiscordPermissionScope.Email, - DiscordPermissionScope.Identify, - DiscordPermissionScope.Connections - // DiscordPermissionScope.Bot, // Not allowed scope - ], - prompt: 'consent' - }; - - strategy = new DiscordStrategy(config, verify); - }); - - it('should be defined', () => { - expect(strategy).toBeDefined(); - }); - - it('should have the correct name', () => { - expect(strategy.name).toBe('discord'); - }); - - it('should validate config', async () => { - const config: DiscordStrategyConfig = { - clientID : 'test-client-id', - clientSecret: 'test-client-secret', - callbackUrl : 'http://localhost:3000/callback', - scope : [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], - prompt : 'consent' - }; - - await expect(strategy['validateConfig'](config)).resolves.toBeUndefined(); - }); - - it('should make API request', async () => { - const mockGet = jest.fn((url, accessToken, callback) => { - callback(null, JSON.stringify({ id: '123' })); + let strategy: DiscordStrategy; + const verify: VerifyFunction = jest.fn(); + + beforeEach(() => { + const config: DiscordStrategyConfig = { + clientID : 'test-client-id', + clientSecret: 'test-client-secret', + callbackUrl : 'http://localhost:3000/callback', + scope : [ + DiscordPermissionScope.Email, + DiscordPermissionScope.Identify, + DiscordPermissionScope.Connections + // DiscordPermissionScope.Bot, // Not allowed scope + ], + prompt: 'consent' + }; + + strategy = new DiscordStrategy(config, verify); }); - strategy['_oauth2'].get = mockGet; - - const result = await strategy['makeApiRequest']<{ id: string }>( - 'https://discord.com/api/users/@me', - 'test-access-token' - ); - - expect(result).toEqual({ id: '123' }); - }); - - it('should fetch user data', async () => { - const mockMakeApiRequest = jest.fn().mockResolvedValue({ id: '123' }); - strategy['makeApiRequest'] = mockMakeApiRequest; - - const result = await strategy['fetchUserData']('test-access-token'); - - expect(result).toEqual({ id: '123' }); - }); - - it('should build profile', () => { - const profileData = { - id : '123', - username : 'testuser', - displayName : 'Test User', - avatar : 'avatar.png', - banner : 'banner.png', - email : 'test@example.com', - verified : true, - mfa_enabled : true, - public_flags: 1, - flags : 1, - locale : 'en-US', - global_name : 'testuser#1234', - premium_type: 1, - connections : [], - guilds : [] - } as unknown as Profile; - - const profile = strategy['buildProfile'](profileData, 'test-access-token'); - - expect(profile).toMatchObject({ - provider : 'discord', - id : '123', - username : 'testuser', - displayName : 'Test User', - avatar : 'avatar.png', - banner : 'banner.png', - email : 'test@example.com', - verified : true, - mfa_enabled : true, - public_flags: 1, - flags : 1, - locale : 'en-US', - global_name : 'testuser#1234', - premium_type: 1, - connections : [], - guilds : [], - access_token: 'test-access-token', - fetchedAt : expect.any(Date), - createdAt : expect.any(Date), - _raw : JSON.stringify(profileData), - _json : profileData + it('should be defined', () => { + expect(strategy).toBeDefined(); }); - }); - it('should fetch scope data', async () => { - const mockMakeApiRequest = jest.fn().mockResolvedValue([{ id: '123' }]); - strategy['makeApiRequest'] = mockMakeApiRequest; + it('should have the correct name', () => { + expect(strategy.name).toBe('discord'); + }); + + it('should validate config', async () => { + const config: DiscordStrategyConfig = { + clientID : 'test-client-id', + clientSecret: 'test-client-secret', + callbackUrl : 'http://localhost:3000/callback', + scope : [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], + prompt : 'consent' + }; + + await expect(strategy['validateConfig'](config)).resolves.toBeUndefined(); + }); + + it('should make API request', async () => { + const mockGet = jest.fn((url, accessToken, callback) => { + callback(null, JSON.stringify({ id: '123' })); + }); + + strategy['_oauth2'].get = mockGet; + + const result = await strategy['makeApiRequest']<{ id: string }>( + 'https://discord.com/api/users/@me', + 'test-access-token' + ); + + expect(result).toEqual({ id: '123' }); + }); + + it('should fetch user data', async () => { + const mockMakeApiRequest = jest.fn().mockResolvedValue({ id: '123' }); + strategy['makeApiRequest'] = mockMakeApiRequest; - const result = await strategy['fetchScopeData']( - DiscordPermissionScope.Connections, - 'test-access-token' - ); + const result = await strategy['fetchUserData']('test-access-token'); - expect(result).toEqual([{ id: '123' }]); - }); + expect(result).toEqual({ id: '123' }); + }); + + it('should build profile', () => { + const profileData = { + id : '123', + username : 'testuser', + displayName : 'Test User', + avatar : 'avatar.png', + banner : 'banner.png', + email : 'test@example.com', + verified : true, + mfa_enabled : true, + public_flags: 1, + flags : 1, + locale : 'en-US', + global_name : 'testuser#1234', + premium_type: 1, + connections : [], + guilds : [] + } as unknown as Profile; + + const profile = strategy['buildProfile'](profileData, 'test-access-token'); + + expect(profile).toMatchObject({ + provider : 'discord', + id : '123', + username : 'testuser', + displayName : 'Test User', + avatar : 'avatar.png', + banner : 'banner.png', + email : 'test@example.com', + verified : true, + mfa_enabled : true, + public_flags: 1, + flags : 1, + locale : 'en-US', + global_name : 'testuser#1234', + premium_type: 1, + connections : [], + guilds : [], + access_token: 'test-access-token', + fetchedAt : expect.any(Date), + createdAt : expect.any(Date), + _raw : JSON.stringify(profileData), + _json : profileData + }); + }); + + it('should fetch scope data', async () => { + const mockMakeApiRequest = jest.fn().mockResolvedValue([{ id: '123' }]); + strategy['makeApiRequest'] = mockMakeApiRequest; + + const result = await strategy['fetchScopeData']( + DiscordPermissionScope.Connections, + 'test-access-token' + ); - it('should no fetch out of scope data', async () => { - const mockMakeApiRequest = jest.fn().mockResolvedValue([{ id: '123' }]); - strategy['makeApiRequest'] = mockMakeApiRequest; + expect(result).toEqual([{ id: '123' }]); + }); - const result = await strategy['fetchScopeData']( - DiscordPermissionScope.Bot, - 'test-access-token' - ); + it('should no fetch out of scope data', async () => { + const mockMakeApiRequest = jest.fn().mockResolvedValue([{ id: '123' }]); + strategy['makeApiRequest'] = mockMakeApiRequest; - expect(result).toEqual(null); - }); + const result = await strategy['fetchScopeData']( + DiscordPermissionScope.Bot, + 'test-access-token' + ); - it('should enrich profile with scopes', async () => { - const profile = { - id : '123', - connections: [], - guilds : [] - } as unknown as Profile; + expect(result).toEqual(null); + }); - const mockFetchScopeData = jest - .fn() - .mockResolvedValueOnce([{ id: 'connection1' }]) - .mockResolvedValueOnce([{ id: 'guild1' }]); + it('should enrich profile with scopes', async () => { + const profile = { + id : '123', + connections: [], + guilds : [] + } as unknown as Profile; - strategy['fetchScopeData'] = mockFetchScopeData; + const mockFetchScopeData = jest + .fn() + .mockResolvedValueOnce([{ id: 'connection1' }]) + .mockResolvedValueOnce([{ id: 'guild1' }]); - await strategy['enrichProfileWithScopes'](profile, 'test-access-token'); + strategy['fetchScopeData'] = mockFetchScopeData; - expect(profile.connections).toEqual([{ id: 'connection1' }]); - expect(profile.guilds).toEqual([{ id: 'guild1' }]); - expect(profile.fetchedAt).toBeInstanceOf(Date); - }); + await strategy['enrichProfileWithScopes'](profile, 'test-access-token'); - it('should calculate creation date', () => { - const id = '123456789012345678'; - const date = strategy['calculateCreationDate'](id); + expect(profile.connections).toEqual([{ id: 'connection1' }]); + expect(profile.guilds).toEqual([{ id: 'guild1' }]); + expect(profile.fetchedAt).toBeInstanceOf(Date); + }); - expect(date).toBeInstanceOf(Date); - }); + it('should calculate creation date', () => { + const id = '123456789012345678'; + const date = strategy['calculateCreationDate'](id); + + expect(date).toBeInstanceOf(Date); + }); - it('should return authorization params', () => { - const options = { prompt: 'consent' }; - const params = strategy.authorizationParams(options); + it('should return authorization params', () => { + const options = { prompt: 'consent' }; + const params = strategy.authorizationParams(options); - expect(params).toMatchObject({ - prompt: 'consent' + expect(params).toMatchObject({ + prompt: 'consent' + }); }); - }); }); diff --git a/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts b/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts index eeb8111c..8a860163 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts @@ -2,248 +2,248 @@ import { Logger } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { validateOrReject } from 'class-validator'; import { - InternalOAuthError, - Strategy as OAuth2Strategy, - StrategyOptions as OAuth2StrategyOptions, - VerifyCallback, - VerifyFunction + InternalOAuthError, + Strategy as OAuth2Strategy, + StrategyOptions as OAuth2StrategyOptions, + VerifyCallback, + VerifyFunction } from 'passport-oauth2'; import { DiscordStrategyConfig } from './DiscordStrategyConfig'; import { - Profile, - ProfileConnection, - ProfileGuild, - ScopeType, - SingleScopeType + Profile, + ProfileConnection, + ProfileGuild, + ScopeType, + SingleScopeType } from './types'; interface AuthorizationParams { - prompt?: string; + prompt?: string; } export default class Strategy extends OAuth2Strategy { - // Static properties - public static DISCORD_EPOCH = 1420070400000; - public static DISCORD_SHIFT = 1 << 22; - - public static DISCORD_API_BASE = 'https://discord.com/api'; - - private readonly logger = new Logger('DiscordStrategy'); - private scope : ScopeType; - private scopeDelay : number; - private fetchScopeEnabled: boolean; - public override name = 'discord'; - prompt? : string; - public constructor(options: DiscordStrategyConfig, verify: VerifyFunction) { - super( - { - scopeSeparator : ' ', - ...options, - authorizationURL: 'https://discord.com/api/oauth2/authorize', - tokenURL : 'https://discord.com/api/oauth2/token' - } as OAuth2StrategyOptions, - verify - ); - - this.validateConfig(options); - this.scope = options.scope; - this.scopeDelay = options.scopeDelay ?? 0; - this.fetchScopeEnabled = options.fetchScope ?? true; - this._oauth2.useAuthorizationHeaderforGET(true); - this.prompt = options.prompt; - } - - private async validateConfig(config: DiscordStrategyConfig): Promise { - try { - const validatedConfig = plainToClass(DiscordStrategyConfig, config); - await validateOrReject(validatedConfig); - } catch (errors) { - this.logger.error(errors); - throw new Error(`Configuration validation failed: ${errors}`); + // Static properties + public static DISCORD_EPOCH = 1420070400000; + public static DISCORD_SHIFT = 1 << 22; + + public static DISCORD_API_BASE = 'https://discord.com/api'; + + private readonly logger = new Logger('DiscordStrategy'); + private scope : ScopeType; + private scopeDelay : number; + private fetchScopeEnabled: boolean; + public override name = 'discord'; + prompt? : string; + public constructor(options: DiscordStrategyConfig, verify: VerifyFunction) { + super( + { + scopeSeparator : ' ', + ...options, + authorizationURL: 'https://discord.com/api/oauth2/authorize', + tokenURL : 'https://discord.com/api/oauth2/token' + } as OAuth2StrategyOptions, + verify + ); + + this.validateConfig(options); + this.scope = options.scope; + this.scopeDelay = options.scopeDelay ?? 0; + this.fetchScopeEnabled = options.fetchScope ?? true; + this._oauth2.useAuthorizationHeaderforGET(true); + this.prompt = options.prompt; } - } - - private async makeApiRequest( - url: string, - accessToken: string - ): Promise { - return new Promise((resolve, reject) => { - this._oauth2.get(url, accessToken, (err, body) => { - if (err) { - reject(new InternalOAuthError(`Failed to fetch from ${url}`, err)); - return; + + private async validateConfig(config: DiscordStrategyConfig): Promise { + try { + const validatedConfig = plainToClass(DiscordStrategyConfig, config); + await validateOrReject(validatedConfig); + } catch (errors) { + this.logger.error(errors); + throw new Error(`Configuration validation failed: ${errors}`); } + } + private async makeApiRequest( + url: string, + accessToken: string + ): Promise { + return new Promise((resolve, reject) => { + this._oauth2.get(url, accessToken, (err, body) => { + if (err) { + reject(new InternalOAuthError(`Failed to fetch from ${url}`, err)); + return; + } + + try { + resolve(JSON.parse(body as string) as T); + } catch (parseError) { + reject(new Error(`Failed to parse response from ${url}`)); + } + }); + }); + } + + private async fetchUserData(accessToken: string): Promise { + return this.makeApiRequest( + `${Strategy.DISCORD_API_BASE}/users/@me`, + accessToken + ); + } + + public override async userProfile(accessToken: string, done: VerifyCallback) { try { - resolve(JSON.parse(body as string) as T); - } catch (parseError) { - reject(new Error(`Failed to parse response from ${url}`)); + const userData = await this.fetchUserData(accessToken); + const profile = this.buildProfile(userData, accessToken); + + if (this.fetchScopeEnabled) { + await this.enrichProfileWithScopes(profile, accessToken); + } + + done(null, profile); + } catch (error) { + this.logger.error('Failed to fetch user profile', error); + done(error); + } + } + + private async enrichProfileWithScopes( + profile: Profile, + accessToken: string + ): Promise { + await Promise.all([ + this.fetchScopeData('connections', accessToken).then( + (data) => (profile.connections = data as ProfileConnection[]) + ), + this.fetchScopeData('guilds', accessToken).then( + (data) => (profile.guilds = data as ProfileGuild[]) + ) + ]); + + profile.fetchedAt = new Date(); + } + + private async fetchScopeData( + scope: SingleScopeType, + accessToken: string + ): Promise { + if (!this.scope.includes(scope)) { + return null; + } + + if (this.scopeDelay > 0) { + await new Promise((resolve) => setTimeout(resolve, this.scopeDelay)); } - }); - }); - } - - private async fetchUserData(accessToken: string): Promise { - return this.makeApiRequest( - `${Strategy.DISCORD_API_BASE}/users/@me`, - accessToken - ); - } - - public override async userProfile(accessToken: string, done: VerifyCallback) { - try { - const userData = await this.fetchUserData(accessToken); - const profile = this.buildProfile(userData, accessToken); - - if (this.fetchScopeEnabled) { - await this.enrichProfileWithScopes(profile, accessToken); - } - - done(null, profile); - } catch (error) { - this.logger.error('Failed to fetch user profile', error); - done(error); + + return this.makeApiRequest( + `${Strategy.DISCORD_API_BASE}/users/@me/${scope}`, + accessToken + ); } - } - - private async enrichProfileWithScopes( - profile: Profile, - accessToken: string - ): Promise { - await Promise.all([ - this.fetchScopeData('connections', accessToken).then( - (data) => (profile.connections = data as ProfileConnection[]) - ), - this.fetchScopeData('guilds', accessToken).then( - (data) => (profile.guilds = data as ProfileGuild[]) - ) - ]); - - profile.fetchedAt = new Date(); - } - - private async fetchScopeData( - scope: SingleScopeType, - accessToken: string - ): Promise { - if (!this.scope.includes(scope)) { - return null; + + private calculateCreationDate(id: string) { + return new Date(+id / Strategy.DISCORD_SHIFT + Strategy.DISCORD_EPOCH); } - if (this.scopeDelay > 0) { - await new Promise((resolve) => setTimeout(resolve, this.scopeDelay)); + private buildProfile(data: Profile, accessToken: string): Profile { + const { id } = data; + return { + provider : 'discord', + id : id, + username : data.username, + displayName : data.displayName, + avatar : data.avatar, + banner : data.banner, + email : data.email, + verified : data.verified, + mfa_enabled : data.mfa_enabled, + public_flags: data.public_flags, + flags : data.flags, + locale : data.locale, + global_name : data.global_name, + premium_type: data.premium_type, + connections : data.connections, + guilds : data.guilds, + access_token: accessToken, + fetchedAt : new Date(), + createdAt : this.calculateCreationDate(id), + _raw : JSON.stringify(data), + _json : data as unknown as Record + }; } - return this.makeApiRequest( - `${Strategy.DISCORD_API_BASE}/users/@me/${scope}`, - accessToken - ); - } - - private calculateCreationDate(id: string) { - return new Date(+id / Strategy.DISCORD_SHIFT + Strategy.DISCORD_EPOCH); - } - - private buildProfile(data: Profile, accessToken: string): Profile { - const { id } = data; - return { - provider : 'discord', - id : id, - username : data.username, - displayName : data.displayName, - avatar : data.avatar, - banner : data.banner, - email : data.email, - verified : data.verified, - mfa_enabled : data.mfa_enabled, - public_flags: data.public_flags, - flags : data.flags, - locale : data.locale, - global_name : data.global_name, - premium_type: data.premium_type, - connections : data.connections, - guilds : data.guilds, - access_token: accessToken, - fetchedAt : new Date(), - createdAt : this.calculateCreationDate(id), - _raw : JSON.stringify(data), - _json : data as unknown as Record - }; - } - - public fetchScope( - scope: SingleScopeType, - accessToken: string, - callback: (err: Error | null, data: Record | null) => void - ): void { + public fetchScope( + scope: SingleScopeType, + accessToken: string, + callback: (err: Error | null, data: Record | null) => void + ): void { // Early return if scope is not included - if (!this.scope.includes(scope)) { - callback(null, null); - return; - } + if (!this.scope.includes(scope)) { + callback(null, null); + return; + } - // Handle scope delay - const delayPromise = new Promise((resolve) => - setTimeout(resolve, this.scopeDelay ?? 0) - ); - - delayPromise - .then(() => { - this._oauth2.get( - `${Strategy.DISCORD_API_BASE}/users/@me/${scope}`, - accessToken, - (err, body) => { - if (err) { - this.logger.error(`Failed to fetch scope ${scope}:`, err); - - callback( - new InternalOAuthError(`Failed to fetch scope: ${scope}`, err), - null - ); - - return; - } + // Handle scope delay + const delayPromise = new Promise((resolve) => + setTimeout(resolve, this.scopeDelay ?? 0) + ); - try { - if (typeof body !== 'string') { - const error = new Error( - `Invalid response type for scope: ${scope}` + delayPromise + .then(() => { + this._oauth2.get( + `${Strategy.DISCORD_API_BASE}/users/@me/${scope}`, + accessToken, + (err, body) => { + if (err) { + this.logger.error(`Failed to fetch scope ${scope}:`, err); + + callback( + new InternalOAuthError(`Failed to fetch scope: ${scope}`, err), + null + ); + + return; + } + + try { + if (typeof body !== 'string') { + const error = new Error( + `Invalid response type for scope: ${scope}` + ); + + this.logger.error(error.message); + callback(error, null); + return; + } + + const json = JSON.parse(body) as Record; + callback(null, json); + } catch (parseError) { + const error = + parseError instanceof Error + ? parseError + : new Error(`Failed to parse scope data: ${scope}`); + + this.logger.error('Parse error:', error); + callback(error, null); + } + } ); - - this.logger.error(error.message); + }) + .catch((error) => { + this.logger.error('Unexpected error:', error); callback(error, null); - return; - } - - const json = JSON.parse(body) as Record; - callback(null, json); - } catch (parseError) { - const error = - parseError instanceof Error - ? parseError - : new Error(`Failed to parse scope data: ${scope}`); - - this.logger.error('Parse error:', error); - callback(error, null); - } - } - ); - }) - .catch((error) => { - this.logger.error('Unexpected error:', error); - callback(error, null); - }); - } - - public override authorizationParams( - options: AuthorizationParams - ): AuthorizationParams & Record { - const params: AuthorizationParams & Record = - super.authorizationParams(options) as Record; - - const { prompt } = this; - if (prompt) params.prompt = prompt; - return params; - } + }); + } + + public override authorizationParams( + options: AuthorizationParams + ): AuthorizationParams & Record { + const params: AuthorizationParams & Record = + super.authorizationParams(options) as Record; + + const { prompt } = this; + if (prompt) params.prompt = prompt; + return params; + } } diff --git a/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts b/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts index 8aeec7d3..0fc785d8 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts @@ -4,64 +4,64 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DiscordStrategy } from './index'; describe('DiscordStrategy', () => { - let discordStrategy: DiscordStrategy; - let configService: ConfigService; + let discordStrategy: DiscordStrategy; + let configService: ConfigService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - DiscordStrategy, - { - provide : ConfigService, - useValue: { - getOrThrow: jest.fn((key: string) => { - switch (key) { - case 'DISCORD_CLIENT_ID': - return 'test-client-id'; - case 'DISCORD_CLIENT_SECRET': - return 'test-client-secret'; - case 'SERVER_URL': - return 'http://localhost:3000'; - default: - return null; - } - }) - } - } - ] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DiscordStrategy, + { + provide : ConfigService, + useValue: { + getOrThrow: jest.fn((key: string) => { + switch (key) { + case 'DISCORD_CLIENT_ID': + return 'test-client-id'; + case 'DISCORD_CLIENT_SECRET': + return 'test-client-secret'; + case 'SERVER_URL': + return 'http://localhost:3000'; + default: + return null; + } + }) + } + } + ] + }).compile(); - discordStrategy = module.get(DiscordStrategy); - configService = module.get(ConfigService); - }); + discordStrategy = module.get(DiscordStrategy); + configService = module.get(ConfigService); + }); - it('should be defined', () => { - expect(discordStrategy).toBeDefined(); - }); + it('should be defined', () => { + expect(discordStrategy).toBeDefined(); + }); - describe('constructor', () => { - it('should throw an error if Discord config is missing', () => { - jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); + describe('constructor', () => { + it('should throw an error if Discord config is missing', () => { + jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); - expect(() => new DiscordStrategy(configService)).toThrowError( - 'OAuth2Strategy requires a clientID option' - ); + expect(() => new DiscordStrategy(configService)).toThrowError( + 'OAuth2Strategy requires a clientID option' + ); + }); }); - }); - describe('validate', () => { - it('should return accessToken, refreshToken, and profile', async () => { - const accessToken = 'test-access-token'; - const refreshToken = 'test-refresh-token'; - const profile = { id: 'test-id', username: 'Test User' }; + describe('validate', () => { + it('should return accessToken, refreshToken, and profile', async () => { + const accessToken = 'test-access-token'; + const refreshToken = 'test-refresh-token'; + const profile = { id: 'test-id', username: 'Test User' }; - const result = await discordStrategy.validate( - accessToken, - refreshToken, - profile - ); + const result = await discordStrategy.validate( + accessToken, + refreshToken, + profile + ); - expect(result).toEqual({ accessToken, refreshToken, profile }); + expect(result).toEqual({ accessToken, refreshToken, profile }); + }); }); - }); }); diff --git a/apps/backend/src/auth/strategies/discord.strategy/index.ts b/apps/backend/src/auth/strategies/discord.strategy/index.ts index 9a4d3e53..82d9d734 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/index.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/index.ts @@ -7,37 +7,37 @@ import { DiscordPermissionScope } from './types'; @Injectable() export class DiscordStrategy extends PassportStrategy(strategy, 'discord') { - private static logger = new Logger(DiscordStrategy.name); - constructor( - @Inject(ConfigService) - configService: ConfigService - ) { - const DISCORD_CLIENT_ID = - configService.getOrThrow('DISCORD_CLIENT_ID'); - - const DISCORD_CLIENT_SECRET = configService.getOrThrow( - 'DISCORD_CLIENT_SECRET' - ); - - const SERVER_URL = configService.getOrThrow('SERVER_URL'); - - const config = { - clientID : DISCORD_CLIENT_ID, - clientSecret: DISCORD_CLIENT_SECRET, - callbackUrl : `${SERVER_URL}/api/v1/auth/discord/callback`, - scope : [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], - fetchScope : true, - prompt : 'none' - }; - - super(config); - } - - async validate(accessToken: string, refreshToken: string, profile: any) { - DiscordStrategy.logger.debug( - `Discord Login Data ${JSON.stringify(profile)}` - ); - - return { accessToken, refreshToken, profile }; - } + private static logger = new Logger(DiscordStrategy.name); + constructor( + @Inject(ConfigService) + configService: ConfigService + ) { + const DISCORD_CLIENT_ID = + configService.getOrThrow('DISCORD_CLIENT_ID'); + + const DISCORD_CLIENT_SECRET = configService.getOrThrow( + 'DISCORD_CLIENT_SECRET' + ); + + const SERVER_URL = configService.getOrThrow('SERVER_URL'); + + const config = { + clientID : DISCORD_CLIENT_ID, + clientSecret: DISCORD_CLIENT_SECRET, + callbackUrl : `${SERVER_URL}/api/v1/auth/discord/callback`, + scope : [DiscordPermissionScope.Email, DiscordPermissionScope.Identify], + fetchScope : true, + prompt : 'none' + }; + + super(config); + } + + async validate(accessToken: string, refreshToken: string, profile: any) { + DiscordStrategy.logger.debug( + `Discord Login Data ${JSON.stringify(profile)}` + ); + + return { accessToken, refreshToken, profile }; + } } diff --git a/apps/backend/src/auth/strategies/discord.strategy/types.ts b/apps/backend/src/auth/strategies/discord.strategy/types.ts index 80f4e3a6..286c5c60 100644 --- a/apps/backend/src/auth/strategies/discord.strategy/types.ts +++ b/apps/backend/src/auth/strategies/discord.strategy/types.ts @@ -4,34 +4,34 @@ import passport from 'passport'; * https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes */ export enum DiscordPermissionScope { - ActivitiesRead = 'activities.read', - ActivitiesWrite = 'activities.write', - ApplicationBuildsRead = 'applications.builds.read', - ApplicationBuildsUpload = 'applications.builds.upload', - ApplicationsCommands = 'applications.commands', - ApplicationsCommandsUpdate = 'applications.commands.update', - ApplicationsCommandsPermissionsUpdate = 'applications.commands.permissions.update', - ApplicationsEntitlements = 'applications.entitlements', - ApplicationsStoreUpdate = 'applications.store.update', - Bot = 'bot', - Connections = 'connections', - DmRead = 'dm_channels.read', - Email = 'email', - GdmJoin = 'gdm.join', - Guilds = 'guilds', - GuildsJoin = 'guilds.join', - GuildMembersRead = 'guilds.members.read', - Identify = 'identify', - MessagesRead = 'messages.read', - RelationshipsRead = 'relationships.read', - RoleConnectionsWrite = 'role_connections.write', - Rpc = 'rpc', - RpcActivitiesUpdate = 'rpc.activities.update', - RpcNotificationsRead = 'rpc.notifications.read', - RpcVoiceRead = 'rpc.voice.read', - RpcVoiceWrite = 'rpc.voice.write', - Voice = 'voice', - WebhookIncoming = 'webhook.incoming' + ActivitiesRead = 'activities.read', + ActivitiesWrite = 'activities.write', + ApplicationBuildsRead = 'applications.builds.read', + ApplicationBuildsUpload = 'applications.builds.upload', + ApplicationsCommands = 'applications.commands', + ApplicationsCommandsUpdate = 'applications.commands.update', + ApplicationsCommandsPermissionsUpdate = 'applications.commands.permissions.update', + ApplicationsEntitlements = 'applications.entitlements', + ApplicationsStoreUpdate = 'applications.store.update', + Bot = 'bot', + Connections = 'connections', + DmRead = 'dm_channels.read', + Email = 'email', + GdmJoin = 'gdm.join', + Guilds = 'guilds', + GuildsJoin = 'guilds.join', + GuildMembersRead = 'guilds.members.read', + Identify = 'identify', + MessagesRead = 'messages.read', + RelationshipsRead = 'relationships.read', + RoleConnectionsWrite = 'role_connections.write', + Rpc = 'rpc', + RpcActivitiesUpdate = 'rpc.activities.update', + RpcNotificationsRead = 'rpc.notifications.read', + RpcVoiceRead = 'rpc.voice.read', + RpcVoiceWrite = 'rpc.voice.write', + Voice = 'voice', + WebhookIncoming = 'webhook.incoming' } export type SingleScopeType = `${DiscordPermissionScope}`; @@ -42,188 +42,188 @@ export type ScopeType = SingleScopeType[]; * https://discord.com/developers/docs/resources/user#user-object */ export interface DiscordUser { - id : string; - username : string; - global_name? : string | undefined; - avatar : string; - bot? : string | undefined; - system? : boolean | undefined; - mfa_enabled? : boolean | undefined; - banner? : string | undefined; - accent_color? : number | undefined; - locale? : string | undefined; - verified? : boolean | undefined; - email? : string | undefined; - flags? : number | undefined; - premium_type? : number | undefined; - public_flags? : number | undefined; - avatar_decoration_data?: AvatarDecorationData | undefined; + id : string; + username : string; + global_name? : string | undefined; + avatar : string; + bot? : string | undefined; + system? : boolean | undefined; + mfa_enabled? : boolean | undefined; + banner? : string | undefined; + accent_color? : number | undefined; + locale? : string | undefined; + verified? : boolean | undefined; + email? : string | undefined; + flags? : number | undefined; + premium_type? : number | undefined; + public_flags? : number | undefined; + avatar_decoration_data?: AvatarDecorationData | undefined; } export interface AvatarDecorationData { - asset : string; - sku_id: string; + asset : string; + sku_id: string; } export interface DiscordAccount { - id : string; - name: string; + id : string; + name: string; } export interface DiscordApplication { - id : string; - name : string; - icon? : string | undefined; - description: string; - bot? : DiscordUser; + id : string; + name : string; + icon? : string | undefined; + description: string; + bot? : DiscordUser; } export interface DiscordIntegration { - id : string; - name : string; - type : string; - enabled : boolean; - syncing? : boolean | undefined; - role_id? : string | undefined; - enable_emoticons? : boolean | undefined; - expire_behavior? : number | undefined; - expire_grace_period?: number | undefined; - user? : DiscordUser | undefined; - account : DiscordAccount; - synced_at? : Date | undefined; - subscriber_count? : number | undefined; - revoked? : boolean | undefined; - application? : DiscordApplication | undefined; - scopes? : ScopeType | undefined; + id : string; + name : string; + type : string; + enabled : boolean; + syncing? : boolean | undefined; + role_id? : string | undefined; + enable_emoticons? : boolean | undefined; + expire_behavior? : number | undefined; + expire_grace_period?: number | undefined; + user? : DiscordUser | undefined; + account : DiscordAccount; + synced_at? : Date | undefined; + subscriber_count? : number | undefined; + revoked? : boolean | undefined; + application? : DiscordApplication | undefined; + scopes? : ScopeType | undefined; } export interface ProfileConnection { - id : string; - name : string; - type : string; - revoked? : boolean | undefined; - integrations?: DiscordIntegration[] | undefined; - verified : boolean; - friend_sync : boolean; - show_activity: boolean; - two_way_link : boolean; - visibility : number; + id : string; + name : string; + type : string; + revoked? : boolean | undefined; + integrations?: DiscordIntegration[] | undefined; + verified : boolean; + friend_sync : boolean; + show_activity: boolean; + two_way_link : boolean; + visibility : number; } export interface DiscordRoleTag { - bot_id? : string | undefined; - integration_id? : string | undefined; - premium_subscriber? : null | undefined; - subscription_listing_id?: string | undefined; - available_for_purchase? : null | undefined; - guild_connections? : null | undefined; + bot_id? : string | undefined; + integration_id? : string | undefined; + premium_subscriber? : null | undefined; + subscription_listing_id?: string | undefined; + available_for_purchase? : null | undefined; + guild_connections? : null | undefined; } export interface DiscordRole { - id : string; - name : string; - color : number; - hoist : boolean; - icon? : string | undefined; - unicode_emoji?: string | undefined; - position : number; - permissions : string; - managed : boolean; - tags? : DiscordRoleTag | undefined; - flags : number; + id : string; + name : string; + color : number; + hoist : boolean; + icon? : string | undefined; + unicode_emoji?: string | undefined; + position : number; + permissions : string; + managed : boolean; + tags? : DiscordRoleTag | undefined; + flags : number; } export interface DiscordEmoji { - id? : string | undefined; - name : string | undefined; - roles? : string[]; - user? : DiscordUser; - require_colons?: boolean | undefined; - managed? : boolean | undefined; - animated? : boolean | undefined; - available? : boolean | undefined; + id? : string | undefined; + name : string | undefined; + roles? : string[]; + user? : DiscordUser; + require_colons?: boolean | undefined; + managed? : boolean | undefined; + animated? : boolean | undefined; + available? : boolean | undefined; } export interface DiscordWelcomeScreenChannel { - channel_id : string; - description: string; - emoji_id? : string | undefined; - emoji_name?: string | undefined; + channel_id : string; + description: string; + emoji_id? : string | undefined; + emoji_name?: string | undefined; } export interface DiscordWelcomeScreen { - description? : string | undefined; - welcome_channels: DiscordWelcomeScreenChannel[]; + description? : string | undefined; + welcome_channels: DiscordWelcomeScreenChannel[]; } export interface DiscordSticker { - id : string; - pack_id? : string | undefined; - name : string; - description: string; - tags : string; - type : number; - format_type: number; - available? : boolean | undefined; - guild_id? : string | undefined; - user? : DiscordUser | undefined; - sort_value?: number | undefined; + id : string; + pack_id? : string | undefined; + name : string; + description: string; + tags : string; + type : number; + format_type: number; + available? : boolean | undefined; + guild_id? : string | undefined; + user? : DiscordUser | undefined; + sort_value?: number | undefined; } export interface ProfileGuild { - id : string; - name : string; - icon? : string | undefined; - icon_hash? : string | undefined; - splash? : string | undefined; - discovery_splash? : string | undefined; - owner? : boolean | string; - owner_id : string; - permissions? : string | undefined; - afk_channel_id? : string | undefined; - afk_timeout? : number | undefined; - widget_enabled : boolean | undefined; - widget_channel_id? : string | undefined; - verification_level? : number | undefined; - default_message_notifications?: number | undefined; - explicit_content_filter? : number | undefined; - roles : DiscordRole[]; - emojis : DiscordEmoji[]; - features : string[]; - mfa_level? : number | undefined; - application_id? : string | undefined; - system_channel_id? : string | undefined; - system_channel_flags? : number | undefined; - rules_channel_id? : string | undefined; - max_presences? : number | undefined; - max_members? : number | undefined; - vanity_url_code? : string | undefined; - description? : string | undefined; - banner? : string | undefined; - premium_tier? : number | undefined; - premium_subscription_count? : number | undefined; - preferred_locale? : string | undefined; - public_updates_channel_id? : string | undefined; - max_video_channel_users? : number | undefined; - max_stage_video_channel_users?: number | undefined; - approximate_member_count? : number | undefined; - approximate_presence_count? : number | undefined; - welcome_screen? : DiscordWelcomeScreen | undefined; - nsfw_level? : number | undefined; - stickers? : DiscordSticker[] | undefined; - premium_progress_bar_enabled? : boolean | undefined; - safety_alerts_channel_id? : string | undefined; + id : string; + name : string; + icon? : string | undefined; + icon_hash? : string | undefined; + splash? : string | undefined; + discovery_splash? : string | undefined; + owner? : boolean | string; + owner_id : string; + permissions? : string | undefined; + afk_channel_id? : string | undefined; + afk_timeout? : number | undefined; + widget_enabled : boolean | undefined; + widget_channel_id? : string | undefined; + verification_level? : number | undefined; + default_message_notifications?: number | undefined; + explicit_content_filter? : number | undefined; + roles : DiscordRole[]; + emojis : DiscordEmoji[]; + features : string[]; + mfa_level? : number | undefined; + application_id? : string | undefined; + system_channel_id? : string | undefined; + system_channel_flags? : number | undefined; + rules_channel_id? : string | undefined; + max_presences? : number | undefined; + max_members? : number | undefined; + vanity_url_code? : string | undefined; + description? : string | undefined; + banner? : string | undefined; + premium_tier? : number | undefined; + premium_subscription_count? : number | undefined; + preferred_locale? : string | undefined; + public_updates_channel_id? : string | undefined; + max_video_channel_users? : number | undefined; + max_stage_video_channel_users?: number | undefined; + approximate_member_count? : number | undefined; + approximate_presence_count? : number | undefined; + welcome_screen? : DiscordWelcomeScreen | undefined; + nsfw_level? : number | undefined; + stickers? : DiscordSticker[] | undefined; + premium_progress_bar_enabled? : boolean | undefined; + safety_alerts_channel_id? : string | undefined; } export interface Profile - extends Omit, + extends Omit, DiscordUser { - provider : string; - connections?: ProfileConnection[] | undefined; - guilds? : ProfileGuild[] | undefined; - access_token: string; - fetchedAt : Date; - createdAt : Date; - _raw : unknown; - _json : Record; + provider : string; + connections?: ProfileConnection[] | undefined; + guilds? : ProfileGuild[] | undefined; + access_token: string; + fetchedAt : Date; + createdAt : Date; + _raw : unknown; + _json : Record; } diff --git a/apps/backend/src/auth/strategies/github.strategy.spec.ts b/apps/backend/src/auth/strategies/github.strategy.spec.ts index 44ecd0fb..f553e052 100644 --- a/apps/backend/src/auth/strategies/github.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/github.strategy.spec.ts @@ -6,64 +6,64 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GithubStrategy } from './github.strategy'; describe('GithubStrategy', () => { - let githubStrategy: GithubStrategy; - let configService: ConfigService; + let githubStrategy: GithubStrategy; + let configService: ConfigService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - GithubStrategy, - { - provide : ConfigService, - useValue: { - getOrThrow: jest.fn((key: string) => { - switch (key) { - case 'GITHUB_CLIENT_ID': - return 'test-client-id'; - case 'GITHUB_CLIENT_SECRET': - return 'test-client-secret'; - case 'SERVER_URL': - return 'http://localhost:3000'; - default: - return null; - } - }) - } - } - ] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GithubStrategy, + { + provide : ConfigService, + useValue: { + getOrThrow: jest.fn((key: string) => { + switch (key) { + case 'GITHUB_CLIENT_ID': + return 'test-client-id'; + case 'GITHUB_CLIENT_SECRET': + return 'test-client-secret'; + case 'SERVER_URL': + return 'http://localhost:3000'; + default: + return null; + } + }) + } + } + ] + }).compile(); - githubStrategy = module.get(GithubStrategy); - configService = module.get(ConfigService); - }); + githubStrategy = module.get(GithubStrategy); + configService = module.get(ConfigService); + }); - it('should be defined', () => { - expect(githubStrategy).toBeDefined(); - }); + it('should be defined', () => { + expect(githubStrategy).toBeDefined(); + }); - describe('constructor', () => { - it('should throw an error if GitHub config is missing', () => { - jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); + describe('constructor', () => { + it('should throw an error if GitHub config is missing', () => { + jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); - expect(() => new GithubStrategy(configService)).toThrowError( - 'OAuth2Strategy requires a clientID option' - ); + expect(() => new GithubStrategy(configService)).toThrowError( + 'OAuth2Strategy requires a clientID option' + ); + }); }); - }); - describe('validate', () => { - it('should return accessToken, refreshToken, and profile', async () => { - const accessToken = 'test-access-token'; - const refreshToken = 'test-refresh-token'; - const profile = { id: 'test-id', displayName: 'Test User' }; + describe('validate', () => { + it('should return accessToken, refreshToken, and profile', async () => { + const accessToken = 'test-access-token'; + const refreshToken = 'test-refresh-token'; + const profile = { id: 'test-id', displayName: 'Test User' }; - const result = await githubStrategy.validate( - accessToken, - refreshToken, - profile - ); + const result = await githubStrategy.validate( + accessToken, + refreshToken, + profile + ); - expect(result).toEqual({ accessToken, refreshToken, profile }); + expect(result).toEqual({ accessToken, refreshToken, profile }); + }); }); - }); }); diff --git a/apps/backend/src/auth/strategies/github.strategy.ts b/apps/backend/src/auth/strategies/github.strategy.ts index c991b1cd..4d620320 100644 --- a/apps/backend/src/auth/strategies/github.strategy.ts +++ b/apps/backend/src/auth/strategies/github.strategy.ts @@ -5,32 +5,32 @@ import strategy from 'passport-github'; @Injectable() export class GithubStrategy extends PassportStrategy(strategy, 'github') { - private static logger = new Logger(GithubStrategy.name); - constructor( - @Inject(ConfigService) - configService: ConfigService - ) { - const GITHUB_CLIENT_ID = - configService.getOrThrow('GITHUB_CLIENT_ID'); + private static logger = new Logger(GithubStrategy.name); + constructor( + @Inject(ConfigService) + configService: ConfigService + ) { + const GITHUB_CLIENT_ID = + configService.getOrThrow('GITHUB_CLIENT_ID'); - const GITHUB_CLIENT_SECRET = configService.getOrThrow( - 'GITHUB_CLIENT_SECRET' - ); + const GITHUB_CLIENT_SECRET = configService.getOrThrow( + 'GITHUB_CLIENT_SECRET' + ); - const SERVER_URL = configService.getOrThrow('SERVER_URL'); + const SERVER_URL = configService.getOrThrow('SERVER_URL'); - super({ - clientID : GITHUB_CLIENT_ID, - clientSecret: GITHUB_CLIENT_SECRET, - redirect_uri: `${SERVER_URL}/api/v1/auth/github/callback`, - scope : 'user:read,user:email', - state : false - }); - } + super({ + clientID : GITHUB_CLIENT_ID, + clientSecret: GITHUB_CLIENT_SECRET, + redirect_uri: `${SERVER_URL}/api/v1/auth/github/callback`, + scope : 'user:read,user:email', + state : false + }); + } - async validate(accessToken: string, refreshToken: string, profile: any) { - GithubStrategy.logger.debug(`GitHub Login Data ${JSON.stringify(profile)}`); + async validate(accessToken: string, refreshToken: string, profile: any) { + GithubStrategy.logger.debug(`GitHub Login Data ${JSON.stringify(profile)}`); - return { accessToken, refreshToken, profile }; - } + return { accessToken, refreshToken, profile }; + } } diff --git a/apps/backend/src/auth/strategies/google.strategy.spec.ts b/apps/backend/src/auth/strategies/google.strategy.spec.ts index 6d20647c..fc19c58b 100644 --- a/apps/backend/src/auth/strategies/google.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/google.strategy.spec.ts @@ -7,61 +7,61 @@ import { VerifyCallback } from 'passport-google-oauth20'; import { GoogleStrategy } from './google.strategy'; describe('GoogleStrategy', () => { - let googleStrategy: GoogleStrategy; - let configService: ConfigService; + let googleStrategy: GoogleStrategy; + let configService: ConfigService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - GoogleStrategy, - { - provide : ConfigService, - useValue: { - getOrThrow: jest.fn((key: string) => { - switch (key) { - case 'GOOGLE_CLIENT_ID': - return 'test-client-id'; - case 'GOOGLE_CLIENT_SECRET': - return 'test-client-secret'; - case 'SERVER_URL': - return 'http://localhost:3000'; - default: - return null; - } - }) - } - } - ] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GoogleStrategy, + { + provide : ConfigService, + useValue: { + getOrThrow: jest.fn((key: string) => { + switch (key) { + case 'GOOGLE_CLIENT_ID': + return 'test-client-id'; + case 'GOOGLE_CLIENT_SECRET': + return 'test-client-secret'; + case 'SERVER_URL': + return 'http://localhost:3000'; + default: + return null; + } + }) + } + } + ] + }).compile(); - googleStrategy = module.get(GoogleStrategy); - configService = module.get(ConfigService); - }); + googleStrategy = module.get(GoogleStrategy); + configService = module.get(ConfigService); + }); - it('should be defined', () => { - expect(googleStrategy).toBeDefined(); - }); + it('should be defined', () => { + expect(googleStrategy).toBeDefined(); + }); - describe('constructor', () => { - it('should throw an error if Google config is missing', () => { - jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); + describe('constructor', () => { + it('should throw an error if Google config is missing', () => { + jest.spyOn(configService, 'getOrThrow').mockReturnValueOnce(null); - expect(() => new GoogleStrategy(configService)).toThrowError( - 'OAuth2Strategy requires a clientID option' - ); + expect(() => new GoogleStrategy(configService)).toThrowError( + 'OAuth2Strategy requires a clientID option' + ); + }); }); - }); - describe('validate', () => { - it('should call done with profile', () => { - const accessToken = 'test-access-token'; - const refreshToken = 'test-refresh-token'; - const profile = { id: 'test-id', displayName: 'Test User' }; - const done: VerifyCallback = jest.fn(); + describe('validate', () => { + it('should call done with profile', () => { + const accessToken = 'test-access-token'; + const refreshToken = 'test-refresh-token'; + const profile = { id: 'test-id', displayName: 'Test User' }; + const done: VerifyCallback = jest.fn(); - googleStrategy.validate(accessToken, refreshToken, profile, done); + googleStrategy.validate(accessToken, refreshToken, profile, done); - expect(done).toHaveBeenCalledWith(null, profile); + expect(done).toHaveBeenCalledWith(null, profile); + }); }); - }); }); diff --git a/apps/backend/src/auth/strategies/google.strategy.ts b/apps/backend/src/auth/strategies/google.strategy.ts index f751f734..3765493f 100644 --- a/apps/backend/src/auth/strategies/google.strategy.ts +++ b/apps/backend/src/auth/strategies/google.strategy.ts @@ -5,38 +5,38 @@ import { Strategy, VerifyCallback } from 'passport-google-oauth20'; @Injectable() export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { - private static logger = new Logger(GoogleStrategy.name); - constructor( - @Inject(ConfigService) - configService: ConfigService - ) { - const GOOGLE_CLIENT_ID = - configService.getOrThrow('GOOGLE_CLIENT_ID'); + private static logger = new Logger(GoogleStrategy.name); + constructor( + @Inject(ConfigService) + configService: ConfigService + ) { + const GOOGLE_CLIENT_ID = + configService.getOrThrow('GOOGLE_CLIENT_ID'); - const GOOGLE_CLIENT_SECRET = configService.getOrThrow( - 'GOOGLE_CLIENT_SECRET' - ); + const GOOGLE_CLIENT_SECRET = configService.getOrThrow( + 'GOOGLE_CLIENT_SECRET' + ); - const SERVER_URL = configService.getOrThrow('SERVER_URL'); + const SERVER_URL = configService.getOrThrow('SERVER_URL'); - const callbackURL = `${SERVER_URL}/api/v1/auth/google/callback`; - GoogleStrategy.logger.debug(`Google Login callbackURL ${callbackURL}`); + const callbackURL = `${SERVER_URL}/api/v1/auth/google/callback`; + GoogleStrategy.logger.debug(`Google Login callbackURL ${callbackURL}`); - super({ - clientID : GOOGLE_CLIENT_ID, - clientSecret: GOOGLE_CLIENT_SECRET, - callbackURL : callbackURL, - scope : ['email', 'profile'] - }); - } + super({ + clientID : GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + callbackURL : callbackURL, + scope : ['email', 'profile'] + }); + } - validate( - accessToken: string, - refreshToken: string, - profile: any, - done: VerifyCallback - ): any { - GoogleStrategy.logger.debug(`Google Login Data ${JSON.stringify(profile)}`); - done(null, profile); - } + validate( + accessToken: string, + refreshToken: string, + profile: any, + done: VerifyCallback + ): any { + GoogleStrategy.logger.debug(`Google Login Data ${JSON.stringify(profile)}`); + done(null, profile); + } } diff --git a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts index 27c7d867..cbffca59 100644 --- a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts +++ b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts @@ -10,147 +10,147 @@ import { UserService } from '@server/user/user.service'; import { MagicLinkEmailStrategy } from './magicLinkEmail.strategy'; describe('MagicLinkEmailStrategy', () => { - let strategy: MagicLinkEmailStrategy; - let userService: UserService; - let mailingService: MailingService; - let _configService: ConfigService; - - const mockUserService = { - findByEmail : jest.fn(), - createWithEmail: jest.fn() - }; - - const mockMailingService = { - sendEmail: jest.fn() - }; - - const mockConfigService = { - get: jest.fn((key: string) => { - if (key === 'MAGIC_LINK_SECRET') return 'test_secret'; - if (key === 'SERVER_URL') return 'http://localhost:3000'; - return null; - }) - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - MagicLinkEmailStrategy, - { provide: UserService, useValue: mockUserService }, - { provide: MailingService, useValue: mockMailingService }, - { provide: ConfigService, useValue: mockConfigService }, - { - provide : 'MAGIC_LINK_SECRET', - useValue: 'test_secret' - }, - { - provide : 'SERVER_URL', - useValue: 'http://localhost:3000' - } - ] - }).compile(); - - strategy = module.get(MagicLinkEmailStrategy); - userService = module.get(UserService); - mailingService = module.get(MailingService); - _configService = module.get(ConfigService); - }); - - it('should be defined', () => { - expect(strategy).toBeDefined(); - }); - - describe('sendMagicLink', () => { - it('should send a magic link email', async () => { - const email = 'test@example.com'; - - const magicLink = - 'http://localhost/api/v1/auth/magic-link/callback?token=test_token'; - - const user = { username: 'testuser', email }; - - mockUserService.findByEmail.mockResolvedValue(user); - - await MagicLinkEmailStrategy.sendMagicLink( - 'http://localhost:3000', - userService, - mailingService - )(email, magicLink); - - expect(mockUserService.findByEmail).toHaveBeenCalledWith(email); - - expect(mockMailingService.sendEmail).toHaveBeenCalledWith({ - to : email, - context: { - magicLink: - 'http://localhost/api/v1/auth/magic-link/callback?token=test_token', - username: 'testuser' - }, - subject : 'Noteblock Magic Link', - template: 'magic-link' - }); + let strategy: MagicLinkEmailStrategy; + let userService: UserService; + let mailingService: MailingService; + let _configService: ConfigService; + + const mockUserService = { + findByEmail : jest.fn(), + createWithEmail: jest.fn() + }; + + const mockMailingService = { + sendEmail: jest.fn() + }; + + const mockConfigService = { + get: jest.fn((key: string) => { + if (key === 'MAGIC_LINK_SECRET') return 'test_secret'; + if (key === 'SERVER_URL') return 'http://localhost:3000'; + return null; + }) + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MagicLinkEmailStrategy, + { provide: UserService, useValue: mockUserService }, + { provide: MailingService, useValue: mockMailingService }, + { provide: ConfigService, useValue: mockConfigService }, + { + provide : 'MAGIC_LINK_SECRET', + useValue: 'test_secret' + }, + { + provide : 'SERVER_URL', + useValue: 'http://localhost:3000' + } + ] + }).compile(); + + strategy = module.get(MagicLinkEmailStrategy); + userService = module.get(UserService); + mailingService = module.get(MailingService); + _configService = module.get(ConfigService); + }); + + it('should be defined', () => { + expect(strategy).toBeDefined(); }); - it('should create a new user if not found and send a magic link email', async () => { - const email = 'testuser@test.com'; + describe('sendMagicLink', () => { + it('should send a magic link email', async () => { + const email = 'test@example.com'; - const magicLink = - 'http://localhost/api/v1/auth/magic-link/callback?token=test_token'; + const magicLink = + 'http://localhost/api/v1/auth/magic-link/callback?token=test_token'; - const user = { username: 'testuser', email }; + const user = { username: 'testuser', email }; - mockUserService.findByEmail.mockResolvedValue(null); - mockUserService.createWithEmail.mockResolvedValue(user); + mockUserService.findByEmail.mockResolvedValue(user); - await MagicLinkEmailStrategy.sendMagicLink( - 'http://localhost:3000', - userService, - mailingService - )(email, magicLink); + await MagicLinkEmailStrategy.sendMagicLink( + 'http://localhost:3000', + userService, + mailingService + )(email, magicLink); - expect(mockUserService.findByEmail).toHaveBeenCalledWith(email); + expect(mockUserService.findByEmail).toHaveBeenCalledWith(email); - expect(mockMailingService.sendEmail).toHaveBeenCalledWith({ - to : email, - context: { - magicLink: + expect(mockMailingService.sendEmail).toHaveBeenCalledWith({ + to : email, + context: { + magicLink: 'http://localhost/api/v1/auth/magic-link/callback?token=test_token', - username: 'testuser' - }, - subject : 'Welcome to Noteblock.world', - template: 'magic-link-new-account' - }); - }); - }); + username: 'testuser' + }, + subject : 'Noteblock Magic Link', + template: 'magic-link' + }); + }); + + it('should create a new user if not found and send a magic link email', async () => { + const email = 'testuser@test.com'; + + const magicLink = + 'http://localhost/api/v1/auth/magic-link/callback?token=test_token'; - describe('validate', () => { - it('should validate the payload and return the user', async () => { - const payload = { destination: 'test@example.com' }; - const user = { username: 'testuser', email: 'test@example.com' }; + const user = { username: 'testuser', email }; - mockUserService.findByEmail.mockResolvedValue(user); + mockUserService.findByEmail.mockResolvedValue(null); + mockUserService.createWithEmail.mockResolvedValue(user); - const result = await strategy.validate(payload); + await MagicLinkEmailStrategy.sendMagicLink( + 'http://localhost:3000', + userService, + mailingService + )(email, magicLink); - expect(result).toEqual(user as UserDocument); + expect(mockUserService.findByEmail).toHaveBeenCalledWith(email); + + expect(mockMailingService.sendEmail).toHaveBeenCalledWith({ + to : email, + context: { + magicLink: + 'http://localhost/api/v1/auth/magic-link/callback?token=test_token', + username: 'testuser' + }, + subject : 'Welcome to Noteblock.world', + template: 'magic-link-new-account' + }); + }); }); - it('should create a new user if not found and return the user', async () => { - const payload = { destination: 'test@example.com' }; + describe('validate', () => { + it('should validate the payload and return the user', async () => { + const payload = { destination: 'test@example.com' }; + const user = { username: 'testuser', email: 'test@example.com' }; + + mockUserService.findByEmail.mockResolvedValue(user); + + const result = await strategy.validate(payload); + + expect(result).toEqual(user as UserDocument); + }); + + it('should create a new user if not found and return the user', async () => { + const payload = { destination: 'test@example.com' }; - mockUserService.findByEmail.mockResolvedValue(null); + mockUserService.findByEmail.mockResolvedValue(null); - mockUserService.createWithEmail.mockResolvedValue({ - email : 'test@example.com', - username: 'test' - }); + mockUserService.createWithEmail.mockResolvedValue({ + email : 'test@example.com', + username: 'test' + }); - const result = await strategy.validate(payload); + const result = await strategy.validate(payload); - expect(result).toEqual({ - email : 'test@example.com', - username: 'test' - } as UserDocument); + expect(result).toEqual({ + email : 'test@example.com', + username: 'test' + } as UserDocument); + }); }); - }); }); diff --git a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts index 92b7aacc..32e0eeaf 100644 --- a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts +++ b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts @@ -6,91 +6,91 @@ import { MailingService } from '@server/mailing/mailing.service'; import { UserService } from '@server/user/user.service'; type authenticationLinkPayload = { - destination: string; + destination: string; }; type magicLinkCallback = (error: any, user: any) => void; @Injectable() export class MagicLinkEmailStrategy extends PassportStrategy( - strategy, - 'magic-link' + strategy, + 'magic-link' ) { - static logger = new Logger(MagicLinkEmailStrategy.name); + static logger = new Logger(MagicLinkEmailStrategy.name); - constructor( - @Inject('MAGIC_LINK_SECRET') - MAGIC_LINK_SECRET: string, - @Inject('SERVER_URL') - SERVER_URL: string, - @Inject(UserService) - private readonly userService: UserService, - @Inject(MailingService) - private readonly mailingService: MailingService - ) { - super({ - secret : MAGIC_LINK_SECRET, - confirmUrl : `${SERVER_URL}/api/v1/auth/magic-link/confirm`, - callbackUrl : `${SERVER_URL}/api/v1/auth/magic-link/callback`, - sendMagicLink: MagicLinkEmailStrategy.sendMagicLink( - SERVER_URL, - userService, - mailingService - ), - verify: ( - payload: authenticationLinkPayload, - callback: magicLinkCallback - ) => { - callback(null, this.validate(payload)); - } - }); - } + constructor( + @Inject('MAGIC_LINK_SECRET') + MAGIC_LINK_SECRET: string, + @Inject('SERVER_URL') + SERVER_URL: string, + @Inject(UserService) + private readonly userService: UserService, + @Inject(MailingService) + private readonly mailingService: MailingService + ) { + super({ + secret : MAGIC_LINK_SECRET, + confirmUrl : `${SERVER_URL}/api/v1/auth/magic-link/confirm`, + callbackUrl : `${SERVER_URL}/api/v1/auth/magic-link/callback`, + sendMagicLink: MagicLinkEmailStrategy.sendMagicLink( + SERVER_URL, + userService, + mailingService + ), + verify: ( + payload: authenticationLinkPayload, + callback: magicLinkCallback + ) => { + callback(null, this.validate(payload)); + } + }); + } - static sendMagicLink = - ( - SERVER_URL: string, - userService: UserService, - mailingService: MailingService - ) => - async (email: string, magicLink: string) => { - const user = await userService.findByEmail(email); + static sendMagicLink = + ( + SERVER_URL: string, + userService: UserService, + mailingService: MailingService + ) => + async (email: string, magicLink: string) => { + const user = await userService.findByEmail(email); - if (!user) { - mailingService.sendEmail({ - to : email, - context: { - magicLink: magicLink, - username : email.split('@')[0] - }, - subject : 'Welcome to Noteblock.world', - template: 'magic-link-new-account' - }); - } else { - mailingService.sendEmail({ - to : email, - context: { - magicLink: magicLink, - username : user.username - }, - subject : 'Noteblock Magic Link', - template: 'magic-link' - }); - } - }; + if (!user) { + mailingService.sendEmail({ + to : email, + context: { + magicLink: magicLink, + username : email.split('@')[0] + }, + subject : 'Welcome to Noteblock.world', + template: 'magic-link-new-account' + }); + } else { + mailingService.sendEmail({ + to : email, + context: { + magicLink: magicLink, + username : user.username + }, + subject : 'Noteblock Magic Link', + template: 'magic-link' + }); + } + }; - async validate(payload: authenticationLinkPayload) { - MagicLinkEmailStrategy.logger.debug( - `Validating payload: ${JSON.stringify(payload)}` - ); + async validate(payload: authenticationLinkPayload) { + MagicLinkEmailStrategy.logger.debug( + `Validating payload: ${JSON.stringify(payload)}` + ); - const user = await this.userService.findByEmail(payload.destination); + const user = await this.userService.findByEmail(payload.destination); - if (!user) { - return await this.userService.createWithEmail(payload.destination); - } + if (!user) { + return await this.userService.createWithEmail(payload.destination); + } - MagicLinkEmailStrategy.logger.debug(`User found: ${user.username}`); + MagicLinkEmailStrategy.logger.debug(`User found: ${user.username}`); - return user; - } + return user; + } } diff --git a/apps/backend/src/auth/types/discordProfile.ts b/apps/backend/src/auth/types/discordProfile.ts index 30d4d82e..78d8ed53 100644 --- a/apps/backend/src/auth/types/discordProfile.ts +++ b/apps/backend/src/auth/types/discordProfile.ts @@ -1,28 +1,28 @@ export type DiscordUser = { - access_token : string; - refresh_token: string; - profile : DiscordProfile; + access_token : string; + refresh_token: string; + profile : DiscordProfile; }; export type DiscordProfile = { - id : string; - username : string; - avatar : string; - discriminator : string; - public_flags : number; - flags : number; - banner : string; - accent_color : string; - global_name : string; - avatar_decoration_data: string | null; - banner_color : string | null; - clan : string | null; - mfa_enabled : boolean; - locale : string; - premium_type : number; - email : string; - verified : boolean; - provider : string; - accessToken : string; - fetchedAt : string; + id : string; + username : string; + avatar : string; + discriminator : string; + public_flags : number; + flags : number; + banner : string; + accent_color : string; + global_name : string; + avatar_decoration_data: string | null; + banner_color : string | null; + clan : string | null; + mfa_enabled : boolean; + locale : string; + premium_type : number; + email : string; + verified : boolean; + provider : string; + accessToken : string; + fetchedAt : string; }; diff --git a/apps/backend/src/auth/types/githubProfile.ts b/apps/backend/src/auth/types/githubProfile.ts index 2e2bc2b5..f12c8c94 100644 --- a/apps/backend/src/auth/types/githubProfile.ts +++ b/apps/backend/src/auth/types/githubProfile.ts @@ -1,65 +1,65 @@ type Email = { - value: string; + value: string; }; type Photo = { - value: string; + value: string; }; type ProfileJson = { - login : string; - id : number; - node_id : string; - avatar_url : string; - gravatar_id : string; - url : string; - html_url : string; - followers_url : string; - following_url : string; - gists_url : string; - starred_url : string; - subscriptions_url : string; - organizations_url : string; - repos_url : string; - events_url : string; - received_events_url: string; - type : string; - site_admin : boolean; - name : string; - company : string; - blog : string; - location : string; - email : string; - hireable : boolean; - bio : string; - twitter_username : string; - public_repos : number; - public_gists : number; - followers : number; - following : number; - created_at : string; - updated_at : string; + login : string; + id : number; + node_id : string; + avatar_url : string; + gravatar_id : string; + url : string; + html_url : string; + followers_url : string; + following_url : string; + gists_url : string; + starred_url : string; + subscriptions_url : string; + organizations_url : string; + repos_url : string; + events_url : string; + received_events_url: string; + type : string; + site_admin : boolean; + name : string; + company : string; + blog : string; + location : string; + email : string; + hireable : boolean; + bio : string; + twitter_username : string; + public_repos : number; + public_gists : number; + followers : number; + following : number; + created_at : string; + updated_at : string; }; type Profile = { - id : string; - displayName: string; - username : string; - profileUrl : string; - emails : Email[]; - photos : Photo[]; - provider : string; - _raw : string; - _json : ProfileJson; + id : string; + displayName: string; + username : string; + profileUrl : string; + emails : Email[]; + photos : Photo[]; + provider : string; + _raw : string; + _json : ProfileJson; }; export type GithubAccessToken = { - accessToken: string; - profile : Profile; + accessToken: string; + profile : Profile; }; export type GithubEmailList = Array<{ - email : string; - primary : string; - visibility: string; + email : string; + primary : string; + visibility: string; }>; diff --git a/apps/backend/src/auth/types/googleProfile.ts b/apps/backend/src/auth/types/googleProfile.ts index 9d29c495..62facdf6 100644 --- a/apps/backend/src/auth/types/googleProfile.ts +++ b/apps/backend/src/auth/types/googleProfile.ts @@ -1,34 +1,34 @@ type Email = { - value : string; - verified: boolean; + value : string; + verified: boolean; }; type Photo = { - value: string; + value: string; }; type ProfileJson = { - sub : string; - name : string; - given_name : string; - family_name? : string; - picture : string; - email : string; - email_verified: boolean; - locale : string; + sub : string; + name : string; + given_name : string; + family_name? : string; + picture : string; + email : string; + email_verified: boolean; + locale : string; }; // TODO: this is a uniform profile type standardized by passport for all providers export type GoogleProfile = { - id : string; - displayName: string; - name: { - familyName?: string; - givenName : string; - }; - emails : Email[]; - photos : Photo[]; - provider: string; - _raw : string; - _json : ProfileJson; + id : string; + displayName: string; + name: { + familyName?: string; + givenName : string; + }; + emails : Email[]; + photos : Photo[]; + provider: string; + _raw : string; + _json : ProfileJson; }; diff --git a/apps/backend/src/auth/types/profile.ts b/apps/backend/src/auth/types/profile.ts index 9e6f6f74..8028d4ad 100644 --- a/apps/backend/src/auth/types/profile.ts +++ b/apps/backend/src/auth/types/profile.ts @@ -1,5 +1,5 @@ export type Profile = { - username : string; - email : string; - profileImage: string; + username : string; + email : string; + profileImage: string; }; diff --git a/apps/backend/src/auth/types/token.ts b/apps/backend/src/auth/types/token.ts index c5025df7..a23085f3 100644 --- a/apps/backend/src/auth/types/token.ts +++ b/apps/backend/src/auth/types/token.ts @@ -1,10 +1,10 @@ export type TokenPayload = { - id : string; - email : string; - username: string; + id : string; + email : string; + username: string; }; export type Tokens = { - access_token : string; - refresh_token: string; + access_token : string; + refresh_token: string; }; diff --git a/apps/backend/src/config/EnvironmentVariables.ts b/apps/backend/src/config/EnvironmentVariables.ts index 43555591..5eec0c0e 100644 --- a/apps/backend/src/config/EnvironmentVariables.ts +++ b/apps/backend/src/config/EnvironmentVariables.ts @@ -2,111 +2,111 @@ import { plainToInstance } from 'class-transformer'; import { IsEnum, IsOptional, IsString, validateSync } from 'class-validator'; enum Environment { - Development = 'development', - Production = 'production' + Development = 'development', + Production = 'production' } export class EnvironmentVariables { - @IsEnum(Environment) - @IsOptional() - NODE_ENV?: Environment; + @IsEnum(Environment) + @IsOptional() + NODE_ENV?: Environment; - // OAuth providers - @IsString() - GITHUB_CLIENT_ID: string; + // OAuth providers + @IsString() + GITHUB_CLIENT_ID: string; - @IsString() - GITHUB_CLIENT_SECRET: string; + @IsString() + GITHUB_CLIENT_SECRET: string; - @IsString() - GOOGLE_CLIENT_ID: string; + @IsString() + GOOGLE_CLIENT_ID: string; - @IsString() - GOOGLE_CLIENT_SECRET: string; + @IsString() + GOOGLE_CLIENT_SECRET: string; - @IsString() - DISCORD_CLIENT_ID: string; + @IsString() + DISCORD_CLIENT_ID: string; - @IsString() - DISCORD_CLIENT_SECRET: string; + @IsString() + DISCORD_CLIENT_SECRET: string; - // Email magic link auth - @IsString() - MAGIC_LINK_SECRET: string; + // Email magic link auth + @IsString() + MAGIC_LINK_SECRET: string; - // jwt auth - @IsString() - JWT_SECRET: string; + // jwt auth + @IsString() + JWT_SECRET: string; - @IsString() - JWT_EXPIRES_IN: string; + @IsString() + JWT_EXPIRES_IN: string; - @IsString() - JWT_REFRESH_SECRET: string; + @IsString() + JWT_REFRESH_SECRET: string; - @IsString() - JWT_REFRESH_EXPIRES_IN: string; + @IsString() + JWT_REFRESH_EXPIRES_IN: string; - // database - @IsString() - MONGO_URL: string; + // database + @IsString() + MONGO_URL: string; - @IsString() - SERVER_URL: string; + @IsString() + SERVER_URL: string; - @IsString() - FRONTEND_URL: string; + @IsString() + FRONTEND_URL: string; - @IsString() - @IsOptional() - APP_DOMAIN: string = 'localhost'; + @IsString() + @IsOptional() + APP_DOMAIN: string = 'localhost'; - @IsString() - RECAPTCHA_KEY: string; + @IsString() + RECAPTCHA_KEY: string; - // s3 - @IsString() - S3_ENDPOINT: string; + // s3 + @IsString() + S3_ENDPOINT: string; - @IsString() - S3_BUCKET_SONGS: string; + @IsString() + S3_BUCKET_SONGS: string; - @IsString() - S3_BUCKET_THUMBS: string; + @IsString() + S3_BUCKET_THUMBS: string; - @IsString() - S3_KEY: string; + @IsString() + S3_KEY: string; - @IsString() - S3_SECRET: string; + @IsString() + S3_SECRET: string; - @IsString() - S3_REGION: string; + @IsString() + S3_REGION: string; - @IsString() - @IsOptional() - WHITELISTED_USERS?: string; + @IsString() + @IsOptional() + WHITELISTED_USERS?: string; - // discord webhook - @IsString() - DISCORD_WEBHOOK_URL: string; + // discord webhook + @IsString() + DISCORD_WEBHOOK_URL: string; - @IsString() - COOKIE_EXPIRES_IN: string; + @IsString() + COOKIE_EXPIRES_IN: string; } export function validate(config: Record) { - const validatedConfig = plainToInstance(EnvironmentVariables, config, { - enableImplicitConversion: true - }); + const validatedConfig = plainToInstance(EnvironmentVariables, config, { + enableImplicitConversion: true + }); - const errors = validateSync(validatedConfig, { - skipMissingProperties: false - }); + const errors = validateSync(validatedConfig, { + skipMissingProperties: false + }); - if (errors.length > 0) { - throw new Error(errors.toString()); - } + if (errors.length > 0) { + throw new Error(errors.toString()); + } - return validatedConfig; + return validatedConfig; } diff --git a/apps/backend/src/email-login/email-login.controller.spec.ts b/apps/backend/src/email-login/email-login.controller.spec.ts index ecd86d63..89961423 100644 --- a/apps/backend/src/email-login/email-login.controller.spec.ts +++ b/apps/backend/src/email-login/email-login.controller.spec.ts @@ -6,18 +6,18 @@ import { EmailLoginController } from './email-login.controller'; import { EmailLoginService } from './email-login.service'; describe('EmailLoginController', () => { - let controller: EmailLoginController; + let controller: EmailLoginController; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [EmailLoginController], - providers : [EmailLoginService] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [EmailLoginController], + providers : [EmailLoginService] + }).compile(); - controller = module.get(EmailLoginController); - }); + controller = module.get(EmailLoginController); + }); - it('should be defined', () => { - expect(controller).toBeDefined(); - }); + it('should be defined', () => { + expect(controller).toBeDefined(); + }); }); diff --git a/apps/backend/src/email-login/email-login.controller.ts b/apps/backend/src/email-login/email-login.controller.ts index 8b6be0a6..a102f95b 100644 --- a/apps/backend/src/email-login/email-login.controller.ts +++ b/apps/backend/src/email-login/email-login.controller.ts @@ -4,5 +4,5 @@ import { EmailLoginService } from './email-login.service'; @Controller('email-login') export class EmailLoginController { - constructor(private readonly emailLoginService: EmailLoginService) {} + constructor(private readonly emailLoginService: EmailLoginService) {} } diff --git a/apps/backend/src/email-login/email-login.module.ts b/apps/backend/src/email-login/email-login.module.ts index 7f624fa2..6445ca90 100644 --- a/apps/backend/src/email-login/email-login.module.ts +++ b/apps/backend/src/email-login/email-login.module.ts @@ -4,7 +4,7 @@ import { EmailLoginController } from './email-login.controller'; import { EmailLoginService } from './email-login.service'; @Module({ - controllers: [EmailLoginController], - providers : [EmailLoginService] + controllers: [EmailLoginController], + providers : [EmailLoginService] }) export class EmailLoginModule {} diff --git a/apps/backend/src/email-login/email-login.service.spec.ts b/apps/backend/src/email-login/email-login.service.spec.ts index 3f063d56..85dcd6b5 100644 --- a/apps/backend/src/email-login/email-login.service.spec.ts +++ b/apps/backend/src/email-login/email-login.service.spec.ts @@ -5,17 +5,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EmailLoginService } from './email-login.service'; describe('EmailLoginService', () => { - let service: EmailLoginService; + let service: EmailLoginService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [EmailLoginService] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EmailLoginService] + }).compile(); - service = module.get(EmailLoginService); - }); + service = module.get(EmailLoginService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/backend/src/file/file.module.ts b/apps/backend/src/file/file.module.ts index 4ac68551..421e6f2b 100644 --- a/apps/backend/src/file/file.module.ts +++ b/apps/backend/src/file/file.module.ts @@ -5,51 +5,51 @@ import { FileService } from './file.service'; @Module({}) export class FileModule { - static forRootAsync(): DynamicModule { - return { - module : FileModule, - imports : [ConfigModule.forRoot()], - providers: [ - { - provide : 'S3_BUCKET_SONGS', - useFactory: (configService: ConfigService) => - configService.getOrThrow('S3_BUCKET_SONGS'), - inject: [ConfigService] - }, - { - provide : 'S3_BUCKET_THUMBS', - useFactory: (configService: ConfigService) => - configService.getOrThrow('S3_BUCKET_THUMBS'), - inject: [ConfigService] - }, - { - provide : 'S3_KEY', - useFactory: (configService: ConfigService) => - configService.getOrThrow('S3_KEY'), - inject: [ConfigService] - }, - { - provide : 'S3_SECRET', - useFactory: (configService: ConfigService) => - configService.getOrThrow('S3_SECRET'), + static forRootAsync(): DynamicModule { + return { + module : FileModule, + imports : [ConfigModule.forRoot()], + providers: [ + { + provide : 'S3_BUCKET_SONGS', + useFactory: (configService: ConfigService) => + configService.getOrThrow('S3_BUCKET_SONGS'), + inject: [ConfigService] + }, + { + provide : 'S3_BUCKET_THUMBS', + useFactory: (configService: ConfigService) => + configService.getOrThrow('S3_BUCKET_THUMBS'), + inject: [ConfigService] + }, + { + provide : 'S3_KEY', + useFactory: (configService: ConfigService) => + configService.getOrThrow('S3_KEY'), + inject: [ConfigService] + }, + { + provide : 'S3_SECRET', + useFactory: (configService: ConfigService) => + configService.getOrThrow('S3_SECRET'), - inject: [ConfigService] - }, - { - provide : 'S3_ENDPOINT', - useFactory: (configService: ConfigService) => - configService.getOrThrow('S3_ENDPOINT'), - inject: [ConfigService] - }, - { - provide : 'S3_REGION', - useFactory: (configService: ConfigService) => - configService.getOrThrow('S3_REGION'), - inject: [ConfigService] - }, - FileService - ], - exports: [FileService] - }; - } + inject: [ConfigService] + }, + { + provide : 'S3_ENDPOINT', + useFactory: (configService: ConfigService) => + configService.getOrThrow('S3_ENDPOINT'), + inject: [ConfigService] + }, + { + provide : 'S3_REGION', + useFactory: (configService: ConfigService) => + configService.getOrThrow('S3_REGION'), + inject: [ConfigService] + }, + FileService + ], + exports: [FileService] + }; + } } diff --git a/apps/backend/src/file/file.service.spec.ts b/apps/backend/src/file/file.service.spec.ts index bf14fb02..c452e598 100644 --- a/apps/backend/src/file/file.service.spec.ts +++ b/apps/backend/src/file/file.service.spec.ts @@ -7,231 +7,231 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FileService } from './file.service'; mock.module('@aws-sdk/client-s3', () => { - const mS3Client = { - send: jest.fn() - }; - - return { - S3Client : jest.fn(() => mS3Client), - GetObjectCommand : jest.fn(), - PutObjectCommand : jest.fn(), - HeadBucketCommand: jest.fn(), - ObjectCannedACL : { - private : 'private', - public_read: 'public-read' - } - }; + const mS3Client = { + send: jest.fn() + }; + + return { + S3Client : jest.fn(() => mS3Client), + GetObjectCommand : jest.fn(), + PutObjectCommand : jest.fn(), + HeadBucketCommand: jest.fn(), + ObjectCannedACL : { + private : 'private', + public_read: 'public-read' + } + }; }); mock.module('@aws-sdk/s3-request-presigner', () => ({ - getSignedUrl: jest.fn() + getSignedUrl: jest.fn() })); describe('FileService', () => { - let fileService: FileService; - let s3Client: S3Client; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - FileService, - { - provide : 'S3_BUCKET_THUMBS', - useValue: 'test-bucket-thumbs' - }, - { - provide : 'S3_BUCKET_SONGS', - useValue: 'test-bucket-songs' - }, - { - provide : 'S3_KEY', - useValue: 'test-key' - }, - { - provide : 'S3_SECRET', - useValue: 'test-secret' - }, - { - provide : 'S3_ENDPOINT', - useValue: 'test-endpoint' - }, - { - provide : 'S3_REGION', - useValue: 'test-region' - } - ] - }).compile(); + let fileService: FileService; + let s3Client: S3Client; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FileService, + { + provide : 'S3_BUCKET_THUMBS', + useValue: 'test-bucket-thumbs' + }, + { + provide : 'S3_BUCKET_SONGS', + useValue: 'test-bucket-songs' + }, + { + provide : 'S3_KEY', + useValue: 'test-key' + }, + { + provide : 'S3_SECRET', + useValue: 'test-secret' + }, + { + provide : 'S3_ENDPOINT', + useValue: 'test-endpoint' + }, + { + provide : 'S3_REGION', + useValue: 'test-region' + } + ] + }).compile(); + + fileService = module.get(FileService); + + s3Client = new S3Client({}); + }); - fileService = module.get(FileService); + it('should be defined', () => { + expect(fileService).toBeDefined(); + }); - s3Client = new S3Client({}); - }); + describe('verifyBucket', () => { + it('should verify the buckets successfully', async () => { + (s3Client.send as jest.Mock) + .mockResolvedValueOnce({}) // Mock for the first bucket + .mockResolvedValueOnce({}); // Mock for the second bucket - it('should be defined', () => { - expect(fileService).toBeDefined(); - }); + await fileService['verifyBucket'](); - describe('verifyBucket', () => { - it('should verify the buckets successfully', async () => { - (s3Client.send as jest.Mock) - .mockResolvedValueOnce({}) // Mock for the first bucket - .mockResolvedValueOnce({}); // Mock for the second bucket + // Ensure the mock was called twice + expect(s3Client.send).toHaveBeenCalledTimes(4); + }); - await fileService['verifyBucket'](); + it('should log an error if bucket verification fails', async () => { + const error = new Error('Bucket not found'); + (s3Client.send as jest.Mock).mockRejectedValueOnce(error); - // Ensure the mock was called twice - expect(s3Client.send).toHaveBeenCalledTimes(4); + await expect(fileService['verifyBucket']()).rejects.toThrow(error); + }); }); - it('should log an error if bucket verification fails', async () => { - const error = new Error('Bucket not found'); - (s3Client.send as jest.Mock).mockRejectedValueOnce(error); + it('should upload a song', async () => { + const buffer = Buffer.from('test'); + const publicId = 'test-id'; + const mockResponse = { ETag: 'mock-etag' }; + (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); - await expect(fileService['verifyBucket']()).rejects.toThrow(error); + const result = await fileService.uploadSong(buffer, publicId); + expect(result).toBe('songs/test-id.nbs'); }); - }); - - it('should upload a song', async () => { - const buffer = Buffer.from('test'); - const publicId = 'test-id'; - const mockResponse = { ETag: 'mock-etag' }; - (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); - - const result = await fileService.uploadSong(buffer, publicId); - expect(result).toBe('songs/test-id.nbs'); - }); - - it('should throw an error if song upload fails', async () => { - const buffer = Buffer.from('test'); - const publicId = 'test-id'; - - (s3Client.send as jest.Mock).mockRejectedValueOnce( - new Error('Upload failed') - ); - - await expect(fileService.uploadSong(buffer, publicId)).rejects.toThrow( - 'Upload failed' - ); - }); - - it('should get a signed URL for a song download', async () => { - const key = 'test-key'; - const filename = 'test-file.nbs'; - const mockUrl = 'https://mock-signed-url'; - (getSignedUrl as jest.Mock).mockResolvedValueOnce(mockUrl); - - const result = await fileService.getSongDownloadUrl(key, filename); - expect(result).toBe(mockUrl); - }); - - it('should throw an error if signed URL generation fails', async () => { - const key = 'test-key'; - const filename = 'test-file.nbs'; - - (getSignedUrl as jest.Mock).mockRejectedValueOnce( - new Error('Signed URL generation failed') - ); - - await expect(fileService.getSongDownloadUrl(key, filename)).rejects.toThrow( - 'Signed URL generation failed' - ); - }); - - it('should upload a thumbnail', async () => { - const buffer = Buffer.from('test'); - const publicId = 'test-id'; - const mockResponse = { ETag: 'mock-etag' }; - (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); - - const result = await fileService.uploadThumbnail(buffer, publicId); - - expect(result).toBe( - 'https://test-bucket-thumbs.s3.test-region.backblazeb2.com/thumbs/test-id.png' - ); - }); - - it('should delete a song', async () => { - const nbsFileUrl = 'test-file.nbs'; - const mockResponse = {}; - (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); - - await fileService.deleteSong(nbsFileUrl); - expect(s3Client.send).toHaveBeenCalled(); - }); - - it('should throw an error if song deletion fails', async () => { - const nbsFileUrl = 'test-file.nbs'; - - (s3Client.send as jest.Mock).mockRejectedValueOnce( - new Error('Deletion failed') - ); - - await expect(fileService.deleteSong(nbsFileUrl)).rejects.toThrow( - 'Deletion failed' - ); - }); - - it('should get a song file', async () => { - const nbsFileUrl = 'test-file.nbs'; - - const mockResponse = { - Body: { - transformToByteArray: jest - .fn() - .mockResolvedValueOnce(new Uint8Array([1, 2, 3])) - } - }; - (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); + it('should throw an error if song upload fails', async () => { + const buffer = Buffer.from('test'); + const publicId = 'test-id'; - const result = await fileService.getSongFile(nbsFileUrl); + (s3Client.send as jest.Mock).mockRejectedValueOnce( + new Error('Upload failed') + ); - // Convert Uint8Array to ArrayBuffer if needed - const arrayBufferResult = result.slice(0, result.byteLength); + await expect(fileService.uploadSong(buffer, publicId)).rejects.toThrow( + 'Upload failed' + ); + }); - expect(arrayBufferResult).toBeInstanceOf(ArrayBuffer); + it('should get a signed URL for a song download', async () => { + const key = 'test-key'; + const filename = 'test-file.nbs'; + const mockUrl = 'https://mock-signed-url'; + (getSignedUrl as jest.Mock).mockResolvedValueOnce(mockUrl); - expect(new Uint8Array(arrayBufferResult)).toEqual( - new Uint8Array([1, 2, 3]) - ); - }); + const result = await fileService.getSongDownloadUrl(key, filename); + expect(result).toBe(mockUrl); + }); - it('should throw an error if song file retrieval fails', async () => { - const nbsFileUrl = 'test-file.nbs'; + it('should throw an error if signed URL generation fails', async () => { + const key = 'test-key'; + const filename = 'test-file.nbs'; - (s3Client.send as jest.Mock).mockRejectedValueOnce( - new Error('Retrieval failed') - ); + (getSignedUrl as jest.Mock).mockRejectedValueOnce( + new Error('Signed URL generation failed') + ); - await expect(fileService.getSongFile(nbsFileUrl)).rejects.toThrow( - 'Retrieval failed' - ); - }); + await expect(fileService.getSongDownloadUrl(key, filename)).rejects.toThrow( + 'Signed URL generation failed' + ); + }); - it('should throw an error if song file is empty', async () => { - const nbsFileUrl = 'test-file.nbs'; + it('should upload a thumbnail', async () => { + const buffer = Buffer.from('test'); + const publicId = 'test-id'; + const mockResponse = { ETag: 'mock-etag' }; + (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); - const mockResponse = { - Body: { - transformToByteArray: jest.fn().mockResolvedValueOnce(null) - } - }; + const result = await fileService.uploadThumbnail(buffer, publicId); - (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); + expect(result).toBe( + 'https://test-bucket-thumbs.s3.test-region.backblazeb2.com/thumbs/test-id.png' + ); + }); - await expect(fileService.getSongFile(nbsFileUrl)).rejects.toThrow( - 'Error getting file' - ); - }); + it('should delete a song', async () => { + const nbsFileUrl = 'test-file.nbs'; + const mockResponse = {}; + (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); + + await fileService.deleteSong(nbsFileUrl); + expect(s3Client.send).toHaveBeenCalled(); + }); + + it('should throw an error if song deletion fails', async () => { + const nbsFileUrl = 'test-file.nbs'; + + (s3Client.send as jest.Mock).mockRejectedValueOnce( + new Error('Deletion failed') + ); + + await expect(fileService.deleteSong(nbsFileUrl)).rejects.toThrow( + 'Deletion failed' + ); + }); - it('should get upload a packed song', async () => { - const buffer = Buffer.from('test'); - const publicId = 'test-id'; - const mockResponse = { ETag: 'mock-etag' }; - (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); + it('should get a song file', async () => { + const nbsFileUrl = 'test-file.nbs'; - const result = await fileService.uploadPackedSong(buffer, publicId); + const mockResponse = { + Body: { + transformToByteArray: jest + .fn() + .mockResolvedValueOnce(new Uint8Array([1, 2, 3])) + } + }; - expect(result).toBe('packed/test-id.zip'); - }); + (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); + + const result = await fileService.getSongFile(nbsFileUrl); + + // Convert Uint8Array to ArrayBuffer if needed + const arrayBufferResult = result.slice(0, result.byteLength); + + expect(arrayBufferResult).toBeInstanceOf(ArrayBuffer); + + expect(new Uint8Array(arrayBufferResult)).toEqual( + new Uint8Array([1, 2, 3]) + ); + }); + + it('should throw an error if song file retrieval fails', async () => { + const nbsFileUrl = 'test-file.nbs'; + + (s3Client.send as jest.Mock).mockRejectedValueOnce( + new Error('Retrieval failed') + ); + + await expect(fileService.getSongFile(nbsFileUrl)).rejects.toThrow( + 'Retrieval failed' + ); + }); + + it('should throw an error if song file is empty', async () => { + const nbsFileUrl = 'test-file.nbs'; + + const mockResponse = { + Body: { + transformToByteArray: jest.fn().mockResolvedValueOnce(null) + } + }; + + (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); + + await expect(fileService.getSongFile(nbsFileUrl)).rejects.toThrow( + 'Error getting file' + ); + }); + + it('should get upload a packed song', async () => { + const buffer = Buffer.from('test'); + const publicId = 'test-id'; + const mockResponse = { ETag: 'mock-etag' }; + (s3Client.send as jest.Mock).mockResolvedValueOnce(mockResponse); + + const result = await fileService.uploadPackedSong(buffer, publicId); + + expect(result).toBe('packed/test-id.zip'); + }); }); diff --git a/apps/backend/src/file/file.service.ts b/apps/backend/src/file/file.service.ts index 8385a57e..3aa548f6 100644 --- a/apps/backend/src/file/file.service.ts +++ b/apps/backend/src/file/file.service.ts @@ -1,258 +1,258 @@ import * as path from 'path'; import { - GetObjectCommand, - HeadBucketCommand, - ObjectCannedACL, - PutObjectCommand, - S3Client + GetObjectCommand, + HeadBucketCommand, + ObjectCannedACL, + PutObjectCommand, + S3Client } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { Inject, Injectable, Logger } from '@nestjs/common'; @Injectable() export class FileService { - private readonly logger = new Logger(FileService.name); - private s3Client: S3Client; - private region : string; - - constructor( - @Inject('S3_BUCKET_SONGS') - private readonly S3_BUCKET_SONGS: string, - @Inject('S3_BUCKET_THUMBS') - private readonly S3_BUCKET_THUMBS: string, - - @Inject('S3_KEY') - private readonly S3_KEY: string, - @Inject('S3_SECRET') - private readonly S3_SECRET: string, - @Inject('S3_ENDPOINT') - private readonly S3_ENDPOINT: string, - @Inject('S3_REGION') - private readonly S3_REGION: string - ) { - this.s3Client = this.getS3Client(); - // verify that the bucket exists - - this.verifyBucket(); - } - - private async verifyBucket() { - try { - this.logger.debug( - `Verifying buckets ${this.S3_BUCKET_SONGS} and ${this.S3_BUCKET_THUMBS}` - ); - - await Promise.all([ - this.s3Client.send( - new HeadBucketCommand({ Bucket: this.S3_BUCKET_SONGS }) - ), - this.s3Client.send( - new HeadBucketCommand({ Bucket: this.S3_BUCKET_THUMBS }) - ) - ]); - - this.logger.debug('Buckets verification successful'); - } catch (error) { - this.logger.error( - `Error verifying buckets ${this.S3_BUCKET_SONGS} and ${this.S3_BUCKET_THUMBS}`, - error - ); - - throw error; + private readonly logger = new Logger(FileService.name); + private s3Client: S3Client; + private region : string; + + constructor( + @Inject('S3_BUCKET_SONGS') + private readonly S3_BUCKET_SONGS: string, + @Inject('S3_BUCKET_THUMBS') + private readonly S3_BUCKET_THUMBS: string, + + @Inject('S3_KEY') + private readonly S3_KEY: string, + @Inject('S3_SECRET') + private readonly S3_SECRET: string, + @Inject('S3_ENDPOINT') + private readonly S3_ENDPOINT: string, + @Inject('S3_REGION') + private readonly S3_REGION: string + ) { + this.s3Client = this.getS3Client(); + // verify that the bucket exists + + this.verifyBucket(); } - } - private getS3Client() { + private async verifyBucket() { + try { + this.logger.debug( + `Verifying buckets ${this.S3_BUCKET_SONGS} and ${this.S3_BUCKET_THUMBS}` + ); + + await Promise.all([ + this.s3Client.send( + new HeadBucketCommand({ Bucket: this.S3_BUCKET_SONGS }) + ), + this.s3Client.send( + new HeadBucketCommand({ Bucket: this.S3_BUCKET_THUMBS }) + ) + ]); + + this.logger.debug('Buckets verification successful'); + } catch (error) { + this.logger.error( + `Error verifying buckets ${this.S3_BUCKET_SONGS} and ${this.S3_BUCKET_THUMBS}`, + error + ); + + throw error; + } + } + + private getS3Client() { // Load environment variables - const key = this.S3_KEY; - const secret = this.S3_SECRET; - const endpoint = this.S3_ENDPOINT; - const region = this.S3_REGION; - - this.region = region; - - // Create S3 client - const s3Client = new S3Client({ - region : region, - endpoint : endpoint, - credentials: { - accessKeyId : key, - secretAccessKey: secret - }, - forcePathStyle: endpoint.includes('localhost') ? true : false - }); - - return s3Client; - } - - // Uploads a song to the S3 bucket and returns the key - public async uploadSong(buffer: Buffer, publicId: string) { - const bucket = this.S3_BUCKET_SONGS; - - const fileName = - 'songs/' + path.parse(publicId).name.replace(/\s/g, '') + '.nbs'; - - const mimetype = 'application/octet-stream'; - - await this.s3_upload( - buffer, - bucket, - fileName, - mimetype, - ObjectCannedACL.private - ); - - return fileName; - } - - public async uploadPackedSong(buffer: Buffer, publicId: string) { - const bucket = this.S3_BUCKET_SONGS; - - const fileName = - 'packed/' + path.parse(publicId).name.replace(/\s/g, '') + '.zip'; - - const mimetype = 'application/zip'; - - await this.s3_upload( - buffer, - bucket, - fileName, - mimetype, - ObjectCannedACL.private - ); - - return fileName; - } - - public async getSongDownloadUrl(key: string, filename: string) { - const bucket = this.S3_BUCKET_SONGS; - - const command = new GetObjectCommand({ - Bucket : bucket, - Key : key, - ResponseContentDisposition: `attachment; filename="${filename.replace( - /[/"]/g, - '_' - )}"` - }); - - const signedUrl = await getSignedUrl(this.s3Client, command, { - expiresIn: 2 * 60 // 2 minutes - }); - - return signedUrl; - } - - public async uploadThumbnail(buffer: Buffer, publicId: string) { - const bucket = this.S3_BUCKET_THUMBS; - - const fileName = - 'thumbs/' + path.parse(publicId).name.replace(/\s/g, '') + '.png'; - - const mimetype = 'image/jpeg'; - - await this.s3_upload( - buffer, - bucket, - fileName, - mimetype, - ObjectCannedACL.public_read - ); - - return this.getThumbnailUrl(fileName); - } - - public getThumbnailUrl(key: string) { - const bucket = this.S3_BUCKET_THUMBS; - const url = this.getPublicFileUrl(key, bucket); - return url; - } - - private getPublicFileUrl(key: string, bucket: string) { - const region = this.region; - if (this.S3_ENDPOINT.includes('localhost')) { - // minio url - return `${this.S3_ENDPOINT}/${bucket}/${key}`; - } // production blackblaze url - else return `https://${bucket}.s3.${region}.backblazeb2.com/${key}`; // TODO: make possible to use custom domain - } - - public async deleteSong(nbsFileUrl: string) { - const bucket = this.S3_BUCKET_SONGS; - - const command = new GetObjectCommand({ - Bucket: bucket, - Key : nbsFileUrl - }); - - try { - await this.s3Client.send(command); - } catch (error) { - this.logger.error('Error deleting file: ', error); - throw error; + const key = this.S3_KEY; + const secret = this.S3_SECRET; + const endpoint = this.S3_ENDPOINT; + const region = this.S3_REGION; + + this.region = region; + + // Create S3 client + const s3Client = new S3Client({ + region : region, + endpoint : endpoint, + credentials: { + accessKeyId : key, + secretAccessKey: secret + }, + forcePathStyle: endpoint.includes('localhost') ? true : false + }); + + return s3Client; + } + + // Uploads a song to the S3 bucket and returns the key + public async uploadSong(buffer: Buffer, publicId: string) { + const bucket = this.S3_BUCKET_SONGS; + + const fileName = + 'songs/' + path.parse(publicId).name.replace(/\s/g, '') + '.nbs'; + + const mimetype = 'application/octet-stream'; + + await this.s3_upload( + buffer, + bucket, + fileName, + mimetype, + ObjectCannedACL.private + ); + + return fileName; + } + + public async uploadPackedSong(buffer: Buffer, publicId: string) { + const bucket = this.S3_BUCKET_SONGS; + + const fileName = + 'packed/' + path.parse(publicId).name.replace(/\s/g, '') + '.zip'; + + const mimetype = 'application/zip'; + + await this.s3_upload( + buffer, + bucket, + fileName, + mimetype, + ObjectCannedACL.private + ); + + return fileName; + } + + public async getSongDownloadUrl(key: string, filename: string) { + const bucket = this.S3_BUCKET_SONGS; + + const command = new GetObjectCommand({ + Bucket : bucket, + Key : key, + ResponseContentDisposition: `attachment; filename="${filename.replace( + /[/"]/g, + '_' + )}"` + }); + + const signedUrl = await getSignedUrl(this.s3Client, command, { + expiresIn: 2 * 60 // 2 minutes + }); + + return signedUrl; + } + + public async uploadThumbnail(buffer: Buffer, publicId: string) { + const bucket = this.S3_BUCKET_THUMBS; + + const fileName = + 'thumbs/' + path.parse(publicId).name.replace(/\s/g, '') + '.png'; + + const mimetype = 'image/jpeg'; + + await this.s3_upload( + buffer, + bucket, + fileName, + mimetype, + ObjectCannedACL.public_read + ); + + return this.getThumbnailUrl(fileName); + } + + public getThumbnailUrl(key: string) { + const bucket = this.S3_BUCKET_THUMBS; + const url = this.getPublicFileUrl(key, bucket); + return url; + } + + private getPublicFileUrl(key: string, bucket: string) { + const region = this.region; + if (this.S3_ENDPOINT.includes('localhost')) { + // minio url + return `${this.S3_ENDPOINT}/${bucket}/${key}`; + } // production blackblaze url + else return `https://${bucket}.s3.${region}.backblazeb2.com/${key}`; // TODO: make possible to use custom domain + } + + public async deleteSong(nbsFileUrl: string) { + const bucket = this.S3_BUCKET_SONGS; + + const command = new GetObjectCommand({ + Bucket: bucket, + Key : nbsFileUrl + }); + + try { + await this.s3Client.send(command); + } catch (error) { + this.logger.error('Error deleting file: ', error); + throw error; + } + + return; } - return; - } - - async s3_upload( - file: Buffer, - bucket: string, - name: string, - mimetype: string, - accessControl: ObjectCannedACL = ObjectCannedACL.public_read - ) { - const params = { - Bucket : bucket, - Key : String(name), - Body : file, - ACL : accessControl, - ContentType : mimetype, - ContentDisposition : `attachment; filename=${name.split('/').pop()}`, - CreateBucketConfiguration: { - LocationConstraint: 'ap-south-1' - } - }; - - const command = new PutObjectCommand(params); - - try { - const s3Response = await this.s3Client.send(command); - return s3Response; - } catch (error) { - this.logger.error('Error uploading file: ', error); - throw error; + async s3_upload( + file: Buffer, + bucket: string, + name: string, + mimetype: string, + accessControl: ObjectCannedACL = ObjectCannedACL.public_read + ) { + const params = { + Bucket : bucket, + Key : String(name), + Body : file, + ACL : accessControl, + ContentType : mimetype, + ContentDisposition : `attachment; filename=${name.split('/').pop()}`, + CreateBucketConfiguration: { + LocationConstraint: 'ap-south-1' + } + }; + + const command = new PutObjectCommand(params); + + try { + const s3Response = await this.s3Client.send(command); + return s3Response; + } catch (error) { + this.logger.error('Error uploading file: ', error); + throw error; + } } - } - public async getSongFile(nbsFileUrl: string): Promise { - const bucket = this.S3_BUCKET_SONGS; + public async getSongFile(nbsFileUrl: string): Promise { + const bucket = this.S3_BUCKET_SONGS; - const command = new GetObjectCommand({ - Bucket: bucket, - Key : nbsFileUrl - }); + const command = new GetObjectCommand({ + Bucket: bucket, + Key : nbsFileUrl + }); - try { - const response = await this.s3Client.send(command); - const byteArray = await response.Body?.transformToByteArray(); + try { + const response = await this.s3Client.send(command); + const byteArray = await response.Body?.transformToByteArray(); - if (!byteArray) { - throw new Error('Error getting file'); - } + if (!byteArray) { + throw new Error('Error getting file'); + } - const arrayBuffer = new ArrayBuffer(byteArray.length); - const view = new Uint8Array(arrayBuffer); + const arrayBuffer = new ArrayBuffer(byteArray.length); + const view = new Uint8Array(arrayBuffer); - for (let i = 0; i < byteArray.length; i++) { - view[i] = byteArray[i]; - } + for (let i = 0; i < byteArray.length; i++) { + view[i] = byteArray[i]; + } - return arrayBuffer; - } catch (error) { - this.logger.error('Error getting file: ', error); - throw error; + return arrayBuffer; + } catch (error) { + this.logger.error('Error getting file: ', error); + throw error; + } } - } } diff --git a/apps/backend/src/lib/GetRequestUser.spec.ts b/apps/backend/src/lib/GetRequestUser.spec.ts index ec7a9e9f..7e25a445 100644 --- a/apps/backend/src/lib/GetRequestUser.spec.ts +++ b/apps/backend/src/lib/GetRequestUser.spec.ts @@ -6,39 +6,39 @@ import { ExecutionContext, HttpException, HttpStatus } from '@nestjs/common'; import { GetRequestToken, validateUser } from './GetRequestUser'; describe('GetRequestToken', () => { - it('should be a defined decorator', () => { - const mockExecutionContext = { - switchToHttp: jest.fn().mockReturnThis() - } as unknown as ExecutionContext; + it('should be a defined decorator', () => { + const mockExecutionContext = { + switchToHttp: jest.fn().mockReturnThis() + } as unknown as ExecutionContext; - const result = GetRequestToken(null, mockExecutionContext); + const result = GetRequestToken(null, mockExecutionContext); - expect(typeof result).toBe('function'); - }); + expect(typeof result).toBe('function'); + }); }); describe('validateUser', () => { - it('should return the user if the user exists', () => { - const mockUser = { - _id : 'test-id', - username: 'testuser' - } as unknown as UserDocument; - - const result = validateUser(mockUser); - - expect(result).toEqual(mockUser); - }); - - it('should throw an error if the user does not exist', () => { - expect(() => validateUser(null)).toThrowError( - new HttpException( - { - error: { - user: 'User not found' - } - }, - HttpStatus.UNAUTHORIZED - ) - ); - }); + it('should return the user if the user exists', () => { + const mockUser = { + _id : 'test-id', + username: 'testuser' + } as unknown as UserDocument; + + const result = validateUser(mockUser); + + expect(result).toEqual(mockUser); + }); + + it('should throw an error if the user does not exist', () => { + expect(() => validateUser(null)).toThrowError( + new HttpException( + { + error: { + user: 'User not found' + } + }, + HttpStatus.UNAUTHORIZED + ) + ); + }); }); diff --git a/apps/backend/src/lib/GetRequestUser.ts b/apps/backend/src/lib/GetRequestUser.ts index 13d37d84..ab34fad0 100644 --- a/apps/backend/src/lib/GetRequestUser.ts +++ b/apps/backend/src/lib/GetRequestUser.ts @@ -1,35 +1,35 @@ import type { UserDocument } from '@nbw/database'; import { - ExecutionContext, - HttpException, - HttpStatus, - createParamDecorator + ExecutionContext, + HttpException, + HttpStatus, + createParamDecorator } from '@nestjs/common'; import type { Request } from 'express'; export const GetRequestToken = createParamDecorator( - (data: unknown, ctx: ExecutionContext) => { - const req = ctx - .switchToHttp() - .getRequest(); + (data: unknown, ctx: ExecutionContext) => { + const req = ctx + .switchToHttp() + .getRequest(); - const user = req.existingUser; + const user = req.existingUser; - return user; - } + return user; + } ); export const validateUser = (user: UserDocument | null) => { - if (!user) { - throw new HttpException( - { - error: { - user: 'User not found' - } - }, - HttpStatus.UNAUTHORIZED - ); - } + if (!user) { + throw new HttpException( + { + error: { + user: 'User not found' + } + }, + HttpStatus.UNAUTHORIZED + ); + } - return user; + return user; }; diff --git a/apps/backend/src/lib/initializeSwagger.spec.ts b/apps/backend/src/lib/initializeSwagger.spec.ts index 319fbf17..c119e85b 100644 --- a/apps/backend/src/lib/initializeSwagger.spec.ts +++ b/apps/backend/src/lib/initializeSwagger.spec.ts @@ -6,43 +6,43 @@ import { SwaggerModule } from '@nestjs/swagger'; import { initializeSwagger } from './initializeSwagger'; mock.module('@nestjs/swagger', () => ({ - DocumentBuilder: jest.fn().mockImplementation(() => ({ - setTitle : jest.fn().mockReturnThis(), - setDescription: jest.fn().mockReturnThis(), - setVersion : jest.fn().mockReturnThis(), - addBearerAuth : jest.fn().mockReturnThis(), - build : jest.fn().mockReturnValue({}) - })), - SwaggerModule: { - createDocument: jest.fn().mockReturnValue({}), - setup : jest.fn() - } + DocumentBuilder: jest.fn().mockImplementation(() => ({ + setTitle : jest.fn().mockReturnThis(), + setDescription: jest.fn().mockReturnThis(), + setVersion : jest.fn().mockReturnThis(), + addBearerAuth : jest.fn().mockReturnThis(), + build : jest.fn().mockReturnValue({}) + })), + SwaggerModule: { + createDocument: jest.fn().mockReturnValue({}), + setup : jest.fn() + } })); describe('initializeSwagger', () => { - let app: INestApplication; - - beforeEach(() => { - app = {} as INestApplication; - }); - - it('should initialize Swagger with the correct configuration', () => { - initializeSwagger(app); - - expect(SwaggerModule.createDocument).toHaveBeenCalledWith( - app, - expect.any(Object) - ); - - expect(SwaggerModule.setup).toHaveBeenCalledWith( - 'api/doc', - app, - expect.any(Object), - { - swaggerOptions: { - persistAuthorization: true - } - } - ); - }); + let app: INestApplication; + + beforeEach(() => { + app = {} as INestApplication; + }); + + it('should initialize Swagger with the correct configuration', () => { + initializeSwagger(app); + + expect(SwaggerModule.createDocument).toHaveBeenCalledWith( + app, + expect.any(Object) + ); + + expect(SwaggerModule.setup).toHaveBeenCalledWith( + 'api/doc', + app, + expect.any(Object), + { + swaggerOptions: { + persistAuthorization: true + } + } + ); + }); }); diff --git a/apps/backend/src/lib/initializeSwagger.ts b/apps/backend/src/lib/initializeSwagger.ts index 06c0871d..423eaa87 100644 --- a/apps/backend/src/lib/initializeSwagger.ts +++ b/apps/backend/src/lib/initializeSwagger.ts @@ -1,25 +1,25 @@ import { INestApplication } from '@nestjs/common'; import { - DocumentBuilder, - SwaggerCustomOptions, - SwaggerModule + DocumentBuilder, + SwaggerCustomOptions, + SwaggerModule } from '@nestjs/swagger'; export function initializeSwagger(app: INestApplication) { - const config = new DocumentBuilder() - .setTitle('NoteBlockWorld API Backend') - .setDescription('Backend application for NoteBlockWorld') - .setVersion('1.0') - .addBearerAuth() - .build(); + const config = new DocumentBuilder() + .setTitle('NoteBlockWorld API Backend') + .setDescription('Backend application for NoteBlockWorld') + .setVersion('1.0') + .addBearerAuth() + .build(); - const document = SwaggerModule.createDocument(app, config); + const document = SwaggerModule.createDocument(app, config); - const swaggerOptions: SwaggerCustomOptions = { - swaggerOptions: { - persistAuthorization: true - } - }; + const swaggerOptions: SwaggerCustomOptions = { + swaggerOptions: { + persistAuthorization: true + } + }; - SwaggerModule.setup('docs', app, document, swaggerOptions); + SwaggerModule.setup('docs', app, document, swaggerOptions); } diff --git a/apps/backend/src/lib/parseToken.spec.ts b/apps/backend/src/lib/parseToken.spec.ts index 274c69ba..1b9d2c6f 100644 --- a/apps/backend/src/lib/parseToken.spec.ts +++ b/apps/backend/src/lib/parseToken.spec.ts @@ -8,79 +8,79 @@ import { AuthService } from '@server/auth/auth.service'; import { ParseTokenPipe } from './parseToken'; describe('ParseTokenPipe', () => { - let parseTokenPipe: ParseTokenPipe; - let authService: AuthService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ParseTokenPipe, - { - provide : AuthService, - useValue: { - getUserFromToken: jest.fn() - } - } - ] - }).compile(); - - parseTokenPipe = module.get(ParseTokenPipe); - authService = module.get(AuthService); - }); - - it('should be defined', () => { - expect(parseTokenPipe).toBeDefined(); - }); - - describe('canActivate', () => { - it('should return true if no authorization header is present', async () => { - const mockExecutionContext = { - switchToHttp: jest.fn().mockReturnThis(), - getRequest : jest.fn().mockReturnValue({ headers: {} }) - } as unknown as ExecutionContext; - - const result = await parseTokenPipe.canActivate(mockExecutionContext); - - expect(result).toBe(true); + let parseTokenPipe: ParseTokenPipe; + let authService: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ParseTokenPipe, + { + provide : AuthService, + useValue: { + getUserFromToken: jest.fn() + } + } + ] + }).compile(); + + parseTokenPipe = module.get(ParseTokenPipe); + authService = module.get(AuthService); }); - it('should return true if user is not found from token', async () => { - const mockExecutionContext = { - switchToHttp: jest.fn().mockReturnThis(), - getRequest : jest.fn().mockReturnValue({ - headers: { authorization: 'Bearer test-token' } - }) - } as unknown as ExecutionContext; + it('should be defined', () => { + expect(parseTokenPipe).toBeDefined(); + }); - jest.spyOn(authService, 'getUserFromToken').mockResolvedValue(null); + describe('canActivate', () => { + it('should return true if no authorization header is present', async () => { + const mockExecutionContext = { + switchToHttp: jest.fn().mockReturnThis(), + getRequest : jest.fn().mockReturnValue({ headers: {} }) + } as unknown as ExecutionContext; - const result = await parseTokenPipe.canActivate(mockExecutionContext); + const result = await parseTokenPipe.canActivate(mockExecutionContext); - expect(result).toBe(true); - expect(authService.getUserFromToken).toHaveBeenCalledWith('test-token'); - }); + expect(result).toBe(true); + }); + + it('should return true if user is not found from token', async () => { + const mockExecutionContext = { + switchToHttp: jest.fn().mockReturnThis(), + getRequest : jest.fn().mockReturnValue({ + headers: { authorization: 'Bearer test-token' } + }) + } as unknown as ExecutionContext; + + jest.spyOn(authService, 'getUserFromToken').mockResolvedValue(null); + + const result = await parseTokenPipe.canActivate(mockExecutionContext); + + expect(result).toBe(true); + expect(authService.getUserFromToken).toHaveBeenCalledWith('test-token'); + }); - it('should set existingUser on request and return true if user is found from token', async () => { - const mockUser = { _id: 'test-id', username: 'testuser' } as any; + it('should set existingUser on request and return true if user is found from token', async () => { + const mockUser = { _id: 'test-id', username: 'testuser' } as any; - const mockExecutionContext = { - switchToHttp: jest.fn().mockReturnThis(), - getRequest : jest.fn().mockReturnValue({ - headers : { authorization: 'Bearer test-token' }, - existingUser: null - }) - } as unknown as ExecutionContext; + const mockExecutionContext = { + switchToHttp: jest.fn().mockReturnThis(), + getRequest : jest.fn().mockReturnValue({ + headers : { authorization: 'Bearer test-token' }, + existingUser: null + }) + } as unknown as ExecutionContext; - jest.spyOn(authService, 'getUserFromToken').mockResolvedValue(mockUser); + jest.spyOn(authService, 'getUserFromToken').mockResolvedValue(mockUser); - const result = await parseTokenPipe.canActivate(mockExecutionContext); + const result = await parseTokenPipe.canActivate(mockExecutionContext); - expect(result).toBe(true); - expect(authService.getUserFromToken).toHaveBeenCalledWith('test-token'); + expect(result).toBe(true); + expect(authService.getUserFromToken).toHaveBeenCalledWith('test-token'); - expect( - mockExecutionContext.switchToHttp().getRequest().existingUser - ).toEqual(mockUser); + expect( + mockExecutionContext.switchToHttp().getRequest().existingUser + ).toEqual(mockUser); + }); }); - }); }); diff --git a/apps/backend/src/lib/parseToken.ts b/apps/backend/src/lib/parseToken.ts index 5f2d0ba6..3b135686 100644 --- a/apps/backend/src/lib/parseToken.ts +++ b/apps/backend/src/lib/parseToken.ts @@ -1,39 +1,39 @@ import { - CanActivate, - ExecutionContext, - Inject, - Injectable, - Logger + CanActivate, + ExecutionContext, + Inject, + Injectable, + Logger } from '@nestjs/common'; import { AuthService } from '@server/auth/auth.service'; @Injectable() export class ParseTokenPipe implements CanActivate { - private static logger = new Logger(ParseTokenPipe.name); + private static logger = new Logger(ParseTokenPipe.name); - constructor( - @Inject(AuthService) - private readonly authService: AuthService - ) {} + constructor( + @Inject(AuthService) + private readonly authService: AuthService + ) {} - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const bearerToken = request.headers.authorization; + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const bearerToken = request.headers.authorization; - if (!bearerToken) { - return true; - } + if (!bearerToken) { + return true; + } - const token = bearerToken.split(' ')[1]; - const user = await this.authService.getUserFromToken(token); + const token = bearerToken.split(' ')[1]; + const user = await this.authService.getUserFromToken(token); - if (!user) { - return true; - } + if (!user) { + return true; + } - request.existingUser = user; + request.existingUser = user; - return true; - } + return true; + } } diff --git a/apps/backend/src/mailing/mailing.controller.spec.ts b/apps/backend/src/mailing/mailing.controller.spec.ts index 5962a32b..713384f3 100644 --- a/apps/backend/src/mailing/mailing.controller.spec.ts +++ b/apps/backend/src/mailing/mailing.controller.spec.ts @@ -6,23 +6,23 @@ import { MailingController } from './mailing.controller'; import { MailingService } from './mailing.service'; describe('MailingController', () => { - let controller: MailingController; + let controller: MailingController; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [MailingController], - providers : [ - { - provide : MailingService, - useValue: {} - } - ] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MailingController], + providers : [ + { + provide : MailingService, + useValue: {} + } + ] + }).compile(); - controller = module.get(MailingController); - }); + controller = module.get(MailingController); + }); - it('should be defined', () => { - expect(controller).toBeDefined(); - }); + it('should be defined', () => { + expect(controller).toBeDefined(); + }); }); diff --git a/apps/backend/src/mailing/mailing.controller.ts b/apps/backend/src/mailing/mailing.controller.ts index 51a4e0ff..2b60e2e5 100644 --- a/apps/backend/src/mailing/mailing.controller.ts +++ b/apps/backend/src/mailing/mailing.controller.ts @@ -4,5 +4,5 @@ import { MailingService } from './mailing.service'; @Controller() export class MailingController { - constructor(private readonly mailingService: MailingService) {} + constructor(private readonly mailingService: MailingService) {} } diff --git a/apps/backend/src/mailing/mailing.module.ts b/apps/backend/src/mailing/mailing.module.ts index f732bf1e..69edf0eb 100644 --- a/apps/backend/src/mailing/mailing.module.ts +++ b/apps/backend/src/mailing/mailing.module.ts @@ -4,8 +4,8 @@ import { MailingController } from './mailing.controller'; import { MailingService } from './mailing.service'; @Module({ - controllers: [MailingController], - providers : [MailingService], - exports : [MailingService] + controllers: [MailingController], + providers : [MailingService], + exports : [MailingService] }) export class MailingModule {} diff --git a/apps/backend/src/mailing/mailing.service.spec.ts b/apps/backend/src/mailing/mailing.service.spec.ts index 0dce3220..d5e46f53 100644 --- a/apps/backend/src/mailing/mailing.service.spec.ts +++ b/apps/backend/src/mailing/mailing.service.spec.ts @@ -6,65 +6,65 @@ import { MailerService } from '@nestjs-modules/mailer'; import { MailingService } from './mailing.service'; const MockedMailerService = { - sendMail: jest.fn() + sendMail: jest.fn() }; describe('MailingService', () => { - let service: MailingService; - let mailerService: MailerService; + let service: MailingService; + let mailerService: MailerService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - MailingService, - { - provide : MailerService, - useValue: MockedMailerService - } - ] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MailingService, + { + provide : MailerService, + useValue: MockedMailerService + } + ] + }).compile(); - service = module.get(MailingService); - mailerService = module.get(MailerService); - }); + service = module.get(MailingService); + mailerService = module.get(MailerService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); - it('should send an email using a template', async () => { - const sendMailSpy = jest - .spyOn(mailerService, 'sendMail') - .mockResolvedValueOnce(undefined); + it('should send an email using a template', async () => { + const sendMailSpy = jest + .spyOn(mailerService, 'sendMail') + .mockResolvedValueOnce(undefined); - const to = 'accountHolder@example.com'; - const subject = 'Test Email'; - const template = 'hello'; + const to = 'accountHolder@example.com'; + const subject = 'Test Email'; + const template = 'hello'; - const context = { - name : 'John Doe', - message: 'Hello, this is a test email!' - }; + const context = { + name : 'John Doe', + message: 'Hello, this is a test email!' + }; - await service.sendEmail({ to, subject, template, context }); + await service.sendEmail({ to, subject, template, context }); - expect(sendMailSpy).toHaveBeenCalledWith({ - to, - subject, - template, - context, - attachments: [ - { - filename: 'background-image.png', - cid : 'background-image', - path : `${__dirname}/templates/img/background-image.png` - }, - { - filename: 'logo.png', - cid : 'logo', - path : `${__dirname}/templates/img/logo.png` - } - ] + expect(sendMailSpy).toHaveBeenCalledWith({ + to, + subject, + template, + context, + attachments: [ + { + filename: 'background-image.png', + cid : 'background-image', + path : `${__dirname}/templates/img/background-image.png` + }, + { + filename: 'logo.png', + cid : 'logo', + path : `${__dirname}/templates/img/logo.png` + } + ] + }); }); - }); }); diff --git a/apps/backend/src/mailing/mailing.service.ts b/apps/backend/src/mailing/mailing.service.ts index b8ebdf19..3259acc0 100644 --- a/apps/backend/src/mailing/mailing.service.ts +++ b/apps/backend/src/mailing/mailing.service.ts @@ -2,52 +2,52 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { MailerService } from '@nestjs-modules/mailer'; interface EmailOptions { - to : string; - subject : string; - template: string; - context: { - [name: string]: any; - }; + to : string; + subject : string; + template: string; + context: { + [name: string]: any; + }; } @Injectable() export class MailingService { - private readonly logger = new Logger(MailingService.name); - constructor( - @Inject(MailerService) - private readonly mailerService: MailerService - ) {} + private readonly logger = new Logger(MailingService.name); + constructor( + @Inject(MailerService) + private readonly mailerService: MailerService + ) {} - async sendEmail({ - to, - subject, - template, - context - }: EmailOptions): Promise { - try { - await this.mailerService.sendMail({ + async sendEmail({ to, subject, - template : `${template}`, // The template file name (without extension) - context, // The context to be passed to the template - attachments: [ - { - filename: 'background-image.png', - cid : 'background-image', - path : `${__dirname}/templates/img/background-image.png` - }, - { - filename: 'logo.png', - cid : 'logo', - path : `${__dirname}/templates/img/logo.png` - } - ] - }); + template, + context + }: EmailOptions): Promise { + try { + await this.mailerService.sendMail({ + to, + subject, + template : `${template}`, // The template file name (without extension) + context, // The context to be passed to the template + attachments: [ + { + filename: 'background-image.png', + cid : 'background-image', + path : `${__dirname}/templates/img/background-image.png` + }, + { + filename: 'logo.png', + cid : 'logo', + path : `${__dirname}/templates/img/logo.png` + } + ] + }); - this.logger.debug(`Email sent to ${to}`); - } catch (error) { - this.logger.error(`Failed to send email to ${to}`, error); - throw error; + this.logger.debug(`Email sent to ${to}`); + } catch (error) { + this.logger.error(`Failed to send email to ${to}`, error); + throw error; + } } - } } diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 7f1f60ab..e95c0cef 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -9,56 +9,56 @@ import { ParseTokenPipe } from './lib/parseToken'; const logger: Logger = new Logger('main.ts'); async function bootstrap() { - const app = await NestFactory.create(AppModule); - app.setGlobalPrefix('v1'); + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix('v1'); - const parseTokenPipe = app.get(ParseTokenPipe); + const parseTokenPipe = app.get(ParseTokenPipe); - app.useGlobalGuards(parseTokenPipe); + app.useGlobalGuards(parseTokenPipe); - app.useGlobalPipes( - new ValidationPipe({ - transform : true, - transformOptions: { - enableImplicitConversion: true - } - }) - ); + app.useGlobalPipes( + new ValidationPipe({ + transform : true, + transformOptions: { + enableImplicitConversion: true + } + }) + ); - app.use(express.json({ limit: '50mb' })); - app.use(express.urlencoded({ extended: true, limit: '50mb' })); + app.use(express.json({ limit: '50mb' })); + app.use(express.urlencoded({ extended: true, limit: '50mb' })); - if (process.env.NODE_ENV === 'development') { - initializeSwagger(app); - } + if (process.env.NODE_ENV === 'development') { + initializeSwagger(app); + } - // enable cors - app.enableCors({ - allowedHeaders: ['content-type', 'authorization', 'src'], - exposedHeaders: ['Content-Disposition'], - origin : [process.env.FRONTEND_URL || '', 'https://bentroen.github.io'], - credentials : true - }); + // enable cors + app.enableCors({ + allowedHeaders: ['content-type', 'authorization', 'src'], + exposedHeaders: ['Content-Disposition'], + origin : [process.env.FRONTEND_URL || '', 'https://bentroen.github.io'], + credentials : true + }); - app.use('/v1', express.static('public')); + app.use('/v1', express.static('public')); - const port = process.env.PORT || '4000'; + const port = process.env.PORT || '4000'; - logger.log('Note Block World API Backend 🎶🌎🌍🌏 '); + logger.log('Note Block World API Backend 🎶🌎🌍🌏 '); - await app.listen(port); + await app.listen(port); - return port; + return port; } bootstrap() - .then((port) => { - logger.warn(`Application is running on: http://localhost:${port}`); + .then((port) => { + logger.warn(`Application is running on: http://localhost:${port}`); - if (process.env.NODE_ENV === 'development') { - logger.warn(`Swagger is running on: http://localhost:${port}/docs`); - } - }) - .catch((error) => { - logger.error(`Error: ${error}`); - }); + if (process.env.NODE_ENV === 'development') { + logger.warn(`Swagger is running on: http://localhost:${port}/docs`); + } + }) + .catch((error) => { + logger.error(`Error: ${error}`); + }); diff --git a/apps/backend/src/seed/seed.controller.spec.ts b/apps/backend/src/seed/seed.controller.spec.ts index f5a725b1..5053a847 100644 --- a/apps/backend/src/seed/seed.controller.spec.ts +++ b/apps/backend/src/seed/seed.controller.spec.ts @@ -6,23 +6,23 @@ import { SeedController } from './seed.controller'; import { SeedService } from './seed.service'; describe('SeedController', () => { - let controller: SeedController; + let controller: SeedController; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SeedController], - providers : [ - { - provide : SeedService, - useValue: {} - } - ] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SeedController], + providers : [ + { + provide : SeedService, + useValue: {} + } + ] + }).compile(); - controller = module.get(SeedController); - }); + controller = module.get(SeedController); + }); - it('should be defined', () => { - expect(controller).toBeDefined(); - }); + it('should be defined', () => { + expect(controller).toBeDefined(); + }); }); diff --git a/apps/backend/src/seed/seed.controller.ts b/apps/backend/src/seed/seed.controller.ts index 5e9b1ed5..cfb870d0 100644 --- a/apps/backend/src/seed/seed.controller.ts +++ b/apps/backend/src/seed/seed.controller.ts @@ -6,16 +6,16 @@ import { SeedService } from './seed.service'; @Controller('seed') @ApiTags('seed') export class SeedController { - constructor(private readonly seedService: SeedService) {} + constructor(private readonly seedService: SeedService) {} - @Get('seed-dev') - @ApiOperation({ - summary: 'Seed the database with development data' - }) - async seed() { - this.seedService.seedDev(); - return { - message: 'Seeding in progress' - }; - } + @Get('seed-dev') + @ApiOperation({ + summary: 'Seed the database with development data' + }) + async seed() { + this.seedService.seedDev(); + return { + message: 'Seeding in progress' + }; + } } diff --git a/apps/backend/src/seed/seed.module.ts b/apps/backend/src/seed/seed.module.ts index 5d8f01d7..cb71e99d 100644 --- a/apps/backend/src/seed/seed.module.ts +++ b/apps/backend/src/seed/seed.module.ts @@ -11,30 +11,30 @@ import { SeedService } from './seed.service'; @Module({}) export class SeedModule { - private static readonly logger = new Logger(SeedModule.name); - static forRoot(): DynamicModule { - if (env.NODE_ENV !== 'development') { - SeedModule.logger.warn('Seeding is only allowed in development mode'); - return { - module: SeedModule - }; - } else { - SeedModule.logger.warn('Seeding is allowed in development mode'); - return { - module : SeedModule, - imports : [UserModule, SongModule, ConfigModule.forRoot()], - providers: [ - ConfigService, - SeedService, - { - provide : 'NODE_ENV', - useFactory: (configService: ConfigService) => - configService.get('NODE_ENV'), - inject: [ConfigService] - } - ], - controllers: [SeedController] - }; + private static readonly logger = new Logger(SeedModule.name); + static forRoot(): DynamicModule { + if (env.NODE_ENV !== 'development') { + SeedModule.logger.warn('Seeding is only allowed in development mode'); + return { + module: SeedModule + }; + } else { + SeedModule.logger.warn('Seeding is allowed in development mode'); + return { + module : SeedModule, + imports : [UserModule, SongModule, ConfigModule.forRoot()], + providers: [ + ConfigService, + SeedService, + { + provide : 'NODE_ENV', + useFactory: (configService: ConfigService) => + configService.get('NODE_ENV'), + inject: [ConfigService] + } + ], + controllers: [SeedController] + }; + } } - } } diff --git a/apps/backend/src/seed/seed.service.spec.ts b/apps/backend/src/seed/seed.service.spec.ts index 83d23979..ebe9765b 100644 --- a/apps/backend/src/seed/seed.service.spec.ts +++ b/apps/backend/src/seed/seed.service.spec.ts @@ -8,36 +8,36 @@ import { UserService } from '@server/user/user.service'; import { SeedService } from './seed.service'; describe('SeedService', () => { - let service: SeedService; + let service: SeedService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SeedService, - { - provide : 'NODE_ENV', - useValue: 'development' - }, - { - provide : UserService, - useValue: { - createWithPassword: jest.fn() - } - }, - { - provide : SongService, - useValue: { - uploadSong : jest.fn(), - getSongById: jest.fn() - } - } - ] - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SeedService, + { + provide : 'NODE_ENV', + useValue: 'development' + }, + { + provide : UserService, + useValue: { + createWithPassword: jest.fn() + } + }, + { + provide : SongService, + useValue: { + uploadSong : jest.fn(), + getSongById: jest.fn() + } + } + ] + }).compile(); - service = module.get(SeedService); - }); + service = module.get(SeedService); + }); - it('should be defined', () => { - expect(service).toBeDefined(); - }); + it('should be defined', () => { + expect(service).toBeDefined(); + }); }); diff --git a/apps/backend/src/seed/seed.service.ts b/apps/backend/src/seed/seed.service.ts index 3882ec17..9ade7b32 100644 --- a/apps/backend/src/seed/seed.service.ts +++ b/apps/backend/src/seed/seed.service.ts @@ -2,19 +2,19 @@ import { Instrument, Note, Song } from '@encode42/nbs.js'; import { faker } from '@faker-js/faker'; import { UPLOAD_CONSTANTS } from '@nbw/config'; import { - CategoryType, - LicenseType, - SongDocument, - UploadSongDto, - UserDocument, - VisibilityType + CategoryType, + LicenseType, + SongDocument, + UploadSongDto, + UserDocument, + VisibilityType } from '@nbw/database'; import { - HttpException, - HttpStatus, - Inject, - Injectable, - Logger + HttpException, + HttpStatus, + Inject, + Injectable, + Logger } from '@nestjs/common'; import { SongService } from '@server/song/song.service'; @@ -22,202 +22,202 @@ import { UserService } from '@server/user/user.service'; @Injectable() export class SeedService { - public readonly logger = new Logger(SeedService.name); - constructor( - @Inject('NODE_ENV') - private readonly NODE_ENV: string, - - @Inject(UserService) - private readonly userService: UserService, - - @Inject(SongService) - private readonly songService: SongService - ) {} - - public async seedDev() { - if (this.NODE_ENV !== 'development') { - this.logger.error('Seeding is only allowed in development mode'); - throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); + public readonly logger = new Logger(SeedService.name); + constructor( + @Inject('NODE_ENV') + private readonly NODE_ENV: string, + + @Inject(UserService) + private readonly userService: UserService, + + @Inject(SongService) + private readonly songService: SongService + ) {} + + public async seedDev() { + if (this.NODE_ENV !== 'development') { + this.logger.error('Seeding is only allowed in development mode'); + throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); + } + + const createdUsers = await this.seedUsers(); + this.logger.log(`Created ${createdUsers.length} users`); + const createdSongs = await this.seedSongs(createdUsers); + this.logger.log(`Created ${createdSongs.length} songs`); } - const createdUsers = await this.seedUsers(); - this.logger.log(`Created ${createdUsers.length} users`); - const createdSongs = await this.seedSongs(createdUsers); - this.logger.log(`Created ${createdSongs.length} songs`); - } - - private async seedUsers() { - const createdUsers: UserDocument[] = []; - - for (let i = 0; i < 100; i++) { - const user = await this.userService.createWithEmail( - faker.internet.email() - ); - - //change user creation date - (user as any).createdAt = this.generateRandomDate( - new Date(2020, 0, 1), - new Date() - ); - - user.loginCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); - user.playCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); - user.description = faker.lorem.paragraph(); - - user.socialLinks = { - youtube : faker.internet.url(), - x : faker.internet.url(), - discord : faker.internet.url(), - instagram : faker.internet.url(), - twitch : faker.internet.url(), - bandcamp : faker.internet.url(), - facebook : faker.internet.url(), - github : faker.internet.url(), - reddit : faker.internet.url(), - snapchat : faker.internet.url(), - soundcloud: faker.internet.url(), - spotify : faker.internet.url(), - steam : faker.internet.url(), - telegram : faker.internet.url(), - tiktok : faker.internet.url() - }; - - // remove some social links randomly to simulate users not having all of them or having none - for (const key in Object.keys(user.socialLinks)) - if (faker.datatype.boolean()) delete (user.socialLinks as any)[key]; - - createdUsers.push(await this.userService.update(user)); + private async seedUsers() { + const createdUsers: UserDocument[] = []; + + for (let i = 0; i < 100; i++) { + const user = await this.userService.createWithEmail( + faker.internet.email() + ); + + //change user creation date + (user as any).createdAt = this.generateRandomDate( + new Date(2020, 0, 1), + new Date() + ); + + user.loginCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); + user.playCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); + user.description = faker.lorem.paragraph(); + + user.socialLinks = { + youtube : faker.internet.url(), + x : faker.internet.url(), + discord : faker.internet.url(), + instagram : faker.internet.url(), + twitch : faker.internet.url(), + bandcamp : faker.internet.url(), + facebook : faker.internet.url(), + github : faker.internet.url(), + reddit : faker.internet.url(), + snapchat : faker.internet.url(), + soundcloud: faker.internet.url(), + spotify : faker.internet.url(), + steam : faker.internet.url(), + telegram : faker.internet.url(), + tiktok : faker.internet.url() + }; + + // remove some social links randomly to simulate users not having all of them or having none + for (const key in Object.keys(user.socialLinks)) + if (faker.datatype.boolean()) delete (user.socialLinks as any)[key]; + + createdUsers.push(await this.userService.update(user)); + } + + return createdUsers; } - return createdUsers; - } - - private async seedSongs(users: UserDocument[]) { - const songs: SongDocument[] = []; - const licenses = Object.keys(UPLOAD_CONSTANTS.licenses); - const categories = Object.keys(UPLOAD_CONSTANTS.categories); - const visibilities = Object.keys(UPLOAD_CONSTANTS.visibility); - - for (const user of users) { - // most users will have 0-5 songs and a few will have 5-10, not a real statist by whatever I just what to test the system in development mode - const songCount = this.generateExponentialRandom(5, 2, 0.5, 10); - - for (let i = 0; i < songCount; i++) { - const nbsSong = this.generateRandomSong(); - const fileData = nbsSong.toArrayBuffer(); - const fileBuffer = Buffer.from(fileData); - - const body: UploadSongDto = { - file: { - buffer : fileData, - size : fileBuffer.length, - mimetype : 'application/octet-stream', - originalname: `${faker.music.songName()}.nbs` - }, - allowDownload: faker.datatype.boolean(), - visibility : faker.helpers.arrayElement( - visibilities - ) as VisibilityType, - title : faker.music.songName(), - originalAuthor : faker.music.artist(), - description : faker.lorem.paragraph(), - license : faker.helpers.arrayElement(licenses) as LicenseType, - category : faker.helpers.arrayElement(categories) as CategoryType, - customInstruments: [], - thumbnailData : { - backgroundColor: faker.internet.color(), - startLayer : faker.helpers.rangeToNumber({ min: 0, max: 4 }), - startTick : faker.helpers.rangeToNumber({ min: 0, max: 100 }), - zoomLevel : faker.helpers.rangeToNumber({ min: 1, max: 5 }) - } - }; - - const uploadSongResponse = await this.songService.uploadSong({ - user, - body, - file: body.file - }); - - const song = await this.songService.getSongById( - uploadSongResponse.publicId - ); - - if (!song) continue; - - //change song creation date - (song as any).createdAt = this.generateRandomDate( - new Date(2020, 0, 1), - new Date() - ); - - song.playCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); - song.downloadCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); - song.likeCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); - await song.save(); - - songs.push(song); - } + private async seedSongs(users: UserDocument[]) { + const songs: SongDocument[] = []; + const licenses = Object.keys(UPLOAD_CONSTANTS.licenses); + const categories = Object.keys(UPLOAD_CONSTANTS.categories); + const visibilities = Object.keys(UPLOAD_CONSTANTS.visibility); + + for (const user of users) { + // most users will have 0-5 songs and a few will have 5-10, not a real statist by whatever I just what to test the system in development mode + const songCount = this.generateExponentialRandom(5, 2, 0.5, 10); + + for (let i = 0; i < songCount; i++) { + const nbsSong = this.generateRandomSong(); + const fileData = nbsSong.toArrayBuffer(); + const fileBuffer = Buffer.from(fileData); + + const body: UploadSongDto = { + file: { + buffer : fileData, + size : fileBuffer.length, + mimetype : 'application/octet-stream', + originalname: `${faker.music.songName()}.nbs` + }, + allowDownload: faker.datatype.boolean(), + visibility : faker.helpers.arrayElement( + visibilities + ) as VisibilityType, + title : faker.music.songName(), + originalAuthor : faker.music.artist(), + description : faker.lorem.paragraph(), + license : faker.helpers.arrayElement(licenses) as LicenseType, + category : faker.helpers.arrayElement(categories) as CategoryType, + customInstruments: [], + thumbnailData : { + backgroundColor: faker.internet.color(), + startLayer : faker.helpers.rangeToNumber({ min: 0, max: 4 }), + startTick : faker.helpers.rangeToNumber({ min: 0, max: 100 }), + zoomLevel : faker.helpers.rangeToNumber({ min: 1, max: 5 }) + } + }; + + const uploadSongResponse = await this.songService.uploadSong({ + user, + body, + file: body.file + }); + + const song = await this.songService.getSongById( + uploadSongResponse.publicId + ); + + if (!song) continue; + + //change song creation date + (song as any).createdAt = this.generateRandomDate( + new Date(2020, 0, 1), + new Date() + ); + + song.playCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); + song.downloadCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); + song.likeCount = faker.helpers.rangeToNumber({ min: 0, max: 1000 }); + await song.save(); + + songs.push(song); + } + } + + return songs; } - return songs; - } + private generateExponentialRandom( + start = 1, + stepScale = 2, + stepProbability = 0.5, + limit = Number.MAX_SAFE_INTEGER + ) { + let max = start; - private generateExponentialRandom( - start = 1, - stepScale = 2, - stepProbability = 0.5, - limit = Number.MAX_SAFE_INTEGER - ) { - let max = start; + while (faker.datatype.boolean(stepProbability) && max < limit) { + max *= stepScale; + } - while (faker.datatype.boolean(stepProbability) && max < limit) { - max *= stepScale; + return faker.number.int({ min: 0, max: Math.min(max, limit) }); } - return faker.number.int({ min: 0, max: Math.min(max, limit) }); - } - - private generateRandomSong() { - const songTest = new Song(); - songTest.meta.author = faker.music.artist(); - songTest.meta.description = faker.lorem.sentence(); - songTest.meta.name = faker.music.songName(); - songTest.meta.originalAuthor = faker.music.artist(); - - songTest.tempo = faker.helpers.rangeToNumber({ min: 20 * 1, max: 20 * 4 }); - const layerCount = faker.helpers.rangeToNumber({ min: 1, max: 5 }); - - for (let layerIndex = 0; layerIndex < layerCount; layerIndex++) { - const instrument = Instrument.builtIn[layerCount]; - const layer = songTest.createLayer(); - layer.meta.name = instrument.meta.name; - - const notes = Array.from({ - length: faker.helpers.rangeToNumber({ min: 20, max: 120 }) - }).map( - () => - new Note(instrument, { - key : faker.helpers.rangeToNumber({ min: 0, max: 127 }), - velocity: faker.helpers.rangeToNumber({ min: 0, max: 127 }), - panning : faker.helpers.rangeToNumber({ min: -1, max: 1 }), - pitch : faker.helpers.rangeToNumber({ min: -1, max: 1 }) - }) - ); - - for (let i = 0; i < notes.length; i++) - songTest.setNote(i * 4, layer, notes[i]); - // "i * 4" is placeholder - this is the tick to place on + private generateRandomSong() { + const songTest = new Song(); + songTest.meta.author = faker.music.artist(); + songTest.meta.description = faker.lorem.sentence(); + songTest.meta.name = faker.music.songName(); + songTest.meta.originalAuthor = faker.music.artist(); + + songTest.tempo = faker.helpers.rangeToNumber({ min: 20 * 1, max: 20 * 4 }); + const layerCount = faker.helpers.rangeToNumber({ min: 1, max: 5 }); + + for (let layerIndex = 0; layerIndex < layerCount; layerIndex++) { + const instrument = Instrument.builtIn[layerCount]; + const layer = songTest.createLayer(); + layer.meta.name = instrument.meta.name; + + const notes = Array.from({ + length: faker.helpers.rangeToNumber({ min: 20, max: 120 }) + }).map( + () => + new Note(instrument, { + key : faker.helpers.rangeToNumber({ min: 0, max: 127 }), + velocity: faker.helpers.rangeToNumber({ min: 0, max: 127 }), + panning : faker.helpers.rangeToNumber({ min: -1, max: 1 }), + pitch : faker.helpers.rangeToNumber({ min: -1, max: 1 }) + }) + ); + + for (let i = 0; i < notes.length; i++) + songTest.setNote(i * 4, layer, notes[i]); + // "i * 4" is placeholder - this is the tick to place on + } + + return songTest; } - return songTest; - } - - private generateRandomDate(from: Date, to: Date): Date { - return new Date( - faker.date.between({ - from: from.getTime(), - to : to.getTime() - }) - ); - } + private generateRandomDate(from: Date, to: Date): Date { + return new Date( + faker.date.between({ + from: from.getTime(), + to : to.getTime() + }) + ); + } } diff --git a/apps/backend/src/song-browser/song-browser.controller.spec.ts b/apps/backend/src/song-browser/song-browser.controller.spec.ts index 3c2bf06c..6be13a0e 100644 --- a/apps/backend/src/song-browser/song-browser.controller.spec.ts +++ b/apps/backend/src/song-browser/song-browser.controller.spec.ts @@ -7,96 +7,96 @@ import { SongBrowserController } from './song-browser.controller'; import { SongBrowserService } from './song-browser.service'; const mockSongBrowserService = { - getFeaturedSongs : jest.fn(), - getRecentSongs : jest.fn(), - getCategories : jest.fn(), - getSongsByCategory: jest.fn() + getFeaturedSongs : jest.fn(), + getRecentSongs : jest.fn(), + getCategories : jest.fn(), + getSongsByCategory: jest.fn() }; describe('SongBrowserController', () => { - let controller: SongBrowserController; - let songBrowserService: SongBrowserService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SongBrowserController], - providers : [ - { - provide : SongBrowserService, - useValue: mockSongBrowserService - } - ] - }).compile(); - - controller = module.get(SongBrowserController); - songBrowserService = module.get(SongBrowserService); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getFeaturedSongs', () => { - it('should return a list of featured songs', async () => { - const featuredSongs: FeaturedSongsDto = {} as FeaturedSongsDto; - - mockSongBrowserService.getFeaturedSongs.mockResolvedValueOnce( - featuredSongs - ); - - const result = await controller.getFeaturedSongs(); - - expect(result).toEqual(featuredSongs); - expect(songBrowserService.getFeaturedSongs).toHaveBeenCalled(); + let controller: SongBrowserController; + let songBrowserService: SongBrowserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SongBrowserController], + providers : [ + { + provide : SongBrowserService, + useValue: mockSongBrowserService + } + ] + }).compile(); + + controller = module.get(SongBrowserController); + songBrowserService = module.get(SongBrowserService); }); - }); - describe('getSongList', () => { - it('should return a list of recent songs', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; - const songList: SongPreviewDto[] = []; + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getFeaturedSongs', () => { + it('should return a list of featured songs', async () => { + const featuredSongs: FeaturedSongsDto = {} as FeaturedSongsDto; + + mockSongBrowserService.getFeaturedSongs.mockResolvedValueOnce( + featuredSongs + ); + + const result = await controller.getFeaturedSongs(); + + expect(result).toEqual(featuredSongs); + expect(songBrowserService.getFeaturedSongs).toHaveBeenCalled(); + }); + }); + + describe('getSongList', () => { + it('should return a list of recent songs', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; + const songList: SongPreviewDto[] = []; - mockSongBrowserService.getRecentSongs.mockResolvedValueOnce(songList); + mockSongBrowserService.getRecentSongs.mockResolvedValueOnce(songList); - const result = await controller.getSongList(query); + const result = await controller.getSongList(query); - expect(result).toEqual(songList); - expect(songBrowserService.getRecentSongs).toHaveBeenCalledWith(query); + expect(result).toEqual(songList); + expect(songBrowserService.getRecentSongs).toHaveBeenCalledWith(query); + }); }); - }); - describe('getCategories', () => { - it('should return a list of song categories and song counts', async () => { - const categories: Record = { - category1: 10, - category2: 5 - }; + describe('getCategories', () => { + it('should return a list of song categories and song counts', async () => { + const categories: Record = { + category1: 10, + category2: 5 + }; - mockSongBrowserService.getCategories.mockResolvedValueOnce(categories); + mockSongBrowserService.getCategories.mockResolvedValueOnce(categories); - const result = await controller.getCategories(); + const result = await controller.getCategories(); - expect(result).toEqual(categories); - expect(songBrowserService.getCategories).toHaveBeenCalled(); + expect(result).toEqual(categories); + expect(songBrowserService.getCategories).toHaveBeenCalled(); + }); }); - }); - describe('getSongsByCategory', () => { - it('should return a list of songs by category', async () => { - const id = 'test-category'; - const query: PageQueryDTO = { page: 1, limit: 10 }; - const songList: SongPreviewDto[] = []; + describe('getSongsByCategory', () => { + it('should return a list of songs by category', async () => { + const id = 'test-category'; + const query: PageQueryDTO = { page: 1, limit: 10 }; + const songList: SongPreviewDto[] = []; - mockSongBrowserService.getSongsByCategory.mockResolvedValueOnce(songList); + mockSongBrowserService.getSongsByCategory.mockResolvedValueOnce(songList); - const result = await controller.getSongsByCategory(id, query); + const result = await controller.getSongsByCategory(id, query); - expect(result).toEqual(songList); + expect(result).toEqual(songList); - expect(songBrowserService.getSongsByCategory).toHaveBeenCalledWith( - id, - query - ); + expect(songBrowserService.getSongsByCategory).toHaveBeenCalledWith( + id, + query + ); + }); }); - }); }); diff --git a/apps/backend/src/song-browser/song-browser.controller.ts b/apps/backend/src/song-browser/song-browser.controller.ts index 1f2aaf8f..57020463 100644 --- a/apps/backend/src/song-browser/song-browser.controller.ts +++ b/apps/backend/src/song-browser/song-browser.controller.ts @@ -1,10 +1,10 @@ import { FeaturedSongsDto, PageQueryDTO, SongPreviewDto } from '@nbw/database'; import { - BadRequestException, - Controller, - Get, - Param, - Query + BadRequestException, + Controller, + Get, + Param, + Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; @@ -14,51 +14,51 @@ import { SongBrowserService } from './song-browser.service'; @ApiTags('song-browser') @ApiTags('song') export class SongBrowserController { - constructor(public readonly songBrowserService: SongBrowserService) {} + constructor(public readonly songBrowserService: SongBrowserService) {} - @Get('/featured') - @ApiOperation({ summary: 'Get a list of featured songs' }) - public async getFeaturedSongs(): Promise { - return await this.songBrowserService.getFeaturedSongs(); - } + @Get('/featured') + @ApiOperation({ summary: 'Get a list of featured songs' }) + public async getFeaturedSongs(): Promise { + return await this.songBrowserService.getFeaturedSongs(); + } + + @Get('/recent') + @ApiOperation({ + summary: 'Get a filtered/sorted list of recent songs with pagination' + }) + public async getSongList( + @Query() query: PageQueryDTO + ): Promise { + return await this.songBrowserService.getRecentSongs(query); + } - @Get('/recent') - @ApiOperation({ - summary: 'Get a filtered/sorted list of recent songs with pagination' - }) - public async getSongList( - @Query() query: PageQueryDTO - ): Promise { - return await this.songBrowserService.getRecentSongs(query); - } + @Get('/categories') + @ApiOperation({ summary: 'Get a list of song categories and song counts' }) + public async getCategories(): Promise> { + return await this.songBrowserService.getCategories(); + } - @Get('/categories') - @ApiOperation({ summary: 'Get a list of song categories and song counts' }) - public async getCategories(): Promise> { - return await this.songBrowserService.getCategories(); - } + @Get('/categories/:id') + @ApiOperation({ summary: 'Get a list of song categories and song counts' }) + public async getSongsByCategory( + @Param('id') id: string, + @Query() query: PageQueryDTO + ): Promise { + return await this.songBrowserService.getSongsByCategory(id, query); + } - @Get('/categories/:id') - @ApiOperation({ summary: 'Get a list of song categories and song counts' }) - public async getSongsByCategory( - @Param('id') id: string, - @Query() query: PageQueryDTO - ): Promise { - return await this.songBrowserService.getSongsByCategory(id, query); - } + @Get('/random') + @ApiOperation({ summary: 'Get a list of songs at random' }) + public async getRandomSongs( + @Query('count') count: string, + @Query('category') category: string + ): Promise { + const countInt = parseInt(count); - @Get('/random') - @ApiOperation({ summary: 'Get a list of songs at random' }) - public async getRandomSongs( - @Query('count') count: string, - @Query('category') category: string - ): Promise { - const countInt = parseInt(count); + if (isNaN(countInt) || countInt < 1 || countInt > 10) { + throw new BadRequestException('Invalid query parameters'); + } - if (isNaN(countInt) || countInt < 1 || countInt > 10) { - throw new BadRequestException('Invalid query parameters'); + return await this.songBrowserService.getRandomSongs(countInt, category); } - - return await this.songBrowserService.getRandomSongs(countInt, category); - } } diff --git a/apps/backend/src/song-browser/song-browser.module.ts b/apps/backend/src/song-browser/song-browser.module.ts index 22600971..fc5511e1 100644 --- a/apps/backend/src/song-browser/song-browser.module.ts +++ b/apps/backend/src/song-browser/song-browser.module.ts @@ -6,8 +6,8 @@ import { SongBrowserController } from './song-browser.controller'; import { SongBrowserService } from './song-browser.service'; @Module({ - providers : [SongBrowserService], - controllers: [SongBrowserController], - imports : [SongModule] + providers : [SongBrowserService], + controllers: [SongBrowserController], + imports : [SongModule] }) export class SongBrowserModule {} diff --git a/apps/backend/src/song-browser/song-browser.service.spec.ts b/apps/backend/src/song-browser/song-browser.service.spec.ts index fbb24898..4eb5e0f8 100644 --- a/apps/backend/src/song-browser/song-browser.service.spec.ts +++ b/apps/backend/src/song-browser/song-browser.service.spec.ts @@ -9,130 +9,130 @@ import { SongService } from '@server/song/song.service'; import { SongBrowserService } from './song-browser.service'; const mockSongService = { - getSongsForTimespan : jest.fn(), - getSongsBeforeTimespan: jest.fn(), - getRecentSongs : jest.fn(), - getCategories : jest.fn(), - getSongsByCategory : jest.fn() + getSongsForTimespan : jest.fn(), + getSongsBeforeTimespan: jest.fn(), + getRecentSongs : jest.fn(), + getCategories : jest.fn(), + getSongsByCategory : jest.fn() }; describe('SongBrowserService', () => { - let service: SongBrowserService; - let songService: SongService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SongBrowserService, - { - provide : SongService, - useValue: mockSongService - } - ] - }).compile(); - - service = module.get(SongBrowserService); - songService = module.get(SongService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('getFeaturedSongs', () => { - it('should return featured songs', async () => { - const songWithUser: SongWithUser = { - title : 'Test Song', - uploader: { username: 'testuser', profileImage: 'testimage' }, - stats : { - duration : 100, - noteCount: 100 - } - } as any; - - jest - .spyOn(songService, 'getSongsForTimespan') - .mockResolvedValue([songWithUser]); - - jest - .spyOn(songService, 'getSongsBeforeTimespan') - .mockResolvedValue([songWithUser]); - - await service.getFeaturedSongs(); - - expect(songService.getSongsForTimespan).toHaveBeenCalled(); - expect(songService.getSongsBeforeTimespan).toHaveBeenCalled(); + let service: SongBrowserService; + let songService: SongService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SongBrowserService, + { + provide : SongService, + useValue: mockSongService + } + ] + }).compile(); + + service = module.get(SongBrowserService); + songService = module.get(SongService); }); - }); - describe('getRecentSongs', () => { - it('should return recent songs', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; + it('should be defined', () => { + expect(service).toBeDefined(); + }); - const songPreviewDto: SongPreviewDto = { - title : 'Test Song', - uploader: { username: 'testuser', profileImage: 'testimage' } - } as any; + describe('getFeaturedSongs', () => { + it('should return featured songs', async () => { + const songWithUser: SongWithUser = { + title : 'Test Song', + uploader: { username: 'testuser', profileImage: 'testimage' }, + stats : { + duration : 100, + noteCount: 100 + } + } as any; + + jest + .spyOn(songService, 'getSongsForTimespan') + .mockResolvedValue([songWithUser]); + + jest + .spyOn(songService, 'getSongsBeforeTimespan') + .mockResolvedValue([songWithUser]); + + await service.getFeaturedSongs(); + + expect(songService.getSongsForTimespan).toHaveBeenCalled(); + expect(songService.getSongsBeforeTimespan).toHaveBeenCalled(); + }); + }); - jest - .spyOn(songService, 'getRecentSongs') - .mockResolvedValue([songPreviewDto]); + describe('getRecentSongs', () => { + it('should return recent songs', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; - const result = await service.getRecentSongs(query); + const songPreviewDto: SongPreviewDto = { + title : 'Test Song', + uploader: { username: 'testuser', profileImage: 'testimage' } + } as any; - expect(result).toEqual([songPreviewDto]); + jest + .spyOn(songService, 'getRecentSongs') + .mockResolvedValue([songPreviewDto]); - expect(songService.getRecentSongs).toHaveBeenCalledWith( - query.page, - query.limit - ); - }); + const result = await service.getRecentSongs(query); + + expect(result).toEqual([songPreviewDto]); + + expect(songService.getRecentSongs).toHaveBeenCalledWith( + query.page, + query.limit + ); + }); - it('should throw an error if query parameters are invalid', async () => { - const query: PageQueryDTO = { page: undefined, limit: undefined }; + it('should throw an error if query parameters are invalid', async () => { + const query: PageQueryDTO = { page: undefined, limit: undefined }; - await expect(service.getRecentSongs(query)).rejects.toThrow( - HttpException - ); + await expect(service.getRecentSongs(query)).rejects.toThrow( + HttpException + ); + }); }); - }); - describe('getCategories', () => { - it('should return categories', async () => { - const categories = { pop: 10, rock: 5 }; + describe('getCategories', () => { + it('should return categories', async () => { + const categories = { pop: 10, rock: 5 }; - jest.spyOn(songService, 'getCategories').mockResolvedValue(categories); + jest.spyOn(songService, 'getCategories').mockResolvedValue(categories); - const result = await service.getCategories(); + const result = await service.getCategories(); - expect(result).toEqual(categories); - expect(songService.getCategories).toHaveBeenCalled(); + expect(result).toEqual(categories); + expect(songService.getCategories).toHaveBeenCalled(); + }); }); - }); - describe('getSongsByCategory', () => { - it('should return songs by category', async () => { - const category = 'pop'; - const query: PageQueryDTO = { page: 1, limit: 10 }; + describe('getSongsByCategory', () => { + it('should return songs by category', async () => { + const category = 'pop'; + const query: PageQueryDTO = { page: 1, limit: 10 }; - const songPreviewDto: SongPreviewDto = { - title : 'Test Song', - uploader: { username: 'testuser', profileImage: 'testimage' } - } as any; + const songPreviewDto: SongPreviewDto = { + title : 'Test Song', + uploader: { username: 'testuser', profileImage: 'testimage' } + } as any; - jest - .spyOn(songService, 'getSongsByCategory') - .mockResolvedValue([songPreviewDto]); + jest + .spyOn(songService, 'getSongsByCategory') + .mockResolvedValue([songPreviewDto]); - const result = await service.getSongsByCategory(category, query); + const result = await service.getSongsByCategory(category, query); - expect(result).toEqual([songPreviewDto]); + expect(result).toEqual([songPreviewDto]); - expect(songService.getSongsByCategory).toHaveBeenCalledWith( - category, - query.page, - query.limit - ); + expect(songService.getSongsByCategory).toHaveBeenCalledWith( + category, + query.page, + query.limit + ); + }); }); - }); }); diff --git a/apps/backend/src/song-browser/song-browser.service.ts b/apps/backend/src/song-browser/song-browser.service.ts index 1da8635e..59bb218b 100644 --- a/apps/backend/src/song-browser/song-browser.service.ts +++ b/apps/backend/src/song-browser/song-browser.service.ts @@ -1,10 +1,10 @@ import { BROWSER_SONGS } from '@nbw/config'; import { - FeaturedSongsDto, - PageQueryDTO, - SongPreviewDto, - SongWithUser, - TimespanType + FeaturedSongsDto, + PageQueryDTO, + SongPreviewDto, + SongWithUser, + TimespanType } from '@nbw/database'; import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; @@ -12,115 +12,115 @@ import { SongService } from '@server/song/song.service'; @Injectable() export class SongBrowserService { - constructor( - @Inject(SongService) - private songService: SongService - ) {} - - public async getFeaturedSongs(): Promise { - const now = new Date(Date.now()); - - const times: Record = { - hour : new Date(Date.now()).setHours(now.getHours() - 1), - day : new Date(Date.now()).setDate(now.getDate() - 1), - week : new Date(Date.now()).setDate(now.getDate() - 7), - month: new Date(Date.now()).setMonth(now.getMonth() - 1), - year : new Date(Date.now()).setFullYear(now.getFullYear() - 1), - all : new Date(0).getTime() - }; - - const songs: Record = { - hour : [], - day : [], - week : [], - month: [], - year : [], - all : [] - }; - - for (const [timespan, time] of Object.entries(times)) { - const songPage = await this.songService.getSongsForTimespan(time); - - // If the length is 0, send an empty array (no songs available in that timespan) - // If the length is less than the page size, pad it with songs "borrowed" - // from the nearest timestamp, regardless of view count - if ( - songPage.length > 0 && + constructor( + @Inject(SongService) + private songService: SongService + ) {} + + public async getFeaturedSongs(): Promise { + const now = new Date(Date.now()); + + const times: Record = { + hour : new Date(Date.now()).setHours(now.getHours() - 1), + day : new Date(Date.now()).setDate(now.getDate() - 1), + week : new Date(Date.now()).setDate(now.getDate() - 7), + month: new Date(Date.now()).setMonth(now.getMonth() - 1), + year : new Date(Date.now()).setFullYear(now.getFullYear() - 1), + all : new Date(0).getTime() + }; + + const songs: Record = { + hour : [], + day : [], + week : [], + month: [], + year : [], + all : [] + }; + + for (const [timespan, time] of Object.entries(times)) { + const songPage = await this.songService.getSongsForTimespan(time); + + // If the length is 0, send an empty array (no songs available in that timespan) + // If the length is less than the page size, pad it with songs "borrowed" + // from the nearest timestamp, regardless of view count + if ( + songPage.length > 0 && songPage.length < BROWSER_SONGS.paddedFeaturedPageSize - ) { - const missing = BROWSER_SONGS.paddedFeaturedPageSize - songPage.length; + ) { + const missing = BROWSER_SONGS.paddedFeaturedPageSize - songPage.length; - const additionalSongs = await this.songService.getSongsBeforeTimespan( - time - ); + const additionalSongs = await this.songService.getSongsBeforeTimespan( + time + ); - songPage.push(...additionalSongs.slice(0, missing)); - } + songPage.push(...additionalSongs.slice(0, missing)); + } - songs[timespan as TimespanType] = songPage; - } + songs[timespan as TimespanType] = songPage; + } + + const featuredSongs = FeaturedSongsDto.create(); + + featuredSongs.hour = songs.hour.map((song) => + SongPreviewDto.fromSongDocumentWithUser(song) + ); + + featuredSongs.day = songs.day.map((song) => + SongPreviewDto.fromSongDocumentWithUser(song) + ); - const featuredSongs = FeaturedSongsDto.create(); + featuredSongs.week = songs.week.map((song) => + SongPreviewDto.fromSongDocumentWithUser(song) + ); - featuredSongs.hour = songs.hour.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song) - ); + featuredSongs.month = songs.month.map((song) => + SongPreviewDto.fromSongDocumentWithUser(song) + ); - featuredSongs.day = songs.day.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song) - ); + featuredSongs.year = songs.year.map((song) => + SongPreviewDto.fromSongDocumentWithUser(song) + ); - featuredSongs.week = songs.week.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song) - ); + featuredSongs.all = songs.all.map((song) => + SongPreviewDto.fromSongDocumentWithUser(song) + ); - featuredSongs.month = songs.month.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song) - ); + return featuredSongs; + } - featuredSongs.year = songs.year.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song) - ); + public async getRecentSongs(query: PageQueryDTO): Promise { + const { page, limit } = query; - featuredSongs.all = songs.all.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song) - ); + if (!page || !limit) { + throw new HttpException( + 'Invalid query parameters', + HttpStatus.BAD_REQUEST + ); + } - return featuredSongs; - } + return await this.songService.getRecentSongs(page, limit); + } - public async getRecentSongs(query: PageQueryDTO): Promise { - const { page, limit } = query; + public async getCategories(): Promise> { + return await this.songService.getCategories(); + } - if (!page || !limit) { - throw new HttpException( - 'Invalid query parameters', - HttpStatus.BAD_REQUEST - ); + public async getSongsByCategory( + category: string, + query: PageQueryDTO + ): Promise { + return await this.songService.getSongsByCategory( + category, + query.page ?? 1, + query.limit ?? 10 + ); } - return await this.songService.getRecentSongs(page, limit); - } - - public async getCategories(): Promise> { - return await this.songService.getCategories(); - } - - public async getSongsByCategory( - category: string, - query: PageQueryDTO - ): Promise { - return await this.songService.getSongsByCategory( - category, - query.page ?? 1, - query.limit ?? 10 - ); - } - - public async getRandomSongs( - count: number, - category: string - ): Promise { - return await this.songService.getRandomSongs(count, category); - } + public async getRandomSongs( + count: number, + category: string + ): Promise { + return await this.songService.getRandomSongs(count, category); + } } diff --git a/apps/backend/src/song/my-songs/my-songs.controller.spec.ts b/apps/backend/src/song/my-songs/my-songs.controller.spec.ts index 43f3af29..6e0ed065 100644 --- a/apps/backend/src/song/my-songs/my-songs.controller.spec.ts +++ b/apps/backend/src/song/my-songs/my-songs.controller.spec.ts @@ -11,74 +11,74 @@ import { SongService } from '../song.service'; import { MySongsController } from './my-songs.controller'; const mockSongService = { - getMySongsPage: jest.fn() + getMySongsPage: jest.fn() }; describe('MySongsController', () => { - let controller: MySongsController; - let songService: SongService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [MySongsController], - providers : [ - { - provide : SongService, - useValue: mockSongService - } - ] - }) - .overrideGuard(AuthGuard('jwt-refresh')) - .useValue({ canActivate: jest.fn(() => true) }) - .compile(); - - controller = module.get(MySongsController); - songService = module.get(SongService); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getMySongsPage', () => { - it('should return a list of songs uploaded by the current authenticated user', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const songPageDto: SongPageDto = { - content: [], - page : 0, - limit : 0, - total : 0 - }; - - mockSongService.getMySongsPage.mockResolvedValueOnce(songPageDto); - - const result = await controller.getMySongsPage(query, user); - - expect(result).toEqual(songPageDto); - expect(songService.getMySongsPage).toHaveBeenCalledWith({ query, user }); + let controller: MySongsController; + let songService: SongService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MySongsController], + providers : [ + { + provide : SongService, + useValue: mockSongService + } + ] + }) + .overrideGuard(AuthGuard('jwt-refresh')) + .useValue({ canActivate: jest.fn(() => true) }) + .compile(); + + controller = module.get(MySongsController); + songService = module.get(SongService); }); - it('should handle thrown an exception if userDocument is null', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; - const user = null; - - await expect(controller.getMySongsPage(query, user)).rejects.toThrow( - HttpException - ); + it('should be defined', () => { + expect(controller).toBeDefined(); }); - it('should handle exceptions', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const error = new Error('Test error'); + describe('getMySongsPage', () => { + it('should return a list of songs uploaded by the current authenticated user', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const songPageDto: SongPageDto = { + content: [], + page : 0, + limit : 0, + total : 0 + }; + + mockSongService.getMySongsPage.mockResolvedValueOnce(songPageDto); + + const result = await controller.getMySongsPage(query, user); + + expect(result).toEqual(songPageDto); + expect(songService.getMySongsPage).toHaveBeenCalledWith({ query, user }); + }); + + it('should handle thrown an exception if userDocument is null', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; + const user = null; + + await expect(controller.getMySongsPage(query, user)).rejects.toThrow( + HttpException + ); + }); + + it('should handle exceptions', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const error = new Error('Test error'); - mockSongService.getMySongsPage.mockRejectedValueOnce(error); + mockSongService.getMySongsPage.mockRejectedValueOnce(error); - await expect(controller.getMySongsPage(query, user)).rejects.toThrow( - 'Test error' - ); + await expect(controller.getMySongsPage(query, user)).rejects.toThrow( + 'Test error' + ); + }); }); - }); }); diff --git a/apps/backend/src/song/my-songs/my-songs.controller.ts b/apps/backend/src/song/my-songs/my-songs.controller.ts index 1f263b84..3507ca0a 100644 --- a/apps/backend/src/song/my-songs/my-songs.controller.ts +++ b/apps/backend/src/song/my-songs/my-songs.controller.ts @@ -12,22 +12,22 @@ import { SongService } from '../song.service'; @Controller('my-songs') @ApiTags('song') export class MySongsController { - constructor(public readonly songService: SongService) {} + constructor(public readonly songService: SongService) {} - @Get('/') - @ApiOperation({ - summary: 'Get a list of songs uploaded by the current authenticated user' - }) - @ApiBearerAuth() - @UseGuards(AuthGuard('jwt-refresh')) - public async getMySongsPage( - @Query() query: PageQueryDTO, - @GetRequestToken() user: UserDocument | null - ): Promise { - user = validateUser(user); - return await this.songService.getMySongsPage({ - query, - user - }); - } + @Get('/') + @ApiOperation({ + summary: 'Get a list of songs uploaded by the current authenticated user' + }) + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt-refresh')) + public async getMySongsPage( + @Query() query: PageQueryDTO, + @GetRequestToken() user: UserDocument | null + ): Promise { + user = validateUser(user); + return await this.songService.getMySongsPage({ + query, + user + }); + } } diff --git a/apps/backend/src/song/song-upload/song-upload.service.spec.ts b/apps/backend/src/song/song-upload/song-upload.service.spec.ts index 56758886..43b525ca 100644 --- a/apps/backend/src/song/song-upload/song-upload.service.spec.ts +++ b/apps/backend/src/song/song-upload/song-upload.service.spec.ts @@ -3,10 +3,10 @@ import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test'; import { Instrument, Layer, Note, Song } from '@encode42/nbs.js'; import type { UserDocument } from '@nbw/database'; import { - SongDocument, - Song as SongEntity, - ThumbnailData, - UploadSongDto + SongDocument, + Song as SongEntity, + ThumbnailData, + UploadSongDto } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -19,408 +19,408 @@ import { SongUploadService } from './song-upload.service'; // mock drawToImage function mock.module('@nbw/thumbnail', () => ({ - drawToImage: jest.fn().mockResolvedValue(Buffer.from('test-image-buffer')) + drawToImage: jest.fn().mockResolvedValue(Buffer.from('test-image-buffer')) })); const mockFileService = { - uploadSong : jest.fn(), - uploadPackedSong: jest.fn(), - uploadThumbnail : jest.fn(), - getSongFile : jest.fn() + uploadSong : jest.fn(), + uploadPackedSong: jest.fn(), + uploadThumbnail : jest.fn(), + getSongFile : jest.fn() }; const mockUserService = { - findByID: jest.fn() + findByID: jest.fn() }; describe('SongUploadService', () => { - let songUploadService: SongUploadService; - let fileService: FileService; - let _userService: UserService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SongUploadService, - { - provide : FileService, - useValue: mockFileService - }, - { - provide : UserService, - useValue: mockUserService - } - ] - }).compile(); - - songUploadService = module.get(SongUploadService); - fileService = module.get(FileService); - _userService = module.get(UserService); - }); - - it('should be defined', () => { - expect(songUploadService).toBeDefined(); - }); - - describe('processUploadedSong', () => { - it('should process and upload a song', async () => { - const file = { buffer: Buffer.from('test') } as Express.Multer.File; - - const user: UserDocument = { - _id : new Types.ObjectId(), - username: 'testuser' - } as UserDocument; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: true, - file : 'somebytes' - }; - - const songEntity = new SongEntity(); - songEntity.uploader = user._id; - - spyOn(songUploadService as any, 'checkIsFileValid').mockImplementation( - (_file: Express.Multer.File) => undefined - ); - - spyOn(songUploadService as any, 'prepareSongForUpload').mockReturnValue({ - nbsSong : new Song(), - songBuffer: Buffer.from('test') - }); - - spyOn( - songUploadService as any, - 'preparePackedSongForUpload' - ).mockResolvedValue(Buffer.from('test')); - - spyOn(songUploadService as any, 'generateSongDocument').mockResolvedValue( - songEntity - ); - - spyOn(songUploadService, 'generateAndUploadThumbnail').mockResolvedValue( - 'http://test.com/thumbnail.png' - ); - - spyOn(songUploadService as any, 'uploadSongFile').mockResolvedValue( - 'http://test.com/file.nbs' - ); - - spyOn(songUploadService as any, 'uploadPackedSongFile').mockResolvedValue( - 'http://test.com/packed-file.nbs' - ); - - const result = await songUploadService.processUploadedSong({ - file, - user, - body - }); - - expect(result).toEqual(songEntity); - - expect((songUploadService as any).checkIsFileValid).toHaveBeenCalledWith( - file - ); - - expect( - (songUploadService as any).prepareSongForUpload - ).toHaveBeenCalledWith(file.buffer, body, user); - - expect( - (songUploadService as any).preparePackedSongForUpload - ).toHaveBeenCalledWith(expect.any(Song), body.customInstruments); - - expect(songUploadService.generateAndUploadThumbnail).toHaveBeenCalledWith( - body.thumbnailData, - expect.any(Song), - expect.any(String) - ); - - expect((songUploadService as any).uploadSongFile).toHaveBeenCalledWith( - expect.any(Buffer), - expect.any(String) - ); - - expect( - (songUploadService as any).uploadPackedSongFile - ).toHaveBeenCalledWith(expect.any(Buffer), expect.any(String)); - }); - }); - - describe('processSongPatch', () => { - it('should process and patch a song', async () => { - const user: UserDocument = { - _id : new Types.ObjectId(), - username: 'testuser' - } as UserDocument; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: true, - file : 'somebytes' - }; - - const songDocument: SongDocument = { - ...body, - publicId : 'test-id', - uploader : user._id, - customInstruments: [], - thumbnailData : body.thumbnailData, - nbsFileUrl : 'http://test.com/file.nbs', - save : jest.fn().mockResolvedValue({}) - } as any; - - spyOn(fileService, 'getSongFile').mockResolvedValue(new ArrayBuffer(0)); - - spyOn(songUploadService as any, 'prepareSongForUpload').mockReturnValue({ - nbsSong : new Song(), - songBuffer: Buffer.from('test') - }); - - spyOn( - songUploadService as any, - 'preparePackedSongForUpload' - ).mockResolvedValue(Buffer.from('test')); - - spyOn(songUploadService, 'generateAndUploadThumbnail').mockResolvedValue( - 'http://test.com/thumbnail.png' - ); - - spyOn(songUploadService as any, 'uploadSongFile').mockResolvedValue( - 'http://test.com/file.nbs' - ); - - spyOn(songUploadService as any, 'uploadPackedSongFile').mockResolvedValue( - 'http://test.com/packed-file.nbs' - ); - - await songUploadService.processSongPatch(songDocument, body, user); - }); - }); - - describe('generateAndUploadThumbnail', () => { - it('should generate and upload a thumbnail', async () => { - const thumbnailData: ThumbnailData = { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }; - - const nbsSong = new Song(); - nbsSong.addLayer(new Layer(1)); - nbsSong.addLayer(new Layer(2)); - const publicId = 'test-id'; - - spyOn(fileService, 'uploadThumbnail').mockResolvedValue( - 'http://test.com/thumbnail.png' - ); - - const result = await songUploadService.generateAndUploadThumbnail( - thumbnailData, - nbsSong, - publicId - ); - - expect(result).toBe('http://test.com/thumbnail.png'); - - expect(fileService.uploadThumbnail).toHaveBeenCalledWith( - expect.any(Buffer), - publicId - ); + let songUploadService: SongUploadService; + let fileService: FileService; + let _userService: UserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SongUploadService, + { + provide : FileService, + useValue: mockFileService + }, + { + provide : UserService, + useValue: mockUserService + } + ] + }).compile(); + + songUploadService = module.get(SongUploadService); + fileService = module.get(FileService); + _userService = module.get(UserService); }); - it('should throw an error if the thumbnail is invalid', async () => { - const thumbnailData: ThumbnailData = { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }; - - const nbsSong = new Song(); - const publicId = 'test-id'; - - spyOn(fileService, 'uploadThumbnail') - // throw an error - .mockRejectedValue(new Error('test error')); - - try { - await songUploadService.generateAndUploadThumbnail( - thumbnailData, - nbsSong, - publicId - ); - } catch (error) { - expect(error).toBeInstanceOf(HttpException); - } + it('should be defined', () => { + expect(songUploadService).toBeDefined(); }); - }); - - describe('uploadSongFile', () => { - it('should upload a song file', async () => { - const file = Buffer.from('test'); - const publicId = 'test-id'; - - spyOn(fileService, 'uploadSong').mockResolvedValue( - 'http://test.com/file.nbs' - ); - const result = await (songUploadService as any).uploadSongFile( - file, - publicId - ); - - expect(result).toBe('http://test.com/file.nbs'); - expect(fileService.uploadSong).toHaveBeenCalledWith(file, publicId); + describe('processUploadedSong', () => { + it('should process and upload a song', async () => { + const file = { buffer: Buffer.from('test') } as Express.Multer.File; + + const user: UserDocument = { + _id : new Types.ObjectId(), + username: 'testuser' + } as UserDocument; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: true, + file : 'somebytes' + }; + + const songEntity = new SongEntity(); + songEntity.uploader = user._id; + + spyOn(songUploadService as any, 'checkIsFileValid').mockImplementation( + (_file: Express.Multer.File) => undefined + ); + + spyOn(songUploadService as any, 'prepareSongForUpload').mockReturnValue({ + nbsSong : new Song(), + songBuffer: Buffer.from('test') + }); + + spyOn( + songUploadService as any, + 'preparePackedSongForUpload' + ).mockResolvedValue(Buffer.from('test')); + + spyOn(songUploadService as any, 'generateSongDocument').mockResolvedValue( + songEntity + ); + + spyOn(songUploadService, 'generateAndUploadThumbnail').mockResolvedValue( + 'http://test.com/thumbnail.png' + ); + + spyOn(songUploadService as any, 'uploadSongFile').mockResolvedValue( + 'http://test.com/file.nbs' + ); + + spyOn(songUploadService as any, 'uploadPackedSongFile').mockResolvedValue( + 'http://test.com/packed-file.nbs' + ); + + const result = await songUploadService.processUploadedSong({ + file, + user, + body + }); + + expect(result).toEqual(songEntity); + + expect((songUploadService as any).checkIsFileValid).toHaveBeenCalledWith( + file + ); + + expect( + (songUploadService as any).prepareSongForUpload + ).toHaveBeenCalledWith(file.buffer, body, user); + + expect( + (songUploadService as any).preparePackedSongForUpload + ).toHaveBeenCalledWith(expect.any(Song), body.customInstruments); + + expect(songUploadService.generateAndUploadThumbnail).toHaveBeenCalledWith( + body.thumbnailData, + expect.any(Song), + expect.any(String) + ); + + expect((songUploadService as any).uploadSongFile).toHaveBeenCalledWith( + expect.any(Buffer), + expect.any(String) + ); + + expect( + (songUploadService as any).uploadPackedSongFile + ).toHaveBeenCalledWith(expect.any(Buffer), expect.any(String)); + }); }); - it('should throw an error if the file is invalid', async () => { - const file = Buffer.from('test'); - const publicId = 'test-id'; - - spyOn(fileService, 'uploadSong').mockRejectedValue( - new Error('test error') - ); - - try { - await (songUploadService as any).uploadSongFile(file, publicId); - } catch (error) { - expect(error).toBeInstanceOf(HttpException); - } + describe('processSongPatch', () => { + it('should process and patch a song', async () => { + const user: UserDocument = { + _id : new Types.ObjectId(), + username: 'testuser' + } as UserDocument; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: true, + file : 'somebytes' + }; + + const songDocument: SongDocument = { + ...body, + publicId : 'test-id', + uploader : user._id, + customInstruments: [], + thumbnailData : body.thumbnailData, + nbsFileUrl : 'http://test.com/file.nbs', + save : jest.fn().mockResolvedValue({}) + } as any; + + spyOn(fileService, 'getSongFile').mockResolvedValue(new ArrayBuffer(0)); + + spyOn(songUploadService as any, 'prepareSongForUpload').mockReturnValue({ + nbsSong : new Song(), + songBuffer: Buffer.from('test') + }); + + spyOn( + songUploadService as any, + 'preparePackedSongForUpload' + ).mockResolvedValue(Buffer.from('test')); + + spyOn(songUploadService, 'generateAndUploadThumbnail').mockResolvedValue( + 'http://test.com/thumbnail.png' + ); + + spyOn(songUploadService as any, 'uploadSongFile').mockResolvedValue( + 'http://test.com/file.nbs' + ); + + spyOn(songUploadService as any, 'uploadPackedSongFile').mockResolvedValue( + 'http://test.com/packed-file.nbs' + ); + + await songUploadService.processSongPatch(songDocument, body, user); + }); }); - }); - - describe('uploadPackedSongFile', () => { - it('should upload a packed song file', async () => { - const file = Buffer.from('test'); - const publicId = 'test-id'; - spyOn(fileService, 'uploadPackedSong').mockResolvedValue( - 'http://test.com/packed-file.nbs' - ); - - const result = await (songUploadService as any).uploadPackedSongFile( - file, - publicId - ); - - expect(result).toBe('http://test.com/packed-file.nbs'); - expect(fileService.uploadPackedSong).toHaveBeenCalledWith(file, publicId); + describe('generateAndUploadThumbnail', () => { + it('should generate and upload a thumbnail', async () => { + const thumbnailData: ThumbnailData = { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }; + + const nbsSong = new Song(); + nbsSong.addLayer(new Layer(1)); + nbsSong.addLayer(new Layer(2)); + const publicId = 'test-id'; + + spyOn(fileService, 'uploadThumbnail').mockResolvedValue( + 'http://test.com/thumbnail.png' + ); + + const result = await songUploadService.generateAndUploadThumbnail( + thumbnailData, + nbsSong, + publicId + ); + + expect(result).toBe('http://test.com/thumbnail.png'); + + expect(fileService.uploadThumbnail).toHaveBeenCalledWith( + expect.any(Buffer), + publicId + ); + }); + + it('should throw an error if the thumbnail is invalid', async () => { + const thumbnailData: ThumbnailData = { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }; + + const nbsSong = new Song(); + const publicId = 'test-id'; + + spyOn(fileService, 'uploadThumbnail') + // throw an error + .mockRejectedValue(new Error('test error')); + + try { + await songUploadService.generateAndUploadThumbnail( + thumbnailData, + nbsSong, + publicId + ); + } catch (error) { + expect(error).toBeInstanceOf(HttpException); + } + }); }); - it('should throw an error if the file is invalid', async () => { - const file = Buffer.from('test'); - const publicId = 'test-id'; - - spyOn(fileService, 'uploadPackedSong').mockRejectedValue( - new Error('test error') - ); + describe('uploadSongFile', () => { + it('should upload a song file', async () => { + const file = Buffer.from('test'); + const publicId = 'test-id'; + + spyOn(fileService, 'uploadSong').mockResolvedValue( + 'http://test.com/file.nbs' + ); + + const result = await (songUploadService as any).uploadSongFile( + file, + publicId + ); + + expect(result).toBe('http://test.com/file.nbs'); + expect(fileService.uploadSong).toHaveBeenCalledWith(file, publicId); + }); + + it('should throw an error if the file is invalid', async () => { + const file = Buffer.from('test'); + const publicId = 'test-id'; + + spyOn(fileService, 'uploadSong').mockRejectedValue( + new Error('test error') + ); + + try { + await (songUploadService as any).uploadSongFile(file, publicId); + } catch (error) { + expect(error).toBeInstanceOf(HttpException); + } + }); + }); - try { - await (songUploadService as any).uploadPackedSongFile(file, publicId); - } catch (error) { - expect(error).toBeInstanceOf(HttpException); - } + describe('uploadPackedSongFile', () => { + it('should upload a packed song file', async () => { + const file = Buffer.from('test'); + const publicId = 'test-id'; + + spyOn(fileService, 'uploadPackedSong').mockResolvedValue( + 'http://test.com/packed-file.nbs' + ); + + const result = await (songUploadService as any).uploadPackedSongFile( + file, + publicId + ); + + expect(result).toBe('http://test.com/packed-file.nbs'); + expect(fileService.uploadPackedSong).toHaveBeenCalledWith(file, publicId); + }); + + it('should throw an error if the file is invalid', async () => { + const file = Buffer.from('test'); + const publicId = 'test-id'; + + spyOn(fileService, 'uploadPackedSong').mockRejectedValue( + new Error('test error') + ); + + try { + await (songUploadService as any).uploadPackedSongFile(file, publicId); + } catch (error) { + expect(error).toBeInstanceOf(HttpException); + } + }); }); - }); - describe('getSongObject', () => { - it('should return a song object from an array buffer', () => { - const songTest = new Song(); + describe('getSongObject', () => { + it('should return a song object from an array buffer', () => { + const songTest = new Song(); - songTest.meta = { - author : 'Nicolas Vycas', - description : 'super cool song', - importName : 'test', - name : 'Cool Test Song', - originalAuthor: 'Nicolas Vycas' - }; + songTest.meta = { + author : 'Nicolas Vycas', + description : 'super cool song', + importName : 'test', + name : 'Cool Test Song', + originalAuthor: 'Nicolas Vycas' + }; - songTest.tempo = 20; + songTest.tempo = 20; - // The following will add 3 layers for 3 instruments, each containing five notes - for (let layerCount = 0; layerCount < 3; layerCount++) { - const instrument = Instrument.builtIn[layerCount]; + // The following will add 3 layers for 3 instruments, each containing five notes + for (let layerCount = 0; layerCount < 3; layerCount++) { + const instrument = Instrument.builtIn[layerCount]; - // Create a layer for the instrument - const layer = songTest.createLayer(); - layer.meta.name = instrument.meta.name; + // Create a layer for the instrument + const layer = songTest.createLayer(); + layer.meta.name = instrument.meta.name; - const notes = [ - new Note(instrument, { key: 40 }), - new Note(instrument, { key: 45 }), - new Note(instrument, { key: 50 }), - new Note(instrument, { key: 45 }), - new Note(instrument, { key: 57 }) - ]; + const notes = [ + new Note(instrument, { key: 40 }), + new Note(instrument, { key: 45 }), + new Note(instrument, { key: 50 }), + new Note(instrument, { key: 45 }), + new Note(instrument, { key: 57 }) + ]; - // Place the notes - for (let i = 0; i < notes.length; i++) { - songTest.setNote(i * 4, layer, notes[i]); // "i * 4" is placeholder - this is the tick to place on - } - } + // Place the notes + for (let i = 0; i < notes.length; i++) { + songTest.setNote(i * 4, layer, notes[i]); // "i * 4" is placeholder - this is the tick to place on + } + } - const buffer = songTest.toArrayBuffer(); + const buffer = songTest.toArrayBuffer(); - const song = songUploadService.getSongObject(buffer); //TODO: For some reason the song is always empty + const song = songUploadService.getSongObject(buffer); //TODO: For some reason the song is always empty - expect(song).toBeInstanceOf(Song); - }); + expect(song).toBeInstanceOf(Song); + }); - it('should throw an error if the array buffer is invalid', () => { - const buffer = new ArrayBuffer(0); + it('should throw an error if the array buffer is invalid', () => { + const buffer = new ArrayBuffer(0); - expect(() => songUploadService.getSongObject(buffer)).toThrow( - HttpException - ); + expect(() => songUploadService.getSongObject(buffer)).toThrow( + HttpException + ); + }); }); - }); - describe('checkIsFileValid', () => { - it('should throw an error if the file is not provided', () => { - expect(() => (songUploadService as any).checkIsFileValid(null)).toThrow( - HttpException - ); - }); + describe('checkIsFileValid', () => { + it('should throw an error if the file is not provided', () => { + expect(() => (songUploadService as any).checkIsFileValid(null)).toThrow( + HttpException + ); + }); - it('should not throw an error if the file is provided', () => { - const file = { buffer: Buffer.from('test') } as Express.Multer.File; + it('should not throw an error if the file is provided', () => { + const file = { buffer: Buffer.from('test') } as Express.Multer.File; - expect(() => - (songUploadService as any).checkIsFileValid(file) - ).not.toThrow(); + expect(() => + (songUploadService as any).checkIsFileValid(file) + ).not.toThrow(); + }); }); - }); - - describe('getSoundsMapping', () => undefined); - describe('getValidSoundsSubset', () => undefined); - describe('validateUploader', () => undefined); - describe('generateSongDocument', () => undefined); - describe('prepareSongForUpload', () => undefined); - describe('preparePackedSongForUpload', () => undefined); + + describe('getSoundsMapping', () => undefined); + describe('getValidSoundsSubset', () => undefined); + describe('validateUploader', () => undefined); + describe('generateSongDocument', () => undefined); + describe('prepareSongForUpload', () => undefined); + describe('preparePackedSongForUpload', () => undefined); }); diff --git a/apps/backend/src/song/song-upload/song-upload.service.ts b/apps/backend/src/song/song-upload/song-upload.service.ts index 4d354f0b..61869aad 100644 --- a/apps/backend/src/song/song-upload/song-upload.service.ts +++ b/apps/backend/src/song/song-upload/song-upload.service.ts @@ -1,25 +1,25 @@ import { Song, fromArrayBuffer, toArrayBuffer } from '@encode42/nbs.js'; import { - SongDocument, - Song as SongEntity, - SongStats, - ThumbnailData, - UploadSongDto, - UserDocument + SongDocument, + Song as SongEntity, + SongStats, + ThumbnailData, + UploadSongDto, + UserDocument } from '@nbw/database'; import { - NoteQuadTree, - SongStatsGenerator, - injectSongFileMetadata, - obfuscateAndPackSong + NoteQuadTree, + SongStatsGenerator, + injectSongFileMetadata, + obfuscateAndPackSong } from '@nbw/song'; import { drawToImage } from '@nbw/thumbnail'; import { - HttpException, - HttpStatus, - Inject, - Injectable, - Logger + HttpException, + HttpStatus, + Inject, + Injectable, + Logger } from '@nestjs/common'; import { Types } from 'mongoose'; @@ -30,443 +30,443 @@ import { generateSongId, removeExtraSpaces } from '../song.util'; @Injectable() export class SongUploadService { - private soundsMapping: Record; - private soundsSubset : Set; + private soundsMapping: Record; + private soundsSubset : Set; - // TODO: move all upload auxiliary methods to new UploadSongService - private readonly logger = new Logger(SongUploadService.name); + // TODO: move all upload auxiliary methods to new UploadSongService + private readonly logger = new Logger(SongUploadService.name); - constructor( - @Inject(FileService) - private fileService: FileService, + constructor( + @Inject(FileService) + private fileService: FileService, - @Inject(UserService) - private userService: UserService - ) {} + @Inject(UserService) + private userService: UserService + ) {} - private async getSoundsMapping(): Promise> { + private async getSoundsMapping(): Promise> { // Object that maps sound paths to their respective hashes - if (!this.soundsMapping) { - const response = await fetch( - process.env.SERVER_URL + '/api/v1/data/soundList.json' - ); + if (!this.soundsMapping) { + const response = await fetch( + process.env.SERVER_URL + '/api/v1/data/soundList.json' + ); - this.soundsMapping = (await response.json()) as Record; - } + this.soundsMapping = (await response.json()) as Record; + } - return this.soundsMapping; - } + return this.soundsMapping; + } - private async getValidSoundsSubset() { + private async getValidSoundsSubset() { // Creates a set of valid sound paths from the full list of sounds in Minecraft - if (!this.soundsSubset) { - try { - const response = await fetch( - process.env.SERVER_URL + '/api/v1/data/soundList.json' - ); - - const soundMapping = (await response.json()) as Record; - const soundList = Object.keys(soundMapping); - this.soundsSubset = new Set(soundList); - } catch (e) { - throw new HttpException( - { - error: { - file: 'An error occurred while retrieving sound list' + if (!this.soundsSubset) { + try { + const response = await fetch( + process.env.SERVER_URL + '/api/v1/data/soundList.json' + ); + + const soundMapping = (await response.json()) as Record; + const soundList = Object.keys(soundMapping); + this.soundsSubset = new Set(soundList); + } catch (e) { + throw new HttpException( + { + error: { + file: 'An error occurred while retrieving sound list' + } + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); } - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } + } + + return this.soundsSubset; } - return this.soundsSubset; - } + private async validateUploader(user: UserDocument): Promise { + const uploader = await this.userService.findByID(user._id.toString()); - private async validateUploader(user: UserDocument): Promise { - const uploader = await this.userService.findByID(user._id.toString()); + if (!uploader) { + throw new HttpException( + 'user not found, contact an administrator', + HttpStatus.UNAUTHORIZED + ); + } - if (!uploader) { - throw new HttpException( - 'user not found, contact an administrator', - HttpStatus.UNAUTHORIZED - ); + return uploader._id; } - return uploader._id; - } - - private async generateSongDocument( - user: UserDocument, - publicId: string, - body: UploadSongDto, - thumbUrl: string, - fileKey: string, - packedFileKey: string, - songStats: SongStats, - file: Express.Multer.File - ): Promise { - const song = new SongEntity(); - song.uploader = await this.validateUploader(user); - song.publicId = publicId; - song.title = removeExtraSpaces(body.title); - song.originalAuthor = removeExtraSpaces(body.originalAuthor); - song.description = removeExtraSpaces(body.description); - song.category = body.category; - song.allowDownload = true;// || body.allowDownload; //TODO: implement allowDownload; - song.visibility = body.visibility; - song.license = body.license; - - // Pad custom instruments to number of instruments in the song, just for safety - const customInstrumentCount = - songStats.instrumentNoteCounts.length - + private async generateSongDocument( + user: UserDocument, + publicId: string, + body: UploadSongDto, + thumbUrl: string, + fileKey: string, + packedFileKey: string, + songStats: SongStats, + file: Express.Multer.File + ): Promise { + const song = new SongEntity(); + song.uploader = await this.validateUploader(user); + song.publicId = publicId; + song.title = removeExtraSpaces(body.title); + song.originalAuthor = removeExtraSpaces(body.originalAuthor); + song.description = removeExtraSpaces(body.description); + song.category = body.category; + song.allowDownload = true;// || body.allowDownload; //TODO: implement allowDownload; + song.visibility = body.visibility; + song.license = body.license; + + // Pad custom instruments to number of instruments in the song, just for safety + const customInstrumentCount = + songStats.instrumentNoteCounts.length - songStats.firstCustomInstrumentIndex; - const paddedInstruments = body.customInstruments.concat( - Array(customInstrumentCount - body.customInstruments.length).fill('') - ); - - song.customInstruments = paddedInstruments; - song.thumbnailData = body.thumbnailData; - song.thumbnailUrl = thumbUrl; - song.nbsFileUrl = fileKey; // s3File.Location; - song.packedSongUrl = packedFileKey; - song.stats = songStats; - song.fileSize = file.size; - - return song; - } - - public async processUploadedSong({ - file, - user, - body - }: { - body: UploadSongDto; - file: Express.Multer.File; - user: UserDocument; - }): Promise { + const paddedInstruments = body.customInstruments.concat( + Array(customInstrumentCount - body.customInstruments.length).fill('') + ); + + song.customInstruments = paddedInstruments; + song.thumbnailData = body.thumbnailData; + song.thumbnailUrl = thumbUrl; + song.nbsFileUrl = fileKey; // s3File.Location; + song.packedSongUrl = packedFileKey; + song.stats = songStats; + song.fileSize = file.size; + + return song; + } + + public async processUploadedSong({ + file, + user, + body + }: { + body: UploadSongDto; + file: Express.Multer.File; + user: UserDocument; + }): Promise { // Is file valid? - this.checkIsFileValid(file); - - // Prepare song for upload - const { nbsSong, songBuffer } = this.prepareSongForUpload( - file.buffer, - body, - user - ); - - // Prepare packed song for upload - // This can generate a client error if the custom instruments are invalid, so it's done before the song is uploaded - const packedSongBuffer = await this.preparePackedSongForUpload( - nbsSong, - body.customInstruments - ); - - // Generate song public ID - const publicId = generateSongId(); - - // Upload song file - const fileKey = await this.uploadSongFile(songBuffer, publicId); - - // Upload packed song file - const packedFileKey = await this.uploadPackedSongFile( - packedSongBuffer, - publicId - ); - - // PROCESS UPLOADED SONG - // TODO: delete file from S3 if remainder of upload method fails - - // Calculate song document's data from NBS file - const songStats = SongStatsGenerator.getSongStats(nbsSong); - - // Generate thumbnail - const thumbUrl: string = await this.generateAndUploadThumbnail( - body.thumbnailData, - nbsSong, - publicId - ); - - // Create song document - const song = await this.generateSongDocument( - user, - publicId, - body, - thumbUrl, - fileKey, - packedFileKey, // TODO: should be packedFileUrl - songStats, - file - ); - - return song; - } - - public async processSongPatch( - songDocument: SongDocument, - body: UploadSongDto, - user: UserDocument - ): Promise { + this.checkIsFileValid(file); + + // Prepare song for upload + const { nbsSong, songBuffer } = this.prepareSongForUpload( + file.buffer, + body, + user + ); + + // Prepare packed song for upload + // This can generate a client error if the custom instruments are invalid, so it's done before the song is uploaded + const packedSongBuffer = await this.preparePackedSongForUpload( + nbsSong, + body.customInstruments + ); + + // Generate song public ID + const publicId = generateSongId(); + + // Upload song file + const fileKey = await this.uploadSongFile(songBuffer, publicId); + + // Upload packed song file + const packedFileKey = await this.uploadPackedSongFile( + packedSongBuffer, + publicId + ); + + // PROCESS UPLOADED SONG + // TODO: delete file from S3 if remainder of upload method fails + + // Calculate song document's data from NBS file + const songStats = SongStatsGenerator.getSongStats(nbsSong); + + // Generate thumbnail + const thumbUrl: string = await this.generateAndUploadThumbnail( + body.thumbnailData, + nbsSong, + publicId + ); + + // Create song document + const song = await this.generateSongDocument( + user, + publicId, + body, + thumbUrl, + fileKey, + packedFileKey, // TODO: should be packedFileUrl + songStats, + file + ); + + return song; + } + + public async processSongPatch( + songDocument: SongDocument, + body: UploadSongDto, + user: UserDocument + ): Promise { // Compare arrays of custom instruments including order - const customInstrumentsChanged = - JSON.stringify(songDocument.customInstruments) !== + const customInstrumentsChanged = + JSON.stringify(songDocument.customInstruments) !== JSON.stringify(body.customInstruments); - const songMetadataChanged = - customInstrumentsChanged || + const songMetadataChanged = + customInstrumentsChanged || songDocument.title !== body.title || songDocument.originalAuthor !== body.originalAuthor || // TODO: verify if song author matches current username // songDocument.uploader.username !== user.username && songDocument.description !== body.description; - // Compare thumbnail data - const thumbnailChanged = - JSON.stringify(songDocument.thumbnailData) !== + // Compare thumbnail data + const thumbnailChanged = + JSON.stringify(songDocument.thumbnailData) !== JSON.stringify(body.thumbnailData); - if (songMetadataChanged || thumbnailChanged) { - // If either the thumbnail or the song metadata changed, we need to - // download the existing song file to replace some fields and reupload it, - // and/or regenerate and reupload the thumbnail + if (songMetadataChanged || thumbnailChanged) { + // If either the thumbnail or the song metadata changed, we need to + // download the existing song file to replace some fields and reupload it, + // and/or regenerate and reupload the thumbnail + + const songFile = await this.fileService.getSongFile( + songDocument.nbsFileUrl + ); + + const originalSongBuffer = Buffer.from(songFile); + + // Regenerate song file + packed song file if metadata or custom instruments changed + if (songMetadataChanged) { + this.logger.log('Song metadata changed; reuploading song files'); + + const { nbsSong, songBuffer } = this.prepareSongForUpload( + originalSongBuffer, + body, + user + ); + + // Obfuscate and pack song with updated custom instruments + const packedSongBuffer = await this.preparePackedSongForUpload( + nbsSong, + body.customInstruments + ); + + // Re-upload song file + await this.uploadSongFile(songBuffer, songDocument.publicId); + + // Re-upload packed song file + await this.uploadPackedSongFile( + packedSongBuffer, + songDocument.publicId + ); + } - const songFile = await this.fileService.getSongFile( - songDocument.nbsFileUrl - ); + if (thumbnailChanged) { + this.logger.log('Thumbnail data changed; re-uploading thumbnail'); - const originalSongBuffer = Buffer.from(songFile); + const nbsSong = this.getSongObject(songFile); - // Regenerate song file + packed song file if metadata or custom instruments changed - if (songMetadataChanged) { - this.logger.log('Song metadata changed; reuploading song files'); + await this.generateAndUploadThumbnail( + body.thumbnailData, + nbsSong, + songDocument.publicId + ); + } + } + } - const { nbsSong, songBuffer } = this.prepareSongForUpload( - originalSongBuffer, - body, - user + private prepareSongForUpload( + songFileBuffer: Buffer, + body: UploadSongDto, + user: UserDocument + ): { nbsSong: Song; songBuffer: Buffer } { + const songFileArrayBuffer = songFileBuffer.buffer.slice( + songFileBuffer.byteOffset, + songFileBuffer.byteOffset + songFileBuffer.byteLength + ) as ArrayBuffer; + + // Is the uploaded file a valid .nbs file? + const nbsSong = this.getSongObject(songFileArrayBuffer); + + // Update NBS file with form values + injectSongFileMetadata( + nbsSong, + removeExtraSpaces(body.title), + removeExtraSpaces(user.username), + removeExtraSpaces(body.originalAuthor), + removeExtraSpaces(body.description), + body.customInstruments ); - // Obfuscate and pack song with updated custom instruments - const packedSongBuffer = await this.preparePackedSongForUpload( - nbsSong, - body.customInstruments - ); + const updatedSongArrayBuffer = toArrayBuffer(nbsSong); + const songBuffer = Buffer.from(updatedSongArrayBuffer); + + return { nbsSong, songBuffer }; + } - // Re-upload song file - await this.uploadSongFile(songBuffer, songDocument.publicId); + private async preparePackedSongForUpload( + nbsSong: Song, + soundsArray: string[] + ): Promise { + const soundsMapping = await this.getSoundsMapping(); + const validSoundsSubset = await this.getValidSoundsSubset(); - // Re-upload packed song file - await this.uploadPackedSongFile( - packedSongBuffer, - songDocument.publicId + this.validateCustomInstruments(soundsArray, validSoundsSubset); + + const packedSongBuffer = await obfuscateAndPackSong( + nbsSong, + soundsArray, + soundsMapping ); - } - if (thumbnailChanged) { - this.logger.log('Thumbnail data changed; re-uploading thumbnail'); + return packedSongBuffer; + } - const nbsSong = this.getSongObject(songFile); + private validateCustomInstruments( + soundsArray: string[], + validSounds: Set + ): void { + const isInstrumentValid = (sound: string) => + sound === '' || validSounds.has(sound); - await this.generateAndUploadThumbnail( - body.thumbnailData, - nbsSong, - songDocument.publicId + const areAllInstrumentsValid = soundsArray.every((sound) => + isInstrumentValid(sound) ); - } - } - } - - private prepareSongForUpload( - songFileBuffer: Buffer, - body: UploadSongDto, - user: UserDocument - ): { nbsSong: Song; songBuffer: Buffer } { - const songFileArrayBuffer = songFileBuffer.buffer.slice( - songFileBuffer.byteOffset, - songFileBuffer.byteOffset + songFileBuffer.byteLength - ) as ArrayBuffer; - - // Is the uploaded file a valid .nbs file? - const nbsSong = this.getSongObject(songFileArrayBuffer); - - // Update NBS file with form values - injectSongFileMetadata( - nbsSong, - removeExtraSpaces(body.title), - removeExtraSpaces(user.username), - removeExtraSpaces(body.originalAuthor), - removeExtraSpaces(body.description), - body.customInstruments - ); - - const updatedSongArrayBuffer = toArrayBuffer(nbsSong); - const songBuffer = Buffer.from(updatedSongArrayBuffer); - - return { nbsSong, songBuffer }; - } - - private async preparePackedSongForUpload( - nbsSong: Song, - soundsArray: string[] - ): Promise { - const soundsMapping = await this.getSoundsMapping(); - const validSoundsSubset = await this.getValidSoundsSubset(); - - this.validateCustomInstruments(soundsArray, validSoundsSubset); - - const packedSongBuffer = await obfuscateAndPackSong( - nbsSong, - soundsArray, - soundsMapping - ); - - return packedSongBuffer; - } - - private validateCustomInstruments( - soundsArray: string[], - validSounds: Set - ): void { - const isInstrumentValid = (sound: string) => - sound === '' || validSounds.has(sound); - - const areAllInstrumentsValid = soundsArray.every((sound) => - isInstrumentValid(sound) - ); - - if (!areAllInstrumentsValid) { - throw new HttpException( - { - error: { - customInstruments: + + if (!areAllInstrumentsValid) { + throw new HttpException( + { + error: { + customInstruments: 'One or more invalid custom instruments have been set' - } - }, - HttpStatus.BAD_REQUEST - ); + } + }, + HttpStatus.BAD_REQUEST + ); + } } - } - - public async generateAndUploadThumbnail( - thumbnailData: ThumbnailData, - nbsSong: Song, - publicId: string - ): Promise { - const { startTick, startLayer, zoomLevel, backgroundColor } = thumbnailData; - - const quadTree = new NoteQuadTree(nbsSong); - - const thumbBuffer = await drawToImage({ - notes : quadTree, - startTick : startTick, - startLayer : startLayer, - zoomLevel : zoomLevel, - backgroundColor: backgroundColor, - imgWidth : 1280, - imgHeight : 768 - }); - - // Upload thumbnail - let thumbUrl: string; - - try { - thumbUrl = await this.fileService.uploadThumbnail(thumbBuffer, publicId); - } catch (e) { - throw new HttpException( - { - error: { - file: "An error occurred while creating the song's thumbnail" - } - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); + + public async generateAndUploadThumbnail( + thumbnailData: ThumbnailData, + nbsSong: Song, + publicId: string + ): Promise { + const { startTick, startLayer, zoomLevel, backgroundColor } = thumbnailData; + + const quadTree = new NoteQuadTree(nbsSong); + + const thumbBuffer = await drawToImage({ + notes : quadTree, + startTick : startTick, + startLayer : startLayer, + zoomLevel : zoomLevel, + backgroundColor: backgroundColor, + imgWidth : 1280, + imgHeight : 768 + }); + + // Upload thumbnail + let thumbUrl: string; + + try { + thumbUrl = await this.fileService.uploadThumbnail(thumbBuffer, publicId); + } catch (e) { + throw new HttpException( + { + error: { + file: "An error occurred while creating the song's thumbnail" + } + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + this.logger.log(`Uploaded thumbnail to ${thumbUrl}`); + + return thumbUrl; } - this.logger.log(`Uploaded thumbnail to ${thumbUrl}`); - - return thumbUrl; - } - - private async uploadSongFile( - file: Buffer, - publicId: string - ): Promise { - let fileKey: string; - - try { - fileKey = await this.fileService.uploadSong(file, publicId); - } catch (e) { - throw new HttpException( - { - error: { - file: 'An error occurred while uploading the packed song file' - } - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); + private async uploadSongFile( + file: Buffer, + publicId: string + ): Promise { + let fileKey: string; + + try { + fileKey = await this.fileService.uploadSong(file, publicId); + } catch (e) { + throw new HttpException( + { + error: { + file: 'An error occurred while uploading the packed song file' + } + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + this.logger.log(`Uploaded song file to ${fileKey}`); + + return fileKey; } - this.logger.log(`Uploaded song file to ${fileKey}`); - - return fileKey; - } - - private async uploadPackedSongFile( - file: Buffer, - publicId: string - ): Promise { - let fileKey: string; - - try { - fileKey = await this.fileService.uploadPackedSong(file, publicId); - } catch (e) { - throw new HttpException( - { - error: { - file: 'An error occurred while uploading the song file' - } - }, - HttpStatus.INTERNAL_SERVER_ERROR - ); + private async uploadPackedSongFile( + file: Buffer, + publicId: string + ): Promise { + let fileKey: string; + + try { + fileKey = await this.fileService.uploadPackedSong(file, publicId); + } catch (e) { + throw new HttpException( + { + error: { + file: 'An error occurred while uploading the song file' + } + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + this.logger.log(`Uploaded packed song file to ${fileKey}`); + + return fileKey; } - this.logger.log(`Uploaded packed song file to ${fileKey}`); - - return fileKey; - } - - public getSongObject(loadedArrayBuffer: ArrayBuffer): Song { - const nbsSong = fromArrayBuffer(loadedArrayBuffer); - - // If the above operation fails, it will return an empty song - if (nbsSong.length === 0) { - throw new HttpException( - { - error: { - file : 'Invalid NBS file', - errors: nbsSong.errors - } - }, - HttpStatus.BAD_REQUEST - ); + public getSongObject(loadedArrayBuffer: ArrayBuffer): Song { + const nbsSong = fromArrayBuffer(loadedArrayBuffer); + + // If the above operation fails, it will return an empty song + if (nbsSong.length === 0) { + throw new HttpException( + { + error: { + file : 'Invalid NBS file', + errors: nbsSong.errors + } + }, + HttpStatus.BAD_REQUEST + ); + } + + return nbsSong; } - return nbsSong; - } - - private checkIsFileValid(file: Express.Multer.File): void { - if (!file) { - throw new HttpException( - { - error: { - file: 'File not found' - } - }, - HttpStatus.BAD_REQUEST - ); + private checkIsFileValid(file: Express.Multer.File): void { + if (!file) { + throw new HttpException( + { + error: { + file: 'File not found' + } + }, + HttpStatus.BAD_REQUEST + ); + } } - } } diff --git a/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts b/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts index 33754c14..ec8c2e77 100644 --- a/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts +++ b/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts @@ -11,254 +11,254 @@ import { getUploadDiscordEmbed } from '../song.util'; import { SongWebhookService } from './song-webhook.service'; mock.module('../song.util', () => ({ - getUploadDiscordEmbed: jest.fn() + getUploadDiscordEmbed: jest.fn() })); const mockSongModel = { - find : jest.fn().mockReturnThis(), - sort : jest.fn().mockReturnThis(), - populate: jest.fn().mockReturnThis(), - save : jest.fn() + find : jest.fn().mockReturnThis(), + sort : jest.fn().mockReturnThis(), + populate: jest.fn().mockReturnThis(), + save : jest.fn() }; describe('SongWebhookService', () => { - let service: SongWebhookService; - let _songModel: Model; - let _configService: ConfigService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports : [ConfigModule.forRoot()], - providers: [ - SongWebhookService, - { - provide : getModelToken(SongEntity.name), - useValue: mockSongModel - }, - { - provide : 'DISCORD_WEBHOOK_URL', - useValue: 'http://localhost/webhook' - } - ] - }).compile(); - - service = module.get(SongWebhookService); - _songModel = module.get>(getModelToken(SongEntity.name)); - _configService = module.get(ConfigService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('postSongWebhook', () => { - it('should post a new webhook message for a song', async () => { - const song: SongWithUser = { - publicId: '123', - uploader: { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; - - (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - - global.fetch = jest.fn().mockResolvedValue({ - json: jest.fn().mockResolvedValue({ id: 'message-id' }) - } as unknown as Response) as unknown as typeof global.fetch; - - const result = await service.postSongWebhook(song); - - expect(result).toBe('message-id'); - - expect(fetch).toHaveBeenCalledWith('http://localhost/webhook?wait=true', { - method : 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}) - }); + let service: SongWebhookService; + let _songModel: Model; + let _configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports : [ConfigModule.forRoot()], + providers: [ + SongWebhookService, + { + provide : getModelToken(SongEntity.name), + useValue: mockSongModel + }, + { + provide : 'DISCORD_WEBHOOK_URL', + useValue: 'http://localhost/webhook' + } + ] + }).compile(); + + service = module.get(SongWebhookService); + _songModel = module.get>(getModelToken(SongEntity.name)); + _configService = module.get(ConfigService); }); - it('should return null if there is an error', async () => { - const song: SongWithUser = { - publicId: '123', - uploader: { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; + it('should be defined', () => { + expect(service).toBeDefined(); + }); - (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); + describe('postSongWebhook', () => { + it('should post a new webhook message for a song', async () => { + const song: SongWithUser = { + publicId: '123', + uploader: { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; - global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; + (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - const result = await service.postSongWebhook(song); + global.fetch = jest.fn().mockResolvedValue({ + json: jest.fn().mockResolvedValue({ id: 'message-id' }) + } as unknown as Response) as unknown as typeof global.fetch; - expect(result).toBeNull(); - }); - }); - - describe('updateSongWebhook', () => { - it('should update the webhook message for a song', async () => { - const song: SongWithUser = { - publicId : '123', - webhookMessageId: 'message-id', - uploader : { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; - - (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - - global.fetch = jest.fn().mockResolvedValue({} as Response) as unknown as typeof global.fetch; - - await service.updateSongWebhook(song); - - expect(fetch).toHaveBeenCalledWith( - 'http://localhost/webhook/messages/message-id', - { - method : 'PATCH', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}) - } - ); - }); + const result = await service.postSongWebhook(song); - it('should log an error if there is an error', async () => { - const song: SongWithUser = { - publicId : '123', - webhookMessageId: 'message-id', - uploader : { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; + expect(result).toBe('message-id'); - (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); + expect(fetch).toHaveBeenCalledWith('http://localhost/webhook?wait=true', { + method : 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) + }); + }); - global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; + it('should return null if there is an error', async () => { + const song: SongWithUser = { + publicId: '123', + uploader: { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; - const loggerSpy = spyOn(service['logger'], 'error'); + (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - await service.updateSongWebhook(song); + global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; - expect(loggerSpy).toHaveBeenCalledWith( - 'Error updating Discord webhook', - expect.any(Error) - ); - }); - }); - - describe('deleteSongWebhook', () => { - it('should delete the webhook message for a song', async () => { - const song: SongWithUser = { - publicId : '123', - webhookMessageId: 'message-id', - uploader : { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; - - global.fetch = jest.fn().mockResolvedValue({} as Response) as unknown as typeof global.fetch; - - await service.deleteSongWebhook(song); - - expect(fetch).toHaveBeenCalledWith( - 'http://localhost/webhook/messages/message-id', - { - method: 'DELETE' - } - ); + const result = await service.postSongWebhook(song); + + expect(result).toBeNull(); + }); }); - it('should log an error if there is an error', async () => { - const song: SongWithUser = { - publicId : '123', - webhookMessageId: 'message-id', - uploader : { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; + describe('updateSongWebhook', () => { + it('should update the webhook message for a song', async () => { + const song: SongWithUser = { + publicId : '123', + webhookMessageId: 'message-id', + uploader : { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; - global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; + (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - const loggerSpy = spyOn(service['logger'], 'error'); + global.fetch = jest.fn().mockResolvedValue({} as Response) as unknown as typeof global.fetch; - await service.deleteSongWebhook(song); + await service.updateSongWebhook(song); - expect(loggerSpy).toHaveBeenCalledWith( - 'Error deleting Discord webhook', - expect.any(Error) - ); - }); - }); + expect(fetch).toHaveBeenCalledWith( + 'http://localhost/webhook/messages/message-id', + { + method : 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) + } + ); + }); - describe('syncSongWebhook', () => { - it('should update the webhook message if the song is public', async () => { - const song: SongWithUser = { - publicId : '123', - webhookMessageId: 'message-id', - visibility : 'public', - uploader : { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; + it('should log an error if there is an error', async () => { + const song: SongWithUser = { + publicId : '123', + webhookMessageId: 'message-id', + uploader : { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; - const updateSpy = spyOn(service, 'updateSongWebhook'); + (getUploadDiscordEmbed as jest.Mock).mockReturnValue({}); - await service.syncSongWebhook(song); + global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; - expect(updateSpy).toHaveBeenCalledWith(song); + const loggerSpy = spyOn(service['logger'], 'error'); + + await service.updateSongWebhook(song); + + expect(loggerSpy).toHaveBeenCalledWith( + 'Error updating Discord webhook', + expect.any(Error) + ); + }); }); - it('should delete the webhook message if the song is not public', async () => { - const song: SongWithUser = { - publicId : '123', - webhookMessageId: 'message-id', - visibility : 'private', - uploader : { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; + describe('deleteSongWebhook', () => { + it('should delete the webhook message for a song', async () => { + const song: SongWithUser = { + publicId : '123', + webhookMessageId: 'message-id', + uploader : { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; - const deleteSpy = spyOn(service, 'deleteSongWebhook'); + global.fetch = jest.fn().mockResolvedValue({} as Response) as unknown as typeof global.fetch; - await service.syncSongWebhook(song); + await service.deleteSongWebhook(song); - expect(deleteSpy).toHaveBeenCalledWith(song); - }); + expect(fetch).toHaveBeenCalledWith( + 'http://localhost/webhook/messages/message-id', + { + method: 'DELETE' + } + ); + }); - it('should post a new webhook message if the song is public and does not have a message', async () => { - const song: SongWithUser = { - publicId : '123', - visibility: 'public', - uploader : { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; + it('should log an error if there is an error', async () => { + const song: SongWithUser = { + publicId : '123', + webhookMessageId: 'message-id', + uploader : { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; - const postSpy = spyOn(service, 'postSongWebhook'); + global.fetch = jest.fn().mockRejectedValue(new Error('Error')) as unknown as typeof global.fetch; - await service.syncSongWebhook(song); + const loggerSpy = spyOn(service['logger'], 'error'); - expect(postSpy).toHaveBeenCalledWith(song); + await service.deleteSongWebhook(song); + + expect(loggerSpy).toHaveBeenCalledWith( + 'Error deleting Discord webhook', + expect.any(Error) + ); + }); }); - it('should return null if the song is not public and does not have a message', async () => { - const song: SongWithUser = { - publicId : '123', - visibility: 'private', - uploader : { username: 'testuser', profileImage: 'testimage' } - } as SongWithUser; + describe('syncSongWebhook', () => { + it('should update the webhook message if the song is public', async () => { + const song: SongWithUser = { + publicId : '123', + webhookMessageId: 'message-id', + visibility : 'public', + uploader : { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; + + const updateSpy = spyOn(service, 'updateSongWebhook'); + + await service.syncSongWebhook(song); + + expect(updateSpy).toHaveBeenCalledWith(song); + }); + + it('should delete the webhook message if the song is not public', async () => { + const song: SongWithUser = { + publicId : '123', + webhookMessageId: 'message-id', + visibility : 'private', + uploader : { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; + + const deleteSpy = spyOn(service, 'deleteSongWebhook'); + + await service.syncSongWebhook(song); + + expect(deleteSpy).toHaveBeenCalledWith(song); + }); + + it('should post a new webhook message if the song is public and does not have a message', async () => { + const song: SongWithUser = { + publicId : '123', + visibility: 'public', + uploader : { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; + + const postSpy = spyOn(service, 'postSongWebhook'); + + await service.syncSongWebhook(song); + + expect(postSpy).toHaveBeenCalledWith(song); + }); + + it('should return null if the song is not public and does not have a message', async () => { + const song: SongWithUser = { + publicId : '123', + visibility: 'private', + uploader : { username: 'testuser', profileImage: 'testimage' } + } as SongWithUser; - const result = await service.syncSongWebhook(song); + const result = await service.syncSongWebhook(song); - expect(result).toBeNull(); + expect(result).toBeNull(); + }); }); - }); - describe('syncAllSongsWebhook', () => { - it('should synchronize the webhook messages for all songs', async () => { - const songs: SongWithUser[] = [ - { - publicId: '123', - uploader: { username: 'testuser', profileImage: 'testimage' }, - save : jest.fn() - } as unknown as SongWithUser - ]; + describe('syncAllSongsWebhook', () => { + it('should synchronize the webhook messages for all songs', async () => { + const songs: SongWithUser[] = [ + { + publicId: '123', + uploader: { username: 'testuser', profileImage: 'testimage' }, + save : jest.fn() + } as unknown as SongWithUser + ]; - mockSongModel.find.mockReturnValue({ - sort : jest.fn().mockReturnThis(), - populate: jest.fn().mockResolvedValue(songs) - }); + mockSongModel.find.mockReturnValue({ + sort : jest.fn().mockReturnThis(), + populate: jest.fn().mockResolvedValue(songs) + }); - const syncSpy = spyOn(service, 'syncSongWebhook'); + const syncSpy = spyOn(service, 'syncSongWebhook'); - await service['syncAllSongsWebhook'](); + await service['syncAllSongsWebhook'](); - expect(syncSpy).toHaveBeenCalledWith(songs[0]); + expect(syncSpy).toHaveBeenCalledWith(songs[0]); + }); }); - }); }); diff --git a/apps/backend/src/song/song-webhook/song-webhook.service.ts b/apps/backend/src/song/song-webhook/song-webhook.service.ts index e5788372..ba551e57 100644 --- a/apps/backend/src/song/song-webhook/song-webhook.service.ts +++ b/apps/backend/src/song/song-webhook/song-webhook.service.ts @@ -7,21 +7,21 @@ import { getUploadDiscordEmbed } from '../song.util'; @Injectable() export class SongWebhookService implements OnModuleInit { - private logger = new Logger(SongWebhookService.name); - - constructor( - @InjectModel(SongEntity.name) - private songModel: Model, - @Inject('DISCORD_WEBHOOK_URL') - private readonly discordWebhookUrl: string | undefined - ) {} - - async onModuleInit() { - this.logger.log('Updating Discord webhooks for all songs'); - this.syncAllSongsWebhook(); - } + private logger = new Logger(SongWebhookService.name); + + constructor( + @InjectModel(SongEntity.name) + private songModel: Model, + @Inject('DISCORD_WEBHOOK_URL') + private readonly discordWebhookUrl: string | undefined + ) {} + + async onModuleInit() { + this.logger.log('Updating Discord webhooks for all songs'); + this.syncAllSongsWebhook(); + } - public async postSongWebhook(song: SongWithUser): Promise { + public async postSongWebhook(song: SongWithUser): Promise { /** * Posts a new webhook message for a song. * @@ -30,35 +30,35 @@ export class SongWebhookService implements OnModuleInit { * @throws {Error} If the Discord webhook URL is not found. * @throws {Error} If there is an error sending the webhook message. */ - const webhookUrl = this.discordWebhookUrl; - - if (!webhookUrl) { - this.logger.error('Discord webhook URL not found'); - return null; - } - - const webhookData = getUploadDiscordEmbed(song); - - try { - const response = await fetch(`${webhookUrl}?wait=true`, { - method : 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(webhookData) - }); - - const data = (await response.json()) as { id: string }; - - //this.logger.log(`Posted webhook message for song ${song.publicId}`); - return data.id; // Discord message ID - } catch (e) { - this.logger.error('Error sending Discord webhook', e); - return null; + const webhookUrl = this.discordWebhookUrl; + + if (!webhookUrl) { + this.logger.error('Discord webhook URL not found'); + return null; + } + + const webhookData = getUploadDiscordEmbed(song); + + try { + const response = await fetch(`${webhookUrl}?wait=true`, { + method : 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(webhookData) + }); + + const data = (await response.json()) as { id: string }; + + //this.logger.log(`Posted webhook message for song ${song.publicId}`); + return data.id; // Discord message ID + } catch (e) { + this.logger.error('Error sending Discord webhook', e); + return null; + } } - } - public async updateSongWebhook(song: SongWithUser) { + public async updateSongWebhook(song: SongWithUser) { /** * Updates the webhook message for a song. * @@ -69,35 +69,35 @@ export class SongWebhookService implements OnModuleInit { * @throws {Error} If there is an error updating the webhook message. */ - if (!song.webhookMessageId) { - throw new Error('Song does not have a webhook message'); - } + if (!song.webhookMessageId) { + throw new Error('Song does not have a webhook message'); + } - const webhookUrl = this.discordWebhookUrl; + const webhookUrl = this.discordWebhookUrl; - if (!webhookUrl) { - this.logger.error('Discord webhook URL not found'); - return; - } + if (!webhookUrl) { + this.logger.error('Discord webhook URL not found'); + return; + } - const webhookData = getUploadDiscordEmbed(song); + const webhookData = getUploadDiscordEmbed(song); - try { - await fetch(`${webhookUrl}/messages/${song.webhookMessageId}`, { - method : 'PATCH', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(webhookData) - }); + try { + await fetch(`${webhookUrl}/messages/${song.webhookMessageId}`, { + method : 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(webhookData) + }); - this.logger.log(`Updated webhook message for song ${song.publicId}`); - } catch (e) { - this.logger.error('Error updating Discord webhook', e); + this.logger.log(`Updated webhook message for song ${song.publicId}`); + } catch (e) { + this.logger.error('Error updating Discord webhook', e); + } } - } - public async deleteSongWebhook(song: SongWithUser) { + public async deleteSongWebhook(song: SongWithUser) { /** * Deletes the webhook message for a song. * @@ -108,29 +108,29 @@ export class SongWebhookService implements OnModuleInit { * @throws {Error} If there is an error deleting the webhook message. * */ - if (!song.webhookMessageId) { - throw new Error('Song does not have a webhook message'); - } - - const webhookUrl = this.discordWebhookUrl; - - if (!webhookUrl) { - this.logger.error('Discord webhook URL not found'); - return; - } - - try { - await fetch(`${webhookUrl}/messages/${song.webhookMessageId}`, { - method: 'DELETE' - }); - - this.logger.log(`Deleted webhook message for song ${song.publicId}`); - } catch (e) { - this.logger.error('Error deleting Discord webhook', e); + if (!song.webhookMessageId) { + throw new Error('Song does not have a webhook message'); + } + + const webhookUrl = this.discordWebhookUrl; + + if (!webhookUrl) { + this.logger.error('Discord webhook URL not found'); + return; + } + + try { + await fetch(`${webhookUrl}/messages/${song.webhookMessageId}`, { + method: 'DELETE' + }); + + this.logger.log(`Deleted webhook message for song ${song.publicId}`); + } catch (e) { + this.logger.error('Error deleting Discord webhook', e); + } } - } - public async syncSongWebhook(song: SongWithUser) { + public async syncSongWebhook(song: SongWithUser) { /** * Synchronizes the webhook message for a song. * @@ -142,27 +142,27 @@ export class SongWebhookService implements OnModuleInit { * @returns {Promise} A promise that resolves with the new or updated message ID, or null if the message was deleted. */ - if (song.webhookMessageId) { - // Update existing message - if (song.visibility === 'public') { - await this.updateSongWebhook(song); - return song.webhookMessageId; - } else { - await this.deleteSongWebhook(song); - return null; - } - } else { - // Post new message - if (song.visibility === 'public') { - const newMessageId = await this.postSongWebhook(song); - return newMessageId; - } else { - return null; - } + if (song.webhookMessageId) { + // Update existing message + if (song.visibility === 'public') { + await this.updateSongWebhook(song); + return song.webhookMessageId; + } else { + await this.deleteSongWebhook(song); + return null; + } + } else { + // Post new message + if (song.visibility === 'public') { + const newMessageId = await this.postSongWebhook(song); + return newMessageId; + } else { + return null; + } + } } - } - private async syncAllSongsWebhook() { + private async syncAllSongsWebhook() { /** * Synchronizes the webhook messages for all songs in the database. * @@ -172,22 +172,22 @@ export class SongWebhookService implements OnModuleInit { * * @returns {Promise} A promise that resolves when all songs have been processed. */ - const songQuery = this.songModel - .find({ webhookMessageId: { $exists: false } }) - .sort({ createdAt: 1 }) - .populate('uploader', 'username profileImage -_id'); + const songQuery = this.songModel + .find({ webhookMessageId: { $exists: false } }) + .sort({ createdAt: 1 }) + .populate('uploader', 'username profileImage -_id'); - for (const songDocument of await songQuery) { - const webhookMessageId = await this.syncSongWebhook( - songDocument as unknown as SongWithUser - ); + for (const songDocument of await songQuery) { + const webhookMessageId = await this.syncSongWebhook( + songDocument as unknown as SongWithUser + ); - songDocument.webhookMessageId = webhookMessageId; + songDocument.webhookMessageId = webhookMessageId; - await songDocument.save(); + await songDocument.save(); - // wait a bit to avoid rate limiting - await new Promise((resolve) => setTimeout(resolve, 2000)); + // wait a bit to avoid rate limiting + await new Promise((resolve) => setTimeout(resolve, 2000)); + } } - } } diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts index 7fe8317f..d83514a1 100644 --- a/apps/backend/src/song/song.controller.spec.ts +++ b/apps/backend/src/song/song.controller.spec.ts @@ -15,330 +15,330 @@ import { SongController } from './song.controller'; import { SongService } from './song.service'; const mockSongService = { - getSongByPage : jest.fn(), - getSong : jest.fn(), - getSongEdit : jest.fn(), - patchSong : jest.fn(), - getSongDownloadUrl: jest.fn(), - deleteSong : jest.fn(), - uploadSong : jest.fn() + getSongByPage : jest.fn(), + getSong : jest.fn(), + getSongEdit : jest.fn(), + patchSong : jest.fn(), + getSongDownloadUrl: jest.fn(), + deleteSong : jest.fn(), + uploadSong : jest.fn() }; const mockFileService = {}; describe('SongController', () => { - let songController: SongController; - let songService: SongService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SongController], - providers : [ - { - provide : SongService, - useValue: mockSongService - }, - { - provide : FileService, - useValue: mockFileService - } - ] - }) - .overrideGuard(AuthGuard('jwt-refresh')) - .useValue({ canActivate: jest.fn(() => true) }) - .compile(); - - songController = module.get(SongController); - songService = module.get(SongService); - }); - - it('should be defined', () => { - expect(songController).toBeDefined(); - }); - - describe('getSongList', () => { - it('should return a list of songs', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; - const songList: SongPreviewDto[] = []; - - mockSongService.getSongByPage.mockResolvedValueOnce(songList); - - const result = await songController.getSongList(query); - - expect(result).toEqual(songList); - expect(songService.getSongByPage).toHaveBeenCalledWith(query); + let songController: SongController; + let songService: SongService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [SongController], + providers : [ + { + provide : SongService, + useValue: mockSongService + }, + { + provide : FileService, + useValue: mockFileService + } + ] + }) + .overrideGuard(AuthGuard('jwt-refresh')) + .useValue({ canActivate: jest.fn(() => true) }) + .compile(); + + songController = module.get(SongController); + songService = module.get(SongService); }); - it('should handle errors', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; - - mockSongService.getSongByPage.mockRejectedValueOnce(new Error('Error')); - - await expect(songController.getSongList(query)).rejects.toThrow('Error'); + it('should be defined', () => { + expect(songController).toBeDefined(); }); - }); - describe('getSong', () => { - it('should return song info by ID', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const song: SongViewDto = {} as SongViewDto; + describe('getSongList', () => { + it('should return a list of songs', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; + const songList: SongPreviewDto[] = []; - mockSongService.getSong.mockResolvedValueOnce(song); + mockSongService.getSongByPage.mockResolvedValueOnce(songList); - const result = await songController.getSong(id, user); + const result = await songController.getSongList(query); - expect(result).toEqual(song); - expect(songService.getSong).toHaveBeenCalledWith(id, user); - }); + expect(result).toEqual(songList); + expect(songService.getSongByPage).toHaveBeenCalledWith(query); + }); - it('should handle errors', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + it('should handle errors', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; - mockSongService.getSong.mockRejectedValueOnce(new Error('Error')); + mockSongService.getSongByPage.mockRejectedValueOnce(new Error('Error')); - await expect(songController.getSong(id, user)).rejects.toThrow('Error'); + await expect(songController.getSongList(query)).rejects.toThrow('Error'); + }); }); - }); - describe('getEditSong', () => { - it('should return song info for editing by ID', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const song: UploadSongDto = {} as UploadSongDto; + describe('getSong', () => { + it('should return song info by ID', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const song: SongViewDto = {} as SongViewDto; - mockSongService.getSongEdit.mockResolvedValueOnce(song); + mockSongService.getSong.mockResolvedValueOnce(song); - const result = await songController.getEditSong(id, user); + const result = await songController.getSong(id, user); - expect(result).toEqual(song); - expect(songService.getSongEdit).toHaveBeenCalledWith(id, user); - }); + expect(result).toEqual(song); + expect(songService.getSong).toHaveBeenCalledWith(id, user); + }); - it('should handle errors', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + it('should handle errors', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - mockSongService.getSongEdit.mockRejectedValueOnce(new Error('Error')); + mockSongService.getSong.mockRejectedValueOnce(new Error('Error')); - await expect(songController.getEditSong(id, user)).rejects.toThrow( - 'Error' - ); + await expect(songController.getSong(id, user)).rejects.toThrow('Error'); + }); }); - }); - describe('patchSong', () => { - it('should edit song info by ID', async () => { - const id = 'test-id'; - const req = { body: {} } as any; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const response: UploadSongResponseDto = {} as UploadSongResponseDto; + describe('getEditSong', () => { + it('should return song info for editing by ID', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const song: UploadSongDto = {} as UploadSongDto; - mockSongService.patchSong.mockResolvedValueOnce(response); + mockSongService.getSongEdit.mockResolvedValueOnce(song); - const result = await songController.patchSong(id, req, user); + const result = await songController.getEditSong(id, user); - expect(result).toEqual(response); - expect(songService.patchSong).toHaveBeenCalledWith(id, req.body, user); - }); + expect(result).toEqual(song); + expect(songService.getSongEdit).toHaveBeenCalledWith(id, user); + }); - it('should handle errors', async () => { - const id = 'test-id'; - const req = { body: {} } as any; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + it('should handle errors', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - mockSongService.patchSong.mockRejectedValueOnce(new Error('Error')); + mockSongService.getSongEdit.mockRejectedValueOnce(new Error('Error')); - await expect(songController.patchSong(id, req, user)).rejects.toThrow( - 'Error' - ); + await expect(songController.getEditSong(id, user)).rejects.toThrow( + 'Error' + ); + }); }); - }); - - describe('getSongFile', () => { - it('should get song .nbs file', async () => { - const id = 'test-id'; - const src = 'test-src'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const res = { - set : jest.fn(), - redirect: jest.fn() - } as unknown as Response; + describe('patchSong', () => { + it('should edit song info by ID', async () => { + const id = 'test-id'; + const req = { body: {} } as any; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const response: UploadSongResponseDto = {} as UploadSongResponseDto; - const url = 'test-url'; + mockSongService.patchSong.mockResolvedValueOnce(response); - mockSongService.getSongDownloadUrl.mockResolvedValueOnce(url); + const result = await songController.patchSong(id, req, user); - await songController.getSongFile(id, src, user, res); + expect(result).toEqual(response); + expect(songService.patchSong).toHaveBeenCalledWith(id, req.body, user); + }); - expect(res.set).toHaveBeenCalledWith({ - 'Content-Disposition' : 'attachment; filename="song.nbs"', - 'Access-Control-Expose-Headers': 'Content-Disposition' - }); + it('should handle errors', async () => { + const id = 'test-id'; + const req = { body: {} } as any; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - expect(res.redirect).toHaveBeenCalledWith(HttpStatus.FOUND, url); + mockSongService.patchSong.mockRejectedValueOnce(new Error('Error')); - expect(songService.getSongDownloadUrl).toHaveBeenCalledWith( - id, - user, - src, - false - ); + await expect(songController.patchSong(id, req, user)).rejects.toThrow( + 'Error' + ); + }); }); - it('should handle errors', async () => { - const id = 'test-id'; - const src = 'test-src'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + describe('getSongFile', () => { + it('should get song .nbs file', async () => { + const id = 'test-id'; + const src = 'test-src'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const res = { - set : jest.fn(), - redirect: jest.fn() - } as unknown as Response; + const res = { + set : jest.fn(), + redirect: jest.fn() + } as unknown as Response; - mockSongService.getSongDownloadUrl.mockRejectedValueOnce( - new Error('Error') - ); + const url = 'test-url'; - await expect( - songController.getSongFile(id, src, user, res) - ).rejects.toThrow('Error'); - }); - }); + mockSongService.getSongDownloadUrl.mockResolvedValueOnce(url); - describe('getSongOpenUrl', () => { - it('should get song .nbs file open URL', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const src = 'downloadButton'; - const url = 'test-url'; + await songController.getSongFile(id, src, user, res); - mockSongService.getSongDownloadUrl.mockResolvedValueOnce(url); + expect(res.set).toHaveBeenCalledWith({ + 'Content-Disposition' : 'attachment; filename="song.nbs"', + 'Access-Control-Expose-Headers': 'Content-Disposition' + }); - const result = await songController.getSongOpenUrl(id, user, src); + expect(res.redirect).toHaveBeenCalledWith(HttpStatus.FOUND, url); - expect(result).toEqual(url); + expect(songService.getSongDownloadUrl).toHaveBeenCalledWith( + id, + user, + src, + false + ); + }); - expect(songService.getSongDownloadUrl).toHaveBeenCalledWith( - id, - user, - 'open', - true - ); - }); - - it('should throw UnauthorizedException if src is invalid', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const src = 'invalid-src'; + it('should handle errors', async () => { + const id = 'test-id'; + const src = 'test-src'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - await expect( - songController.getSongOpenUrl(id, user, src) - ).rejects.toThrow(UnauthorizedException); - }); + const res = { + set : jest.fn(), + redirect: jest.fn() + } as unknown as Response; - it('should handle errors', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const src = 'downloadButton'; + mockSongService.getSongDownloadUrl.mockRejectedValueOnce( + new Error('Error') + ); - mockSongService.getSongDownloadUrl.mockRejectedValueOnce( - new Error('Error') - ); + await expect( + songController.getSongFile(id, src, user, res) + ).rejects.toThrow('Error'); + }); + }); - await expect( - songController.getSongOpenUrl(id, user, src) - ).rejects.toThrow('Error'); + describe('getSongOpenUrl', () => { + it('should get song .nbs file open URL', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const src = 'downloadButton'; + const url = 'test-url'; + + mockSongService.getSongDownloadUrl.mockResolvedValueOnce(url); + + const result = await songController.getSongOpenUrl(id, user, src); + + expect(result).toEqual(url); + + expect(songService.getSongDownloadUrl).toHaveBeenCalledWith( + id, + user, + 'open', + true + ); + }); + + it('should throw UnauthorizedException if src is invalid', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const src = 'invalid-src'; + + await expect( + songController.getSongOpenUrl(id, user, src) + ).rejects.toThrow(UnauthorizedException); + }); + + it('should handle errors', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const src = 'downloadButton'; + + mockSongService.getSongDownloadUrl.mockRejectedValueOnce( + new Error('Error') + ); + + await expect( + songController.getSongOpenUrl(id, user, src) + ).rejects.toThrow('Error'); + }); }); - }); - describe('deleteSong', () => { - it('should delete a song', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + describe('deleteSong', () => { + it('should delete a song', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - mockSongService.deleteSong.mockResolvedValueOnce(undefined); + mockSongService.deleteSong.mockResolvedValueOnce(undefined); - await songController.deleteSong(id, user); + await songController.deleteSong(id, user); - expect(songService.deleteSong).toHaveBeenCalledWith(id, user); - }); + expect(songService.deleteSong).toHaveBeenCalledWith(id, user); + }); - it('should handle errors', async () => { - const id = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + it('should handle errors', async () => { + const id = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - mockSongService.deleteSong.mockRejectedValueOnce(new Error('Error')); + mockSongService.deleteSong.mockRejectedValueOnce(new Error('Error')); - await expect(songController.deleteSong(id, user)).rejects.toThrow( - 'Error' - ); - }); - }); - - describe('createSong', () => { - it('should upload a song', async () => { - const file = { buffer: Buffer.from('test') } as Express.Multer.File; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'cc_by_sa', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - file : undefined, - allowDownload: false - }; - - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const response: UploadSongResponseDto = {} as UploadSongResponseDto; - - mockSongService.uploadSong.mockResolvedValueOnce(response); - - const result = await songController.createSong(file, body, user); - - expect(result).toEqual(response); - expect(songService.uploadSong).toHaveBeenCalledWith({ body, file, user }); + await expect(songController.deleteSong(id, user)).rejects.toThrow( + 'Error' + ); + }); }); - it('should handle errors', async () => { - const file = { buffer: Buffer.from('test') } as Express.Multer.File; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'cc_by_sa', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - file : undefined, - allowDownload: false - }; - - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - mockSongService.uploadSong.mockRejectedValueOnce(new Error('Error')); - - await expect(songController.createSong(file, body, user)).rejects.toThrow( - 'Error' - ); + describe('createSong', () => { + it('should upload a song', async () => { + const file = { buffer: Buffer.from('test') } as Express.Multer.File; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'cc_by_sa', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + file : undefined, + allowDownload: false + }; + + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const response: UploadSongResponseDto = {} as UploadSongResponseDto; + + mockSongService.uploadSong.mockResolvedValueOnce(response); + + const result = await songController.createSong(file, body, user); + + expect(result).toEqual(response); + expect(songService.uploadSong).toHaveBeenCalledWith({ body, file, user }); + }); + + it('should handle errors', async () => { + const file = { buffer: Buffer.from('test') } as Express.Multer.File; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'cc_by_sa', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + file : undefined, + allowDownload: false + }; + + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + mockSongService.uploadSong.mockRejectedValueOnce(new Error('Error')); + + await expect(songController.createSong(file, body, user)).rejects.toThrow( + 'Error' + ); + }); }); - }); }); diff --git a/apps/backend/src/song/song.controller.ts b/apps/backend/src/song/song.controller.ts index e64bca9e..08328d11 100644 --- a/apps/backend/src/song/song.controller.ts +++ b/apps/backend/src/song/song.controller.ts @@ -1,40 +1,40 @@ import { UPLOAD_CONSTANTS } from '@nbw/config'; import type { UserDocument } from '@nbw/database'; import { - PageQueryDTO, - SongPreviewDto, - SongViewDto, - UploadSongDto, - UploadSongResponseDto + PageQueryDTO, + SongPreviewDto, + SongViewDto, + UploadSongDto, + UploadSongResponseDto } from '@nbw/database'; import type { RawBodyRequest } from '@nestjs/common'; import { - Body, - Controller, - Delete, - Get, - Headers, - HttpStatus, - Param, - Patch, - Post, - Query, - Req, - Res, - UnauthorizedException, - UploadedFile, - UseGuards, - UseInterceptors + Body, + Controller, + Delete, + Get, + Headers, + HttpStatus, + Param, + Patch, + Post, + Query, + Req, + Res, + UnauthorizedException, + UploadedFile, + UseGuards, + UseInterceptors } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { FileInterceptor } from '@nestjs/platform-express'; import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; import { - ApiBearerAuth, - ApiBody, - ApiConsumes, - ApiOperation, - ApiTags + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiOperation, + ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; @@ -48,146 +48,146 @@ import { SongService } from './song.service'; @Controller('song') @ApiTags('song') export class SongController { - static multerConfig: MulterOptions = { - limits: { - fileSize: UPLOAD_CONSTANTS.file.maxSize - }, - fileFilter: (req, file, cb) => { - if (!file.originalname.match(/\.(nbs)$/)) { - return cb(new Error('Only .nbs files are allowed!'), false); - } - - cb(null, true); + static multerConfig: MulterOptions = { + limits: { + fileSize: UPLOAD_CONSTANTS.file.maxSize + }, + fileFilter: (req, file, cb) => { + if (!file.originalname.match(/\.(nbs)$/)) { + return cb(new Error('Only .nbs files are allowed!'), false); + } + + cb(null, true); + } + }; + + constructor( + public readonly songService: SongService, + public readonly fileService: FileService + ) {} + + @Get('/') + @ApiOperation({ + summary: 'Get a filtered/sorted list of songs with pagination' + }) + public async getSongList( + @Query() query: PageQueryDTO + ): Promise { + return await this.songService.getSongByPage(query); } - }; - - constructor( - public readonly songService: SongService, - public readonly fileService: FileService - ) {} - - @Get('/') - @ApiOperation({ - summary: 'Get a filtered/sorted list of songs with pagination' - }) - public async getSongList( - @Query() query: PageQueryDTO - ): Promise { - return await this.songService.getSongByPage(query); - } - - @Get('/:id') - @ApiOperation({ summary: 'Get song info by ID' }) - public async getSong( - @Param('id') id: string, - @GetRequestToken() user: UserDocument | null - ): Promise { - return await this.songService.getSong(id, user); - } - - @Get('/:id/edit') - @ApiOperation({ summary: 'Get song info for editing by ID' }) - @UseGuards(AuthGuard('jwt-refresh')) - @ApiBearerAuth() - public async getEditSong( - @Param('id') id: string, - @GetRequestToken() user: UserDocument | null - ): Promise { - user = validateUser(user); - return await this.songService.getSongEdit(id, user); - } - - @Patch('/:id/edit') - @UseGuards(AuthGuard('jwt-refresh')) - @ApiBearerAuth() - @ApiOperation({ summary: 'Edit song info by ID' }) - @ApiBody({ - description: 'Upload Song', - type : UploadSongResponseDto - }) - public async patchSong( - @Param('id') id: string, - @Req() req: RawBodyRequest, - @GetRequestToken() user: UserDocument | null - ): Promise { - user = validateUser(user); - //TODO: Fix this weird type casting and raw body access - const body = req.body as unknown as UploadSongDto; - return await this.songService.patchSong(id, body, user); - } - - @Get('/:id/download') - @ApiOperation({ summary: 'Get song .nbs file' }) - public async getSongFile( - @Param('id') id: string, - @Query('src') src: string, - @GetRequestToken() user: UserDocument | null, - @Res() res: Response - ): Promise { - user = validateUser(user); - - // TODO: no longer used - res.set({ - 'Content-Disposition' : 'attachment; filename="song.nbs"', - // Expose the Content-Disposition header to the client - 'Access-Control-Expose-Headers': 'Content-Disposition' - }); - - const url = await this.songService.getSongDownloadUrl(id, user, src, false); - res.redirect(HttpStatus.FOUND, url); - } - - @Get('/:id/open') - @ApiOperation({ summary: 'Get song .nbs file' }) - public async getSongOpenUrl( - @Param('id') id: string, - @GetRequestToken() user: UserDocument | null, - @Headers('src') src: string - ): Promise { - if (src != 'downloadButton') { - throw new UnauthorizedException('Invalid source'); + + @Get('/:id') + @ApiOperation({ summary: 'Get song info by ID' }) + public async getSong( + @Param('id') id: string, + @GetRequestToken() user: UserDocument | null + ): Promise { + return await this.songService.getSong(id, user); + } + + @Get('/:id/edit') + @ApiOperation({ summary: 'Get song info for editing by ID' }) + @UseGuards(AuthGuard('jwt-refresh')) + @ApiBearerAuth() + public async getEditSong( + @Param('id') id: string, + @GetRequestToken() user: UserDocument | null + ): Promise { + user = validateUser(user); + return await this.songService.getSongEdit(id, user); + } + + @Patch('/:id/edit') + @UseGuards(AuthGuard('jwt-refresh')) + @ApiBearerAuth() + @ApiOperation({ summary: 'Edit song info by ID' }) + @ApiBody({ + description: 'Upload Song', + type : UploadSongResponseDto + }) + public async patchSong( + @Param('id') id: string, + @Req() req: RawBodyRequest, + @GetRequestToken() user: UserDocument | null + ): Promise { + user = validateUser(user); + //TODO: Fix this weird type casting and raw body access + const body = req.body as unknown as UploadSongDto; + return await this.songService.patchSong(id, body, user); + } + + @Get('/:id/download') + @ApiOperation({ summary: 'Get song .nbs file' }) + public async getSongFile( + @Param('id') id: string, + @Query('src') src: string, + @GetRequestToken() user: UserDocument | null, + @Res() res: Response + ): Promise { + user = validateUser(user); + + // TODO: no longer used + res.set({ + 'Content-Disposition' : 'attachment; filename="song.nbs"', + // Expose the Content-Disposition header to the client + 'Access-Control-Expose-Headers': 'Content-Disposition' + }); + + const url = await this.songService.getSongDownloadUrl(id, user, src, false); + res.redirect(HttpStatus.FOUND, url); + } + + @Get('/:id/open') + @ApiOperation({ summary: 'Get song .nbs file' }) + public async getSongOpenUrl( + @Param('id') id: string, + @GetRequestToken() user: UserDocument | null, + @Headers('src') src: string + ): Promise { + if (src != 'downloadButton') { + throw new UnauthorizedException('Invalid source'); + } + + const url = await this.songService.getSongDownloadUrl( + id, + user, + 'open', + true + ); + + return url; } - const url = await this.songService.getSongDownloadUrl( - id, - user, - 'open', - true - ); - - return url; - } - - @Delete('/:id') - @UseGuards(AuthGuard('jwt-refresh')) - @ApiBearerAuth() - @ApiOperation({ summary: 'Delete a song' }) - public async deleteSong( - @Param('id') id: string, - @GetRequestToken() user: UserDocument | null - ): Promise { - user = validateUser(user); - await this.songService.deleteSong(id, user); - } - - @Post('/') - @UseGuards(AuthGuard('jwt-refresh')) - @ApiBearerAuth() - @ApiConsumes('multipart/form-data') - @ApiBody({ - description: 'Upload Song', - type : UploadSongResponseDto - }) - @UseInterceptors(FileInterceptor('file', SongController.multerConfig)) - @ApiOperation({ - summary: 'Upload a .nbs file and send the song data, creating a new song' - }) - public async createSong( - @UploadedFile() file: Express.Multer.File, - @Body() body: UploadSongDto, - @GetRequestToken() user: UserDocument | null - ): Promise { - user = validateUser(user); - return await this.songService.uploadSong({ body, file, user }); - } + @Delete('/:id') + @UseGuards(AuthGuard('jwt-refresh')) + @ApiBearerAuth() + @ApiOperation({ summary: 'Delete a song' }) + public async deleteSong( + @Param('id') id: string, + @GetRequestToken() user: UserDocument | null + ): Promise { + user = validateUser(user); + await this.songService.deleteSong(id, user); + } + + @Post('/') + @UseGuards(AuthGuard('jwt-refresh')) + @ApiBearerAuth() + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: 'Upload Song', + type : UploadSongResponseDto + }) + @UseInterceptors(FileInterceptor('file', SongController.multerConfig)) + @ApiOperation({ + summary: 'Upload a .nbs file and send the song data, creating a new song' + }) + public async createSong( + @UploadedFile() file: Express.Multer.File, + @Body() body: UploadSongDto, + @GetRequestToken() user: UserDocument | null + ): Promise { + user = validateUser(user); + return await this.songService.uploadSong({ body, file, user }); + } } diff --git a/apps/backend/src/song/song.module.ts b/apps/backend/src/song/song.module.ts index 7c62b565..a5aea3e7 100644 --- a/apps/backend/src/song/song.module.ts +++ b/apps/backend/src/song/song.module.ts @@ -14,24 +14,24 @@ import { SongController } from './song.controller'; import { SongService } from './song.service'; @Module({ - imports: [ - MongooseModule.forFeature([{ name: Song.name, schema: SongSchema }]), - AuthModule, - UserModule, - FileModule.forRootAsync() - ], - providers: [ - SongService, - SongUploadService, - SongWebhookService, - { - inject : [ConfigService], - provide : 'DISCORD_WEBHOOK_URL', - useFactory: (configService: ConfigService) => - configService.getOrThrow('DISCORD_WEBHOOK_URL') - } - ], - controllers: [SongController, MySongsController], - exports : [SongService] + imports: [ + MongooseModule.forFeature([{ name: Song.name, schema: SongSchema }]), + AuthModule, + UserModule, + FileModule.forRootAsync() + ], + providers: [ + SongService, + SongUploadService, + SongWebhookService, + { + inject : [ConfigService], + provide : 'DISCORD_WEBHOOK_URL', + useFactory: (configService: ConfigService) => + configService.getOrThrow('DISCORD_WEBHOOK_URL') + } + ], + controllers: [SongController, MySongsController], + exports : [SongService] }) export class SongModule {} diff --git a/apps/backend/src/song/song.service.spec.ts b/apps/backend/src/song/song.service.spec.ts index 6108457e..40630d28 100644 --- a/apps/backend/src/song/song.service.spec.ts +++ b/apps/backend/src/song/song.service.spec.ts @@ -2,15 +2,15 @@ import { jest, describe, beforeEach, expect, it } from 'bun:test'; import type { UserDocument } from '@nbw/database'; import { - SongDocument, - Song as SongEntity, - SongPreviewDto, - SongSchema, - SongStats, - SongViewDto, - SongWithUser, - UploadSongDto, - UploadSongResponseDto + SongDocument, + Song as SongEntity, + SongPreviewDto, + SongSchema, + SongStats, + SongViewDto, + SongWithUser, + UploadSongDto, + UploadSongResponseDto } from '@nbw/database'; import { HttpException } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; @@ -24,1029 +24,1029 @@ import { SongWebhookService } from './song-webhook/song-webhook.service'; import { SongService } from './song.service'; const mockFileService = { - deleteSong : jest.fn(), - getSongDownloadUrl: jest.fn() + deleteSong : jest.fn(), + getSongDownloadUrl: jest.fn() }; const mockSongUploadService = { - processUploadedSong: jest.fn(), - processSongPatch : jest.fn() + processUploadedSong: jest.fn(), + processSongPatch : jest.fn() }; const mockSongWebhookService = { - syncAllSongsWebhook: jest.fn(), - postSongWebhook : jest.fn(), - updateSongWebhook : jest.fn(), - deleteSongWebhook : jest.fn(), - syncSongWebhook : jest.fn() + syncAllSongsWebhook: jest.fn(), + postSongWebhook : jest.fn(), + updateSongWebhook : jest.fn(), + deleteSongWebhook : jest.fn(), + syncSongWebhook : jest.fn() }; describe('SongService', () => { - let service: SongService; - let fileService: FileService; - let songUploadService: SongUploadService; - let songModel: Model; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SongService, - { - provide : SongWebhookService, - useValue: mockSongWebhookService - }, - { - provide : getModelToken(SongEntity.name), - useValue: mongoose.model(SongEntity.name, SongSchema) - }, - { - provide : FileService, - useValue: mockFileService - }, - { - provide : SongUploadService, - useValue: mockSongUploadService - } - ] - }).compile(); - - service = module.get(SongService); - fileService = module.get(FileService); - songUploadService = module.get(SongUploadService); - songModel = module.get>(getModelToken(SongEntity.name)); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('uploadSong', () => { - it('should upload a song', async () => { - const file = { buffer: Buffer.from('test') } as Express.Multer.File; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: true, - file : 'somebytes' - }; - - const commonData = { - publicId : 'public-song-id', - createdAt: new Date(), - stats : { - midiFileName : 'test.mid', - noteCount : 100, - tickCount : 1000, - layerCount : 10, - tempo : 120, - tempoRange : [100, 150], - timeSignature : 4, - duration : 60, - loop : true, - loopStartTick : 0, - minutesSpent : 10, - vanillaInstrumentCount : 10, - customInstrumentCount : 0, - firstCustomInstrumentIndex: 0, - outOfRangeNoteCount : 0, - detunedNoteCount : 0, - customInstrumentNoteCount : 0, - incompatibleNoteCount : 0, - compatible : true, - instrumentNoteCounts : [10] - }, - fileSize : 424242, - packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl : 'http://test.com/file.nbs', - thumbnailUrl : 'http://test.com/thumbnail.nbs', - uploader : user._id - }; - - const songEntity = new SongEntity(); - songEntity.uploader = user._id; - - const songDocument: SongDocument = { - ...songEntity, - ...commonData - } as any; - - songDocument.populate = jest.fn().mockResolvedValue({ - ...songEntity, - ...commonData, - uploader: { username: 'testuser', profileImage: 'testimage' } - } as unknown as SongWithUser); - - songDocument.save = jest.fn().mockResolvedValue(songDocument); - - const populatedSong = { - ...songEntity, - ...commonData, - uploader: { username: 'testuser', profileImage: 'testimage' } - } as unknown as SongWithUser; - - jest - .spyOn(songUploadService, 'processUploadedSong') - .mockResolvedValue(songEntity); - - jest.spyOn(songModel, 'create').mockResolvedValue(songDocument as any); - - const result = await service.uploadSong({ file, user, body }); - - expect(result).toEqual( - UploadSongResponseDto.fromSongWithUserDocument(populatedSong) - ); - - expect(songUploadService.processUploadedSong).toHaveBeenCalledWith({ - file, - user, - body - }); - - expect(songModel.create).toHaveBeenCalledWith(songEntity); - expect(songDocument.save).toHaveBeenCalled(); - - expect(songDocument.populate).toHaveBeenCalledWith( - 'uploader', - 'username profileImage -_id' - ); + let service: SongService; + let fileService: FileService; + let songUploadService: SongUploadService; + let songModel: Model; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SongService, + { + provide : SongWebhookService, + useValue: mockSongWebhookService + }, + { + provide : getModelToken(SongEntity.name), + useValue: mongoose.model(SongEntity.name, SongSchema) + }, + { + provide : FileService, + useValue: mockFileService + }, + { + provide : SongUploadService, + useValue: mockSongUploadService + } + ] + }).compile(); + + service = module.get(SongService); + fileService = module.get(FileService); + songUploadService = module.get(SongUploadService); + songModel = module.get>(getModelToken(SongEntity.name)); }); - }); - - describe('deleteSong', () => { - it('should delete a song', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const songEntity = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: true, - file : 'somebytes', - publicId : 'public-song-id', - createdAt : new Date(), - stats : {} as SongStats, - fileSize : 424242, - packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl : 'http://test.com/file.nbs', - thumbnailUrl : 'http://test.com/thumbnail.nbs', - uploader : user._id - } as unknown as SongEntity; - - const populatedSong = { - ...songEntity, - uploader: { username: 'testuser', profileImage: 'testimage' } - } as unknown as SongWithUser; - - const mockFindOne = { - exec: jest.fn().mockResolvedValue({ - ...songEntity, - populate: jest.fn().mockResolvedValue(populatedSong) - }) - }; - - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); - - jest.spyOn(songModel, 'deleteOne').mockReturnValue({ - exec: jest.fn().mockResolvedValue({}) - } as any); - - jest.spyOn(fileService, 'deleteSong').mockResolvedValue(undefined); - - const result = await service.deleteSong(publicId, user); - - expect(result).toEqual( - UploadSongResponseDto.fromSongWithUserDocument(populatedSong) - ); - - expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); - expect(songModel.deleteOne).toHaveBeenCalledWith({ publicId }); - - expect(fileService.deleteSong).toHaveBeenCalledWith( - songEntity.nbsFileUrl - ); + + it('should be defined', () => { + expect(service).toBeDefined(); }); - it('should throw an error if song is not found', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + describe('uploadSong', () => { + it('should upload a song', async () => { + const file = { buffer: Buffer.from('test') } as Express.Multer.File; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: true, + file : 'somebytes' + }; + + const commonData = { + publicId : 'public-song-id', + createdAt: new Date(), + stats : { + midiFileName : 'test.mid', + noteCount : 100, + tickCount : 1000, + layerCount : 10, + tempo : 120, + tempoRange : [100, 150], + timeSignature : 4, + duration : 60, + loop : true, + loopStartTick : 0, + minutesSpent : 10, + vanillaInstrumentCount : 10, + customInstrumentCount : 0, + firstCustomInstrumentIndex: 0, + outOfRangeNoteCount : 0, + detunedNoteCount : 0, + customInstrumentNoteCount : 0, + incompatibleNoteCount : 0, + compatible : true, + instrumentNoteCounts : [10] + }, + fileSize : 424242, + packedSongUrl: 'http://test.com/packed-file.nbs', + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + uploader : user._id + }; + + const songEntity = new SongEntity(); + songEntity.uploader = user._id; + + const songDocument: SongDocument = { + ...songEntity, + ...commonData + } as any; + + songDocument.populate = jest.fn().mockResolvedValue({ + ...songEntity, + ...commonData, + uploader: { username: 'testuser', profileImage: 'testimage' } + } as unknown as SongWithUser); + + songDocument.save = jest.fn().mockResolvedValue(songDocument); + + const populatedSong = { + ...songEntity, + ...commonData, + uploader: { username: 'testuser', profileImage: 'testimage' } + } as unknown as SongWithUser; + + jest + .spyOn(songUploadService, 'processUploadedSong') + .mockResolvedValue(songEntity); + + jest.spyOn(songModel, 'create').mockResolvedValue(songDocument as any); + + const result = await service.uploadSong({ file, user, body }); + + expect(result).toEqual( + UploadSongResponseDto.fromSongWithUserDocument(populatedSong) + ); + + expect(songUploadService.processUploadedSong).toHaveBeenCalledWith({ + file, + user, + body + }); + + expect(songModel.create).toHaveBeenCalledWith(songEntity); + expect(songDocument.save).toHaveBeenCalled(); + + expect(songDocument.populate).toHaveBeenCalledWith( + 'uploader', + 'username profileImage -_id' + ); + }); + }); - const mockFindOne = { - findOne: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(null) - }; + describe('deleteSong', () => { + it('should delete a song', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const songEntity = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: true, + file : 'somebytes', + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, + packedSongUrl: 'http://test.com/packed-file.nbs', + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + uploader : user._id + } as unknown as SongEntity; + + const populatedSong = { + ...songEntity, + uploader: { username: 'testuser', profileImage: 'testimage' } + } as unknown as SongWithUser; + + const mockFindOne = { + exec: jest.fn().mockResolvedValue({ + ...songEntity, + populate: jest.fn().mockResolvedValue(populatedSong) + }) + }; + + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + + jest.spyOn(songModel, 'deleteOne').mockReturnValue({ + exec: jest.fn().mockResolvedValue({}) + } as any); + + jest.spyOn(fileService, 'deleteSong').mockResolvedValue(undefined); + + const result = await service.deleteSong(publicId, user); + + expect(result).toEqual( + UploadSongResponseDto.fromSongWithUserDocument(populatedSong) + ); + + expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); + expect(songModel.deleteOne).toHaveBeenCalledWith({ publicId }); + + expect(fileService.deleteSong).toHaveBeenCalledWith( + songEntity.nbsFileUrl + ); + }); - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + it('should throw an error if song is not found', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - await expect(service.deleteSong(publicId, user)).rejects.toThrow( - HttpException - ); - }); + const mockFindOne = { + findOne: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(null) + }; - it('should throw an error if user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const songEntity = new SongEntity(); - songEntity.uploader = new mongoose.Types.ObjectId(); // Different uploader + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); - const mockFindOne = { - exec: jest.fn().mockResolvedValue(songEntity) - }; + await expect(service.deleteSong(publicId, user)).rejects.toThrow( + HttpException + ); + }); - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + it('should throw an error if user is unauthorized', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const songEntity = new SongEntity(); + songEntity.uploader = new mongoose.Types.ObjectId(); // Different uploader - await expect(service.deleteSong(publicId, user)).rejects.toThrow( - HttpException - ); - }); + const mockFindOne = { + exec: jest.fn().mockResolvedValue(songEntity) + }; - it('should throw an error if user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const songEntity = new SongEntity(); - songEntity.uploader = new mongoose.Types.ObjectId(); // Different uploader + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); - const mockFindOne = { - exec: jest.fn().mockResolvedValue(songEntity) - }; + await expect(service.deleteSong(publicId, user)).rejects.toThrow( + HttpException + ); + }); - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + it('should throw an error if user is unauthorized', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const songEntity = new SongEntity(); + songEntity.uploader = new mongoose.Types.ObjectId(); // Different uploader - await expect(service.deleteSong(publicId, user)).rejects.toThrow( - HttpException - ); - }); - }); - - describe('patchSong', () => { - it('should patch a song', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: true, - file : 'somebytes' - }; - - const missingData = { - publicId : 'public-song-id', - createdAt: new Date(), - stats : { - midiFileName : 'test.mid', - noteCount : 100, - tickCount : 1000, - layerCount : 10, - tempo : 120, - tempoRange : [100, 150], - timeSignature : 4, - duration : 60, - loop : true, - loopStartTick : 0, - minutesSpent : 10, - vanillaInstrumentCount : 10, - customInstrumentCount : 0, - firstCustomInstrumentIndex: 0, - outOfRangeNoteCount : 0, - detunedNoteCount : 0, - customInstrumentNoteCount : 0, - incompatibleNoteCount : 0, - compatible : true, - instrumentNoteCounts : [10] - }, - fileSize : 424242, - packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl : 'http://test.com/file.nbs', - thumbnailUrl : 'http://test.com/thumbnail.nbs', - uploader : user._id - }; - - const songDocument: SongDocument = { - ...missingData - } as any; - - songDocument.save = jest.fn().mockResolvedValue(songDocument); - - songDocument.populate = jest.fn().mockResolvedValue({ - ...missingData, - uploader: { username: 'testuser', profileImage: 'testimage' } - }); - - const populatedSong = { - ...missingData, - uploader: { username: 'testuser', profileImage: 'testimage' } - }; - - jest.spyOn(songModel, 'findOne').mockResolvedValue(songDocument); - - jest - .spyOn(songUploadService, 'processSongPatch') - .mockResolvedValue(undefined); - - const result = await service.patchSong(publicId, body, user); - - expect(result).toEqual( - UploadSongResponseDto.fromSongWithUserDocument(populatedSong as any) - ); - - expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); - - expect(songUploadService.processSongPatch).toHaveBeenCalledWith( - songDocument, - body, - user - ); - - expect(songDocument.save).toHaveBeenCalled(); - - expect(songDocument.populate).toHaveBeenCalledWith( - 'uploader', - 'username profileImage -_id' - ); - }, 10000); // Increase the timeout to 10000 ms - - it('should throw an error if song is not found', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - file : 'somebytes', - allowDownload: false - }; - - jest.spyOn(songModel, 'findOne').mockReturnValue(null as any); - - await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException - ); - }); + const mockFindOne = { + exec: jest.fn().mockResolvedValue(songEntity) + }; - it('should throw an error if user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - file : 'somebytes', - allowDownload: false - }; - - const songEntity = { - uploader: 'different-user-id' - } as any; - - jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); - - await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException - ); - }); + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); - it('should throw an error if user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const body: UploadSongDto = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - file : 'somebytes', - allowDownload: false - }; - - const songEntity = { - uploader: 'different-user-id' - } as any; - - jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity); - - await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException - ); + await expect(service.deleteSong(publicId, user)).rejects.toThrow( + HttpException + ); + }); }); - it('should throw an error if user no changes are provided', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const body: UploadSongDto = { - file : undefined, - allowDownload : false, - visibility : 'public', - title : '', - originalAuthor: '', - description : '', - category : 'pop', - thumbnailData : { - backgroundColor: '#000000', - startLayer : 0, - startTick : 0, - zoomLevel : 1 - }, - license : 'standard', - customInstruments: [] - }; - - const songEntity = { - uploader : user._id, - file : undefined, - allowDownload : false, - visibility : 'public', - title : '', - originalAuthor: '', - description : '', - category : 'pop', - thumbnailData : { - backgroundColor: '#000000', - startLayer : 0, - startTick : 0, - zoomLevel : 1 - }, - license : 'standard', - customInstruments: [] - } as any; - - jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); - - await expect(service.patchSong(publicId, body, user)).rejects.toThrow( - HttpException - ); + describe('patchSong', () => { + it('should patch a song', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: true, + file : 'somebytes' + }; + + const missingData = { + publicId : 'public-song-id', + createdAt: new Date(), + stats : { + midiFileName : 'test.mid', + noteCount : 100, + tickCount : 1000, + layerCount : 10, + tempo : 120, + tempoRange : [100, 150], + timeSignature : 4, + duration : 60, + loop : true, + loopStartTick : 0, + minutesSpent : 10, + vanillaInstrumentCount : 10, + customInstrumentCount : 0, + firstCustomInstrumentIndex: 0, + outOfRangeNoteCount : 0, + detunedNoteCount : 0, + customInstrumentNoteCount : 0, + incompatibleNoteCount : 0, + compatible : true, + instrumentNoteCounts : [10] + }, + fileSize : 424242, + packedSongUrl: 'http://test.com/packed-file.nbs', + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + uploader : user._id + }; + + const songDocument: SongDocument = { + ...missingData + } as any; + + songDocument.save = jest.fn().mockResolvedValue(songDocument); + + songDocument.populate = jest.fn().mockResolvedValue({ + ...missingData, + uploader: { username: 'testuser', profileImage: 'testimage' } + }); + + const populatedSong = { + ...missingData, + uploader: { username: 'testuser', profileImage: 'testimage' } + }; + + jest.spyOn(songModel, 'findOne').mockResolvedValue(songDocument); + + jest + .spyOn(songUploadService, 'processSongPatch') + .mockResolvedValue(undefined); + + const result = await service.patchSong(publicId, body, user); + + expect(result).toEqual( + UploadSongResponseDto.fromSongWithUserDocument(populatedSong as any) + ); + + expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); + + expect(songUploadService.processSongPatch).toHaveBeenCalledWith( + songDocument, + body, + user + ); + + expect(songDocument.save).toHaveBeenCalled(); + + expect(songDocument.populate).toHaveBeenCalledWith( + 'uploader', + 'username profileImage -_id' + ); + }, 10000); // Increase the timeout to 10000 ms + + it('should throw an error if song is not found', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + file : 'somebytes', + allowDownload: false + }; + + jest.spyOn(songModel, 'findOne').mockReturnValue(null as any); + + await expect(service.patchSong(publicId, body, user)).rejects.toThrow( + HttpException + ); + }); + + it('should throw an error if user is unauthorized', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + file : 'somebytes', + allowDownload: false + }; + + const songEntity = { + uploader: 'different-user-id' + } as any; + + jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); + + await expect(service.patchSong(publicId, body, user)).rejects.toThrow( + HttpException + ); + }); + + it('should throw an error if user is unauthorized', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const body: UploadSongDto = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + file : 'somebytes', + allowDownload: false + }; + + const songEntity = { + uploader: 'different-user-id' + } as any; + + jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity); + + await expect(service.patchSong(publicId, body, user)).rejects.toThrow( + HttpException + ); + }); + + it('should throw an error if user no changes are provided', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const body: UploadSongDto = { + file : undefined, + allowDownload : false, + visibility : 'public', + title : '', + originalAuthor: '', + description : '', + category : 'pop', + thumbnailData : { + backgroundColor: '#000000', + startLayer : 0, + startTick : 0, + zoomLevel : 1 + }, + license : 'standard', + customInstruments: [] + }; + + const songEntity = { + uploader : user._id, + file : undefined, + allowDownload : false, + visibility : 'public', + title : '', + originalAuthor: '', + description : '', + category : 'pop', + thumbnailData : { + backgroundColor: '#000000', + startLayer : 0, + startTick : 0, + zoomLevel : 1 + }, + license : 'standard', + customInstruments: [] + } as any; + + jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); + + await expect(service.patchSong(publicId, body, user)).rejects.toThrow( + HttpException + ); + }); }); - }); - describe('getSongByPage', () => { - it('should return a list of songs by page', async () => { - const query = { - page : 1, - limit: 10, - sort : 'createdAt', - order: true - }; + describe('getSongByPage', () => { + it('should return a list of songs by page', async () => { + const query = { + page : 1, + limit: 10, + sort : 'createdAt', + order: true + }; - const songList: SongWithUser[] = []; + const songList: SongWithUser[] = []; - const mockFind = { - sort : jest.fn().mockReturnThis(), - skip : jest.fn().mockReturnThis(), - limit : jest.fn().mockReturnThis(), - populate: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(songList) - }; + const mockFind = { + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit : jest.fn().mockReturnThis(), + populate: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(songList) + }; - jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); + jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); - const result = await service.getSongByPage(query); + const result = await service.getSongByPage(query); - expect(result).toEqual( - songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)) - ); + expect(result).toEqual( + songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)) + ); - expect(songModel.find).toHaveBeenCalledWith({ visibility: 'public' }); - expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: 1 }); + expect(songModel.find).toHaveBeenCalledWith({ visibility: 'public' }); + expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: 1 }); - expect(mockFind.skip).toHaveBeenCalledWith( - query.page * query.limit - query.limit - ); + expect(mockFind.skip).toHaveBeenCalledWith( + query.page * query.limit - query.limit + ); - expect(mockFind.limit).toHaveBeenCalledWith(query.limit); - expect(mockFind.exec).toHaveBeenCalled(); - }); + expect(mockFind.limit).toHaveBeenCalledWith(query.limit); + expect(mockFind.exec).toHaveBeenCalled(); + }); - it('should throw an error if the query is invalid', async () => { - const query = { - page : undefined, - limit: undefined, - sort : undefined, - order: true - }; + it('should throw an error if the query is invalid', async () => { + const query = { + page : undefined, + limit: undefined, + sort : undefined, + order: true + }; - const songList: SongWithUser[] = []; + const songList: SongWithUser[] = []; - const mockFind = { - sort : jest.fn().mockReturnThis(), - skip : jest.fn().mockReturnThis(), - limit : jest.fn().mockReturnThis(), - populate: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(songList) - }; + const mockFind = { + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit : jest.fn().mockReturnThis(), + populate: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(songList) + }; - jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); + jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); - expect(service.getSongByPage(query)).rejects.toThrow(HttpException); - }); - }); - - describe('getSong', () => { - it('should return song info by ID', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const songDocument = { - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - file : 'somebytes', - allowDownload: false, - uploader : {}, - save : jest.fn() - } as any; - - songDocument.save = jest.fn().mockResolvedValue(songDocument); - - const mockFindOne = { - populate: jest.fn().mockResolvedValue(songDocument), - ...songDocument - }; - - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); - - const result = await service.getSong(publicId, user); - - expect(result).toEqual(SongViewDto.fromSongDocument(songDocument)); - expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); + expect(service.getSongByPage(query)).rejects.toThrow(HttpException); + }); }); - it('should throw an error if song is not found', async () => { - const publicId = 'test-id'; + describe('getSong', () => { + it('should return song info by ID', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const songDocument = { + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + file : 'somebytes', + allowDownload: false, + uploader : {}, + save : jest.fn() + } as any; + + songDocument.save = jest.fn().mockResolvedValue(songDocument); + + const mockFindOne = { + populate: jest.fn().mockResolvedValue(songDocument), + ...songDocument + }; + + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + + const result = await service.getSong(publicId, user); + + expect(result).toEqual(SongViewDto.fromSongDocument(songDocument)); + expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); + }); - const user: UserDocument = { - _id: 'test-user-id' - } as unknown as UserDocument; + it('should throw an error if song is not found', async () => { + const publicId = 'test-id'; - const mockFindOne = null; + const user: UserDocument = { + _id: 'test-user-id' + } as unknown as UserDocument; - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + const mockFindOne = null; - await expect(service.getSong(publicId, user)).rejects.toThrow( - HttpException - ); - }); + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); - it('should throw an error if song is private and user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + await expect(service.getSong(publicId, user)).rejects.toThrow( + HttpException + ); + }); - const songEntity = { - publicId : 'test-public-id', - visibility: 'private', - uploader : 'different-user-id' - }; + it('should throw an error if song is private and user is unauthorized', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); + const songEntity = { + publicId : 'test-public-id', + visibility: 'private', + uploader : 'different-user-id' + }; - expect(service.getSong(publicId, user)).rejects.toThrow(HttpException); - }); + jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); - it('should throw an error if song is private and user is not logged in', async () => { - const publicId = 'test-id'; - const user: UserDocument = null as any; + expect(service.getSong(publicId, user)).rejects.toThrow(HttpException); + }); - const songEntity = { - publicId : 'test-public-id', - visibility: 'private', - uploader : 'different-user-id' - }; + it('should throw an error if song is private and user is not logged in', async () => { + const publicId = 'test-id'; + const user: UserDocument = null as any; - jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); - expect(service.getSong(publicId, user)).rejects.toThrow(HttpException); - }); - }); - - describe('getSongDownloadUrl', () => { - it('should return song download URL', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const songEntity = { - visibility : 'public', - uploader : 'test-user-id', - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: true, - publicId : 'public-song-id', - createdAt : new Date(), - stats : {} as SongStats, - fileSize : 424242, - packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl : 'http://test.com/file.nbs', - thumbnailUrl : 'http://test.com/thumbnail.nbs', - save : jest.fn() - }; - - const url = 'http://test.com/song.nbs'; - - jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); - jest.spyOn(fileService, 'getSongDownloadUrl').mockResolvedValue(url); - - const result = await service.getSongDownloadUrl(publicId, user); - - expect(result).toEqual(url); - expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); - - expect(fileService.getSongDownloadUrl).toHaveBeenCalledWith( - songEntity.nbsFileUrl, - `${songEntity.title}.nbs` - ); + const songEntity = { + publicId : 'test-public-id', + visibility: 'private', + uploader : 'different-user-id' + }; + + jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); + expect(service.getSong(publicId, user)).rejects.toThrow(HttpException); + }); }); - it('should throw an error if song is not found', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + describe('getSongDownloadUrl', () => { + it('should return song download URL', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const songEntity = { + visibility : 'public', + uploader : 'test-user-id', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: true, + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, + packedSongUrl: 'http://test.com/packed-file.nbs', + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + save : jest.fn() + }; + + const url = 'http://test.com/song.nbs'; + + jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); + jest.spyOn(fileService, 'getSongDownloadUrl').mockResolvedValue(url); + + const result = await service.getSongDownloadUrl(publicId, user); + + expect(result).toEqual(url); + expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); + + expect(fileService.getSongDownloadUrl).toHaveBeenCalledWith( + songEntity.nbsFileUrl, + `${songEntity.title}.nbs` + ); + }); - jest.spyOn(songModel, 'findOne').mockResolvedValue(null); + it('should throw an error if song is not found', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException - ); - }); + jest.spyOn(songModel, 'findOne').mockResolvedValue(null); - it('should throw an error if song is private and user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( + HttpException + ); + }); - const songEntity = { - visibility: 'private', - uploader : 'different-user-id' - }; + it('should throw an error if song is private and user is unauthorized', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); + const songEntity = { + visibility: 'private', + uploader : 'different-user-id' + }; - await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException - ); - }); + jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); - it('should throw an error if no packed song URL is available and allowDownload is false', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const songEntity = { - visibility : 'public', - uploader : 'test-user-id', - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: false, - publicId : 'public-song-id', - createdAt : new Date(), - stats : {} as SongStats, - fileSize : 424242, - packedSongUrl: undefined, - nbsFileUrl : 'http://test.com/file.nbs', - thumbnailUrl : 'http://test.com/thumbnail.nbs', - save : jest.fn() - }; - - jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); - - await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException - ); - }); + await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( + HttpException + ); + }); - it('should throw an error in case of an internal error in fileService', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + it('should throw an error if no packed song URL is available and allowDownload is false', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const songEntity = { + visibility : 'public', + uploader : 'test-user-id', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: false, + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, + packedSongUrl: undefined, + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + save : jest.fn() + }; + + jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); + + await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( + HttpException + ); + }); - jest - .spyOn(fileService, 'getSongDownloadUrl') - .mockImplementationOnce(() => { - throw new Error('Internal error'); + it('should throw an error in case of an internal error in fileService', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + jest + .spyOn(fileService, 'getSongDownloadUrl') + .mockImplementationOnce(() => { + throw new Error('Internal error'); + }); + + await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( + HttpException + ); }); - await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException - ); + it('should throw an error in case of an internal error on saving the song', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const songEntity = { + visibility : 'public', + uploader : 'test-user-id', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: true, + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, + packedSongUrl: 'http://test.com/packed-file.nbs', + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs', + save : jest.fn().mockImplementationOnce(() => { + throw new Error('Error saving song'); + }) + }; + + jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); + + jest + .spyOn(fileService, 'getSongDownloadUrl') + .mockResolvedValue('http://test.com/song.nbs'); + + await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( + HttpException + ); + }); }); - it('should throw an error in case of an internal error on saving the song', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const songEntity = { - visibility : 'public', - uploader : 'test-user-id', - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: true, - publicId : 'public-song-id', - createdAt : new Date(), - stats : {} as SongStats, - fileSize : 424242, - packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl : 'http://test.com/file.nbs', - thumbnailUrl : 'http://test.com/thumbnail.nbs', - save : jest.fn().mockImplementationOnce(() => { - throw new Error('Error saving song'); - }) - }; - - jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity); - - jest - .spyOn(fileService, 'getSongDownloadUrl') - .mockResolvedValue('http://test.com/song.nbs'); - - await expect(service.getSongDownloadUrl(publicId, user)).rejects.toThrow( - HttpException - ); - }); - }); - - describe('getMySongsPage', () => { - it('should return a list of songs uploaded by the user', async () => { - const query = { - page : 1, - limit: 10, - sort : 'createdAt', - order: true - }; - - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const songList: SongWithUser[] = []; - - const mockFind = { - sort : jest.fn().mockReturnThis(), - skip : jest.fn().mockReturnThis(), - limit: jest.fn().mockResolvedValue(songList) - }; - - jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); - jest.spyOn(songModel, 'countDocuments').mockResolvedValue(0); - - const result = await service.getMySongsPage({ query, user }); - - expect(result).toEqual({ - content: songList.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song) - ), - page : 1, - limit: 10, - total: 0 - }); - - expect(songModel.find).toHaveBeenCalledWith({ uploader: user._id }); - expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: 1 }); - - expect(mockFind.skip).toHaveBeenCalledWith( - query.page * query.limit - query.limit - ); - - expect(mockFind.limit).toHaveBeenCalledWith(query.limit); - - expect(songModel.countDocuments).toHaveBeenCalledWith({ - uploader: user._id - }); + describe('getMySongsPage', () => { + it('should return a list of songs uploaded by the user', async () => { + const query = { + page : 1, + limit: 10, + sort : 'createdAt', + order: true + }; + + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const songList: SongWithUser[] = []; + + const mockFind = { + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit: jest.fn().mockResolvedValue(songList) + }; + + jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); + jest.spyOn(songModel, 'countDocuments').mockResolvedValue(0); + + const result = await service.getMySongsPage({ query, user }); + + expect(result).toEqual({ + content: songList.map((song) => + SongPreviewDto.fromSongDocumentWithUser(song) + ), + page : 1, + limit: 10, + total: 0 + }); + + expect(songModel.find).toHaveBeenCalledWith({ uploader: user._id }); + expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: 1 }); + + expect(mockFind.skip).toHaveBeenCalledWith( + query.page * query.limit - query.limit + ); + + expect(mockFind.limit).toHaveBeenCalledWith(query.limit); + + expect(songModel.countDocuments).toHaveBeenCalledWith({ + uploader: user._id + }); + }); }); - }); - describe('getSongEdit', () => { - it('should return song info for editing by ID', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const songEntity = new SongEntity(); - songEntity.uploader = user._id; // Ensure uploader is set + describe('getSongEdit', () => { + it('should return song info for editing by ID', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const songEntity = new SongEntity(); + songEntity.uploader = user._id; // Ensure uploader is set - const mockFindOne = { - exec : jest.fn().mockResolvedValue(songEntity), - populate: jest.fn().mockReturnThis() - }; + const mockFindOne = { + exec : jest.fn().mockResolvedValue(songEntity), + populate: jest.fn().mockReturnThis() + }; - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); - const result = await service.getSongEdit(publicId, user); + const result = await service.getSongEdit(publicId, user); - expect(result).toEqual(UploadSongDto.fromSongDocument(songEntity as any)); + expect(result).toEqual(UploadSongDto.fromSongDocument(songEntity as any)); - expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); - }); + expect(songModel.findOne).toHaveBeenCalledWith({ publicId }); + }); - it('should throw an error if song is not found', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + it('should throw an error if song is not found', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const findOneMock = { - findOne: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(null) - }; + const findOneMock = { + findOne: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(null) + }; - jest.spyOn(songModel, 'findOne').mockReturnValue(findOneMock as any); + jest.spyOn(songModel, 'findOne').mockReturnValue(findOneMock as any); - await expect(service.getSongEdit(publicId, user)).rejects.toThrow( - HttpException - ); - }); + await expect(service.getSongEdit(publicId, user)).rejects.toThrow( + HttpException + ); + }); - it('should throw an error if user is unauthorized', async () => { - const publicId = 'test-id'; - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - - const songEntity = { - uploader : 'different-user-id', - title : 'Test Song', - originalAuthor : 'Test Author', - description : 'Test Description', - category : 'alternative', - visibility : 'public', - license : 'standard', - customInstruments: [], - thumbnailData : { - startTick : 0, - startLayer : 0, - zoomLevel : 1, - backgroundColor: '#000000' - }, - allowDownload: true, - publicId : 'public-song-id', - createdAt : new Date(), - stats : {} as SongStats, - fileSize : 424242, - packedSongUrl: 'http://test.com/packed-file.nbs', - nbsFileUrl : 'http://test.com/file.nbs', - thumbnailUrl : 'http://test.com/thumbnail.nbs' - } as unknown as SongEntity; - - const findOneMock = { - findOne: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(songEntity) - }; - - jest.spyOn(songModel, 'findOne').mockReturnValue(findOneMock as any); - - await expect(service.getSongEdit(publicId, user)).rejects.toThrow( - HttpException - ); + it('should throw an error if user is unauthorized', async () => { + const publicId = 'test-id'; + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + + const songEntity = { + uploader : 'different-user-id', + title : 'Test Song', + originalAuthor : 'Test Author', + description : 'Test Description', + category : 'alternative', + visibility : 'public', + license : 'standard', + customInstruments: [], + thumbnailData : { + startTick : 0, + startLayer : 0, + zoomLevel : 1, + backgroundColor: '#000000' + }, + allowDownload: true, + publicId : 'public-song-id', + createdAt : new Date(), + stats : {} as SongStats, + fileSize : 424242, + packedSongUrl: 'http://test.com/packed-file.nbs', + nbsFileUrl : 'http://test.com/file.nbs', + thumbnailUrl : 'http://test.com/thumbnail.nbs' + } as unknown as SongEntity; + + const findOneMock = { + findOne: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(songEntity) + }; + + jest.spyOn(songModel, 'findOne').mockReturnValue(findOneMock as any); + + await expect(service.getSongEdit(publicId, user)).rejects.toThrow( + HttpException + ); + }); }); - }); - describe('getCategories', () => { - it('should return a list of song categories and their counts', async () => { - const categories = [ - { _id: 'category1', count: 10 }, - { _id: 'category2', count: 5 } - ]; + describe('getCategories', () => { + it('should return a list of song categories and their counts', async () => { + const categories = [ + { _id: 'category1', count: 10 }, + { _id: 'category2', count: 5 } + ]; - jest.spyOn(songModel, 'aggregate').mockResolvedValue(categories); + jest.spyOn(songModel, 'aggregate').mockResolvedValue(categories); - const result = await service.getCategories(); + const result = await service.getCategories(); - expect(result).toEqual({ category1: 10, category2: 5 }); + expect(result).toEqual({ category1: 10, category2: 5 }); - expect(songModel.aggregate).toHaveBeenCalledWith([ - { $match: { visibility: 'public' } }, - { $group: { _id: '$category', count: { $sum: 1 } } }, - { $sort: { count: -1 } } - ]); + expect(songModel.aggregate).toHaveBeenCalledWith([ + { $match: { visibility: 'public' } }, + { $group: { _id: '$category', count: { $sum: 1 } } }, + { $sort: { count: -1 } } + ]); + }); }); - }); - - describe('getSongsByCategory', () => { - it('should return a list of songs by category', async () => { - const category = 'test-category'; - const page = 1; - const limit = 10; - const songList: SongWithUser[] = []; - - const mockFind = { - sort : jest.fn().mockReturnThis(), - skip : jest.fn().mockReturnThis(), - limit : jest.fn().mockReturnThis(), - populate: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(songList) - }; - - jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); - - const result = await service.getSongsByCategory(category, page, limit); - - expect(result).toEqual( - songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)) - ); - - expect(songModel.find).toHaveBeenCalledWith({ - category, - visibility: 'public' - }); - - expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: -1 }); - expect(mockFind.skip).toHaveBeenCalledWith(page * limit - limit); - expect(mockFind.limit).toHaveBeenCalledWith(limit); - - expect(mockFind.populate).toHaveBeenCalledWith( - 'uploader', - 'username profileImage -_id' - ); - - expect(mockFind.exec).toHaveBeenCalled(); + + describe('getSongsByCategory', () => { + it('should return a list of songs by category', async () => { + const category = 'test-category'; + const page = 1; + const limit = 10; + const songList: SongWithUser[] = []; + + const mockFind = { + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit : jest.fn().mockReturnThis(), + populate: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(songList) + }; + + jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any); + + const result = await service.getSongsByCategory(category, page, limit); + + expect(result).toEqual( + songList.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)) + ); + + expect(songModel.find).toHaveBeenCalledWith({ + category, + visibility: 'public' + }); + + expect(mockFind.sort).toHaveBeenCalledWith({ createdAt: -1 }); + expect(mockFind.skip).toHaveBeenCalledWith(page * limit - limit); + expect(mockFind.limit).toHaveBeenCalledWith(limit); + + expect(mockFind.populate).toHaveBeenCalledWith( + 'uploader', + 'username profileImage -_id' + ); + + expect(mockFind.exec).toHaveBeenCalled(); + }); }); - }); }); diff --git a/apps/backend/src/song/song.service.ts b/apps/backend/src/song/song.service.ts index c8ad1aee..74cf6735 100644 --- a/apps/backend/src/song/song.service.ts +++ b/apps/backend/src/song/song.service.ts @@ -1,21 +1,21 @@ import { BROWSER_SONGS } from '@nbw/config'; import type { UserDocument } from '@nbw/database'; import { - PageQueryDTO, - Song as SongEntity, - SongPageDto, - SongPreviewDto, - SongViewDto, - SongWithUser, - UploadSongDto, - UploadSongResponseDto + PageQueryDTO, + Song as SongEntity, + SongPageDto, + SongPreviewDto, + SongViewDto, + SongWithUser, + UploadSongDto, + UploadSongResponseDto } from '@nbw/database'; import { - HttpException, - HttpStatus, - Inject, - Injectable, - Logger + HttpException, + HttpStatus, + Inject, + Injectable, + Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; @@ -28,112 +28,112 @@ import { removeExtraSpaces } from './song.util'; @Injectable() export class SongService { - private logger = new Logger(SongService.name); - constructor( - @InjectModel(SongEntity.name) - private songModel: Model, - - @Inject(FileService) - private fileService: FileService, - - @Inject(SongUploadService) - private songUploadService: SongUploadService, - - @Inject(SongWebhookService) - private songWebhookService: SongWebhookService - ) {} - - public async getSongById(publicId: string) { - return this.songModel.findOne({ - publicId - }); - } - - public async uploadSong({ - file, - user, - body - }: { - body: UploadSongDto; - file: Express.Multer.File; - user: UserDocument; - }): Promise { - const song = await this.songUploadService.processUploadedSong({ - file, - user, - body - }); - - // Create song document - const songDocument = await this.songModel.create(song); - - // Post Discord webhook - const populatedSong = (await songDocument.populate( - 'uploader', - 'username profileImage -_id' - )) as unknown as SongWithUser; - - const webhookMessageId = await this.songWebhookService.syncSongWebhook( - populatedSong - ); - - songDocument.webhookMessageId = webhookMessageId; - - // Save song document - await songDocument.save(); - - return UploadSongResponseDto.fromSongWithUserDocument(populatedSong); - } - - public async deleteSong( - publicId: string, - user: UserDocument - ): Promise { - const foundSong = await this.songModel - .findOne({ publicId: publicId }) - .exec(); - - if (!foundSong) { - throw new HttpException('Song not found', HttpStatus.NOT_FOUND); + private logger = new Logger(SongService.name); + constructor( + @InjectModel(SongEntity.name) + private songModel: Model, + + @Inject(FileService) + private fileService: FileService, + + @Inject(SongUploadService) + private songUploadService: SongUploadService, + + @Inject(SongWebhookService) + private songWebhookService: SongWebhookService + ) {} + + public async getSongById(publicId: string) { + return this.songModel.findOne({ + publicId + }); } - if (foundSong.uploader.toString() !== user?._id.toString()) { - throw new HttpException('Song not found', HttpStatus.UNAUTHORIZED); + public async uploadSong({ + file, + user, + body + }: { + body: UploadSongDto; + file: Express.Multer.File; + user: UserDocument; + }): Promise { + const song = await this.songUploadService.processUploadedSong({ + file, + user, + body + }); + + // Create song document + const songDocument = await this.songModel.create(song); + + // Post Discord webhook + const populatedSong = (await songDocument.populate( + 'uploader', + 'username profileImage -_id' + )) as unknown as SongWithUser; + + const webhookMessageId = await this.songWebhookService.syncSongWebhook( + populatedSong + ); + + songDocument.webhookMessageId = webhookMessageId; + + // Save song document + await songDocument.save(); + + return UploadSongResponseDto.fromSongWithUserDocument(populatedSong); } - await this.songModel.deleteOne({ publicId: publicId }).exec(); + public async deleteSong( + publicId: string, + user: UserDocument + ): Promise { + const foundSong = await this.songModel + .findOne({ publicId: publicId }) + .exec(); - await this.fileService.deleteSong(foundSong.nbsFileUrl); + if (!foundSong) { + throw new HttpException('Song not found', HttpStatus.NOT_FOUND); + } + + if (foundSong.uploader.toString() !== user?._id.toString()) { + throw new HttpException('Song not found', HttpStatus.UNAUTHORIZED); + } - const populatedSong = (await foundSong.populate( - 'uploader', - 'username profileImage -_id' - )) as unknown as SongWithUser; + await this.songModel.deleteOne({ publicId: publicId }).exec(); - await this.songWebhookService.deleteSongWebhook(populatedSong); + await this.fileService.deleteSong(foundSong.nbsFileUrl); - return UploadSongResponseDto.fromSongWithUserDocument(populatedSong); - } + const populatedSong = (await foundSong.populate( + 'uploader', + 'username profileImage -_id' + )) as unknown as SongWithUser; - public async patchSong( - publicId: string, - body: UploadSongDto, - user: UserDocument - ): Promise { - const foundSong = await this.songModel.findOne({ - publicId: publicId - }); + await this.songWebhookService.deleteSongWebhook(populatedSong); - if (!foundSong) { - throw new HttpException('Song not found', HttpStatus.NOT_FOUND); + return UploadSongResponseDto.fromSongWithUserDocument(populatedSong); } - if (foundSong.uploader.toString() !== user?._id.toString()) { - throw new HttpException('Song not found', HttpStatus.UNAUTHORIZED); - } + public async patchSong( + publicId: string, + body: UploadSongDto, + user: UserDocument + ): Promise { + const foundSong = await this.songModel.findOne({ + publicId: publicId + }); + + if (!foundSong) { + throw new HttpException('Song not found', HttpStatus.NOT_FOUND); + } + + if (foundSong.uploader.toString() !== user?._id.toString()) { + throw new HttpException('Song not found', HttpStatus.UNAUTHORIZED); + } - if ( - foundSong.title === body.title && + if ( + foundSong.title === body.title && foundSong.originalAuthor === body.originalAuthor && foundSong.description === body.description && foundSong.category === body.category && @@ -144,338 +144,338 @@ export class SongService { JSON.stringify(body.thumbnailData) && JSON.stringify(foundSong.customInstruments) === JSON.stringify(body.customInstruments) - ) { - throw new HttpException('No changes detected', HttpStatus.BAD_REQUEST); - } + ) { + throw new HttpException('No changes detected', HttpStatus.BAD_REQUEST); + } - // Check if updates to the song files and/or thumbnail are necessary; - // if so, update and reupload them - await this.songUploadService.processSongPatch(foundSong, body, user); - - // Update song document - foundSong.title = removeExtraSpaces(body.title); - foundSong.originalAuthor = removeExtraSpaces(body.originalAuthor); - foundSong.description = removeExtraSpaces(body.description); - foundSong.category = body.category; - foundSong.allowDownload = body.allowDownload; - foundSong.visibility = body.visibility; - foundSong.license = body.license; - foundSong.thumbnailData = body.thumbnailData; - foundSong.customInstruments = body.customInstruments; - - // Update document's last update time - foundSong.updatedAt = new Date(); - - const populatedSong = (await foundSong.populate( - 'uploader', - 'username profileImage -_id' - )) as unknown as SongWithUser; - - const webhookMessageId = await this.songWebhookService.syncSongWebhook( - populatedSong - ); - - foundSong.webhookMessageId = webhookMessageId; - - // Save song document - await foundSong.save(); - - return UploadSongResponseDto.fromSongWithUserDocument(populatedSong); - } - - public async getSongByPage(query: PageQueryDTO): Promise { - const { page, limit, sort, order } = query; - - if (!page || !limit || !sort) { - throw new HttpException( - 'Invalid query parameters', - HttpStatus.BAD_REQUEST - ); - } + // Check if updates to the song files and/or thumbnail are necessary; + // if so, update and reupload them + await this.songUploadService.processSongPatch(foundSong, body, user); + + // Update song document + foundSong.title = removeExtraSpaces(body.title); + foundSong.originalAuthor = removeExtraSpaces(body.originalAuthor); + foundSong.description = removeExtraSpaces(body.description); + foundSong.category = body.category; + foundSong.allowDownload = body.allowDownload; + foundSong.visibility = body.visibility; + foundSong.license = body.license; + foundSong.thumbnailData = body.thumbnailData; + foundSong.customInstruments = body.customInstruments; + + // Update document's last update time + foundSong.updatedAt = new Date(); + + const populatedSong = (await foundSong.populate( + 'uploader', + 'username profileImage -_id' + )) as unknown as SongWithUser; + + const webhookMessageId = await this.songWebhookService.syncSongWebhook( + populatedSong + ); - const songs = (await this.songModel - .find({ - visibility: 'public' - }) - .sort({ - [sort]: order ? 1 : -1 - }) - .skip(page * limit - limit) - .limit(limit) - .populate('uploader', 'username profileImage -_id') - .exec()) as unknown as SongWithUser[]; - - return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); - } - - public async getRecentSongs( - page: number, - limit: number - ): Promise { - const queryObject: any = { - visibility: 'public' - }; - - const data = (await this.songModel - .find(queryObject) - .sort({ - createdAt: -1 - }) - .skip(page * limit - limit) - .limit(limit) - .populate('uploader', 'username profileImage -_id') - .exec()) as unknown as SongWithUser[]; - - return data.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); - } - - public async getSongsForTimespan(timespan: number): Promise { - return this.songModel - .find({ - visibility: 'public', - createdAt : { - $gte: timespan - } - }) - .sort({ playCount: -1 }) - .limit(BROWSER_SONGS.featuredPageSize) - .populate('uploader', 'username profileImage -_id') - .exec(); - } - - public async getSongsBeforeTimespan( - timespan: number - ): Promise { - return this.songModel - .find({ - visibility: 'public', - createdAt : { - $lt: timespan - } - }) - .sort({ createdAt: -1 }) - .limit(BROWSER_SONGS.featuredPageSize) - .populate('uploader', 'username profileImage -_id') - .exec(); - } - - public async getSong( - publicId: string, - user: UserDocument | null - ): Promise { - const foundSong = await this.songModel.findOne({ publicId: publicId }); - - if (!foundSong) { - throw new HttpException('Song not found', HttpStatus.NOT_FOUND); + foundSong.webhookMessageId = webhookMessageId; + + // Save song document + await foundSong.save(); + + return UploadSongResponseDto.fromSongWithUserDocument(populatedSong); } - if (foundSong.visibility === 'private') { - if (!user) { - throw new HttpException('Song not found', HttpStatus.NOT_FOUND); - } + public async getSongByPage(query: PageQueryDTO): Promise { + const { page, limit, sort, order } = query; + + if (!page || !limit || !sort) { + throw new HttpException( + 'Invalid query parameters', + HttpStatus.BAD_REQUEST + ); + } - if (foundSong.uploader.toString() !== user._id.toString()) { - throw new HttpException('Song not found', HttpStatus.NOT_FOUND); - } + const songs = (await this.songModel + .find({ + visibility: 'public' + }) + .sort({ + [sort]: order ? 1 : -1 + }) + .skip(page * limit - limit) + .limit(limit) + .populate('uploader', 'username profileImage -_id') + .exec()) as unknown as SongWithUser[]; + + return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); } - // increment view count - foundSong.playCount++; - await foundSong.save(); - - const populatedSong = await foundSong.populate( - 'uploader', - 'username profileImage -_id' - ); - - return SongViewDto.fromSongDocument(populatedSong); - } - - // TODO: service should not handle HTTP -> https://www.reddit.com/r/node/comments/uoicw1/should_i_return_status_code_from_service_layer/ - public async getSongDownloadUrl( - publicId: string, - user: UserDocument | null, - src?: string, - packed: boolean = false - ): Promise { - const foundSong = await this.songModel.findOne({ publicId: publicId }); - - if (!foundSong) { - throw new HttpException('Song not found with ID', HttpStatus.NOT_FOUND); + public async getRecentSongs( + page: number, + limit: number + ): Promise { + const queryObject: any = { + visibility: 'public' + }; + + const data = (await this.songModel + .find(queryObject) + .sort({ + createdAt: -1 + }) + .skip(page * limit - limit) + .limit(limit) + .populate('uploader', 'username profileImage -_id') + .exec()) as unknown as SongWithUser[]; + + return data.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); } - if (foundSong.visibility !== 'public') { - if (!user || foundSong.uploader.toString() !== user._id.toString()) { - throw new HttpException( - 'This song is private', - HttpStatus.UNAUTHORIZED - ); - } + public async getSongsForTimespan(timespan: number): Promise { + return this.songModel + .find({ + visibility: 'public', + createdAt : { + $gte: timespan + } + }) + .sort({ playCount: -1 }) + .limit(BROWSER_SONGS.featuredPageSize) + .populate('uploader', 'username profileImage -_id') + .exec(); } - if (!packed && !foundSong.allowDownload) { - throw new HttpException( - 'The uploader has disabled downloads of this song', - HttpStatus.UNAUTHORIZED - ); + public async getSongsBeforeTimespan( + timespan: number + ): Promise { + return this.songModel + .find({ + visibility: 'public', + createdAt : { + $lt: timespan + } + }) + .sort({ createdAt: -1 }) + .limit(BROWSER_SONGS.featuredPageSize) + .populate('uploader', 'username profileImage -_id') + .exec(); } - const fileKey = packed ? foundSong.packedSongUrl : foundSong.nbsFileUrl; - const fileExt = packed ? '.zip' : '.nbs'; + public async getSong( + publicId: string, + user: UserDocument | null + ): Promise { + const foundSong = await this.songModel.findOne({ publicId: publicId }); - const fileName = `${foundSong.title}${fileExt}`; + if (!foundSong) { + throw new HttpException('Song not found', HttpStatus.NOT_FOUND); + } - try { - const url = await this.fileService.getSongDownloadUrl(fileKey, fileName); + if (foundSong.visibility === 'private') { + if (!user) { + throw new HttpException('Song not found', HttpStatus.NOT_FOUND); + } - // increment download count - if (!packed && src === 'downloadButton') foundSong.downloadCount++; - await foundSong.save(); + if (foundSong.uploader.toString() !== user._id.toString()) { + throw new HttpException('Song not found', HttpStatus.NOT_FOUND); + } + } - return url; - } catch (e) { - this.logger.error('Error getting song file', e); - throw new HttpException( - 'An error occurred while retrieving the song file', - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - } - - public async getMySongsPage({ - query, - user - }: { - query: PageQueryDTO; - user : UserDocument; - }): Promise { - const page = parseInt(query.page?.toString() ?? '1'); - const limit = parseInt(query.limit?.toString() ?? '10'); - const order = query.order ? query.order : false; - const sort = query.sort ? query.sort : 'recent'; - - const songData = (await this.songModel - .find({ - uploader: user._id - }) - .sort({ - [sort]: order ? 1 : -1 - }) - .skip(limit * (page - 1)) - .limit(limit)) as unknown as SongWithUser[]; - - const total = await this.songModel.countDocuments({ - uploader: user._id - }); - - return { - content: songData.map((song) => - SongPreviewDto.fromSongDocumentWithUser(song) - ), - page : page, - limit: limit, - total: total - }; - } - - public async getSongEdit( - publicId: string, - user: UserDocument - ): Promise { - const foundSong = await this.songModel - .findOne({ publicId: publicId }) - .exec(); - - if (!foundSong) { - throw new HttpException('Song not found', HttpStatus.NOT_FOUND); - } + // increment view count + foundSong.playCount++; + await foundSong.save(); + + const populatedSong = await foundSong.populate( + 'uploader', + 'username profileImage -_id' + ); - if (foundSong.uploader.toString() !== user?._id.toString()) { - throw new HttpException('Song not found', HttpStatus.UNAUTHORIZED); + return SongViewDto.fromSongDocument(populatedSong); } - return UploadSongDto.fromSongDocument(foundSong); - } + // TODO: service should not handle HTTP -> https://www.reddit.com/r/node/comments/uoicw1/should_i_return_status_code_from_service_layer/ + public async getSongDownloadUrl( + publicId: string, + user: UserDocument | null, + src?: string, + packed: boolean = false + ): Promise { + const foundSong = await this.songModel.findOne({ publicId: publicId }); + + if (!foundSong) { + throw new HttpException('Song not found with ID', HttpStatus.NOT_FOUND); + } - public async getCategories(): Promise> { - // Return an object with categories and their counts, minus empty categories, minus private songs, and sort by count + if (foundSong.visibility !== 'public') { + if (!user || foundSong.uploader.toString() !== user._id.toString()) { + throw new HttpException( + 'This song is private', + HttpStatus.UNAUTHORIZED + ); + } + } - const categories = (await this.songModel.aggregate([ - { - $match: { - visibility: 'public' + if (!packed && !foundSong.allowDownload) { + throw new HttpException( + 'The uploader has disabled downloads of this song', + HttpStatus.UNAUTHORIZED + ); } - }, - { - $group: { - _id : '$category', - count: { $sum: 1 } + + const fileKey = packed ? foundSong.packedSongUrl : foundSong.nbsFileUrl; + const fileExt = packed ? '.zip' : '.nbs'; + + const fileName = `${foundSong.title}${fileExt}`; + + try { + const url = await this.fileService.getSongDownloadUrl(fileKey, fileName); + + // increment download count + if (!packed && src === 'downloadButton') foundSong.downloadCount++; + await foundSong.save(); + + return url; + } catch (e) { + this.logger.error('Error getting song file', e); + throw new HttpException( + 'An error occurred while retrieving the song file', + HttpStatus.INTERNAL_SERVER_ERROR + ); } - }, - { - $sort: { - count: -1 + } + + public async getMySongsPage({ + query, + user + }: { + query: PageQueryDTO; + user : UserDocument; + }): Promise { + const page = parseInt(query.page?.toString() ?? '1'); + const limit = parseInt(query.limit?.toString() ?? '10'); + const order = query.order ? query.order : false; + const sort = query.sort ? query.sort : 'recent'; + + const songData = (await this.songModel + .find({ + uploader: user._id + }) + .sort({ + [sort]: order ? 1 : -1 + }) + .skip(limit * (page - 1)) + .limit(limit)) as unknown as SongWithUser[]; + + const total = await this.songModel.countDocuments({ + uploader: user._id + }); + + return { + content: songData.map((song) => + SongPreviewDto.fromSongDocumentWithUser(song) + ), + page : page, + limit: limit, + total: total + }; + } + + public async getSongEdit( + publicId: string, + user: UserDocument + ): Promise { + const foundSong = await this.songModel + .findOne({ publicId: publicId }) + .exec(); + + if (!foundSong) { + throw new HttpException('Song not found', HttpStatus.NOT_FOUND); } - } - ])) as unknown as { _id: string; count: number }[]; - - // Return object with category names as keys and counts as values - return categories.reduce((acc, category) => { - if (category._id) { - acc[category._id] = category.count; - } - - return acc; - }, {} as Record); - } - - public async getSongsByCategory( - category: string, - page: number, - limit: number - ): Promise { - const songs = (await this.songModel - .find({ - category : category, - visibility: 'public' - }) - .sort({ createdAt: -1 }) - .skip(page * limit - limit) - .limit(limit) - .populate('uploader', 'username profileImage -_id') - .exec()) as unknown as SongWithUser[]; - - return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); - } - - public async getRandomSongs( - count: number, - category: string - ): Promise { - const songs = (await this.songModel - .aggregate([ - { - $match: { - visibility: 'public' - } - }, - { - $sample: { - size: count - } + + if (foundSong.uploader.toString() !== user?._id.toString()) { + throw new HttpException('Song not found', HttpStatus.UNAUTHORIZED); } - ]) - .exec()) as unknown as SongWithUser[]; - await this.songModel.populate(songs, { - path : 'uploader', - select: 'username profileImage -_id' - }); + return UploadSongDto.fromSongDocument(foundSong); + } + + public async getCategories(): Promise> { + // Return an object with categories and their counts, minus empty categories, minus private songs, and sort by count + + const categories = (await this.songModel.aggregate([ + { + $match: { + visibility: 'public' + } + }, + { + $group: { + _id : '$category', + count: { $sum: 1 } + } + }, + { + $sort: { + count: -1 + } + } + ])) as unknown as { _id: string; count: number }[]; + + // Return object with category names as keys and counts as values + return categories.reduce((acc, category) => { + if (category._id) { + acc[category._id] = category.count; + } + + return acc; + }, {} as Record); + } - return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); - } + public async getSongsByCategory( + category: string, + page: number, + limit: number + ): Promise { + const songs = (await this.songModel + .find({ + category : category, + visibility: 'public' + }) + .sort({ createdAt: -1 }) + .skip(page * limit - limit) + .limit(limit) + .populate('uploader', 'username profileImage -_id') + .exec()) as unknown as SongWithUser[]; + + return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); + } - public async getAllSongs() { - return this.songModel.find({}); - } + public async getRandomSongs( + count: number, + category: string + ): Promise { + const songs = (await this.songModel + .aggregate([ + { + $match: { + visibility: 'public' + } + }, + { + $sample: { + size: count + } + } + ]) + .exec()) as unknown as SongWithUser[]; + + await this.songModel.populate(songs, { + path : 'uploader', + select: 'username profileImage -_id' + }); + + return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); + } + + public async getAllSongs() { + return this.songModel.find({}); + } } diff --git a/apps/backend/src/song/song.util.ts b/apps/backend/src/song/song.util.ts index bf098caa..cb5dfaf1 100644 --- a/apps/backend/src/song/song.util.ts +++ b/apps/backend/src/song/song.util.ts @@ -3,99 +3,99 @@ import { SongWithUser } from '@nbw/database'; import { customAlphabet } from 'nanoid'; export const formatDuration = (totalSeconds: number) => { - const minutes = Math.floor(Math.ceil(totalSeconds) / 60); - const seconds = Math.ceil(totalSeconds) % 60; + const minutes = Math.floor(Math.ceil(totalSeconds) / 60); + const seconds = Math.ceil(totalSeconds) % 60; - const formattedTime = `${minutes.toFixed().padStart(1, '0')}:${seconds - .toFixed() - .padStart(2, '0')}`; + const formattedTime = `${minutes.toFixed().padStart(1, '0')}:${seconds + .toFixed() + .padStart(2, '0')}`; - return formattedTime; + return formattedTime; }; export function removeExtraSpaces(input: string): string { - return input - .replace(/ +/g, ' ') // replace multiple spaces with one space - .replace(/\n\n+/g, '\n\n') // replace 3+ newlines with two newlines - .trim(); // remove leading and trailing spaces + return input + .replace(/ +/g, ' ') // replace multiple spaces with one space + .replace(/\n\n+/g, '\n\n') // replace 3+ newlines with two newlines + .trim(); // remove leading and trailing spaces } const alphabet = - '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const nanoid = customAlphabet(alphabet, 10); export const generateSongId = () => { - return nanoid(); + return nanoid(); }; export function getUploadDiscordEmbed({ - title, - description, - uploader, - createdAt, - publicId, - thumbnailUrl, - thumbnailData, - originalAuthor, - category, - license, - stats + title, + description, + uploader, + createdAt, + publicId, + thumbnailUrl, + thumbnailData, + originalAuthor, + category, + license, + stats }: SongWithUser) { - let fieldsArray = []; + let fieldsArray = []; - if (originalAuthor) { - fieldsArray.push({ - name : 'Original Author', - value : originalAuthor, - inline: false - }); - } - - fieldsArray = fieldsArray.concat([ - { - name : 'Category', - value : UPLOAD_CONSTANTS.categories[category], - inline: true - }, - { - name : 'Notes', - value : stats.noteCount.toLocaleString('en-US'), - inline: true - }, - { - name : 'Length', - value : formatDuration(stats.duration), - inline: true + if (originalAuthor) { + fieldsArray.push({ + name : 'Original Author', + value : originalAuthor, + inline: false + }); } - ]); - return { - embeds: [ - { - title : title, - description: description, - color : Number('0x' + thumbnailData.backgroundColor.replace('#', '')), - timestamp : createdAt.toISOString(), - footer : { - text: UPLOAD_CONSTANTS.licenses[license] - ? UPLOAD_CONSTANTS.licenses[license].shortName - : 'Unknown License' - }, - author: { - name : uploader.username, - icon_url: uploader.profileImage - //url: 'https://noteblock.world/user/${uploaderName}', + fieldsArray = fieldsArray.concat([ + { + name : 'Category', + value : UPLOAD_CONSTANTS.categories[category], + inline: true }, - fields: fieldsArray, - url : `https://noteblock.world/song/${publicId}`, - image : { - url: thumbnailUrl + { + name : 'Notes', + value : stats.noteCount.toLocaleString('en-US'), + inline: true }, - thumbnail: { - url: 'https://noteblock.world/nbw-color.png' + { + name : 'Length', + value : formatDuration(stats.duration), + inline: true } - } - ] - }; + ]); + + return { + embeds: [ + { + title : title, + description: description, + color : Number('0x' + thumbnailData.backgroundColor.replace('#', '')), + timestamp : createdAt.toISOString(), + footer : { + text: UPLOAD_CONSTANTS.licenses[license] + ? UPLOAD_CONSTANTS.licenses[license].shortName + : 'Unknown License' + }, + author: { + name : uploader.username, + icon_url: uploader.profileImage + //url: 'https://noteblock.world/user/${uploaderName}', + }, + fields: fieldsArray, + url : `https://noteblock.world/song/${publicId}`, + image : { + url: thumbnailUrl + }, + thumbnail: { + url: 'https://noteblock.world/nbw-color.png' + } + } + ] + }; } diff --git a/apps/backend/src/user/user.controller.spec.ts b/apps/backend/src/user/user.controller.spec.ts index ebc5f0d8..05cb1d0b 100644 --- a/apps/backend/src/user/user.controller.spec.ts +++ b/apps/backend/src/user/user.controller.spec.ts @@ -7,84 +7,84 @@ import { UserController } from './user.controller'; import { UserService } from './user.service'; const mockUserService = { - getUserByEmailOrId: jest.fn(), - getUserPaginated : jest.fn(), - getSelfUserData : jest.fn() + getUserByEmailOrId: jest.fn(), + getUserPaginated : jest.fn(), + getSelfUserData : jest.fn() }; describe('UserController', () => { - let userController: UserController; - let userService: UserService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [UserController], - providers : [ - { - provide : UserService, - useValue: mockUserService - } - ] - }).compile(); - - userController = module.get(UserController); - userService = module.get(UserService); - }); - - it('should be defined', () => { - expect(userController).toBeDefined(); - }); - - describe('getUser', () => { - it('should return user data by email or ID', async () => { - const query: GetUser = { - email : 'test@email.com', - username: 'test-username', - id : 'test-id' - }; - - const user = { email: 'test@example.com' }; - - mockUserService.getUserByEmailOrId.mockResolvedValueOnce(user); - - const result = await userController.getUser(query); - - expect(result).toEqual(user); - expect(userService.getUserByEmailOrId).toHaveBeenCalledWith(query); + let userController: UserController; + let userService: UserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + providers : [ + { + provide : UserService, + useValue: mockUserService + } + ] + }).compile(); + + userController = module.get(UserController); + userService = module.get(UserService); }); - }); - describe('getUserPaginated', () => { - it('should return paginated user data', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; - const paginatedUsers = { items: [], total: 0 }; + it('should be defined', () => { + expect(userController).toBeDefined(); + }); + + describe('getUser', () => { + it('should return user data by email or ID', async () => { + const query: GetUser = { + email : 'test@email.com', + username: 'test-username', + id : 'test-id' + }; - mockUserService.getUserPaginated.mockResolvedValueOnce(paginatedUsers); + const user = { email: 'test@example.com' }; - const result = await userController.getUserPaginated(query); + mockUserService.getUserByEmailOrId.mockResolvedValueOnce(user); - expect(result).toEqual(paginatedUsers); - expect(userService.getUserPaginated).toHaveBeenCalledWith(query); + const result = await userController.getUser(query); + + expect(result).toEqual(user); + expect(userService.getUserByEmailOrId).toHaveBeenCalledWith(query); + }); }); - }); - describe('getMe', () => { - it('should return the token owner data', async () => { - const user: UserDocument = { _id: 'test-user-id' } as UserDocument; - const userData = { _id: 'test-user-id', email: 'test@example.com' }; + describe('getUserPaginated', () => { + it('should return paginated user data', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; + const paginatedUsers = { items: [], total: 0 }; - mockUserService.getSelfUserData.mockResolvedValueOnce(userData); + mockUserService.getUserPaginated.mockResolvedValueOnce(paginatedUsers); - const result = await userController.getMe(user); + const result = await userController.getUserPaginated(query); - expect(result).toEqual(userData); - expect(userService.getSelfUserData).toHaveBeenCalledWith(user); + expect(result).toEqual(paginatedUsers); + expect(userService.getUserPaginated).toHaveBeenCalledWith(query); + }); }); - it('should handle null user', async () => { - const user = null; + describe('getMe', () => { + it('should return the token owner data', async () => { + const user: UserDocument = { _id: 'test-user-id' } as UserDocument; + const userData = { _id: 'test-user-id', email: 'test@example.com' }; + + mockUserService.getSelfUserData.mockResolvedValueOnce(userData); + + const result = await userController.getMe(user); + + expect(result).toEqual(userData); + expect(userService.getSelfUserData).toHaveBeenCalledWith(user); + }); + + it('should handle null user', async () => { + const user = null; - await expect(userController.getMe(user)).rejects.toThrow(HttpException); + await expect(userController.getMe(user)).rejects.toThrow(HttpException); + }); }); - }); }); diff --git a/apps/backend/src/user/user.controller.ts b/apps/backend/src/user/user.controller.ts index c1197fec..c31cc461 100644 --- a/apps/backend/src/user/user.controller.ts +++ b/apps/backend/src/user/user.controller.ts @@ -9,43 +9,43 @@ import { UserService } from './user.service'; @Controller('user') export class UserController { - constructor( - @Inject(UserService) - private readonly userService: UserService - ) {} + constructor( + @Inject(UserService) + private readonly userService: UserService + ) {} - @Get() - @ApiTags('user') - @ApiBearerAuth() - async getUser(@Query() query: GetUser) { - return await this.userService.getUserByEmailOrId(query); - } + @Get() + @ApiTags('user') + @ApiBearerAuth() + async getUser(@Query() query: GetUser) { + return await this.userService.getUserByEmailOrId(query); + } - @Get() - @ApiTags('user') - @ApiBearerAuth() - async getUserPaginated(@Query() query: PageQueryDTO) { - return await this.userService.getUserPaginated(query); - } + @Get() + @ApiTags('user') + @ApiBearerAuth() + async getUserPaginated(@Query() query: PageQueryDTO) { + return await this.userService.getUserPaginated(query); + } - @Get('me') - @ApiTags('user') - @ApiBearerAuth() - @ApiOperation({ summary: 'Get the token owner data' }) - async getMe(@GetRequestToken() user: UserDocument | null) { - user = validateUser(user); - return await this.userService.getSelfUserData(user); - } + @Get('me') + @ApiTags('user') + @ApiBearerAuth() + @ApiOperation({ summary: 'Get the token owner data' }) + async getMe(@GetRequestToken() user: UserDocument | null) { + user = validateUser(user); + return await this.userService.getSelfUserData(user); + } - @Patch('username') - @ApiTags('user') - @ApiBearerAuth() - @ApiOperation({ summary: 'Update the username' }) - async updateUsername( - @GetRequestToken() user: UserDocument | null, - @Body() body: UpdateUsernameDto - ) { - user = validateUser(user); - return await this.userService.updateUsername(user, body); - } + @Patch('username') + @ApiTags('user') + @ApiBearerAuth() + @ApiOperation({ summary: 'Update the username' }) + async updateUsername( + @GetRequestToken() user: UserDocument | null, + @Body() body: UpdateUsernameDto + ) { + user = validateUser(user); + return await this.userService.updateUsername(user, body); + } } diff --git a/apps/backend/src/user/user.module.ts b/apps/backend/src/user/user.module.ts index c8f9225e..cbbbcca7 100644 --- a/apps/backend/src/user/user.module.ts +++ b/apps/backend/src/user/user.module.ts @@ -6,11 +6,11 @@ import { UserController } from './user.controller'; import { UserService } from './user.service'; @Module({ - imports: [ - MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]) - ], - providers : [UserService], - controllers: [UserController], - exports : [UserService] + imports: [ + MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]) + ], + providers : [UserService], + controllers: [UserController], + exports : [UserService] }) export class UserModule {} diff --git a/apps/backend/src/user/user.service.spec.ts b/apps/backend/src/user/user.service.spec.ts index b397f1bb..d2ed163d 100644 --- a/apps/backend/src/user/user.service.spec.ts +++ b/apps/backend/src/user/user.service.spec.ts @@ -1,9 +1,9 @@ import { - CreateUser, - GetUser, - PageQueryDTO, - User, - UserDocument + CreateUser, + GetUser, + PageQueryDTO, + User, + UserDocument } from '@nbw/database'; import { HttpException, HttpStatus } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; @@ -13,513 +13,513 @@ import { Model } from 'mongoose'; import { UserService } from './user.service'; const mockUserModel = { - create : jest.fn(), - findOne : jest.fn(), - findById : jest.fn(), - find : jest.fn(), - save : jest.fn(), - exec : jest.fn(), - select : jest.fn(), - countDocuments: jest.fn() + create : jest.fn(), + findOne : jest.fn(), + findById : jest.fn(), + find : jest.fn(), + save : jest.fn(), + exec : jest.fn(), + select : jest.fn(), + countDocuments: jest.fn() }; describe('UserService', () => { - let service: UserService; - let userModel: Model; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - UserService, - { - provide : getModelToken(User.name), - useValue: mockUserModel - } - ] - }).compile(); - - service = module.get(UserService); - userModel = module.get>(getModelToken(User.name)); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('create', () => { - it('should create a new user', async () => { - const createUserDto: CreateUser = { - username : 'testuser', - email : 'test@example.com', - profileImage: 'testimage.png' - }; - - const user = { - ...createUserDto, - save: jest.fn().mockReturnThis() - } as any; - - jest.spyOn(userModel, 'create').mockReturnValue(user); - - const result = await service.create(createUserDto); - - expect(result).toEqual(user); - expect(userModel.create).toHaveBeenCalledWith(createUserDto); - expect(user.save).toHaveBeenCalled(); + let service: UserService; + let userModel: Model; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UserService, + { + provide : getModelToken(User.name), + useValue: mockUserModel + } + ] + }).compile(); + + service = module.get(UserService); + userModel = module.get>(getModelToken(User.name)); }); - }); - describe('findByEmail', () => { - it('should find a user by email', async () => { - const email = 'test@example.com'; - const user = { email } as UserDocument; + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('create', () => { + it('should create a new user', async () => { + const createUserDto: CreateUser = { + username : 'testuser', + email : 'test@example.com', + profileImage: 'testimage.png' + }; + + const user = { + ...createUserDto, + save: jest.fn().mockReturnThis() + } as any; - jest.spyOn(userModel, 'findOne').mockReturnValue({ - exec: jest.fn().mockResolvedValue(user) - } as any); + jest.spyOn(userModel, 'create').mockReturnValue(user); - const result = await service.findByEmail(email); + const result = await service.create(createUserDto); - expect(result).toEqual(user); - expect(userModel.findOne).toHaveBeenCalledWith({ email }); + expect(result).toEqual(user); + expect(userModel.create).toHaveBeenCalledWith(createUserDto); + expect(user.save).toHaveBeenCalled(); + }); }); - }); - describe('findByID', () => { - it('should find a user by ID', async () => { - const id = 'test-id'; - const user = { _id: id } as UserDocument; + describe('findByEmail', () => { + it('should find a user by email', async () => { + const email = 'test@example.com'; + const user = { email } as UserDocument; - jest.spyOn(userModel, 'findById').mockReturnValue({ - exec: jest.fn().mockResolvedValue(user) - } as any); + jest.spyOn(userModel, 'findOne').mockReturnValue({ + exec: jest.fn().mockResolvedValue(user) + } as any); - const result = await service.findByID(id); + const result = await service.findByEmail(email); - expect(result).toEqual(user); - expect(userModel.findById).toHaveBeenCalledWith(id); + expect(result).toEqual(user); + expect(userModel.findOne).toHaveBeenCalledWith({ email }); + }); }); - }); - describe('getUserPaginated', () => { - it('should return paginated users', async () => { - const query: PageQueryDTO = { page: 1, limit: 10 }; - const users = [{ username: 'testuser' }] as UserDocument[]; + describe('findByID', () => { + it('should find a user by ID', async () => { + const id = 'test-id'; + const user = { _id: id } as UserDocument; - const usersPage = { - users, - total: 1, - page : 1, - limit: 10 - }; + jest.spyOn(userModel, 'findById').mockReturnValue({ + exec: jest.fn().mockResolvedValue(user) + } as any); - const mockFind = { - sort : jest.fn().mockReturnThis(), - skip : jest.fn().mockReturnThis(), - limit: jest.fn().mockResolvedValue(users) - }; + const result = await service.findByID(id); - jest.spyOn(userModel, 'find').mockReturnValue(mockFind as any); - jest.spyOn(userModel, 'countDocuments').mockResolvedValue(1); + expect(result).toEqual(user); + expect(userModel.findById).toHaveBeenCalledWith(id); + }); + }); - const result = await service.getUserPaginated(query); + describe('getUserPaginated', () => { + it('should return paginated users', async () => { + const query: PageQueryDTO = { page: 1, limit: 10 }; + const users = [{ username: 'testuser' }] as UserDocument[]; - expect(result).toEqual(usersPage); - expect(userModel.find).toHaveBeenCalledWith({}); - }); - }); + const usersPage = { + users, + total: 1, + page : 1, + limit: 10 + }; - describe('getUserByEmailOrId', () => { - it('should find a user by email', async () => { - const query: GetUser = { email: 'test@example.com' }; - const user = { email: 'test@example.com' } as UserDocument; + const mockFind = { + sort : jest.fn().mockReturnThis(), + skip : jest.fn().mockReturnThis(), + limit: jest.fn().mockResolvedValue(users) + }; - jest.spyOn(service, 'findByEmail').mockResolvedValue(user); + jest.spyOn(userModel, 'find').mockReturnValue(mockFind as any); + jest.spyOn(userModel, 'countDocuments').mockResolvedValue(1); - const result = await service.getUserByEmailOrId(query); + const result = await service.getUserPaginated(query); - expect(result).toEqual(user); - expect(service.findByEmail).toHaveBeenCalledWith(query.email); + expect(result).toEqual(usersPage); + expect(userModel.find).toHaveBeenCalledWith({}); + }); }); - it('should find a user by ID', async () => { - const query: GetUser = { id: 'test-id' }; - const user = { _id: 'test-id' } as UserDocument; - - jest.spyOn(service, 'findByID').mockResolvedValue(user); + describe('getUserByEmailOrId', () => { + it('should find a user by email', async () => { + const query: GetUser = { email: 'test@example.com' }; + const user = { email: 'test@example.com' } as UserDocument; - const result = await service.getUserByEmailOrId(query); + jest.spyOn(service, 'findByEmail').mockResolvedValue(user); - expect(result).toEqual(user); - expect(service.findByID).toHaveBeenCalledWith(query.id); - }); + const result = await service.getUserByEmailOrId(query); - it('should throw an error if username is provided', async () => { - const query: GetUser = { username: 'testuser' }; + expect(result).toEqual(user); + expect(service.findByEmail).toHaveBeenCalledWith(query.email); + }); - await expect(service.getUserByEmailOrId(query)).rejects.toThrow( - new HttpException( - 'Username is not supported yet', - HttpStatus.BAD_REQUEST - ) - ); - }); + it('should find a user by ID', async () => { + const query: GetUser = { id: 'test-id' }; + const user = { _id: 'test-id' } as UserDocument; - it('should throw an error if neither email nor ID is provided', async () => { - const query: GetUser = {}; + jest.spyOn(service, 'findByID').mockResolvedValue(user); - await expect(service.getUserByEmailOrId(query)).rejects.toThrow( - new HttpException( - 'You must provide an email or an id', - HttpStatus.BAD_REQUEST - ) - ); - }); - }); + const result = await service.getUserByEmailOrId(query); - describe('getHydratedUser', () => { - it('should return a hydrated user', async () => { - const user = { _id: 'test-id' } as UserDocument; - const hydratedUser = { ...user, songs: [] } as unknown as UserDocument; + expect(result).toEqual(user); + expect(service.findByID).toHaveBeenCalledWith(query.id); + }); - jest.spyOn(userModel, 'findById').mockReturnValue({ - populate: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(hydratedUser) - } as any); + it('should throw an error if username is provided', async () => { + const query: GetUser = { username: 'testuser' }; - const result = await service.getHydratedUser(user); + await expect(service.getUserByEmailOrId(query)).rejects.toThrow( + new HttpException( + 'Username is not supported yet', + HttpStatus.BAD_REQUEST + ) + ); + }); - expect(result).toEqual(hydratedUser); - expect(userModel.findById).toHaveBeenCalledWith(user._id); + it('should throw an error if neither email nor ID is provided', async () => { + const query: GetUser = {}; - expect(userModel.findById(user._id).populate).toHaveBeenCalledWith( - 'songs' - ); + await expect(service.getUserByEmailOrId(query)).rejects.toThrow( + new HttpException( + 'You must provide an email or an id', + HttpStatus.BAD_REQUEST + ) + ); + }); }); - }); - describe('getSelfUserData', () => { - it('should return self user data', async () => { - const user = { _id: 'test-id' } as UserDocument; - const userData = { ...user, lastSeen: new Date() } as UserDocument; + describe('getHydratedUser', () => { + it('should return a hydrated user', async () => { + const user = { _id: 'test-id' } as UserDocument; + const hydratedUser = { ...user, songs: [] } as unknown as UserDocument; - jest.spyOn(service, 'findByID').mockResolvedValue(userData); + jest.spyOn(userModel, 'findById').mockReturnValue({ + populate: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(hydratedUser) + } as any); - const result = await service.getSelfUserData(user); + const result = await service.getHydratedUser(user); - expect(result).toEqual(userData); - expect(service.findByID).toHaveBeenCalledWith(user._id.toString()); + expect(result).toEqual(hydratedUser); + expect(userModel.findById).toHaveBeenCalledWith(user._id); + + expect(userModel.findById(user._id).populate).toHaveBeenCalledWith( + 'songs' + ); + }); }); - it('should throw an error if user is not found', async () => { - const user = { _id: 'test-id' } as UserDocument; + describe('getSelfUserData', () => { + it('should return self user data', async () => { + const user = { _id: 'test-id' } as UserDocument; + const userData = { ...user, lastSeen: new Date() } as UserDocument; - jest.spyOn(service, 'findByID').mockResolvedValue(null); + jest.spyOn(service, 'findByID').mockResolvedValue(userData); - await expect(service.getSelfUserData(user)).rejects.toThrow( - new HttpException('user not found', HttpStatus.NOT_FOUND) - ); - }); + const result = await service.getSelfUserData(user); - it('should update lastSeen and increment loginStreak if lastSeen is before today', async () => { - const user = { _id: 'test-id' } as UserDocument; - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday.setHours(0, 0, 0, 0); + expect(result).toEqual(userData); + expect(service.findByID).toHaveBeenCalledWith(user._id.toString()); + }); - const userData = { - ...user, - lastSeen : yesterday, - loginStreak: 1, - save : jest.fn().mockResolvedValue(true) - } as unknown as UserDocument; + it('should throw an error if user is not found', async () => { + const user = { _id: 'test-id' } as UserDocument; - jest.spyOn(service, 'findByID').mockResolvedValue(userData); + jest.spyOn(service, 'findByID').mockResolvedValue(null); - const result = await service.getSelfUserData(user); + await expect(service.getSelfUserData(user)).rejects.toThrow( + new HttpException('user not found', HttpStatus.NOT_FOUND) + ); + }); - expect(result.lastSeen).toBeInstanceOf(Date); - expect(result.loginStreak).toBe(2); - expect(userData.save).toHaveBeenCalled(); - }); + it('should update lastSeen and increment loginStreak if lastSeen is before today', async () => { + const user = { _id: 'test-id' } as UserDocument; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); - it('should not update lastSeen or increment loginStreak if lastSeen is today', async () => { - const user = { _id: 'test-id' } as UserDocument; - const today = new Date(); - today.setHours(0, 0, 0, 0); + const userData = { + ...user, + lastSeen : yesterday, + loginStreak: 1, + save : jest.fn().mockResolvedValue(true) + } as unknown as UserDocument; - const userData = { - ...user, - lastSeen : today, - loginStreak: 1, - save : jest.fn().mockResolvedValue(true) - } as unknown as UserDocument; + jest.spyOn(service, 'findByID').mockResolvedValue(userData); - jest.spyOn(service, 'findByID').mockResolvedValue(userData); + const result = await service.getSelfUserData(user); - const result = await service.getSelfUserData(user); + expect(result.lastSeen).toBeInstanceOf(Date); + expect(result.loginStreak).toBe(2); + expect(userData.save).toHaveBeenCalled(); + }); - expect(result.lastSeen).toEqual(today); - expect(result.loginStreak).toBe(1); - expect(userData.save).not.toHaveBeenCalled(); - }); + it('should not update lastSeen or increment loginStreak if lastSeen is today', async () => { + const user = { _id: 'test-id' } as UserDocument; + const today = new Date(); + today.setHours(0, 0, 0, 0); - it('should reset loginStreak if lastSeen is not yesterday', async () => { - const user = { _id: 'test-id' } as UserDocument; - const twoDaysAgo = new Date(); - twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); - twoDaysAgo.setHours(0, 0, 0, 0); + const userData = { + ...user, + lastSeen : today, + loginStreak: 1, + save : jest.fn().mockResolvedValue(true) + } as unknown as UserDocument; - const userData = { - ...user, - lastSeen : twoDaysAgo, - loginStreak: 5, - save : jest.fn().mockResolvedValue(true) - } as unknown as UserDocument; + jest.spyOn(service, 'findByID').mockResolvedValue(userData); - jest.spyOn(service, 'findByID').mockResolvedValue(userData); + const result = await service.getSelfUserData(user); - const result = await service.getSelfUserData(user); + expect(result.lastSeen).toEqual(today); + expect(result.loginStreak).toBe(1); + expect(userData.save).not.toHaveBeenCalled(); + }); - expect(result.lastSeen).toBeInstanceOf(Date); - expect(result.loginStreak).toBe(1); - expect(userData.save).toHaveBeenCalled(); - }); + it('should reset loginStreak if lastSeen is not yesterday', async () => { + const user = { _id: 'test-id' } as UserDocument; + const twoDaysAgo = new Date(); + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); + twoDaysAgo.setHours(0, 0, 0, 0); - it('should increment loginCount if lastSeen is not today', async () => { - const user = { _id: 'test-id' } as UserDocument; - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday.setHours(0, 0, 0, 0); + const userData = { + ...user, + lastSeen : twoDaysAgo, + loginStreak: 5, + save : jest.fn().mockResolvedValue(true) + } as unknown as UserDocument; - const userData = { - ...user, - lastSeen : yesterday, - loginCount: 5, - save : jest.fn().mockResolvedValue(true) - } as unknown as UserDocument; + jest.spyOn(service, 'findByID').mockResolvedValue(userData); - jest.spyOn(service, 'findByID').mockResolvedValue(userData); + const result = await service.getSelfUserData(user); - const result = await service.getSelfUserData(user); + expect(result.lastSeen).toBeInstanceOf(Date); + expect(result.loginStreak).toBe(1); + expect(userData.save).toHaveBeenCalled(); + }); - expect(result.lastSeen).toBeInstanceOf(Date); - expect(result.loginCount).toBe(6); - expect(userData.save).toHaveBeenCalled(); - }); + it('should increment loginCount if lastSeen is not today', async () => { + const user = { _id: 'test-id' } as UserDocument; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); - it('should not increment loginCount if lastSeen is today', async () => { - const user = { _id: 'test-id' } as UserDocument; + const userData = { + ...user, + lastSeen : yesterday, + loginCount: 5, + save : jest.fn().mockResolvedValue(true) + } as unknown as UserDocument; - const today = new Date(); - today.setHours(0, 0, 0, 0); + jest.spyOn(service, 'findByID').mockResolvedValue(userData); - const userData = { - ...user, - lastSeen : today, - loginCount: 5, - save : jest.fn().mockResolvedValue(true) - } as unknown as UserDocument; + const result = await service.getSelfUserData(user); - jest.spyOn(service, 'findByID').mockResolvedValue(userData); + expect(result.lastSeen).toBeInstanceOf(Date); + expect(result.loginCount).toBe(6); + expect(userData.save).toHaveBeenCalled(); + }); + + it('should not increment loginCount if lastSeen is today', async () => { + const user = { _id: 'test-id' } as UserDocument; - const result = await service.getSelfUserData(user); + const today = new Date(); + today.setHours(0, 0, 0, 0); - expect(result.lastSeen).toEqual(today); - expect(result.loginCount).toBe(5); - expect(userData.save).not.toHaveBeenCalled(); - }); + const userData = { + ...user, + lastSeen : today, + loginCount: 5, + save : jest.fn().mockResolvedValue(true) + } as unknown as UserDocument; - it('should increment maxLoginStreak if login streak exceeds max', async () => { - const user = { _id: 'test-id' } as UserDocument; + jest.spyOn(service, 'findByID').mockResolvedValue(userData); + + const result = await service.getSelfUserData(user); - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday.setHours(0, 0, 0, 0); + expect(result.lastSeen).toEqual(today); + expect(result.loginCount).toBe(5); + expect(userData.save).not.toHaveBeenCalled(); + }); - const userData = { - ...user, - lastSeen : yesterday, - loginStreak : 8, - maxLoginStreak: 8, - save : jest.fn().mockResolvedValue(true) - } as unknown as UserDocument; + it('should increment maxLoginStreak if login streak exceeds max', async () => { + const user = { _id: 'test-id' } as UserDocument; - jest.spyOn(service, 'findByID').mockResolvedValue(userData); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); - const result = await service.getSelfUserData(user); + const userData = { + ...user, + lastSeen : yesterday, + loginStreak : 8, + maxLoginStreak: 8, + save : jest.fn().mockResolvedValue(true) + } as unknown as UserDocument; - expect(result.maxLoginStreak).toBe(9); - expect(userData.save).toHaveBeenCalled(); - }); + jest.spyOn(service, 'findByID').mockResolvedValue(userData); + + const result = await service.getSelfUserData(user); - it('should not increment maxLoginStreak if login streak is less than the max', async () => { - const user = { _id: 'test-id' } as UserDocument; + expect(result.maxLoginStreak).toBe(9); + expect(userData.save).toHaveBeenCalled(); + }); - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday.setHours(0, 0, 0, 0); + it('should not increment maxLoginStreak if login streak is less than the max', async () => { + const user = { _id: 'test-id' } as UserDocument; - const userData = { - ...user, - lastSeen : yesterday, - loginStreak : 4, - maxLoginStreak: 8, - save : jest.fn().mockResolvedValue(true) - } as unknown as UserDocument; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); + + const userData = { + ...user, + lastSeen : yesterday, + loginStreak : 4, + maxLoginStreak: 8, + save : jest.fn().mockResolvedValue(true) + } as unknown as UserDocument; - jest.spyOn(service, 'findByID').mockResolvedValue(userData); + jest.spyOn(service, 'findByID').mockResolvedValue(userData); - const result = await service.getSelfUserData(user); + const result = await service.getSelfUserData(user); - expect(result.maxLoginStreak).toBe(8); - expect(userData.save).toHaveBeenCalled(); + expect(result.maxLoginStreak).toBe(8); + expect(userData.save).toHaveBeenCalled(); + }); }); - }); - describe('usernameExists', () => { - it('should return true if username exists', async () => { - const username = 'testuser'; - const user = { username } as UserDocument; + describe('usernameExists', () => { + it('should return true if username exists', async () => { + const username = 'testuser'; + const user = { username } as UserDocument; - jest.spyOn(userModel, 'findOne').mockReturnValue({ - select: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(user) - } as any); + jest.spyOn(userModel, 'findOne').mockReturnValue({ + select: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(user) + } as any); - const result = await service.usernameExists(username); + const result = await service.usernameExists(username); - expect(result).toBe(true); - expect(userModel.findOne).toHaveBeenCalledWith({ username }); + expect(result).toBe(true); + expect(userModel.findOne).toHaveBeenCalledWith({ username }); - expect(userModel.findOne({ username }).select).toHaveBeenCalledWith( - 'username' - ); - }); + expect(userModel.findOne({ username }).select).toHaveBeenCalledWith( + 'username' + ); + }); - it('should return false if username does not exist', async () => { - const username = 'testuser'; + it('should return false if username does not exist', async () => { + const username = 'testuser'; - jest.spyOn(userModel, 'findOne').mockReturnValue({ - select: jest.fn().mockReturnThis(), - exec : jest.fn().mockResolvedValue(null) - } as any); + jest.spyOn(userModel, 'findOne').mockReturnValue({ + select: jest.fn().mockReturnThis(), + exec : jest.fn().mockResolvedValue(null) + } as any); - const result = await service.usernameExists(username); + const result = await service.usernameExists(username); - expect(result).toBe(false); - expect(userModel.findOne).toHaveBeenCalledWith({ username }); + expect(result).toBe(false); + expect(userModel.findOne).toHaveBeenCalledWith({ username }); - expect(userModel.findOne({ username }).select).toHaveBeenCalledWith( - 'username' - ); + expect(userModel.findOne({ username }).select).toHaveBeenCalledWith( + 'username' + ); + }); }); - }); - describe('generateUsername', () => { - it('should generate a unique username', async () => { - const inputUsername = 'test user'; - const baseUsername = 'test_user'; + describe('generateUsername', () => { + it('should generate a unique username', async () => { + const inputUsername = 'test user'; + const baseUsername = 'test_user'; - jest.spyOn(service, 'usernameExists').mockResolvedValueOnce(false); + jest.spyOn(service, 'usernameExists').mockResolvedValueOnce(false); - const result = await service.generateUsername(inputUsername); + const result = await service.generateUsername(inputUsername); - expect(result).toBe(baseUsername); - expect(service.usernameExists).toHaveBeenCalledWith(baseUsername); - }); + expect(result).toBe(baseUsername); + expect(service.usernameExists).toHaveBeenCalledWith(baseUsername); + }); - it('should generate a unique username with a number suffix if base username is taken', async () => { - const inputUsername = 'test user'; - const baseUsername = 'test_user'; + it('should generate a unique username with a number suffix if base username is taken', async () => { + const inputUsername = 'test user'; + const baseUsername = 'test_user'; - jest - .spyOn(service, 'usernameExists') - .mockResolvedValueOnce(true) - .mockResolvedValueOnce(true) - .mockResolvedValueOnce(false); + jest + .spyOn(service, 'usernameExists') + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false); - const result = await service.generateUsername(inputUsername); + const result = await service.generateUsername(inputUsername); - expect(result).toMatch('test_user_2'); - expect(service.usernameExists).toHaveBeenCalledWith(baseUsername); + expect(result).toMatch('test_user_2'); + expect(service.usernameExists).toHaveBeenCalledWith(baseUsername); + }); }); - }); - describe('normalizeUsername', () => { - it('should normalize a username', () => { - const inputUsername = 'tést user'; - const normalizedUsername = 'test_user'; + describe('normalizeUsername', () => { + it('should normalize a username', () => { + const inputUsername = 'tést user'; + const normalizedUsername = 'test_user'; - const result = (service as any).normalizeUsername(inputUsername); + const result = (service as any).normalizeUsername(inputUsername); - expect(result).toBe(normalizedUsername); - }); + expect(result).toBe(normalizedUsername); + }); - it('should remove special characters from a username', () => { - const inputUsername = '静_かな'; - const normalizedUsername = '_'; + it('should remove special characters from a username', () => { + const inputUsername = '静_かな'; + const normalizedUsername = '_'; - const result = (service as any).normalizeUsername(inputUsername); + const result = (service as any).normalizeUsername(inputUsername); - expect(result).toBe(normalizedUsername); - }); + expect(result).toBe(normalizedUsername); + }); - it('should replace spaces with underscores in a username', () => { - const inputUsername = 'Имя пользователя'; - const normalizedUsername = '_'; + it('should replace spaces with underscores in a username', () => { + const inputUsername = 'Имя пользователя'; + const normalizedUsername = '_'; - const result = (service as any).normalizeUsername(inputUsername); + const result = (service as any).normalizeUsername(inputUsername); - expect(result).toBe(normalizedUsername); - }); + expect(result).toBe(normalizedUsername); + }); - it('should replace spaces with underscores in a username', () => { - const inputUsername = 'Eglė Čepulytė'; - const normalizedUsername = 'Egle_Cepulyte'; + it('should replace spaces with underscores in a username', () => { + const inputUsername = 'Eglė Čepulytė'; + const normalizedUsername = 'Egle_Cepulyte'; - const result = (service as any).normalizeUsername(inputUsername); + const result = (service as any).normalizeUsername(inputUsername); - expect(result).toBe(normalizedUsername); + expect(result).toBe(normalizedUsername); + }); }); - }); - describe('updateUsername', () => { - it('should update a user username', async () => { - const user = { - username: 'testuser', - save : jest.fn().mockReturnThis() - } as unknown as UserDocument; + describe('updateUsername', () => { + it('should update a user username', async () => { + const user = { + username: 'testuser', + save : jest.fn().mockReturnThis() + } as unknown as UserDocument; - const body = { username: 'newuser' }; + const body = { username: 'newuser' }; - jest.spyOn(service, 'usernameExists').mockResolvedValue(false); + jest.spyOn(service, 'usernameExists').mockResolvedValue(false); - const result = await service.updateUsername(user, body); + const result = await service.updateUsername(user, body); - expect(result).toEqual({ - username : 'newuser', - publicName: undefined, - email : undefined - }); + expect(result).toEqual({ + username : 'newuser', + publicName: undefined, + email : undefined + }); - expect(user.username).toBe(body.username); - expect(service.usernameExists).toHaveBeenCalledWith(body.username); - }); + expect(user.username).toBe(body.username); + expect(service.usernameExists).toHaveBeenCalledWith(body.username); + }); - it('should throw an error if username already exists', async () => { - const user = { - username: 'testuser', - save : jest.fn().mockReturnThis() - } as unknown as UserDocument; + it('should throw an error if username already exists', async () => { + const user = { + username: 'testuser', + save : jest.fn().mockReturnThis() + } as unknown as UserDocument; - const body = { username: 'newuser' }; + const body = { username: 'newuser' }; - jest.spyOn(service, 'usernameExists').mockResolvedValue(true); + jest.spyOn(service, 'usernameExists').mockResolvedValue(true); - await expect(service.updateUsername(user, body)).rejects.toThrow( - new HttpException('Username already exists', HttpStatus.BAD_REQUEST) - ); + await expect(service.updateUsername(user, body)).rejects.toThrow( + new HttpException('Username already exists', HttpStatus.BAD_REQUEST) + ); + }); }); - }); }); diff --git a/apps/backend/src/user/user.service.ts b/apps/backend/src/user/user.service.ts index 6a509fa3..46ad1754 100644 --- a/apps/backend/src/user/user.service.ts +++ b/apps/backend/src/user/user.service.ts @@ -1,11 +1,11 @@ import { - CreateUser, - GetUser, - PageQueryDTO, - UpdateUsernameDto, - User, - UserDocument, - UserDto + CreateUser, + GetUser, + PageQueryDTO, + UpdateUsernameDto, + User, + UserDocument, + UserDto } from '@nbw/database'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; @@ -14,222 +14,222 @@ import { Model } from 'mongoose'; @Injectable() export class UserService { - constructor(@InjectModel(User.name) private userModel: Model) {} - - public async create(user_registered: CreateUser) { - await validate(user_registered); - const user = await this.userModel.create(user_registered); - user.username = user_registered.username; - user.email = user_registered.email; - user.profileImage = user_registered.profileImage; - - return await user.save(); - } - - public async update(user: UserDocument): Promise { - try { - return (await this.userModel.findByIdAndUpdate(user._id, user, { - new: true // return the updated document - })) as UserDocument; - } catch (error) { - if (error instanceof Error) { - throw error; - } - - throw new Error(String(error)); - } - } + constructor(@InjectModel(User.name) private userModel: Model) {} - public async createWithEmail(email: string): Promise { - // verify if user exists same email, username or publicName - const userByEmail = await this.findByEmail(email); + public async create(user_registered: CreateUser) { + await validate(user_registered); + const user = await this.userModel.create(user_registered); + user.username = user_registered.username; + user.email = user_registered.email; + user.profileImage = user_registered.profileImage; - if (userByEmail) { - throw new HttpException( - 'Email already registered', - HttpStatus.BAD_REQUEST - ); + return await user.save(); } - const emailPrefixUsername = await this.generateUsername( - email.split('@')[0] - ); + public async update(user: UserDocument): Promise { + try { + return (await this.userModel.findByIdAndUpdate(user._id, user, { + new: true // return the updated document + })) as UserDocument; + } catch (error) { + if (error instanceof Error) { + throw error; + } + + throw new Error(String(error)); + } + } - const user = await this.userModel.create({ - email : email, - username : emailPrefixUsername, - publicName: emailPrefixUsername - }); + public async createWithEmail(email: string): Promise { + // verify if user exists same email, username or publicName + const userByEmail = await this.findByEmail(email); + + if (userByEmail) { + throw new HttpException( + 'Email already registered', + HttpStatus.BAD_REQUEST + ); + } + + const emailPrefixUsername = await this.generateUsername( + email.split('@')[0] + ); + + const user = await this.userModel.create({ + email : email, + username : emailPrefixUsername, + publicName: emailPrefixUsername + }); + + return user; + } - return user; - } + public async findByEmail(email: string): Promise { + const user = await this.userModel.findOne({ email }).exec(); - public async findByEmail(email: string): Promise { - const user = await this.userModel.findOne({ email }).exec(); + return user; + } - return user; - } + public async findByID(objectID: string): Promise { + const user = await this.userModel.findById(objectID).exec(); - public async findByID(objectID: string): Promise { - const user = await this.userModel.findById(objectID).exec(); + return user; + } - return user; - } + public async findByPublicName( + publicName: string + ): Promise { + const user = await this.userModel.findOne({ publicName }); - public async findByPublicName( - publicName: string - ): Promise { - const user = await this.userModel.findOne({ publicName }); + return user; + } - return user; - } + public async findByUsername(username: string): Promise { + const user = await this.userModel.findOne({ username }); - public async findByUsername(username: string): Promise { - const user = await this.userModel.findOne({ username }); + return user; + } - return user; - } + public async getUserPaginated(query: PageQueryDTO) { + const { page = 1, limit = 10, sort = 'createdAt', order = 'asc' } = query; - public async getUserPaginated(query: PageQueryDTO) { - const { page = 1, limit = 10, sort = 'createdAt', order = 'asc' } = query; + const skip = (page - 1) * limit; + const sortOrder = order === 'asc' ? 1 : -1; - const skip = (page - 1) * limit; - const sortOrder = order === 'asc' ? 1 : -1; + const users = await this.userModel + .find({}) + .sort({ [sort]: sortOrder }) + .skip(skip) + .limit(limit); - const users = await this.userModel - .find({}) - .sort({ [sort]: sortOrder }) - .skip(skip) - .limit(limit); + const total = await this.userModel.countDocuments(); - const total = await this.userModel.countDocuments(); + return { + users, + total, + page, + limit + }; + } - return { - users, - total, - page, - limit - }; - } + public async getUserByEmailOrId(query: GetUser) { + const { email, id, username } = query; - public async getUserByEmailOrId(query: GetUser) { - const { email, id, username } = query; + if (email) { + return await this.findByEmail(email); + } - if (email) { - return await this.findByEmail(email); - } + if (id) { + return await this.findByID(id); + } - if (id) { - return await this.findByID(id); - } + if (username) { + throw new HttpException( + 'Username is not supported yet', + HttpStatus.BAD_REQUEST + ); + } - if (username) { - throw new HttpException( - 'Username is not supported yet', - HttpStatus.BAD_REQUEST - ); + throw new HttpException( + 'You must provide an email or an id', + HttpStatus.BAD_REQUEST + ); } - throw new HttpException( - 'You must provide an email or an id', - HttpStatus.BAD_REQUEST - ); - } - - public async getHydratedUser(user: UserDocument) { - const hydratedUser = await this.userModel - .findById(user._id) - .populate('songs') - .exec(); + public async getHydratedUser(user: UserDocument) { + const hydratedUser = await this.userModel + .findById(user._id) + .populate('songs') + .exec(); - return hydratedUser; - } + return hydratedUser; + } - public async getSelfUserData(user: UserDocument) { - const userData = await this.findByID(user._id.toString()); - if (!userData) - throw new HttpException('user not found', HttpStatus.NOT_FOUND); + public async getSelfUserData(user: UserDocument) { + const userData = await this.findByID(user._id.toString()); + if (!userData) + throw new HttpException('user not found', HttpStatus.NOT_FOUND); - const today = new Date(); - today.setHours(0, 0, 0, 0); // Set the time to the start of the day + const today = new Date(); + today.setHours(0, 0, 0, 0); // Set the time to the start of the day - const lastSeenDate = new Date(userData.lastSeen); - lastSeenDate.setHours(0, 0, 0, 0); // Set the time to the start of the day + const lastSeenDate = new Date(userData.lastSeen); + lastSeenDate.setHours(0, 0, 0, 0); // Set the time to the start of the day - if (lastSeenDate < today) { - userData.lastSeen = new Date(); + if (lastSeenDate < today) { + userData.lastSeen = new Date(); - // if the last seen date is not yesterday, reset the login streak - const yesterday = new Date(today); - yesterday.setDate(today.getDate() - 1); + // if the last seen date is not yesterday, reset the login streak + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); - if (lastSeenDate < yesterday) userData.loginStreak = 1; - else { - userData.loginStreak += 1; - if (userData.loginStreak > userData.maxLoginStreak) - userData.maxLoginStreak = userData.loginStreak; - } + if (lastSeenDate < yesterday) userData.loginStreak = 1; + else { + userData.loginStreak += 1; + if (userData.loginStreak > userData.maxLoginStreak) + userData.maxLoginStreak = userData.loginStreak; + } - userData.loginCount++; + userData.loginCount++; - userData.save(); // no need to await this, we already have the data to send back - } // if equal or greater, do nothing about the login streak + userData.save(); // no need to await this, we already have the data to send back + } // if equal or greater, do nothing about the login streak - return userData; - } + return userData; + } - public async usernameExists(username: string) { - const user = await this.userModel - .findOne({ username }) - .select('username') - .exec(); + public async usernameExists(username: string) { + const user = await this.userModel + .findOne({ username }) + .select('username') + .exec(); - return !!user; - } + return !!user; + } - private normalizeUsername = (inputUsername: string) => - inputUsername - .replace(' ', '_') - .normalize('NFKD') - .replace(/[\u0300-\u036f]/g, '') - .replace(/[^a-zA-Z0-9_]/g, ''); + private normalizeUsername = (inputUsername: string) => + inputUsername + .replace(' ', '_') + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-zA-Z0-9_]/g, ''); - public async generateUsername(inputUsername: string) { + public async generateUsername(inputUsername: string) { // Normalize username (remove accents, replace spaces with underscores) - const baseUsername = this.normalizeUsername(inputUsername); + const baseUsername = this.normalizeUsername(inputUsername); - let newUsername = baseUsername; - let counter = 1; + let newUsername = baseUsername; + let counter = 1; - // Check if the base username already exists - while (await this.usernameExists(newUsername)) { - newUsername = `${baseUsername}_${counter}`; - counter++; + // Check if the base username already exists + while (await this.usernameExists(newUsername)) { + newUsername = `${baseUsername}_${counter}`; + counter++; + } + + return newUsername; } - return newUsername; - } + public async updateUsername(user: UserDocument, body: UpdateUsernameDto) { + let { username } = body; + username = this.normalizeUsername(username); - public async updateUsername(user: UserDocument, body: UpdateUsernameDto) { - let { username } = body; - username = this.normalizeUsername(username); + if (await this.usernameExists(username)) { + throw new HttpException( + 'Username already exists', + HttpStatus.BAD_REQUEST + ); + } - if (await this.usernameExists(username)) { - throw new HttpException( - 'Username already exists', - HttpStatus.BAD_REQUEST - ); - } + if (user.username === username) { + throw new HttpException('Username is the same', HttpStatus.BAD_REQUEST); + } - if (user.username === username) { - throw new HttpException('Username is the same', HttpStatus.BAD_REQUEST); - } + user.username = username; + user.lastEdited = new Date(); - user.username = username; - user.lastEdited = new Date(); + await user.save(); - await user.save(); - - return UserDto.fromEntity(user); - } + return UserDto.fromEntity(user); + } } diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index e4ae21a7..742532c6 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -116,8 +116,8 @@ export default function RootLayout({ {process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_GA_ID && ( - - )} + + )} ); diff --git a/apps/frontend/src/global.d.ts b/apps/frontend/src/global.d.ts index a5f1e30d..d72c8f2f 100644 --- a/apps/frontend/src/global.d.ts +++ b/apps/frontend/src/global.d.ts @@ -4,9 +4,9 @@ import type { SoundListType } from '@nbw/database'; interface Window { - latestVersionSoundList: SoundListType; + latestVersionSoundList: SoundListType; } declare global { - var latestVersionSoundList: SoundListType; + var latestVersionSoundList: SoundListType; } diff --git a/apps/frontend/src/lib/axios/ClientAxios.ts b/apps/frontend/src/lib/axios/ClientAxios.ts index cf78f749..a349f439 100644 --- a/apps/frontend/src/lib/axios/ClientAxios.ts +++ b/apps/frontend/src/lib/axios/ClientAxios.ts @@ -5,26 +5,26 @@ import { getTokenLocal } from './token.utils'; export const baseApiURL = process.env.NEXT_PUBLIC_API_URL; const ClientAxios = axios.create({ - baseURL : baseApiURL, - withCredentials: true + baseURL : baseApiURL, + withCredentials: true }); // Add a request interceptor to add the token to the request ClientAxios.interceptors.request.use( - (config) => { - try { - const token = getTokenLocal(); + (config) => { + try { + const token = getTokenLocal(); - config.headers.authorization = `Bearer ${token}`; + config.headers.authorization = `Bearer ${token}`; - return config; - } catch { - return config; + return config; + } catch { + return config; + } + }, + (error) => { + return Promise.reject(error); } - }, - (error) => { - return Promise.reject(error); - } ); export default ClientAxios; diff --git a/apps/frontend/src/lib/axios/index.ts b/apps/frontend/src/lib/axios/index.ts index b2905b6d..4881bc11 100644 --- a/apps/frontend/src/lib/axios/index.ts +++ b/apps/frontend/src/lib/axios/index.ts @@ -3,8 +3,8 @@ import axios from 'axios'; export const baseApiURL = process.env.NEXT_PUBLIC_API_URL; const axiosInstance = axios.create({ - baseURL : baseApiURL, - withCredentials: true + baseURL : baseApiURL, + withCredentials: true }); export default axiosInstance; diff --git a/apps/frontend/src/lib/axios/token.utils.ts b/apps/frontend/src/lib/axios/token.utils.ts index e3ed7f97..4bd0763c 100644 --- a/apps/frontend/src/lib/axios/token.utils.ts +++ b/apps/frontend/src/lib/axios/token.utils.ts @@ -1,23 +1,23 @@ export const getTokenLocal = (): string | never => { - // get the token cookie - const cookie = document.cookie; + // get the token cookie + const cookie = document.cookie; - const token = cookie - .split('; ') - .find((row) => row.startsWith('token')) - ?.split('=')[1]; + const token = cookie + .split('; ') + .find((row) => row.startsWith('token')) + ?.split('=')[1]; - // TODO: should be changed to a redirect to the login page? - if (!token) throw new InvalidTokenError('Token not found'); + // TODO: should be changed to a redirect to the login page? + if (!token) throw new InvalidTokenError('Token not found'); - return token; + return token; }; export class InvalidTokenError extends Error { - constructor(msg: string) { - super(msg); + constructor(msg: string) { + super(msg); - // Set the prototype explicitly. - Object.setPrototypeOf(this, InvalidTokenError.prototype); - } + // Set the prototype explicitly. + Object.setPrototypeOf(this, InvalidTokenError.prototype); + } } diff --git a/apps/frontend/src/lib/posts.ts b/apps/frontend/src/lib/posts.ts index 10e9485c..326a3fe5 100644 --- a/apps/frontend/src/lib/posts.ts +++ b/apps/frontend/src/lib/posts.ts @@ -4,97 +4,97 @@ import path from 'path'; import matter from 'gray-matter'; export type PostType = { - id : string; - title : string; - shortTitle? : string; - date : Date; - image : string; - content : string; - author? : string; - authorImage?: string; + id : string; + title : string; + shortTitle? : string; + date : Date; + image : string; + content : string; + author? : string; + authorImage?: string; }; const blogPostIds = fs - .readdirSync(path.join(process.cwd(), 'posts', 'blog')) - .reduce((acc, fileName) => { + .readdirSync(path.join(process.cwd(), 'posts', 'blog')) + .reduce((acc, fileName) => { // Remove ".md" and date prefix to get post ID - const postId = fileName.replace(/\.md$/, '').split('_')[1]; - acc[postId] = fileName; - return acc; - }, {} as Record); + const postId = fileName.replace(/\.md$/, '').split('_')[1]; + acc[postId] = fileName; + return acc; + }, {} as Record); const helpPostIds = fs - .readdirSync(path.join(process.cwd(), 'posts', 'help')) - .reduce((acc, fileName) => { + .readdirSync(path.join(process.cwd(), 'posts', 'help')) + .reduce((acc, fileName) => { // Remove ".md" and number prefix to get help article ID - const helpId = fileName.replace(/\.md$/, '').split('_')[1]; - acc[helpId] = fileName; - return acc; - }, {} as Record); + const helpId = fileName.replace(/\.md$/, '').split('_')[1]; + acc[helpId] = fileName; + return acc; + }, {} as Record); export function getSortedPostsData( - postsPath: 'help' | 'blog', - sortBy: 'id' | 'date' + postsPath: 'help' | 'blog', + sortBy: 'id' | 'date' ) { - const postsDirectory = path.join(process.cwd(), 'posts', postsPath); + const postsDirectory = path.join(process.cwd(), 'posts', postsPath); - // Get file names under /posts - const fileNames = fs.readdirSync(postsDirectory); + // Get file names under /posts + const fileNames = fs.readdirSync(postsDirectory); - const allPostsData = fileNames.map((fileName) => { + const allPostsData = fileNames.map((fileName) => { // Remove ".md" and prefix from file name to get post ID - const id = fileName.replace(/\.md$/, '').split('_')[1]; - return getPostData(postsPath, id); - }); - - // Sort posts - let sortFunction; - - if (sortBy === 'date') { - sortFunction = (a: PostType, b: PostType) => { - if (a.date < b.date) { - return 1; - } else { - return -1; - } - }; - } else { - sortFunction = (a: PostType, b: PostType) => { - if (a.id > b.id) { - return 1; - } else { - return -1; - } - }; - } - - return allPostsData.sort((a, b) => sortFunction(a, b)); + const id = fileName.replace(/\.md$/, '').split('_')[1]; + return getPostData(postsPath, id); + }); + + // Sort posts + let sortFunction; + + if (sortBy === 'date') { + sortFunction = (a: PostType, b: PostType) => { + if (a.date < b.date) { + return 1; + } else { + return -1; + } + }; + } else { + sortFunction = (a: PostType, b: PostType) => { + if (a.id > b.id) { + return 1; + } else { + return -1; + } + }; + } + + return allPostsData.sort((a, b) => sortFunction(a, b)); } export function getPostData( - postsPath: 'help' | 'blog', - postId: string + postsPath: 'help' | 'blog', + postId: string ): PostType { - // Look for the file in the posts directory that contains postId as suffix - const fileName = - postsPath === 'blog' ? blogPostIds[postId] : helpPostIds[postId]; + // Look for the file in the posts directory that contains postId as suffix + const fileName = + postsPath === 'blog' ? blogPostIds[postId] : helpPostIds[postId]; - if (!fileName) { - throw new Error(`No file found for post ID: ${postId}`); - } + if (!fileName) { + throw new Error(`No file found for post ID: ${postId}`); + } - const fullPath = path.join(process.cwd(), 'posts', postsPath, fileName); + const fullPath = path.join(process.cwd(), 'posts', postsPath, fileName); - // Read markdown file as string - const fileContents = fs.readFileSync(fullPath, 'utf8'); + // Read markdown file as string + const fileContents = fs.readFileSync(fullPath, 'utf8'); - // Use gray-matter to parse the post metadata section - const matterResult = matter(fileContents); + // Use gray-matter to parse the post metadata section + const matterResult = matter(fileContents); - // Combine the data with the id - return { - id : postId, - ...(matterResult.data as Omit), - content: matterResult.content - }; + // Combine the data with the id + return { + id : postId, + ...(matterResult.data as Omit), + content: matterResult.content + }; } diff --git a/apps/frontend/src/lib/tailwind.utils.ts b/apps/frontend/src/lib/tailwind.utils.ts index 9ad0df42..dd53ea89 100644 --- a/apps/frontend/src/lib/tailwind.utils.ts +++ b/apps/frontend/src/lib/tailwind.utils.ts @@ -2,5 +2,5 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)); } diff --git a/apps/frontend/src/modules/auth/components/client/login.util.ts b/apps/frontend/src/modules/auth/components/client/login.util.ts index 98750629..acdfc0fd 100644 --- a/apps/frontend/src/modules/auth/components/client/login.util.ts +++ b/apps/frontend/src/modules/auth/components/client/login.util.ts @@ -3,18 +3,18 @@ import { useEffect } from 'react'; export function deleteAuthCookies() { - // delete cookie - const cookiesToBeDeleted = ['refresh_token', 'token']; + // delete cookie + const cookiesToBeDeleted = ['refresh_token', 'token']; - cookiesToBeDeleted.forEach((cookie) => { - if (!document) return; + cookiesToBeDeleted.forEach((cookie) => { + if (!document) return; - if (process.env.NODE_ENV === 'development') { - document.cookie = `${cookie}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/`; - } else { - document.cookie = `${cookie}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Domain=${process.env.NEXT_PUBLIC_APP_DOMAIN}`; - } - }); + if (process.env.NODE_ENV === 'development') { + document.cookie = `${cookie}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/`; + } else { + document.cookie = `${cookie}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Domain=${process.env.NEXT_PUBLIC_APP_DOMAIN}`; + } + }); } /** @@ -38,18 +38,18 @@ export function deleteAuthCookies() { * @returns {null} This hook does not return anything. */ export function useSignOut() { - //const router = useRouter(); - function signOut() { - deleteAuthCookies(); - /* We have to use window.location.href here, + //const router = useRouter(); + function signOut() { + deleteAuthCookies(); + /* We have to use window.location.href here, because next should clear the cached page in the client side */ - window.location.href = '/'; - } + window.location.href = '/'; + } - useEffect(() => { - signOut(); - }, []); + useEffect(() => { + signOut(); + }, []); - return null; // we don't need to return anything + return null; // we don't need to return anything } diff --git a/apps/frontend/src/modules/auth/features/auth.utils.ts b/apps/frontend/src/modules/auth/features/auth.utils.ts index 9e5c6593..4e353c16 100644 --- a/apps/frontend/src/modules/auth/features/auth.utils.ts +++ b/apps/frontend/src/modules/auth/features/auth.utils.ts @@ -4,54 +4,54 @@ import axiosInstance from '../../../lib/axios'; import { LoggedUserData } from '../types/User'; export function getTokenServer(): { value: string } | null { - const cookieStore = cookies(); - const token = cookieStore.get('token'); + const cookieStore = cookies(); + const token = cookieStore.get('token'); - return token as { value: string } | null; + return token as { value: string } | null; } export const checkLogin = async () => { - // get token from cookies - const token = getTokenServer(); - // if token is not null, redirect to home page - if (!token) return false; - if (!token.value) return false; + // get token from cookies + const token = getTokenServer(); + // if token is not null, redirect to home page + if (!token) return false; + if (!token.value) return false; - try { + try { // verify the token with the server - const res = await axiosInstance.get('/auth/verify', { - headers: { - authorization: `Bearer ${token.value}` - } - }); - - // if the token is valid, redirect to home page - if (res.status === 200) return true; - else return false; - } catch { - return false; - } + const res = await axiosInstance.get('/auth/verify', { + headers: { + authorization: `Bearer ${token.value}` + } + }); + + // if the token is valid, redirect to home page + if (res.status === 200) return true; + else return false; + } catch { + return false; + } }; export const getUserData = async (): Promise => { - // get token from cookies - const token = getTokenServer(); - // if token is not null, redirect to home page - if (!token) throw new Error('No token found'); - if (!token.value) throw new Error('No token found'); + // get token from cookies + const token = getTokenServer(); + // if token is not null, redirect to home page + if (!token) throw new Error('No token found'); + if (!token.value) throw new Error('No token found'); - try { + try { // verify the token with the server - const res = await axiosInstance.get('/user/me', { - headers: { - authorization: `Bearer ${token.value}` - } - }); - - // if the token is valid, redirect to home page - if (res.status === 200) return res.data as LoggedUserData; - else throw new Error('Invalid token'); - } catch { - throw new Error('Invalid token'); - } + const res = await axiosInstance.get('/user/me', { + headers: { + authorization: `Bearer ${token.value}` + } + }); + + // if the token is valid, redirect to home page + if (res.status === 200) return res.data as LoggedUserData; + else throw new Error('Invalid token'); + } catch { + throw new Error('Invalid token'); + } }; diff --git a/apps/frontend/src/modules/auth/types/User.ts b/apps/frontend/src/modules/auth/types/User.ts index ef7c5418..7c243869 100644 --- a/apps/frontend/src/modules/auth/types/User.ts +++ b/apps/frontend/src/modules/auth/types/User.ts @@ -1,57 +1,57 @@ export type UserTokenData = { - id : string; - email : string; - username: string; + id : string; + email : string; + username: string; }; export type LoggedUserData = { - loginStreak : number; - loginCount : number; - playCount : number; - username : string; - publicName : string; - email : string; - description : string; - profileImage : string; - socialLinks : SocialLinks; - prefersDarkTheme: boolean; - creationDate : string; - lastEdited : string; - lastLogin : string; - createdAt : string; - updatedAt : string; - id : string; + loginStreak : number; + loginCount : number; + playCount : number; + username : string; + publicName : string; + email : string; + description : string; + profileImage : string; + socialLinks : SocialLinks; + prefersDarkTheme: boolean; + creationDate : string; + lastEdited : string; + lastLogin : string; + createdAt : string; + updatedAt : string; + id : string; }; export enum SocialLinksTypes { - BANDCAMP = 'bandcamp', - DISCORD = 'discord', - FACEBOOK = 'facebook', - GITHUB = 'github', - INSTAGRAM = 'instagram', - REDDIT = 'reddit', - SNAPCHAT = 'snapchat', - SOUNDCLOUD = 'soundcloud', - SPOTIFY = 'spotify', - STEAM = 'steam', - TELEGRAM = 'telegram', - TIKTOK = 'tiktok', - THREADS = 'threads', - TWITCH = 'twitch', - X = 'x', - YOUTUBE = 'youtube' + BANDCAMP = 'bandcamp', + DISCORD = 'discord', + FACEBOOK = 'facebook', + GITHUB = 'github', + INSTAGRAM = 'instagram', + REDDIT = 'reddit', + SNAPCHAT = 'snapchat', + SOUNDCLOUD = 'soundcloud', + SPOTIFY = 'spotify', + STEAM = 'steam', + TELEGRAM = 'telegram', + TIKTOK = 'tiktok', + THREADS = 'threads', + TWITCH = 'twitch', + X = 'x', + YOUTUBE = 'youtube' } export type SocialLinks = { - [K in SocialLinksTypes]?: string; + [K in SocialLinksTypes]?: string; }; export type UserProfileData = { - lastLogin : Date; - loginStreak : number; - playCount : number; - publicName : string; - description : string; - profileImage: string; - socialLinks : SocialLinks; + lastLogin : Date; + loginStreak : number; + playCount : number; + publicName : string; + description : string; + profileImage: string; + socialLinks : SocialLinks; }; diff --git a/apps/frontend/src/modules/shared/components/client/GenericModal.tsx b/apps/frontend/src/modules/shared/components/client/GenericModal.tsx index 6169d467..ecac96e8 100644 --- a/apps/frontend/src/modules/shared/components/client/GenericModal.tsx +++ b/apps/frontend/src/modules/shared/components/client/GenericModal.tsx @@ -27,8 +27,8 @@ const GenericModal = ({ setIsOpen ? () => setIsOpen(false) : () => { - return; - } + return; + } } > { setIsHidden(true); - setTimeout(() => { - setIsHidden(false); - }, 1000 * 60 * 5); // Reappers after 5 minutes + setTimeout(() => { + setIsHidden(false); + }, 1000 * 60 * 5); // Reappers after 5 minutes }} > ( -

- AdSense Client ID is not set -

- ) +

+ AdSense Client ID is not set +

+ ) : () => null; return isHidden ? ( diff --git a/apps/frontend/src/modules/shared/util/format.ts b/apps/frontend/src/modules/shared/util/format.ts index 37e996f6..9cbd636a 100644 --- a/apps/frontend/src/modules/shared/util/format.ts +++ b/apps/frontend/src/modules/shared/util/format.ts @@ -1,95 +1,95 @@ // TODO: Move to shared/util export const formatDuration = (totalSeconds: number) => { - const minutes = Math.floor(Math.ceil(totalSeconds) / 60); - const seconds = Math.ceil(totalSeconds) % 60; + const minutes = Math.floor(Math.ceil(totalSeconds) / 60); + const seconds = Math.ceil(totalSeconds) % 60; - const formattedTime = `${minutes.toFixed().padStart(1, '0')}:${seconds - .toFixed() - .padStart(2, '0')}`; + const formattedTime = `${minutes.toFixed().padStart(1, '0')}:${seconds + .toFixed() + .padStart(2, '0')}`; - return formattedTime; + return formattedTime; }; export const formatTimeAgo = (date: Date) => { - if (!date) return ''; + if (!date) return ''; - const now = new Date(); - const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); + const now = new Date(); + const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); - if (seconds < 60) { - return 'just now'; - } + if (seconds < 60) { + return 'just now'; + } - const minutes = Math.floor(seconds / 60); + const minutes = Math.floor(seconds / 60); - if (minutes == 1) { - return 'a minute ago'; - } + if (minutes == 1) { + return 'a minute ago'; + } - if (minutes < 60) { - return `${minutes} minutes ago`; - } + if (minutes < 60) { + return `${minutes} minutes ago`; + } - const hours = Math.floor(minutes / 60); + const hours = Math.floor(minutes / 60); - if (hours <= 1) { - return 'an hour ago'; - } + if (hours <= 1) { + return 'an hour ago'; + } - if (hours < 24) { - return `${hours} hours ago`; - } + if (hours < 24) { + return `${hours} hours ago`; + } - const days = Math.floor(hours / 24); + const days = Math.floor(hours / 24); - if (days <= 1) { - return 'yesterday'; - } + if (days <= 1) { + return 'yesterday'; + } - if (days < 7) { - return `${days} days ago`; - } + if (days < 7) { + return `${days} days ago`; + } - const weeks = Math.floor(days / 7); + const weeks = Math.floor(days / 7); - if (weeks <= 1) { - return 'a week ago'; - } + if (weeks <= 1) { + return 'a week ago'; + } - if (weeks < 4) { - return `${weeks} weeks ago`; - } + if (weeks < 4) { + return `${weeks} weeks ago`; + } - const months = Math.floor(days / 30); + const months = Math.floor(days / 30); - if (months <= 1) { - return 'a month ago'; - } + if (months <= 1) { + return 'a month ago'; + } - if (months < 12) { - return `${months} months ago`; - } + if (months < 12) { + return `${months} months ago`; + } - const years = Math.floor(days / 365); + const years = Math.floor(days / 365); - if (years <= 1) { - return 'a year ago'; - } + if (years <= 1) { + return 'a year ago'; + } - return `${years} years ago`; + return `${years} years ago`; }; export const formatTimeSpent = (totalMinutes: number) => { - if (totalMinutes < 60) { - return `${totalMinutes} min`; - } + if (totalMinutes < 60) { + return `${totalMinutes} min`; + } - const hours = Math.floor(totalMinutes / 60); - const minutes = Math.ceil(totalMinutes % 60); + const hours = Math.floor(totalMinutes / 60); + const minutes = Math.ceil(totalMinutes % 60); - if (minutes === 0) { - return `${hours}h`; - } + if (minutes === 0) { + return `${hours}h`; + } - return `${hours}h ${minutes}min`; + return `${hours}h ${minutes}min`; }; diff --git a/apps/frontend/src/modules/song/components/client/SongForm.tsx b/apps/frontend/src/modules/song/components/client/SongForm.tsx index 6b9d1a7d..3b088d4d 100644 --- a/apps/frontend/src/modules/song/components/client/SongForm.tsx +++ b/apps/frontend/src/modules/song/components/client/SongForm.tsx @@ -299,8 +299,8 @@ export const SongForm = ({ formMethods.watch('license') === 'none' ? '' : UPLOAD_CONSTANTS.licenses[ - formMethods.watch('license') as LicenseType - ]?.uploadDescription + formMethods.watch('license') as LicenseType + ]?.uploadDescription } {...register('license')} > diff --git a/apps/frontend/src/modules/song/components/client/SongForm.zod.ts b/apps/frontend/src/modules/song/components/client/SongForm.zod.ts index fe0dfa9c..83b846d6 100644 --- a/apps/frontend/src/modules/song/components/client/SongForm.zod.ts +++ b/apps/frontend/src/modules/song/components/client/SongForm.zod.ts @@ -2,80 +2,80 @@ import { THUMBNAIL_CONSTANTS, UPLOAD_CONSTANTS } from '@nbw/config'; import { z as zod } from 'zod'; export const thumbnailDataSchema = zod.object({ - zoomLevel: zod - .number() - .int() - .min(THUMBNAIL_CONSTANTS.zoomLevel.min) - .max(THUMBNAIL_CONSTANTS.zoomLevel.max) - .default(THUMBNAIL_CONSTANTS.zoomLevel.default), - startTick: zod - .number() - .int() - .min(0) - .default(THUMBNAIL_CONSTANTS.startTick.default), - startLayer: zod - .number() - .int() - .min(0) - .default(THUMBNAIL_CONSTANTS.startLayer.default), - backgroundColor: zod - .string() - .regex(/^#[0-9a-fA-F]{6}$/) - .default(THUMBNAIL_CONSTANTS.backgroundColor.default) + zoomLevel: zod + .number() + .int() + .min(THUMBNAIL_CONSTANTS.zoomLevel.min) + .max(THUMBNAIL_CONSTANTS.zoomLevel.max) + .default(THUMBNAIL_CONSTANTS.zoomLevel.default), + startTick: zod + .number() + .int() + .min(0) + .default(THUMBNAIL_CONSTANTS.startTick.default), + startLayer: zod + .number() + .int() + .min(0) + .default(THUMBNAIL_CONSTANTS.startLayer.default), + backgroundColor: zod + .string() + .regex(/^#[0-9a-fA-F]{6}$/) + .default(THUMBNAIL_CONSTANTS.backgroundColor.default) }); const visibility = Object.keys(UPLOAD_CONSTANTS.visibility) as Readonly< - string[] + string[] >; const categories = Object.keys(UPLOAD_CONSTANTS.categories) as Readonly< - string[] + string[] >; const licenses = Object.keys(UPLOAD_CONSTANTS.licenses) as Readonly; export const SongFormSchema = zod.object({ - allowDownload: zod.boolean().default(true), + allowDownload: zod.boolean().default(true), - // @ts-ignore - visibility: zod.enum(visibility).default('public'), - title : zod - .string() - .max(UPLOAD_CONSTANTS.title.maxLength, { - message: `Title must be shorter than ${UPLOAD_CONSTANTS.title.maxLength} characters` - }) - .min(1, { - message: 'Title is required' + // @ts-ignore + visibility: zod.enum(visibility).default('public'), + title : zod + .string() + .max(UPLOAD_CONSTANTS.title.maxLength, { + message: `Title must be shorter than ${UPLOAD_CONSTANTS.title.maxLength} characters` + }) + .min(1, { + message: 'Title is required' + }), + originalAuthor: zod + .string() + .max(UPLOAD_CONSTANTS.originalAuthor.maxLength, { + message: `Original author must be shorter than ${UPLOAD_CONSTANTS.originalAuthor.maxLength} characters` + }) + .min(0), + author : zod.string().optional(), + description: zod.string().max(UPLOAD_CONSTANTS.description.maxLength, { + message: `Description must be less than ${UPLOAD_CONSTANTS.description.maxLength} characters` }), - originalAuthor: zod - .string() - .max(UPLOAD_CONSTANTS.originalAuthor.maxLength, { - message: `Original author must be shorter than ${UPLOAD_CONSTANTS.originalAuthor.maxLength} characters` - }) - .min(0), - author : zod.string().optional(), - description: zod.string().max(UPLOAD_CONSTANTS.description.maxLength, { - message: `Description must be less than ${UPLOAD_CONSTANTS.description.maxLength} characters` - }), - thumbnailData : thumbnailDataSchema, - customInstruments: zod.array(zod.string()), - license : zod + thumbnailData : thumbnailDataSchema, + customInstruments: zod.array(zod.string()), + license : zod // @ts-ignore - .enum(licenses, { - message: 'Please select a license' - }) - .refine((value) => Object.keys(UPLOAD_CONSTANTS.licenses).includes(value)) - .default(UPLOAD_CONSTANTS.license.default), + .enum(licenses, { + message: 'Please select a license' + }) + .refine((value) => Object.keys(UPLOAD_CONSTANTS.licenses).includes(value)) + .default(UPLOAD_CONSTANTS.license.default), - // @ts-ignore - category: zod.enum(categories).default(UPLOAD_CONSTANTS.CATEGORY_DEFAULT) + // @ts-ignore + category: zod.enum(categories).default(UPLOAD_CONSTANTS.CATEGORY_DEFAULT) }); export const uploadSongFormSchema = SongFormSchema.extend({}); export const editSongFormSchema = SongFormSchema.extend({ - id: zod.string() + id: zod.string() }); export type ThumbnailDataForm = zod.infer; diff --git a/apps/frontend/src/modules/song/util/downloadSong.ts b/apps/frontend/src/modules/song/util/downloadSong.ts index 54840deb..d4a7fc8f 100644 --- a/apps/frontend/src/modules/song/util/downloadSong.ts +++ b/apps/frontend/src/modules/song/util/downloadSong.ts @@ -6,57 +6,57 @@ import axios from '@web/lib/axios'; import { getTokenLocal } from '@web/lib/axios/token.utils'; export const downloadSongFile = async (song: { - publicId: string; - title : string; + publicId: string; + title : string; }) => { - const token = getTokenLocal(); - - axios - .get(`/song/${song.publicId}/download`, { - params: { - src: 'downloadButton' - }, - headers: { - authorization: `Bearer ${token}` - }, - responseType : 'blob', - withCredentials: true - }) - .then((res) => { - const url = window.URL.createObjectURL(res.data); - const link = document.createElement('a'); - link.href = url; - - link.setAttribute('download', `${song.title}.nbs`); - document.body.appendChild(link); - link.click(); - - // Clean up - link.remove(); - window.URL.revokeObjectURL(url); - }); + const token = getTokenLocal(); + + axios + .get(`/song/${song.publicId}/download`, { + params: { + src: 'downloadButton' + }, + headers: { + authorization: `Bearer ${token}` + }, + responseType : 'blob', + withCredentials: true + }) + .then((res) => { + const url = window.URL.createObjectURL(res.data); + const link = document.createElement('a'); + link.href = url; + + link.setAttribute('download', `${song.title}.nbs`); + document.body.appendChild(link); + link.click(); + + // Clean up + link.remove(); + window.URL.revokeObjectURL(url); + }); }; export const openSongInNBS = async (song: { publicId: string }) => { - axios - .get(`/song/${song.publicId}/open`, { - headers: { - src: 'downloadButton' - } - }) - .then((response) => { - const responseUrl = response.data; - const nbsUrl = 'nbs://' + responseUrl; - - // Create a link element and click it to open the song in NBS - const link = document.createElement('a'); - link.href = nbsUrl; - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(nbsUrl); - }) - .catch(() => { - toast.error('Failed to open song in NBS'); - }); + axios + .get(`/song/${song.publicId}/open`, { + headers: { + src: 'downloadButton' + } + }) + .then((response) => { + const responseUrl = response.data; + const nbsUrl = 'nbs://' + responseUrl; + + // Create a link element and click it to open the song in NBS + const link = document.createElement('a'); + link.href = nbsUrl; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(nbsUrl); + }) + .catch(() => { + toast.error('Failed to open song in NBS'); + }); }; diff --git a/apps/frontend/src/modules/user/features/song.util.ts b/apps/frontend/src/modules/user/features/song.util.ts index 8552abd9..b4bd52c8 100644 --- a/apps/frontend/src/modules/user/features/song.util.ts +++ b/apps/frontend/src/modules/user/features/song.util.ts @@ -1,11 +1,11 @@ import axiosInstance from '@web/lib/axios'; export const getUserSongs = async (userId: string) => { - const res = await axiosInstance.get('/song/user', { - params: { - id: userId - } - }); + const res = await axiosInstance.get('/song/user', { + params: { + id: userId + } + }); - return res.data; + return res.data; }; diff --git a/apps/frontend/src/modules/user/features/user.util.ts b/apps/frontend/src/modules/user/features/user.util.ts index cfd083d9..0aac2759 100644 --- a/apps/frontend/src/modules/user/features/user.util.ts +++ b/apps/frontend/src/modules/user/features/user.util.ts @@ -2,13 +2,13 @@ import axiosInstance from '../../../lib/axios'; import { UserProfileData } from '../../auth/types/User'; export const getUserProfileData = async ( - id: string + id: string ): Promise => { - try { - const res = await axiosInstance.get(`/user/?id=${id}`); - if (res.status === 200) return res.data as UserProfileData; - else throw new Error('Failed to get user data'); - } catch { - throw new Error('Failed to get user data'); - } + try { + const res = await axiosInstance.get(`/user/?id=${id}`); + if (res.status === 200) return res.data as UserProfileData; + else throw new Error('Failed to get user data'); + } catch { + throw new Error('Failed to get user data'); + } }; diff --git a/packages/configs/src/colors.ts b/packages/configs/src/colors.ts index 1cdab6f5..8c3d4995 100644 --- a/packages/configs/src/colors.ts +++ b/packages/configs/src/colors.ts @@ -25,114 +25,114 @@ https://tailwindcss.com/docs/customizing-colors import colors from 'tailwindcss/colors'; export const BG_COLORS = { - red: { - key : 'red', - name : 'Red', - light: colors.red[400], - dark : colors.red[900] - }, - orange: { - key : 'orange', - name : 'Orange', - light: colors.orange[400], - dark : colors.orange[900] - }, - amber: { - key : 'amber', - name : 'Amber', - light: colors.amber[400], - dark : colors.amber[900] - }, - yellow: { - key : 'yellow', - name : 'Yellow', - light: colors.yellow[400], - dark : colors.yellow[900] - }, - lime: { - key : 'lime', - name : 'Lime', - light: colors.lime[400], - dark : colors.lime[900] - }, - green: { - key : 'green', - name : 'Green', - light: colors.green[400], - dark : colors.green[900] - }, - emerald: { - key : 'emerald', - name : 'Emerald', - light: colors.emerald[400], - dark : colors.emerald[900] - }, - teal: { - key : 'teal', - name : 'Teal', - light: colors.teal[400], - dark : colors.teal[900] - }, - cyan: { - key : 'cyan', - name : 'Cyan', - light: colors.cyan[400], - dark : colors.cyan[900] - }, - sky: { - key : 'sky', - name : 'Sky', - light: colors.sky[400], - dark : colors.sky[900] - }, - blue: { - key : 'blue', - name : 'Blue', - light: colors.blue[400], - dark : colors.blue[900] - }, - indigo: { - key : 'indigo', - name : 'Indigo', - light: colors.indigo[400], - dark : colors.indigo[900] - }, - violet: { - key : 'violet', - name : 'Violet', - light: colors.violet[400], - dark : colors.violet[900] - }, - purple: { - key : 'purple', - name : 'Purple', - light: colors.purple[400], - dark : colors.purple[900] - }, - fuchsia: { - key : 'fuchsia', - name : 'Fuchsia', - light: colors.fuchsia[400], - dark : colors.fuchsia[900] - }, - pink: { - key : 'pink', - name : 'Pink', - light: colors.pink[400], - dark : colors.pink[900] - }, - rose: { - key : 'rose', - name : 'Rose', - light: colors.rose[400], - dark : colors.rose[900] - }, - gray: { - key : 'gray', - name : 'Gray', - light: colors.zinc[200], - dark : colors.zinc[800] - } + red: { + key : 'red', + name : 'Red', + light: colors.red[400], + dark : colors.red[900] + }, + orange: { + key : 'orange', + name : 'Orange', + light: colors.orange[400], + dark : colors.orange[900] + }, + amber: { + key : 'amber', + name : 'Amber', + light: colors.amber[400], + dark : colors.amber[900] + }, + yellow: { + key : 'yellow', + name : 'Yellow', + light: colors.yellow[400], + dark : colors.yellow[900] + }, + lime: { + key : 'lime', + name : 'Lime', + light: colors.lime[400], + dark : colors.lime[900] + }, + green: { + key : 'green', + name : 'Green', + light: colors.green[400], + dark : colors.green[900] + }, + emerald: { + key : 'emerald', + name : 'Emerald', + light: colors.emerald[400], + dark : colors.emerald[900] + }, + teal: { + key : 'teal', + name : 'Teal', + light: colors.teal[400], + dark : colors.teal[900] + }, + cyan: { + key : 'cyan', + name : 'Cyan', + light: colors.cyan[400], + dark : colors.cyan[900] + }, + sky: { + key : 'sky', + name : 'Sky', + light: colors.sky[400], + dark : colors.sky[900] + }, + blue: { + key : 'blue', + name : 'Blue', + light: colors.blue[400], + dark : colors.blue[900] + }, + indigo: { + key : 'indigo', + name : 'Indigo', + light: colors.indigo[400], + dark : colors.indigo[900] + }, + violet: { + key : 'violet', + name : 'Violet', + light: colors.violet[400], + dark : colors.violet[900] + }, + purple: { + key : 'purple', + name : 'Purple', + light: colors.purple[400], + dark : colors.purple[900] + }, + fuchsia: { + key : 'fuchsia', + name : 'Fuchsia', + light: colors.fuchsia[400], + dark : colors.fuchsia[900] + }, + pink: { + key : 'pink', + name : 'Pink', + light: colors.pink[400], + dark : colors.pink[900] + }, + rose: { + key : 'rose', + name : 'Rose', + light: colors.rose[400], + dark : colors.rose[900] + }, + gray: { + key : 'gray', + name : 'Gray', + light: colors.zinc[200], + dark : colors.zinc[800] + } } as const; export const bgColorsArray = Object.values(BG_COLORS); diff --git a/packages/configs/src/song.ts b/packages/configs/src/song.ts index 68cc327f..7c411774 100644 --- a/packages/configs/src/song.ts +++ b/packages/configs/src/song.ts @@ -1,131 +1,131 @@ import { BG_COLORS } from './colors'; export const THUMBNAIL_CONSTANTS = { - zoomLevel: { - min : 1, - max : 5, - default: 3 - }, - startTick: { - default: 0 - }, - startLayer: { - default: 0 - }, - backgroundColor: { - default: BG_COLORS.gray.dark - } + zoomLevel: { + min : 1, + max : 5, + default: 3 + }, + startTick: { + default: 0 + }, + startLayer: { + default: 0 + }, + backgroundColor: { + default: BG_COLORS.gray.dark + } } as const; export const MIMETYPE_NBS = 'application/octet-stream' as const; export const UPLOAD_CONSTANTS = { - file: { - maxSize: 1024 * 1024 * 3 // 3 MB - }, + file: { + maxSize: 1024 * 1024 * 3 // 3 MB + }, - title: { - minLength: 3, - maxLength: 100 - }, + title: { + minLength: 3, + maxLength: 100 + }, - description: { - maxLength: 1000 - }, + description: { + maxLength: 1000 + }, - originalAuthor: { - maxLength: 50 - }, + originalAuthor: { + maxLength: 50 + }, - category: { - default: 'none' - }, + category: { + default: 'none' + }, - license: { - default: 'none' - }, + license: { + default: 'none' + }, - customInstruments: { - maxCount: 240 - }, + customInstruments: { + maxCount: 240 + }, - categories: { - none : 'No category', - rock : 'Rock', - pop : 'Pop', - jazz : 'Jazz', - blues : 'Blues', - country : 'Country', - metal : 'Metal', - hiphop : 'Hip-Hop', - rap : 'Rap', - reggae : 'Reggae', - classical : 'Classical', - electronic : 'Electronic', - dance : 'Dance', - rnb : 'R&B', - soul : 'Soul', - funk : 'Funk', - punk : 'Punk', - alternative : 'Alternative', - indie : 'Indie', - folk : 'Folk', - latin : 'Latin', - world : 'World', - other : 'Other', - vocaloid : 'Vocaloid', - soundtrack : 'Soundtrack', - instrumental: 'Instrumental', - ambient : 'Ambient', - gaming : 'Gaming', - anime : 'Anime', - movies_tv : 'Movies & TV', - chiptune : 'Chiptune', - lofi : 'Lofi', - kpop : 'K-pop', - jpop : 'J-pop' - }, + categories: { + none : 'No category', + rock : 'Rock', + pop : 'Pop', + jazz : 'Jazz', + blues : 'Blues', + country : 'Country', + metal : 'Metal', + hiphop : 'Hip-Hop', + rap : 'Rap', + reggae : 'Reggae', + classical : 'Classical', + electronic : 'Electronic', + dance : 'Dance', + rnb : 'R&B', + soul : 'Soul', + funk : 'Funk', + punk : 'Punk', + alternative : 'Alternative', + indie : 'Indie', + folk : 'Folk', + latin : 'Latin', + world : 'World', + other : 'Other', + vocaloid : 'Vocaloid', + soundtrack : 'Soundtrack', + instrumental: 'Instrumental', + ambient : 'Ambient', + gaming : 'Gaming', + anime : 'Anime', + movies_tv : 'Movies & TV', + chiptune : 'Chiptune', + lofi : 'Lofi', + kpop : 'K-pop', + jpop : 'J-pop' + }, - licenses: { - standard: { - name : 'Standard License', - shortName: 'Standard License', - description: + licenses: { + standard: { + name : 'Standard License', + shortName: 'Standard License', + description: "The author reserves all rights. You may not use this song without the author's permission.", - uploadDescription: + uploadDescription: 'You allow us to distribute your song on Note Block World. Other users can listen to it, but they cannot use the song without your permission.' - }, - cc_by_sa: { - name : 'Creative Commons - Attribution-ShareAlike 4.0', - shortName: 'CC BY-SA 4.0', - description: + }, + cc_by_sa: { + name : 'Creative Commons - Attribution-ShareAlike 4.0', + shortName: 'CC BY-SA 4.0', + description: 'You can copy, modify, and distribute this song, even for commercial purposes, as long as you credit the author and provide a link to the song. If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.\n\nFor more information, please visit the [Creative Commons website](https://creativecommons.org/licenses/by-sa/4.0/).', - uploadDescription: + uploadDescription: 'Anyone can copy, modify, remix, adapt and distribute this song, even for commercial purposes, as long as attribution is provided and the modifications are distributed under the same license.\nFor more information, visit the [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) website.' - } - }, + } + }, - visibility: { - public : 'Public', - private: 'Private' - } + visibility: { + public : 'Public', + private: 'Private' + } } as const; export const TIMESPANS = [ - 'hour', - 'day', - 'week', - 'month', - 'year', - 'all' + 'hour', + 'day', + 'week', + 'month', + 'year', + 'all' ] as const; export const MY_SONGS = { - PAGE_SIZE: 10, - SORT : 'createdAt' + PAGE_SIZE: 10, + SORT : 'createdAt' } as const; export const BROWSER_SONGS = { - featuredPageSize : 10, - paddedFeaturedPageSize: 5 + featuredPageSize : 10, + paddedFeaturedPageSize: 5 } as const; diff --git a/packages/configs/src/user.ts b/packages/configs/src/user.ts index 9daf496e..213473d4 100644 --- a/packages/configs/src/user.ts +++ b/packages/configs/src/user.ts @@ -1,5 +1,5 @@ export const USER_CONSTANTS = { - USERNAME_MIN_LENGTH: 3, - USERNAME_MAX_LENGTH: 32, - ALLOWED_REGEXP : /^[a-zA-Z0-9-_.]*$/ + USERNAME_MIN_LENGTH: 3, + USERNAME_MAX_LENGTH: 32, + ALLOWED_REGEXP : /^[a-zA-Z0-9-_.]*$/ } as const; diff --git a/packages/database/src/common/dto/PageQuery.dto.ts b/packages/database/src/common/dto/PageQuery.dto.ts index 1fd7ed6a..23ea7591 100644 --- a/packages/database/src/common/dto/PageQuery.dto.ts +++ b/packages/database/src/common/dto/PageQuery.dto.ts @@ -2,69 +2,69 @@ import { TIMESPANS } from '@nbw/config'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { - IsBoolean, - IsEnum, - IsNotEmpty, - IsNumber, - IsOptional, - IsString, - Max, - Min + IsBoolean, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + Max, + Min } from 'class-validator'; import type { TimespanType } from '../../song/dto/types'; export class PageQueryDTO { - @Min(1) - @ApiProperty({ - example : 1, - description: 'page' - }) - page?: number = 1; + @Min(1) + @ApiProperty({ + example : 1, + description: 'page' + }) + page?: number = 1; - @IsNotEmpty() - @IsNumber({ - allowNaN : false, - allowInfinity : false, - maxDecimalPlaces: 0 - }) - @Min(1) - @Max(100) - @ApiProperty({ - example : 20, - description: 'limit' - }) - limit?: number; + @IsNotEmpty() + @IsNumber({ + allowNaN : false, + allowInfinity : false, + maxDecimalPlaces: 0 + }) + @Min(1) + @Max(100) + @ApiProperty({ + example : 20, + description: 'limit' + }) + limit?: number; - @IsString() - @IsOptional() - @ApiProperty({ - example : 'field', - description: 'Sorts the results by the specified field.', - required : false - }) - sort?: string = 'createdAt'; + @IsString() + @IsOptional() + @ApiProperty({ + example : 'field', + description: 'Sorts the results by the specified field.', + required : false + }) + sort?: string = 'createdAt'; - @IsBoolean() - @Transform(({ value }) => value === 'true') - @ApiProperty({ - example: false, - description: + @IsBoolean() + @Transform(({ value }) => value === 'true') + @ApiProperty({ + example: false, + description: 'Sorts the results in ascending order if true; in descending order if false.', - required: false - }) - order?: boolean = false; + required: false + }) + order?: boolean = false; - @IsEnum(TIMESPANS) - @IsOptional() - @ApiProperty({ - example : 'hour', - description: 'Filters the results by the specified timespan.', - required : false - }) - timespan?: TimespanType; + @IsEnum(TIMESPANS) + @IsOptional() + @ApiProperty({ + example : 'hour', + description: 'Filters the results by the specified timespan.', + required : false + }) + timespan?: TimespanType; - constructor(partial: Partial) { - Object.assign(this, partial); - } + constructor(partial: Partial) { + Object.assign(this, partial); + } } diff --git a/packages/database/src/song/dto/CustomInstrumentData.dto.ts b/packages/database/src/song/dto/CustomInstrumentData.dto.ts index 8cb3e835..3adf03ca 100644 --- a/packages/database/src/song/dto/CustomInstrumentData.dto.ts +++ b/packages/database/src/song/dto/CustomInstrumentData.dto.ts @@ -1,6 +1,6 @@ import { IsNotEmpty } from 'class-validator'; export class CustomInstrumentData { - @IsNotEmpty() - sound: string[]; + @IsNotEmpty() + sound: string[]; } diff --git a/packages/database/src/song/dto/FeaturedSongsDto.dto.ts b/packages/database/src/song/dto/FeaturedSongsDto.dto.ts index 346df4db..21e821b6 100644 --- a/packages/database/src/song/dto/FeaturedSongsDto.dto.ts +++ b/packages/database/src/song/dto/FeaturedSongsDto.dto.ts @@ -1,21 +1,21 @@ import { SongPreviewDto } from './SongPreview.dto'; export class FeaturedSongsDto { - hour : SongPreviewDto[]; - day : SongPreviewDto[]; - week : SongPreviewDto[]; - month: SongPreviewDto[]; - year : SongPreviewDto[]; - all : SongPreviewDto[]; + hour : SongPreviewDto[]; + day : SongPreviewDto[]; + week : SongPreviewDto[]; + month: SongPreviewDto[]; + year : SongPreviewDto[]; + all : SongPreviewDto[]; - public static create(): FeaturedSongsDto { - return { - hour : [], - day : [], - week : [], - month: [], - year : [], - all : [] - }; - } + public static create(): FeaturedSongsDto { + return { + hour : [], + day : [], + week : [], + month: [], + year : [], + all : [] + }; + } } diff --git a/packages/database/src/song/dto/SongPage.dto.ts b/packages/database/src/song/dto/SongPage.dto.ts index c83af5d9..a6c51fba 100644 --- a/packages/database/src/song/dto/SongPage.dto.ts +++ b/packages/database/src/song/dto/SongPage.dto.ts @@ -3,32 +3,32 @@ import { IsArray, IsNotEmpty, IsNumber, ValidateNested } from 'class-validator'; import { SongPreviewDto } from './SongPreview.dto'; export class SongPageDto { - @IsNotEmpty() - @IsArray() - @ValidateNested() - content: Array; + @IsNotEmpty() + @IsArray() + @ValidateNested() + content: Array; - @IsNotEmpty() - @IsNumber({ - allowNaN : false, - allowInfinity : false, - maxDecimalPlaces: 0 - }) - page: number; + @IsNotEmpty() + @IsNumber({ + allowNaN : false, + allowInfinity : false, + maxDecimalPlaces: 0 + }) + page: number; - @IsNotEmpty() - @IsNumber({ - allowNaN : false, - allowInfinity : false, - maxDecimalPlaces: 0 - }) - limit: number; + @IsNotEmpty() + @IsNumber({ + allowNaN : false, + allowInfinity : false, + maxDecimalPlaces: 0 + }) + limit: number; - @IsNotEmpty() - @IsNumber({ - allowNaN : false, - allowInfinity : false, - maxDecimalPlaces: 0 - }) - total: number; + @IsNotEmpty() + @IsNumber({ + allowNaN : false, + allowInfinity : false, + maxDecimalPlaces: 0 + }) + total: number; } diff --git a/packages/database/src/song/dto/SongPreview.dto.ts b/packages/database/src/song/dto/SongPreview.dto.ts index a11c4a46..c98bfeb5 100644 --- a/packages/database/src/song/dto/SongPreview.dto.ts +++ b/packages/database/src/song/dto/SongPreview.dto.ts @@ -3,73 +3,73 @@ import { IsNotEmpty, IsString, IsUrl, MaxLength } from 'class-validator'; import type { SongWithUser } from '../entity/song.entity'; type SongPreviewUploader = { - username : string; - profileImage: string; + username : string; + profileImage: string; }; export class SongPreviewDto { - @IsString() - @IsNotEmpty() - publicId: string; - - @IsNotEmpty() - uploader: SongPreviewUploader; - - @IsNotEmpty() - @IsString() - @MaxLength(128) - title: string; - - @IsNotEmpty() - @IsString() - description: string; - - @IsNotEmpty() - @IsString() - @MaxLength(64) - originalAuthor: string; - - @IsNotEmpty() - duration: number; - - @IsNotEmpty() - noteCount: number; - - @IsNotEmpty() - @IsUrl() - thumbnailUrl: string; - - @IsNotEmpty() - createdAt: Date; - - @IsNotEmpty() - updatedAt: Date; - - @IsNotEmpty() - playCount: number; - - @IsNotEmpty() - @IsString() - visibility: string; - - constructor(partial: Partial) { - Object.assign(this, partial); - } - - public static fromSongDocumentWithUser(song: SongWithUser): SongPreviewDto { - return new SongPreviewDto({ - publicId : song.publicId, - uploader : song.uploader, - title : song.title, - description : song.description, - originalAuthor: song.originalAuthor, - duration : song.stats.duration, - noteCount : song.stats.noteCount, - thumbnailUrl : song.thumbnailUrl, - createdAt : song.createdAt, - updatedAt : song.updatedAt, - playCount : song.playCount, - visibility : song.visibility - }); - } + @IsString() + @IsNotEmpty() + publicId: string; + + @IsNotEmpty() + uploader: SongPreviewUploader; + + @IsNotEmpty() + @IsString() + @MaxLength(128) + title: string; + + @IsNotEmpty() + @IsString() + description: string; + + @IsNotEmpty() + @IsString() + @MaxLength(64) + originalAuthor: string; + + @IsNotEmpty() + duration: number; + + @IsNotEmpty() + noteCount: number; + + @IsNotEmpty() + @IsUrl() + thumbnailUrl: string; + + @IsNotEmpty() + createdAt: Date; + + @IsNotEmpty() + updatedAt: Date; + + @IsNotEmpty() + playCount: number; + + @IsNotEmpty() + @IsString() + visibility: string; + + constructor(partial: Partial) { + Object.assign(this, partial); + } + + public static fromSongDocumentWithUser(song: SongWithUser): SongPreviewDto { + return new SongPreviewDto({ + publicId : song.publicId, + uploader : song.uploader, + title : song.title, + description : song.description, + originalAuthor: song.originalAuthor, + duration : song.stats.duration, + noteCount : song.stats.noteCount, + thumbnailUrl : song.thumbnailUrl, + createdAt : song.createdAt, + updatedAt : song.updatedAt, + playCount : song.playCount, + visibility : song.visibility + }); + } } diff --git a/packages/database/src/song/dto/SongStats.ts b/packages/database/src/song/dto/SongStats.ts index db904fa7..4553d2f8 100644 --- a/packages/database/src/song/dto/SongStats.ts +++ b/packages/database/src/song/dto/SongStats.ts @@ -1,69 +1,69 @@ import { - IsBoolean, - IsInt, - IsNumber, - IsString, - ValidateIf + IsBoolean, + IsInt, + IsNumber, + IsString, + ValidateIf } from 'class-validator'; export class SongStats { - @IsString() - midiFileName: string; + @IsString() + midiFileName: string; - @IsInt() - noteCount: number; + @IsInt() + noteCount: number; - @IsInt() - tickCount: number; + @IsInt() + tickCount: number; - @IsInt() - layerCount: number; + @IsInt() + layerCount: number; - @IsNumber() - tempo: number; + @IsNumber() + tempo: number; - @IsNumber() - @ValidateIf((_, value) => value !== null) - tempoRange: number[] | null; + @IsNumber() + @ValidateIf((_, value) => value !== null) + tempoRange: number[] | null; - @IsNumber() - timeSignature: number; + @IsNumber() + timeSignature: number; - @IsNumber() - duration: number; + @IsNumber() + duration: number; - @IsBoolean() - loop: boolean; + @IsBoolean() + loop: boolean; - @IsInt() - loopStartTick: number; + @IsInt() + loopStartTick: number; - @IsNumber() - minutesSpent: number; + @IsNumber() + minutesSpent: number; - @IsInt() - vanillaInstrumentCount: number; + @IsInt() + vanillaInstrumentCount: number; - @IsInt() - customInstrumentCount: number; + @IsInt() + customInstrumentCount: number; - @IsInt() - firstCustomInstrumentIndex: number; + @IsInt() + firstCustomInstrumentIndex: number; - @IsInt() - outOfRangeNoteCount: number; + @IsInt() + outOfRangeNoteCount: number; - @IsInt() - detunedNoteCount: number; + @IsInt() + detunedNoteCount: number; - @IsInt() - customInstrumentNoteCount: number; + @IsInt() + customInstrumentNoteCount: number; - @IsInt() - incompatibleNoteCount: number; + @IsInt() + incompatibleNoteCount: number; - @IsBoolean() - compatible: boolean; + @IsBoolean() + compatible: boolean; - instrumentNoteCounts: number[]; + instrumentNoteCounts: number[]; } diff --git a/packages/database/src/song/dto/SongView.dto.ts b/packages/database/src/song/dto/SongView.dto.ts index c33f5a77..ac2641f9 100644 --- a/packages/database/src/song/dto/SongView.dto.ts +++ b/packages/database/src/song/dto/SongView.dto.ts @@ -1,10 +1,10 @@ import { - IsBoolean, - IsDate, - IsNotEmpty, - IsNumber, - IsString, - IsUrl + IsBoolean, + IsDate, + IsNotEmpty, + IsNumber, + IsString, + IsUrl } from 'class-validator'; import type { SongDocument } from '../entity/song.entity'; @@ -13,96 +13,96 @@ import { SongStats } from './SongStats'; import type { CategoryType, LicenseType, VisibilityType } from './types'; export type SongViewUploader = { - username : string; - profileImage: string; + username : string; + profileImage: string; }; export class SongViewDto { - @IsString() - @IsNotEmpty() - publicId: string; - - @IsDate() - @IsNotEmpty() - createdAt: Date; - - @IsNotEmpty() - uploader: SongViewUploader; - - @IsUrl() - @IsNotEmpty() - thumbnailUrl: string; - - @IsNumber() - @IsNotEmpty() - playCount: number; - - @IsNumber() - @IsNotEmpty() - downloadCount: number; - - @IsNumber() - @IsNotEmpty() - likeCount: number; - - @IsBoolean() - @IsNotEmpty() - allowDownload: boolean; - - @IsString() - @IsNotEmpty() - title: string; - - @IsString() - originalAuthor: string; - - @IsString() - description: string; - - @IsString() - @IsNotEmpty() - visibility: VisibilityType; - - @IsString() - @IsNotEmpty() - category: CategoryType; - - @IsString() - @IsNotEmpty() - license: LicenseType; - - customInstruments: string[]; - - @IsNumber() - @IsNotEmpty() - fileSize: number; - - @IsNotEmpty() - stats: SongStats; - - public static fromSongDocument(song: SongDocument): SongViewDto { - return new SongViewDto({ - publicId : song.publicId, - createdAt : song.createdAt, - uploader : song.uploader as unknown as SongViewUploader, - thumbnailUrl : song.thumbnailUrl, - playCount : song.playCount, - downloadCount : song.downloadCount, - likeCount : song.likeCount, - allowDownload : song.allowDownload, - title : song.title, - originalAuthor : song.originalAuthor, - description : song.description, - category : song.category, - visibility : song.visibility, - license : song.license, - customInstruments: song.customInstruments, - fileSize : song.fileSize, - stats : song.stats - }); - } - - constructor(song: SongViewDto) { - Object.assign(this, song); - } + @IsString() + @IsNotEmpty() + publicId: string; + + @IsDate() + @IsNotEmpty() + createdAt: Date; + + @IsNotEmpty() + uploader: SongViewUploader; + + @IsUrl() + @IsNotEmpty() + thumbnailUrl: string; + + @IsNumber() + @IsNotEmpty() + playCount: number; + + @IsNumber() + @IsNotEmpty() + downloadCount: number; + + @IsNumber() + @IsNotEmpty() + likeCount: number; + + @IsBoolean() + @IsNotEmpty() + allowDownload: boolean; + + @IsString() + @IsNotEmpty() + title: string; + + @IsString() + originalAuthor: string; + + @IsString() + description: string; + + @IsString() + @IsNotEmpty() + visibility: VisibilityType; + + @IsString() + @IsNotEmpty() + category: CategoryType; + + @IsString() + @IsNotEmpty() + license: LicenseType; + + customInstruments: string[]; + + @IsNumber() + @IsNotEmpty() + fileSize: number; + + @IsNotEmpty() + stats: SongStats; + + public static fromSongDocument(song: SongDocument): SongViewDto { + return new SongViewDto({ + publicId : song.publicId, + createdAt : song.createdAt, + uploader : song.uploader as unknown as SongViewUploader, + thumbnailUrl : song.thumbnailUrl, + playCount : song.playCount, + downloadCount : song.downloadCount, + likeCount : song.likeCount, + allowDownload : song.allowDownload, + title : song.title, + originalAuthor : song.originalAuthor, + description : song.description, + category : song.category, + visibility : song.visibility, + license : song.license, + customInstruments: song.customInstruments, + fileSize : song.fileSize, + stats : song.stats + }); + } + + constructor(song: SongViewDto) { + Object.assign(this, song); + } } diff --git a/packages/database/src/song/dto/ThumbnailData.dto.ts b/packages/database/src/song/dto/ThumbnailData.dto.ts index 82f11669..bd87655d 100644 --- a/packages/database/src/song/dto/ThumbnailData.dto.ts +++ b/packages/database/src/song/dto/ThumbnailData.dto.ts @@ -3,47 +3,47 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsHexColor, IsInt, IsNotEmpty, Max, Min } from 'class-validator'; export class ThumbnailData { - @IsNotEmpty() - @Max(THUMBNAIL_CONSTANTS.zoomLevel.max) - @Min(THUMBNAIL_CONSTANTS.zoomLevel.min) - @IsInt() - @ApiProperty({ - description: 'Zoom level of the cover image', - example : THUMBNAIL_CONSTANTS.zoomLevel.default - }) - zoomLevel: number; + @IsNotEmpty() + @Max(THUMBNAIL_CONSTANTS.zoomLevel.max) + @Min(THUMBNAIL_CONSTANTS.zoomLevel.min) + @IsInt() + @ApiProperty({ + description: 'Zoom level of the cover image', + example : THUMBNAIL_CONSTANTS.zoomLevel.default + }) + zoomLevel: number; - @IsNotEmpty() - @Min(0) - @IsInt() - @ApiProperty({ - description: 'X position of the cover image', - example : THUMBNAIL_CONSTANTS.startTick.default - }) - startTick: number; + @IsNotEmpty() + @Min(0) + @IsInt() + @ApiProperty({ + description: 'X position of the cover image', + example : THUMBNAIL_CONSTANTS.startTick.default + }) + startTick: number; - @IsNotEmpty() - @Min(0) - @ApiProperty({ - description: 'Y position of the cover image', - example : THUMBNAIL_CONSTANTS.startLayer.default - }) - startLayer: number; + @IsNotEmpty() + @Min(0) + @ApiProperty({ + description: 'Y position of the cover image', + example : THUMBNAIL_CONSTANTS.startLayer.default + }) + startLayer: number; - @IsNotEmpty() - @IsHexColor() - @ApiProperty({ - description: 'Background color of the cover image', - example : THUMBNAIL_CONSTANTS.backgroundColor.default - }) - backgroundColor: string; + @IsNotEmpty() + @IsHexColor() + @ApiProperty({ + description: 'Background color of the cover image', + example : THUMBNAIL_CONSTANTS.backgroundColor.default + }) + backgroundColor: string; - static getApiExample(): ThumbnailData { - return { - zoomLevel : 3, - startTick : 0, - startLayer : 0, - backgroundColor: '#F0F0F0' - }; - } + static getApiExample(): ThumbnailData { + return { + zoomLevel : 3, + startTick : 0, + startLayer : 0, + backgroundColor: '#F0F0F0' + }; + } } diff --git a/packages/database/src/song/dto/UploadSongDto.dto.ts b/packages/database/src/song/dto/UploadSongDto.dto.ts index 5cb0c83f..14183af1 100644 --- a/packages/database/src/song/dto/UploadSongDto.dto.ts +++ b/packages/database/src/song/dto/UploadSongDto.dto.ts @@ -2,13 +2,13 @@ import { UPLOAD_CONSTANTS } from '@nbw/config'; import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { - IsArray, - IsBoolean, - IsIn, - IsNotEmpty, - IsString, - MaxLength, - ValidateNested + IsArray, + IsBoolean, + IsIn, + IsNotEmpty, + IsString, + MaxLength, + ValidateNested } from 'class-validator'; import type { SongDocument } from '../entity/song.entity'; @@ -17,125 +17,125 @@ import { ThumbnailData } from './ThumbnailData.dto'; import type { CategoryType, LicenseType, VisibilityType } from './types'; const visibility = Object.keys(UPLOAD_CONSTANTS.visibility) as Readonly< - string[] + string[] >; const categories = Object.keys(UPLOAD_CONSTANTS.categories) as Readonly< - string[] + string[] >; const licenses = Object.keys(UPLOAD_CONSTANTS.licenses) as Readonly; export class UploadSongDto { - @ApiProperty({ - description: 'The file to upload', - - // @ts-ignore //TODO: fix this - type: 'file' - }) - file: any; //TODO: Express.Multer.File; - - @IsNotEmpty() - @IsBoolean() - @Type(() => Boolean) - @ApiProperty({ - default : true, - description: 'Whether the song can be downloaded by other users', - example : true - }) - allowDownload: boolean; - - @IsNotEmpty() - @IsString() - @IsIn(visibility) - @ApiProperty({ - enum : visibility, - default : visibility[0], - description: 'The visibility of the song', - example : visibility[0] - }) - visibility: VisibilityType; - - @IsNotEmpty() - @IsString() - @MaxLength(UPLOAD_CONSTANTS.title.maxLength) - @ApiProperty({ - description: 'Title of the song', - example : 'My Song' - }) - title: string; - - @IsString() - @MaxLength(UPLOAD_CONSTANTS.originalAuthor.maxLength) - @ApiProperty({ - description: 'Original author of the song', - example : 'Myself' - }) - originalAuthor: string; - - @IsString() - @MaxLength(UPLOAD_CONSTANTS.description.maxLength) - @ApiProperty({ - description: 'Description of the song', - example : 'This is my song' - }) - description: string; - - @IsNotEmpty() - @IsString() - @IsIn(categories) - @ApiProperty({ - enum : categories, - description: 'Category of the song', - example : categories[0] - }) - category: CategoryType; - - @IsNotEmpty() - @ValidateNested() - @Type(() => ThumbnailData) - @Transform(({ value }) => JSON.parse(value)) - @ApiProperty({ - description: 'Thumbnail data of the song', - example : ThumbnailData.getApiExample() - }) - thumbnailData: ThumbnailData; - - @IsNotEmpty() - @IsString() - @IsIn(licenses) - @ApiProperty({ - enum : licenses, - default : licenses[0], - description: 'The visibility of the song', - example : licenses[0] - }) - license: LicenseType; - - @IsArray() - @MaxLength(UPLOAD_CONSTANTS.customInstruments.maxCount, { each: true }) - @ApiProperty({ - description: + @ApiProperty({ + description: 'The file to upload', + + // @ts-ignore //TODO: fix this + type: 'file' + }) + file: any; //TODO: Express.Multer.File; + + @IsNotEmpty() + @IsBoolean() + @Type(() => Boolean) + @ApiProperty({ + default : true, + description: 'Whether the song can be downloaded by other users', + example : true + }) + allowDownload: boolean; + + @IsNotEmpty() + @IsString() + @IsIn(visibility) + @ApiProperty({ + enum : visibility, + default : visibility[0], + description: 'The visibility of the song', + example : visibility[0] + }) + visibility: VisibilityType; + + @IsNotEmpty() + @IsString() + @MaxLength(UPLOAD_CONSTANTS.title.maxLength) + @ApiProperty({ + description: 'Title of the song', + example : 'My Song' + }) + title: string; + + @IsString() + @MaxLength(UPLOAD_CONSTANTS.originalAuthor.maxLength) + @ApiProperty({ + description: 'Original author of the song', + example : 'Myself' + }) + originalAuthor: string; + + @IsString() + @MaxLength(UPLOAD_CONSTANTS.description.maxLength) + @ApiProperty({ + description: 'Description of the song', + example : 'This is my song' + }) + description: string; + + @IsNotEmpty() + @IsString() + @IsIn(categories) + @ApiProperty({ + enum : categories, + description: 'Category of the song', + example : categories[0] + }) + category: CategoryType; + + @IsNotEmpty() + @ValidateNested() + @Type(() => ThumbnailData) + @Transform(({ value }) => JSON.parse(value)) + @ApiProperty({ + description: 'Thumbnail data of the song', + example : ThumbnailData.getApiExample() + }) + thumbnailData: ThumbnailData; + + @IsNotEmpty() + @IsString() + @IsIn(licenses) + @ApiProperty({ + enum : licenses, + default : licenses[0], + description: 'The visibility of the song', + example : licenses[0] + }) + license: LicenseType; + + @IsArray() + @MaxLength(UPLOAD_CONSTANTS.customInstruments.maxCount, { each: true }) + @ApiProperty({ + description: 'List of custom instrument paths, one for each custom instrument in the song, relative to the assets/minecraft/sounds folder' - }) - @Transform(({ value }) => JSON.parse(value)) - customInstruments: string[]; - - constructor(partial: Partial) { - Object.assign(this, partial); - } - - public static fromSongDocument(song: SongDocument): UploadSongDto { - return new UploadSongDto({ - allowDownload : song.allowDownload, - visibility : song.visibility, - title : song.title, - originalAuthor : song.originalAuthor, - description : song.description, - category : song.category, - thumbnailData : song.thumbnailData, - license : song.license, - customInstruments: song.customInstruments ?? [] - }); - } + }) + @Transform(({ value }) => JSON.parse(value)) + customInstruments: string[]; + + constructor(partial: Partial) { + Object.assign(this, partial); + } + + public static fromSongDocument(song: SongDocument): UploadSongDto { + return new UploadSongDto({ + allowDownload : song.allowDownload, + visibility : song.visibility, + title : song.title, + originalAuthor : song.originalAuthor, + description : song.description, + category : song.category, + thumbnailData : song.thumbnailData, + license : song.license, + customInstruments: song.customInstruments ?? [] + }); + } } diff --git a/packages/database/src/song/dto/UploadSongResponseDto.dto.ts b/packages/database/src/song/dto/UploadSongResponseDto.dto.ts index f6d49cbd..b7485bd2 100644 --- a/packages/database/src/song/dto/UploadSongResponseDto.dto.ts +++ b/packages/database/src/song/dto/UploadSongResponseDto.dto.ts @@ -1,10 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { - IsNotEmpty, - IsString, - MaxLength, - ValidateNested + IsNotEmpty, + IsString, + MaxLength, + ValidateNested } from 'class-validator'; import type { SongWithUser } from '../entity/song.entity'; @@ -13,61 +13,61 @@ import * as SongViewDto from './SongView.dto'; import { ThumbnailData } from './ThumbnailData.dto'; export class UploadSongResponseDto { - @IsString() - @IsNotEmpty() - @ApiProperty({ - description: 'ID of the song', - example : '1234567890abcdef12345678' - }) - publicId: string; + @IsString() + @IsNotEmpty() + @ApiProperty({ + description: 'ID of the song', + example : '1234567890abcdef12345678' + }) + publicId: string; - @IsNotEmpty() - @IsString() - @MaxLength(128) - @ApiProperty({ - description: 'Title of the song', - example : 'My Song' - }) - title: string; + @IsNotEmpty() + @IsString() + @MaxLength(128) + @ApiProperty({ + description: 'Title of the song', + example : 'My Song' + }) + title: string; - @IsString() - @MaxLength(64) - @ApiProperty({ - description: 'Original author of the song', - example : 'Myself' - }) - uploader: SongViewDto.SongViewUploader; + @IsString() + @MaxLength(64) + @ApiProperty({ + description: 'Original author of the song', + example : 'Myself' + }) + uploader: SongViewDto.SongViewUploader; - @IsNotEmpty() - @ValidateNested() - @Type(() => ThumbnailData) - @Transform(({ value }) => JSON.parse(value)) - @ApiProperty({ - description: 'Thumbnail data of the song', - example : ThumbnailData.getApiExample() - }) - thumbnailUrl: string; + @IsNotEmpty() + @ValidateNested() + @Type(() => ThumbnailData) + @Transform(({ value }) => JSON.parse(value)) + @ApiProperty({ + description: 'Thumbnail data of the song', + example : ThumbnailData.getApiExample() + }) + thumbnailUrl: string; - @IsNotEmpty() - duration: number; + @IsNotEmpty() + duration: number; - @IsNotEmpty() - noteCount: number; + @IsNotEmpty() + noteCount: number; - constructor(partial: Partial) { - Object.assign(this, partial); - } + constructor(partial: Partial) { + Object.assign(this, partial); + } - public static fromSongWithUserDocument( - song: SongWithUser - ): UploadSongResponseDto { - return new UploadSongResponseDto({ - publicId : song.publicId, - title : song.title, - uploader : song.uploader, - duration : song.stats.duration, - thumbnailUrl: song.thumbnailUrl, - noteCount : song.stats.noteCount - }); - } + public static fromSongWithUserDocument( + song: SongWithUser + ): UploadSongResponseDto { + return new UploadSongResponseDto({ + publicId : song.publicId, + title : song.title, + uploader : song.uploader, + duration : song.stats.duration, + thumbnailUrl: song.thumbnailUrl, + noteCount : song.stats.noteCount + }); + } } diff --git a/packages/database/src/song/entity/song.entity.ts b/packages/database/src/song/entity/song.entity.ts index f76a30c8..0510419f 100644 --- a/packages/database/src/song/entity/song.entity.ts +++ b/packages/database/src/song/entity/song.entity.ts @@ -9,89 +9,89 @@ import { ThumbnailData } from '../dto/ThumbnailData.dto'; import type { CategoryType, LicenseType, VisibilityType } from '../dto/types'; @Schema({ - timestamps: true, - versionKey: false, - toJSON : { - virtuals : true, - transform: (doc, ret) => { - delete ret._id; + timestamps: true, + versionKey: false, + toJSON : { + virtuals : true, + transform: (doc, ret) => { + delete ret._id; + } } - } }) export class Song { - @Prop({ type: String, required: true, unique: true }) - publicId: string; + @Prop({ type: String, required: true, unique: true }) + publicId: string; - @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) - createdAt: Date; // Added automatically by Mongoose: https://mongoosejs.com/docs/timestamps.html + @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) + createdAt: Date; // Added automatically by Mongoose: https://mongoosejs.com/docs/timestamps.html - @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) - updatedAt: Date; // Added automatically by Mongoose: https://mongoosejs.com/docs/timestamps.html + @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) + updatedAt: Date; // Added automatically by Mongoose: https://mongoosejs.com/docs/timestamps.html - @Prop({ type: MongooseSchema.Types.ObjectId, required: true, ref: 'User' }) - uploader: Types.ObjectId | User; // Populated with the uploader's user document + @Prop({ type: MongooseSchema.Types.ObjectId, required: true, ref: 'User' }) + uploader: Types.ObjectId | User; // Populated with the uploader's user document - @Prop({ type: String, required: true }) - thumbnailUrl: string; + @Prop({ type: String, required: true }) + thumbnailUrl: string; - @Prop({ type: String, required: true }) - nbsFileUrl: string; + @Prop({ type: String, required: true }) + nbsFileUrl: string; - @Prop({ type: String, required: true }) - packedSongUrl: string; + @Prop({ type: String, required: true }) + packedSongUrl: string; - // SONG DOCUMENT ATTRIBUTES + // SONG DOCUMENT ATTRIBUTES - @Prop({ type: Number, required: true, default: 0 }) - playCount: number; + @Prop({ type: Number, required: true, default: 0 }) + playCount: number; - @Prop({ type: Number, required: true, default: 0 }) - downloadCount: number; + @Prop({ type: Number, required: true, default: 0 }) + downloadCount: number; - @Prop({ type: Number, required: true, default: 0 }) - likeCount: number; + @Prop({ type: Number, required: true, default: 0 }) + likeCount: number; - // SONG FILE ATTRIBUTES (Populated from upload form - updatable) + // SONG FILE ATTRIBUTES (Populated from upload form - updatable) - @Prop({ type: ThumbnailData, required: true }) - thumbnailData: ThumbnailData; + @Prop({ type: ThumbnailData, required: true }) + thumbnailData: ThumbnailData; - @Prop({ type: String, required: true }) - category: CategoryType; + @Prop({ type: String, required: true }) + category: CategoryType; - @Prop({ type: String, required: true }) - visibility: VisibilityType; + @Prop({ type: String, required: true }) + visibility: VisibilityType; - @Prop({ type: String, required: true }) - license: LicenseType; + @Prop({ type: String, required: true }) + license: LicenseType; - @Prop({ type: Array, required: true }) - customInstruments: string[]; + @Prop({ type: Array, required: true }) + customInstruments: string[]; - @Prop({ type: Boolean, required: true, default: true }) - allowDownload: boolean; + @Prop({ type: Boolean, required: true, default: true }) + allowDownload: boolean; - @Prop({ type: String, required: true }) - title: string; + @Prop({ type: String, required: true }) + title: string; - @Prop({ type: String, required: false }) - originalAuthor: string; + @Prop({ type: String, required: false }) + originalAuthor: string; - @Prop({ type: String, required: false }) - description: string; + @Prop({ type: String, required: false }) + description: string; - // SONG FILE ATTRIBUTES (Populated from NBS file - immutable) + // SONG FILE ATTRIBUTES (Populated from NBS file - immutable) - @Prop({ type: Number, required: true }) - fileSize: number; + @Prop({ type: Number, required: true }) + fileSize: number; - @Prop({ type: SongStats, required: true }) - stats: SongStats; + @Prop({ type: SongStats, required: true }) + stats: SongStats; - // EXTERNAL ATTRIBUTES + // EXTERNAL ATTRIBUTES - @Prop({ type: String, required: false }) - webhookMessageId?: string | null; + @Prop({ type: String, required: false }) + webhookMessageId?: string | null; } export const SongSchema = SchemaFactory.createForClass(Song); @@ -99,5 +99,5 @@ export const SongSchema = SchemaFactory.createForClass(Song); export type SongDocument = Song & HydratedDocument; export type SongWithUser = Omit & { - uploader: SongViewUploader; + uploader: SongViewUploader; }; diff --git a/packages/database/src/user/dto/CreateUser.dto.ts b/packages/database/src/user/dto/CreateUser.dto.ts index 9a590c29..c10491bf 100644 --- a/packages/database/src/user/dto/CreateUser.dto.ts +++ b/packages/database/src/user/dto/CreateUser.dto.ts @@ -1,41 +1,41 @@ import { ApiProperty } from '@nestjs/swagger'; import { - IsEmail, - IsNotEmpty, - IsString, - IsUrl, - MaxLength + IsEmail, + IsNotEmpty, + IsString, + IsUrl, + MaxLength } from 'class-validator'; export class CreateUser { - @IsNotEmpty() - @IsString() - @MaxLength(64) - @IsEmail() - @ApiProperty({ - description: 'Email of the user', - example : 'vycasnicolas@gmailcom' - }) - email: string; + @IsNotEmpty() + @IsString() + @MaxLength(64) + @IsEmail() + @ApiProperty({ + description: 'Email of the user', + example : 'vycasnicolas@gmailcom' + }) + email: string; - @IsNotEmpty() - @IsString() - @MaxLength(64) - @ApiProperty({ - description: 'Username of the user', - example : 'tomast1137' - }) - username: string; + @IsNotEmpty() + @IsString() + @MaxLength(64) + @ApiProperty({ + description: 'Username of the user', + example : 'tomast1137' + }) + username: string; - @IsNotEmpty() - @IsUrl() - @ApiProperty({ - description: 'Profile image of the user', - example : 'https://example.com/image.png' - }) - profileImage: string; + @IsNotEmpty() + @IsUrl() + @ApiProperty({ + description: 'Profile image of the user', + example : 'https://example.com/image.png' + }) + profileImage: string; - constructor(partial: Partial) { - Object.assign(this, partial); - } + constructor(partial: Partial) { + Object.assign(this, partial); + } } diff --git a/packages/database/src/user/dto/GetUser.dto.ts b/packages/database/src/user/dto/GetUser.dto.ts index 7ff170db..198ee092 100644 --- a/packages/database/src/user/dto/GetUser.dto.ts +++ b/packages/database/src/user/dto/GetUser.dto.ts @@ -1,45 +1,45 @@ import { ApiProperty } from '@nestjs/swagger'; import { - IsEmail, - IsMongoId, - IsOptional, - IsString, - MaxLength, - MinLength + IsEmail, + IsMongoId, + IsOptional, + IsString, + MaxLength, + MinLength } from 'class-validator'; export class GetUser { - @IsString() - @IsOptional() - @MaxLength(64) - @IsEmail() - @ApiProperty({ - description: 'Email of the user', - example : 'vycasnicolas@gmailcom' - }) - email?: string; + @IsString() + @IsOptional() + @MaxLength(64) + @IsEmail() + @ApiProperty({ + description: 'Email of the user', + example : 'vycasnicolas@gmailcom' + }) + email?: string; - @IsString() - @IsOptional() - @MaxLength(64) - @ApiProperty({ - description: 'Username of the user', - example : 'tomast1137' - }) - username?: string; + @IsString() + @IsOptional() + @MaxLength(64) + @ApiProperty({ + description: 'Username of the user', + example : 'tomast1137' + }) + username?: string; - @IsString() - @IsOptional() - @MaxLength(64) - @MinLength(24) - @IsMongoId() - @ApiProperty({ - description: 'ID of the user', - example : 'replace0me6b5f0a8c1a6d8c' - }) - id?: string; + @IsString() + @IsOptional() + @MaxLength(64) + @MinLength(24) + @IsMongoId() + @ApiProperty({ + description: 'ID of the user', + example : 'replace0me6b5f0a8c1a6d8c' + }) + id?: string; - constructor(partial: Partial) { - Object.assign(this, partial); - } + constructor(partial: Partial) { + Object.assign(this, partial); + } } diff --git a/packages/database/src/user/dto/Login.dto copy.ts b/packages/database/src/user/dto/Login.dto copy.ts index b433a0d2..207a0693 100644 --- a/packages/database/src/user/dto/Login.dto copy.ts +++ b/packages/database/src/user/dto/Login.dto copy.ts @@ -2,8 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; export class LoginDto { - @ApiProperty() - @IsString() - @IsNotEmpty() - public email: string; + @ApiProperty() + @IsString() + @IsNotEmpty() + public email: string; } diff --git a/packages/database/src/user/dto/LoginWithEmail.dto.ts b/packages/database/src/user/dto/LoginWithEmail.dto.ts index 27c2d9cc..097a812b 100644 --- a/packages/database/src/user/dto/LoginWithEmail.dto.ts +++ b/packages/database/src/user/dto/LoginWithEmail.dto.ts @@ -2,8 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; export class LoginWithEmailDto { - @ApiProperty() - @IsString() - @IsNotEmpty() - public email: string; + @ApiProperty() + @IsString() + @IsNotEmpty() + public email: string; } diff --git a/packages/database/src/user/dto/NewEmailUser.dto.ts b/packages/database/src/user/dto/NewEmailUser.dto.ts index 11ecbb22..fe60f246 100644 --- a/packages/database/src/user/dto/NewEmailUser.dto.ts +++ b/packages/database/src/user/dto/NewEmailUser.dto.ts @@ -1,30 +1,30 @@ import { ApiProperty } from '@nestjs/swagger'; import { - IsEmail, - IsNotEmpty, - IsString, - MaxLength, - MinLength + IsEmail, + IsNotEmpty, + IsString, + MaxLength, + MinLength } from 'class-validator'; export class NewEmailUserDto { - @ApiProperty({ - description: 'User name', - example : 'Tomast1337' - }) - @IsString() - @IsNotEmpty() - @MaxLength(64) - @MinLength(4) - username: string; + @ApiProperty({ + description: 'User name', + example : 'Tomast1337' + }) + @IsString() + @IsNotEmpty() + @MaxLength(64) + @MinLength(4) + username: string; - @ApiProperty({ - description: 'User email', - example : 'vycasnicolas@gmail.com' - }) - @IsString() - @IsNotEmpty() - @MaxLength(64) - @IsEmail() - email: string; + @ApiProperty({ + description: 'User email', + example : 'vycasnicolas@gmail.com' + }) + @IsString() + @IsNotEmpty() + @MaxLength(64) + @IsEmail() + email: string; } diff --git a/packages/database/src/user/dto/SingleUsePass.dto.ts b/packages/database/src/user/dto/SingleUsePass.dto.ts index e1e04c25..75635f68 100644 --- a/packages/database/src/user/dto/SingleUsePass.dto.ts +++ b/packages/database/src/user/dto/SingleUsePass.dto.ts @@ -2,13 +2,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; export class SingleUsePassDto { - @ApiProperty() - @IsString() - @IsNotEmpty() - id: string; + @ApiProperty() + @IsString() + @IsNotEmpty() + id: string; - @ApiProperty() - @IsString() - @IsNotEmpty() - pass: string; + @ApiProperty() + @IsString() + @IsNotEmpty() + pass: string; } diff --git a/packages/database/src/user/dto/UpdateUsername.dto.ts b/packages/database/src/user/dto/UpdateUsername.dto.ts index 622824b1..b9b85059 100644 --- a/packages/database/src/user/dto/UpdateUsername.dto.ts +++ b/packages/database/src/user/dto/UpdateUsername.dto.ts @@ -3,13 +3,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; export class UpdateUsernameDto { - @IsString() - @MaxLength(USER_CONSTANTS.USERNAME_MAX_LENGTH) - @MinLength(USER_CONSTANTS.USERNAME_MIN_LENGTH) - @Matches(USER_CONSTANTS.ALLOWED_REGEXP) - @ApiProperty({ - description: 'Username of the user', - example : 'tomast1137' - }) - username: string; + @IsString() + @MaxLength(USER_CONSTANTS.USERNAME_MAX_LENGTH) + @MinLength(USER_CONSTANTS.USERNAME_MIN_LENGTH) + @Matches(USER_CONSTANTS.ALLOWED_REGEXP) + @ApiProperty({ + description: 'Username of the user', + example : 'tomast1137' + }) + username: string; } diff --git a/packages/database/src/user/dto/user.dto.ts b/packages/database/src/user/dto/user.dto.ts index 413ddede..24e7fe4b 100644 --- a/packages/database/src/user/dto/user.dto.ts +++ b/packages/database/src/user/dto/user.dto.ts @@ -1,16 +1,16 @@ import { User } from '../entity/user.entity'; export class UserDto { - username : string; - publicName: string; - email : string; - static fromEntity(user: User): UserDto { - const userDto: UserDto = { - username : user.username, - publicName: user.publicName, - email : user.email - }; + username : string; + publicName: string; + email : string; + static fromEntity(user: User): UserDto { + const userDto: UserDto = { + username : user.username, + publicName: user.publicName, + email : user.email + }; - return userDto; - } + return userDto; + } } diff --git a/packages/database/src/user/entity/user.entity.ts b/packages/database/src/user/entity/user.entity.ts index 20e58e9e..26b0e89c 100644 --- a/packages/database/src/user/entity/user.entity.ts +++ b/packages/database/src/user/entity/user.entity.ts @@ -4,86 +4,86 @@ import { Schema as MongooseSchema } from 'mongoose'; @Schema({}) class SocialLinks { - bandcamp? : string; - discord? : string; - facebook? : string; - github? : string; - instagram? : string; - reddit? : string; - snapchat? : string; - soundcloud?: string; - spotify? : string; - steam? : string; - telegram? : string; - tiktok? : string; - threads? : string; - twitch? : string; - x? : string; - youtube? : string; + bandcamp? : string; + discord? : string; + facebook? : string; + github? : string; + instagram? : string; + reddit? : string; + snapchat? : string; + soundcloud?: string; + spotify? : string; + steam? : string; + telegram? : string; + tiktok? : string; + threads? : string; + twitch? : string; + x? : string; + youtube? : string; } @Schema({ - timestamps: true, - toJSON : { - virtuals : true, - transform: (doc, ret) => { - delete ret._id; - delete ret.__v; + timestamps: true, + toJSON : { + virtuals : true, + transform: (doc, ret) => { + delete ret._id; + delete ret.__v; + } } - } }) export class User { - @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) - creationDate: Date; + @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) + creationDate: Date; - @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) - lastEdited: Date; + @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) + lastEdited: Date; - @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) - lastSeen: Date; + @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) + lastSeen: Date; - @Prop({ type: Number, required: true, default: 0 }) - loginCount: number; + @Prop({ type: Number, required: true, default: 0 }) + loginCount: number; - @Prop({ type: Number, required: true, default: 0 }) - loginStreak: number; + @Prop({ type: Number, required: true, default: 0 }) + loginStreak: number; - @Prop({ type: Number, required: true, default: 0 }) - maxLoginStreak: number; + @Prop({ type: Number, required: true, default: 0 }) + maxLoginStreak: number; - @Prop({ type: Number, required: true, default: 0 }) - playCount: number; + @Prop({ type: Number, required: true, default: 0 }) + playCount: number; - @Prop({ type: String, required: true }) - username: string; + @Prop({ type: String, required: true }) + username: string; - @Prop({ type: String, required: true, default: '#' }) - publicName: string; + @Prop({ type: String, required: true, default: '#' }) + publicName: string; - @Prop({ type: String, required: true, unique: true }) - email: string; + @Prop({ type: String, required: true, unique: true }) + email: string; - @Prop({ type: String, required: false, default: null }) - singleUsePass?: string; + @Prop({ type: String, required: false, default: null }) + singleUsePass?: string; - @Prop({ type: String, required: false, default: null }) - singleUsePassID?: string; + @Prop({ type: String, required: false, default: null }) + singleUsePassID?: string; - @Prop({ type: String, required: true, default: 'No description provided' }) - description: string; + @Prop({ type: String, required: true, default: 'No description provided' }) + description: string; - @Prop({ - type : String, - required: true, - default : '/img/note-block-pfp.jpg' - }) - profileImage: string; + @Prop({ + type : String, + required: true, + default : '/img/note-block-pfp.jpg' + }) + profileImage: string; - @Prop({ type: SocialLinks, required: false, default: {} }) - socialLinks: SocialLinks; + @Prop({ type: SocialLinks, required: false, default: {} }) + socialLinks: SocialLinks; - @Prop({ type: Boolean, required: true, default: true }) - prefersDarkTheme: boolean; + @Prop({ type: Boolean, required: true, default: true }) + prefersDarkTheme: boolean; } export const UserSchema = SchemaFactory.createForClass(User); diff --git a/packages/song/src/injectMetadata.ts b/packages/song/src/injectMetadata.ts index bec26cf2..237a5dc1 100644 --- a/packages/song/src/injectMetadata.ts +++ b/packages/song/src/injectMetadata.ts @@ -2,28 +2,28 @@ import { Song } from '@encode42/nbs.js'; import unidecode from 'unidecode'; export function injectSongFileMetadata( - nbsSong: Song, - title: string, - author: string, - originalAuthor: string, - description: string, - soundPaths: string[] + nbsSong: Song, + title: string, + author: string, + originalAuthor: string, + description: string, + soundPaths: string[] ) { - if (description != '') description += '\n\n'; - description += 'Uploaded to Note Block World'; + if (description != '') description += '\n\n'; + description += 'Uploaded to Note Block World'; - nbsSong.meta.name = unidecode(title); - nbsSong.meta.author = unidecode(author); - nbsSong.meta.originalAuthor = unidecode(originalAuthor); - nbsSong.meta.description = unidecode(description); + nbsSong.meta.name = unidecode(title); + nbsSong.meta.author = unidecode(author); + nbsSong.meta.originalAuthor = unidecode(originalAuthor); + nbsSong.meta.description = unidecode(description); - // Assign sound files to standard Minecraft asset names (machine- and human-friendly) - // (Also ensures the downloaded song matches what the user picked when uploading) - for (const [id, soundPath] of soundPaths.entries()) { - const customId = nbsSong.instruments.firstCustomIndex + id; - const newSoundPath = soundPath.replace('/sounds/', '/'); + // Assign sound files to standard Minecraft asset names (machine- and human-friendly) + // (Also ensures the downloaded song matches what the user picked when uploading) + for (const [id, soundPath] of soundPaths.entries()) { + const customId = nbsSong.instruments.firstCustomIndex + id; + const newSoundPath = soundPath.replace('/sounds/', '/'); - // @ts-ignore //TODO: fix this - nbsSong.instruments.loaded[customId].meta.soundFile = newSoundPath; - } + // @ts-ignore //TODO: fix this + nbsSong.instruments.loaded[customId].meta.soundFile = newSoundPath; + } } diff --git a/packages/song/src/notes.ts b/packages/song/src/notes.ts index d5a0e10f..aa35e9df 100644 --- a/packages/song/src/notes.ts +++ b/packages/song/src/notes.ts @@ -8,68 +8,68 @@ type TreeNode = Rectangle; export { Rectangle } from '@timohausmann/quadtree-ts'; export class NoteQuadTree { - private quadtree: Quadtree; + private quadtree: Quadtree; - public width = 0; - public height = 0; + public width = 0; + public height = 0; - constructor(song: Song) { - const width = song.length; - const height = song.layers.length; + constructor(song: Song) { + const width = song.length; + const height = song.layers.length; - this.quadtree = new Quadtree({ width, height }); + this.quadtree = new Quadtree({ width, height }); - for (const [layerId, layer] of song.layers.entries()) { - for (const tickStr in layer.notes) { - const note = layer.notes[tickStr]; - const tick = parseInt(tickStr); + for (const [layerId, layer] of song.layers.entries()) { + for (const tickStr in layer.notes) { + const note = layer.notes[tickStr]; + const tick = parseInt(tickStr); - const treeItem = new Rectangle({ - x : tick, - y : layerId, - width : 1, - height: 1, - data : { ...note, tick: tick, layer: layerId } - }); + const treeItem = new Rectangle({ + x : tick, + y : layerId, + width : 1, + height: 1, + data : { ...note, tick: tick, layer: layerId } + }); - // @ts-ignore //TODO: fix this - this.quadtree.insert(treeItem); + // @ts-ignore //TODO: fix this + this.quadtree.insert(treeItem); - if (tick > this.width) this.width = tick; - if (layerId > this.height) this.height = layerId; - } + if (tick > this.width) this.width = tick; + if (layerId > this.height) this.height = layerId; + } + } } - } - public getNotesInRect({ - x1, - y1, - x2, - y2 - }: { - x1: number; - y1: number; - x2: number; - y2: number; - }): Note[] { - const rect = new Rectangle({ - x : Math.min(x1, x2), - y : Math.min(y1, y2), - width : Math.abs(x2 - x1), - height: Math.abs(y2 - y1) - }); + public getNotesInRect({ + x1, + y1, + x2, + y2 + }: { + x1: number; + y1: number; + x2: number; + y2: number; + }): Note[] { + const rect = new Rectangle({ + x : Math.min(x1, x2), + y : Math.min(y1, y2), + width : Math.abs(x2 - x1), + height: Math.abs(y2 - y1) + }); - return this.quadtree - .retrieve(rect) - .flatMap((node) => { - return node.data ? [node.data] : []; - }) - .filter( - (note) => - note.tick >= x1 && + return this.quadtree + .retrieve(rect) + .flatMap((node) => { + return node.data ? [node.data] : []; + }) + .filter( + (note) => + note.tick >= x1 && note.tick <= x2 && note.layer >= y1 && note.layer <= y2 - ); - } + ); + } } diff --git a/packages/song/src/obfuscate.ts b/packages/song/src/obfuscate.ts index a13f05a2..ef4f4cc2 100644 --- a/packages/song/src/obfuscate.ts +++ b/packages/song/src/obfuscate.ts @@ -3,112 +3,112 @@ import { Instrument, Layer, Note, Song } from '@encode42/nbs.js'; import { getInstrumentNoteCounts, getTempoChangerInstrumentIds } from './util'; export class SongObfuscator { - private song : Song; - private soundPaths: string[]; - private output : Song; - - public static obfuscateSong(song: Song, soundPaths: string[]) { - return new SongObfuscator(song, soundPaths).output; - } - - private constructor(song: Song, soundPaths: string[]) { - this.song = song; - this.soundPaths = soundPaths; - this.output = this.generateObfuscatedSong(); - } - - private generateObfuscatedSong(): Song { - const song = this.song; - const output = new Song(); - - // ✅ Clear work stats - // ✅ Copy: title, author, description, loop info, time signature - this.copyMetaAndStats(song, output); - const instrumentMapping = this.resolveInstruments(song, output); - this.resolveNotes(song, output, instrumentMapping); - - return output; - } - - private copyMetaAndStats(song: Song, output: Song) { - output.meta.name = song.meta.name; - output.meta.author = song.meta.author; - output.meta.originalAuthor = song.meta.originalAuthor; - output.meta.description = song.meta.description; - output.meta.importName = song.meta.importName; - - output.loop.enabled = song.loop.enabled; - output.loop.startTick = song.loop.startTick; - output.loop.totalLoops = song.loop.totalLoops; - - output.tempo = song.tempo; - output.timeSignature = song.timeSignature; - } - - private resolveInstruments(song: Song, output: Song): Record { + private song : Song; + private soundPaths: string[]; + private output : Song; + + public static obfuscateSong(song: Song, soundPaths: string[]) { + return new SongObfuscator(song, soundPaths).output; + } + + private constructor(song: Song, soundPaths: string[]) { + this.song = song; + this.soundPaths = soundPaths; + this.output = this.generateObfuscatedSong(); + } + + private generateObfuscatedSong(): Song { + const song = this.song; + const output = new Song(); + + // ✅ Clear work stats + // ✅ Copy: title, author, description, loop info, time signature + this.copyMetaAndStats(song, output); + const instrumentMapping = this.resolveInstruments(song, output); + this.resolveNotes(song, output, instrumentMapping); + + return output; + } + + private copyMetaAndStats(song: Song, output: Song) { + output.meta.name = song.meta.name; + output.meta.author = song.meta.author; + output.meta.originalAuthor = song.meta.originalAuthor; + output.meta.description = song.meta.description; + output.meta.importName = song.meta.importName; + + output.loop.enabled = song.loop.enabled; + output.loop.startTick = song.loop.startTick; + output.loop.totalLoops = song.loop.totalLoops; + + output.tempo = song.tempo; + output.timeSignature = song.timeSignature; + } + + private resolveInstruments(song: Song, output: Song): Record { // ✅ Remove unused instruments // ✅ Remove instrument info (name, press) - keep sound hash and pitch - const noteCountPerInstrument = getInstrumentNoteCounts(song); + const noteCountPerInstrument = getInstrumentNoteCounts(song); - const instrumentMapping: Record = {}; + const instrumentMapping: Record = {}; - for (const [ - instrumentId, - instrument - ] of song.instruments.loaded.entries()) { - if (instrument.builtIn) { - instrumentMapping[instrumentId] = instrumentId; - continue; - } + for (const [ + instrumentId, + instrument + ] of song.instruments.loaded.entries()) { + if (instrument.builtIn) { + instrumentMapping[instrumentId] = instrumentId; + continue; + } - // Remove unused instruments (no notes or no sound defined) - const instrumentName = instrument.meta.name; - const customId = instrumentId - output.instruments.firstCustomIndex; - const soundFilePath = this.soundPaths[customId]; - const isTempoChanger = instrument.meta.name === 'Tempo Changer'; + // Remove unused instruments (no notes or no sound defined) + const instrumentName = instrument.meta.name; + const customId = instrumentId - output.instruments.firstCustomIndex; + const soundFilePath = this.soundPaths[customId]; + const isTempoChanger = instrument.meta.name === 'Tempo Changer'; - if ( - !isTempoChanger && + if ( + !isTempoChanger && (noteCountPerInstrument[instrumentId] === 0 || soundFilePath === '') - ) { - console.log( - `Skipping instrument '${instrumentName}' with ${noteCountPerInstrument[instrumentId]}`, - `notes and sound file '${soundFilePath}' (custom ID: ${customId})` - ); - - continue; - } - - // Remove instrument info - const newInstrumentId = output.instruments.loaded.length; - const newCustomId = newInstrumentId - output.instruments.firstCustomIndex; - - console.log( - `Keeping instrument '${instrumentName}' with`, - `${noteCountPerInstrument[instrumentId]} notes and sound file`, - `'${this.soundPaths[customId]}' (custom ID: ${customId} -> ${newCustomId})` - ); - - const newInstrument = new Instrument(newInstrumentId, { - name : instrumentName === 'Tempo Changer' ? 'Tempo Changer' : '', - soundFile: soundFilePath, - key : instrument.key, - pressKey : false - }); - - output.instruments.loaded.push(newInstrument); - instrumentMapping[instrumentId] = newInstrumentId; + ) { + console.log( + `Skipping instrument '${instrumentName}' with ${noteCountPerInstrument[instrumentId]}`, + `notes and sound file '${soundFilePath}' (custom ID: ${customId})` + ); + + continue; + } + + // Remove instrument info + const newInstrumentId = output.instruments.loaded.length; + const newCustomId = newInstrumentId - output.instruments.firstCustomIndex; + + console.log( + `Keeping instrument '${instrumentName}' with`, + `${noteCountPerInstrument[instrumentId]} notes and sound file`, + `'${this.soundPaths[customId]}' (custom ID: ${customId} -> ${newCustomId})` + ); + + const newInstrument = new Instrument(newInstrumentId, { + name : instrumentName === 'Tempo Changer' ? 'Tempo Changer' : '', + soundFile: soundFilePath, + key : instrument.key, + pressKey : false + }); + + output.instruments.loaded.push(newInstrument); + instrumentMapping[instrumentId] = newInstrumentId; + } + + return instrumentMapping; } - return instrumentMapping; - } - - private resolveNotes( - song: Song, - output: Song, - instrumentMapping: Record - ) { + private resolveNotes( + song: Song, + output: Song, + instrumentMapping: Record + ) { // ✅ Pile notes at the top // ✅ Bake layer volume into note velocity // ✅ Bake layer pan into note pan @@ -117,119 +117,119 @@ export class SongObfuscator { // ✅ Remove everything in locked layers // ✅ Remove all layer info (name, volume, panning) - const resolveKeyAndPitch = (note: Note) => { - const factoredPitch = note.key * 100 + note.pitch; + const resolveKeyAndPitch = (note: Note) => { + const factoredPitch = note.key * 100 + note.pitch; - let key, pitch; + let key, pitch; - // TODO: add regression test for this behavior - if (factoredPitch < 0) { - // Below A0 - key = 0; - pitch = factoredPitch; - } else if (factoredPitch >= 87 * 100) { - // Above C8 - key = 87; - pitch = factoredPitch - 87 * 100; - } else { - key = Math.floor((factoredPitch + 50) / 100); - pitch = ((factoredPitch + 50) % 100) - 50; - } + // TODO: add regression test for this behavior + if (factoredPitch < 0) { + // Below A0 + key = 0; + pitch = factoredPitch; + } else if (factoredPitch >= 87 * 100) { + // Above C8 + key = 87; + pitch = factoredPitch - 87 * 100; + } else { + key = Math.floor((factoredPitch + 50) / 100); + pitch = ((factoredPitch + 50) % 100) - 50; + } - return { key, pitch }; - }; + return { key, pitch }; + }; - const resolveInstrument = (note: Note) => - instrumentMapping[note.instrument]; + const resolveInstrument = (note: Note) => + instrumentMapping[note.instrument]; - const resolveVelocity = (note: Note, layer: Layer) => { - let velocity = ((note.velocity / 100) * layer.volume) / 100; - velocity = Math.round(velocity * 100); + const resolveVelocity = (note: Note, layer: Layer) => { + let velocity = ((note.velocity / 100) * layer.volume) / 100; + velocity = Math.round(velocity * 100); - return velocity; - }; + return velocity; + }; - const resolvePanning = (note: Note, layer: Layer) => { - let panning; + const resolvePanning = (note: Note, layer: Layer) => { + let panning; - if (layer.stereo === 0) { - panning = note.panning; - } else { - panning = (note.panning + layer.stereo) / 2; - } + if (layer.stereo === 0) { + panning = note.panning; + } else { + panning = (note.panning + layer.stereo) / 2; + } - return panning; - }; + return panning; + }; - const getObfuscatedNote = (note: Note, layer: Layer) => { - const { key, pitch } = resolveKeyAndPitch(note); - const instrument = resolveInstrument(note); - const velocity = resolveVelocity(note, layer); - const panning = resolvePanning(note, layer); + const getObfuscatedNote = (note: Note, layer: Layer) => { + const { key, pitch } = resolveKeyAndPitch(note); + const instrument = resolveInstrument(note); + const velocity = resolveVelocity(note, layer); + const panning = resolvePanning(note, layer); - // Create new note - const newNote = new Note(instrument, { - key, - velocity, - panning, - pitch - }); + // Create new note + const newNote = new Note(instrument, { + key, + velocity, + panning, + pitch + }); - return newNote; - }; + return newNote; + }; - const addNoteToOutput = (tick: number, note: Note) => { - // Adds a note at tick at the first row that does not have a note yet + const addNoteToOutput = (tick: number, note: Note) => { + // Adds a note at tick at the first row that does not have a note yet - let layerIdToAddNoteTo = lastLayerInTick.get(tick); + let layerIdToAddNoteTo = lastLayerInTick.get(tick); - if (layerIdToAddNoteTo === undefined) { - layerIdToAddNoteTo = 0; - } + if (layerIdToAddNoteTo === undefined) { + layerIdToAddNoteTo = 0; + } - lastLayerInTick.set(tick, layerIdToAddNoteTo + 1); + lastLayerInTick.set(tick, layerIdToAddNoteTo + 1); - const layerToAddNoteTo = - output.layers[layerIdToAddNoteTo] || output.createLayer(); + const layerToAddNoteTo = + output.layers[layerIdToAddNoteTo] || output.createLayer(); - output.setNote(tick, layerToAddNoteTo, note); - }; + output.setNote(tick, layerToAddNoteTo, note); + }; - const tempoChangerIds = getTempoChangerInstrumentIds(song); - const lastLayerInTick = new Map(); + const tempoChangerIds = getTempoChangerInstrumentIds(song); + const lastLayerInTick = new Map(); - for (const layer of song.layers) { - // Skip locked and silent layers - if (layer.isLocked || layer.volume === 0) { - continue; - } + for (const layer of song.layers) { + // Skip locked and silent layers + if (layer.isLocked || layer.volume === 0) { + continue; + } - for (const tickStr in layer.notes) { - const note = layer.notes[tickStr]; - const tick = parseInt(tickStr); + for (const tickStr in layer.notes) { + const note = layer.notes[tickStr]; + const tick = parseInt(tickStr); - // Skip silent notes except if they are tempo changers + // Skip silent notes except if they are tempo changers - // @ts-ignore //TODO: fix this - const isTempoChanger = tempoChangerIds.includes(note.instrument); + // @ts-ignore //TODO: fix this + const isTempoChanger = tempoChangerIds.includes(note.instrument); - // @ts-ignore //TODO: fix this - if (note.velocity === 0 && !isTempoChanger) continue; + // @ts-ignore //TODO: fix this + if (note.velocity === 0 && !isTempoChanger) continue; - // Skip notes with deleted instruments + // Skip notes with deleted instruments - // @ts-ignore //TODO: fix this - if (instrumentMapping[note.instrument] === undefined) continue; + // @ts-ignore //TODO: fix this + if (instrumentMapping[note.instrument] === undefined) continue; - // Add obfuscated note to output + // Add obfuscated note to output - // @ts-ignore //TODO: fix this - const newNote = getObfuscatedNote(note, layer); + // @ts-ignore //TODO: fix this + const newNote = getObfuscatedNote(note, layer); - // @ts-ignore //TODO: fix this - if (isTempoChanger) newNote.pitch = note.pitch; - addNoteToOutput(tick, newNote); - } + // @ts-ignore //TODO: fix this + if (isTempoChanger) newNote.pitch = note.pitch; + addNoteToOutput(tick, newNote); + } + } } - } } diff --git a/packages/song/src/pack.ts b/packages/song/src/pack.ts index 566397be..99062607 100644 --- a/packages/song/src/pack.ts +++ b/packages/song/src/pack.ts @@ -4,74 +4,74 @@ import JSZip from 'jszip'; import { SongObfuscator } from './obfuscate'; export async function obfuscateAndPackSong( - nbsSong: Song, - soundsArray: string[], - soundsMapping: Record + nbsSong: Song, + soundsArray: string[], + soundsMapping: Record ) { - // Create a ZIP file with the obfuscated song in the root as 'song.nbs' - // and the sounds in a 'sounds' folder. Return the ZIP file as a buffer. + // Create a ZIP file with the obfuscated song in the root as 'song.nbs' + // and the sounds in a 'sounds' folder. Return the ZIP file as a buffer. - // Import JSZip as a CommonJS module - // (see: https://github.com/Stuk/jszip/issues/890) + // Import JSZip as a CommonJS module + // (see: https://github.com/Stuk/jszip/issues/890) - // Create a new empty ZIP file - const zip = new JSZip(); + // Create a new empty ZIP file + const zip = new JSZip(); - // Create a 'sounds' folder in the ZIP file - const soundsFolder = zip.folder('sounds'); + // Create a 'sounds' folder in the ZIP file + const soundsFolder = zip.folder('sounds'); - if (!soundsFolder) { - throw new Error('Failed to create sounds folder'); - } + if (!soundsFolder) { + throw new Error('Failed to create sounds folder'); + } - for (const sound of soundsArray) { - if (!sound) continue; + for (const sound of soundsArray) { + if (!sound) continue; - const hash = soundsMapping[sound] || ''; + const hash = soundsMapping[sound] || ''; - if (!hash) { - console.error(`Sound file ${sound} not found in sounds mapping`); - continue; - } + if (!hash) { + console.error(`Sound file ${sound} not found in sounds mapping`); + continue; + } - // Download the sound from Mojang servers - const soundFileUrl = `https://resources.download.minecraft.net/${hash.slice( - 0, - 2 - )}/${hash}`; - - let soundFileBuffer: ArrayBuffer; - - try { - const response = await fetch(soundFileUrl); - soundFileBuffer = await response.arrayBuffer(); - console.log(`Retrieved sound file with hash ${hash}`); - } catch (e) { - console.error(`Error retrieving sound file with hash ${hash}: ${e}`); - continue; - } + // Download the sound from Mojang servers + const soundFileUrl = `https://resources.download.minecraft.net/${hash.slice( + 0, + 2 + )}/${hash}`; - // Add sounds to the 'sounds' folder - const soundFileName = hash; - soundsFolder.file(soundFileName, soundFileBuffer); - } + let soundFileBuffer: ArrayBuffer; + + try { + const response = await fetch(soundFileUrl); + soundFileBuffer = await response.arrayBuffer(); + console.log(`Retrieved sound file with hash ${hash}`); + } catch (e) { + console.error(`Error retrieving sound file with hash ${hash}: ${e}`); + continue; + } + + // Add sounds to the 'sounds' folder + const soundFileName = hash; + soundsFolder.file(soundFileName, soundFileBuffer); + } - const soundHashes = soundsArray.map((sound) => soundsMapping[sound] || ''); + const soundHashes = soundsArray.map((sound) => soundsMapping[sound] || ''); - // Obfuscate the song and add it to the ZIP file - const obfuscatedSong = SongObfuscator.obfuscateSong(nbsSong, soundHashes); - const songBuffer = obfuscatedSong.toArrayBuffer(); - zip.file('song.nbs', songBuffer); + // Obfuscate the song and add it to the ZIP file + const obfuscatedSong = SongObfuscator.obfuscateSong(nbsSong, soundHashes); + const songBuffer = obfuscatedSong.toArrayBuffer(); + zip.file('song.nbs', songBuffer); - // Generate the ZIP file as a buffer - const zipBuffer = await zip.generateAsync({ - type : 'nodebuffer', - mimeType: 'application/zip', // default - comment : 'Uploaded to Note Block World' + // Generate the ZIP file as a buffer + const zipBuffer = await zip.generateAsync({ + type : 'nodebuffer', + mimeType: 'application/zip', // default + comment : 'Uploaded to Note Block World' // TODO: explore adding a password to the ZIP file // https://github.com/Stuk/jszip/issues/115 // https://github.com/Stuk/jszip/pull/696 - }); + }); - return zipBuffer; + return zipBuffer; } diff --git a/packages/song/src/parse.ts b/packages/song/src/parse.ts index 7d721eda..d0ff4da6 100644 --- a/packages/song/src/parse.ts +++ b/packages/song/src/parse.ts @@ -5,73 +5,73 @@ import type { InstrumentArray, SongFileType } from './types'; import { getInstrumentNoteCounts } from './util'; async function getVanillaSoundList() { - // Object that maps sound paths to their respective hashes + // Object that maps sound paths to their respective hashes - const response = await fetch( - process.env.NEXT_PUBLIC_API_URL + '/data/soundList.json' - ); + const response = await fetch( + process.env.NEXT_PUBLIC_API_URL + '/data/soundList.json' + ); - const soundsMapping = (await response.json()) as Record; - const vanillaSoundList = Object.keys(soundsMapping); + const soundsMapping = (await response.json()) as Record; + const vanillaSoundList = Object.keys(soundsMapping); - return vanillaSoundList; + return vanillaSoundList; } export async function parseSongFromBuffer( - buffer: ArrayBuffer + buffer: ArrayBuffer ): Promise { - const song = fromArrayBuffer(buffer); - - if (song.length === 0) { - throw new Error('Invalid song'); - } - - const quadTree = new NoteQuadTree(song); - - const vanillaSoundList = await getVanillaSoundList(); - - return { - title : song.meta.name, - author : song.meta.author, - originalAuthor: song.meta.originalAuthor, - description : song.meta.description, - length : quadTree.width, - height : quadTree.height, - arrayBuffer : buffer, - notes : quadTree, - instruments : getInstruments(song, vanillaSoundList) - }; + const song = fromArrayBuffer(buffer); + + if (song.length === 0) { + throw new Error('Invalid song'); + } + + const quadTree = new NoteQuadTree(song); + + const vanillaSoundList = await getVanillaSoundList(); + + return { + title : song.meta.name, + author : song.meta.author, + originalAuthor: song.meta.originalAuthor, + description : song.meta.description, + length : quadTree.width, + height : quadTree.height, + arrayBuffer : buffer, + notes : quadTree, + instruments : getInstruments(song, vanillaSoundList) + }; } const getInstruments = ( - song: Song, - vanillaSoundList: string[] + song: Song, + vanillaSoundList: string[] ): InstrumentArray => { - const blockCounts = getInstrumentNoteCounts(song); - - const firstCustomIndex = song.instruments.firstCustomIndex; + const blockCounts = getInstrumentNoteCounts(song); - const customInstruments = song.instruments.loaded.filter( - (instrument) => instrument.builtIn === false - ); + const firstCustomIndex = song.instruments.firstCustomIndex; - return customInstruments.map((instrument, id) => { - let soundFile = ''; - - const fullSoundPath = instrument.meta.soundFile.replace( - 'minecraft/', - 'minecraft/sounds/' + const customInstruments = song.instruments.loaded.filter( + (instrument) => instrument.builtIn === false ); - if (vanillaSoundList.includes(fullSoundPath)) { - soundFile = fullSoundPath; - } - - return { - id : id, - name : instrument.meta.name || '', - file : soundFile, - count: blockCounts[id + firstCustomIndex] || 0 - }; - }); + return customInstruments.map((instrument, id) => { + let soundFile = ''; + + const fullSoundPath = instrument.meta.soundFile.replace( + 'minecraft/', + 'minecraft/sounds/' + ); + + if (vanillaSoundList.includes(fullSoundPath)) { + soundFile = fullSoundPath; + } + + return { + id : id, + name : instrument.meta.name || '', + file : soundFile, + count: blockCounts[id + firstCustomIndex] || 0 + }; + }); }; diff --git a/packages/song/src/stats.ts b/packages/song/src/stats.ts index 93224557..35661f7b 100644 --- a/packages/song/src/stats.ts +++ b/packages/song/src/stats.ts @@ -7,325 +7,325 @@ import { getTempoChangerInstrumentIds } from './util'; // SongStatsGenerator.getSongStats(song) export class SongStatsGenerator { - public static getSongStats(song: Song) { - return new SongStatsGenerator(song).toObject(); - } - - private song : Song; - private stats: SongStatsType; - - private constructor(song: Song) { - this.song = song; - - const midiFileName = this.getMidiFileName(); - - const tempoChangerInstrumentIds = getTempoChangerInstrumentIds(this.song); - - const { - noteCount, - tickCount, - layerCount, - outOfRangeNoteCount, - detunedNoteCount, - customInstrumentNoteCount, - incompatibleNoteCount, - instrumentNoteCounts - } = this.getCounts(tempoChangerInstrumentIds); - - const tempoSegments = this.getTempoSegments(tempoChangerInstrumentIds); - - const tempo = this.getTempo(); - const tempoRange = this.getTempoRange(tempoSegments); - const timeSignature = this.getTimeSignature(); - const duration = this.getDuration(tempoSegments); - const loop = this.getLoop(); - const loopStartTick = this.getLoopStartTick(); - const minutesSpent = this.getMinutesSpent(); - - const { vanillaInstrumentCount, customInstrumentCount } = - this.getVanillaAndCustomUsedInstrumentCounts( - instrumentNoteCounts, - tempoChangerInstrumentIds - ); - - const firstCustomInstrumentIndex = this.getFirstCustomInstrumentIndex(); - - const compatible = incompatibleNoteCount === 0; - - this.stats = { - midiFileName, - noteCount, - tickCount, - layerCount, - tempo, - tempoRange, - timeSignature, - duration, - loop, - loopStartTick, - minutesSpent, - vanillaInstrumentCount, - customInstrumentCount, - firstCustomInstrumentIndex, - instrumentNoteCounts, - customInstrumentNoteCount, - outOfRangeNoteCount, - detunedNoteCount, - incompatibleNoteCount, - compatible - }; - } - - private toObject() { - return this.stats; - } - - private getMidiFileName(): string { - return this.song.meta.importName || ''; - } - - private getCounts(tempoChangerInstruments: number[]): { - noteCount : number; - tickCount : number; - layerCount : number; - outOfRangeNoteCount : number; - detunedNoteCount : number; - customInstrumentNoteCount: number; - incompatibleNoteCount : number; - instrumentNoteCounts : number[]; - } { - let noteCount = 0; - let tickCount = 0; - let layerCount = 0; - let outOfRangeNoteCount = 0; - let detunedNoteCount = 0; - let customInstrumentNoteCount = 0; - let incompatibleNoteCount = 0; - - const instrumentNoteCounts = Array( - this.song.instruments.loaded.length - ).fill(0); - - for (const [layerId, layer] of this.song.layers.entries()) { - for (const tickStr in layer.notes) { - const note = layer.notes[tickStr]; - const tick = parseInt(tickStr); - - if (tick > tickCount) { - tickCount = tick; - } + public static getSongStats(song: Song) { + return new SongStatsGenerator(song).toObject(); + } + + private song : Song; + private stats: SongStatsType; + + private constructor(song: Song) { + this.song = song; + + const midiFileName = this.getMidiFileName(); + + const tempoChangerInstrumentIds = getTempoChangerInstrumentIds(this.song); + + const { + noteCount, + tickCount, + layerCount, + outOfRangeNoteCount, + detunedNoteCount, + customInstrumentNoteCount, + incompatibleNoteCount, + instrumentNoteCounts + } = this.getCounts(tempoChangerInstrumentIds); + + const tempoSegments = this.getTempoSegments(tempoChangerInstrumentIds); + + const tempo = this.getTempo(); + const tempoRange = this.getTempoRange(tempoSegments); + const timeSignature = this.getTimeSignature(); + const duration = this.getDuration(tempoSegments); + const loop = this.getLoop(); + const loopStartTick = this.getLoopStartTick(); + const minutesSpent = this.getMinutesSpent(); + + const { vanillaInstrumentCount, customInstrumentCount } = + this.getVanillaAndCustomUsedInstrumentCounts( + instrumentNoteCounts, + tempoChangerInstrumentIds + ); + + const firstCustomInstrumentIndex = this.getFirstCustomInstrumentIndex(); + + const compatible = incompatibleNoteCount === 0; + + this.stats = { + midiFileName, + noteCount, + tickCount, + layerCount, + tempo, + tempoRange, + timeSignature, + duration, + loop, + loopStartTick, + minutesSpent, + vanillaInstrumentCount, + customInstrumentCount, + firstCustomInstrumentIndex, + instrumentNoteCounts, + customInstrumentNoteCount, + outOfRangeNoteCount, + detunedNoteCount, + incompatibleNoteCount, + compatible + }; + } - // The song may store empty layers at the bottom. We actually want the last layer with a note in it - if (layerId > layerCount) { - layerCount = layerId; + private toObject() { + return this.stats; + } + + private getMidiFileName(): string { + return this.song.meta.importName || ''; + } + + private getCounts(tempoChangerInstruments: number[]): { + noteCount : number; + tickCount : number; + layerCount : number; + outOfRangeNoteCount : number; + detunedNoteCount : number; + customInstrumentNoteCount: number; + incompatibleNoteCount : number; + instrumentNoteCounts : number[]; + } { + let noteCount = 0; + let tickCount = 0; + let layerCount = 0; + let outOfRangeNoteCount = 0; + let detunedNoteCount = 0; + let customInstrumentNoteCount = 0; + let incompatibleNoteCount = 0; + + const instrumentNoteCounts = Array( + this.song.instruments.loaded.length + ).fill(0); + + for (const [layerId, layer] of this.song.layers.entries()) { + for (const tickStr in layer.notes) { + const note = layer.notes[tickStr]; + const tick = parseInt(tickStr); + + if (tick > tickCount) { + tickCount = tick; + } + + // The song may store empty layers at the bottom. We actually want the last layer with a note in it + if (layerId > layerCount) { + layerCount = layerId; + } + + // @ts-ignore //TODO: fix this + const effectivePitch = note.key + note.pitch / 100; + + // Differences between Note Block Studio and this implementation: + + // DETUNED NOTES + // The behavior here differs from Open Note Block Studio v3.10, since it doesn't consider + // non-integer/microtonal notes when deciding if a song is compatible. This is likely to + // change in the future. Since this is relevant to knowing accurately if vanilla note blocks + // can support the song, NBW uses a more modern approach of counting microtonal notes as + // outside the 2-octave range - treating it as only the piano keys between 33-57 and not + // anything in the interval between them. + + // INSTRUMENT PITCH + // We also use the instrument's original pitch when determining if it's out-of-range. + // Note Block Studio also doesn't take this into account - since importing custom sounds + // into the game was out of question back in the legacy versions, we used to only need + // to worry about vanilla note block compatibility (for schematics). + // Now that data packs are a thing, out-of-range notes become relevant not only due to + // note block's key range, but also because the same limit applies to Minecraft's audio + // engine as a whole (e.g. /playsound etc). + // But if the instrument's key is not set to F#4 (45), the range supported by Minecraft + // (without needing to re-pitch the sound externally) also changes (it is always one octave + // above and below the instrument's key). Note Block Studio doesn't account for this - the + // supported range is always F#3 to F#5 (33-57) - but we do because it's useful to know if + // the default Minecraft sounds are enough to play the song (i.e. you can play it using only + // a custom sounds.json in a resource pack). + + // @ts-ignore //TODO: fix this + const instrumentKey = this.song.instruments.loaded[note.instrument].key; // F#4 = 45 + const minRange = 45 - (instrumentKey - 45) - 12; // F#3 = 33 + const maxRange = 45 - (instrumentKey - 45) + 12; // F#5 = 57 + + const isOutOfRange = + effectivePitch < minRange || effectivePitch > maxRange; + + // Don't consider tempo changers as detuned notes or custom instruments + const isTempoChanger = tempoChangerInstruments.includes( + // @ts-ignore //TODO: fix this + note.instrument + ); + + // @ts-ignore //TODO: fix this + const hasDetune = note.pitch % 100 !== 0; + + const usesCustomInstrument = // @ts-ignore //TODO: fix this + note.instrument >= this.song.instruments.firstCustomIndex; + + if (!isTempoChanger) { + if (isOutOfRange) outOfRangeNoteCount++; + if (hasDetune) detunedNoteCount++; + if (usesCustomInstrument) customInstrumentNoteCount++; + if (isOutOfRange || hasDetune || usesCustomInstrument) + incompatibleNoteCount++; + } + + // @ts-ignore //TODO: fix this + instrumentNoteCounts[note.instrument]++; + noteCount++; + } } - // @ts-ignore //TODO: fix this - const effectivePitch = note.key + note.pitch / 100; - - // Differences between Note Block Studio and this implementation: - - // DETUNED NOTES - // The behavior here differs from Open Note Block Studio v3.10, since it doesn't consider - // non-integer/microtonal notes when deciding if a song is compatible. This is likely to - // change in the future. Since this is relevant to knowing accurately if vanilla note blocks - // can support the song, NBW uses a more modern approach of counting microtonal notes as - // outside the 2-octave range - treating it as only the piano keys between 33-57 and not - // anything in the interval between them. - - // INSTRUMENT PITCH - // We also use the instrument's original pitch when determining if it's out-of-range. - // Note Block Studio also doesn't take this into account - since importing custom sounds - // into the game was out of question back in the legacy versions, we used to only need - // to worry about vanilla note block compatibility (for schematics). - // Now that data packs are a thing, out-of-range notes become relevant not only due to - // note block's key range, but also because the same limit applies to Minecraft's audio - // engine as a whole (e.g. /playsound etc). - // But if the instrument's key is not set to F#4 (45), the range supported by Minecraft - // (without needing to re-pitch the sound externally) also changes (it is always one octave - // above and below the instrument's key). Note Block Studio doesn't account for this - the - // supported range is always F#3 to F#5 (33-57) - but we do because it's useful to know if - // the default Minecraft sounds are enough to play the song (i.e. you can play it using only - // a custom sounds.json in a resource pack). - - // @ts-ignore //TODO: fix this - const instrumentKey = this.song.instruments.loaded[note.instrument].key; // F#4 = 45 - const minRange = 45 - (instrumentKey - 45) - 12; // F#3 = 33 - const maxRange = 45 - (instrumentKey - 45) + 12; // F#5 = 57 - - const isOutOfRange = - effectivePitch < minRange || effectivePitch > maxRange; - - // Don't consider tempo changers as detuned notes or custom instruments - const isTempoChanger = tempoChangerInstruments.includes( - // @ts-ignore //TODO: fix this - note.instrument - ); + // Get end of last tick/layer instead of start + tickCount++; + layerCount++; + + return { + noteCount, + tickCount, + layerCount, + outOfRangeNoteCount, + detunedNoteCount, + customInstrumentNoteCount, + incompatibleNoteCount, + instrumentNoteCounts + }; + } - // @ts-ignore //TODO: fix this - const hasDetune = note.pitch % 100 !== 0; + private getTempo(): number { + return this.song.tempo; + } - const usesCustomInstrument = // @ts-ignore //TODO: fix this - note.instrument >= this.song.instruments.firstCustomIndex; + private getTempoRange( + tempoSegments: Record + ): number[] | null { + const tempoValues = Object.values(tempoSegments); + // If song has only the tempo set at the beginning, we have no tempo changes (indicated as null) + if (tempoValues.length === 1) return null; - if (!isTempoChanger) { - if (isOutOfRange) outOfRangeNoteCount++; - if (hasDetune) detunedNoteCount++; - if (usesCustomInstrument) customInstrumentNoteCount++; - if (isOutOfRange || hasDetune || usesCustomInstrument) - incompatibleNoteCount++; - } + const minTempo = Math.min(...tempoValues); + const maxTempo = Math.max(...tempoValues); - // @ts-ignore //TODO: fix this - instrumentNoteCounts[note.instrument]++; - noteCount++; - } + return [minTempo, maxTempo]; } - // Get end of last tick/layer instead of start - tickCount++; - layerCount++; - - return { - noteCount, - tickCount, - layerCount, - outOfRangeNoteCount, - detunedNoteCount, - customInstrumentNoteCount, - incompatibleNoteCount, - instrumentNoteCounts - }; - } - - private getTempo(): number { - return this.song.tempo; - } - - private getTempoRange( - tempoSegments: Record - ): number[] | null { - const tempoValues = Object.values(tempoSegments); - // If song has only the tempo set at the beginning, we have no tempo changes (indicated as null) - if (tempoValues.length === 1) return null; - - const minTempo = Math.min(...tempoValues); - const maxTempo = Math.max(...tempoValues); - - return [minTempo, maxTempo]; - } - - private getTempoSegments( - tempoChangerInstruments: number[] - ): Record { - const tempoSegments: Record = {}; - - if (tempoChangerInstruments.length > 0) { - // TODO: toReversed - for (const layer of Array.from(this.song.layers).reverse()) { - for (const tickStr in layer.notes) { - const note = layer.notes[tickStr]; - const tick = parseInt(tickStr); - - // @ts-ignore //TODO: fix this // Not a tempo changer - if (!tempoChangerInstruments.includes(note.instrument)) continue; - - // The tempo change isn't effective if there's another tempo changer in the same tick, - // so we iterate layers bottom to top and skip the block if a tempo changer has already - // been found in this tick - if (tick in tempoSegments) continue; - - // @ts-ignore //TODO: fix this - const tempo = Math.abs(note.pitch) / 15; // note pitch = BPM = (t/s) * 15 - tempoSegments[tick] = tempo; + private getTempoSegments( + tempoChangerInstruments: number[] + ): Record { + const tempoSegments: Record = {}; + + if (tempoChangerInstruments.length > 0) { + // TODO: toReversed + for (const layer of Array.from(this.song.layers).reverse()) { + for (const tickStr in layer.notes) { + const note = layer.notes[tickStr]; + const tick = parseInt(tickStr); + + // @ts-ignore //TODO: fix this // Not a tempo changer + if (!tempoChangerInstruments.includes(note.instrument)) continue; + + // The tempo change isn't effective if there's another tempo changer in the same tick, + // so we iterate layers bottom to top and skip the block if a tempo changer has already + // been found in this tick + if (tick in tempoSegments) continue; + + // @ts-ignore //TODO: fix this + const tempo = Math.abs(note.pitch) / 15; // note pitch = BPM = (t/s) * 15 + tempoSegments[tick] = tempo; + } + } } - } + + // If there isn't a tempo changer at tick 0, we add one there to set the starting tempo + tempoSegments[0] = 0 in tempoSegments ? tempoSegments[0] : this.song.tempo; + + return tempoSegments; + } + + private getTimeSignature(): number { + return this.song.timeSignature; } - // If there isn't a tempo changer at tick 0, we add one there to set the starting tempo - tempoSegments[0] = 0 in tempoSegments ? tempoSegments[0] : this.song.tempo; + private getDuration(tempoSegments: Record): number { + const tempoChangeTicks = Object.keys(tempoSegments).map((tick) => + parseInt(tick) + ); + + tempoChangeTicks.sort((a, b) => a - b); + + let duration = 0; - return tempoSegments; - } + // Add end of last tick to close last tempo segment + const lastTick = this.song.length + 1; - private getTimeSignature(): number { - return this.song.timeSignature; - } + if (!(lastTick in tempoChangeTicks)) { + tempoChangeTicks.push(lastTick); + } - private getDuration(tempoSegments: Record): number { - const tempoChangeTicks = Object.keys(tempoSegments).map((tick) => - parseInt(tick) - ); + // Iterate pairs of tempo change ticks and calculate their length + for (let i = 0; i < tempoChangeTicks.length - 1; i++) { + const currTick = tempoChangeTicks[i]; + const nextTick = tempoChangeTicks[i + 1]; - tempoChangeTicks.sort((a, b) => a - b); + // @ts-ignore //TODO: fix this + const currTempo = tempoSegments[currTick]; - let duration = 0; + // @ts-ignore //TODO: fix this + const segmentDurationTicks = nextTick - currTick; + const timePerTick = 1 / currTempo; - // Add end of last tick to close last tempo segment - const lastTick = this.song.length + 1; + duration += segmentDurationTicks * timePerTick; + } - if (!(lastTick in tempoChangeTicks)) { - tempoChangeTicks.push(lastTick); + return duration; } - // Iterate pairs of tempo change ticks and calculate their length - for (let i = 0; i < tempoChangeTicks.length - 1; i++) { - const currTick = tempoChangeTicks[i]; - const nextTick = tempoChangeTicks[i + 1]; + private getLoop(): boolean { + return this.song.loop.enabled; + } - // @ts-ignore //TODO: fix this - const currTempo = tempoSegments[currTick]; + private getLoopStartTick(): number { + return this.song.loop.startTick; + } - // @ts-ignore //TODO: fix this - const segmentDurationTicks = nextTick - currTick; - const timePerTick = 1 / currTempo; + private getMinutesSpent(): number { + return this.song.stats.minutesSpent; + } - duration += segmentDurationTicks * timePerTick; + private getVanillaAndCustomUsedInstrumentCounts( + noteCountsPerInstrument: number[], + tempoChangerInstruments: number[] + ): { + vanillaInstrumentCount: number; + customInstrumentCount : number; + } { + const firstCustomIndex = this.song.instruments.firstCustomIndex; + + // We want the count of instruments that have at least one note in the song + // (which tells us how many instruments are effectively used in the song) + + const vanillaInstrumentCount = noteCountsPerInstrument + .slice(0, firstCustomIndex) + .filter((count) => count > 0).length; + + const customInstrumentCount = noteCountsPerInstrument + .filter((_, index) => !tempoChangerInstruments.includes(index)) + .slice(firstCustomIndex) + .filter((count) => count > 0).length; + + return { + vanillaInstrumentCount, + customInstrumentCount + }; } - return duration; - } - - private getLoop(): boolean { - return this.song.loop.enabled; - } - - private getLoopStartTick(): number { - return this.song.loop.startTick; - } - - private getMinutesSpent(): number { - return this.song.stats.minutesSpent; - } - - private getVanillaAndCustomUsedInstrumentCounts( - noteCountsPerInstrument: number[], - tempoChangerInstruments: number[] - ): { - vanillaInstrumentCount: number; - customInstrumentCount : number; - } { - const firstCustomIndex = this.song.instruments.firstCustomIndex; - - // We want the count of instruments that have at least one note in the song - // (which tells us how many instruments are effectively used in the song) - - const vanillaInstrumentCount = noteCountsPerInstrument - .slice(0, firstCustomIndex) - .filter((count) => count > 0).length; - - const customInstrumentCount = noteCountsPerInstrument - .filter((_, index) => !tempoChangerInstruments.includes(index)) - .slice(firstCustomIndex) - .filter((count) => count > 0).length; - - return { - vanillaInstrumentCount, - customInstrumentCount - }; - } - - private getFirstCustomInstrumentIndex(): number { - return this.song.instruments.firstCustomIndex; - } + private getFirstCustomInstrumentIndex(): number { + return this.song.instruments.firstCustomIndex; + } } diff --git a/packages/song/src/types.ts b/packages/song/src/types.ts index d141c1d4..65ded8b3 100644 --- a/packages/song/src/types.ts +++ b/packages/song/src/types.ts @@ -3,31 +3,31 @@ import { SongStats } from '@nbw/database'; import { NoteQuadTree } from './notes'; export type SongFileType = { - title : string; - description : string; - author : string; - originalAuthor: string; - length : number; - height : number; - arrayBuffer : ArrayBuffer; - notes : NoteQuadTree; - instruments : InstrumentArray; + title : string; + description : string; + author : string; + originalAuthor: string; + length : number; + height : number; + arrayBuffer : ArrayBuffer; + notes : NoteQuadTree; + instruments : InstrumentArray; }; export type InstrumentArray = Instrument[]; export type Note = { - tick : number; - layer : number; - key : number; - instrument: number; + tick : number; + layer : number; + key : number; + instrument: number; }; export type Instrument = { - id : number; - name : string; - file : string; - count: number; + id : number; + name : string; + file : string; + count: number; }; export type SongStatsType = InstanceType; diff --git a/packages/song/src/util.ts b/packages/song/src/util.ts index ba9e722d..5f32b5ba 100644 --- a/packages/song/src/util.ts +++ b/packages/song/src/util.ts @@ -1,28 +1,28 @@ import { Song } from '@encode42/nbs.js'; export function getTempoChangerInstrumentIds(song: Song): number[] { - return song.instruments.loaded.flatMap((instrument, id) => - instrument.meta.name === 'Tempo Changer' ? [id] : [] - ); + return song.instruments.loaded.flatMap((instrument, id) => + instrument.meta.name === 'Tempo Changer' ? [id] : [] + ); } export function getInstrumentNoteCounts(song: Song): Record { - const blockCounts = Object.fromEntries( - Object.keys(song.instruments.loaded).map((instrumentId) => [ - instrumentId, - 0 - ]) - ); + const blockCounts = Object.fromEntries( + Object.keys(song.instruments.loaded).map((instrumentId) => [ + instrumentId, + 0 + ]) + ); - for (const layer of song.layers) { - for (const tick in layer.notes) { - const note = layer.notes[tick]; + for (const layer of song.layers) { + for (const tick in layer.notes) { + const note = layer.notes[tick]; - // @ts-ignore //TODO: fix this - const instrumentId = note.instrument; - blockCounts[instrumentId] = (blockCounts[instrumentId] || 0) + 1; + // @ts-ignore //TODO: fix this + const instrumentId = note.instrument; + blockCounts[instrumentId] = (blockCounts[instrumentId] || 0) + 1; + } } - } - return blockCounts; + return blockCounts; } diff --git a/packages/song/tests/song/index.spec.ts b/packages/song/tests/song/index.spec.ts index ea6cd645..d99a00c6 100644 --- a/packages/song/tests/song/index.spec.ts +++ b/packages/song/tests/song/index.spec.ts @@ -13,185 +13,185 @@ import { openSongFromPath } from './util'; // TODO: refactor to use a proper test runner (e.g. jest) const testSongPaths = { - simple : 'files/testSimple.nbs', - extraPopulatedLayer : 'files/testExtraPopulatedLayer.nbs', - loop : 'files/testLoop.nbs', - detune : 'files/testDetune.nbs', - outOfRange : 'files/testOutOfRange.nbs', - outOfRangeCustomPitch : 'files/testOutOfRangeCustomPitch.nbs', - customInstrumentNoUsage : 'files/testCustomInstrumentNoUsage.nbs', - customInstrumentUsage : 'files/testCustomInstrumentUsage.nbs', - tempoChangerWithStart : 'files/testTempoChangerWithStart.nbs', - tempoChangerNoStart : 'files/testTempoChangerNoStart.nbs', - tempoChangerDifferentStart: 'files/testTempoChangerDifferentStart.nbs', - tempoChangerOverlap : 'files/testTempoChangerOverlap.nbs', - tempoChangerMultipleInstruments: + simple : 'files/testSimple.nbs', + extraPopulatedLayer : 'files/testExtraPopulatedLayer.nbs', + loop : 'files/testLoop.nbs', + detune : 'files/testDetune.nbs', + outOfRange : 'files/testOutOfRange.nbs', + outOfRangeCustomPitch : 'files/testOutOfRangeCustomPitch.nbs', + customInstrumentNoUsage : 'files/testCustomInstrumentNoUsage.nbs', + customInstrumentUsage : 'files/testCustomInstrumentUsage.nbs', + tempoChangerWithStart : 'files/testTempoChangerWithStart.nbs', + tempoChangerNoStart : 'files/testTempoChangerNoStart.nbs', + tempoChangerDifferentStart: 'files/testTempoChangerDifferentStart.nbs', + tempoChangerOverlap : 'files/testTempoChangerOverlap.nbs', + tempoChangerMultipleInstruments: 'files/testTempoChangerMultipleInstruments.nbs' }; const testSongStats = Object.fromEntries( - Object.entries(testSongPaths).map(([name, path]) => { - return [name, SongStatsGenerator.getSongStats(openSongFromPath(path))]; - }) + Object.entries(testSongPaths).map(([name, path]) => { + return [name, SongStatsGenerator.getSongStats(openSongFromPath(path))]; + }) ); describe('SongStatsGenerator', () => { - it('Test that the stats are correctly calculated for a simple song with no special properties.', () => { - const stats = testSongStats.simple; - - assert(stats.midiFileName === ''); - assert(stats.noteCount === 10); - assert(stats.tickCount === 19); - assert(stats.layerCount === 3); - assert(stats.tempo === 10.0); - assert(stats.tempoRange === null); - assert(stats.timeSignature === 4); - assert(stats.duration.toFixed(2) === '1.90'); - assert(stats.loop === false); - assert(stats.loopStartTick === 0); - // assert(stats.minutesSpent === 0); - assert(stats.vanillaInstrumentCount === 5); - assert(stats.customInstrumentCount === 0); - assert(stats.firstCustomInstrumentIndex === 16); - assert(stats.customInstrumentNoteCount === 0); - assert(stats.outOfRangeNoteCount === 0); - assert(stats.detunedNoteCount === 0); - assert(stats.incompatibleNoteCount === 0); - assert(stats.compatible === true); - - assert( - stats.instrumentNoteCounts.toString() === + it('Test that the stats are correctly calculated for a simple song with no special properties.', () => { + const stats = testSongStats.simple; + + assert(stats.midiFileName === ''); + assert(stats.noteCount === 10); + assert(stats.tickCount === 19); + assert(stats.layerCount === 3); + assert(stats.tempo === 10.0); + assert(stats.tempoRange === null); + assert(stats.timeSignature === 4); + assert(stats.duration.toFixed(2) === '1.90'); + assert(stats.loop === false); + assert(stats.loopStartTick === 0); + // assert(stats.minutesSpent === 0); + assert(stats.vanillaInstrumentCount === 5); + assert(stats.customInstrumentCount === 0); + assert(stats.firstCustomInstrumentIndex === 16); + assert(stats.customInstrumentNoteCount === 0); + assert(stats.outOfRangeNoteCount === 0); + assert(stats.detunedNoteCount === 0); + assert(stats.incompatibleNoteCount === 0); + assert(stats.compatible === true); + + assert( + stats.instrumentNoteCounts.toString() === [5, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 2].toString() - ); - }); + ); + }); - it('Test that the stats are correctly calculated for a song with an extra populated layer. This means that the last layer has a property changed (like volume or pitch) but no note blocks.', () => { - const stats = testSongStats.extraPopulatedLayer; + it('Test that the stats are correctly calculated for a song with an extra populated layer. This means that the last layer has a property changed (like volume or pitch) but no note blocks.', () => { + const stats = testSongStats.extraPopulatedLayer; - // Should be 5 if we want the last layer with a property changed, regardless - // of the last layer with a note block. We currently don't account for this. - assert(stats.layerCount === 3); - }); + // Should be 5 if we want the last layer with a property changed, regardless + // of the last layer with a note block. We currently don't account for this. + assert(stats.layerCount === 3); + }); - it('Test that the loop values are correct for a song that loops.', () => { - const stats = testSongStats.loop; + it('Test that the loop values are correct for a song that loops.', () => { + const stats = testSongStats.loop; - assert(stats.loop === true); - assert(stats.loopStartTick === 7); - }); + assert(stats.loop === true); + assert(stats.loopStartTick === 7); + }); - it('Test that notes with microtonal detune values are properly counted, and make the song incompatible. Also checks that notes crossing the 2-octave range boundary via pitch values are taken into account.', () => { - const stats = testSongStats.detune; + it('Test that notes with microtonal detune values are properly counted, and make the song incompatible. Also checks that notes crossing the 2-octave range boundary via pitch values are taken into account.', () => { + const stats = testSongStats.detune; - assert(stats.noteCount === 10); - assert(stats.detunedNoteCount === 4); - assert(stats.incompatibleNoteCount === 6); - assert(stats.outOfRangeNoteCount === 2); - assert(stats.compatible === false); - }); + assert(stats.noteCount === 10); + assert(stats.detunedNoteCount === 4); + assert(stats.incompatibleNoteCount === 6); + assert(stats.outOfRangeNoteCount === 2); + assert(stats.compatible === false); + }); - it('Test that notes outside the 2-octave range are properly counted in a song where every instrument uses the default pitch (F#4 - 45).', () => { - const stats = testSongStats.outOfRange; + it('Test that notes outside the 2-octave range are properly counted in a song where every instrument uses the default pitch (F#4 - 45).', () => { + const stats = testSongStats.outOfRange; - assert(stats.outOfRangeNoteCount === 6); - assert(stats.incompatibleNoteCount === 6); - assert(stats.compatible === false); - }); + assert(stats.outOfRangeNoteCount === 6); + assert(stats.incompatibleNoteCount === 6); + assert(stats.compatible === false); + }); - it("Test that notes outside the 2-octave range are properly counted in a song with instruments that use custom pitch values. The code should calculate the 2-octave supported range based on the instrument's pitch value.", () => { - const stats = testSongStats.outOfRangeCustomPitch; + it("Test that notes outside the 2-octave range are properly counted in a song with instruments that use custom pitch values. The code should calculate the 2-octave supported range based on the instrument's pitch value.", () => { + const stats = testSongStats.outOfRangeCustomPitch; - assert(stats.outOfRangeNoteCount === stats.noteCount - 3); - }); + assert(stats.outOfRangeNoteCount === stats.noteCount - 3); + }); - it("Test that the instrument counts are correctly calculated if the song contains custom instruments, but doesn't use them in any note.", () => { - const stats = testSongStats.customInstrumentNoUsage; + it("Test that the instrument counts are correctly calculated if the song contains custom instruments, but doesn't use them in any note.", () => { + const stats = testSongStats.customInstrumentNoUsage; - assert(stats.customInstrumentCount === 0); - assert(stats.customInstrumentNoteCount === 0); + assert(stats.customInstrumentCount === 0); + assert(stats.customInstrumentNoteCount === 0); - assert(stats.compatible === true); - }); + assert(stats.compatible === true); + }); - it('Test that the instrument counts are correctly calculated if the song contains custom instruments and uses them.', () => { + it('Test that the instrument counts are correctly calculated if the song contains custom instruments and uses them.', () => { // Test that the instrument counts are correctly calculated if the song contains custom instruments and uses them. - const stats = testSongStats.customInstrumentUsage; - const firstCustomIndex = stats.firstCustomInstrumentIndex; + const stats = testSongStats.customInstrumentUsage; + const firstCustomIndex = stats.firstCustomInstrumentIndex; - assert(stats.customInstrumentCount === 2); - assert(stats.customInstrumentNoteCount > 0); + assert(stats.customInstrumentCount === 2); + assert(stats.customInstrumentNoteCount > 0); - assert(stats.instrumentNoteCounts[firstCustomIndex + 0] === 3); - assert(stats.instrumentNoteCounts[firstCustomIndex + 1] === 0); - assert(stats.instrumentNoteCounts[firstCustomIndex + 2] === 2); + assert(stats.instrumentNoteCounts[firstCustomIndex + 0] === 3); + assert(stats.instrumentNoteCounts[firstCustomIndex + 1] === 0); + assert(stats.instrumentNoteCounts[firstCustomIndex + 2] === 2); - assert(stats.compatible === false); - }); + assert(stats.compatible === false); + }); - it("Test with tempo changes. Includes a tempo changer at the start of the song which matches the song's default tempo.", () => { - const stats = testSongStats.tempoChangerWithStart; + it("Test with tempo changes. Includes a tempo changer at the start of the song which matches the song's default tempo.", () => { + const stats = testSongStats.tempoChangerWithStart; - const duration = (1 / 10 + 1 / 12 + 1 / 14 + 1 / 16 + 1 / 18) * 4; + const duration = (1 / 10 + 1 / 12 + 1 / 14 + 1 / 16 + 1 / 18) * 4; - assert(duration.toFixed(2) === stats.duration.toFixed(2)); - assert(stats.tempo === 10.0); - assert(stats.tempoRange?.toString() === [10.0, 18.0].toString()); + assert(duration.toFixed(2) === stats.duration.toFixed(2)); + assert(stats.tempo === 10.0); + assert(stats.tempoRange?.toString() === [10.0, 18.0].toString()); - // Tempo changers shouldn't count as detuned notes, increase custom instrument count - // or incompatible note count. - assert(stats.detunedNoteCount === 0); - assert(stats.customInstrumentCount === 0); - assert(stats.customInstrumentNoteCount === 0); - assert(stats.incompatibleNoteCount === 0); - assert(stats.compatible === true); - }); + // Tempo changers shouldn't count as detuned notes, increase custom instrument count + // or incompatible note count. + assert(stats.detunedNoteCount === 0); + assert(stats.customInstrumentCount === 0); + assert(stats.customInstrumentNoteCount === 0); + assert(stats.incompatibleNoteCount === 0); + assert(stats.compatible === true); + }); - it("Omits the tempo changer at the start. The code should properly consider the song's default tempo at the start of the song.", () => { - const stats = testSongStats.tempoChangerNoStart; + it("Omits the tempo changer at the start. The code should properly consider the song's default tempo at the start of the song.", () => { + const stats = testSongStats.tempoChangerNoStart; - const duration = (1 / 10 + 1 / 12 + 1 / 14 + 1 / 16 + 1 / 18) * 4; + const duration = (1 / 10 + 1 / 12 + 1 / 14 + 1 / 16 + 1 / 18) * 4; - assert(duration.toFixed(2) === stats.duration.toFixed(2)); - assert(stats.tempo === 10.0); - assert(stats.tempoRange?.toString() === [10.0, 18.0].toString()); - }); + assert(duration.toFixed(2) === stats.duration.toFixed(2)); + assert(stats.tempo === 10.0); + assert(stats.tempoRange?.toString() === [10.0, 18.0].toString()); + }); - it("Includes a tempo changer at the start of the song with a different tempo than the song's default tempo.", () => { + it("Includes a tempo changer at the start of the song with a different tempo than the song's default tempo.", () => { // Includes a tempo changer at the start of the song with a different tempo // than the song's default tempo. The code should ignore the song's default // tempo and use the tempo from the tempo changer for calculating the song's // duration and tempo range. However, the 'tempo' attribute should still be set // to the song's default tempo. - const stats = testSongStats.tempoChangerDifferentStart; + const stats = testSongStats.tempoChangerDifferentStart; - const duration = (1 / 20 + 1 / 12 + 1 / 14 + 1 / 16 + 1 / 18) * 4; + const duration = (1 / 20 + 1 / 12 + 1 / 14 + 1 / 16 + 1 / 18) * 4; - assert(duration.toFixed(2) === stats.duration.toFixed(2)); - assert(stats.tempo === 10.0); - assert(stats.tempoRange?.toString() === [12.0, 20.0].toString()); - }); + assert(duration.toFixed(2) === stats.duration.toFixed(2)); + assert(stats.tempo === 10.0); + assert(stats.tempoRange?.toString() === [12.0, 20.0].toString()); + }); - it('Includes overlapping tempo changers within the same tick. The code should only consider the bottom-most tempo changer in each tick.', () => { - const stats = testSongStats.tempoChangerOverlap; + it('Includes overlapping tempo changers within the same tick. The code should only consider the bottom-most tempo changer in each tick.', () => { + const stats = testSongStats.tempoChangerOverlap; - const duration = (1 / 10 + 1 / 12 + 1 / 4 + 1 / 16 + 1 / 18) * 4; + const duration = (1 / 10 + 1 / 12 + 1 / 4 + 1 / 16 + 1 / 18) * 4; - assert(duration.toFixed(2) === stats.duration.toFixed(2)); - assert(stats.tempo === 10.0); - assert(stats.tempoRange?.toString() === [4.0, 18.0].toString()); - }); + assert(duration.toFixed(2) === stats.duration.toFixed(2)); + assert(stats.tempo === 10.0); + assert(stats.tempoRange?.toString() === [4.0, 18.0].toString()); + }); - it('Test that multiple tempo changer instruments are properly handled.', () => { - const stats = testSongStats.tempoChangerMultipleInstruments; + it('Test that multiple tempo changer instruments are properly handled.', () => { + const stats = testSongStats.tempoChangerMultipleInstruments; - const duration = (1 / 10 + 1 / 12 + 1 / 14 + 1 / 16 + 1 / 18) * 4; + const duration = (1 / 10 + 1 / 12 + 1 / 14 + 1 / 16 + 1 / 18) * 4; - assert(duration.toFixed(2) === stats.duration.toFixed(2)); - assert(stats.tempo === 10.0); - assert(stats.tempoRange?.toString() === [10.0, 18.0].toString()); + assert(duration.toFixed(2) === stats.duration.toFixed(2)); + assert(stats.tempo === 10.0); + assert(stats.tempoRange?.toString() === [10.0, 18.0].toString()); - assert(stats.detunedNoteCount === 0); - }); + assert(stats.detunedNoteCount === 0); + }); }); diff --git a/packages/song/tests/song/util.ts b/packages/song/tests/song/util.ts index 40698920..fa729617 100644 --- a/packages/song/tests/song/util.ts +++ b/packages/song/tests/song/util.ts @@ -4,25 +4,25 @@ import { join, resolve } from 'path'; import { Song, fromArrayBuffer } from '@encode42/nbs.js'; export function openSongFromPath(path: string): Song { - // Specify the relative path to the file - const filePath = join(resolve(__dirname), path); + // Specify the relative path to the file + const filePath = join(resolve(__dirname), path); - // Read the file and get its array buffer - const buffer = asArrayBuffer(readFileSync(filePath)); + // Read the file and get its array buffer + const buffer = asArrayBuffer(readFileSync(filePath)); - const song = fromArrayBuffer(buffer); + const song = fromArrayBuffer(buffer); - return song; + return song; } function asArrayBuffer(buffer: Buffer): ArrayBuffer { - const arrayBuffer = new ArrayBuffer(buffer.length); - const view = new Uint8Array(arrayBuffer); + const arrayBuffer = new ArrayBuffer(buffer.length); + const view = new Uint8Array(arrayBuffer); - for (let i = 0; i < buffer.length; ++i) { + for (let i = 0; i < buffer.length; ++i) { // @ts-ignore //TODO: fix this - view[i] = buffer[i]; - } + view[i] = buffer[i]; + } - return arrayBuffer; + return arrayBuffer; } diff --git a/packages/sounds/src/fetchSoundList.ts b/packages/sounds/src/fetchSoundList.ts index c1881c13..69d2e289 100644 --- a/packages/sounds/src/fetchSoundList.ts +++ b/packages/sounds/src/fetchSoundList.ts @@ -1,86 +1,86 @@ import type { AssetIndexData, VersionData, VersionManifest } from './types'; const MANIFEST_URL = - 'https://piston-meta.mojang.com/mc/game/version_manifest_v2.json'; + 'https://piston-meta.mojang.com/mc/game/version_manifest_v2.json'; async function fetchWithCache(url: string) { - const response = await fetch(url); - console.log('fetchWithCache', url, response.status); - return response.json(); + const response = await fetch(url); + console.log('fetchWithCache', url, response.status); + return response.json(); } async function fetchVersionManifest() { - const url = MANIFEST_URL; - const data = await fetchWithCache(url); - return data as VersionManifest; + const url = MANIFEST_URL; + const data = await fetchWithCache(url); + return data as VersionManifest; } async function getLatestVersion(type: 'release' | 'snapshot' = 'release') { - const manifestData = await fetchVersionManifest(); - const version = manifestData.latest[type]; - return version; + const manifestData = await fetchVersionManifest(); + const version = manifestData.latest[type]; + return version; } async function getVersionSummary(version: string) { - const manifestData = await fetchVersionManifest(); - const versionData = manifestData.versions.find((v) => v.id === version); + const manifestData = await fetchVersionManifest(); + const versionData = manifestData.versions.find((v) => v.id === version); - if (!versionData) { - throw new Error(`Version ${version} not found`); - } + if (!versionData) { + throw new Error(`Version ${version} not found`); + } - return versionData; + return versionData; } async function getVersionData(version: string) { - const versionData = await getVersionSummary(version); - const url = versionData.url; - const data = await fetchWithCache(url); - return data as VersionData; + const versionData = await getVersionSummary(version); + const url = versionData.url; + const data = await fetchWithCache(url); + return data as VersionData; } async function getAssetIndexSummary(version: string) { - const versionData = await getVersionData(version); - const assetIndex = versionData.assetIndex; - return assetIndex; + const versionData = await getVersionData(version); + const assetIndex = versionData.assetIndex; + return assetIndex; } async function getAssetIndexObjects(version: string) { - const assetIndex = await getAssetIndexSummary(version); - const url = assetIndex.url; - const data = (await fetchWithCache(url)) as AssetIndexData; - return data.objects; + const assetIndex = await getAssetIndexSummary(version); + const url = assetIndex.url; + const data = (await fetchWithCache(url)) as AssetIndexData; + return data.objects; } async function getAssetIndexSounds(version: string) { - const objects = await getAssetIndexObjects(version); + const objects = await getAssetIndexObjects(version); - // Get a new record with only keys that end with '.ogg' - const sounds = Object.entries(objects).filter(([key]) => - key.endsWith('.ogg') - ); + // Get a new record with only keys that end with '.ogg' + const sounds = Object.entries(objects).filter(([key]) => + key.endsWith('.ogg') + ); - return sounds; + return sounds; } async function getSoundList(version: string) { - const sounds = await getAssetIndexSounds(version); + const sounds = await getAssetIndexSounds(version); - // Return an object with sound names as keys and hashes as values - const soundList = Object.fromEntries( - sounds.map(([key, { hash }]) => [key, hash]) - ); + // Return an object with sound names as keys and hashes as values + const soundList = Object.fromEntries( + sounds.map(([key, { hash }]) => [key, hash]) + ); - return soundList; + return soundList; } export async function getLatestVersionSoundList() { - const version = await getLatestVersion(); - const soundList = await getSoundList(version); - return soundList; + const version = await getLatestVersion(); + const soundList = await getSoundList(version); + return soundList; } // Export the return type of the function as SoundList export type SoundListType = Awaited< - ReturnType + ReturnType >; diff --git a/packages/sounds/src/types.ts b/packages/sounds/src/types.ts index cad1dc49..69d36f6c 100644 --- a/packages/sounds/src/types.ts +++ b/packages/sounds/src/types.ts @@ -1,98 +1,98 @@ export type VersionSummary = { - id : string; - type : 'release' | 'snapshot' | 'old_beta' | 'old_alpha'; - url : string; - time : string; - releaseTime : string; - sha1 : string; - complianceLevel: number; + id : string; + type : 'release' | 'snapshot' | 'old_beta' | 'old_alpha'; + url : string; + time : string; + releaseTime : string; + sha1 : string; + complianceLevel: number; }; export type VersionManifest = { - latest: { - release : string; - snapshot: string; - }; - versions: VersionSummary[]; + latest: { + release : string; + snapshot: string; + }; + versions: VersionSummary[]; }; type Action = 'allow' | 'disallow'; type OS = { - name: string; + name: string; }; type Rule = { - action: Action; - os : OS; + action: Action; + os : OS; }; type Artifact = { - path: string; - sha1: string; - size: number; - url : string; + path: string; + sha1: string; + size: number; + url : string; }; type Downloads = { - artifact: Artifact; + artifact: Artifact; }; type Library = { - downloads: Downloads; - name : string; - rules? : Rule[]; + downloads: Downloads; + name : string; + rules? : Rule[]; }; type File = { - id : string; - sha1: string; - size: number; - url : string; + id : string; + sha1: string; + size: number; + url : string; }; type Client = { - argument: string; - file : File; - type : string; + argument: string; + file : File; + type : string; }; type Logging = { - client: Client; + client: Client; }; export type VersionData = { - arguments: { - game: (string | { rules: Rule[]; value: string })[]; - jvm : string[]; - }; - assetIndex: { - id : string; - sha1 : string; - size : number; - totalSize: number; - url : string; - }; - assets : string; - complianceLevel: number; - downloads: { - client : Artifact; - client_mappings: Artifact; - server : Artifact; - server_mappings: Artifact; - }; - id : string; - libraries : Library[]; - logging : Logging; - mainClass : string; - minimumLauncherVersion: number; - releaseTime : string; - time : string; - type : string; + arguments: { + game: (string | { rules: Rule[]; value: string })[]; + jvm : string[]; + }; + assetIndex: { + id : string; + sha1 : string; + size : number; + totalSize: number; + url : string; + }; + assets : string; + complianceLevel: number; + downloads: { + client : Artifact; + client_mappings: Artifact; + server : Artifact; + server_mappings: Artifact; + }; + id : string; + libraries : Library[]; + logging : Logging; + mainClass : string; + minimumLauncherVersion: number; + releaseTime : string; + time : string; + type : string; }; export type AssetIndexRecord = Record; export type AssetIndexData = { - objects: AssetIndexRecord; + objects: AssetIndexRecord; }; diff --git a/packages/sounds/src/util.ts b/packages/sounds/src/util.ts index a8d82da0..2754cf31 100644 --- a/packages/sounds/src/util.ts +++ b/packages/sounds/src/util.ts @@ -3,23 +3,23 @@ export type RecordKey = string | number | symbol; export class TwoWayMap { - private map : Map; - private reverseMap: Map; + private map : Map; + private reverseMap: Map; - constructor(map: Map) { - this.map = map; - this.reverseMap = new Map(); + constructor(map: Map) { + this.map = map; + this.reverseMap = new Map(); - for (const [key, value] of map.entries()) { - this.reverseMap.set(value, key); + for (const [key, value] of map.entries()) { + this.reverseMap.set(value, key); + } } - } - get(key: T): U | undefined { - return this.map.get(key); - } + get(key: T): U | undefined { + return this.map.get(key); + } - revGet(key: U): T | undefined { - return this.reverseMap.get(key); - } + revGet(key: U): T | undefined { + return this.reverseMap.get(key); + } } diff --git a/packages/thumbnail/src/canvasFactory.ts b/packages/thumbnail/src/canvasFactory.ts index 3efc0d25..fbf2d5e4 100644 --- a/packages/thumbnail/src/canvasFactory.ts +++ b/packages/thumbnail/src/canvasFactory.ts @@ -7,172 +7,172 @@ import type { CanvasUtils } from './types'; let canvasUtils: CanvasUtils; if (typeof document === 'undefined') { - // Node.js/Bun environment - try { - const canvasModule = require('@napi-rs/canvas') as typeof NapiRs; - const path = require('path') as typeof Path; - - const { - createCanvas: nodeCreateCanvas, - loadImage: nodeLoadImage, - GlobalFonts - } = canvasModule; - - const getPath = (filename: string): string => { - const workingDir = process.cwd(); - - // Try different possible locations for the asset files - const possiblePaths = [ - // When running from package directory - path.join(workingDir, filename.split('/').join(path.sep)), - // When running from root directory - path.join( - workingDir, - 'packages', - 'thumbnail', - filename.split('/').join(path.sep) - ) - ]; - - // Check which path exists - const fs = require('fs'); - - for (const fullPath of possiblePaths) { - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - - // Default to first path if none exist - return possiblePaths[0]!; - }; - - const saveToImage = (canvas: NapiRs.Canvas) => canvas.encode('png'); - - const useFont = () => { - const path = getPath('assets/fonts/Lato-Regular.ttf').toString(); - console.log('Font path: ', path); - - GlobalFonts.registerFromPath(path, 'Lato'); - }; - - let noteBlockImage: Promise; - + // Node.js/Bun environment try { - const path = getPath('assets/img/note-block-grayscale.png'); + const canvasModule = require('@napi-rs/canvas') as typeof NapiRs; + const path = require('path') as typeof Path; + + const { + createCanvas: nodeCreateCanvas, + loadImage: nodeLoadImage, + GlobalFonts + } = canvasModule; + + const getPath = (filename: string): string => { + const workingDir = process.cwd(); + + // Try different possible locations for the asset files + const possiblePaths = [ + // When running from package directory + path.join(workingDir, filename.split('/').join(path.sep)), + // When running from root directory + path.join( + workingDir, + 'packages', + 'thumbnail', + filename.split('/').join(path.sep) + ) + ]; + + // Check which path exists + const fs = require('fs'); + + for (const fullPath of possiblePaths) { + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + // Default to first path if none exist + return possiblePaths[0]!; + }; + + const saveToImage = (canvas: NapiRs.Canvas) => canvas.encode('png'); + + const useFont = () => { + const path = getPath('assets/fonts/Lato-Regular.ttf').toString(); + console.log('Font path: ', path); + + GlobalFonts.registerFromPath(path, 'Lato'); + }; + + let noteBlockImage: Promise; + + try { + const path = getPath('assets/img/note-block-grayscale.png'); + + noteBlockImage = nodeLoadImage(path); + } catch (error) { + console.error('Error loading image: ', error); + noteBlockImage = Promise.reject(error); + } - noteBlockImage = nodeLoadImage(path); + canvasUtils = { + createCanvas : nodeCreateCanvas, + loadImage : nodeLoadImage, + getPath, + useFont, + saveToImage, + noteBlockImage, + DrawingCanvas: canvasModule.Canvas, + RenderedImage: canvasModule.Image + }; } catch (error) { - console.error('Error loading image: ', error); - noteBlockImage = Promise.reject(error); - } - - canvasUtils = { - createCanvas : nodeCreateCanvas, - loadImage : nodeLoadImage, - getPath, - useFont, - saveToImage, - noteBlockImage, - DrawingCanvas: canvasModule.Canvas, - RenderedImage: canvasModule.Image - }; - } catch (error) { // Fallback for when @napi-rs/canvas is not available (e.g., in browser build) - console.warn('@napi-rs/canvas not available, using browser fallbacks'); - + console.warn('@napi-rs/canvas not available, using browser fallbacks'); + + const createCanvas = (width: number, height: number) => { + if (typeof OffscreenCanvas !== 'undefined') { + return new OffscreenCanvas(width, height); + } + + throw new Error('OffscreenCanvas not available'); + }; + + const loadImage = (src: string): Promise => { + return Promise.reject( + new Error('loadImage not available in this environment') + ); + }; + + const getPath = (filename: string) => filename; + + const saveToImage = (_canvas: any) => { + throw new Error('saveToImage not implemented in browser'); + }; + + const useFont = () => { + // No-op in fallback + }; + + const noteBlockImage = Promise.reject( + new Error('noteBlockImage not available in this environment') + ); + + canvasUtils = { + createCanvas, + loadImage, + getPath, + useFont, + saveToImage, + noteBlockImage, + DrawingCanvas: HTMLCanvasElement as any, + RenderedImage: HTMLImageElement as any + }; + } +} else { + // Browser environment const createCanvas = (width: number, height: number) => { - if (typeof OffscreenCanvas !== 'undefined') { return new OffscreenCanvas(width, height); - } - - throw new Error('OffscreenCanvas not available'); }; - const loadImage = (src: string): Promise => { - return Promise.reject( - new Error('loadImage not available in this environment') - ); + const loadImage = (src: string): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); }; const getPath = (filename: string) => filename; const saveToImage = (_canvas: any) => { - throw new Error('saveToImage not implemented in browser'); + console.warn('saveToImage not implemented in browser'); + throw new Error('saveToImage not implemented in browser'); }; const useFont = () => { - // No-op in fallback - }; + const font = new FontFace('Lato', 'url(/fonts/Lato-Regular.ttf)'); - const noteBlockImage = Promise.reject( - new Error('noteBlockImage not available in this environment') - ); - - canvasUtils = { - createCanvas, - loadImage, - getPath, - useFont, - saveToImage, - noteBlockImage, - DrawingCanvas: HTMLCanvasElement as any, - RenderedImage: HTMLImageElement as any + font.load().then((loadedFont) => { + document.fonts.add(loadedFont); + }); }; - } -} else { - // Browser environment - const createCanvas = (width: number, height: number) => { - return new OffscreenCanvas(width, height); - }; - - const loadImage = (src: string): Promise => { - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = reject; - img.src = src; - }); - }; - - const getPath = (filename: string) => filename; - - const saveToImage = (_canvas: any) => { - console.warn('saveToImage not implemented in browser'); - throw new Error('saveToImage not implemented in browser'); - }; - const useFont = () => { - const font = new FontFace('Lato', 'url(/fonts/Lato-Regular.ttf)'); + const noteBlockImagePath = getPath('/img/note-block-grayscale.png'); - font.load().then((loadedFont) => { - document.fonts.add(loadedFont); - }); - }; + const noteBlockImage = loadImage(noteBlockImagePath); - const noteBlockImagePath = getPath('/img/note-block-grayscale.png'); - - const noteBlockImage = loadImage(noteBlockImagePath); + canvasUtils = { + createCanvas, + loadImage, + getPath, + useFont, + saveToImage, + noteBlockImage, + DrawingCanvas: HTMLCanvasElement, + RenderedImage: HTMLImageElement + }; +} - canvasUtils = { +export const { createCanvas, loadImage, getPath, useFont, saveToImage, noteBlockImage, - DrawingCanvas: HTMLCanvasElement, - RenderedImage: HTMLImageElement - }; -} - -export const { - createCanvas, - loadImage, - getPath, - useFont, - saveToImage, - noteBlockImage, - DrawingCanvas, - RenderedImage + DrawingCanvas, + RenderedImage } = canvasUtils; diff --git a/packages/thumbnail/src/index.ts b/packages/thumbnail/src/index.ts index 9461ee3b..14f626fb 100644 --- a/packages/thumbnail/src/index.ts +++ b/packages/thumbnail/src/index.ts @@ -4,10 +4,10 @@ export * from './utils'; import type { Note } from '@nbw/song'; import { - createCanvas, - noteBlockImage, - saveToImage, - useFont + createCanvas, + noteBlockImage, + saveToImage, + useFont } from './canvasFactory'; import type { Canvas, DrawParams } from './types'; import { getKeyText, instrumentColors, isDarkColor, tintImage } from './utils'; @@ -15,7 +15,7 @@ import { getKeyText, instrumentColors, isDarkColor, tintImage } from './utils'; useFont(); export const swap = async (src: Canvas, dst: Canvas) => { - /** + /** * Run a `drawFunction` that returns a canvas and draw it to the passed `canvas`. * * @param drawFunction - Function that draws to a canvas and returns it @@ -23,127 +23,127 @@ export const swap = async (src: Canvas, dst: Canvas) => { * * @returns Nothing */ - // Get canvas context - const ctx = dst.getContext('2d'); + // Get canvas context + const ctx = dst.getContext('2d'); - if (!ctx) { - throw new Error('Could not get canvas context'); - } + if (!ctx) { + throw new Error('Could not get canvas context'); + } - // Swap the canvas - ctx.drawImage(src, 0, 0); + // Swap the canvas + ctx.drawImage(src, 0, 0); }; export const drawNotesOffscreen = async ({ - notes, - startTick, - startLayer, - zoomLevel, - backgroundColor, - canvasWidth, - //canvasHeight, - imgWidth = 1280, - imgHeight = 768 + notes, + startTick, + startLayer, + zoomLevel, + backgroundColor, + canvasWidth, + //canvasHeight, + imgWidth = 1280, + imgHeight = 768 }: DrawParams) => { - // Create new offscreen canvas - const canvas = createCanvas(imgWidth, imgHeight); - const ctx = canvas.getContext('2d'); + // Create new offscreen canvas + const canvas = createCanvas(imgWidth, imgHeight); + const ctx = canvas.getContext('2d'); - if (!ctx) { - throw new Error('Could not get offscreen canvas context'); - } + if (!ctx) { + throw new Error('Could not get offscreen canvas context'); + } - // Disable anti-aliasing - ctx.imageSmoothingEnabled = false; + // Disable anti-aliasing + ctx.imageSmoothingEnabled = false; - // Calculate effective zoom level - const zoomFactor = 2 ** (zoomLevel - 1); + // Calculate effective zoom level + const zoomFactor = 2 ** (zoomLevel - 1); - // Set scale to draw image at correct thumbnail size - if (canvasWidth !== undefined) { - const scale = canvasWidth / imgWidth; - ctx.scale(scale, scale); - } + // Set scale to draw image at correct thumbnail size + if (canvasWidth !== undefined) { + const scale = canvasWidth / imgWidth; + ctx.scale(scale, scale); + } - const width = canvas.width; - const height = canvas.height; + const width = canvas.width; + const height = canvas.height; - ctx.clearRect(0, 0, width, height); + ctx.clearRect(0, 0, width, height); - // Draw background - ctx.fillStyle = backgroundColor; - ctx.fillRect(0, 0, width, height); + // Draw background + ctx.fillStyle = backgroundColor; + ctx.fillRect(0, 0, width, height); - // Check if the background color is dark or light - const isDark = isDarkColor(backgroundColor, 90); + // Check if the background color is dark or light + const isDark = isDarkColor(backgroundColor, 90); - if (isDark) { - ctx.fillStyle = 'rgba(255, 255, 255, 0.15)'; - } else { - ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; - } + if (isDark) { + ctx.fillStyle = 'rgba(255, 255, 255, 0.15)'; + } else { + ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; + } - // Draw vertical lines - for (let i = 0; i < width; i += 8 * zoomFactor) { - ctx.fillRect(i, 0, 1, height); - } + // Draw vertical lines + for (let i = 0; i < width; i += 8 * zoomFactor) { + ctx.fillRect(i, 0, 1, height); + } - const loadedNoteBlockImage = await noteBlockImage; + const loadedNoteBlockImage = await noteBlockImage; - // Iterate through note blocks and draw them - const endTick = startTick + width / (zoomFactor * 8) - 1; - const endLayer = startLayer + height / (zoomFactor * 8) - 1; + // Iterate through note blocks and draw them + const endTick = startTick + width / (zoomFactor * 8) - 1; + const endLayer = startLayer + height / (zoomFactor * 8) - 1; - const visibleNotes = notes.getNotesInRect({ - x1: startTick, - y1: startLayer, - x2: endTick, - y2: endLayer - }); + const visibleNotes = notes.getNotesInRect({ + x1: startTick, + y1: startLayer, + x2: endTick, + y2: endLayer + }); - visibleNotes.forEach(async (note: Note) => { + visibleNotes.forEach(async (note: Note) => { // Calculate position - const x = (note.tick - startTick) * 8 * zoomFactor; - const y = (note.layer - startLayer) * 8 * zoomFactor; - const overlayColor = instrumentColors[note.instrument % 16] ?? '#FF00FF'; - - if (!loadedNoteBlockImage) { - throw new Error('Note block image not loaded'); - } - - // Draw the note block - ctx.drawImage( - tintImage(loadedNoteBlockImage, overlayColor), - x, - y, - 8 * zoomFactor, - 8 * zoomFactor - ); - - // Draw the key text - const keyText = getKeyText(note.key); - ctx.fillStyle = '#ffffff'; - ctx.font = `${3 * zoomFactor}px Lato`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(keyText, x + 4 * zoomFactor, y + 4 * zoomFactor); - }); - - return canvas; + const x = (note.tick - startTick) * 8 * zoomFactor; + const y = (note.layer - startLayer) * 8 * zoomFactor; + const overlayColor = instrumentColors[note.instrument % 16] ?? '#FF00FF'; + + if (!loadedNoteBlockImage) { + throw new Error('Note block image not loaded'); + } + + // Draw the note block + ctx.drawImage( + tintImage(loadedNoteBlockImage, overlayColor), + x, + y, + 8 * zoomFactor, + 8 * zoomFactor + ); + + // Draw the key text + const keyText = getKeyText(note.key); + ctx.fillStyle = '#ffffff'; + ctx.font = `${3 * zoomFactor}px Lato`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(keyText, x + 4 * zoomFactor, y + 4 * zoomFactor); + }); + + return canvas; }; export const drawToImage = async (params: DrawParams): Promise => { - let canvas; - const { imgWidth, imgHeight } = params; + let canvas; + const { imgWidth, imgHeight } = params; - if (!canvas) { - canvas = createCanvas(imgWidth, imgHeight); - } + if (!canvas) { + canvas = createCanvas(imgWidth, imgHeight); + } - const output = await drawNotesOffscreen(params); - const byteArray = await saveToImage(output); + const output = await drawNotesOffscreen(params); + const byteArray = await saveToImage(output); - // Convert to Buffer - const buffer = Buffer.from(byteArray); - return buffer; + // Convert to Buffer + const buffer = Buffer.from(byteArray); + return buffer; }; diff --git a/packages/thumbnail/src/types.ts b/packages/thumbnail/src/types.ts index 0329c9f2..8346bb0e 100644 --- a/packages/thumbnail/src/types.ts +++ b/packages/thumbnail/src/types.ts @@ -4,15 +4,15 @@ import type { NoteQuadTree } from '@nbw/song'; import { DrawingCanvas, RenderedImage } from './canvasFactory'; export interface DrawParams { - notes : NoteQuadTree; - startTick : number; - startLayer : number; - zoomLevel : number; - backgroundColor: string; - canvasWidth? : number; - canvasHeight? : number; - imgWidth : number; - imgHeight : number; + notes : NoteQuadTree; + startTick : number; + startLayer : number; + zoomLevel : number; + backgroundColor: string; + canvasWidth? : number; + canvasHeight? : number; + imgWidth : number; + imgHeight : number; } export type Canvas = typeof DrawingCanvas; @@ -26,12 +26,12 @@ import type { */ export interface CanvasUtils { - createCanvas(width: number, height: number): any; - loadImage(src: string): Promise; - getPath(filename: string): string | URL; - useFont(): void; - saveToImage(canvas: HTMLCanvasElement | NapiRs.Canvas): Promise; - noteBlockImage: Promise | any; - DrawingCanvas : any; - RenderedImage : any; + createCanvas(width: number, height: number): any; + loadImage(src: string): Promise; + getPath(filename: string): string | URL; + useFont(): void; + saveToImage(canvas: HTMLCanvasElement | NapiRs.Canvas): Promise; + noteBlockImage: Promise | any; + DrawingCanvas : any; + RenderedImage : any; } diff --git a/packages/thumbnail/src/utils.spec.ts b/packages/thumbnail/src/utils.spec.ts index ed3fa582..58765045 100644 --- a/packages/thumbnail/src/utils.spec.ts +++ b/packages/thumbnail/src/utils.spec.ts @@ -3,155 +3,155 @@ import { describe, expect, it } from 'bun:test'; import { getKeyText, instrumentColors, isDarkColor } from './utils'; describe('instrumentColors', () => { - it('should contain 16 color codes', () => { - expect(instrumentColors).toHaveLength(16); + it('should contain 16 color codes', () => { + expect(instrumentColors).toHaveLength(16); - instrumentColors.forEach((color) => { - expect(color).toMatch(/^#[0-9a-f]{6}$/); + instrumentColors.forEach((color) => { + expect(color).toMatch(/^#[0-9a-f]{6}$/); + }); }); - }); }); describe('getKeyText', () => { - it('should return correct note and octave for given key number', () => { + it('should return correct note and octave for given key number', () => { // Full range of keys from 0 to 87 - const testCases = [ - { key: 0, expected: 'A0' }, - { key: 1, expected: 'A#0' }, - { key: 2, expected: 'B0' }, - { key: 3, expected: 'C1' }, - { key: 4, expected: 'C#1' }, - { key: 5, expected: 'D1' }, - { key: 6, expected: 'D#1' }, - { key: 7, expected: 'E1' }, - { key: 8, expected: 'F1' }, - { key: 9, expected: 'F#1' }, - { key: 10, expected: 'G1' }, - { key: 11, expected: 'G#1' }, - { key: 12, expected: 'A1' }, - { key: 13, expected: 'A#1' }, - { key: 14, expected: 'B1' }, - { key: 15, expected: 'C2' }, - { key: 16, expected: 'C#2' }, - { key: 17, expected: 'D2' }, - { key: 18, expected: 'D#2' }, - { key: 19, expected: 'E2' }, - { key: 20, expected: 'F2' }, - { key: 21, expected: 'F#2' }, - { key: 22, expected: 'G2' }, - { key: 23, expected: 'G#2' }, - { key: 24, expected: 'A2' }, - { key: 25, expected: 'A#2' }, - { key: 26, expected: 'B2' }, - { key: 27, expected: 'C3' }, - { key: 28, expected: 'C#3' }, - { key: 29, expected: 'D3' }, - { key: 30, expected: 'D#3' }, - { key: 31, expected: 'E3' }, - { key: 32, expected: 'F3' }, - { key: 33, expected: 'F#3' }, - { key: 34, expected: 'G3' }, - { key: 35, expected: 'G#3' }, - { key: 36, expected: 'A3' }, - { key: 37, expected: 'A#3' }, - { key: 38, expected: 'B3' }, - { key: 39, expected: 'C4' }, - { key: 40, expected: 'C#4' }, - { key: 41, expected: 'D4' }, - { key: 42, expected: 'D#4' }, - { key: 43, expected: 'E4' }, - { key: 44, expected: 'F4' }, - { key: 45, expected: 'F#4' }, - { key: 46, expected: 'G4' }, - { key: 47, expected: 'G#4' }, - { key: 48, expected: 'A4' }, - { key: 49, expected: 'A#4' }, - { key: 50, expected: 'B4' }, - { key: 51, expected: 'C5' }, - { key: 52, expected: 'C#5' }, - { key: 53, expected: 'D5' }, - { key: 54, expected: 'D#5' }, - { key: 55, expected: 'E5' }, - { key: 56, expected: 'F5' }, - { key: 57, expected: 'F#5' }, - { key: 58, expected: 'G5' }, - { key: 59, expected: 'G#5' }, - { key: 60, expected: 'A5' }, - { key: 61, expected: 'A#5' }, - { key: 62, expected: 'B5' }, - { key: 63, expected: 'C6' }, - { key: 64, expected: 'C#6' }, - { key: 65, expected: 'D6' }, - { key: 66, expected: 'D#6' }, - { key: 67, expected: 'E6' }, - { key: 68, expected: 'F6' }, - { key: 69, expected: 'F#6' }, - { key: 70, expected: 'G6' }, - { key: 71, expected: 'G#6' }, - { key: 72, expected: 'A6' }, - { key: 73, expected: 'A#6' }, - { key: 74, expected: 'B6' }, - { key: 75, expected: 'C7' }, - { key: 76, expected: 'C#7' }, - { key: 77, expected: 'D7' }, - { key: 78, expected: 'D#7' }, - { key: 79, expected: 'E7' }, - { key: 80, expected: 'F7' }, - { key: 81, expected: 'F#7' }, - { key: 82, expected: 'G7' }, - { key: 83, expected: 'G#7' }, - { key: 84, expected: 'A7' }, - { key: 85, expected: 'A#7' }, - { key: 86, expected: 'B7' }, - { key: 87, expected: 'C8' } - ]; + const testCases = [ + { key: 0, expected: 'A0' }, + { key: 1, expected: 'A#0' }, + { key: 2, expected: 'B0' }, + { key: 3, expected: 'C1' }, + { key: 4, expected: 'C#1' }, + { key: 5, expected: 'D1' }, + { key: 6, expected: 'D#1' }, + { key: 7, expected: 'E1' }, + { key: 8, expected: 'F1' }, + { key: 9, expected: 'F#1' }, + { key: 10, expected: 'G1' }, + { key: 11, expected: 'G#1' }, + { key: 12, expected: 'A1' }, + { key: 13, expected: 'A#1' }, + { key: 14, expected: 'B1' }, + { key: 15, expected: 'C2' }, + { key: 16, expected: 'C#2' }, + { key: 17, expected: 'D2' }, + { key: 18, expected: 'D#2' }, + { key: 19, expected: 'E2' }, + { key: 20, expected: 'F2' }, + { key: 21, expected: 'F#2' }, + { key: 22, expected: 'G2' }, + { key: 23, expected: 'G#2' }, + { key: 24, expected: 'A2' }, + { key: 25, expected: 'A#2' }, + { key: 26, expected: 'B2' }, + { key: 27, expected: 'C3' }, + { key: 28, expected: 'C#3' }, + { key: 29, expected: 'D3' }, + { key: 30, expected: 'D#3' }, + { key: 31, expected: 'E3' }, + { key: 32, expected: 'F3' }, + { key: 33, expected: 'F#3' }, + { key: 34, expected: 'G3' }, + { key: 35, expected: 'G#3' }, + { key: 36, expected: 'A3' }, + { key: 37, expected: 'A#3' }, + { key: 38, expected: 'B3' }, + { key: 39, expected: 'C4' }, + { key: 40, expected: 'C#4' }, + { key: 41, expected: 'D4' }, + { key: 42, expected: 'D#4' }, + { key: 43, expected: 'E4' }, + { key: 44, expected: 'F4' }, + { key: 45, expected: 'F#4' }, + { key: 46, expected: 'G4' }, + { key: 47, expected: 'G#4' }, + { key: 48, expected: 'A4' }, + { key: 49, expected: 'A#4' }, + { key: 50, expected: 'B4' }, + { key: 51, expected: 'C5' }, + { key: 52, expected: 'C#5' }, + { key: 53, expected: 'D5' }, + { key: 54, expected: 'D#5' }, + { key: 55, expected: 'E5' }, + { key: 56, expected: 'F5' }, + { key: 57, expected: 'F#5' }, + { key: 58, expected: 'G5' }, + { key: 59, expected: 'G#5' }, + { key: 60, expected: 'A5' }, + { key: 61, expected: 'A#5' }, + { key: 62, expected: 'B5' }, + { key: 63, expected: 'C6' }, + { key: 64, expected: 'C#6' }, + { key: 65, expected: 'D6' }, + { key: 66, expected: 'D#6' }, + { key: 67, expected: 'E6' }, + { key: 68, expected: 'F6' }, + { key: 69, expected: 'F#6' }, + { key: 70, expected: 'G6' }, + { key: 71, expected: 'G#6' }, + { key: 72, expected: 'A6' }, + { key: 73, expected: 'A#6' }, + { key: 74, expected: 'B6' }, + { key: 75, expected: 'C7' }, + { key: 76, expected: 'C#7' }, + { key: 77, expected: 'D7' }, + { key: 78, expected: 'D#7' }, + { key: 79, expected: 'E7' }, + { key: 80, expected: 'F7' }, + { key: 81, expected: 'F#7' }, + { key: 82, expected: 'G7' }, + { key: 83, expected: 'G#7' }, + { key: 84, expected: 'A7' }, + { key: 85, expected: 'A#7' }, + { key: 86, expected: 'B7' }, + { key: 87, expected: 'C8' } + ]; - testCases.forEach(({ key, expected }) => { - expect(getKeyText(key)).toBe(expected); + testCases.forEach(({ key, expected }) => { + expect(getKeyText(key)).toBe(expected); + }); }); - }); }); describe('isDarkColor', () => { - it('should correctly identify dark colors', () => { + it('should correctly identify dark colors', () => { // Test with default threshold (40) - expect(isDarkColor('#000000')).toBe(true); // black - expect(isDarkColor('#0000FF')).toBe(true); // blue - expect(isDarkColor('#FF0000')).toBe(false); // red - expect(isDarkColor('#00FF00')).toBe(false); // green - expect(isDarkColor('#333333')).toBe(false); // mid gray - expect(isDarkColor('#444444')).toBe(false); // dark gray - expect(isDarkColor('#888888')).toBe(false); // light gray - expect(isDarkColor('#be6b6b')).toBe(false); // light red - expect(isDarkColor('#1964ac')).toBe(false); // first instrument color - expect(isDarkColor('#FFFFFF')).toBe(false); // white - expect(isDarkColor('#F0F0F0')).toBe(false); // light gray - expect(isDarkColor('#be6b6b')).toBe(false); // light red - }); + expect(isDarkColor('#000000')).toBe(true); // black + expect(isDarkColor('#0000FF')).toBe(true); // blue + expect(isDarkColor('#FF0000')).toBe(false); // red + expect(isDarkColor('#00FF00')).toBe(false); // green + expect(isDarkColor('#333333')).toBe(false); // mid gray + expect(isDarkColor('#444444')).toBe(false); // dark gray + expect(isDarkColor('#888888')).toBe(false); // light gray + expect(isDarkColor('#be6b6b')).toBe(false); // light red + expect(isDarkColor('#1964ac')).toBe(false); // first instrument color + expect(isDarkColor('#FFFFFF')).toBe(false); // white + expect(isDarkColor('#F0F0F0')).toBe(false); // light gray + expect(isDarkColor('#be6b6b')).toBe(false); // light red + }); - it('should respect custom threshold', () => { + it('should respect custom threshold', () => { // TODO: Add more test cases for different thresholds - expect(isDarkColor('#888888', 100)).toBe(false); - expect(isDarkColor('#888888', 50)).toBe(false); - expect(isDarkColor('#444444', 50)).toBe(false); - expect(isDarkColor('#444444', 130)).toBe(true); - }); + expect(isDarkColor('#888888', 100)).toBe(false); + expect(isDarkColor('#888888', 50)).toBe(false); + expect(isDarkColor('#444444', 50)).toBe(false); + expect(isDarkColor('#444444', 130)).toBe(true); + }); - it('should handle invalid color strings', () => { + it('should handle invalid color strings', () => { // This is testing the implementation detail that falls back to empty string - expect(isDarkColor('notacolor')).toBe(true); - expect(isDarkColor('')).toBe(true); - }); + expect(isDarkColor('notacolor')).toBe(true); + expect(isDarkColor('')).toBe(true); + }); }); describe('getLuma', () => { - // Note: getLuma isn't exported, but we can test it through isDarkColor - it('should calculate luma correctly', () => { + // Note: getLuma isn't exported, but we can test it through isDarkColor + it('should calculate luma correctly', () => { // These values are calculated from the formula in the code - expect(isDarkColor('#000000')).toBe(true); // 0 - expect(isDarkColor('#FFFFFF')).toBe(false); // 255 - expect(isDarkColor('#FF0000')).toBe(false); // ~54.213 - expect(isDarkColor('#00FF00')).toBe(false); // ~182.376 - expect(isDarkColor('#0000FF')).toBe(true); // ~18.414 - }); + expect(isDarkColor('#000000')).toBe(true); // 0 + expect(isDarkColor('#FFFFFF')).toBe(false); // 255 + expect(isDarkColor('#FF0000')).toBe(false); // ~54.213 + expect(isDarkColor('#00FF00')).toBe(false); // ~182.376 + expect(isDarkColor('#0000FF')).toBe(true); // ~18.414 + }); }); diff --git a/packages/thumbnail/src/utils.ts b/packages/thumbnail/src/utils.ts index 4f2a7c96..38975834 100644 --- a/packages/thumbnail/src/utils.ts +++ b/packages/thumbnail/src/utils.ts @@ -2,94 +2,94 @@ import { createCanvas } from './canvasFactory'; import type { Canvas, Image } from './types'; export const instrumentColors = [ - '#1964ac', - '#3c8e48', - '#be6b6b', - '#bebe19', - '#9d5a98', - '#572b21', - '#bec65c', - '#be19be', - '#52908d', - '#bebebe', - '#1991be', - '#be2328', - '#be5728', - '#19be19', - '#be1957', - '#575757' + '#1964ac', + '#3c8e48', + '#be6b6b', + '#bebe19', + '#9d5a98', + '#572b21', + '#bec65c', + '#be19be', + '#52908d', + '#bebebe', + '#1991be', + '#be2328', + '#be5728', + '#19be19', + '#be1957', + '#575757' ]; const tintedImages: Record = {}; // Function to apply tint to an image export const tintImage = (image: Image, color: string): Canvas => { - if (tintedImages[color]) { - return tintedImages[color]; - } + if (tintedImages[color]) { + return tintedImages[color]; + } - const canvas = createCanvas(image.width, image.height); - const ctx = canvas.getContext('2d'); + const canvas = createCanvas(image.width, image.height); + const ctx = canvas.getContext('2d'); - if (!ctx) { - throw new Error('Could not get canvas context'); - } + if (!ctx) { + throw new Error('Could not get canvas context'); + } - // Fill background with the color - ctx.fillStyle = color; - ctx.fillRect(0, 0, canvas.width, canvas.height); + // Fill background with the color + ctx.fillStyle = color; + ctx.fillRect(0, 0, canvas.width, canvas.height); - // Apply the note block texture to the color - ctx.globalCompositeOperation = 'hard-light'; - ctx.globalAlpha = 0.67; - ctx.drawImage(image, 0, 0); + // Apply the note block texture to the color + ctx.globalCompositeOperation = 'hard-light'; + ctx.globalAlpha = 0.67; + ctx.drawImage(image, 0, 0); - // Reset canvas settings - ctx.globalCompositeOperation = 'source-over'; - ctx.globalAlpha = 1; + // Reset canvas settings + ctx.globalCompositeOperation = 'source-over'; + ctx.globalAlpha = 1; - tintedImages[color] = canvas; + tintedImages[color] = canvas; - return canvas; + return canvas; }; // Function to convert key number to key text export const getKeyText = (key: number): string => { - const octaves = Math.floor((key + 9) / 12); - - const notes = [ - 'C', - 'C#', - 'D', - 'D#', - 'E', - 'F', - 'F#', - 'G', - 'G#', - 'A', - 'A#', - 'B' - ]; - - const note = notes[(key + 9) % 12]; - - return `${note}${octaves}`; + const octaves = Math.floor((key + 9) / 12); + + const notes = [ + 'C', + 'C#', + 'D', + 'D#', + 'E', + 'F', + 'F#', + 'G', + 'G#', + 'A', + 'A#', + 'B' + ]; + + const note = notes[(key + 9) % 12]; + + return `${note}${octaves}`; }; const getLuma = (color: string): number => { - // source: https://stackoverflow.com/a/12043228/9045426 - const c = color?.substring(1) || ''; // strip # - const rgb = parseInt(c, 16); // convert rrggbb to decimal - const r = (rgb >> 16) & 255; // extract red - const g = (rgb >> 8) & 255; // extract green - const b = (rgb >> 0) & 255; // extract blue + // source: https://stackoverflow.com/a/12043228/9045426 + const c = color?.substring(1) || ''; // strip # + const rgb = parseInt(c, 16); // convert rrggbb to decimal + const r = (rgb >> 16) & 255; // extract red + const g = (rgb >> 8) & 255; // extract green + const b = (rgb >> 0) & 255; // extract blue - const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709 + const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709 - return luma; + return luma; }; export const isDarkColor = (color: string, threshold = 40): boolean => { - return getLuma(color) < threshold; + return getLuma(color) < threshold; }; From 58cf32ec7e8fea6986d2af378ec8353d81f3592c Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 11:30:14 -0300 Subject: [PATCH 12/21] refactor: remove unnecessary type assertions in service files and improve context initialization in frontend --- apps/backend/src/auth/auth.service.ts | 2 +- apps/backend/src/song/song.service.spec.ts | 8 ++++---- apps/backend/src/user/user.service.ts | 2 +- .../browse/components/client/context/HomePage.context.tsx | 2 +- .../modules/my-songs/components/client/MySongsTable.tsx | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index cccc2e27..1ae08006 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -215,7 +215,7 @@ export class AuthService { } public async getUserFromToken(token: string): Promise { - const decoded = this.jwtService.decode(token) as TokenPayload; + const decoded = this.jwtService.decode(token); if (!decoded) { return null; diff --git a/apps/backend/src/song/song.service.spec.ts b/apps/backend/src/song/song.service.spec.ts index 40630d28..9fcea05e 100644 --- a/apps/backend/src/song/song.service.spec.ts +++ b/apps/backend/src/song/song.service.spec.ts @@ -455,7 +455,7 @@ describe('SongService', () => { uploader: 'different-user-id' } as any; - jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); + jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity); await expect(service.patchSong(publicId, body, user)).rejects.toThrow( HttpException @@ -536,7 +536,7 @@ describe('SongService', () => { customInstruments: [] } as any; - jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any); + jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity); await expect(service.patchSong(publicId, body, user)).rejects.toThrow( HttpException @@ -638,7 +638,7 @@ describe('SongService', () => { ...songDocument }; - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne); const result = await service.getSong(publicId, user); @@ -655,7 +655,7 @@ describe('SongService', () => { const mockFindOne = null; - jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any); + jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne); await expect(service.getSong(publicId, user)).rejects.toThrow( HttpException diff --git a/apps/backend/src/user/user.service.ts b/apps/backend/src/user/user.service.ts index 46ad1754..3106ae9b 100644 --- a/apps/backend/src/user/user.service.ts +++ b/apps/backend/src/user/user.service.ts @@ -30,7 +30,7 @@ export class UserService { try { return (await this.userModel.findByIdAndUpdate(user._id, user, { new: true // return the updated document - })) as UserDocument; + })); } catch (error) { if (error instanceof Error) { throw error; diff --git a/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx b/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx index 04effa29..b5ed6953 100644 --- a/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx +++ b/apps/frontend/src/modules/browse/components/client/context/HomePage.context.tsx @@ -9,7 +9,7 @@ import { RecentSongsProvider } from './RecentSongs.context'; type HomePageContextType = null; const HomePageContext = createContext( - null as HomePageContextType + null ); export function HomePageProvider({ diff --git a/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx b/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx index 6862be9e..ef6b417f 100644 --- a/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx @@ -49,7 +49,7 @@ const SongRows = ({ const content = !page ? Array(pageSize).fill(null) - : (page.content as SongPreviewDtoType[]); + : (page.content); return ( <> From eb4b5e723db10fae3de5a0f6b19c784adcbfd85a Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 11:38:00 -0300 Subject: [PATCH 13/21] fix: update Swagger setup path from 'api/doc' to 'docs' in test file --- apps/backend/src/lib/initializeSwagger.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/lib/initializeSwagger.spec.ts b/apps/backend/src/lib/initializeSwagger.spec.ts index c119e85b..a944d027 100644 --- a/apps/backend/src/lib/initializeSwagger.spec.ts +++ b/apps/backend/src/lib/initializeSwagger.spec.ts @@ -35,7 +35,7 @@ describe('initializeSwagger', () => { ); expect(SwaggerModule.setup).toHaveBeenCalledWith( - 'api/doc', + 'docs', app, expect.any(Object), { From bd1dd3eefab5c140710eb65160cc7753ac3d620d Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 11:42:00 -0300 Subject: [PATCH 14/21] chore: add @types/unidecode dependency and enhance postinstall script for building and linking workspace packages --- CONTRIBUTING.md | 2 ++ bun.lock | 3 +++ package.json | 5 ++++- packages/song/package.json | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 767ae572..76eba88b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,6 +117,8 @@ To install all dependencies, run in the root of the project: bun install ``` +> **Note**: This will automatically build all workspace packages (database, song, thumbnail, sounds) via the `postinstall` hook to ensure proper workspace dependency linking. + --- ## Running the Project diff --git a/bun.lock b/bun.lock index 3cf4b2b0..03491005 100644 --- a/bun.lock +++ b/bun.lock @@ -193,6 +193,7 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/unidecode": "^1.1.0", "typescript": "^5", }, "peerDependencies": { @@ -1008,6 +1009,8 @@ "@types/tailwindcss": ["@types/tailwindcss@3.1.0", "", { "dependencies": { "tailwindcss": "*" } }, "sha512-JxPzrm609hzvF4nmOI3StLjbBEP3WWQxDDJESqR1nh94h7gyyy3XSl0hn5RBMJ9mPudlLjtaXs5YEBtLw7CnPA=="], + "@types/unidecode": ["@types/unidecode@1.1.0", "", {}, "sha512-NTIsFsTe9WRek39/8DDj7KiQ0nU33DHMrKwNHcD1rKlUvn4N0Rc4Di8q/Xavs8bsDZmBa4MMtQA8+HNgwfxC/A=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="], diff --git a/package.json b/package.json index aec7215d..66341dc1 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ } }, "scripts": { + "postinstall": "bun run build:packages && bun run link:packages", + "build:packages": "cd packages/database && bun run build && cd ../song && bun run build && cd ../thumbnail && bun run build && cd ../sounds && bun run build", + "link:packages": "mkdir -p apps/backend/node_modules/@nbw && cd apps/backend/node_modules/@nbw && ln -sf ../../../../packages/database database && ln -sf ../../../../packages/song song && ln -sf ../../../../packages/thumbnail thumbnail && ln -sf ../../../../packages/sounds sounds", "dev:docker": "docker-compose -f docker-compose-dev.yml up -d && bun run dev && docker-compose down", "build:server": "bun run build:data && cd apps/backend && bun run build", "build:web": "cd ./apps/frontend && bun run build", @@ -46,7 +49,7 @@ "dev:server": "cd ./apps/backend && bun run start:dev", "dev:web": "cd ./apps/frontend && bun run start", "lint": "eslint \"**/*.{ts,tsx}\" --fix", - "test": "cd ./apps/backend && bun test", + "test": "bun run build:packages && cd ./apps/backend && bun test", "cy:open": "bun run test:cy", "test:cy": "cd ./tests && bun run cy:open", "prettier": "prettier --write ." diff --git a/packages/song/package.json b/packages/song/package.json index e7d40a85..18c45321 100644 --- a/packages/song/package.json +++ b/packages/song/package.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/unidecode": "^1.1.0", "typescript": "^5" }, "dependencies": { From 0d07788ac5a3e5b891f559a2fe576213f3fa7bb0 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 13:42:29 -0300 Subject: [PATCH 15/21] chore: update GitHub workflows to include environment variables and streamline linting and testing steps --- .github/workflows/lint.yml | 8 +++++--- .github/workflows/tests.yml | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea8c84c3..da78ec58 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,15 +19,17 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Install bun uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install - - name: Install dependencies # (assuming your project has dependencies) - run: bun install # You can use npm/yarn/pnpm instead if you prefer + - name: Run linter + run: bun run lint - name: Check for changes id: verify-changed-files diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0b6fe977..3ba9bcbe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,16 +13,18 @@ on: jobs: test: runs-on: ubuntu-latest + env: + THUMBNAIL_URL: ${{ vars.THUMBNAIL_URL }} steps: - name: Checkout uses: actions/checkout@v4 - + - name: Install bun uses: oven-sh/setup-bun@v2 - - - name: Install dependencies # (assuming your project has dependencies) - run: bun install # You can use npm/yarn/pnpm instead if you prefer + + - name: Install dependencies + run: bun install - name: Run tests run: bun run test From 0704c54fdeaa30d3910c6af573ef5be35d16c6b3 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 13:51:13 -0300 Subject: [PATCH 16/21] refactor: reorganize thumbnail constants and update song preview DTO to use specific visibility type --- packages/configs/index.ts | 2 +- packages/configs/src/song.ts | 19 ------------------- packages/configs/src/thumbnail.ts | 18 ++++++++++++++++++ .../database/src/song/dto/SongPreview.dto.ts | 4 +++- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/configs/index.ts b/packages/configs/index.ts index 3f2f3f8c..059cc061 100644 --- a/packages/configs/index.ts +++ b/packages/configs/index.ts @@ -1 +1 @@ -console.log('Hello via Bun!'); +// Entry point for the configs package diff --git a/packages/configs/src/song.ts b/packages/configs/src/song.ts index 7c411774..1d9dc9a3 100644 --- a/packages/configs/src/song.ts +++ b/packages/configs/src/song.ts @@ -1,22 +1,3 @@ -import { BG_COLORS } from './colors'; - -export const THUMBNAIL_CONSTANTS = { - zoomLevel: { - min : 1, - max : 5, - default: 3 - }, - startTick: { - default: 0 - }, - startLayer: { - default: 0 - }, - backgroundColor: { - default: BG_COLORS.gray.dark - } -} as const; - export const MIMETYPE_NBS = 'application/octet-stream' as const; export const UPLOAD_CONSTANTS = { diff --git a/packages/configs/src/thumbnail.ts b/packages/configs/src/thumbnail.ts index e69de29b..2d0edb1d 100644 --- a/packages/configs/src/thumbnail.ts +++ b/packages/configs/src/thumbnail.ts @@ -0,0 +1,18 @@ +import { BG_COLORS } from './colors'; + +export const THUMBNAIL_CONSTANTS = { + zoomLevel: { + min : 1, + max : 5, + default: 3 + }, + startTick: { + default: 0 + }, + startLayer: { + default: 0 + }, + backgroundColor: { + default: BG_COLORS.gray.dark + } +} as const; diff --git a/packages/database/src/song/dto/SongPreview.dto.ts b/packages/database/src/song/dto/SongPreview.dto.ts index c98bfeb5..0cc1876a 100644 --- a/packages/database/src/song/dto/SongPreview.dto.ts +++ b/packages/database/src/song/dto/SongPreview.dto.ts @@ -2,6 +2,8 @@ import { IsNotEmpty, IsString, IsUrl, MaxLength } from 'class-validator'; import type { SongWithUser } from '../entity/song.entity'; +import type { VisibilityType } from './types'; + type SongPreviewUploader = { username : string; profileImage: string; @@ -50,7 +52,7 @@ export class SongPreviewDto { @IsNotEmpty() @IsString() - visibility: string; + visibility: VisibilityType; constructor(partial: Partial) { Object.assign(this, partial); From 7457253bb83663b7b98c74b1a7d9e527113ab5c3 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Wed, 24 Sep 2025 13:57:57 -0300 Subject: [PATCH 17/21] chore: add configs package to package.json and update build and link scripts to include configs --- package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 66341dc1..6fafe3c3 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,10 @@ "import": "./apps/frontend/dist/index.js", "types": "./apps/frontend/dist/index.d.ts" }, + "./config": { + "import": "./packages/configs/dist/index.js", + "types": "./packages/configs/dist/index.d.ts" + }, "./database": { "import": "./packages/database/dist/index.js", "types": "./packages/database/dist/index.d.ts" @@ -37,8 +41,8 @@ }, "scripts": { "postinstall": "bun run build:packages && bun run link:packages", - "build:packages": "cd packages/database && bun run build && cd ../song && bun run build && cd ../thumbnail && bun run build && cd ../sounds && bun run build", - "link:packages": "mkdir -p apps/backend/node_modules/@nbw && cd apps/backend/node_modules/@nbw && ln -sf ../../../../packages/database database && ln -sf ../../../../packages/song song && ln -sf ../../../../packages/thumbnail thumbnail && ln -sf ../../../../packages/sounds sounds", + "build:packages": "cd packages/configs && bun run build && cd ../database && bun run build && cd ../song && bun run build && cd ../thumbnail && bun run build && cd ../sounds && bun run build", + "link:packages": "mkdir -p apps/backend/node_modules/@nbw && cd apps/backend/node_modules/@nbw && ln -sf ../../../../packages/configs config && ln -sf ../../../../packages/database database && ln -sf ../../../../packages/song song && ln -sf ../../../../packages/thumbnail thumbnail && ln -sf ../../../../packages/sounds sounds", "dev:docker": "docker-compose -f docker-compose-dev.yml up -d && bun run dev && docker-compose down", "build:server": "bun run build:data && cd apps/backend && bun run build", "build:web": "cd ./apps/frontend && bun run build", From 98924e1fa68c9f28e7f27edfcbd626bf9fec12fb Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Thu, 25 Sep 2025 09:48:09 -0300 Subject: [PATCH 18/21] chore: update ESLint configuration to include React plugin and enhance rules for JSX files --- .../shared/components/client/Command.tsx | 2 +- bun.lock | 39 ++++++++++++------- eslint.config.js | 19 +++++++++ package.json | 1 + 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/apps/frontend/src/modules/shared/components/client/Command.tsx b/apps/frontend/src/modules/shared/components/client/Command.tsx index b170787c..8154ace1 100644 --- a/apps/frontend/src/modules/shared/components/client/Command.tsx +++ b/apps/frontend/src/modules/shared/components/client/Command.tsx @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Command as CommandPrimitive } from 'cmdk'; import * as React from 'react'; -import { cn } from '../../../../lib/tailwind.utils'; +import { cn } from '@web/lib/tailwind.utils'; const Command = React.forwardRef< React.ElementRef, diff --git a/bun.lock b/bun.lock index 03491005..1b4f9ada 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-mdx": "^3.6.2", "eslint-plugin-prettier": "^4.2.5", + "eslint-plugin-react": "^7.37.5", "globals": "^16.4.0", "prettier": "^2.8.8", "typescript-eslint": "^8.44.1", @@ -1535,7 +1536,7 @@ "eslint-plugin-prettier": ["eslint-plugin-prettier@4.2.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", "prettier": ">=2.0.0" } }, "sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg=="], - "eslint-plugin-react": ["eslint-plugin-react@7.37.4", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ=="], + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw=="], @@ -2345,7 +2346,7 @@ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - "object.entries": ["object.entries@1.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ=="], + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], @@ -2607,7 +2608,7 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], @@ -3283,8 +3284,12 @@ "eslint-config-next/eslint-plugin-import": ["eslint-plugin-import@2.31.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A=="], + "eslint-config-next/eslint-plugin-react": ["eslint-plugin-react@7.37.4", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-import-resolver-node/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "eslint-mdx/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -3297,10 +3302,6 @@ "eslint-plugin-markdown/mdast-util-from-markdown": ["mdast-util-from-markdown@0.8.5", "", { "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-string": "^2.0.0", "micromark": "~2.11.0", "parse-entities": "^2.0.0", "unist-util-stringify-position": "^2.0.0" } }, "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ=="], - "eslint-plugin-react/array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], - - "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], - "espree/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "estree-util-to-js/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -3393,6 +3394,8 @@ "jest-mock/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "jest-resolve/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "jest-runner/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], "jest-runner/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], @@ -3829,6 +3832,8 @@ "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "postcss-import/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -3837,6 +3842,8 @@ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "pug-filters/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "read-package-json-fast/json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], @@ -3899,6 +3906,8 @@ "tailwindcss/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "tailwindcss/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "terser-webpack-plugin/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], @@ -4095,6 +4104,10 @@ "eslint-config-next/eslint-plugin-import/tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + "eslint-config-next/eslint-plugin-react/array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + + "eslint-config-next/eslint-plugin-react/object.entries": ["object.entries@1.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ=="], + "eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], "eslint-plugin-jsx-a11y/array-includes/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], @@ -4109,8 +4122,6 @@ "eslint-plugin-markdown/mdast-util-from-markdown/unist-util-stringify-position": ["unist-util-stringify-position@2.0.3", "", { "dependencies": { "@types/unist": "^2.0.2" } }, "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g=="], - "eslint-plugin-react/array-includes/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], @@ -4321,6 +4332,8 @@ "eslint-config-next/eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + "eslint-config-next/eslint-plugin-react/array-includes/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + "eslint-plugin-jsx-a11y/array-includes/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], "eslint-plugin-jsx-a11y/array-includes/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], @@ -4341,10 +4354,6 @@ "eslint-plugin-markdown/mdast-util-from-markdown/unist-util-stringify-position/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "eslint-plugin-react/array-includes/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - - "eslint-plugin-react/array-includes/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], - "jsx-ast-utils/array-includes/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], "jsx-ast-utils/array-includes/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], @@ -4429,6 +4438,10 @@ "eslint-config-next/eslint-plugin-import/array.prototype.findlastindex/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + "eslint-config-next/eslint-plugin-react/array-includes/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "eslint-config-next/eslint-plugin-react/array-includes/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + "eslint-plugin-markdown/mdast-util-from-markdown/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="], "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/eslint.config.js b/eslint.config.js index b726cb34..4d9df994 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,7 @@ import tseslint from 'typescript-eslint'; import globals from 'globals'; import importPlugin from 'eslint-plugin-import'; import stylistic from '@stylistic/eslint-plugin'; +import react from 'eslint-plugin-react'; export default tseslint.config( // Global ignores. @@ -121,6 +122,24 @@ export default tseslint.config( }], }, }, + + // React specific configuration + { + files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], + plugins: { + react, + }, + rules: { + ...react.configs.recommended.rules, + 'react/react-in-jsx-scope': 'off', // Not needed with modern React + 'react/no-unknown-property': ['error', { ignore: ['custom-prop', 'cmdk-input-wrapper', 'cmdk-group-heading'] }] + }, + settings: { + react: { + version: 'detect', // Automatically detect the React version + }, + }, + }, // Override for JSX files { files: ['**/*.jsx', '**/*.tsx'], diff --git a/package.json b/package.json index 6fafe3c3..c5975eb8 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-mdx": "^3.6.2", "eslint-plugin-prettier": "^4.2.5", + "eslint-plugin-react": "^7.37.5", "globals": "^16.4.0", "prettier": "^2.8.8", "typescript-eslint": "^8.44.1" From 98fe383a181c1cce6206a00cfd8e4d62ab8b2450 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Thu, 25 Sep 2025 22:49:37 -0300 Subject: [PATCH 19/21] chore: streamline package build and start scripts in package.json, remove unused bun.lock files from database, song, sounds, and thumbnail packages --- package.json | 26 +++++++++++++------------- packages/database/bun.lock | 29 ----------------------------- packages/song/bun.lock | 29 ----------------------------- packages/sounds/bun.lock | 29 ----------------------------- packages/thumbnail/bun.lock | 29 ----------------------------- 5 files changed, 13 insertions(+), 129 deletions(-) delete mode 100644 packages/database/bun.lock delete mode 100644 packages/song/bun.lock delete mode 100644 packages/sounds/bun.lock delete mode 100644 packages/thumbnail/bun.lock diff --git a/package.json b/package.json index c5975eb8..937a1df9 100644 --- a/package.json +++ b/package.json @@ -40,22 +40,22 @@ } }, "scripts": { - "postinstall": "bun run build:packages && bun run link:packages", - "build:packages": "cd packages/configs && bun run build && cd ../database && bun run build && cd ../song && bun run build && cd ../thumbnail && bun run build && cd ../sounds && bun run build", - "link:packages": "mkdir -p apps/backend/node_modules/@nbw && cd apps/backend/node_modules/@nbw && ln -sf ../../../../packages/configs config && ln -sf ../../../../packages/database database && ln -sf ../../../../packages/song song && ln -sf ../../../../packages/thumbnail thumbnail && ln -sf ../../../../packages/sounds sounds", + "postinstall": "bun run build:packages", + "build:packages": "bun run --filter '@nbw/config' build && bun run --filter '@nbw/sounds' build && bun run --filter '@nbw/database' build && bun run --filter '@nbw/song' build && bun run --filter '@nbw/thumbnail' build", + "build:packages:fast": "bun run --filter '@nbw/config' --filter '@nbw/sounds' --parallel build && bun run --filter '@nbw/database' build && bun run --filter '@nbw/song' build && bun run --filter '@nbw/thumbnail' build", "dev:docker": "docker-compose -f docker-compose-dev.yml up -d && bun run dev && docker-compose down", - "build:server": "bun run build:data && cd apps/backend && bun run build", - "build:web": "cd ./apps/frontend && bun run build", - "start:server": "cd ./apps/backend && bun run start", - "start:server:prod": "cd ./apps/backend && bun run start", - "start:web:prod": "cd ./apps/frontend && bun run start", - "dev": "concurrently --success first -n \"server,web\" --prefix-colors \"cyan,magenta\" --prefix \"{name} {time}\" \"cd ./apps/backend && bun run start:dev\" \"cd ./apps/frontend && bun run start\"", - "dev:server": "cd ./apps/backend && bun run start:dev", - "dev:web": "cd ./apps/frontend && bun run start", + "build:server": "bun run build:data && bun run --filter '@nbw/backend' build", + "build:web": "bun run --filter '@nbw/frontend' build", + "start:server": "bun run --filter '@nbw/backend' start", + "start:server:prod": "bun run --filter '@nbw/backend' start", + "start:web:prod": "bun run --filter '@nbw/frontend' start", + "dev": "concurrently --success first -n \"server,web\" --prefix-colors \"cyan,magenta\" --prefix \"{name} {time}\" \"bun run --filter '@nbw/backend' start:dev\" \"bun run --filter '@nbw/frontend' start\"", + "dev:server": "bun run --filter '@nbw/backend' start:dev", + "dev:web": "bun run --filter '@nbw/frontend' start", "lint": "eslint \"**/*.{ts,tsx}\" --fix", - "test": "bun run build:packages && cd ./apps/backend && bun test", + "test": "bun run build:packages && bun run --filter '@nbw/backend' test", "cy:open": "bun run test:cy", - "test:cy": "cd ./tests && bun run cy:open", + "test:cy": "bun run --filter 'tests' cy:open", "prettier": "prettier --write ." }, "keywords": [], diff --git a/packages/database/bun.lock b/packages/database/bun.lock deleted file mode 100644 index bbbc828b..00000000 --- a/packages/database/bun.lock +++ /dev/null @@ -1,29 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "database", - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], - - "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], - - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], - - "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - } -} diff --git a/packages/song/bun.lock b/packages/song/bun.lock deleted file mode 100644 index d8a1b172..00000000 --- a/packages/song/bun.lock +++ /dev/null @@ -1,29 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "song", - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], - - "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], - - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], - - "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - } -} diff --git a/packages/sounds/bun.lock b/packages/sounds/bun.lock deleted file mode 100644 index 591943e0..00000000 --- a/packages/sounds/bun.lock +++ /dev/null @@ -1,29 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "sounds", - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], - - "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], - - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], - - "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - } -} diff --git a/packages/thumbnail/bun.lock b/packages/thumbnail/bun.lock deleted file mode 100644 index 567e0c32..00000000 --- a/packages/thumbnail/bun.lock +++ /dev/null @@ -1,29 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "thumbnail", - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], - - "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], - - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], - - "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - } -} From 54ee6d5eab35465b44d97f4ab38d3a89f8b0fa64 Mon Sep 17 00:00:00 2001 From: Bentroen <29354120+Bentroen@users.noreply.github.com> Date: Thu, 25 Sep 2025 22:59:17 -0300 Subject: [PATCH 20/21] chore: remove workbench color customizations from VS Code settings file --- .vscode/settings.json | 40 +++++++++------------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 640488af..c1877421 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,32 +1,10 @@ { - "editor.formatOnSave": true, - "eslint.validate": [ - "typescript" - ], - "eslint.run": "onType", - "eslint.format.enable": true, - "mdx.server.enable": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit" - }, - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#fac977", - "activityBar.background": "#fac977", - "activityBar.foreground": "#15202b", - "activityBar.inactiveForeground": "#15202b99", - "activityBarBadge.background": "#069a62", - "activityBarBadge.foreground": "#e7e7e7", - "commandCenter.border": "#15202b99", - "sash.hoverBorder": "#fac977", - "statusBar.background": "#f8b646", - "statusBar.foreground": "#15202b", - "statusBarItem.hoverBackground": "#f6a315", - "statusBarItem.remoteBackground": "#f8b646", - "statusBarItem.remoteForeground": "#15202b", - "titleBar.activeBackground": "#f8b646", - "titleBar.activeForeground": "#15202b", - "titleBar.inactiveBackground": "#f8b64699", - "titleBar.inactiveForeground": "#15202b99" - }, - "peacock.color": "#f8b646" -} \ No newline at end of file + "editor.formatOnSave": true, + "eslint.validate": ["typescript"], + "eslint.run": "onType", + "eslint.format.enable": true, + "mdx.server.enable": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + } +} From 7dee3df090a318382979b2de1d0d154a79029b2d Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Fri, 26 Sep 2025 08:33:52 -0300 Subject: [PATCH 21/21] chore: remove @stylistic/eslint-plugin from devDependencies and update ESLint configuration for improved rule management --- bun.lock | 35 +++++++---------- eslint.config.js | 98 +++++++++--------------------------------------- package.json | 1 - 3 files changed, 31 insertions(+), 103 deletions(-) diff --git a/bun.lock b/bun.lock index 1b4f9ada..a5ae02da 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ }, "devDependencies": { "@eslint/js": "^9.35.0", - "@stylistic/eslint-plugin": "^5.4.0", "@types/bun": "^1.2.10", "@types/node": "^20.3.1", "@typescript-eslint/eslint-plugin": "^8.43.0", @@ -868,8 +867,6 @@ "@smithy/util-waiter": ["@smithy/util-waiter@3.2.0", "", { "dependencies": { "@smithy/abort-controller": "^3.1.9", "@smithy/types": "^3.7.2", "tslib": "^2.6.2" } }, "sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg=="], - "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.44.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-UG8hdElzuBDzIbjG1QDwnYH0MQ73YLXDFHgZzB4Zh/YJfnw8XNsloVtytqzx0I2Qky9THSdpTmi8Vjn/pf/Lew=="], - "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], "@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], @@ -1038,7 +1035,7 @@ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "@typescript-eslint/typescript-estree": "8.43.0", "@typescript-eslint/utils": "8.43.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.43.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.43.0", "@typescript-eslint/tsconfig-utils": "8.43.0", "@typescript-eslint/types": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw=="], @@ -2442,7 +2439,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.1", "", {}, "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg=="], "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -3042,8 +3039,6 @@ "@angular-devkit/core/jsonc-parser": ["jsonc-parser@3.2.1", "", {}, "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA=="], - "@angular-devkit/core/picomatch": ["picomatch@4.0.1", "", {}, "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg=="], - "@angular-devkit/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="], "@angular-devkit/core/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -3196,24 +3191,10 @@ "@types/whatwg-url/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - - "@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - - "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - - "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - - "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.43.0", "", {}, "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw=="], - "ajv-formats/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], "alce/esprima": ["esprima@1.2.5", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ=="], @@ -4250,12 +4231,16 @@ "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1" } }, "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg=="], + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.44.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.44.1", "@typescript-eslint/types": "^8.44.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA=="], "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.44.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ=="], + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], "typescript-eslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -4264,6 +4249,8 @@ "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "@typescript-eslint/visitor-keys": "8.44.1" } }, "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], + "unified-engine/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "unified-engine/concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -4398,6 +4385,12 @@ "terser-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], + + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.44.1", "", {}, "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ=="], + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.44.1", "", { "dependencies": { "@typescript-eslint/types": "8.44.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw=="], diff --git a/eslint.config.js b/eslint.config.js index 4d9df994..41ca948a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,11 +2,10 @@ import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import globals from 'globals'; import importPlugin from 'eslint-plugin-import'; -import stylistic from '@stylistic/eslint-plugin'; import react from 'eslint-plugin-react'; export default tseslint.config( - // Global ignores. + // Global ignores (no changes here) { ignores: [ '**/node_modules/**', @@ -21,109 +20,56 @@ export default tseslint.config( ], }, - // Apply base recommended configurations. + // Base recommended configurations (no changes here) js.configs.recommended, ...tseslint.configs.recommended, - // A single, unified object for all custom rules and plugin configurations. + // Main configuration object { languageOptions: { - globals: { - ...globals.node, - ...globals.es2021, // A modern equivalent of the 'es6' env - }, + globals: { ...globals.node, ...globals.es2021, ...globals.bun }, }, plugins: { 'import': importPlugin, - '@stylistic': stylistic, }, settings: { 'import/resolver': { typescript: { - // Point to all tsconfig.json files in your workspaces - project: [ - 'apps/*/tsconfig.json', - 'packages/*/tsconfig.json', - './tsconfig.json', // Also include the root tsconfig as a fallback - ], + project: ['apps/*/tsconfig.json', 'packages/*/tsconfig.json', './tsconfig.json'], }, node: true, }, - // Allow Bun built-in modules 'import/core-modules': ['bun:test', 'bun:sqlite', 'bun'], }, rules: { - // Manually include rules from the import plugin's recommended configs. ...importPlugin.configs.recommended.rules, ...importPlugin.configs.typescript.rules, - // Your custom rules from the original file. + // Core and TypeScript rules (keep these) 'no-console': 'warn', - 'max-len': ['error', { - code: 1024, - ignoreComments: true, - ignoreUrls: true, - ignoreStrings: true, - ignoreTemplateLiterals: true, - }], '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-require-imports': 'warn', '@typescript-eslint/ban-ts-comment': 'warn', '@typescript-eslint/no-unused-vars': [ 'warn', - { - vars: 'all', - varsIgnorePattern: '^_', - args: 'after-used', - argsIgnorePattern: '^_', - }, + { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }, ], + 'lines-between-class-members': ['warn', 'always', { exceptAfterSingleLine: true }], // 👈 ADD THIS RULE + + // Import rules (keep these) 'import/order': ['error', { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - 'object', - 'unknown', - ], - 'pathGroups': [{ - pattern: '@/**', - group: 'internal', - }], + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'unknown'], + 'pathGroups': [{ pattern: '@/**', group: 'internal' }], pathGroupsExcludedImportTypes: ['builtin'], 'newlines-between': 'always', - alphabetize: { - order: 'asc', - caseInsensitive: true, - }, + alphabetize: { order: 'asc', caseInsensitive: true }, }], 'import/newline-after-import': 'error', 'import/no-duplicates': 'error', - - // Spacing rules for consistency - '@stylistic/indent': ['error', 4], // Set default indentation to 4 spaces. - '@stylistic/space-infix-ops': 'error', // Enforces spaces around operators like +, =, etc. - '@stylistic/keyword-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around keywords like if, else. - '@stylistic/arrow-spacing': ['error', { 'before': true, 'after': true }], // Enforces spaces around arrow in arrow functions. - '@stylistic/space-before-blocks': 'error', // Enforces a space before opening curly braces. - '@stylistic/object-curly-spacing': ['error', 'always'], // Enforces spaces inside curly braces: { foo } not {foo}. - '@stylistic/comma-spacing': ['error', { 'before': false, 'after': true }], // Enforces space after a comma, not before. - '@stylistic/space-before-function-paren': ['error', { 'anonymous': 'always', 'named': 'never', 'asyncArrow': 'always' }], // Controls space before function parentheses. - '@stylistic/comma-dangle': ['error', 'never'], // Disallows trailing commas - '@stylistic/key-spacing': ['error', { - align: { - beforeColon: false, - afterColon: true, - on: 'colon', - }, - }], }, }, - // React specific configuration + // React specific configuration (no changes here) { files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], plugins: { @@ -131,21 +77,11 @@ export default tseslint.config( }, rules: { ...react.configs.recommended.rules, - 'react/react-in-jsx-scope': 'off', // Not needed with modern React + 'react/react-in-jsx-scope': 'off', 'react/no-unknown-property': ['error', { ignore: ['custom-prop', 'cmdk-input-wrapper', 'cmdk-group-heading'] }] }, settings: { - react: { - version: 'detect', // Automatically detect the React version - }, - }, - }, - // Override for JSX files - { - files: ['**/*.jsx', '**/*.tsx'], - rules: { - '@stylistic/indent': ['error', 2], // Set indentation to 2 spaces for JSX files. + react: { version: 'detect' }, }, }, -); - +); \ No newline at end of file diff --git a/package.json b/package.json index 937a1df9..fb01ffa9 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ ], "devDependencies": { "@eslint/js": "^9.35.0", - "@stylistic/eslint-plugin": "^5.4.0", "@types/bun": "^1.2.10", "@types/node": "^20.3.1", "@typescript-eslint/eslint-plugin": "^8.43.0",