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() {
);
@@ -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)