diff --git a/apps/frontend/package.json b/apps/frontend/package.json index f6186544..09e6a894 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --webpack", "build": "next build --webpack", "start": "next start", "lint": "eslint \"src/**/*.{ts,tsx}\" --fix", diff --git a/bun.lock b/bun.lock index b2cdbafe..6624ec51 100644 --- a/bun.lock +++ b/bun.lock @@ -187,11 +187,10 @@ "name": "@nbw/database", "dependencies": { "@nbw/config": "workspace:*", + "@nbw/validation": "workspace:*", "@nestjs/common": "^11.1.9", "@nestjs/mongoose": "^10.1.0", "@nestjs/swagger": "^11.2.3", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.3", "mongoose": "^9.0.1", }, "devDependencies": { @@ -206,7 +205,6 @@ "name": "@nbw/song", "dependencies": { "@encode42/nbs.js": "^5.0.2", - "@nbw/database": "workspace:*", "@timohausmann/quadtree-ts": "^2.2.2", "jszip": "^3.10.1", "unidecode": "^1.1.0", @@ -248,6 +246,24 @@ "typescript": "^5", }, }, + "packages/validation": { + "name": "@nbw/validation", + "dependencies": { + "@nbw/config": "workspace:*", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", + "mongoose": "^9.0.1", + "zod": "^4.1.13", + "zod-validation-error": "^5.0.0", + }, + "devDependencies": { + "@types/bun": "^1.3.4", + "typescript": "^5.9.3", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, }, "trustedDependencies": [ "@nestjs/core", @@ -693,6 +709,8 @@ "@nbw/thumbnail": ["@nbw/thumbnail@workspace:packages/thumbnail"], + "@nbw/validation": ["@nbw/validation@workspace:packages/validation"], + "@nestjs-modules/mailer": ["@nestjs-modules/mailer@2.0.2", "", { "dependencies": { "@css-inline/css-inline": "0.14.1", "glob": "10.3.12" }, "optionalDependencies": { "@types/ejs": "^3.1.5", "@types/mjml": "^4.7.4", "@types/pug": "^2.0.10", "ejs": "^3.1.10", "handlebars": "^4.7.8", "liquidjs": "^10.11.1", "mjml": "^4.15.3", "preview-email": "^3.0.19", "pug": "^3.0.2" }, "peerDependencies": { "@nestjs/common": ">=7.0.9", "@nestjs/core": ">=7.0.9", "nodemailer": ">=6.4.6" } }, "sha512-+z4mADQasg0H1ZaGu4zZTuKv2pu+XdErqx99PLFPzCDNTN/q9U59WPgkxVaHnsvKHNopLj5Xap7G4ZpptduoYw=="], "@nestjs/cli": ["@nestjs/cli@11.0.14", "", { "dependencies": { "@angular-devkit/core": "19.2.19", "@angular-devkit/schematics": "19.2.19", "@angular-devkit/schematics-cli": "19.2.19", "@inquirer/prompts": "7.10.1", "@nestjs/schematics": "^11.0.1", "ansis": "4.2.0", "chokidar": "4.0.3", "cli-table3": "0.6.5", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "9.1.0", "glob": "13.0.0", "node-emoji": "1.11.0", "ora": "5.4.1", "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.2.0", "typescript": "5.9.3", "webpack": "5.103.0", "webpack-node-externals": "3.0.0" }, "peerDependencies": { "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", "@swc/core": "^1.3.62" }, "optionalPeers": ["@swc/cli", "@swc/core"], "bin": { "nest": "bin/nest.js" } }, "sha512-YwP03zb5VETTwelXU+AIzMVbEZKk/uxJL+z9pw0mdG9ogAtqZ6/mpmIM4nEq/NU8D0a7CBRLcMYUmWW/55pfqw=="], @@ -3331,6 +3349,8 @@ "@nbw/thumbnail/jest": ["jest@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw=="], + "@nbw/validation/@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@nestjs-modules/mailer/glob": ["glob@10.3.12", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.6", "minimatch": "^9.0.1", "minipass": "^7.0.4", "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg=="], "@nestjs/cli/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], @@ -4095,6 +4115,8 @@ "@nbw/thumbnail/jest/jest-cli": ["jest-cli@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "chalk": "^4.0.0", "create-jest": "^29.7.0", "exit": "^0.1.2", "import-local": "^3.0.2", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg=="], + "@nbw/validation/@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "@nestjs-modules/mailer/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@nestjs-modules/mailer/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], @@ -4433,6 +4455,8 @@ "@nbw/thumbnail/jest/jest-cli/jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + "@nbw/validation/@types/bun/bun-types/@types/node": ["@types/node@24.10.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA=="], + "@nestjs-modules/mailer/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@nestjs-modules/mailer/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -4689,6 +4713,8 @@ "@nbw/thumbnail/jest/jest-cli/jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "@nbw/validation/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@nestjs/mongoose/mongoose/mongodb/mongodb-connection-string-url/@types/whatwg-url": ["@types/whatwg-url@8.2.2", "", { "dependencies": { "@types/node": "*", "@types/webidl-conversions": "*" } }, "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA=="], "@nestjs/mongoose/mongoose/mongodb/mongodb-connection-string-url/whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], diff --git a/packages/database/package.json b/packages/database/package.json index 61e5d06f..297e963d 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -9,10 +9,6 @@ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" - }, - "./types": { - "import": "./dist/song/dto/types.js", - "types": "./dist/song/dto/types.d.ts" } }, "scripts": { @@ -32,10 +28,9 @@ "@nestjs/common": "^11.1.9", "@nestjs/mongoose": "^10.1.0", "@nestjs/swagger": "^11.2.3", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.3", "mongoose": "^9.0.1", - "@nbw/config": "workspace:*" + "@nbw/config": "workspace:*", + "@nbw/validation": "workspace:*" }, "peerDependencies": { "typescript": "^5" diff --git a/packages/database/src/common/dto/Page.dto.ts b/packages/database/src/common/dto/Page.dto.ts deleted file mode 100644 index 32a6a469..00000000 --- a/packages/database/src/common/dto/Page.dto.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - IsArray, - IsBoolean, - IsNotEmpty, - IsNumber, - IsOptional, - IsString, - ValidateNested, -} from 'class-validator'; - -export class PageDto { - @IsNotEmpty() - @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, - }) - @ApiProperty({ example: 150, description: 'Total number of items available' }) - total: number; - - @IsNotEmpty() - @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, - }) - @ApiProperty({ example: 1, description: 'Current page number' }) - page: number; - - @IsNotEmpty() - @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, - }) - @ApiProperty({ example: 20, description: 'Number of items per page' }) - limit: number; - - @IsOptional() - @IsString() - @ApiProperty({ example: 'createdAt', description: 'Field used for sorting' }) - sort?: string; - - @IsNotEmpty() - @IsBoolean() - @ApiProperty({ - example: false, - description: 'Sort order: true for ascending, false for descending', - }) - order: boolean; - - @IsNotEmpty() - @IsArray() - @ValidateNested({ each: true }) - @ApiProperty({ - description: 'Array of items for the current page', - isArray: true, - }) - content: T[]; - - constructor(partial: Partial>) { - Object.assign(this, partial); - } -} diff --git a/packages/database/src/common/dto/PageQuery.dto.ts b/packages/database/src/common/dto/PageQuery.dto.ts deleted file mode 100644 index a0f0025c..00000000 --- a/packages/database/src/common/dto/PageQuery.dto.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsBoolean, - IsEnum, - IsNotEmpty, - IsNumber, - IsOptional, - IsString, - Max, - Min, -} from 'class-validator'; - -import { TIMESPANS } from '@nbw/config'; - -import type { TimespanType } from '../../song/dto/types'; - -export class PageQueryDTO { - @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; - - @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: - 'Sorts the results in ascending order if true; in descending order if false.', - required: false, - }) - order?: boolean = false; - - @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); - } -} diff --git a/packages/database/src/common/dto/types.ts b/packages/database/src/common/dto/types.ts deleted file mode 100644 index 2b92e50b..00000000 --- a/packages/database/src/common/dto/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { PageQueryDTO } from './PageQuery.dto'; - -export type PageQueryDTOType = InstanceType; diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index a9e1cc0a..166095be 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -1,26 +1,2 @@ -export * from './common/dto/Page.dto'; -export * from './common/dto/PageQuery.dto'; -export * from './common/dto/types'; - -export * from './song/dto/CustomInstrumentData.dto'; -export * from './song/dto/FeaturedSongsDto.dto'; -export * from './song/dto/SongListQuery.dto'; -export * from './song/dto/SongPage.dto'; -export * from './song/dto/SongPreview.dto'; -export * from './song/dto/SongStats'; -export * from './song/dto/SongView.dto'; -export * from './song/dto/ThumbnailData.dto'; -export * from './song/dto/UploadSongDto.dto'; -export * from './song/dto/UploadSongResponseDto.dto'; -export * from './song/dto/types'; -export * from './song/entity/song.entity'; - -export * from './user/dto/CreateUser.dto'; -export * from './user/dto/GetUser.dto'; -export * from './user/dto/Login.dto copy'; -export * from './user/dto/LoginWithEmail.dto'; -export * from './user/dto/NewEmailUser.dto'; -export * from './user/dto/SingleUsePass.dto'; -export * from './user/dto/UpdateUsername.dto'; -export * from './user/dto/user.dto'; -export * from './user/entity/user.entity'; +export * from './song/song.entity'; +export * from './user/user.entity'; diff --git a/packages/database/src/index.web.ts b/packages/database/src/index.web.ts deleted file mode 100644 index 3a9cde9e..00000000 --- a/packages/database/src/index.web.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Web-specific exports (excludes Mongoose entities) -export * from './common/dto/Page.dto'; -export * from './common/dto/PageQuery.dto'; -export * from './common/dto/types'; - -export * from './song/dto/CustomInstrumentData.dto'; -export * from './song/dto/FeaturedSongsDto.dto'; -export * from './song/dto/SongListQuery.dto'; -export * from './song/dto/SongPage.dto'; -export * from './song/dto/SongPreview.dto'; -export * from './song/dto/SongStats'; -export * from './song/dto/SongView.dto'; -export * from './song/dto/ThumbnailData.dto'; -export * from './song/dto/UploadSongDto.dto'; -export * from './song/dto/UploadSongResponseDto.dto'; -export * from './song/dto/types'; -// Note: song.entity is excluded for web builds - -export * from './user/dto/CreateUser.dto'; -export * from './user/dto/GetUser.dto'; -export * from './user/dto/Login.dto copy'; -export * from './user/dto/LoginWithEmail.dto'; -export * from './user/dto/NewEmailUser.dto'; -export * from './user/dto/SingleUsePass.dto'; -export * from './user/dto/UpdateUsername.dto'; -export * from './user/dto/user.dto'; -// Note: user.entity is excluded for web builds diff --git a/packages/database/src/song/dto/CustomInstrumentData.dto.ts b/packages/database/src/song/dto/CustomInstrumentData.dto.ts deleted file mode 100644 index 8cb3e835..00000000 --- a/packages/database/src/song/dto/CustomInstrumentData.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsNotEmpty } from 'class-validator'; - -export class CustomInstrumentData { - @IsNotEmpty() - sound: string[]; -} diff --git a/packages/database/src/song/dto/FeaturedSongsDto.dto.ts b/packages/database/src/song/dto/FeaturedSongsDto.dto.ts deleted file mode 100644 index 65d6eff7..00000000 --- a/packages/database/src/song/dto/FeaturedSongsDto.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SongPreviewDto } from './SongPreview.dto'; - -export class FeaturedSongsDto { - hour: SongPreviewDto[]; - day: SongPreviewDto[]; - week: SongPreviewDto[]; - month: SongPreviewDto[]; - year: SongPreviewDto[]; - all: SongPreviewDto[]; - - public static create(): FeaturedSongsDto { - return { - hour: [], - day: [], - week: [], - month: [], - year: [], - all: [], - }; - } -} diff --git a/packages/database/src/song/dto/SongListQuery.dto.ts b/packages/database/src/song/dto/SongListQuery.dto.ts deleted file mode 100644 index ae7f72bf..00000000 --- a/packages/database/src/song/dto/SongListQuery.dto.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - IsEnum, - IsNumber, - IsOptional, - IsString, - Max, - Min, -} from 'class-validator'; - -export enum SongSortType { - RECENT = 'recent', - RANDOM = 'random', - PLAY_COUNT = 'playCount', - TITLE = 'title', - DURATION = 'duration', - NOTE_COUNT = 'noteCount', -} - -export enum SongOrderType { - ASC = 'asc', - DESC = 'desc', -} - -export class SongListQueryDTO { - @IsString() - @IsOptional() - @ApiProperty({ - example: 'my search query', - description: 'Search string to filter songs by title or description', - required: false, - }) - q?: string; - - @IsEnum(SongSortType) - @IsOptional() - @ApiProperty({ - enum: SongSortType, - example: SongSortType.RECENT, - description: 'Sort songs by the specified criteria', - required: false, - }) - sort?: SongSortType = SongSortType.RECENT; - - @IsEnum(SongOrderType) - @IsOptional() - @ApiProperty({ - enum: SongOrderType, - example: SongOrderType.DESC, - description: 'Sort order (only applies if sort is not random)', - required: false, - }) - order?: SongOrderType = SongOrderType.DESC; - - @IsString() - @IsOptional() - @ApiProperty({ - example: 'pop', - description: - 'Filter by category. If left empty, returns songs in any category', - required: false, - }) - category?: string; - - @IsString() - @IsOptional() - @ApiProperty({ - example: 'username123', - description: - 'Filter by uploader username. If provided, will only return songs uploaded by that user', - required: false, - }) - uploader?: string; - - @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, - }) - @Min(1) - @ApiProperty({ - example: 1, - description: 'Page number', - required: false, - }) - page?: number = 1; - - @IsNumber({ - allowNaN: false, - allowInfinity: false, - maxDecimalPlaces: 0, - }) - @Min(1) - @Max(100) - @ApiProperty({ - example: 10, - description: 'Number of items to return per page', - required: false, - }) - limit?: number = 10; - - constructor(partial: Partial) { - Object.assign(this, partial); - } -} diff --git a/packages/database/src/song/dto/SongPage.dto.ts b/packages/database/src/song/dto/SongPage.dto.ts deleted file mode 100644 index 4e1e0a70..00000000 --- a/packages/database/src/song/dto/SongPage.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IsArray, IsNotEmpty, IsNumber, ValidateNested } from 'class-validator'; - -import { SongPreviewDto } from './SongPreview.dto'; - -export class SongPageDto { - @IsNotEmpty() - @IsArray() - @ValidateNested() - content: Array; - - @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, - }) - total: number; -} diff --git a/packages/database/src/song/dto/SongPreview.dto.ts b/packages/database/src/song/dto/SongPreview.dto.ts deleted file mode 100644 index 38ce760a..00000000 --- a/packages/database/src/song/dto/SongPreview.dto.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { IsNotEmpty, IsString, IsUrl, MaxLength } from 'class-validator'; - -import type { SongWithUser } from '../../song/entity/song.entity'; - -import type { VisibilityType } from './types'; - -type SongPreviewUploader = { - 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: VisibilityType; - - 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 deleted file mode 100644 index 49cb712b..00000000 --- a/packages/database/src/song/dto/SongStats.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - IsBoolean, - IsInt, - IsNumber, - IsString, - ValidateIf, -} from 'class-validator'; - -export class SongStats { - @IsString() - midiFileName: string; - - @IsInt() - noteCount: number; - - @IsInt() - tickCount: number; - - @IsInt() - layerCount: number; - - @IsNumber() - tempo: number; - - @IsNumber() - @ValidateIf((_, value) => value !== null) - tempoRange: number[] | null; - - @IsNumber() - timeSignature: number; - - @IsNumber() - duration: number; - - @IsBoolean() - loop: boolean; - - @IsInt() - loopStartTick: number; - - @IsNumber() - minutesSpent: number; - - @IsInt() - vanillaInstrumentCount: number; - - @IsInt() - customInstrumentCount: number; - - @IsInt() - firstCustomInstrumentIndex: number; - - @IsInt() - outOfRangeNoteCount: number; - - @IsInt() - detunedNoteCount: number; - - @IsInt() - customInstrumentNoteCount: number; - - @IsInt() - incompatibleNoteCount: number; - - @IsBoolean() - compatible: boolean; - - instrumentNoteCounts: number[]; -} diff --git a/packages/database/src/song/dto/SongView.dto.ts b/packages/database/src/song/dto/SongView.dto.ts deleted file mode 100644 index 58d07c04..00000000 --- a/packages/database/src/song/dto/SongView.dto.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - IsBoolean, - IsDate, - IsNotEmpty, - IsNumber, - IsString, - IsUrl, -} from 'class-validator'; - -import { SongStats } from '../../song/dto/SongStats'; -import type { SongDocument } from '../../song/entity/song.entity'; - -import type { CategoryType, LicenseType, VisibilityType } from './types'; - -export type SongViewUploader = { - 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); - } -} diff --git a/packages/database/src/song/dto/ThumbnailData.dto.ts b/packages/database/src/song/dto/ThumbnailData.dto.ts deleted file mode 100644 index efc4e4ed..00000000 --- a/packages/database/src/song/dto/ThumbnailData.dto.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsHexColor, IsInt, IsNotEmpty, Max, Min } from 'class-validator'; - -import { THUMBNAIL_CONSTANTS } from '@nbw/config'; - -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() - @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() - @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', - }; - } -} diff --git a/packages/database/src/song/dto/UploadSongDto.dto.ts b/packages/database/src/song/dto/UploadSongDto.dto.ts deleted file mode 100644 index 8e973050..00000000 --- a/packages/database/src/song/dto/UploadSongDto.dto.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { - IsArray, - IsBoolean, - IsIn, - IsNotEmpty, - IsString, - MaxLength, - ValidateNested, -} from 'class-validator'; - -import { UPLOAD_CONSTANTS } from '@nbw/config'; - -import type { SongDocument } from '../../song/entity/song.entity'; - -import { ThumbnailData } from './ThumbnailData.dto'; -import type { CategoryType, LicenseType, VisibilityType } from './types'; - -const visibility = Object.keys(UPLOAD_CONSTANTS.visibility) as Readonly< - string[] ->; - -const categories = Object.keys(UPLOAD_CONSTANTS.categories) as Readonly< - 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: - '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 ?? [], - }); - } -} diff --git a/packages/database/src/song/dto/UploadSongResponseDto.dto.ts b/packages/database/src/song/dto/UploadSongResponseDto.dto.ts deleted file mode 100644 index b83acb2b..00000000 --- a/packages/database/src/song/dto/UploadSongResponseDto.dto.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { - IsNotEmpty, - IsString, - MaxLength, - ValidateNested, -} from 'class-validator'; - -import type { SongWithUser } from '../../song/entity/song.entity'; - -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; - - @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; - - @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() - noteCount: number; - - 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, - }); - } -} diff --git a/packages/database/src/song/dto/types.ts b/packages/database/src/song/dto/types.ts deleted file mode 100644 index 2a529119..00000000 --- a/packages/database/src/song/dto/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { TIMESPANS, UPLOAD_CONSTANTS } from '@nbw/config'; - -import { CustomInstrumentData } from './CustomInstrumentData.dto'; -import { FeaturedSongsDto } from './FeaturedSongsDto.dto'; -import { SongPageDto } from './SongPage.dto'; -import { SongPreviewDto } from './SongPreview.dto'; -import { SongViewDto } from './SongView.dto'; -import { ThumbnailData as ThumbnailData } from './ThumbnailData.dto'; -import { UploadSongDto } from './UploadSongDto.dto'; -import { UploadSongResponseDto } from './UploadSongResponseDto.dto'; - -export type UploadSongDtoType = InstanceType; - -export type UploadSongNoFileDtoType = Omit; - -export type UploadSongResponseDtoType = InstanceType< - typeof UploadSongResponseDto ->; - -export type SongViewDtoType = InstanceType; - -export type SongPreviewDtoType = InstanceType; - -export type SongPageDtoType = InstanceType; - -export type CustomInstrumentDataType = InstanceType< - typeof CustomInstrumentData ->; - -export type FeaturedSongsDtoType = InstanceType; - -export type ThumbnailDataType = InstanceType; - -export type VisibilityType = keyof typeof UPLOAD_CONSTANTS.visibility; - -export type CategoryType = keyof typeof UPLOAD_CONSTANTS.categories; - -export type LicenseType = keyof typeof UPLOAD_CONSTANTS.licenses; - -export type SongsFolder = Record; - -export type TimespanType = (typeof TIMESPANS)[number]; diff --git a/packages/database/src/song/entity/song.entity.ts b/packages/database/src/song/song.entity.ts similarity index 91% rename from packages/database/src/song/entity/song.entity.ts rename to packages/database/src/song/song.entity.ts index 29d7bd41..69c7ca66 100644 --- a/packages/database/src/song/entity/song.entity.ts +++ b/packages/database/src/song/song.entity.ts @@ -1,10 +1,13 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; -import { SongStats } from '../dto/SongStats'; -import type { SongViewUploader } from '../dto/SongView.dto'; -import { ThumbnailData } from '../dto/ThumbnailData.dto'; -import type { CategoryType, LicenseType, VisibilityType } from '../dto/types'; +import { SongStats, ThumbnailData } from '@nbw/validation'; +import type { + SongViewUploader, + CategoryType, + LicenseType, + VisibilityType, +} from '@nbw/validation'; @Schema({ timestamps: true, diff --git a/packages/database/src/user/dto/CreateUser.dto.ts b/packages/database/src/user/dto/CreateUser.dto.ts deleted file mode 100644 index ec6ca8f8..00000000 --- a/packages/database/src/user/dto/CreateUser.dto.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - 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) - @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; - - 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 deleted file mode 100644 index 3feb46a3..00000000 --- a/packages/database/src/user/dto/GetUser.dto.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - 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) - @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; - - 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 deleted file mode 100644 index b433a0d2..00000000 --- a/packages/database/src/user/dto/Login.dto copy.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class LoginDto { - @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 deleted file mode 100644 index 27c2d9cc..00000000 --- a/packages/database/src/user/dto/LoginWithEmail.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class LoginWithEmailDto { - @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 deleted file mode 100644 index 33be8301..00000000 --- a/packages/database/src/user/dto/NewEmailUser.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - 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 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 deleted file mode 100644 index e1e04c25..00000000 --- a/packages/database/src/user/dto/SingleUsePass.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class SingleUsePassDto { - @ApiProperty() - @IsString() - @IsNotEmpty() - id: 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 deleted file mode 100644 index bc6276e8..00000000 --- a/packages/database/src/user/dto/UpdateUsername.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; - -import { USER_CONSTANTS } from '@nbw/config'; - -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; -} diff --git a/packages/database/src/user/dto/user.dto.ts b/packages/database/src/user/dto/user.dto.ts deleted file mode 100644 index a611c20f..00000000 --- a/packages/database/src/user/dto/user.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -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, - }; - - return userDto; - } -} diff --git a/packages/database/src/user/entity/user.entity.ts b/packages/database/src/user/user.entity.ts similarity index 100% rename from packages/database/src/user/entity/user.entity.ts rename to packages/database/src/user/user.entity.ts diff --git a/packages/validation/README.md b/packages/validation/README.md new file mode 100644 index 00000000..32ec1674 --- /dev/null +++ b/packages/validation/README.md @@ -0,0 +1,15 @@ +# database + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/validation/jest.config.js b/packages/validation/jest.config.js new file mode 100644 index 00000000..fb1e071a --- /dev/null +++ b/packages/validation/jest.config.js @@ -0,0 +1,33 @@ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testRegex: '.*\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.json', + ignoreCodes: ['TS151001'], + }, + ], + }, + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: './coverage', + testEnvironment: 'node', + moduleNameMapper: { + '^@shared/(.*)$': '/../shared/$1', + '^@server/(.*)$': '/src/$1', + }, + testPathIgnorePatterns: [ + '/node_modules/', + '/dist/', + '/coverage/', + ], + coveragePathIgnorePatterns: [ + '/node_modules/', + '/coverage/', + '/dist/', + '.eslintrc.js', + 'jest.config.js', + ], +}; diff --git a/packages/validation/package.json b/packages/validation/package.json new file mode 100644 index 00000000..d0e4908b --- /dev/null +++ b/packages/validation/package.json @@ -0,0 +1,36 @@ +{ + "name": "@nbw/validation", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "private": true, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "bun run clean && bun run build:js && bun run build:types", + "build:js": "tsc --project tsconfig.build.json", + "build:types": "tsc --project tsconfig.types.json", + "clean": "rm -rf dist", + "dev": "tsc --project tsconfig.build.json --watch", + "lint": "eslint \"src/**/*.ts\" --fix", + "test": "bun test **/*.spec.ts" + }, + "devDependencies": { + "@types/bun": "^1.3.4", + "typescript": "^5.9.3" + }, + "dependencies": { + "mongoose": "^9.0.1", + "zod": "^4.1.13", + "zod-validation-error": "^5.0.0", + "@nbw/config": "workspace:*" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/packages/validation/src/common/Page.dto.ts b/packages/validation/src/common/Page.dto.ts new file mode 100644 index 00000000..32169d29 --- /dev/null +++ b/packages/validation/src/common/Page.dto.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +export function createPageDtoSchema(itemSchema: T) { + return z.object({ + total: z.number().int().min(0), + page: z.number().int().min(1), + limit: z.number().int().min(1), + sort: z.string().optional(), + order: z.boolean(), + content: z.array(itemSchema), + }); +} + +export type PageDto = { + total: number; + page: number; + limit: number; + sort?: string; + order: boolean; + content: T[]; +}; diff --git a/packages/validation/src/common/PageQuery.dto.ts b/packages/validation/src/common/PageQuery.dto.ts new file mode 100644 index 00000000..db823f8d --- /dev/null +++ b/packages/validation/src/common/PageQuery.dto.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +import { TIMESPANS } from '@nbw/config'; + +export const pageQueryDTOSchema = z.object({ + page: z.number().int().min(1).optional().default(1), + limit: z.number().int().min(1).max(100).optional(), + sort: z.string().optional().default('createdAt'), + order: z + .union([z.boolean(), z.string().transform((val) => val === 'true')]) + .optional() + .default(false), + timespan: z.enum(TIMESPANS as unknown as [string, ...string[]]).optional(), +}); + +export type PageQueryDTO = z.infer; diff --git a/packages/validation/src/common/types.ts b/packages/validation/src/common/types.ts new file mode 100644 index 00000000..71038d8f --- /dev/null +++ b/packages/validation/src/common/types.ts @@ -0,0 +1,3 @@ +import type { PageQueryDTO } from './PageQuery.dto'; + +export type PageQueryDTOType = PageQueryDTO; diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts new file mode 100644 index 00000000..b142d134 --- /dev/null +++ b/packages/validation/src/index.ts @@ -0,0 +1,24 @@ +export * from './common/Page.dto'; +export * from './common/PageQuery.dto'; +export * from './common/types'; + +export * from './song/CustomInstrumentData.dto'; +export * from './song/FeaturedSongsDto.dto'; +export * from './song/SongListQuery.dto'; +export * from './song/SongPage.dto'; +export * from './song/SongPreview.dto'; +export * from './song/SongStats'; +export * from './song/SongView.dto'; +export * from './song/ThumbnailData.dto'; +export * from './song/UploadSongDto.dto'; +export * from './song/UploadSongResponseDto.dto'; +export * from './song/types'; + +export * from './user/CreateUser.dto'; +export * from './user/GetUser.dto'; +export * from './user/Login.dto copy'; +export * from './user/LoginWithEmail.dto'; +export * from './user/NewEmailUser.dto'; +export * from './user/SingleUsePass.dto'; +export * from './user/UpdateUsername.dto'; +export * from './user/user.dto'; diff --git a/packages/validation/src/song/CustomInstrumentData.dto.ts b/packages/validation/src/song/CustomInstrumentData.dto.ts new file mode 100644 index 00000000..ad5efb38 --- /dev/null +++ b/packages/validation/src/song/CustomInstrumentData.dto.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const customInstrumentDataSchema = z.object({ + sound: z.array(z.string()).min(1), +}); + +export type CustomInstrumentData = z.infer; diff --git a/packages/validation/src/song/FeaturedSongsDto.dto.ts b/packages/validation/src/song/FeaturedSongsDto.dto.ts new file mode 100644 index 00000000..0ee7b5a1 --- /dev/null +++ b/packages/validation/src/song/FeaturedSongsDto.dto.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import { songPreviewDtoSchema } from './SongPreview.dto'; + +export const featuredSongsDtoSchema = z.object({ + hour: z.array(songPreviewDtoSchema), + day: z.array(songPreviewDtoSchema), + week: z.array(songPreviewDtoSchema), + month: z.array(songPreviewDtoSchema), + year: z.array(songPreviewDtoSchema), + all: z.array(songPreviewDtoSchema), +}); + +export type FeaturedSongsDto = z.infer; + +export const createFeaturedSongsDto = (): FeaturedSongsDto => { + return { + hour: [], + day: [], + week: [], + month: [], + year: [], + all: [], + }; +}; diff --git a/packages/validation/src/song/SongListQuery.dto.ts b/packages/validation/src/song/SongListQuery.dto.ts new file mode 100644 index 00000000..f9b06d56 --- /dev/null +++ b/packages/validation/src/song/SongListQuery.dto.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; + +export enum SongSortType { + RECENT = 'recent', + RANDOM = 'random', + PLAY_COUNT = 'playCount', + TITLE = 'title', + DURATION = 'duration', + NOTE_COUNT = 'noteCount', +} + +export enum SongOrderType { + ASC = 'asc', + DESC = 'desc', +} + +export const songListQueryDTOSchema = z.object({ + q: z.string().optional(), + sort: z.nativeEnum(SongSortType).optional().default(SongSortType.RECENT), + order: z.nativeEnum(SongOrderType).optional().default(SongOrderType.DESC), + category: z.string().optional(), + uploader: z.string().optional(), + page: z.number().int().min(1).optional().default(1), + limit: z.number().int().min(1).max(100).optional().default(10), +}); + +export type SongListQueryDTO = z.infer; diff --git a/packages/validation/src/song/SongPage.dto.ts b/packages/validation/src/song/SongPage.dto.ts new file mode 100644 index 00000000..015a6cc2 --- /dev/null +++ b/packages/validation/src/song/SongPage.dto.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +import { songPreviewDtoSchema } from './SongPreview.dto'; + +export const songPageDtoSchema = z.object({ + content: z.array(songPreviewDtoSchema), + page: z.number().int().min(1), + limit: z.number().int().min(1), + total: z.number().int().min(0), +}); + +export type SongPageDto = z.infer; diff --git a/packages/validation/src/song/SongPreview.dto.ts b/packages/validation/src/song/SongPreview.dto.ts new file mode 100644 index 00000000..bfc7275b --- /dev/null +++ b/packages/validation/src/song/SongPreview.dto.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; + +import type { VisibilityType } from './types'; + +const songPreviewUploaderSchema = z.object({ + username: z.string(), + profileImage: z.string(), +}); + +export type SongPreviewUploader = z.infer; + +export const songPreviewDtoSchema = z.object({ + publicId: z.string().min(1), + uploader: songPreviewUploaderSchema, + title: z.string().min(1).max(128), + description: z.string().min(1), + originalAuthor: z.string().min(1).max(64), + duration: z.number().min(0), + noteCount: z.number().int().min(0), + thumbnailUrl: z.string().url(), + createdAt: z.date(), + updatedAt: z.date(), + playCount: z.number().int().min(0), + visibility: z.string() as z.ZodType, +}); + +export type SongPreviewDto = z.infer; diff --git a/packages/validation/src/song/SongStats.ts b/packages/validation/src/song/SongStats.ts new file mode 100644 index 00000000..6e1b9b06 --- /dev/null +++ b/packages/validation/src/song/SongStats.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +export const songStatsSchema = z.object({ + midiFileName: z.string(), + noteCount: z.number().int(), + tickCount: z.number().int(), + layerCount: z.number().int(), + tempo: z.number(), + tempoRange: z.array(z.number()).nullable(), + timeSignature: z.number(), + duration: z.number(), + loop: z.boolean(), + loopStartTick: z.number().int(), + minutesSpent: z.number(), + vanillaInstrumentCount: z.number().int(), + customInstrumentCount: z.number().int(), + firstCustomInstrumentIndex: z.number().int(), + outOfRangeNoteCount: z.number().int(), + detunedNoteCount: z.number().int(), + customInstrumentNoteCount: z.number().int(), + incompatibleNoteCount: z.number().int(), + compatible: z.boolean(), + instrumentNoteCounts: z.array(z.number().int()), +}); + +export type SongStats = z.infer; diff --git a/packages/validation/src/song/SongView.dto.ts b/packages/validation/src/song/SongView.dto.ts new file mode 100644 index 00000000..7de76baf --- /dev/null +++ b/packages/validation/src/song/SongView.dto.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; + +import { songStatsSchema } from './SongStats'; +import type { CategoryType, LicenseType, VisibilityType } from './types'; + +export const songViewUploaderSchema = z.object({ + username: z.string(), + profileImage: z.string(), +}); + +export type SongViewUploader = z.infer; + +export const songViewDtoSchema = z.object({ + publicId: z.string().min(1), + createdAt: z.date(), + uploader: songViewUploaderSchema, + thumbnailUrl: z.string().url(), + playCount: z.number().int().min(0), + downloadCount: z.number().int().min(0), + likeCount: z.number().int().min(0), + allowDownload: z.boolean(), + title: z.string().min(1), + originalAuthor: z.string(), + description: z.string(), + visibility: z.string() as z.ZodType, + category: z.string() as z.ZodType, + license: z.string() as z.ZodType, + customInstruments: z.array(z.string()), + fileSize: z.number().int().min(0), + stats: songStatsSchema, +}); + +export type SongViewDto = z.infer; diff --git a/packages/validation/src/song/ThumbnailData.dto.ts b/packages/validation/src/song/ThumbnailData.dto.ts new file mode 100644 index 00000000..5b877116 --- /dev/null +++ b/packages/validation/src/song/ThumbnailData.dto.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import { THUMBNAIL_CONSTANTS } from '@nbw/config'; + +export const thumbnailDataSchema = z.object({ + zoomLevel: z + .number() + .int() + .min(THUMBNAIL_CONSTANTS.zoomLevel.min) + .max(THUMBNAIL_CONSTANTS.zoomLevel.max), + startTick: z.number().int().min(0), + startLayer: z.number().int().min(0), + backgroundColor: z.string().regex(/^#[0-9a-fA-F]{6}$/), +}); + +export type ThumbnailData = z.infer; + +export const getThumbnailDataExample = (): ThumbnailData => { + return { + zoomLevel: 3, + startTick: 0, + startLayer: 0, + backgroundColor: '#F0F0F0', + }; +}; diff --git a/packages/validation/src/song/UploadSongDto.dto.ts b/packages/validation/src/song/UploadSongDto.dto.ts new file mode 100644 index 00000000..c7a74e9a --- /dev/null +++ b/packages/validation/src/song/UploadSongDto.dto.ts @@ -0,0 +1,54 @@ +import { z } from 'zod'; + +import { UPLOAD_CONSTANTS } from '@nbw/config'; + +import { thumbnailDataSchema } from './ThumbnailData.dto'; +import type { CategoryType, LicenseType, VisibilityType } from './types'; + +const visibility = Object.keys(UPLOAD_CONSTANTS.visibility) as Readonly< + string[] +>; + +const categories = Object.keys(UPLOAD_CONSTANTS.categories) as Readonly< + string[] +>; + +const licenses = Object.keys(UPLOAD_CONSTANTS.licenses) as Readonly; + +// Note: file field is not validated by zod as it's handled by multer/file upload middleware +export const uploadSongDtoSchema = z.object({ + file: z.any(), // Express.Multer.File - handled by upload middleware + allowDownload: z + .union([z.boolean(), z.string().transform((val) => val === 'true')]) + .pipe(z.boolean()), + visibility: z.enum( + visibility as [string, ...string[]], + ) as z.ZodType, + title: z.string().min(1).max(UPLOAD_CONSTANTS.title.maxLength), + originalAuthor: z.string().max(UPLOAD_CONSTANTS.originalAuthor.maxLength), + description: z.string().max(UPLOAD_CONSTANTS.description.maxLength), + category: z.enum( + categories as [string, ...string[]], + ) as z.ZodType, + thumbnailData: z + .union([ + thumbnailDataSchema, + z + .string() + .transform((val) => JSON.parse(val)) + .pipe(thumbnailDataSchema), + ]) + .pipe(thumbnailDataSchema), + license: z.enum(licenses as [string, ...string[]]) as z.ZodType, + customInstruments: z + .union([ + z.array(z.string()), + z + .string() + .transform((val) => JSON.parse(val)) + .pipe(z.array(z.string())), + ]) + .pipe(z.array(z.string()).max(UPLOAD_CONSTANTS.customInstruments.maxCount)), +}); + +export type UploadSongDto = z.infer; diff --git a/packages/validation/src/song/UploadSongResponseDto.dto.ts b/packages/validation/src/song/UploadSongResponseDto.dto.ts new file mode 100644 index 00000000..f77bd316 --- /dev/null +++ b/packages/validation/src/song/UploadSongResponseDto.dto.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +import { songViewUploaderSchema } from './SongView.dto'; + +export const uploadSongResponseDtoSchema = z.object({ + publicId: z.string().min(1), + title: z.string().min(1).max(128), + uploader: songViewUploaderSchema, + thumbnailUrl: z.string().url(), + duration: z.number().min(0), + noteCount: z.number().int().min(0), +}); + +export type UploadSongResponseDto = z.infer; diff --git a/packages/validation/src/song/types.ts b/packages/validation/src/song/types.ts new file mode 100644 index 00000000..680a4d59 --- /dev/null +++ b/packages/validation/src/song/types.ts @@ -0,0 +1,38 @@ +import { TIMESPANS, UPLOAD_CONSTANTS } from '@nbw/config'; + +import type { CustomInstrumentData } from './CustomInstrumentData.dto'; +import type { FeaturedSongsDto } from './FeaturedSongsDto.dto'; +import type { SongPageDto } from './SongPage.dto'; +import type { SongPreviewDto } from './SongPreview.dto'; +import type { SongViewDto } from './SongView.dto'; +import type { ThumbnailData } from './ThumbnailData.dto'; +import type { UploadSongDto } from './UploadSongDto.dto'; +import type { UploadSongResponseDto } from './UploadSongResponseDto.dto'; + +export type UploadSongDtoType = UploadSongDto; + +export type UploadSongNoFileDtoType = Omit; + +export type UploadSongResponseDtoType = UploadSongResponseDto; + +export type SongViewDtoType = SongViewDto; + +export type SongPreviewDtoType = SongPreviewDto; + +export type SongPageDtoType = SongPageDto; + +export type CustomInstrumentDataType = CustomInstrumentData; + +export type FeaturedSongsDtoType = FeaturedSongsDto; + +export type ThumbnailDataType = ThumbnailData; + +export type VisibilityType = keyof typeof UPLOAD_CONSTANTS.visibility; + +export type CategoryType = keyof typeof UPLOAD_CONSTANTS.categories; + +export type LicenseType = keyof typeof UPLOAD_CONSTANTS.licenses; + +export type SongsFolder = Record; + +export type TimespanType = (typeof TIMESPANS)[number]; diff --git a/packages/validation/src/user/CreateUser.dto.ts b/packages/validation/src/user/CreateUser.dto.ts new file mode 100644 index 00000000..9eda7bfc --- /dev/null +++ b/packages/validation/src/user/CreateUser.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const createUserSchema = z.object({ + email: z.string().email().max(64).min(1), + username: z.string().max(64).min(1), + profileImage: z.string().url(), +}); + +export type CreateUser = z.infer; diff --git a/packages/validation/src/user/GetUser.dto.ts b/packages/validation/src/user/GetUser.dto.ts new file mode 100644 index 00000000..8453d48f --- /dev/null +++ b/packages/validation/src/user/GetUser.dto.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const getUserSchema = z.object({ + email: z.string().email().max(64).optional(), + username: z.string().max(64).optional(), + id: z + .string() + .regex(/^[0-9a-fA-F]{24}$/) + .min(24) + .max(24) + .optional(), +}); + +export type GetUser = z.infer; diff --git a/packages/validation/src/user/Login.dto copy.ts b/packages/validation/src/user/Login.dto copy.ts new file mode 100644 index 00000000..e53f40f4 --- /dev/null +++ b/packages/validation/src/user/Login.dto copy.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const loginDtoSchema = z.object({ + email: z.string().email().min(1), +}); + +export type LoginDto = z.infer; diff --git a/packages/validation/src/user/LoginWithEmail.dto.ts b/packages/validation/src/user/LoginWithEmail.dto.ts new file mode 100644 index 00000000..3a4dc5b8 --- /dev/null +++ b/packages/validation/src/user/LoginWithEmail.dto.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const loginWithEmailDtoSchema = z.object({ + email: z.string().email().min(1), +}); + +export type LoginWithEmailDto = z.infer; diff --git a/packages/validation/src/user/NewEmailUser.dto.ts b/packages/validation/src/user/NewEmailUser.dto.ts new file mode 100644 index 00000000..cd71645e --- /dev/null +++ b/packages/validation/src/user/NewEmailUser.dto.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const newEmailUserDtoSchema = z.object({ + username: z.string().min(4).max(64), + email: z.string().email().max(64).min(1), +}); + +export type NewEmailUserDto = z.infer; diff --git a/packages/validation/src/user/SingleUsePass.dto.ts b/packages/validation/src/user/SingleUsePass.dto.ts new file mode 100644 index 00000000..b827a726 --- /dev/null +++ b/packages/validation/src/user/SingleUsePass.dto.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const singleUsePassDtoSchema = z.object({ + id: z.string().min(1), + pass: z.string().min(1), +}); + +export type SingleUsePassDto = z.infer; diff --git a/packages/validation/src/user/UpdateUsername.dto.ts b/packages/validation/src/user/UpdateUsername.dto.ts new file mode 100644 index 00000000..b6ea0a84 --- /dev/null +++ b/packages/validation/src/user/UpdateUsername.dto.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { USER_CONSTANTS } from '@nbw/config'; + +export const updateUsernameDtoSchema = z.object({ + username: z + .string() + .min(USER_CONSTANTS.USERNAME_MIN_LENGTH) + .max(USER_CONSTANTS.USERNAME_MAX_LENGTH) + .regex(USER_CONSTANTS.ALLOWED_REGEXP), +}); + +export type UpdateUsernameDto = z.infer; diff --git a/packages/validation/src/user/user.dto.ts b/packages/validation/src/user/user.dto.ts new file mode 100644 index 00000000..42626de8 --- /dev/null +++ b/packages/validation/src/user/user.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const userDtoSchema = z.object({ + username: z.string(), + publicName: z.string(), + email: z.string(), +}); + +export type UserDto = z.infer; diff --git a/packages/validation/tsconfig.build.json b/packages/validation/tsconfig.build.json new file mode 100644 index 00000000..0fc0958a --- /dev/null +++ b/packages/validation/tsconfig.build.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + // Unlike other packages where we use a bundler to output JS, + // this package uses tsc for its build step. + // We must disable the default 'composite' config to output JS. + "composite": false, + "declaration": false, + "emitDeclarationOnly": false, + + // Module target for Node runtime + "module": "CommonJS", + "moduleResolution": "node", + "target": "ES2021", + + // Allow ES imports + "verbatimModuleSyntax": false + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/validation/tsconfig.json b/packages/validation/tsconfig.json new file mode 100644 index 00000000..9547fe19 --- /dev/null +++ b/packages/validation/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.package.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo", + + // Database runtime requirements + "emitDecoratorMetadata": true + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/validation/tsconfig.types.json b/packages/validation/tsconfig.types.json new file mode 100644 index 00000000..be175bda --- /dev/null +++ b/packages/validation/tsconfig.types.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + // Emit-only configuration for building types. + "declaration": true, + "emitDeclarationOnly": true + } +} diff --git a/scripts/build.ts b/scripts/build.ts index 72961906..2ad5a447 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -4,6 +4,7 @@ import { $ } from 'bun'; // the sub array is for packages that can be built in parallel const packages: (string | string[])[] = [ '@nbw/config', + '@nbw/validation', '@nbw/database', '@nbw/song', '@nbw/sounds', diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index a627c79a..c1531054 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -15,6 +15,7 @@ { "path": "packages/database" }, { "path": "packages/song" }, { "path": "packages/sounds" }, - { "path": "packages/thumbnail" } + { "path": "packages/thumbnail" }, + { "path": "packages/validation" } ] } diff --git a/tsconfig.json b/tsconfig.json index 48dd729b..007cb111 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ { "path": "packages/database" }, { "path": "packages/song" }, { "path": "packages/sounds" }, - { "path": "packages/thumbnail" } + { "path": "packages/thumbnail" }, + { "path": "packages/validation" } ] }