diff --git a/.docker/docker-compose.local.yml b/.docker/docker-compose.local.yml new file mode 100644 index 0000000..f42adb2 --- /dev/null +++ b/.docker/docker-compose.local.yml @@ -0,0 +1,65 @@ +services: + db: + container_name: nestjs-turbo-postgres + image: postgres:17 + restart: always + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: nestjs_turbo + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + TZ: "UTC" + ports: + - "25432:5432" + networks: + - nestjs-turbo-network + + redis: + image: redis/redis-stack:latest + restart: always + ports: + - "6379:6379" + - "8001:8001" + volumes: + - redis_data:/data + healthcheck: + test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ] + environment: + REDIS_ARGS: "--requirepass redispass" + networks: + - nestjs-turbo-network + + maildev: + build: + context: . + dockerfile: maildev.Dockerfile + ports: + - ${MAIL_CLIENT_PORT}:1080 + - ${MAIL_PORT}:1025 + networks: + - nestjs-turbo-network + + pgadmin: + container_name: pgadmin + image: dpage/pgadmin4 + ports: + - "18080:80" + volumes: + - pgadmin_data:/root/.pgadmin + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: 12345678 + PGADMIN_CONFIG_WTF_CSRF_ENABLED: "False" + PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: "False" + networks: + - nestjs-turbo-network + +volumes: + postgres_data: + pgadmin_data: + redis_data: + +networks: + nestjs-turbo-network: + driver: bridge diff --git a/.docker/docker-compose.mysql.local.yml b/.docker/docker-compose.mysql.local.yml new file mode 100644 index 0000000..5e90a69 --- /dev/null +++ b/.docker/docker-compose.mysql.local.yml @@ -0,0 +1,47 @@ +services: + db: + container_name: nestjs-turbo-mysql + image: mysql + restart: always + volumes: + - mysql_data:/var/lib/mysql + environment: + MYSQL_DATABASE: realworld_api + MYSQL_ROOT_PASSWORD: 12345678 + ports: + - "13306:3306" + networks: + - nestjs-turbo-network + + redis: + image: redis/redis-stack:latest + restart: always + ports: + - "6379:6379" + - "8001:8001" + volumes: + - redis_data:/data + healthcheck: + test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ] + environment: + REDIS_ARGS: "--requirepass redispass" + networks: + - nestjs-turbo-network + + maildev: + build: + context: . + dockerfile: maildev.Dockerfile + ports: + - ${MAIL_CLIENT_PORT}:1080 + - ${MAIL_PORT}:1025 + networks: + - nestjs-turbo-network + +volumes: + mysql_data: + redis_data: + +networks: + nestjs-turbo-network: + driver: bridge diff --git a/apps/realworld-api/.env.example b/apps/realworld-api/.env.example index d7efd37..ecf037f 100644 --- a/apps/realworld-api/.env.example +++ b/apps/realworld-api/.env.example @@ -2,7 +2,7 @@ NODE_ENV=development ##== Application -APP_NAME="Admin API" +APP_NAME="Realworld API" APP_URL=http://localhost:3000 APP_PORT=3000 APP_DEBUG=false diff --git a/apps/realworld-api/src/api/profile/profile.module.ts b/apps/realworld-api/src/api/profile/profile.module.ts index ed4142f..aebfab0 100644 --- a/apps/realworld-api/src/api/profile/profile.module.ts +++ b/apps/realworld-api/src/api/profile/profile.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserEntity } from '@repo/database-typeorm'; -import { UserFollowsEntity } from '@repo/database-typeorm/entities/user-follows.entity'; +import { UserEntity, UserFollowsEntity } from '@repo/database-typeorm'; import { ProfileController } from './profile.controller'; import { ProfileService } from './profile.service'; diff --git a/apps/realworld-api/src/api/profile/profile.service.ts b/apps/realworld-api/src/api/profile/profile.service.ts index 8c408bb..717de1f 100644 --- a/apps/realworld-api/src/api/profile/profile.service.ts +++ b/apps/realworld-api/src/api/profile/profile.service.ts @@ -2,8 +2,7 @@ import { ErrorCode } from '@/constants/error-code.constant'; import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ValidationException } from '@repo/api'; -import { UserEntity } from '@repo/database-typeorm'; -import { UserFollowsEntity } from '@repo/database-typeorm/entities/user-follows.entity'; +import { UserEntity, UserFollowsEntity } from '@repo/database-typeorm'; import { Repository } from 'typeorm'; import { ProfileDto, ProfileResDto } from './dto/profile.dto'; diff --git a/apps/realworld-api/src/app.module.ts b/apps/realworld-api/src/app.module.ts index 03f1960..f91a80f 100644 --- a/apps/realworld-api/src/app.module.ts +++ b/apps/realworld-api/src/app.module.ts @@ -21,6 +21,7 @@ import authConfig from './api/auth/config/auth.config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AllConfigType } from './config/config.type'; +// import { TypeOrmConfigService } from './database/mysql-typeorm-config.service'; // Uncomment this line if you are using MySQL import { TypeOrmConfigService } from './database/typeorm-config.service'; const configModule = ConfigModule.forRoot({ diff --git a/apps/realworld-api/src/database/mysql-typeorm-config.service.ts b/apps/realworld-api/src/database/mysql-typeorm-config.service.ts new file mode 100644 index 0000000..73aa211 --- /dev/null +++ b/apps/realworld-api/src/database/mysql-typeorm-config.service.ts @@ -0,0 +1,51 @@ +import { AllConfigType } from '@/config/config.type'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; +import { join } from 'path'; + +@Injectable() +export class TypeOrmConfigService implements TypeOrmOptionsFactory { + constructor(private configService: ConfigService) {} + + createTypeOrmOptions(): TypeOrmModuleOptions { + const modulePath = require.resolve('@repo/mysql-typeorm'); + const nodeModulesDir = join(modulePath, '..', '..'); + + return { + type: this.configService.get('database.type', { infer: true }), + host: this.configService.get('database.host', { infer: true }), + port: this.configService.get('database.port', { infer: true }), + username: this.configService.get('database.username', { infer: true }), + password: this.configService.get('database.password', { infer: true }), + database: this.configService.get('database.name', { infer: true }), + synchronize: this.configService.get('database.synchronize', { + infer: true, + }), + dropSchema: false, + keepConnectionAlive: true, + logger: 'debug', + entities: [join(nodeModulesDir, 'dist', '**', '*.entity.{ts,js}')], + poolSize: this.configService.get('database.maxConnections', { + infer: true, + }), + ssl: this.configService.get('database.sslEnabled', { infer: true }) + ? { + rejectUnauthorized: this.configService.get( + 'database.rejectUnauthorized', + { infer: true }, + ), + ca: + this.configService.get('database.ca', { infer: true }) ?? + undefined, + key: + this.configService.get('database.key', { infer: true }) ?? + undefined, + cert: + this.configService.get('database.cert', { infer: true }) ?? + undefined, + } + : undefined, + } as TypeOrmModuleOptions; + } +} diff --git a/apps/realworld-api/src/database/typeorm-config.service.ts b/apps/realworld-api/src/database/typeorm-config.service.ts index 027b31c..e09c576 100644 --- a/apps/realworld-api/src/database/typeorm-config.service.ts +++ b/apps/realworld-api/src/database/typeorm-config.service.ts @@ -54,6 +54,6 @@ export class TypeOrmConfigService implements TypeOrmOptionsFactory { undefined, } : undefined, - } as TypeOrmModuleOptions; + } as unknown as TypeOrmModuleOptions; } } diff --git a/apps/realworld-api/tsconfig.json b/apps/realworld-api/tsconfig.json index 2bbeb55..e48ddc5 100644 --- a/apps/realworld-api/tsconfig.json +++ b/apps/realworld-api/tsconfig.json @@ -16,6 +16,7 @@ "@/guards/*": ["src/guards/*"], "@/interceptors/*": ["src/interceptors/*"], "@/utils/*": ["src/utils/*"] + // "@repo/database-typeorm": ["node_modules/@repo/mysql-typeorm"], // Uncomment this line if you are using MySQL } } } diff --git a/packages/mysql-typeorm/.env.example b/packages/mysql-typeorm/.env.example new file mode 100644 index 0000000..fe3c9b5 --- /dev/null +++ b/packages/mysql-typeorm/.env.example @@ -0,0 +1,17 @@ +##== Environment +NODE_ENV=development + +##== Database +DATABASE_TYPE=mysql +DATABASE_HOST=localhost +DATABASE_PORT=3306 +DATABASE_USERNAME=root +DATABASE_PASSWORD=12345678 +DATABASE_LOGGING=true +DATABASE_SYNCHRONIZE=false +DATABASE_MAX_CONNECTIONS=100 +DATABASE_SSL_ENABLED=false +DATABASE_REJECT_UNAUTHORIZED=false +DATABASE_CA= +DATABASE_KEY= +DATABASE_CERT= diff --git a/packages/mysql-typeorm/.prettierrc.mjs b/packages/mysql-typeorm/.prettierrc.mjs new file mode 100644 index 0000000..02f39b4 --- /dev/null +++ b/packages/mysql-typeorm/.prettierrc.mjs @@ -0,0 +1,3 @@ +import prettier from '@repo/eslint-config/prettier-base.config.mjs'; + +export default { ...prettier }; diff --git a/packages/mysql-typeorm/eslint.config.mjs b/packages/mysql-typeorm/eslint.config.mjs new file mode 100644 index 0000000..b2ec0fe --- /dev/null +++ b/packages/mysql-typeorm/eslint.config.mjs @@ -0,0 +1,3 @@ +import nest from '@repo/eslint-config/eslint-nest.config.mjs'; + +export default [...nest]; diff --git a/packages/mysql-typeorm/package.json b/packages/mysql-typeorm/package.json new file mode 100644 index 0000000..17bf1c2 --- /dev/null +++ b/packages/mysql-typeorm/package.json @@ -0,0 +1,63 @@ +{ + "name": "@repo/mysql-typeorm", + "version": "0.0.0", + "private": true, + "license": "MIT", + "scripts": { + "clean": "rm -rf dist", + "dev": "pnpm build --watch", + "build": "tsc -b -v", + "lint": "eslint \"{src,test}/**/*.ts\"", + "typeorm": "pnpm clean && env-cmd typeorm-ts-node-commonjs -d src/data-source.ts", + "migration:up": "pnpm typeorm migration:run", + "migration:down": "pnpm typeorm migration:revert", + "migration:show": "pnpm typeorm migration:show", + "migration:create": "typeorm migration:create", + "migration:generate": "pnpm typeorm migration:generate --pretty", + "typeorm-ex": "pnpm clean && pnpm build && env-cmd typeorm-extension", + "db:create": "pnpm typeorm-ex db:create", + "db:drop": "pnpm typeorm-ex db:drop", + "seed:run": "pnpm typeorm-ex seed:run", + "seed:create": "pnpm typeorm-ex seed:create" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "./dist/**", + "!./dist/factories", + "!./dist/migrations", + "!./dist/seeds" + ], + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./dist/*.d.ts", + "default": "./dist/*.js" + } + }, + "dependencies": { + "@nestjs/config": "^3.3.0", + "@nestjs/mapped-types": "*", + "@repo/nest-common": "workspace:*", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "env-cmd": "^10.1.0", + "pg": "^8.13.1", + "typeorm": "^0.3.20", + "typeorm-extension": "^3.6.3" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/node": "^20.17.2", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} diff --git a/packages/mysql-typeorm/src/config/database-config.type.ts b/packages/mysql-typeorm/src/config/database-config.type.ts new file mode 100644 index 0000000..dc010ec --- /dev/null +++ b/packages/mysql-typeorm/src/config/database-config.type.ts @@ -0,0 +1,16 @@ +export type DatabaseConfig = { + type: string; + host: string; + port: number; + password: string; + name: string; + username: string; + logging: boolean; + synchronize: boolean; + maxConnections: number; + sslEnabled: boolean; + rejectUnauthorized: boolean; + ca?: string; + key?: string; + cert?: string; +}; diff --git a/packages/mysql-typeorm/src/config/database.config.ts b/packages/mysql-typeorm/src/config/database.config.ts new file mode 100644 index 0000000..ad3723b --- /dev/null +++ b/packages/mysql-typeorm/src/config/database.config.ts @@ -0,0 +1,104 @@ +import { registerAs } from '@nestjs/config'; +import { validateConfig } from '@repo/nest-common'; +import { + IsBoolean, + IsInt, + IsOptional, + IsPositive, + IsString, + Max, + Min, + ValidateIf, +} from 'class-validator'; +import { DatabaseConfig } from './database-config.type'; + +class EnvironmentVariablesValidator { + @ValidateIf((envValues) => envValues.DATABASE_URL) + @IsString() + DATABASE_URL: string; + + @ValidateIf((envValues) => !envValues.DATABASE_URL) + @IsString() + DATABASE_TYPE: string; + + @ValidateIf((envValues) => !envValues.DATABASE_URL) + @IsString() + DATABASE_HOST: string; + + @ValidateIf((envValues) => !envValues.DATABASE_URL) + @IsInt() + @Min(0) + @Max(65535) + DATABASE_PORT: number; + + @ValidateIf((envValues) => !envValues.DATABASE_URL) + @IsString() + DATABASE_PASSWORD: string; + + @ValidateIf((envValues) => !envValues.DATABASE_URL) + @IsString() + DATABASE_NAME: string; + + @ValidateIf((envValues) => !envValues.DATABASE_URL) + @IsString() + DATABASE_USERNAME: string; + + @IsBoolean() + @IsOptional() + DATABASE_LOGGING: boolean; + + @IsBoolean() + @IsOptional() + DATABASE_SYNCHRONIZE: boolean; + + @IsInt() + @IsPositive() + @IsOptional() + DATABASE_MAX_CONNECTIONS: number; + + @IsBoolean() + @IsOptional() + DATABASE_SSL_ENABLED: boolean; + + @IsBoolean() + @IsOptional() + DATABASE_REJECT_UNAUTHORIZED: boolean; + + @IsString() + @IsOptional() + DATABASE_CA: string; + + @IsString() + @IsOptional() + DATABASE_KEY: string; + + @IsString() + @IsOptional() + DATABASE_CERT: string; +} + +export default registerAs('database', () => { + console.info(`Register DatabaseConfig from environment variables`); + validateConfig(process.env, EnvironmentVariablesValidator); + + return { + type: process.env.DATABASE_TYPE, + host: process.env.DATABASE_HOST, + port: process.env.DATABASE_PORT + ? parseInt(process.env.DATABASE_PORT, 10) + : 5432, + password: process.env.DATABASE_PASSWORD, + name: process.env.DATABASE_NAME, + username: process.env.DATABASE_USERNAME, + logging: process.env.DATABASE_LOGGING === 'true', + synchronize: process.env.DATABASE_SYNCHRONIZE === 'true', + maxConnections: process.env.DATABASE_MAX_CONNECTIONS + ? parseInt(process.env.DATABASE_MAX_CONNECTIONS, 10) + : 100, + sslEnabled: process.env.DATABASE_SSL_ENABLED === 'true', + rejectUnauthorized: process.env.DATABASE_REJECT_UNAUTHORIZED === 'true', + ca: process.env.DATABASE_CA, + key: process.env.DATABASE_KEY, + cert: process.env.DATABASE_CERT, + }; +}); diff --git a/packages/mysql-typeorm/src/config/index.ts b/packages/mysql-typeorm/src/config/index.ts new file mode 100644 index 0000000..2c84ddb --- /dev/null +++ b/packages/mysql-typeorm/src/config/index.ts @@ -0,0 +1,5 @@ +import databaseConfig from './database.config'; +export * from './database-config.type'; +export * from './typeorm-custom-logger'; + +export { databaseConfig }; diff --git a/packages/mysql-typeorm/src/config/typeorm-custom-logger.ts b/packages/mysql-typeorm/src/config/typeorm-custom-logger.ts new file mode 100644 index 0000000..5c5a390 --- /dev/null +++ b/packages/mysql-typeorm/src/config/typeorm-custom-logger.ts @@ -0,0 +1,208 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Logger } from '@nestjs/common'; +import { + QueryRunner, + Logger as TypeOrmLogger, + type LogLevel, + type LogMessageType, + type LoggerOptions, +} from 'typeorm'; + +export class TypeOrmCustomLogger implements TypeOrmLogger { + static getInstance(connectionName: string, options: LoggerOptions) { + const logger = new Logger(`TypeORM[${connectionName}]`); + return new TypeOrmCustomLogger(logger, options); + } + + constructor( + private readonly logger: Logger, + private readonly options: LoggerOptions, + ) {} + + /** + * Logs query and parameters used in it. + */ + logQuery(query: string, parameters?: any[], _queryRunner?: QueryRunner) { + if (!this.isLogEnabledFor('query')) { + return; + } + + const sql = + query + + (parameters && parameters.length + ? ' -- PARAMETERS: ' + this.stringifyParams(parameters) + : ''); + this.logger.log(`query: ${sql}`); + } + + /** + * Logs query that is failed. + */ + logQueryError( + error: string, + query: string, + parameters?: any[], + _queryRunner?: QueryRunner, + ) { + if (!this.isLogEnabledFor('query-error')) { + return; + } + + const sql = + query + + (parameters && parameters.length + ? ' -- PARAMETERS: ' + this.stringifyParams(parameters) + : ''); + this.logger.error(`query failed: ${sql}`); + this.logger.error(`error:`, error); + } + + /** + * Logs query that is slow. + */ + logQuerySlow( + time: number, + query: string, + parameters?: any[], + _queryRunner?: QueryRunner, + ) { + if (!this.isLogEnabledFor('query-slow')) { + return; + } + + const sql = + query + + (parameters && parameters.length + ? ' -- PARAMETERS: ' + this.stringifyParams(parameters) + : ''); + this.logger.warn(`query is slow: ${sql}`); + this.logger.warn(`execution time: ${time}`); + } + + /** + * Logs events from the schema build process. + */ + logSchemaBuild(message: string, _queryRunner?: QueryRunner) { + if (!this.isLogEnabledFor('schema-build')) { + return; + } + + this.logger.log(message); + } + + /** + * Logs events from the migrations run process. + */ + logMigration(message: string, _queryRunner?: QueryRunner) { + if (!this.isLogEnabledFor('migration')) { + return; + } + + this.logger.log(message); + } + + /** + * Perform logging using given logger, or by default to the this.logger. + * Log has its own level and message. + */ + log( + level: 'log' | 'info' | 'warn', + message: any, + _queryRunner?: QueryRunner, + ) { + switch (level) { + case 'log': + if (!this.isLogEnabledFor('log')) { + return; + } + + this.logger.log(message); + break; + case 'info': + if (!this.isLogEnabledFor('info')) { + return; + } + + this.logger.log(message); + break; + case 'warn': + if (!this.isLogEnabledFor('warn')) { + return; + } + + this.logger.warn(message); + break; + } + } + + /** + * Check is logging for level or message type is enabled. + */ + protected isLogEnabledFor(type?: LogLevel | LogMessageType) { + switch (type) { + case 'query': + return ( + this.options === 'all' || + this.options === true || + (Array.isArray(this.options) && this.options.indexOf('query') !== -1) + ); + + case 'error': + case 'query-error': + return ( + this.options === 'all' || + this.options === true || + (Array.isArray(this.options) && this.options.indexOf('error') !== -1) + ); + + case 'query-slow': + return true; + + case 'schema': + case 'schema-build': + return ( + this.options === 'all' || + (Array.isArray(this.options) && this.options.indexOf('schema') !== -1) + ); + + case 'migration': + return true; + + case 'log': + return ( + this.options === 'all' || + (Array.isArray(this.options) && this.options.indexOf('log') !== -1) + ); + + case 'info': + return ( + this.options === 'all' || + (Array.isArray(this.options) && this.options.indexOf('info') !== -1) + ); + + case 'warn': + return ( + this.options === 'all' || + (Array.isArray(this.options) && this.options.indexOf('warn') !== -1) + ); + + default: + return false; + } + } + + /** + * Converts parameters to a string. + * Sometimes parameters can have circular objects and therefor we are handle this case too. + */ + protected stringifyParams(parameters: any[]) { + try { + return JSON.stringify(parameters); + } catch (error) { + // most probably circular objects in parameters + return parameters; + } + } +} + +export default TypeOrmCustomLogger; diff --git a/packages/mysql-typeorm/src/data-source.ts b/packages/mysql-typeorm/src/data-source.ts new file mode 100644 index 0000000..8dc13e4 --- /dev/null +++ b/packages/mysql-typeorm/src/data-source.ts @@ -0,0 +1,41 @@ +import 'reflect-metadata'; +import { DataSource, DataSourceOptions } from 'typeorm'; +import { SeederOptions } from 'typeorm-extension'; + +export const dataSource = new DataSource({ + type: process.env.DATABASE_TYPE, + url: process.env.DATABASE_URL, + host: process.env.DATABASE_HOST, + port: process.env.DATABASE_PORT + ? parseInt(process.env.DATABASE_PORT, 10) + : 5432, + username: process.env.DATABASE_USERNAME, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + synchronize: process.env.DATABASE_SYNCHRONIZE === 'true', + dropSchema: false, + keepConnectionAlive: true, + logging: process.env.NODE_ENV !== 'production', + entities: [ + __dirname + '/dist/**/*.entity{.js}', // this configuration is for typeorm-extension seeder + __dirname + '/entities/**/*.entity{.ts,.js}', // this configuration is for tyeporm migration + ], + migrations: [__dirname + '/migrations/**/*{.ts,.js}'], + migrationsTableName: 'migrations', + poolSize: process.env.DATABASE_MAX_CONNECTIONS + ? parseInt(process.env.DATABASE_MAX_CONNECTIONS, 10) + : 100, + ssl: + process.env.DATABASE_SSL_ENABLED === 'true' + ? { + rejectUnauthorized: + process.env.DATABASE_REJECT_UNAUTHORIZED === 'true', + ca: process.env.DATABASE_CA ?? undefined, + key: process.env.DATABASE_KEY ?? undefined, + cert: process.env.DATABASE_CERT ?? undefined, + } + : undefined, + seeds: [__dirname + '/seeds/**/*{.ts,.js}'], + seedTracking: true, + factories: [__dirname + '/factories/**/*{.ts,.js}'], +} as DataSourceOptions & SeederOptions); diff --git a/packages/mysql-typeorm/src/decorators/order.decorator.ts b/packages/mysql-typeorm/src/decorators/order.decorator.ts new file mode 100644 index 0000000..d6e1bda --- /dev/null +++ b/packages/mysql-typeorm/src/decorators/order.decorator.ts @@ -0,0 +1,24 @@ +const ORDER_KEY = Symbol.for('order_key'); + +export function Order(value: number): PropertyDecorator { + return (target, propertyKey) => { + Reflect.defineMetadata(ORDER_KEY, value, target, propertyKey); + }; +} + +export function getOrder( + target: unknown, + propertyKey: string | symbol, + defaultVal = 0, +) { + if (typeof target !== 'object' || target === null) { + return defaultVal; + } + + const result = Reflect.getMetadata(ORDER_KEY, target, propertyKey); + if (typeof result === 'number') { + return result; + } + + return defaultVal; +} diff --git a/packages/mysql-typeorm/src/entities/abstract.entity.ts b/packages/mysql-typeorm/src/entities/abstract.entity.ts new file mode 100644 index 0000000..78ee053 --- /dev/null +++ b/packages/mysql-typeorm/src/entities/abstract.entity.ts @@ -0,0 +1,8 @@ +import { plainToInstance } from 'class-transformer'; +import { BaseEntity } from 'typeorm'; + +export abstract class AbstractEntity extends BaseEntity { + toDto(dtoClass: new () => Dto): Dto { + return plainToInstance(dtoClass, this); + } +} diff --git a/packages/mysql-typeorm/src/entities/article.entity.ts b/packages/mysql-typeorm/src/entities/article.entity.ts new file mode 100644 index 0000000..be9239c --- /dev/null +++ b/packages/mysql-typeorm/src/entities/article.entity.ts @@ -0,0 +1,91 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + type Relation, + UpdateDateColumn, +} from 'typeorm'; +import { AbstractEntity } from './abstract.entity'; +import { CommentEntity } from './comment.entity'; +import { TagEntity } from './tag.entity'; +import { UserEntity } from './user.entity'; + +@Entity('article') +export class ArticleEntity extends AbstractEntity { + constructor(data?: Partial) { + super(); + Object.assign(this, data); + } + + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_article_id' }) + id!: number; + + @Column() + @Index('UQ_article_slug', ['slug'], { unique: true }) + slug!: string; + + @Column() + title!: string; + + @Column({ default: '' }) + description!: string; + + @Column({ type: 'text' }) + body!: string; + + @Column({ name: 'author_id' }) + authorId: number; + + @ManyToOne(() => UserEntity, (user) => user.articles) + @JoinColumn({ + name: 'author_id', + referencedColumnName: 'id', + foreignKeyConstraintName: 'FK_article_user', + }) + author: UserEntity; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + // default: () => 'CURRENT_TIMESTAMP', // TODO: Have a issue with this: https://github.com/typeorm/typeorm/issues/10833 + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + // default: () => 'CURRENT_TIMESTAMP', // TODO: Have a issue with this: https://github.com/typeorm/typeorm/issues/10833 + nullable: false, + }) + updatedAt: Date; + + @ManyToMany(() => TagEntity) + @JoinTable({ + name: 'article_to_tag', + joinColumn: { + name: 'article_id', + foreignKeyConstraintName: 'FK_article_to_tag_article', + referencedColumnName: 'id', + }, + inverseJoinColumn: { + name: 'tag_id', + foreignKeyConstraintName: 'FK_article_to_tag_tag', + referencedColumnName: 'id', + }, + }) + tags: Relation; + + @OneToMany(() => CommentEntity, (comment) => comment.article) + comments: Relation; + + @ManyToMany(() => UserEntity, (user) => user.favorites) + favoritedBy: Relation; +} diff --git a/packages/mysql-typeorm/src/entities/comment.entity.ts b/packages/mysql-typeorm/src/entities/comment.entity.ts new file mode 100644 index 0000000..be664cd --- /dev/null +++ b/packages/mysql-typeorm/src/entities/comment.entity.ts @@ -0,0 +1,64 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { AbstractEntity } from './abstract.entity'; +import { ArticleEntity } from './article.entity'; +import { UserEntity } from './user.entity'; + +@Entity('comment') +export class CommentEntity extends AbstractEntity { + constructor(data?: Partial) { + super(); + Object.assign(this, data); + } + + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_comment_id' }) + id!: number; + + @Column() + body!: string; + + @Column({ name: 'article_id' }) + articleId!: number; + + @ManyToOne(() => ArticleEntity, (article) => article.comments) + @JoinColumn({ + name: 'article_id', + referencedColumnName: 'id', + foreignKeyConstraintName: 'FK_comment_article', + }) + article: ArticleEntity; + + @Column({ name: 'author_id' }) + authorId!: number; + + @ManyToOne(() => UserEntity, (user) => user.comments) + @JoinColumn({ + name: 'author_id', + referencedColumnName: 'id', + foreignKeyConstraintName: 'FK_comment_user', + }) + author: UserEntity; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + // default: () => 'CURRENT_TIMESTAMP', // TODO: Have a issue with this: https://github.com/typeorm/typeorm/issues/10833 + nullable: false, + }) + createdAt: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + // default: () => 'CURRENT_TIMESTAMP', // TODO: Have a issue with this: https://github.com/typeorm/typeorm/issues/10833 + nullable: false, + }) + updatedAt: Date; +} diff --git a/packages/mysql-typeorm/src/entities/index.ts b/packages/mysql-typeorm/src/entities/index.ts new file mode 100644 index 0000000..14364db --- /dev/null +++ b/packages/mysql-typeorm/src/entities/index.ts @@ -0,0 +1,5 @@ +export * from './article.entity'; +export * from './comment.entity'; +export * from './tag.entity'; +export * from './user-follows.entity'; +export * from './user.entity'; diff --git a/packages/mysql-typeorm/src/entities/tag.entity.ts b/packages/mysql-typeorm/src/entities/tag.entity.ts new file mode 100644 index 0000000..6c6274e --- /dev/null +++ b/packages/mysql-typeorm/src/entities/tag.entity.ts @@ -0,0 +1,12 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +import { AbstractEntity } from './abstract.entity'; + +@Entity('tag') +export class TagEntity extends AbstractEntity { + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_tag_id' }) + id!: number; + + @Column() + @Index('UQ_tag_name', ['name'], { unique: true }) + name!: string; +} diff --git a/packages/mysql-typeorm/src/entities/user-follows.entity.ts b/packages/mysql-typeorm/src/entities/user-follows.entity.ts new file mode 100644 index 0000000..1a152ca --- /dev/null +++ b/packages/mysql-typeorm/src/entities/user-follows.entity.ts @@ -0,0 +1,52 @@ +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { AbstractEntity } from './abstract.entity'; +import { UserEntity } from './user.entity'; + +@Entity('user_follows') +@Index( + 'UQ_user_follows_follower_id_followee_id', + ['followerId', 'followeeId'], + { + unique: true, + }, +) +export class UserFollowsEntity extends AbstractEntity { + constructor(data?: Partial) { + super(); + Object.assign(this, data); + } + + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_user_follows_id' }) + id: number; + + @Column({ name: 'follower_id' }) + @Index('UQ_user_follows_follower_id', ['followerId']) + followerId: number; + + @Column({ name: 'followee_id' }) + @Index('UQ_user_follows_followee_id', ['followeeId']) + followeeId: number; + + @ManyToOne(() => UserEntity, (user) => user.following) + @JoinColumn({ + name: 'follower_id', + referencedColumnName: 'id', + foreignKeyConstraintName: 'FK_user_follows_follower_id', + }) + follower: UserEntity; + + @ManyToOne(() => UserEntity, (user) => user.followers) + @JoinColumn({ + name: 'followee_id', + referencedColumnName: 'id', + foreignKeyConstraintName: 'FK_user_follows_followee_id', + }) + followee: UserEntity; +} diff --git a/packages/mysql-typeorm/src/entities/user.entity.ts b/packages/mysql-typeorm/src/entities/user.entity.ts new file mode 100644 index 0000000..e4597e3 --- /dev/null +++ b/packages/mysql-typeorm/src/entities/user.entity.ts @@ -0,0 +1,76 @@ +import { hashPassword as hashPass } from '@repo/nest-common'; +import { + BeforeInsert, + BeforeUpdate, + Column, + Entity, + Index, + JoinTable, + ManyToMany, + OneToMany, + PrimaryGeneratedColumn, + type Relation, +} from 'typeorm'; +import { AbstractEntity } from './abstract.entity'; +import { ArticleEntity } from './article.entity'; +import { CommentEntity } from './comment.entity'; +import { UserFollowsEntity } from './user-follows.entity'; + +@Entity('user') +export class UserEntity extends AbstractEntity { + @PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_user_id' }) + id!: number; + + @Column() + @Index('UQ_user_username', ['username'], { unique: true }) + username!: string; + + @Column() + @Index('UQ_user_email', ['email'], { unique: true }) + email!: string; + + @Column() + password!: string; + + @Column({ default: '' }) + image!: string; + + @Column({ default: '' }) + bio!: string; + + @BeforeInsert() + @BeforeUpdate() + async hashPassword() { + if (this.password) { + this.password = await hashPass(this.password); + } + } + + @OneToMany(() => ArticleEntity, (article) => article.author) + articles: Relation; + + @OneToMany(() => CommentEntity, (comment) => comment.author) + comments: Relation; + + @ManyToMany(() => ArticleEntity, (article) => article.favoritedBy) + @JoinTable({ + name: 'user_favorites', + joinColumn: { + name: 'user_id', + referencedColumnName: 'id', + foreignKeyConstraintName: 'FK_user_favorites_user', + }, + inverseJoinColumn: { + name: 'article_id', + referencedColumnName: 'id', + foreignKeyConstraintName: 'FK_user_favorites_article', + }, + }) + favorites: Relation; + + @OneToMany(() => UserFollowsEntity, (userFollow) => userFollow.follower) + following: Relation; + + @OneToMany(() => UserFollowsEntity, (userFollow) => userFollow.followee) + followers: Relation; +} diff --git a/packages/mysql-typeorm/src/factories/article.factory.ts b/packages/mysql-typeorm/src/factories/article.factory.ts new file mode 100644 index 0000000..f9357c4 --- /dev/null +++ b/packages/mysql-typeorm/src/factories/article.factory.ts @@ -0,0 +1,14 @@ +import { setSeederFactory } from 'typeorm-extension'; +import { ArticleEntity } from '../entities'; + +export default setSeederFactory(ArticleEntity, (fake) => { + const article = new ArticleEntity(); + + article.title = fake.lorem.sentence(); + article.slug = fake.lorem.slug(); + article.description = fake.lorem.sentence(); + article.body = fake.lorem.paragraphs(10); + article.authorId = 1; + + return article; +}); diff --git a/packages/mysql-typeorm/src/factories/comment.factory.ts b/packages/mysql-typeorm/src/factories/comment.factory.ts new file mode 100644 index 0000000..3fb178d --- /dev/null +++ b/packages/mysql-typeorm/src/factories/comment.factory.ts @@ -0,0 +1,12 @@ +import { setSeederFactory } from 'typeorm-extension'; +import { CommentEntity } from '../entities'; + +export default setSeederFactory(CommentEntity, (fake) => { + const comment = new CommentEntity(); + + comment.body = fake.lorem.paragraphs(1); + comment.articleId = 1; + comment.authorId = 1; + + return comment; +}); diff --git a/packages/mysql-typeorm/src/factories/tag.factory.ts b/packages/mysql-typeorm/src/factories/tag.factory.ts new file mode 100644 index 0000000..eddbbeb --- /dev/null +++ b/packages/mysql-typeorm/src/factories/tag.factory.ts @@ -0,0 +1,14 @@ +import { setSeederFactory } from 'typeorm-extension'; +import { TagEntity } from '../entities'; + +export default setSeederFactory(TagEntity, async (fake) => { + const tag = new TagEntity(); + + let uniqueName: string; + do { + uniqueName = fake.lorem.words({ min: 1, max: 4 }); + } while (await TagEntity.findOneBy({ name: uniqueName })); + tag.name = uniqueName; + + return tag; +}); diff --git a/packages/mysql-typeorm/src/factories/user.factory.ts b/packages/mysql-typeorm/src/factories/user.factory.ts new file mode 100644 index 0000000..27e05e6 --- /dev/null +++ b/packages/mysql-typeorm/src/factories/user.factory.ts @@ -0,0 +1,16 @@ +import { setSeederFactory } from 'typeorm-extension'; +import { UserEntity } from '../entities'; + +export default setSeederFactory(UserEntity, (fake) => { + const user = new UserEntity(); + + const firstName = fake.person.firstName(); + const lastName = fake.person.lastName(); + user.username = `${firstName.toLowerCase()}${lastName.toLowerCase()}`; + user.email = fake.internet.email({ firstName, lastName }); + user.password = '12345678'; + user.bio = fake.lorem.sentence(); + user.image = fake.image.avatar(); + + return user; +}); diff --git a/packages/mysql-typeorm/src/index.ts b/packages/mysql-typeorm/src/index.ts new file mode 100644 index 0000000..6e40329 --- /dev/null +++ b/packages/mysql-typeorm/src/index.ts @@ -0,0 +1,2 @@ +export * from './config'; +export * from './entities'; diff --git a/packages/mysql-typeorm/src/migrations/1730189454962-create-user-table.ts b/packages/mysql-typeorm/src/migrations/1730189454962-create-user-table.ts new file mode 100644 index 0000000..b562aea --- /dev/null +++ b/packages/mysql-typeorm/src/migrations/1730189454962-create-user-table.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateUserTable1730189454962 implements MigrationInterface { + name = 'CreateUserTable1730189454962'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE \`user\` ( + \`id\` int NOT NULL AUTO_INCREMENT, + \`username\` varchar(255) NOT NULL, + \`email\` varchar(255) NOT NULL, + \`password\` varchar(255) NOT NULL, + \`image\` varchar(255) NOT NULL DEFAULT '', + \`bio\` varchar(255) NOT NULL DEFAULT '', + UNIQUE INDEX \`UQ_user_username\` (\`username\`), + UNIQUE INDEX \`UQ_user_email\` (\`email\`), + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX \`UQ_user_email\` ON \`user\` + `); + await queryRunner.query(` + DROP INDEX \`UQ_user_username\` ON \`user\` + `); + await queryRunner.query(` + DROP TABLE \`user\` + `); + } +} diff --git a/packages/mysql-typeorm/src/migrations/1730193511880-create-tag-table.ts b/packages/mysql-typeorm/src/migrations/1730193511880-create-tag-table.ts new file mode 100644 index 0000000..5cdb704 --- /dev/null +++ b/packages/mysql-typeorm/src/migrations/1730193511880-create-tag-table.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateTagTable1730193511880 implements MigrationInterface { + name = 'CreateTagTable1730193511880'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE \`tag\` ( + \`id\` int NOT NULL AUTO_INCREMENT, + \`name\` varchar(255) NOT NULL, + UNIQUE INDEX \`UQ_tag_name\` (\`name\`), + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX \`UQ_tag_name\` ON \`tag\` + `); + await queryRunner.query(` + DROP TABLE \`tag\` + `); + } +} diff --git a/packages/mysql-typeorm/src/migrations/1730193570087-create-article-table.ts b/packages/mysql-typeorm/src/migrations/1730193570087-create-article-table.ts new file mode 100644 index 0000000..589aa40 --- /dev/null +++ b/packages/mysql-typeorm/src/migrations/1730193570087-create-article-table.ts @@ -0,0 +1,38 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateArticleTable1730193570087 implements MigrationInterface { + name = 'CreateArticleTable1730193570087'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE \`article\` ( + \`id\` int NOT NULL AUTO_INCREMENT, + \`slug\` varchar(255) NOT NULL, + \`title\` varchar(255) NOT NULL, + \`description\` varchar(255) NOT NULL DEFAULT '', + \`body\` text NOT NULL, + \`author_id\` int NOT NULL, + \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + UNIQUE INDEX \`UQ_article_slug\` (\`slug\`), + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `); + await queryRunner.query(` + ALTER TABLE \`article\` + ADD CONSTRAINT \`FK_article_user\` FOREIGN KEY (\`author_id\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`article\` DROP FOREIGN KEY \`FK_article_user\` + `); + await queryRunner.query(` + DROP INDEX \`UQ_article_slug\` ON \`article\` + `); + await queryRunner.query(` + DROP TABLE \`article\` + `); + } +} diff --git a/packages/mysql-typeorm/src/migrations/1730195822280-create-article-to-tag-table.ts b/packages/mysql-typeorm/src/migrations/1730195822280-create-article-to-tag-table.ts new file mode 100644 index 0000000..571c099 --- /dev/null +++ b/packages/mysql-typeorm/src/migrations/1730195822280-create-article-to-tag-table.ts @@ -0,0 +1,45 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateArticleToTagTable1730195822280 + implements MigrationInterface +{ + name = 'CreateArticleToTagTable1730195822280'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE \`article_to_tag\` ( + \`article_id\` int NOT NULL, + \`tag_id\` int NOT NULL, + INDEX \`IDX_fd50220e818ef33364f75af495\` (\`article_id\`), + INDEX \`IDX_991d528d94da3e1b66444208ed\` (\`tag_id\`), + PRIMARY KEY (\`article_id\`, \`tag_id\`) + ) ENGINE = InnoDB + `); + await queryRunner.query(` + ALTER TABLE \`article_to_tag\` + ADD CONSTRAINT \`FK_article_to_tag_article\` FOREIGN KEY (\`article_id\`) REFERENCES \`article\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE + `); + await queryRunner.query(` + ALTER TABLE \`article_to_tag\` + ADD CONSTRAINT \`FK_article_to_tag_tag\` FOREIGN KEY (\`tag_id\`) REFERENCES \`tag\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`article_to_tag\` DROP FOREIGN KEY \`FK_article_to_tag_tag\` + `); + await queryRunner.query(` + ALTER TABLE \`article_to_tag\` DROP FOREIGN KEY \`FK_article_to_tag_article\` + `); + await queryRunner.query(` + DROP INDEX \`IDX_991d528d94da3e1b66444208ed\` ON \`article_to_tag\` + `); + await queryRunner.query(` + DROP INDEX \`IDX_fd50220e818ef33364f75af495\` ON \`article_to_tag\` + `); + await queryRunner.query(` + DROP TABLE \`article_to_tag\` + `); + } +} diff --git a/packages/mysql-typeorm/src/migrations/1730196400597-create-user-favorites-table.ts b/packages/mysql-typeorm/src/migrations/1730196400597-create-user-favorites-table.ts new file mode 100644 index 0000000..675439b --- /dev/null +++ b/packages/mysql-typeorm/src/migrations/1730196400597-create-user-favorites-table.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateUserFavoritesTable1730196400597 + implements MigrationInterface +{ + name = 'CreateUserFavoritesTable1730196400597'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE \`user_favorites\` ( + \`user_id\` int NOT NULL, + \`article_id\` int NOT NULL, + INDEX \`IDX_5238ce0a21cc77dc16c8efe3d3\` (\`user_id\`), + INDEX \`IDX_57c7c9e22aad40815268f28b5f\` (\`article_id\`), + PRIMARY KEY (\`user_id\`, \`article_id\`) + ) ENGINE = InnoDB + `); + + await queryRunner.query(` + ALTER TABLE \`user_favorites\` + ADD CONSTRAINT \`FK_user_favorites_user\` FOREIGN KEY (\`user_id\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE + `); + await queryRunner.query(` + ALTER TABLE \`user_favorites\` + ADD CONSTRAINT \`FK_user_favorites_article\` FOREIGN KEY (\`article_id\`) REFERENCES \`article\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`user_favorites\` DROP FOREIGN KEY \`FK_user_favorites_article\` + `); + await queryRunner.query(` + ALTER TABLE \`user_favorites\` DROP FOREIGN KEY \`FK_user_favorites_user\` + `); + + await queryRunner.query(` + DROP INDEX \`IDX_57c7c9e22aad40815268f28b5f\` ON \`user_favorites\` + `); + await queryRunner.query(` + DROP INDEX \`IDX_5238ce0a21cc77dc16c8efe3d3\` ON \`user_favorites\` + `); + await queryRunner.query(` + DROP TABLE \`user_favorites\` + `); + } +} diff --git a/packages/mysql-typeorm/src/migrations/1730196525915-create-user-follows-table.ts b/packages/mysql-typeorm/src/migrations/1730196525915-create-user-follows-table.ts new file mode 100644 index 0000000..e96abec --- /dev/null +++ b/packages/mysql-typeorm/src/migrations/1730196525915-create-user-follows-table.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateUserFollowsTable1730196525915 implements MigrationInterface { + name = 'CreateUserFollowsTable1730196525915'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE \`user_follows\` ( + \`id\` int NOT NULL AUTO_INCREMENT, + \`follower_id\` int NOT NULL, + \`followee_id\` int NOT NULL, + INDEX \`UQ_user_follows_follower_id\` (\`follower_id\`), + INDEX \`UQ_user_follows_followee_id\` (\`followee_id\`), + UNIQUE INDEX \`UQ_user_follows_follower_id_followee_id\` (\`follower_id\`, \`followee_id\`), + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `); + await queryRunner.query(` + ALTER TABLE \`user_follows\` + ADD CONSTRAINT \`FK_user_follows_follower_id\` FOREIGN KEY (\`follower_id\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + await queryRunner.query(` + ALTER TABLE \`user_follows\` + ADD CONSTRAINT \`FK_user_follows_followee_id\` FOREIGN KEY (\`followee_id\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`user_follows\` DROP FOREIGN KEY \`FK_user_follows_followee_id\` + `); + await queryRunner.query(` + ALTER TABLE \`user_follows\` DROP FOREIGN KEY \`FK_user_follows_follower_id\` + `); + await queryRunner.query(` + DROP INDEX \`UQ_user_follows_follower_id_followee_id\` ON \`user_follows\` + `); + await queryRunner.query(` + DROP INDEX \`UQ_user_follows_followee_id\` ON \`user_follows\` + `); + await queryRunner.query(` + DROP INDEX \`UQ_user_follows_follower_id\` ON \`user_follows\` + `); + await queryRunner.query(` + DROP TABLE \`user_follows\` + `); + } +} diff --git a/packages/mysql-typeorm/src/migrations/1730198033817-create-comment-table.ts b/packages/mysql-typeorm/src/migrations/1730198033817-create-comment-table.ts new file mode 100644 index 0000000..fced19f --- /dev/null +++ b/packages/mysql-typeorm/src/migrations/1730198033817-create-comment-table.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateCommentTable1730198033817 implements MigrationInterface { + name = 'CreateCommentTable1730198033817'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE \`comment\` ( + \`id\` int NOT NULL AUTO_INCREMENT, + \`body\` varchar(255) NOT NULL, + \`article_id\` int NOT NULL, + \`author_id\` int NOT NULL, + \`created_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updated_at\` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `); + await queryRunner.query(` + ALTER TABLE \`comment\` + ADD CONSTRAINT \`FK_comment_article\` FOREIGN KEY (\`article_id\`) REFERENCES \`article\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + await queryRunner.query(` + ALTER TABLE \`comment\` + ADD CONSTRAINT \`FK_comment_user\` FOREIGN KEY (\`author_id\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`comment\` DROP FOREIGN KEY \`FK_comment_user\` + `); + await queryRunner.query(` + ALTER TABLE \`comment\` DROP FOREIGN KEY \`FK_comment_article\` + `); + await queryRunner.query(` + DROP TABLE \`comment\` + `); + } +} diff --git a/packages/mysql-typeorm/src/seeds/1732019848273-user-seeder.ts b/packages/mysql-typeorm/src/seeds/1732019848273-user-seeder.ts new file mode 100644 index 0000000..dcd05c5 --- /dev/null +++ b/packages/mysql-typeorm/src/seeds/1732019848273-user-seeder.ts @@ -0,0 +1,26 @@ +import { DataSource } from 'typeorm'; +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { UserEntity } from '../entities'; + +export class UserSeeder1732019848273 implements Seeder { + track = false; + + public async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager, + ): Promise { + const repository = dataSource.getRepository(UserEntity); + const userFactory = factoryManager.get(UserEntity); + + const adminUser = await repository.findOneBy({ username: 'admin' }); + if (!adminUser) { + const user = await userFactory.make({ + username: 'admin', + email: 'admin@example.com', + }); + await repository.insert(user); + } + + await userFactory.saveMany(10); + } +} diff --git a/packages/mysql-typeorm/src/seeds/1732028144623-tag-seeder.ts b/packages/mysql-typeorm/src/seeds/1732028144623-tag-seeder.ts new file mode 100644 index 0000000..a167afc --- /dev/null +++ b/packages/mysql-typeorm/src/seeds/1732028144623-tag-seeder.ts @@ -0,0 +1,15 @@ +import { DataSource } from 'typeorm'; +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { TagEntity } from '../entities'; + +export class TagSeeder1732028144623 implements Seeder { + track = false; + + public async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager, + ): Promise { + const tagFactory = factoryManager.get(TagEntity); + await tagFactory.saveMany(10); + } +} diff --git a/packages/mysql-typeorm/src/seeds/1732028230352-article-seeder.ts b/packages/mysql-typeorm/src/seeds/1732028230352-article-seeder.ts new file mode 100644 index 0000000..523c4db --- /dev/null +++ b/packages/mysql-typeorm/src/seeds/1732028230352-article-seeder.ts @@ -0,0 +1,44 @@ +import { getRandomInt } from '@repo/nest-common'; +import { DataSource } from 'typeorm'; +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { ArticleEntity, TagEntity, UserEntity } from '../entities'; + +export class ArticleSeeder1732028230352 implements Seeder { + track = false; + + public async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager, + ): Promise { + // Get random users + const userRepository = dataSource.getRepository(UserEntity); + const numberOfUsers = await userRepository.count(); + const randomOffset = getRandomInt(0, numberOfUsers - 1); + + const users = await userRepository + .createQueryBuilder('user') + .skip(randomOffset) + .take(10) + .getMany(); + + // Get random tags + const tagRepository = dataSource.getRepository(TagEntity); + const numberOfTags = await tagRepository.count(); + const randomTagOffset = getRandomInt(0, numberOfTags - 1); + + const tags = await tagRepository + .createQueryBuilder('tag') + .skip(randomTagOffset) + .take(10) + .getMany(); + + const articleFactory = factoryManager.get(ArticleEntity); + for (const user of users) { + const randomTagNumber = getRandomInt(0, tags.length - 1); + await articleFactory.saveMany(5, { + authorId: user.id, + tags: tags.slice(0, randomTagNumber).slice(0, 5), + }); + } + } +} diff --git a/packages/mysql-typeorm/src/seeds/1732031567099-comment-seeder.ts b/packages/mysql-typeorm/src/seeds/1732031567099-comment-seeder.ts new file mode 100644 index 0000000..c6b66b4 --- /dev/null +++ b/packages/mysql-typeorm/src/seeds/1732031567099-comment-seeder.ts @@ -0,0 +1,44 @@ +import { getRandomInt } from '@repo/nest-common'; +import { DataSource } from 'typeorm'; +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { ArticleEntity, CommentEntity, UserEntity } from '../entities'; + +export class CommentSeeder1732031567099 implements Seeder { + track = false; + + public async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager, + ): Promise { + // Get random users + const userRepository = dataSource.getRepository(UserEntity); + const numberOfUsers = await userRepository.count(); + const randomOffset = getRandomInt(0, numberOfUsers - 1); + + const users = await userRepository + .createQueryBuilder('user') + .skip(randomOffset) + .take(10) + .getMany(); + + // Get random articles + const articleRepository = dataSource.getRepository(ArticleEntity); + const numberOfArticles = await articleRepository.count(); + const randomArticleOffset = getRandomInt(0, numberOfArticles - 1); + + const articles = await articleRepository + .createQueryBuilder('article') + .skip(randomArticleOffset) + .take(10) + .getMany(); + + const commentFactory = factoryManager.get(CommentEntity); + for (const user of users) { + const randomArticleNumber = getRandomInt(0, articles.length - 1); + await commentFactory.saveMany(5, { + authorId: user.id, + articleId: articles[randomArticleNumber].id, + }); + } + } +} diff --git a/packages/mysql-typeorm/src/seeds/1732032435851-user-follows-seeder.ts b/packages/mysql-typeorm/src/seeds/1732032435851-user-follows-seeder.ts new file mode 100644 index 0000000..d56be59 --- /dev/null +++ b/packages/mysql-typeorm/src/seeds/1732032435851-user-follows-seeder.ts @@ -0,0 +1,45 @@ +import { getRandomInt } from '@repo/nest-common'; +import { DataSource } from 'typeorm'; +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { UserEntity, UserFollowsEntity } from '../entities'; + +export class UserFollowsSeeder1732032435851 implements Seeder { + track = false; + + public async run( + dataSource: DataSource, + _factoryManager: SeederFactoryManager, + ): Promise { + // Get random users + const userRepository = dataSource.getRepository(UserEntity); + const numberOfUsers = await userRepository.count(); + const randomOffset = getRandomInt(0, numberOfUsers - 1); + + const users = await userRepository + .createQueryBuilder('user') + .skip(randomOffset) + .take(10) + .getMany(); + + const userFollowsRepository = dataSource.getRepository(UserFollowsEntity); + for (const user of users) { + const randomUserNumber = getRandomInt(0, users.length - 1); + const randomFoloweeId = users[randomUserNumber].id; + const followeeId = + randomFoloweeId === user.id + ? users[randomUserNumber + 1].id + : randomFoloweeId; + const isExist = await userFollowsRepository.existsBy({ + followerId: user.id, + followeeId, + }); + + if (!isExist) { + await userFollowsRepository.save({ + followerId: user.id, + followeeId, + }); + } + } + } +} diff --git a/packages/mysql-typeorm/src/seeds/1732032454792-user-favorites-seeder.ts b/packages/mysql-typeorm/src/seeds/1732032454792-user-favorites-seeder.ts new file mode 100644 index 0000000..8a44e8c --- /dev/null +++ b/packages/mysql-typeorm/src/seeds/1732032454792-user-favorites-seeder.ts @@ -0,0 +1,48 @@ +import { getRandomInt } from '@repo/nest-common'; +import { DataSource } from 'typeorm'; +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { ArticleEntity, UserEntity } from '../entities'; + +export class UserFavoritesSeeder1732032454792 implements Seeder { + track = false; + + public async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager, + ): Promise { + // Get random users + const userRepository = dataSource.getRepository(UserEntity); + const numberOfUsers = await userRepository.count(); + const randomOffset = getRandomInt(0, numberOfUsers - 1); + + const users = await userRepository + .createQueryBuilder('user') + .leftJoinAndSelect('user.favorites', 'favorites') + .skip(randomOffset) + .take(10) + .getMany(); + + // Get random articles + const articleRepository = dataSource.getRepository(ArticleEntity); + const numberOfArticles = await articleRepository.count(); + const randomArticleOffset = getRandomInt(0, numberOfArticles - 1); + + const articles = await articleRepository + .createQueryBuilder('article') + .skip(randomArticleOffset) + .take(10) + .getMany(); + + for (const user of users) { + const randomArticleNumber = getRandomInt(0, articles.length - 1); + const isExist = user.favorites.some( + (favorite) => favorite.id === articles[randomArticleNumber].id, + ); + + if (!isExist) { + user.favorites.push(articles[randomArticleNumber]); + await userRepository.save(user); + } + } + } +} diff --git a/packages/mysql-typeorm/tsconfig.json b/packages/mysql-typeorm/tsconfig.json new file mode 100644 index 0000000..f72ad89 --- /dev/null +++ b/packages/mysql-typeorm/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@repo/typescript-config/nestjs.json", + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "incremental": false, + "baseUrl": ".", + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"], + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c82da3d..cc1336d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,52 @@ importers: specifier: ^5.6.3 version: 5.6.3 + apps/docs: + dependencies: + '@repo/ui': + specifier: workspace:* + version: link:../../packages/ui + next: + specifier: 14.2.10 + version: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nextra: + specifier: ^2.13.4 + version: 2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nextra-theme-docs: + specifier: ^2.13.4 + version: 2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@repo/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/node': + specifier: ^20 + version: 20.17.6 + '@types/react': + specifier: ^18 + version: 18.3.12 + '@types/react-dom': + specifier: ^18 + version: 18.3.1 + eslint: + specifier: ^9.13.0 + version: 9.14.0(jiti@2.4.0) + eslint-config-next: + specifier: 15.0.2 + version: 15.0.2(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3) + typescript: + specifier: ^5 + version: 5.6.3 + apps/realworld-api: dependencies: '@fastify/compress': @@ -58,13 +104,13 @@ importers: version: 8.0.5(@fastify/static@7.0.4)(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^10.0.2 - version: 10.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3))) + version: 10.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3))) '@repo/api': specifier: workspace:* version: link:../../packages/api - '@repo/database-typeorm': + '@repo/mysql-typeorm': specifier: workspace:* - version: link:../../packages/database-typeorm + version: link:../../packages/mysql-typeorm '@repo/nest-common': specifier: workspace:* version: link:../../packages/nest-common @@ -94,7 +140,7 @@ importers: version: 1.6.6 typeorm: specifier: ^0.3.20 - version: 0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) + version: 0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) devDependencies: '@nestjs/cli': specifier: ^10.4.7 @@ -157,52 +203,6 @@ importers: specifier: ^5.6.3 version: 5.6.3 - apps/docs: - dependencies: - '@repo/ui': - specifier: workspace:* - version: link:../../packages/ui - next: - specifier: 14.2.10 - version: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: - specifier: ^2.13.4 - version: 2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra-theme-docs: - specifier: ^2.13.4 - version: 2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: - specifier: 18.3.1 - version: 18.3.1 - react-dom: - specifier: 18.3.1 - version: 18.3.1(react@18.3.1) - devDependencies: - '@repo/eslint-config': - specifier: workspace:* - version: link:../../packages/eslint-config - '@repo/typescript-config': - specifier: workspace:* - version: link:../../packages/typescript-config - '@types/node': - specifier: ^20 - version: 20.17.6 - '@types/react': - specifier: ^18 - version: 18.3.12 - '@types/react-dom': - specifier: ^18 - version: 18.3.1 - eslint: - specifier: ^9.13.0 - version: 9.14.0(jiti@2.4.0) - eslint-config-next: - specifier: 15.0.2 - version: 15.0.2(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3) - typescript: - specifier: ^5 - version: 5.6.3 - apps/user-api: dependencies: '@nestjs/common': @@ -405,10 +405,10 @@ importers: version: 8.13.1 typeorm: specifier: ^0.3.20 - version: 0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) + version: 0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) typeorm-extension: specifier: ^3.6.3 - version: 3.6.3(typeorm@0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3))) + version: 3.6.3(typeorm@0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3))) devDependencies: '@repo/eslint-config': specifier: workspace:* @@ -474,6 +474,55 @@ importers: specifier: ^8.14.0 version: 8.14.0(eslint@9.14.0(jiti@2.4.0))(typescript@5.6.3) + packages/mysql-typeorm: + dependencies: + '@nestjs/config': + specifier: ^3.3.0 + version: 3.3.0(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1) + '@nestjs/mapped-types': + specifier: '*' + version: 2.0.6(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + '@repo/nest-common': + specifier: workspace:* + version: link:../nest-common + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.1 + env-cmd: + specifier: ^10.1.0 + version: 10.1.0 + pg: + specifier: ^8.13.1 + version: 8.13.1 + typeorm: + specifier: ^0.3.20 + version: 0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) + typeorm-extension: + specifier: ^3.6.3 + version: 3.6.3(typeorm@0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3))) + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/node': + specifier: ^20.17.2 + version: 20.17.6 + ts-loader: + specifier: ^9.5.1 + version: 9.5.1(typescript@5.6.3)(webpack@5.96.1(@swc/core@1.9.2(@swc/helpers@0.5.15))) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3) + typescript: + specifier: ^5.6.3 + version: 5.6.3 + packages/nest-common: dependencies: '@nestjs/common': @@ -2308,6 +2357,10 @@ packages: avvio@8.4.0: resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axe-core@4.10.2: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} @@ -3009,6 +3062,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3745,6 +3802,9 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4251,6 +4311,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -4714,6 +4777,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -4747,6 +4813,10 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lru.min@1.1.1: + resolution: {integrity: sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -5082,9 +5152,17 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mysql2@3.11.4: + resolution: {integrity: sha512-Z2o3tY4Z8EvSRDwknaC40MdZ3+m0sKbpnXrShQLdxPrAvcNli7jLrD2Zd2IzsRMw4eK9Yle500FDmlkIqp+krg==} + engines: {node: '>= 8.0'} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -5963,6 +6041,9 @@ packages: sentence-case@2.1.1: resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -6124,6 +6205,10 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -8145,13 +8230,13 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.3(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7) - '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)))': + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)))': dependencies: '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(reflect-metadata@0.2.2)(rxjs@7.8.1) reflect-metadata: 0.2.2 rxjs: 7.8.1 - typeorm: 0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) + typeorm: 0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) uuid: 9.0.1 '@next/env@14.2.10': {} @@ -9152,6 +9237,9 @@ snapshots: '@fastify/error': 3.4.1 fastq: 1.17.1 + aws-ssl-profiles@1.1.2: + optional: true + axe-core@4.10.2: {} axobject-query@4.1.0: {} @@ -9976,6 +10064,9 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: + optional: true + depd@2.0.0: {} dequal@2.0.3: {} @@ -11011,6 +11102,11 @@ snapshots: functions-have-names@1.2.3: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + optional: true + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -11611,6 +11707,9 @@ snapshots: is-plain-obj@4.1.0: {} + is-property@1.0.2: + optional: true + is-reference@3.0.3: dependencies: '@types/estree': 1.0.6 @@ -12271,6 +12370,9 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + long@5.2.3: + optional: true + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -12302,6 +12404,9 @@ snapshots: lru-cache@7.18.3: {} + lru.min@1.1.1: + optional: true + magic-string@0.30.8: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -12907,12 +13012,30 @@ snapshots: mute-stream@1.0.0: {} + mysql2@3.11.4: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.2.3 + lru.min: 1.1.1 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + optional: true + mz@2.7.0: dependencies: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + optional: true + nanoid@3.3.7: {} natural-compare@1.4.0: {} @@ -13929,6 +14052,9 @@ snapshots: no-case: 2.3.2 upper-case-first: 1.1.2 + seq-queue@0.0.5: + optional: true + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -14104,6 +14230,9 @@ snapshots: sprintf-js@1.1.3: {} + sqlstring@2.3.3: + optional: true + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -14583,7 +14712,7 @@ snapshots: typedarray@0.0.6: optional: true - typeorm-extension@3.6.3(typeorm@0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3))): + typeorm-extension@3.6.3(typeorm@0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3))): dependencies: '@faker-js/faker': 8.4.1 consola: 3.2.3 @@ -14593,10 +14722,10 @@ snapshots: rapiq: 0.9.0 reflect-metadata: 0.2.2 smob: 1.5.0 - typeorm: 0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) + typeorm: 0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)) yargs: 17.7.2 - typeorm@0.3.20(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)): + typeorm@0.3.20(mysql2@3.11.4)(pg@8.13.1)(ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3)): dependencies: '@sqltools/formatter': 1.2.5 app-root-path: 3.1.0 @@ -14614,6 +14743,7 @@ snapshots: uuid: 9.0.1 yargs: 17.7.2 optionalDependencies: + mysql2: 3.11.4 pg: 8.13.1 ts-node: 10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.6)(typescript@5.6.3) transitivePeerDependencies: