diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f818211..078deef 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,10 +17,10 @@ Include any relevant background context. Select all that apply: -* [ ] Bug fix (non-breaking change which fixes an issue) -* [ ] New feature (non-breaking change which adds functionality) -* [ ] Breaking change (fix or feature that could cause existing functionality to not work as expected) -* [ ] Refactor / Chore (code cleanup, dependencies, tooling, etc.) +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that could cause existing functionality to not work as expected) +- [ ] Refactor / Chore (code cleanup, dependencies, tooling, etc.) --- @@ -39,10 +39,10 @@ If none, write: `N/A`. ## Checklist -* [ ] My code follows the project’s code style. -* [ ] I have run all linter. -* [ ] I have checked that my changes do not introduce breaking changes. -* [ ] I have commented my code where needed, especially complex logic. +- [ ] My code follows the project’s code style. +- [ ] I have run all linter. +- [ ] I have checked that my changes do not introduce breaking changes. +- [ ] I have commented my code where needed, especially complex logic. --- @@ -57,8 +57,8 @@ If none, write: `N/A`. Add any extra details for reviewers: -* Deployment notes -* New dependencies or migrations -* Performance considerations -* Known limitations or follow-ups -If none, write: `N/A`. +- Deployment notes +- New dependencies or migrations +- Performance considerations +- Known limitations or follow-ups + If none, write: `N/A`. diff --git a/.github/scripts/workflow-lint-pr/README.md b/.github/scripts/workflow-lint-pr/README.md index a573824..e05dfb9 100644 --- a/.github/scripts/workflow-lint-pr/README.md +++ b/.github/scripts/workflow-lint-pr/README.md @@ -132,8 +132,6 @@ cd packages/ui npx eslint src/ ``` - - **Note:** The workflow dynamically discovers all workspaces from `pnpm-workspace.yaml`, so you don't need to manually maintain a list. Any new workspace you add will automatically be included in linting. ## Recommended Testing Flow diff --git a/README.md b/README.md index 26723b0..e593de5 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ A **modern full-stack TypeScript monorepo** using: -* **Next.js** (Web) -* **NestJS** (API) -* **Expo** (Mobile) -* **tRPC** (End-to-end type-safe API calls) -* **PostgreSQL** or **MongoDB** (Database - your choice!) -* **Prisma** or **Mongoose** (ORM/ODM - based on your DB choice) -* **Better Auth** (Authentication with OAuth, Email OTP) -* **SonarQube** (Code quality) -* **Rollbar** (Error tracking) -* **Turborepo** (Build orchestration) +- **Next.js** (Web) +- **NestJS** (API) +- **Expo** (Mobile) +- **tRPC** (End-to-end type-safe API calls) +- **PostgreSQL** or **MongoDB** (Database - your choice!) +- **Prisma** or **Mongoose** (ORM/ODM - based on your DB choice) +- **Better Auth** (Authentication with OAuth, Email OTP) +- **SonarQube** (Code quality) +- **Rollbar** (Error tracking) +- **Turborepo** (Build orchestration) This repository is structured for scalability, developer experience, and seamless cross-platform sharing of logic and types. @@ -34,7 +34,8 @@ Make sure the following are installed **before** setup: ## πŸ—οΈ Setup Guide ### 0. Prerequisites -Before proceeding with the setup, ensure you have all the required prerequisites installed. + +Before proceeding with the setup, ensure you have all the required prerequisites installed. Missing any of these may cause the setup to fail or not work as expected. ### 1. Clone the Repository @@ -49,7 +50,9 @@ Once the repository is cloned, you can run an **automated setup script** from th ```bash npm run setup ``` + or + ```bash node setup.js ``` @@ -183,28 +186,28 @@ This command runs all apps (API, Web, and Mobile) concurrently using Turborepo. ## 🧩 Tech Stack Highlights -* ⚑ **Turborepo** – Monorepo management -* πŸ’¬ **tRPC** – End-to-end type-safe API communication -* 🧠 **Zod** – Runtime validation & schema definition -* πŸ—„οΈ **Database Options** – Choose between: - * **PostgreSQL + Prisma** – Relational database with type-safe ORM - * **MongoDB + Mongoose** – NoSQL database with ODM -* πŸ” **Better Auth** – Modern authentication framework with: - * Email OTP authentication - * Google OAuth - * Expo mobile support - * Email verification -* πŸ’» **Next.js** – Web frontend -* πŸ“± **Expo (React Native)** – Mobile app -* 🧱 **NestJS** – Backend API -* 🧩 **Shared Packages** – Centralized types & logic +- ⚑ **Turborepo** – Monorepo management +- πŸ’¬ **tRPC** – End-to-end type-safe API communication +- 🧠 **Zod** – Runtime validation & schema definition +- πŸ—„οΈ **Database Options** – Choose between: + - **PostgreSQL + Prisma** – Relational database with type-safe ORM + - **MongoDB + Mongoose** – NoSQL database with ODM +- πŸ” **Better Auth** – Modern authentication framework with: + - Email OTP authentication + - Google OAuth + - Expo mobile support + - Email verification +- πŸ’» **Next.js** – Web frontend +- πŸ“± **Expo (React Native)** – Mobile app +- 🧱 **NestJS** – Backend API +- 🧩 **Shared Packages** – Centralized types & logic --- ## πŸ§‘β€πŸ’» Development Notes -* Keep Docker running while developing backend/API. -* Use `pnpm` consistently β€” **do not use npm or yarn**. -* Better Auth requires proper environment variables for OAuth providers. +- Keep Docker running while developing backend/API. +- Use `pnpm` consistently β€” **do not use npm or yarn**. +- Better Auth requires proper environment variables for OAuth providers. --- diff --git a/apps/api/.env.example b/apps/api/.env.example index ceb93de..6f8d86d 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -22,7 +22,8 @@ MONGODB_CONTAINER_NAME=app-mongodb MONGODB_PORT=27017 MONGODB_USER=admin MONGODB_PASSWORD=admin123 -DATABASE_URL_MONGODB=mongodb://admin:admin123@localhost:27017/appdb?authSource=admin +MONGODB_REPLICA_SET_KEY=your_key_here +DATABASE_URL_MONGODB=mongodb://admin:admin123@localhost:27017/appdb?authSource=admin&replicaSet=rs0&directConnection=true # Mongo Express MONGO_EXPRESS_CONTAINER_NAME=app-mongo-express diff --git a/apps/api/docker-compose.mongo.yml b/apps/api/docker-compose.mongo.yml index 34b42bb..3987495 100644 --- a/apps/api/docker-compose.mongo.yml +++ b/apps/api/docker-compose.mongo.yml @@ -1,31 +1,34 @@ name: app-mongo-db services: mongodb: - image: mongodb/mongodb-community-server:latest + image: bitnami/mongodb:latest container_name: ${MONGODB_CONTAINER_NAME} restart: unless-stopped environment: - MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USER} - MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD} + MONGODB_REPLICA_SET_MODE: primary + MONGODB_REPLICA_SET_NAME: rs0 + MONGODB_REPLICA_SET_KEY: ${MONGODB_REPLICA_SET_KEY} + MONGODB_ROOT_USER: ${MONGODB_USER} + MONGODB_ROOT_PASSWORD: ${MONGODB_PASSWORD} + MONGODB_ADVERTISED_HOSTNAME: mongodb ports: - "${MONGODB_PORT}:27017" volumes: - - mongodb_data:/data/db + - mongodb_data:/bitnami/mongodb healthcheck: - test: mongosh -u ${MONGODB_USER} -p ${MONGODB_PASSWORD} --authenticationDatabase admin --quiet --eval "db.runCommand('ping').ok" + test: > + bash -c "mongosh -u ${MONGODB_USER} -p ${MONGODB_PASSWORD} --authenticationDatabase admin --quiet --eval 'rs.status().ok' | grep 1" interval: 10s timeout: 5s - retries: 5 - start_period: 20s + retries: 30 + start_period: 10s mongo-express: image: mongo-express:latest container_name: ${MONGO_EXPRESS_CONTAINER_NAME} restart: unless-stopped environment: - ME_CONFIG_MONGODB_SERVER: mongodb - ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGODB_USER} - ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGODB_PASSWORD} + ME_CONFIG_MONGODB_URL: "mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@mongodb:27017/?authSource=admin&replicaSet=rs0" ME_CONFIG_BASICAUTH_USERNAME: ${MONGO_EXPRESS_USER} ME_CONFIG_BASICAUTH_PASSWORD: ${MONGO_EXPRESS_PASSWORD} ports: diff --git a/apps/api/eslint-rules/plugin.mjs b/apps/api/eslint-rules/plugin.mjs new file mode 100644 index 0000000..375bdf1 --- /dev/null +++ b/apps/api/eslint-rules/plugin.mjs @@ -0,0 +1,7 @@ +import { requireTransaction } from './require-transaction.mjs'; + +export const plugin = { + rules: { + 'require-transactional': requireTransaction, + }, +}; diff --git a/apps/api/eslint-rules/require-transaction.mjs b/apps/api/eslint-rules/require-transaction.mjs new file mode 100644 index 0000000..a2d66a1 --- /dev/null +++ b/apps/api/eslint-rules/require-transaction.mjs @@ -0,0 +1,38 @@ +export const requireTransaction = { + meta: { + type: 'problem', + docs: { + description: + 'Warn if a service class is missing @AutoTransaction decorator', + recommended: 'warn', + }, + messages: { + missingClassAutoTransaction: + "Service class '{{name}}' is missing @AutoTransaction decorator.", + }, + schema: [], + }, + create(context) { + return { + ClassDeclaration(node) { + const className = node.id?.name || ''; + if (!className.endsWith('Service')) return; + + const decorators = node.decorators || []; + const hasTransactional = decorators.some( + (d) => + d.expression?.callee?.name === 'AutoTransaction' || + d.expression?.name === 'AutoTransaction', + ); + + if (!hasTransactional) { + context.report({ + node, + messageId: 'missingClassAutoTransaction', + data: { name: className }, + }); + } + }, + }; + }, +}; diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs index 4f0adc0..43d7411 100644 --- a/apps/api/eslint.config.mjs +++ b/apps/api/eslint.config.mjs @@ -1,11 +1,12 @@ // @ts-check -import { config as baseConfig } from "@repo/eslint-config/base"; -import tseslint from "typescript-eslint"; +import { config as baseConfig } from '@repo/eslint-config/base'; +import tseslint from 'typescript-eslint'; +import { plugin as customPlugin } from './eslint-rules/plugin.mjs'; export default tseslint.config( ...baseConfig, { - ignores: ["eslint.config.mjs", "src/generated/**"], + ignores: ['eslint.config.mjs', 'src/generated/**', 'eslint-rules/**'], }, { languageOptions: { @@ -14,5 +15,11 @@ export default tseslint.config( tsconfigRootDir: import.meta.dirname, }, }, - } -); \ No newline at end of file + plugins: { + custom: customPlugin, + }, + rules: { + 'custom/require-transactional': 'warn', + }, + }, +); diff --git a/apps/api/package.json b/apps/api/package.json index 60b840c..bd442ba 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -18,11 +18,16 @@ "test:e2e": "jest --config ./test/jest-e2e.json", "db:seed:mongo": "dotenvx run -- ts-node -r tsconfig-paths/register scripts/seed/seed-mongoose.ts", "db:seed:prisma": "cd ../../packages/prisma-db && pnpm run db:seed", - "db:seed:all": "pnpm run db:seed:prisma && pnpm run db:seed:mongo" + "db:seed:all": "pnpm run db:seed:prisma && pnpm run db:seed:mongo", + "generate:repo:prisma": "ts-node -r tsconfig-paths/register scripts/code-generation/prisma/repositories/generate-repository.ts", + "generate:repo:mongo": "ts-node -r tsconfig-paths/register scripts/code-generation/mongoose/repositories/generate-repository.ts" }, "dependencies": { "@andeanwide/nestjs-rollbar": "^1.0.0", "@better-auth/expo": "1.3.34", + "@nestjs-cls/transactional": "^3.1.1", + "@nestjs-cls/transactional-adapter-mongoose": "^1.1.25", + "@nestjs-cls/transactional-adapter-prisma": "^1.3.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -35,6 +40,7 @@ "@thallesp/nestjs-better-auth": "^2.1.0", "better-auth": "1.3.34", "mongoose": "^9.0.0", + "nestjs-cls": "^6.1.0", "nestjs-trpc": "^1.6.1", "reflect-metadata": "^0.2.2", "resend": "^6.4.2", @@ -42,13 +48,13 @@ "zod": "3.25.5" }, "devDependencies": { - "@repo/db-seeder": "workspace:*", "@better-auth/cli": "1.3.34", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", + "@repo/db-seeder": "workspace:*", "@repo/eslint-config": "workspace:*", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", diff --git a/apps/api/scripts/code-generation/mongoose/repositories/generate-repository.ts b/apps/api/scripts/code-generation/mongoose/repositories/generate-repository.ts new file mode 100644 index 0000000..7cb3933 --- /dev/null +++ b/apps/api/scripts/code-generation/mongoose/repositories/generate-repository.ts @@ -0,0 +1,98 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +interface GeneratorConfig { + entityName: string; +} + +export class RepositoryGenerator { + private readonly entityName: string; + private readonly entityNameCapitalized: string; + private readonly outputDir: string; + private readonly templatesDir: string; + + constructor(config: GeneratorConfig) { + this.entityName = config.entityName.toLowerCase(); + this.entityNameCapitalized = + this.entityName.charAt(0).toUpperCase() + this.entityName.slice(1); + this.outputDir = path.join( + __dirname, + '../../../../src/modules', + this.entityName, + 'repositories/mongoose', + ); + this.templatesDir = path.join(__dirname, 'templates'); + } + + generate(): void { + if (!fs.existsSync(this.outputDir)) { + fs.mkdirSync(this.outputDir, { recursive: true }); + } + + this.generateEntity(); + this.generateInterface(); + this.generateRepository(); + + console.log( + `βœ… Generated repository for ${this.entityNameCapitalized} in ${this.outputDir}`, + ); + } + + private generateEntity(): void { + const content = this.loadTemplate('entity.template.txt'); + const filePath = path.join( + this.outputDir, + `${this.entityName}.mongoose-entity.ts`, + ); + fs.writeFileSync(filePath, content, 'utf-8'); + } + + private generateInterface(): void { + const content = this.loadTemplate('interface.template.txt'); + const filePath = path.join( + this.outputDir, + `${this.entityName}.mongoose-repository.interface.ts`, + ); + fs.writeFileSync(filePath, content, 'utf-8'); + } + + private generateRepository(): void { + const content = this.loadTemplate('repository.template.txt'); + const filePath = path.join( + this.outputDir, + `${this.entityName}.mongoose-repository.ts`, + ); + fs.writeFileSync(filePath, content, 'utf-8'); + } + + private loadTemplate(templateName: string): string { + const templatePath = path.join(this.templatesDir, templateName); + let template = fs.readFileSync(templatePath, 'utf-8'); + + // Replace placeholders + template = template.replace( + /{{ENTITY_NAME_CAPITALIZED}}/g, + this.entityNameCapitalized, + ); + template = template.replace(/{{ENTITY_NAME_LOWER}}/g, this.entityName); + + return template; + } +} + +if (require.main === module) { + const args = process.argv.slice(2); + const entityName = args[0]; + + if (!entityName) { + console.error('❌ Usage: pnpm run generate:repo:mongo '); + console.error(' Example: pnpm run generate:repo:mongo crud'); + process.exit(1); + } + + const generator = new RepositoryGenerator({ + entityName, + }); + + generator.generate(); +} diff --git a/apps/api/scripts/code-generation/mongoose/repositories/templates/entity.template.txt b/apps/api/scripts/code-generation/mongoose/repositories/templates/entity.template.txt new file mode 100644 index 0000000..b790686 --- /dev/null +++ b/apps/api/scripts/code-generation/mongoose/repositories/templates/entity.template.txt @@ -0,0 +1,8 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { MongooseBaseEntity } from '../../../../repositories/mongoose/mongoose.base-entity'; + +@Schema({ collection: '{{ENTITY_NAME_LOWER}}', timestamps: true }) +export class {{ENTITY_NAME_CAPITALIZED}}MongooseEntity extends MongooseBaseEntity {} + +export const {{ENTITY_NAME_CAPITALIZED}}MongooseSchema = + SchemaFactory.createForClass({{ENTITY_NAME_CAPITALIZED}}MongooseEntity); diff --git a/apps/api/scripts/code-generation/mongoose/repositories/templates/interface.template.txt b/apps/api/scripts/code-generation/mongoose/repositories/templates/interface.template.txt new file mode 100644 index 0000000..b0343f4 --- /dev/null +++ b/apps/api/scripts/code-generation/mongoose/repositories/templates/interface.template.txt @@ -0,0 +1,13 @@ +import { IMongooseRepository } from '../../../../repositories/mongoose/mongoose.repository.interface'; +import { MongooseBaseRepository } from '../../../../repositories/mongoose/mongoose.base-repository'; +import { {{ENTITY_NAME_CAPITALIZED}} } from '../../schemas/{{ENTITY_NAME_LOWER}}.schema'; +import { {{ENTITY_NAME_CAPITALIZED}}MongooseEntity } from './{{ENTITY_NAME_LOWER}}.mongoose-entity'; + +export type I{{ENTITY_NAME_CAPITALIZED}}MongooseRepository = IMongooseRepository< + {{ENTITY_NAME_CAPITALIZED}}, + {{ENTITY_NAME_CAPITALIZED}}MongooseEntity +>; + +export abstract class {{ENTITY_NAME_CAPITALIZED}}MongooseBaseRepository + extends MongooseBaseRepository<{{ENTITY_NAME_CAPITALIZED}}, {{ENTITY_NAME_CAPITALIZED}}MongooseEntity> + implements I{{ENTITY_NAME_CAPITALIZED}}MongooseRepository {} diff --git a/apps/api/scripts/code-generation/mongoose/repositories/templates/repository.template.txt b/apps/api/scripts/code-generation/mongoose/repositories/templates/repository.template.txt new file mode 100644 index 0000000..2f80462 --- /dev/null +++ b/apps/api/scripts/code-generation/mongoose/repositories/templates/repository.template.txt @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { + InjectTransactionHost, + TransactionHost, +} from '@nestjs-cls/transactional'; +import { TransactionalAdapterMongoose } from '@nestjs-cls/transactional-adapter-mongoose'; +import { {{ENTITY_NAME_CAPITALIZED}}MongooseEntity } from './{{ENTITY_NAME_LOWER}}.mongoose-entity'; +import { {{ENTITY_NAME_CAPITALIZED}} } from '../../schemas/{{ENTITY_NAME_LOWER}}.schema'; +import { {{ENTITY_NAME_CAPITALIZED}}MongooseBaseRepository } from './{{ENTITY_NAME_LOWER}}.mongoose-repository.interface'; +import { ServerConstants } from '../../../../constants/server.constants'; + +@Injectable() +export class {{ENTITY_NAME_CAPITALIZED}}MongooseRepository extends {{ENTITY_NAME_CAPITALIZED}}MongooseBaseRepository { + constructor( + @InjectModel({{ENTITY_NAME_CAPITALIZED}}MongooseEntity.name) + {{ENTITY_NAME_LOWER}}Model: Model<{{ENTITY_NAME_CAPITALIZED}}MongooseEntity>, + @InjectTransactionHost(ServerConstants.TransactionConnectionNames.Mongoose) + mongoTxHost: TransactionHost, + ) { + super({{ENTITY_NAME_LOWER}}Model, mongoTxHost); + } + + protected toDomainEntity(dbEntity: {{ENTITY_NAME_CAPITALIZED}}MongooseEntity): {{ENTITY_NAME_CAPITALIZED}} { + throw new Error('Method not implemented.'); + + // Complete Conversion Below + // return { + // id: dbEntity._id?.toString() ?? '', + // }; + } +} diff --git a/apps/api/scripts/code-generation/prisma/repositories/generate-repository.ts b/apps/api/scripts/code-generation/prisma/repositories/generate-repository.ts new file mode 100644 index 0000000..03de629 --- /dev/null +++ b/apps/api/scripts/code-generation/prisma/repositories/generate-repository.ts @@ -0,0 +1,88 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +interface GeneratorConfig { + entityName: string; +} + +export class RepositoryGenerator { + private readonly entityName: string; + private readonly entityNameCapitalized: string; + private readonly outputDir: string; + private readonly templatesDir: string; + + constructor(config: GeneratorConfig) { + this.entityName = config.entityName.toLowerCase(); + this.entityNameCapitalized = + this.entityName.charAt(0).toUpperCase() + this.entityName.slice(1); + this.outputDir = path.join( + __dirname, + '../../../../src/modules', + this.entityName, + 'repositories/prisma', + ); + this.templatesDir = path.join(__dirname, 'templates'); + } + + generate(): void { + if (!fs.existsSync(this.outputDir)) { + fs.mkdirSync(this.outputDir, { recursive: true }); + } + + this.generateInterface(); + this.generateRepository(); + + console.log( + `βœ… Generated repository for ${this.entityNameCapitalized} in ${this.outputDir}`, + ); + } + + private generateInterface(): void { + const content = this.loadTemplate('interface.template.txt'); + const filePath = path.join( + this.outputDir, + `${this.entityName}.prisma-repository.interface.ts`, + ); + fs.writeFileSync(filePath, content, 'utf-8'); + } + + private generateRepository(): void { + const content = this.loadTemplate('repository.template.txt'); + const filePath = path.join( + this.outputDir, + `${this.entityName}.prisma-repository.ts`, + ); + fs.writeFileSync(filePath, content, 'utf-8'); + } + + private loadTemplate(templateName: string): string { + const templatePath = path.join(this.templatesDir, templateName); + let template = fs.readFileSync(templatePath, 'utf-8'); + + // Replace placeholders + template = template.replace( + /{{ENTITY_NAME_CAPITALIZED}}/g, + this.entityNameCapitalized, + ); + template = template.replace(/{{ENTITY_NAME_LOWER}}/g, this.entityName); + + return template; + } +} + +if (require.main === module) { + const args = process.argv.slice(2); + const entityName = args[0]; + + if (!entityName) { + console.error('❌ Usage: pnpm run generate:repo:prisma '); + console.error(' Example: pnpm run generate:repo:prisma crud'); + process.exit(1); + } + + const generator = new RepositoryGenerator({ + entityName, + }); + + generator.generate(); +} diff --git a/apps/api/scripts/code-generation/prisma/repositories/templates/interface.template.txt b/apps/api/scripts/code-generation/prisma/repositories/templates/interface.template.txt new file mode 100644 index 0000000..39c892b --- /dev/null +++ b/apps/api/scripts/code-generation/prisma/repositories/templates/interface.template.txt @@ -0,0 +1,36 @@ +import { Prisma } from '@repo/prisma-db'; + +export interface I{{ENTITY_NAME_CAPITALIZED}}PrismaRepository { + create( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}CreateArgs, + ): Promise>; + createMany(args: Prisma.{{ENTITY_NAME_CAPITALIZED}}CreateManyArgs): Promise; + + findFirst( + args?: Prisma.{{ENTITY_NAME_CAPITALIZED}}FindFirstArgs, + ): Promise | null>; + findUnique( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}FindUniqueArgs, + ): Promise | null>; + findMany( + args?: Prisma.{{ENTITY_NAME_CAPITALIZED}}FindManyArgs, + ): Promise[]>; + + update( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}UpdateArgs, + ): Promise>; + updateMany(args: Prisma.{{ENTITY_NAME_CAPITALIZED}}UpdateManyArgs): Promise; + upsert( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}UpsertArgs, + ): Promise>; + + delete( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}DeleteArgs, + ): Promise>; + deleteMany(args?: Prisma.{{ENTITY_NAME_CAPITALIZED}}DeleteManyArgs): Promise; + + count(args?: Prisma.{{ENTITY_NAME_CAPITALIZED}}CountArgs): Promise; + aggregate( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}AggregateArgs, + ): Promise>; +} diff --git a/apps/api/scripts/code-generation/prisma/repositories/templates/repository.template.txt b/apps/api/scripts/code-generation/prisma/repositories/templates/repository.template.txt new file mode 100644 index 0000000..2378324 --- /dev/null +++ b/apps/api/scripts/code-generation/prisma/repositories/templates/repository.template.txt @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; +import { + TransactionHost, + InjectTransactionHost, +} from '@nestjs-cls/transactional'; +import { Prisma } from '@repo/prisma-db'; +import { I{{ENTITY_NAME_CAPITALIZED}}PrismaRepository } from './{{ENTITY_NAME_LOWER}}.prisma-repository.interface'; +import { PrismaTransactionAdapter } from '../../../prisma/prisma.module'; +import { ServerConstants } from '../../../../constants/server.constants'; + +@Injectable() +export class {{ENTITY_NAME_CAPITALIZED}}PrismaRepository implements I{{ENTITY_NAME_CAPITALIZED}}PrismaRepository { + constructor( + @InjectTransactionHost(ServerConstants.TransactionConnectionNames.Prisma) + protected readonly prismaTxHost: TransactionHost, + ) {} + + protected get delegate(): Prisma.{{ENTITY_NAME_CAPITALIZED}}Delegate { + return this.prismaTxHost.tx.{{ENTITY_NAME_LOWER}}; + } + + create( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}CreateArgs, + ): Promise> { + return this.delegate.create(args); + } + + createMany(args: Prisma.{{ENTITY_NAME_CAPITALIZED}}CreateManyArgs): Promise { + return this.delegate.createMany(args); + } + + findFirst( + args?: Prisma.{{ENTITY_NAME_CAPITALIZED}}FindFirstArgs, + ): Promise | null> { + return this.delegate.findFirst(args); + } + + findUnique( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}FindUniqueArgs, + ): Promise | null> { + return this.delegate.findUnique(args); + } + + findMany( + args?: Prisma.{{ENTITY_NAME_CAPITALIZED}}FindManyArgs, + ): Promise[]> { + return this.delegate.findMany(args); + } + + update( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}UpdateArgs, + ): Promise> { + return this.delegate.update(args); + } + + updateMany(args: Prisma.{{ENTITY_NAME_CAPITALIZED}}UpdateManyArgs): Promise { + return this.delegate.updateMany(args); + } + + upsert( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}UpsertArgs, + ): Promise> { + return this.delegate.upsert(args); + } + + delete( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}DeleteArgs, + ): Promise> { + return this.delegate.delete(args); + } + + deleteMany(args?: Prisma.{{ENTITY_NAME_CAPITALIZED}}DeleteManyArgs): Promise { + return this.delegate.deleteMany(args); + } + + count(args?: Prisma.{{ENTITY_NAME_CAPITALIZED}}CountArgs): Promise { + return this.delegate.count(args); + } + + aggregate( + args: Prisma.{{ENTITY_NAME_CAPITALIZED}}AggregateArgs, + ): Promise> { + return this.delegate.aggregate(args); + } +} diff --git a/apps/api/scripts/seed/seeders/crud.seeder.ts b/apps/api/scripts/seed/seeders/crud.seeder.ts index 630c094..247d006 100644 --- a/apps/api/scripts/seed/seeders/crud.seeder.ts +++ b/apps/api/scripts/seed/seeders/crud.seeder.ts @@ -1,20 +1,23 @@ import * as mongoose from 'mongoose'; import { - CrudDocument, - CrudSchema, -} from '../../../src/modules/crud/models/crud.model'; + CrudMongooseEntity, + CrudMongooseSchema, +} from '../../../src/modules/crud/repositories/mongoose/crud.mongoose-entity'; import { MongooseSeeder } from './mongoose.seeder'; import { SeedLogger } from '@repo/db-seeder'; import { StringExtensions } from '@repo/utils-core'; -export class CrudSeeder extends MongooseSeeder> { +export class CrudSeeder extends MongooseSeeder> { readonly entityName = 'CRUD'; readonly seedDataFile = 'crud.json'; - readonly model: mongoose.Model; constructor() { - super(); - this.model = mongoose.model(CrudDocument.name, CrudSchema); + super( + mongoose.model( + CrudMongooseEntity.name, + CrudMongooseSchema, + ), + ); } validate(): string[] { diff --git a/apps/api/scripts/seed/seeders/mongoose.seeder.ts b/apps/api/scripts/seed/seeders/mongoose.seeder.ts index b2b4e6b..7dd4a48 100644 --- a/apps/api/scripts/seed/seeders/mongoose.seeder.ts +++ b/apps/api/scripts/seed/seeders/mongoose.seeder.ts @@ -3,9 +3,10 @@ import * as path from 'node:path'; import { BaseSeeder } from '@repo/db-seeder'; export abstract class MongooseSeeder extends BaseSeeder { - abstract readonly model: mongoose.Model; + protected readonly model: mongoose.Model; - protected constructor() { + protected constructor(model: mongoose.Model) { super(path.join(__dirname, '..', 'seed-data')); + this.model = model; } } diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 0d34d55..4ed8a5f 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -10,7 +10,13 @@ import { AuthModule } from './modules/auth/auth.module'; import { EmailModule } from './modules/email/email.module'; import { trpcErrorFormatter } from './trpc/trpc-error-formatter'; import { PrismaModule } from './modules/prisma/prisma.module'; -import { MongooseModule } from '@nestjs/mongoose'; +import { PrismaService } from './modules/prisma/prisma.service'; +import { getConnectionToken, MongooseModule } from '@nestjs/mongoose'; +import { ClsModule } from 'nestjs-cls'; +import { ClsPluginTransactional } from '@nestjs-cls/transactional'; +import { TransactionalAdapterMongoose } from '@nestjs-cls/transactional-adapter-mongoose'; +import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma'; +import { ServerConstants } from './constants/server.constants'; @Module({ imports: [ @@ -25,6 +31,29 @@ import { MongooseModule } from '@nestjs/mongoose'; }), PrismaModule, MongooseModule.forRoot(process.env.DATABASE_URL_MONGODB!), + ClsModule.forRoot({ + global: true, + middleware: { + mount: true, + }, + plugins: [ + new ClsPluginTransactional({ + connectionName: ServerConstants.TransactionConnectionNames.Mongoose, + imports: [MongooseModule], + adapter: new TransactionalAdapterMongoose({ + mongooseConnectionToken: getConnectionToken(), + }), + }), + new ClsPluginTransactional({ + connectionName: ServerConstants.TransactionConnectionNames.Prisma, + imports: [PrismaModule], + adapter: new TransactionalAdapterPrisma({ + prismaInjectionToken: PrismaService, + }), + enableTransactionProxy: true, + }), + ], + }), AuthModule, RollbarModule.register({ accessToken: process.env.ROLLBAR_ACCESS_TOKEN!, diff --git a/apps/api/src/constants/server.constants.ts b/apps/api/src/constants/server.constants.ts new file mode 100644 index 0000000..dd077d5 --- /dev/null +++ b/apps/api/src/constants/server.constants.ts @@ -0,0 +1,11 @@ +export class ServerConstants { + static readonly TransactionConnectionNames = { + Mongoose: 'MONGOOSE_CONNECTION', + Prisma: 'PRISMA_CONNECTION', + } as const; + + static readonly Repositories = { + MongooseCrudInterface: Symbol('ICrudMongooseRepository'), + PrismaCrudInterface: Symbol('ICrudPrismaRepository'), + } as const; +} diff --git a/apps/api/src/decorators/class/auto-transaction.decorator.ts b/apps/api/src/decorators/class/auto-transaction.decorator.ts new file mode 100644 index 0000000..f2ab010 --- /dev/null +++ b/apps/api/src/decorators/class/auto-transaction.decorator.ts @@ -0,0 +1,224 @@ +import 'reflect-metadata'; +import { + Transactional, + Propagation, + type TransactionalAdapter, +} from '@nestjs-cls/transactional'; +import { NO_TRANSACTION_KEY } from '../constants'; +import { mkdirSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +type TOptionsFromAdapter = + TAdapter extends TransactionalAdapter + ? TOptions + : never; + +class TransactionalLogger { + private readonly processDir: string; + + constructor() { + const baseLogDir = join(process.cwd(), 'tmp', 'transaction'); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const random = Math.random().toString(36).substring(2, 8); + const processId = `${timestamp}_${random}`; + + this.processDir = join(baseLogDir, processId); + mkdirSync(this.processDir, { recursive: true }); + + this.log('Session', 'Transaction validation session started'); + this.log('Session', `Process ID: ${processId}`); + this.log('Session', `Log directory: ${this.processDir}`); + } + + log(className: string, message: string): void { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}`; + + const classLogFile = join(this.processDir, `${className}.log`); + + writeFileSync(classLogFile, logMessage + '\n', { + flag: 'a', + encoding: 'utf-8', + }); + + // console.log(`[${timestamp}] [${className}] ${message}`); + } + + getProcessDir(): string { + return this.processDir; + } +} + +const logger = new TransactionalLogger(); + +/** + * Run the decorated class methods in a transaction. + * + * @param options Transaction options depending on the adapter. + */ +export function AutoTransaction( + options?: TOptionsFromAdapter, +): ClassDecorator; +/** + * Run the decorated class methods in a transaction. + * + * @param propagation The propagation mode to use, @see{Propagation}. + */ +export function AutoTransaction(propagation?: Propagation): ClassDecorator; +/** + * Run the decorated class methods in a transaction. + * + * @param connectionName The name of the connection to use. + */ +export function AutoTransaction(connectionName?: string): ClassDecorator; +/** + * Run the decorated class methods in a transaction. + * + * @param connectionName The name of the connection to use. + * @param options Transaction options depending on the adapter. + */ +export function AutoTransaction( + connectionName: string, + options?: TOptionsFromAdapter, +): ClassDecorator; +/** + * Run the decorated class methods in a transaction. + * + * @param connectionName The name of the connection to use. + * @param propagation The propagation mode to use, @see{Propagation}. + */ +export function AutoTransaction( + connectionName: string, + propagation?: Propagation, +): ClassDecorator; +/** + * Run the decorated class methods in a transaction. + * + * @param propagation The propagation mode to use, @see{Propagation}. + * @param options Transaction options depending on the adapter. + */ +export function AutoTransaction( + propagation: Propagation, + options?: TOptionsFromAdapter, +): ClassDecorator; +/** + * Run the decorated class methods in a transaction. + * @param connectionName The name of the connection to use. + * @param propagation The propagation mode to use, @see{Propagation}. + * @param options Transaction options depending on the adapter. + */ +export function AutoTransaction( + connectionName: string, + propagation: Propagation, + options?: TOptionsFromAdapter, +): ClassDecorator; +export function AutoTransaction( + firstParam?: string | Propagation | TOptionsFromAdapter, + secondParam?: Propagation | TOptionsFromAdapter, + thirdParam?: TOptionsFromAdapter, +): ClassDecorator { + return (target) => { + if (typeof target !== 'function') { + throw new Error( + `@AutoTransaction can only be used on classes, but the target is not a function.`, + ); + } + + const className = target.name; + const proto = target.prototype as Record; + + if (!proto) { + throw new Error( + `@AutoTransaction failed on ${className}: Target has no prototype`, + ); + } + + if (proto.constructor !== target) { + throw new Error( + `@AutoTransaction can only be used on classes, but ${className || 'the target'} does not appear to be a class constructor.`, + ); + } + + const methodsProcessed: string[] = []; + const methodsSkipped: string[] = []; + const methodsWithNoTransaction: string[] = []; + + for (const name of Object.getOwnPropertyNames(proto)) { + if (name === 'constructor') { + continue; + } + + const descriptor = Object.getOwnPropertyDescriptor(proto, name); + if (!descriptor) { + methodsSkipped.push(`${name} (no descriptor)`); + continue; + } + + if (typeof descriptor.value !== 'function') { + methodsSkipped.push(`${name} (not a function)`); + continue; + } + + const noTransaction = Reflect.hasMetadata( + NO_TRANSACTION_KEY, + descriptor.value as object, + ); + + if (noTransaction) { + methodsWithNoTransaction.push(name); + continue; + } + + const originalFunctionRef = descriptor.value as object; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + (Transactional as any)(firstParam, secondParam, thirdParam)( + proto, + name, + descriptor, + ); + + if (originalFunctionRef === descriptor.value) { + throw new Error( + `@AutoTransaction has not been applied on ${className}.${name}: ` + + `Function reference unchanged.`, + ); + } + + logger.log(className, `βœ“ ${name}: Function wrapped in transaction proxy`); + + Object.defineProperty(proto, name, descriptor); + + methodsProcessed.push(name); + } + + logger.log(className, `@AutoTransaction decorator applied`); + + logger.log(className, `Summary:`); + logger.log( + className, + ` - Methods wrapped: ${methodsProcessed.length} (${methodsProcessed.join(', ') || 'none'})`, + ); + logger.log( + className, + ` - Methods with @NoTransaction: ${methodsWithNoTransaction.length} (${methodsWithNoTransaction.join(', ') || 'none'})`, + ); + logger.log( + className, + ` - Methods skipped: ${methodsSkipped.length} (${methodsSkipped.join(', ') || 'none'})`, + ); + + if ( + methodsProcessed.length === 0 && + methodsWithNoTransaction.length === 0 + ) { + logger.log( + className, + `⚠ Warning: No methods were wrapped. This may indicate the decorator is applied to a class with no methods.`, + ); + } + + logger.log(className, `Process directory: ${logger.getProcessDir()}`); + }; +} diff --git a/apps/api/src/decorators/constants.ts b/apps/api/src/decorators/constants.ts new file mode 100644 index 0000000..1d501ff --- /dev/null +++ b/apps/api/src/decorators/constants.ts @@ -0,0 +1 @@ +export const NO_TRANSACTION_KEY = 'no-transaction'; diff --git a/apps/api/src/decorators/method/no-transaction.decorator.ts b/apps/api/src/decorators/method/no-transaction.decorator.ts new file mode 100644 index 0000000..8816b8f --- /dev/null +++ b/apps/api/src/decorators/method/no-transaction.decorator.ts @@ -0,0 +1,9 @@ +import { SetMetadata } from '@nestjs/common'; +import { NO_TRANSACTION_KEY } from '../constants'; + +// Decorator to indicate that a method should not be wrapped in a database transaction +export const NoTransaction = (reasonToDisable: string) => + SetMetadata(NO_TRANSACTION_KEY, { + transactionDisabled: true, + disableReason: reasonToDisable, + }); diff --git a/apps/api/src/modules/auth/auth.service.ts b/apps/api/src/modules/auth/auth.service.ts index 7e6d2c1..6f4d74b 100644 --- a/apps/api/src/modules/auth/auth.service.ts +++ b/apps/api/src/modules/auth/auth.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable custom/require-transactional */ import { Injectable } from '@nestjs/common'; import type { Auth } from 'better-auth'; import { EmailService } from '../email/email.service'; diff --git a/apps/api/src/modules/crud/crud.module.ts b/apps/api/src/modules/crud/crud.module.ts index 2a7a149..fc0b0e0 100644 --- a/apps/api/src/modules/crud/crud.module.ts +++ b/apps/api/src/modules/crud/crud.module.ts @@ -1,19 +1,37 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { PrismaModule } from '../prisma/prisma.module'; -import { CrudDocument, CrudSchema } from './models/crud.model'; -import { CrudRepository } from './repositories/crud.repository'; -import { CrudService } from './crud.service'; +import { + CrudMongooseEntity, + CrudMongooseSchema, +} from './repositories/mongoose/crud.mongoose-entity'; import { CrudRouter } from './crud.router'; +import { CrudMongooseRepository } from './repositories/mongoose/crud.mongoose-repository'; +import { CrudPrismaRepository } from './repositories/prisma/crud.prisma-repository'; +import { CrudMongooseService } from './services/crud.mongoose.service'; +import { CrudPrismaService } from './services/crud.prisma.service'; +import { ServerConstants } from '../../constants/server.constants'; @Module({ imports: [ PrismaModule, MongooseModule.forFeature([ - { name: CrudDocument.name, schema: CrudSchema }, + { name: CrudMongooseEntity.name, schema: CrudMongooseSchema }, ]), ], - providers: [CrudRepository, CrudService, CrudRouter], - exports: [CrudRepository, CrudService], + providers: [ + { + provide: ServerConstants.Repositories.MongooseCrudInterface, + useClass: CrudMongooseRepository, + }, + { + provide: ServerConstants.Repositories.PrismaCrudInterface, + useClass: CrudPrismaRepository, + }, + CrudMongooseService, + CrudPrismaService, + CrudRouter, + ], + exports: [CrudMongooseService, CrudPrismaService], }) export class CrudModule {} diff --git a/apps/api/src/modules/crud/crud.router.ts b/apps/api/src/modules/crud/crud.router.ts index ed64bdc..d85e1ea 100644 --- a/apps/api/src/modules/crud/crud.router.ts +++ b/apps/api/src/modules/crud/crud.router.ts @@ -1,5 +1,6 @@ import { Input, Mutation, Query, Router, UseMiddlewares } from 'nestjs-trpc'; -import { CrudService } from './crud.service'; +import { CrudMongooseService } from './services/crud.mongoose.service'; +import { CrudPrismaService } from './services/crud.prisma.service'; import * as CrudSchema from './schemas/crud.schema'; import { ZCrudCreateRequest, @@ -18,21 +19,114 @@ import { AuthMiddleware } from '../auth/auth.middleware'; @Router({ alias: 'crud' }) export class CrudRouter { - constructor(private readonly crudService: CrudService) {} + constructor( + private readonly crudMongooseService: CrudMongooseService, + private readonly crudPrismaService: CrudPrismaService, + ) {} + + // ==================== MONGOOSE ENDPOINTS ==================== + + @UseMiddlewares(AuthMiddleware) + @Mutation({ + input: ZCrudCreateRequest, + output: ZCrudCreateResponse, + }) + async createCrudMongo( + @Input() req: CrudSchema.TCrudCreateRequest, + ): Promise { + const created = await this.crudMongooseService.createCrud(req); + return { + success: created != null, + id: created?.id, + message: created + ? '[Mongoose] Item created successfully' + : 'Failed to create item', + }; + } + + @Query({ + input: ZCrudFindAllRequest, + output: ZCrudFindAllResponse, + }) + async findAllMongo( + @Input() req?: CrudSchema.TCrudFindAllRequest, + ): Promise { + const limit = req?.limit ?? 10; + const offset = req?.offset ?? 0; + const data = await this.crudMongooseService.findAll(); + + return { + success: data != null, + cruds: data, + total: data.length, + limit, + offset, + }; + } + + @Query({ + input: ZCrudFindOneRequest, + output: ZCrudFindOneResponse, + }) + async findOneCrudMongo( + @Input() req: CrudSchema.TCrudFindOneRequest, + ): Promise { + const result = await this.crudMongooseService.findOne(req.id); + return result ?? null; + } + + @UseMiddlewares(AuthMiddleware) + @Mutation({ + input: ZCrudUpdateRequest, + output: ZCrudUpdateResponse, + }) + async updateCrudMongo( + @Input() req: CrudSchema.TCrudUpdateRequest, + ): Promise { + const updated = await this.crudMongooseService.update(req.id, req.data); + return { + success: updated != null, + data: updated ?? undefined, + message: updated + ? '[Mongoose] Item updated successfully' + : 'Failed to update item', + }; + } + + @UseMiddlewares(AuthMiddleware) + @Mutation({ + input: ZCrudDeleteRequest, + output: ZCrudDeleteResponse, + }) + async deleteCrudMongo( + @Input() req: CrudSchema.TCrudDeleteRequest, + ): Promise { + const deleted = await this.crudMongooseService.delete(req.id); + return { + success: deleted != null, + message: deleted + ? '[Mongoose] Item deleted successfully' + : 'Failed to delete item', + }; + } + + // ==================== PRISMA ENDPOINTS ==================== @UseMiddlewares(AuthMiddleware) @Mutation({ input: ZCrudCreateRequest, output: ZCrudCreateResponse, }) - async createCrud( + async createCrudPrisma( @Input() req: CrudSchema.TCrudCreateRequest, ): Promise { - const created = await this.crudService.createCrud(req); + const created = await this.crudPrismaService.createCrud(req); return { success: created != null, id: created?.id, - message: created ? 'Item created successfully' : 'Failed to create item', + message: created + ? '[Prisma] Item created successfully' + : 'Failed to create item', }; } @@ -40,12 +134,12 @@ export class CrudRouter { input: ZCrudFindAllRequest, output: ZCrudFindAllResponse, }) - async findAll( + async findAllPrisma( @Input() req?: CrudSchema.TCrudFindAllRequest, ): Promise { const limit = req?.limit ?? 10; const offset = req?.offset ?? 0; - const data = await this.crudService.findAll(); + const data = await this.crudPrismaService.findAll(); return { success: data != null, @@ -60,10 +154,10 @@ export class CrudRouter { input: ZCrudFindOneRequest, output: ZCrudFindOneResponse, }) - async findOneCrud( + async findOneCrudPrisma( @Input() req: CrudSchema.TCrudFindOneRequest, ): Promise { - const result = await this.crudService.findOne(req.id); + const result = await this.crudPrismaService.findOne(req.id); return result ?? null; } @@ -72,14 +166,16 @@ export class CrudRouter { input: ZCrudUpdateRequest, output: ZCrudUpdateResponse, }) - async updateCrud( + async updateCrudPrisma( @Input() req: CrudSchema.TCrudUpdateRequest, ): Promise { - const updated = await this.crudService.update(req.id, req.data); + const updated = await this.crudPrismaService.update(req.id, req.data); return { success: updated != null, data: updated ?? undefined, - message: updated ? 'Item updated successfully' : 'Failed to update item', + message: updated + ? '[Prisma] Item updated successfully' + : 'Failed to update item', }; } @@ -88,13 +184,15 @@ export class CrudRouter { input: ZCrudDeleteRequest, output: ZCrudDeleteResponse, }) - async deleteCrud( + async deleteCrudPrisma( @Input() req: CrudSchema.TCrudDeleteRequest, ): Promise { - const deleted = await this.crudService.delete(req.id); + const deleted = await this.crudPrismaService.delete(req.id); return { success: deleted != null, - message: deleted ? 'Item deleted successfully' : 'Failed to delete item', + message: deleted + ? '[Prisma] Item deleted successfully' + : 'Failed to delete item', }; } } diff --git a/apps/api/src/modules/crud/crud.service.ts b/apps/api/src/modules/crud/crud.service.ts deleted file mode 100644 index 596690c..0000000 --- a/apps/api/src/modules/crud/crud.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CrudRepository } from './repositories/crud.repository'; -import { - CreateCrudDto, - CrudEntity, - UpdateCrudDto, -} from './schemas/crud.schema'; - -@Injectable() -export class CrudService { - constructor(private readonly crudRepository: CrudRepository) {} - - async createCrud(data: CreateCrudDto): Promise { - return this.crudRepository.create(data); - } - - async findAll(): Promise { - return this.crudRepository.find(); - } - - async findOne(id: string): Promise { - const crud = await this.crudRepository.findOne(id); - if (!crud) throw new NotFoundException(`Crud with id ${id} not found`); - return crud; - } - - async update(id: string, data: UpdateCrudDto): Promise { - const updated = await this.crudRepository.update(id, data); - if (!updated) throw new NotFoundException(`Crud with id ${id} not found`); - return updated; - } - - async delete(id: string): Promise { - const deleted = await this.crudRepository.delete(id); - if (!deleted) throw new NotFoundException(`Crud with id ${id} not found`); - return deleted; - } -} diff --git a/apps/api/src/modules/crud/models/crud.model.ts b/apps/api/src/modules/crud/models/crud.model.ts deleted file mode 100644 index c1595c1..0000000 --- a/apps/api/src/modules/crud/models/crud.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; - -@Schema({ timestamps: true, collection: 'crud' }) -export class CrudDocument extends Document { - @Prop({ required: true }) - content: string; - - @Prop() - createdAt: Date; - - @Prop() - updatedAt: Date; -} - -export const CrudSchema = SchemaFactory.createForClass(CrudDocument); diff --git a/apps/api/src/modules/crud/repositories/crud.repository.ts b/apps/api/src/modules/crud/repositories/crud.repository.ts deleted file mode 100644 index 6dfdea8..0000000 --- a/apps/api/src/modules/crud/repositories/crud.repository.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; -import { - CreateCrudDto, - CrudEntity, - UpdateCrudDto, -} from '../schemas/crud.schema'; - -@Injectable() -export class CrudRepository { - constructor( - private readonly prisma: PrismaService, - // @InjectModel(CrudDocument.name) - // private readonly crudModel: Model, - ) {} - - async find(): Promise { - return this.prisma.crud.findMany({ - orderBy: { createdAt: 'desc' }, - }); - - // const docs = await this.crudModel.find().sort({ createdAt: -1 }).exec(); - // return docs.map((doc) => this.toEntity(doc)); - } - - async findOne(id: string): Promise { - return this.prisma.crud.findUnique({ - where: { id }, - }); - - // const doc = await this.crudModel.findById(id).exec(); - // return doc ? this.toEntity(doc) : null; - } - - async create(data: CreateCrudDto): Promise { - return this.prisma.crud.create({ - data, - }); - - // const doc = await this.crudModel.create(data); - // return this.toEntity(doc); - } - - async update(id: string, data: UpdateCrudDto): Promise { - return this.prisma.crud.update({ - where: { id }, - data, - }); - - // const doc = await this.crudModel - // .findByIdAndUpdate(id, data, { new: true }) - // .exec(); - // return doc ? this.toEntity(doc) : null; - } - - async delete(id: string): Promise { - return this.prisma.crud.delete({ - where: { id }, - }); - - // const doc = await this.crudModel.findByIdAndDelete(id).exec(); - // return doc ? this.toEntity(doc) : null; - } - - // private toEntity(doc: CrudDocument): CrudEntity { - // return { - // id: doc._id.toString(), - // content: doc.content, - // createdAt: doc.createdAt, - // updatedAt: doc.updatedAt, - // }; - // } -} diff --git a/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-entity.ts b/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-entity.ts new file mode 100644 index 0000000..21f1247 --- /dev/null +++ b/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-entity.ts @@ -0,0 +1,11 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { MongooseBaseEntity } from '../../../../repositories/mongoose/mongoose.base-entity'; + +@Schema({ collection: 'crud', timestamps: true }) +export class CrudMongooseEntity extends MongooseBaseEntity { + @Prop({ required: true }) + content: string; +} + +export const CrudMongooseSchema = + SchemaFactory.createForClass(CrudMongooseEntity); diff --git a/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-repository.interface.ts b/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-repository.interface.ts new file mode 100644 index 0000000..0c8432d --- /dev/null +++ b/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-repository.interface.ts @@ -0,0 +1,13 @@ +import { IMongooseRepository } from '../../../../repositories/mongoose/mongoose.repository.interface'; +import { Crud } from '../../schemas/crud.schema'; +import { CrudMongooseEntity } from './crud.mongoose-entity'; +import { MongooseBaseRepository } from '../../../../repositories/mongoose/mongoose.base-repository'; + +export type ICrudMongooseRepository = IMongooseRepository< + Crud, + CrudMongooseEntity +>; + +export abstract class CrudMongooseBaseRepository + extends MongooseBaseRepository + implements ICrudMongooseRepository {} diff --git a/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-repository.ts b/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-repository.ts new file mode 100644 index 0000000..d35f592 --- /dev/null +++ b/apps/api/src/modules/crud/repositories/mongoose/crud.mongoose-repository.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { + InjectTransactionHost, + TransactionHost, +} from '@nestjs-cls/transactional'; +import { TransactionalAdapterMongoose } from '@nestjs-cls/transactional-adapter-mongoose'; +import { CrudMongooseEntity } from './crud.mongoose-entity'; +import { Crud } from '../../schemas/crud.schema'; +import { CrudMongooseBaseRepository } from './crud.mongoose-repository.interface'; +import { ServerConstants } from '../../../../constants/server.constants'; + +@Injectable() +export class CrudMongooseRepository extends CrudMongooseBaseRepository { + constructor( + @InjectModel(CrudMongooseEntity.name) + crudModel: Model, + @InjectTransactionHost(ServerConstants.TransactionConnectionNames.Mongoose) + mongoTxHost: TransactionHost, + ) { + super(crudModel, mongoTxHost); + } + + protected toDomainEntity(dbEntity: CrudMongooseEntity): Crud { + return { + id: dbEntity._id?.toString() ?? '', + content: dbEntity.content, + createdAt: dbEntity.createdAt, + updatedAt: dbEntity.updatedAt, + }; + } +} diff --git a/apps/api/src/modules/crud/repositories/prisma/crud.prisma-repository.interface.ts b/apps/api/src/modules/crud/repositories/prisma/crud.prisma-repository.interface.ts new file mode 100644 index 0000000..fd5b5b4 --- /dev/null +++ b/apps/api/src/modules/crud/repositories/prisma/crud.prisma-repository.interface.ts @@ -0,0 +1,36 @@ +import { Prisma } from '@repo/prisma-db'; + +export interface ICrudPrismaRepository { + create( + args: Prisma.CrudCreateArgs, + ): Promise>; + createMany(args: Prisma.CrudCreateManyArgs): Promise; + + findFirst( + args?: Prisma.CrudFindFirstArgs, + ): Promise | null>; + findUnique( + args: Prisma.CrudFindUniqueArgs, + ): Promise | null>; + findMany( + args?: Prisma.CrudFindManyArgs, + ): Promise[]>; + + update( + args: Prisma.CrudUpdateArgs, + ): Promise>; + updateMany(args: Prisma.CrudUpdateManyArgs): Promise; + upsert( + args: Prisma.CrudUpsertArgs, + ): Promise>; + + delete( + args: Prisma.CrudDeleteArgs, + ): Promise>; + deleteMany(args?: Prisma.CrudDeleteManyArgs): Promise; + + count(args?: Prisma.CrudCountArgs): Promise; + aggregate( + args: Prisma.CrudAggregateArgs, + ): Promise>; +} diff --git a/apps/api/src/modules/crud/repositories/prisma/crud.prisma-repository.ts b/apps/api/src/modules/crud/repositories/prisma/crud.prisma-repository.ts new file mode 100644 index 0000000..b42ccc6 --- /dev/null +++ b/apps/api/src/modules/crud/repositories/prisma/crud.prisma-repository.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; +import { + TransactionHost, + InjectTransactionHost, +} from '@nestjs-cls/transactional'; +import { Prisma } from '@repo/prisma-db'; +import { ICrudPrismaRepository } from './crud.prisma-repository.interface'; +import { PrismaTransactionAdapter } from '../../../prisma/prisma.module'; +import { ServerConstants } from '../../../../constants/server.constants'; + +@Injectable() +export class CrudPrismaRepository implements ICrudPrismaRepository { + constructor( + @InjectTransactionHost(ServerConstants.TransactionConnectionNames.Prisma) + protected readonly prismaTxHost: TransactionHost, + ) {} + + protected get delegate(): Prisma.CrudDelegate { + return this.prismaTxHost.tx.crud; + } + + create( + args: Prisma.CrudCreateArgs, + ): Promise> { + return this.delegate.create(args); + } + + createMany(args: Prisma.CrudCreateManyArgs): Promise { + return this.delegate.createMany(args); + } + + findFirst( + args?: Prisma.CrudFindFirstArgs, + ): Promise | null> { + return this.delegate.findFirst(args); + } + + findUnique( + args: Prisma.CrudFindUniqueArgs, + ): Promise | null> { + return this.delegate.findUnique(args); + } + + findMany( + args?: Prisma.CrudFindManyArgs, + ): Promise[]> { + return this.delegate.findMany(args); + } + + update( + args: Prisma.CrudUpdateArgs, + ): Promise> { + return this.delegate.update(args); + } + + updateMany(args: Prisma.CrudUpdateManyArgs): Promise { + return this.delegate.updateMany(args); + } + + upsert( + args: Prisma.CrudUpsertArgs, + ): Promise> { + return this.delegate.upsert(args); + } + + delete( + args: Prisma.CrudDeleteArgs, + ): Promise> { + return this.delegate.delete(args); + } + + deleteMany(args?: Prisma.CrudDeleteManyArgs): Promise { + return this.delegate.deleteMany(args); + } + + count(args?: Prisma.CrudCountArgs): Promise { + return this.delegate.count(args); + } + + aggregate( + args: Prisma.CrudAggregateArgs, + ): Promise> { + return this.delegate.aggregate(args); + } +} diff --git a/apps/api/src/modules/crud/schemas/crud.schema.ts b/apps/api/src/modules/crud/schemas/crud.schema.ts index 1b51a3b..b5a7ab0 100644 --- a/apps/api/src/modules/crud/schemas/crud.schema.ts +++ b/apps/api/src/modules/crud/schemas/crud.schema.ts @@ -1,18 +1,19 @@ import { z } from 'zod'; -import { ZBaseRequest, ZBaseResponse } from '../../../schemas/base.schema'; +import { + ZBaseEntity, + ZBaseRequest, + ZBaseResponse, +} from '../../../schemas/base.schema'; -export const ZCrudEntity = z.object({ - id: z.string(), +export const ZCrud = ZBaseEntity.extend({ content: z.string().min(1).max(1000), - createdAt: z.date(), - updatedAt: z.date(), }); -export const ZCreateCrudDto = ZCrudEntity.pick({ +export const ZCreateCrudDto = ZCrud.pick({ content: true, }); -export const ZUpdateCrudDto = ZCrudEntity.pick({ +export const ZUpdateCrudDto = ZCrud.pick({ content: true, }); @@ -26,7 +27,7 @@ export const ZCrudFindOneRequest = ZBaseRequest.extend({ id: z.string(), }); -export const ZCrudFindOneResponse = ZCrudEntity.nullable(); +export const ZCrudFindOneResponse = ZCrud.nullable(); export const ZCrudFindAllRequest = ZBaseRequest.extend({ limit: z.number().int().positive().max(100).default(10).optional(), @@ -34,7 +35,7 @@ export const ZCrudFindAllRequest = ZBaseRequest.extend({ }); export const ZCrudFindAllResponse = ZBaseResponse.extend({ - cruds: z.array(ZCrudEntity), + cruds: z.array(ZCrud), total: z.number().int().nonnegative(), limit: z.number().int().positive(), offset: z.number().int().nonnegative(), @@ -48,7 +49,7 @@ export const ZCrudUpdateRequest = ZBaseRequest.extend({ }); export const ZCrudUpdateResponse = ZBaseResponse.extend({ - data: ZCrudEntity.optional(), + data: ZCrud.optional(), }); export const ZCrudDeleteRequest = ZBaseRequest.extend({ @@ -57,7 +58,7 @@ export const ZCrudDeleteRequest = ZBaseRequest.extend({ export const ZCrudDeleteResponse = ZBaseResponse; -export type CrudEntity = z.infer; +export type Crud = z.infer; export type CreateCrudDto = z.infer; export type UpdateCrudDto = z.infer; diff --git a/apps/api/src/modules/crud/services/crud.mongoose.service.ts b/apps/api/src/modules/crud/services/crud.mongoose.service.ts new file mode 100644 index 0000000..63cce68 --- /dev/null +++ b/apps/api/src/modules/crud/services/crud.mongoose.service.ts @@ -0,0 +1,65 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Crud } from '../schemas/crud.schema'; +import { NoTransaction } from '../../../decorators/method/no-transaction.decorator'; +import { AutoTransaction } from '../../../decorators/class/auto-transaction.decorator'; +import { ServerConstants } from '../../../constants/server.constants'; +import { ICrudMongooseRepository } from '../repositories/mongoose/crud.mongoose-repository.interface'; +import { Logger, StringExtensions } from '@repo/utils-core'; +import { Propagation } from '@nestjs-cls/transactional'; + +@Injectable() +@AutoTransaction( + ServerConstants.TransactionConnectionNames.Mongoose, + Propagation.Required, +) +export class CrudMongooseService { + constructor( + @Inject(ServerConstants.Repositories.MongooseCrudInterface) + private readonly crudRepository: ICrudMongooseRepository, + ) {} + + async createCrud(data: Partial): Promise { + if (StringExtensions.IsNullOrEmpty(data.content)) { + throw new BadRequestException('Content is Empty'); + } + + const created = await this.crudRepository.create({ + content: data.content, + }); + + Logger.instance.debug('[Mongoose] Created:', created); + return created; + } + + @NoTransaction('No Reason, Testing if skipping transaction works') + async findAll(): Promise { + return this.crudRepository.find(); + } + + @NoTransaction('dont care if transaction is broken') + async findOne(id: string): Promise { + const crud = await this.crudRepository.findById(id); + if (!crud) throw new NotFoundException(`Crud with id ${id} not found`); + return crud; + } + + async update(id: string, data: Partial): Promise { + const updated = await this.crudRepository.findByIdAndUpdate(id, { + content: data.content, + }); + if (!updated) throw new NotFoundException(`Crud with id ${id} not found`); + return updated; + } + + async delete(id: string): Promise { + const deleted = await this.crudRepository.findByIdAndDelete(id); + if (!deleted) throw new NotFoundException(`Crud with id ${id} not found`); + Logger.instance.debug('[Mongoose] Deleted:', deleted); + return deleted; + } +} diff --git a/apps/api/src/modules/crud/services/crud.prisma.service.ts b/apps/api/src/modules/crud/services/crud.prisma.service.ts new file mode 100644 index 0000000..28af965 --- /dev/null +++ b/apps/api/src/modules/crud/services/crud.prisma.service.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { Crud } from '../schemas/crud.schema'; +import { NoTransaction } from '../../../decorators/method/no-transaction.decorator'; +import { AutoTransaction } from '../../../decorators/class/auto-transaction.decorator'; +import { ServerConstants } from '../../../constants/server.constants'; +import { ICrudPrismaRepository } from '../repositories/prisma/crud.prisma-repository.interface'; +import { Logger } from '@repo/utils-core'; +import { Propagation } from '@nestjs-cls/transactional'; + +@Injectable() +@AutoTransaction( + ServerConstants.TransactionConnectionNames.Prisma, + Propagation.Required, +) +export class CrudPrismaService { + constructor( + @Inject(ServerConstants.Repositories.PrismaCrudInterface) + private readonly crudRepository: ICrudPrismaRepository, + ) {} + + async createCrud(data: Partial): Promise { + const created = await this.crudRepository.create({ + data: { + content: data.content!, + }, + }); + + Logger.instance.debug('[Prisma] Created:', created); + return created; + } + + @NoTransaction('No Reason, Testing if skipping transaction works') + async findAll(): Promise { + return await this.crudRepository.findMany(); + } + + @NoTransaction('dont care if transaction is broken') + async findOne(id: string): Promise { + return this.crudRepository.findUnique({ + where: { id }, + }); + } + + async update(id: string, data: Partial): Promise { + const updated = await this.crudRepository.update({ + where: { id }, + data: { content: data.content }, + }); + if (!updated) throw new NotFoundException(`Crud with id ${id} not found`); + return updated; + } + + async delete(id: string): Promise { + const deleted = await this.crudRepository.delete({ + where: { id }, + }); + + if (!deleted) throw new NotFoundException(`Crud with id ${id} not found`); + Logger.instance.debug('[Prisma] Deleted:', deleted); + return deleted; + } +} diff --git a/apps/api/src/modules/email/email.service.ts b/apps/api/src/modules/email/email.service.ts index f93e004..61469b6 100644 --- a/apps/api/src/modules/email/email.service.ts +++ b/apps/api/src/modules/email/email.service.ts @@ -1,15 +1,16 @@ +/* eslint-disable custom/require-transactional */ import { Injectable } from '@nestjs/common'; import { - Logger, COMPANY_NAME, + Logger, PRODUCT_NAME, SUPPORT_EMAIL, } from '@repo/utils-core'; import { Resend } from 'resend'; import { EmailTemplateName, - SendEmailArgs, RenderedEmail, + SendEmailArgs, } from './types/email.types'; import { renderEmail } from './email.renderer'; diff --git a/apps/api/src/modules/prisma/prisma.module.ts b/apps/api/src/modules/prisma/prisma.module.ts index ec0ce32..87d35d8 100644 --- a/apps/api/src/modules/prisma/prisma.module.ts +++ b/apps/api/src/modules/prisma/prisma.module.ts @@ -1,8 +1,12 @@ import { Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; +import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma'; @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} + +export type PrismaTransactionAdapter = + TransactionalAdapterPrisma; diff --git a/apps/api/src/modules/prisma/prisma.service.ts b/apps/api/src/modules/prisma/prisma.service.ts index 06c2426..0a1c5e9 100644 --- a/apps/api/src/modules/prisma/prisma.service.ts +++ b/apps/api/src/modules/prisma/prisma.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable custom/require-transactional */ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@repo/prisma-db'; diff --git a/apps/api/src/repositories/mongoose/mongoose.base-entity.ts b/apps/api/src/repositories/mongoose/mongoose.base-entity.ts new file mode 100644 index 0000000..9762118 --- /dev/null +++ b/apps/api/src/repositories/mongoose/mongoose.base-entity.ts @@ -0,0 +1,9 @@ +import { Types } from 'mongoose'; +import { Prop } from '@nestjs/mongoose'; + +export class MongooseBaseEntity { + _id?: Types.ObjectId; + + @Prop() createdAt: Date; + @Prop() updatedAt: Date; +} diff --git a/apps/api/src/repositories/mongoose/mongoose.base-repository.ts b/apps/api/src/repositories/mongoose/mongoose.base-repository.ts new file mode 100644 index 0000000..119cbb3 --- /dev/null +++ b/apps/api/src/repositories/mongoose/mongoose.base-repository.ts @@ -0,0 +1,184 @@ +import { IMongooseRepository } from './mongoose.repository.interface'; +import { + InsertManyOptions, + Model, + ProjectionType, + QueryFilter, + QueryOptions, + UpdateQuery, +} from 'mongoose'; +import { BaseEntity } from '../../schemas/base.schema'; +import { TransactionHost } from '@nestjs-cls/transactional'; +import { TransactionalAdapterMongoose } from '@nestjs-cls/transactional-adapter-mongoose'; +import { MongooseBaseEntity } from './mongoose.base-entity'; + +export abstract class MongooseBaseRepository< + TDomainEntity extends BaseEntity, + TDbEntity extends MongooseBaseEntity, +> implements IMongooseRepository +{ + protected readonly model: Model; + protected readonly mongoTxHost: TransactionHost; + + protected constructor( + model: Model, + mongoTxHost: TransactionHost, + ) { + this.model = model; + this.mongoTxHost = mongoTxHost; + } + + async create(entity: Partial): Promise { + const doc = new this.model(entity); + await doc.save({ session: this.mongoTxHost.tx }); + return this.toDomainEntity(doc.toObject()); + } + + async createMany( + entities: Partial[], + options?: InsertManyOptions, + ): Promise { + const docs = await this.model.insertMany(entities, { + ...options, + session: this.mongoTxHost.tx, + }); + + return docs.map((doc) => this.toDomainEntity(doc.toObject())); + } + + async find( + filter?: QueryFilter, + projection?: ProjectionType, + options?: QueryOptions, + ): Promise { + const docs = await this.model + .find(filter, projection, options) + .session(this.mongoTxHost.tx) + .lean(); + + return docs.map((doc) => this.toDomainEntity(doc)); + } + + async findById( + id: string, + projection?: ProjectionType, + options?: QueryOptions, + ): Promise { + const doc = await this.model + .findById(id, projection, options) + .session(this.mongoTxHost.tx) + .lean(); + + return doc ? this.toDomainEntity(doc) : null; + } + + async findOne( + filter?: QueryFilter, + projection?: ProjectionType, + options?: QueryOptions, + ): Promise { + const doc = await this.model + .findOne(filter, projection, options) + .session(this.mongoTxHost.tx) + .lean(); + + return doc ? this.toDomainEntity(doc) : null; + } + + async updateOneById( + id: string, + update: UpdateQuery, + ): Promise { + return this.updateOne({ _id: id }, update); + } + + async updateOne( + filter: QueryFilter, + update: UpdateQuery, + ): Promise { + const updateResult = await this.model + .updateOne(filter, update) + .session(this.mongoTxHost.tx); + + return updateResult.modifiedCount > 0; + } + + async updateMany( + filter: QueryFilter, + update: UpdateQuery, + ): Promise { + const updateResult = await this.model + .updateMany(filter, update) + .session(this.mongoTxHost.tx); + + return updateResult.modifiedCount > 0; + } + + async findByIdAndUpdate( + id: string, + update: UpdateQuery, + options?: QueryOptions, + ): Promise { + const doc = await this.model + .findByIdAndUpdate(id, update, { new: true, ...options }) + .session(this.mongoTxHost.tx) + .lean(); + + return doc ? this.toDomainEntity(doc) : null; + } + + async findOneAndUpdate( + filter: QueryFilter, + update: UpdateQuery, + options?: QueryOptions, + ): Promise { + const doc = await this.model + .findOneAndUpdate(filter, update, { new: true, ...options }) + .session(this.mongoTxHost.tx) + .lean(); + + return doc ? this.toDomainEntity(doc) : null; + } + + async deleteOneById(id: string): Promise { + return this.deleteOne({ _id: id }); + } + + async deleteOne(filter: QueryFilter): Promise { + const deleteResult = await this.model + .deleteOne(filter) + .session(this.mongoTxHost.tx); + + return deleteResult.deletedCount > 0; + } + + async deleteMany(filter: QueryFilter): Promise { + const deleteResult = await this.model + .deleteMany(filter) + .session(this.mongoTxHost.tx); + + return deleteResult.deletedCount > 0; + } + + async findByIdAndDelete(id: string): Promise { + const deletedDoc = await this.model + .findByIdAndDelete(id) + .session(this.mongoTxHost.tx) + .lean(); + + return deletedDoc ? this.toDomainEntity(deletedDoc) : null; + } + + async findOneAndDelete( + filter: QueryFilter, + ): Promise { + const deletedDoc = await this.model + .findOneAndDelete(filter) + .session(this.mongoTxHost.tx) + .lean(); + + return deletedDoc ? this.toDomainEntity(deletedDoc) : null; + } + + protected abstract toDomainEntity(tDbEntity: TDbEntity): TDomainEntity; +} diff --git a/apps/api/src/repositories/mongoose/mongoose.repository.interface.ts b/apps/api/src/repositories/mongoose/mongoose.repository.interface.ts new file mode 100644 index 0000000..2fbe61c --- /dev/null +++ b/apps/api/src/repositories/mongoose/mongoose.repository.interface.ts @@ -0,0 +1,60 @@ +import { + InsertManyOptions, + ProjectionType, + QueryFilter, + QueryOptions, + UpdateQuery, +} from 'mongoose'; +import { MongooseBaseEntity } from './mongoose.base-entity'; +import { BaseEntity } from '../../schemas/base.schema'; + +export interface IMongooseRepository< + TDomainEntity extends BaseEntity, + TDbEntity extends MongooseBaseEntity, +> { + create(entity: Partial): Promise; + createMany( + docs: Partial[], + options?: InsertManyOptions, + ): Promise; + + find( + filter?: QueryFilter, + projection?: ProjectionType, + options?: QueryOptions, + ): Promise; + findById(id: string): Promise; + findOne( + filter?: QueryFilter, + projection?: ProjectionType, + options?: QueryOptions, + ): Promise; + + updateOneById(id: string, update: UpdateQuery): Promise; + updateOne( + filter: QueryFilter, + update: UpdateQuery, + ): Promise; + updateMany( + filter: QueryFilter, + update: UpdateQuery, + ): Promise; + findByIdAndUpdate( + id: string, + update: UpdateQuery, + options?: QueryOptions, + ): Promise; + findOneAndUpdate( + filter: QueryFilter, + update: UpdateQuery, + options?: QueryOptions, + ): Promise; + + deleteOneById(id: string): Promise; + deleteOne(filter: QueryFilter): Promise; + deleteMany(filter: QueryFilter): Promise; + findByIdAndDelete(id: string): Promise; + findOneAndDelete( + filter: QueryFilter, + ): Promise; +} diff --git a/apps/api/src/schemas/base.schema.ts b/apps/api/src/schemas/base.schema.ts index 85ad944..17d7797 100644 --- a/apps/api/src/schemas/base.schema.ts +++ b/apps/api/src/schemas/base.schema.ts @@ -10,5 +10,12 @@ export const ZBaseResponse = z.object({ message: z.string().optional(), }); +export const ZBaseEntity = z.object({ + id: z.string(), + createdAt: z.date(), + updatedAt: z.date(), +}); + export type TBaseRequest = z.infer; export type TBaseResponse = z.infer; +export type BaseEntity = z.infer; diff --git a/apps/mobile/app/crud.tsx b/apps/mobile/app/crud.tsx index 6e438c2..a313a91 100644 --- a/apps/mobile/app/crud.tsx +++ b/apps/mobile/app/crud.tsx @@ -11,6 +11,13 @@ import { useState } from "react"; import { router } from "expo-router"; import { trpc } from "@repo/trpc/client"; +type DbType = "mongoose" | "prisma"; + +interface CrudItem { + id: string; + content: string; +} + const styles = StyleSheet.create({ container: { flex: 1, @@ -19,7 +26,7 @@ const styles = StyleSheet.create({ safeArea: { flex: 1, }, - content: { + scrollContent: { padding: 32, }, backButton: { @@ -32,18 +39,79 @@ const styles = StyleSheet.create({ color: "#94a3b8", fontSize: 16, }, - header: { + mainHeader: { alignItems: "center", marginBottom: 48, }, + mainTitle: { + fontSize: 28, + fontWeight: "bold", + color: "#60a5fa", + marginBottom: 8, + }, + mainSubtitle: { + fontSize: 14, + color: "#94a3b8", + marginBottom: 4, + }, + techStack: { + fontSize: 12, + color: "#64748b", + }, + dbTabsContainer: { + flexDirection: "row", + gap: 12, + marginBottom: 32, + }, + dbTab: { + flex: 1, + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 8, + borderWidth: 2, + borderColor: "#334155", + backgroundColor: "#1e293b", + alignItems: "center", + }, + dbTabActive: { + borderColor: "#3b82f6", + backgroundColor: "#1e3a8a", + }, + dbTabActiveMongoose: { + borderColor: "#10b981", + backgroundColor: "#064e3b", + }, + dbTabText: { + color: "#94a3b8", + fontSize: 16, + fontWeight: "600", + }, + dbTabTextActive: { + color: "#60a5fa", + }, + dbTabTextActiveMongoose: { + color: "#34d399", + }, + panelContainer: { + marginBottom: 32, + }, + header: { + alignItems: "center", + marginBottom: 24, + }, headerIcon: { width: 32, height: 32, borderRadius: 16, - backgroundColor: "#06b6d4", justifyContent: "center", alignItems: "center", - marginBottom: 16, + marginBottom: 12, + }, + headerIconMongoose: { + backgroundColor: "#10b981", + }, + headerIconPrisma: { + backgroundColor: "#3b82f6", }, headerIconText: { color: "#0f172a", @@ -51,17 +119,18 @@ const styles = StyleSheet.create({ fontWeight: "bold", }, title: { - fontSize: 32, + fontSize: 24, fontWeight: "bold", - color: "#60a5fa", marginBottom: 8, }, - subtitle: { - fontSize: 14, - color: "#94a3b8", + titleMongoose: { + color: "#10b981", + }, + titlePrisma: { + color: "#3b82f6", }, inputSection: { - marginBottom: 32, + marginBottom: 24, gap: 12, }, input: { @@ -75,7 +144,6 @@ const styles = StyleSheet.create({ fontSize: 16, }, button: { - backgroundColor: "#3b82f6", paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8, @@ -83,6 +151,12 @@ const styles = StyleSheet.create({ alignItems: "center", minHeight: 48, }, + buttonMongoose: { + backgroundColor: "#10b981", + }, + buttonPrisma: { + backgroundColor: "#3b82f6", + }, buttonDisabled: { backgroundColor: "#475569", }, @@ -91,6 +165,18 @@ const styles = StyleSheet.create({ fontWeight: "600", fontSize: 16, }, + refreshButton: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 6, + marginBottom: 16, + alignSelf: "center", + }, + refreshButtonText: { + color: "#ffffff", + fontWeight: "600", + fontSize: 14, + }, listContainer: { backgroundColor: "#1e293b", borderRadius: 8, @@ -98,8 +184,6 @@ const styles = StyleSheet.create({ borderColor: "#334155", overflow: "hidden", flex: 1, - minHeight: 200, - maxHeight: 400, }, listHeader: { backgroundColor: "#334155", @@ -134,13 +218,18 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: "#334155", borderWidth: 1, - borderColor: "#3b82f6", borderRadius: 4, paddingHorizontal: 8, paddingVertical: 6, color: "#ffffff", fontSize: 16, }, + editInputMongoose: { + borderColor: "#10b981", + }, + editInputPrisma: { + borderColor: "#3b82f6", + }, deleteButton: { padding: 8, marginLeft: 12, @@ -164,52 +253,55 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "center", alignItems: "center", - }, - refreshButton: { - backgroundColor: "#3b82f6", - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 6, - marginBottom: 12, - }, - refreshButtonText: { - color: "#ffffff", - fontWeight: "600", - fontSize: 14, + paddingVertical: 32, }, }); -export default function CrudPage() { +function CrudPanel({ dbType }: { dbType: DbType }) { const utils = trpc.useUtils(); - const [content, setContent] = useState(""); const [editingId, setEditingId] = useState(null); const [editingContent, setEditingContent] = useState(""); - // Queries - const crudList = trpc.crud.findAll.useQuery( + const isMongoose = dbType === "mongoose"; + const label = isMongoose ? "Mongoose (MongoDB)" : "Prisma (PostgreSQL)"; + + // Queries - dynamically choose endpoint + const crudList = trpc.crud[ + isMongoose ? "findAllMongo" : "findAllPrisma" + ].useQuery( {}, { refetchOnWindowFocus: false, - refetchOnMount: true, }, ); // Mutations - const createCrud = trpc.crud.createCrud.useMutation({ + const createCrud = trpc.crud[ + isMongoose ? "createCrudMongo" : "createCrudPrisma" + ].useMutation({ onSuccess: () => { - void utils.crud.findAll.invalidate(); + void utils.crud[ + isMongoose ? "findAllMongo" : "findAllPrisma" + ].invalidate(); setContent(""); }, }); - const deleteCrud = trpc.crud.deleteCrud.useMutation({ - onSuccess: () => utils.crud.findAll.invalidate(), + const deleteCrud = trpc.crud[ + isMongoose ? "deleteCrudMongo" : "deleteCrudPrisma" + ].useMutation({ + onSuccess: () => + utils.crud[isMongoose ? "findAllMongo" : "findAllPrisma"].invalidate(), }); - const updateCrud = trpc.crud.updateCrud?.useMutation({ + const updateCrud = trpc.crud[ + isMongoose ? "updateCrudMongo" : "updateCrudPrisma" + ].useMutation({ onSuccess: () => { - void utils.crud.findAll.invalidate(); + void utils.crud[ + isMongoose ? "findAllMongo" : "findAllPrisma" + ].invalidate(); setEditingId(null); setEditingContent(""); }, @@ -222,7 +314,7 @@ export default function CrudPage() { const handleUpdate = (id: string) => { if (!editingContent.trim()) return; - updateCrud?.mutate({ id, data: { content: editingContent } }); + updateCrud.mutate({ id, data: { content: editingContent } }); }; const handleDelete = (id: string) => { @@ -230,15 +322,18 @@ export default function CrudPage() { }; const handleRefresh = () => { - void utils.crud.findAll.invalidate(); + void utils.crud[isMongoose ? "findAllMongo" : "findAllPrisma"].invalidate(); }; - const renderItem = ({ item }: { item: { id: string; content: string } }) => ( + const renderItem = ({ item }: { item: CrudItem }) => ( {editingId === item.id ? ( <> ); - const renderListContent = () => { - if (crudList.isLoading) { - return ( - - + const renderHeader = () => ( + + + + {isMongoose ? "M" : "P"} - ); - } + + {label} + + - if (crudList.data && crudList.data.cruds.length > 0) { - return ( - <> - - - {crudList.data.cruds.length}{" "} - {crudList.data.cruds.length === 1 ? "Item" : "Items"} - - - item.id} - scrollEnabled + + + + + {createCrud.isPending ? "Adding..." : "Add"} + + + + + + + {crudList.isRefetching ? "Refreshing..." : "Refresh"} + + + + + + {crudList.data?.cruds.length || 0}{" "} + {crudList.data?.cruds.length === 1 ? "Item" : "Items"} + + + + ); + + if (crudList.isLoading) { + return ( + + {renderHeader()} + + - - ); - } + + + ); + } + if (!crudList.data || crudList.data.cruds.length === 0) { return ( - - - No items yet. Add one to get started! - + + {renderHeader()} + + + No items yet. Add one to get started! + + ); - }; + } + + return ( + + item.id} + ListHeaderComponent={renderHeader} + contentContainerStyle={{ paddingBottom: 20 }} + /> + + ); +} + +export default function CrudPage() { + const [selectedDb, setSelectedDb] = useState("prisma"); return ( - + router.push("/")} @@ -331,49 +500,52 @@ export default function CrudPage() { Back to Home - - - βœ“ - - BE Tech Stack CRUD - NextJs, NestJs, Expo, Trpc + + Dual Database CRUD Demo + + Comparison of Mongoose (MongoDB) and Prisma (PostgreSQL) + + + Expo (React Native) β€’ NestJs β€’ tRPC β€’ Transactions + - - + + setSelectedDb("mongoose")} + > + + Mongoose + + setSelectedDb("prisma")} > - - {createCrud.isPending ? "Adding..." : "Add"} + + Prisma - - - {crudList.isRefetching ? "Refreshing..." : "Refresh"} - - - - {renderListContent()} + diff --git a/apps/web/app/crud-demo/page.tsx b/apps/web/app/crud-demo/page.tsx index a66a59f..c1cfa05 100644 --- a/apps/web/app/crud-demo/page.tsx +++ b/apps/web/app/crud-demo/page.tsx @@ -4,14 +4,35 @@ import { useState } from "react"; import { trpc } from "@repo/trpc/client"; import Link from "next/link"; -export default function CrudDemo() { +type DbType = "mongoose" | "prisma"; + +interface CrudItem { + id: string; + content: string; +} + +function CrudPanel({ dbType }: { dbType: DbType }) { const utils = trpc.useUtils(); const [content, setContent] = useState(""); const [editingId, setEditingId] = useState(null); const [editingContent, setEditingContent] = useState(""); - // Queries - const crudList = trpc.crud.findAll.useQuery( + const isMongoose = dbType === "mongoose"; + const label = isMongoose ? "Mongoose (MongoDB)" : "Prisma (PostgreSQL)"; + const gradientColors = isMongoose + ? "from-green-400 to-emerald-400" + : "from-blue-400 to-cyan-400"; + const buttonColors = isMongoose + ? "from-green-500 to-green-600 hover:from-green-600 hover:to-green-700" + : "from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700"; + const hoverColor = isMongoose + ? "hover:text-green-400" + : "hover:text-blue-400"; + + // Queries - dynamically choose endpoint + const crudList = trpc.crud[ + isMongoose ? "findAllMongo" : "findAllPrisma" + ].useQuery( {}, { refetchOnWindowFocus: false, @@ -19,20 +40,31 @@ export default function CrudDemo() { ); // Mutations - const createCrud = trpc.crud.createCrud.useMutation({ + const createCrud = trpc.crud[ + isMongoose ? "createCrudMongo" : "createCrudPrisma" + ].useMutation({ onSuccess: () => { - void utils.crud.findAll.invalidate(); + void utils.crud[ + isMongoose ? "findAllMongo" : "findAllPrisma" + ].invalidate(); setContent(""); }, }); - const deleteCrud = trpc.crud.deleteCrud.useMutation({ - onSuccess: () => utils.crud.findAll.invalidate(), + const deleteCrud = trpc.crud[ + isMongoose ? "deleteCrudMongo" : "deleteCrudPrisma" + ].useMutation({ + onSuccess: () => + utils.crud[isMongoose ? "findAllMongo" : "findAllPrisma"].invalidate(), }); - const updateCrud = trpc.crud.updateCrud?.useMutation({ + const updateCrud = trpc.crud[ + isMongoose ? "updateCrudMongo" : "updateCrudPrisma" + ].useMutation({ onSuccess: () => { - void utils.crud.findAll.invalidate(); + void utils.crud[ + isMongoose ? "findAllMongo" : "findAllPrisma" + ].invalidate(); setEditingId(null); setEditingContent(""); }, @@ -45,7 +77,7 @@ export default function CrudDemo() { const handleUpdate = (id: string) => { if (!editingContent.trim()) return; - updateCrud?.mutate({ id, data: { content: editingContent } }); + updateCrud.mutate({ id, data: { content: editingContent } }); }; const handleDelete = (id: string) => { @@ -53,7 +85,7 @@ export default function CrudDemo() { }; const handleRefresh = () => { - void utils.crud.findAll.invalidate(); + void utils.crud[isMongoose ? "findAllMongo" : "findAllPrisma"].invalidate(); }; const renderListContent = () => { @@ -75,60 +107,62 @@ export default function CrudDemo() {

    - {crudList.data.cruds.map( - (item: { id: string; content: string }) => ( -
  • - {editingId === item.id ? ( - setEditingContent(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && editingContent.trim()) { - handleUpdate(item.id); - } else if (e.key === "Escape") { - setEditingId(null); - } - }} - autoFocus - className="flex-1 bg-slate-600 border border-blue-400 rounded px-2 py-1 text-white focus:outline-none focus:ring-2 focus:ring-blue-400" - /> - ) : ( - - )} + {crudList.data.cruds.map((item: CrudItem) => ( +
  • + {editingId === item.id ? ( + setEditingContent(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && editingContent.trim()) { + handleUpdate(item.id); + } else if (e.key === "Escape") { + setEditingId(null); + } + }} + autoFocus + className={`flex-1 bg-slate-600 border rounded px-2 py-1 text-white focus:outline-none focus:ring-2 ${ + isMongoose + ? "border-green-400 focus:ring-green-400" + : "border-blue-400 focus:ring-blue-400" + }`} + /> + ) : ( -
  • - ), - )} + )} + + + ))}
); @@ -143,9 +177,66 @@ export default function CrudDemo() { ); }; + return ( +
+
+
+
+ + {isMongoose ? "M" : "P"} + +
+

+ {label} +

+
+
+ +
+
+ setContent(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleCreate()} + disabled={createCrud.isPending} + /> + +
+
+ +
+ +
+ +
+ {renderListContent()} +
+
+ ); +} + +export default function CrudDemo() { return (
-
+
-
-
- βœ“ -
-

- BE Tech Stack CRUD -

-
-

- NextJs (tailwindcss), NestJs, Expo, Trpc +

+ Dual Database CRUD Demo +

+

+ Side-by-side comparison of Mongoose (MongoDB) and Prisma + (PostgreSQL) +

+

+ NextJs (TailwindCSS) β€’ NestJs β€’ tRPC β€’ Transactions

-
-
- setContent(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleCreate()} - disabled={createCrud.isPending} - /> - -
-
- -
- -
- -
- {renderListContent()} +
+ +
diff --git a/packages/db-seeder/README.md b/packages/db-seeder/README.md index 4477494..ddfba06 100644 --- a/packages/db-seeder/README.md +++ b/packages/db-seeder/README.md @@ -14,7 +14,7 @@ Shared database seeding infrastructure for all database types (Prisma, Mongoose, This package is already available in the monorepo workspace: ```typescript -import { BaseSeeder, runSeeders, SeedLogger } from '@repo/db-seeder'; +import { BaseSeeder, runSeeders, SeedLogger } from "@repo/db-seeder"; ``` ## Architecture @@ -103,19 +103,16 @@ Create `seed-data/users.json`: ### 3. Create Seed Script ```typescript -import { PrismaClient } from '@prisma/client'; -import { runSeeders } from '@repo/db-seeder'; -import { UserSeeder } from './seeders/user.seeder'; -import { PostSeeder } from './seeders/post.seeder'; +import { PrismaClient } from "@prisma/client"; +import { runSeeders } from "@repo/db-seeder"; +import { UserSeeder } from "./seeders/user.seeder"; +import { PostSeeder } from "./seeders/post.seeder"; const prisma = new PrismaClient(); runSeeders({ - seeders: [ - new UserSeeder(prisma), - new PostSeeder(prisma), - ], - loggerPrefix: '[SEED_PRISMA]', + seeders: [new UserSeeder(prisma), new PostSeeder(prisma)], + loggerPrefix: "[SEED_PRISMA]", onDisconnect: async () => { await prisma.$disconnect(); }, @@ -128,15 +125,15 @@ runSeeders({ All logs follow a consistent color scheme: -| Element | Color | Example | -|---------|-------|---------| -| Prefix `[SEED_*]` | Cyan | `[SEED_PRISMA]` | -| Entity Context | Magenta | `[USER]` | -| Step Context | Yellow | `[Step 1]` | -| Success Messages | Green | `βœ“ Seeded 5 records` | -| Error Messages | Red | `βœ— Validation failed` | -| Info Messages | Blue | General information | -| Separators | Gray | Visual dividers | +| Element | Color | Example | +| ----------------- | ------- | --------------------- | +| Prefix `[SEED_*]` | Cyan | `[SEED_PRISMA]` | +| Entity Context | Magenta | `[USER]` | +| Step Context | Yellow | `[Step 1]` | +| Success Messages | Green | `βœ“ Seeded 5 records` | +| Error Messages | Red | `βœ— Validation failed` | +| Info Messages | Blue | General information | +| Separators | Gray | Visual dividers | ## API Reference @@ -145,6 +142,7 @@ All logs follow a consistent color scheme: Main function to run the seeding process. **Parameters:** + - `seeders`: Array of seeder instances - `loggerPrefix`: Custom prefix for logs (e.g., `"[SEED_PRISMA]"`) - `onConnect`: Optional callback to connect to database @@ -155,7 +153,7 @@ Main function to run the seeding process. ```typescript await runSeeders({ seeders: [new UserSeeder(prisma)], - loggerPrefix: '[SEED_PRISMA]', + loggerPrefix: "[SEED_PRISMA]", onConnect: async () => { await database.connect(); }, @@ -170,15 +168,18 @@ await runSeeders({ Abstract base class for all seeders. **Constructor:** + - `seedDataDir`: Path to seed data directory **Properties:** + - `entityName`: Name of the entity (e.g., "USER", "POST") - `seedFile`: JSON file name (e.g., "users.json") - `records`: Loaded records array - `logger`: SeedLogger instance **Methods:** + - `loadData()`: Loads data from JSON file (implemented) - `validate()`: Validate records (must implement) - `clean()`: Clean database (must implement) @@ -189,6 +190,7 @@ Abstract base class for all seeders. Static logger class with colored output. **Methods:** + - `log(message, context?)`: Blue info messages - `success(message, context?)`: Green success messages - `error(message, context?)`: Red error messages @@ -247,14 +249,14 @@ To add support for a new database type: Example for TypeORM: ```typescript -import { BaseSeeder as SharedBaseSeeder } from '@repo/db-seeder'; -import { Repository } from 'typeorm'; +import { BaseSeeder as SharedBaseSeeder } from "@repo/db-seeder"; +import { Repository } from "typeorm"; export abstract class TypeORMSeeder extends SharedBaseSeeder { abstract readonly repository: Repository; constructor() { - super(path.join(__dirname, '..', 'seed-data')); + super(path.join(__dirname, "..", "seed-data")); } async clean(): Promise { @@ -265,4 +267,4 @@ export abstract class TypeORMSeeder extends SharedBaseSeeder { await this.repository.save(this.records); } } -``` \ No newline at end of file +``` diff --git a/packages/sonarqube/README.md b/packages/sonarqube/README.md index 2e40082..40d7dbd 100644 --- a/packages/sonarqube/README.md +++ b/packages/sonarqube/README.md @@ -202,24 +202,24 @@ Use this only when you want a **fresh reset**. ## Project Structure -| File/Directory | Purpose | -| ------------------------ | ------------------------------------------------ | -| `.env` | Personal environment configuration | -| `.env.example` | Template for creating `.env` | -| `docker-compose.yml` | Defines SonarQube server and PostgreSQL database | +| File/Directory | Purpose | +| ---------------------------- | ------------------------------------------------ | +| `.env` | Personal environment configuration | +| `.env.example` | Template for creating `.env` | +| `docker-compose.yml` | Defines SonarQube server and PostgreSQL database | | `docker-compose-scanner.yml` | Defines the scanner container | -| `sonar-project.properties` | Project analysis configuration | -| `README.md` | This setup guide | -| **unix/** | **Scripts for macOS/Linux** | -| `unix/start.sh` | Starts SonarQube server using Docker | -| `unix/stop.sh` | Gracefully stops SonarQube containers | -| `unix/clean.sh` | Removes all SonarQube containers and volumes | -| `unix/scan.sh` | Runs the SonarQube scanner for code analysis | -| **win/** | **Scripts for Windows** | -| `win/start.bat` | Starts SonarQube server using Docker | -| `win/stop.bat` | Gracefully stops SonarQube containers | -| `win/clean.bat` | Removes all SonarQube containers and volumes | -| `win/scan.bat` | Runs the SonarQube scanner for code analysis | +| `sonar-project.properties` | Project analysis configuration | +| `README.md` | This setup guide | +| **unix/** | **Scripts for macOS/Linux** | +| `unix/start.sh` | Starts SonarQube server using Docker | +| `unix/stop.sh` | Gracefully stops SonarQube containers | +| `unix/clean.sh` | Removes all SonarQube containers and volumes | +| `unix/scan.sh` | Runs the SonarQube scanner for code analysis | +| **win/** | **Scripts for Windows** | +| `win/start.bat` | Starts SonarQube server using Docker | +| `win/stop.bat` | Gracefully stops SonarQube containers | +| `win/clean.bat` | Removes all SonarQube containers and volumes | +| `win/scan.bat` | Runs the SonarQube scanner for code analysis | > You do not need to edit any `.yml` files β€” all configuration is handled via `.env`. diff --git a/packages/trpc/src/server/server.ts b/packages/trpc/src/server/server.ts index 3090a09..77d4966 100644 --- a/packages/trpc/src/server/server.ts +++ b/packages/trpc/src/server/server.ts @@ -6,14 +6,15 @@ const publicProcedure = t.procedure; const appRouter = t.router({ crud: t.router({ - createCrud: publicProcedure.input(z.object({ + createCrudMongo: publicProcedure.input(z.object({ requestId: z.string().uuid().optional(), timestamp: z.number().optional(), }).merge(z.object({ id: z.string(), - content: z.string().min(1).max(1000), createdAt: z.date(), updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), }).pick({ content: true, }))).output(z.object({ @@ -22,7 +23,7 @@ const appRouter = t.router({ }).extend({ id: z.string(), })).mutation(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), - findAll: publicProcedure.input(z.object({ + findAllMongo: publicProcedure.input(z.object({ requestId: z.string().uuid().optional(), timestamp: z.number().optional(), }).extend({ @@ -34,35 +35,38 @@ const appRouter = t.router({ }).extend({ cruds: z.array(z.object({ id: z.string(), - content: z.string().min(1).max(1000), createdAt: z.date(), updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), })), total: z.number().int().nonnegative(), limit: z.number().int().positive(), offset: z.number().int().nonnegative(), })).query(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), - findOneCrud: publicProcedure.input(z.object({ + findOneCrudMongo: publicProcedure.input(z.object({ requestId: z.string().uuid().optional(), timestamp: z.number().optional(), }).extend({ id: z.string(), })).output(z.object({ id: z.string(), - content: z.string().min(1).max(1000), createdAt: z.date(), updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), }).nullable()).query(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), - updateCrud: publicProcedure.input(z.object({ + updateCrudMongo: publicProcedure.input(z.object({ requestId: z.string().uuid().optional(), timestamp: z.number().optional(), }).extend({ id: z.string(), data: z.object({ id: z.string(), - content: z.string().min(1).max(1000), createdAt: z.date(), updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), }).pick({ content: true, }).refine((data) => Object.keys(data).length > 0, { @@ -74,12 +78,100 @@ const appRouter = t.router({ }).extend({ data: z.object({ id: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + }).extend({ content: z.string().min(1).max(1000), + }).optional(), + })).mutation(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), + deleteCrudMongo: publicProcedure.input(z.object({ + requestId: z.string().uuid().optional(), + timestamp: z.number().optional(), + }).extend({ + id: z.string(), + })).output(z.object({ + success: z.boolean(), + message: z.string().optional(), + })).mutation(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), + createCrudPrisma: publicProcedure.input(z.object({ + requestId: z.string().uuid().optional(), + timestamp: z.number().optional(), + }).merge(z.object({ + id: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), + }).pick({ + content: true, + }))).output(z.object({ + success: z.boolean(), + message: z.string().optional(), + }).extend({ + id: z.string(), + })).mutation(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), + findAllPrisma: publicProcedure.input(z.object({ + requestId: z.string().uuid().optional(), + timestamp: z.number().optional(), + }).extend({ + limit: z.number().int().positive().max(100).default(10).optional(), + offset: z.number().int().nonnegative().default(0).optional(), + })).output(z.object({ + success: z.boolean(), + message: z.string().optional(), + }).extend({ + cruds: z.array(z.object({ + id: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), + })), + total: z.number().int().nonnegative(), + limit: z.number().int().positive(), + offset: z.number().int().nonnegative(), + })).query(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), + findOneCrudPrisma: publicProcedure.input(z.object({ + requestId: z.string().uuid().optional(), + timestamp: z.number().optional(), + }).extend({ + id: z.string(), + })).output(z.object({ + id: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), + }).nullable()).query(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), + updateCrudPrisma: publicProcedure.input(z.object({ + requestId: z.string().uuid().optional(), + timestamp: z.number().optional(), + }).extend({ + id: z.string(), + data: z.object({ + id: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), + }).pick({ + content: true, + }).refine((data) => Object.keys(data).length > 0, { + message: 'At least one field must be provided for update', + }), + })).output(z.object({ + success: z.boolean(), + message: z.string().optional(), + }).extend({ + data: z.object({ + id: z.string(), createdAt: z.date(), updatedAt: z.date(), + }).extend({ + content: z.string().min(1).max(1000), }).optional(), })).mutation(async () => "PLACEHOLDER_DO_NOT_REMOVE" as any), - deleteCrud: publicProcedure.input(z.object({ + deleteCrudPrisma: publicProcedure.input(z.object({ requestId: z.string().uuid().optional(), timestamp: z.number().optional(), }).extend({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ce0feb..eb663db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,15 @@ importers: '@better-auth/expo': specifier: 1.3.34 version: 1.3.34(better-auth@1.3.34(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(expo-constants@18.0.9(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.12)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.0)(react@19.1.0)))(expo-crypto@15.0.7(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.12)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)))(expo-linking@8.0.8(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.0)(react@19.1.0))(react@19.1.0))(expo-secure-store@15.0.7(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.12)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)))(expo-web-browser@15.0.8(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.12)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.0)(react@19.1.0))) + '@nestjs-cls/transactional': + specifier: ^3.1.1 + version: 3.1.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs-cls/transactional-adapter-mongoose': + specifier: ^1.1.25 + version: 1.1.25(@nestjs-cls/transactional@3.1.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(mongoose@9.0.0)(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs-cls/transactional-adapter-prisma': + specifier: ^1.3.1 + version: 1.3.1(@nestjs-cls/transactional@3.1.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@prisma/client@6.17.1(prisma@6.17.1(typescript@5.9.2))(typescript@5.9.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(prisma@6.17.1(typescript@5.9.2)) '@nestjs/common': specifier: ^11.0.1 version: 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -77,6 +86,9 @@ importers: mongoose: specifier: ^9.0.0 version: 9.0.0 + nestjs-cls: + specifier: ^6.1.0 + version: 6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-trpc: specifier: ^1.6.1 version: 1.6.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@trpc/server@11.1.2(typescript@5.9.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)(zod@3.25.5) @@ -1760,6 +1772,33 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@nestjs-cls/transactional-adapter-mongoose@1.1.25': + resolution: {integrity: sha512-UdzqunX9cyU9O/3m1SLsIimVv/cDc496PFfoJGXzlMEGXilWByLcjfZ1+n/6pTICI7qhFvM+PZBITyJTjR617Q==} + engines: {node: '>=18'} + peerDependencies: + '@nestjs-cls/transactional': ^3.1.1 + mongoose: '>= 8' + nestjs-cls: ^6.1.0 + + '@nestjs-cls/transactional-adapter-prisma@1.3.1': + resolution: {integrity: sha512-u9MmT1DmOuIbxcK6dSqNqMZu0M0YakEGl9PMG8TwN6gyU5AN7XwXYf3EOlIFG2z916pCqUn7+DXXnXd3kUC5PQ==} + engines: {node: '>=18'} + peerDependencies: + '@nestjs-cls/transactional': ^3.1.1 + '@prisma/client': '> 4 < 7' + nestjs-cls: ^6.1.0 + prisma: '> 4 < 7' + + '@nestjs-cls/transactional@3.1.1': + resolution: {integrity: sha512-wP45dYbhMmlxSuEyCpULr7/T67v6vMy0Wh2vDSlvEBm/Dx3FYLlFR+yWeqQWQW+bpGe7wusbip3cNZ9F/4izkQ==} + engines: {node: '>=18'} + peerDependencies: + '@nestjs/common': '>= 10 < 12' + '@nestjs/core': '>= 10 < 12' + nestjs-cls: ^6.1.0 + reflect-metadata: '*' + rxjs: '>= 7' + '@nestjs/cli@11.0.10': resolution: {integrity: sha512-4waDT0yGWANg0pKz4E47+nUrqIJv/UqrZ5wLPkCqc7oMGRMWKAaw1NDZ9rKsaqhqvxb2LfI5+uXOWr4yi94DOQ==} engines: {node: '>= 20.11'} @@ -5903,6 +5942,15 @@ packages: nested-error-stacks@2.0.1: resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} + nestjs-cls@6.1.0: + resolution: {integrity: sha512-n4T2aXy5cQxrB8VIGzUF3JBK8lXbRDaWdDS0g7uDKshvYn8NGWMe46PX1p+sb2SxiC0Gjrsff44hkk0kvpS+Ug==} + engines: {node: '>=18'} + peerDependencies: + '@nestjs/common': '>= 10 < 12' + '@nestjs/core': '>= 10 < 12' + reflect-metadata: '*' + rxjs: '>= 7' + nestjs-trpc@1.6.1: resolution: {integrity: sha512-FZ0n1n9czPaZ5HHoDM7hlWsR1g3C79MhZ1Dg1xidehrl9Agk4F3XX71aLh8fgVD5sNR08B3d/xITaLP3PkOUWw==} peerDependencies: @@ -5916,6 +5964,7 @@ packages: next@15.5.6: resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -9478,6 +9527,27 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nestjs-cls/transactional-adapter-mongoose@1.1.25(@nestjs-cls/transactional@3.1.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(mongoose@9.0.0)(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs-cls/transactional': 3.1.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + mongoose: 9.0.0 + nestjs-cls: 6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + + '@nestjs-cls/transactional-adapter-prisma@1.3.1(@nestjs-cls/transactional@3.1.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@prisma/client@6.17.1(prisma@6.17.1(typescript@5.9.2))(typescript@5.9.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(prisma@6.17.1(typescript@5.9.2))': + dependencies: + '@nestjs-cls/transactional': 3.1.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@prisma/client': 6.17.1(prisma@6.17.1(typescript@5.9.2))(typescript@5.9.2) + nestjs-cls: 6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + prisma: 6.17.1(typescript@5.9.2) + + '@nestjs-cls/transactional@3.1.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + nestjs-cls: 6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + '@nestjs/cli@11.0.10(@types/node@22.18.11)': dependencies: '@angular-devkit/core': 19.2.15(chokidar@4.0.3) @@ -14286,6 +14356,13 @@ snapshots: nested-error-stacks@2.0.1: {} + nestjs-cls@6.1.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2): + dependencies: + '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + nestjs-trpc@1.6.1(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@trpc/server@11.1.2(typescript@5.9.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)(zod@3.25.5): dependencies: '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2)