kybernus 2.0.10 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/package.json +1 -1
  2. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/PostgresUserRepository.java.hbs +40 -0
  3. package/templates/java-spring/clean/src/main/resources/application.properties.hbs +18 -0
  4. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{infrastructure/web/controller → adapters/inbound/web}/AuthController.java.hbs +4 -5
  5. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/persistence/JpaUserAdapter.java.hbs +40 -0
  6. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/persistence/entity/UserEntity.java.hbs +61 -0
  7. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/persistence/repository/JpaUserRepository.java.hbs +11 -0
  8. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{infrastructure/security/SecurityAdapters.java.hbs → adapters/outbound/security/SecurityAdapter.java.hbs} +14 -14
  9. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{domain/entity → core/domain}/User.java.hbs +2 -2
  10. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{domain/usecase → core/ports/inbound}/LoginUserUseCase.java.hbs +8 -8
  11. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{domain/usecase → core/ports/inbound}/RegisterUserUseCase.java.hbs +7 -8
  12. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{domain/repository → core/ports/outbound}/UserRepository.java.hbs +4 -4
  13. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{application → core}/service/AuthService.java.hbs +9 -9
  14. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/{{projectNamePascalCase}}Application.java.hbs +2 -2
  15. package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +18 -0
  16. package/templates/nestjs/clean/package.json.hbs +9 -3
  17. package/templates/nestjs/clean/prisma/schema.prisma.hbs +20 -0
  18. package/templates/nestjs/clean/src/app.module.ts.hbs +17 -0
  19. package/templates/nestjs/clean/src/auth.module.ts.hbs +12 -10
  20. package/templates/nestjs/clean/src/infrastructure/database/prisma.service.ts.hbs +13 -0
  21. package/templates/nestjs/clean/src/infrastructure/database/repositories/prisma.user.repository.ts.hbs +32 -0
  22. package/templates/nestjs/clean/src/main.ts.hbs +11 -0
  23. package/templates/nestjs/hexagonal/package.json.hbs +9 -3
  24. package/templates/nestjs/hexagonal/prisma/schema.prisma +20 -0
  25. package/templates/nestjs/hexagonal/src/adapters/outbound/persistence/prisma.service.ts.hbs +13 -0
  26. package/templates/nestjs/hexagonal/src/adapters/outbound/persistence/prisma.user.adapter.ts.hbs +32 -0
  27. package/templates/nestjs/hexagonal/src/app.module.ts.hbs +17 -0
  28. package/templates/nestjs/hexagonal/src/auth.module.ts.hbs +15 -13
  29. package/templates/nestjs/hexagonal/src/main.ts.hbs +11 -0
  30. package/templates/nextjs/mvc/package.json.hbs +35 -32
  31. package/templates/nextjs/mvc/prisma/schema.prisma.hbs +12 -9
  32. package/templates/nextjs/mvc/src/lib/db.ts +15 -0
  33. package/templates/nodejs-express/clean/docker-compose.yml.hbs +5 -6
  34. package/templates/nodejs-express/clean/package.json.hbs +14 -8
  35. package/templates/nodejs-express/clean/prisma/schema.prisma +20 -0
  36. package/templates/nodejs-express/clean/src/config/index.ts +27 -0
  37. package/templates/nodejs-express/clean/src/index.ts.hbs +20 -24
  38. package/templates/nodejs-express/clean/src/infrastructure/database/PrismaUserRepository.ts.hbs +61 -0
  39. package/templates/nodejs-express/clean/src/infrastructure/database/prisma.ts.hbs +5 -0
  40. package/templates/nodejs-express/clean/src/infrastructure/http/controllers/AuthController.ts.hbs +24 -40
  41. package/templates/nodejs-express/clean/src/infrastructure/http/middlewares/errorHandler.ts +24 -0
  42. package/templates/nodejs-express/clean/tsconfig.json.hbs +8 -17
  43. package/templates/nodejs-express/hexagonal/docker-compose.yml.hbs +5 -6
  44. package/templates/nodejs-express/hexagonal/package.json.hbs +14 -8
  45. package/templates/nodejs-express/hexagonal/prisma/schema.prisma +20 -0
  46. package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/AuthController.ts.hbs +29 -44
  47. package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/middlewares/errorHandler.ts +24 -0
  48. package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/PrismaUserAdapter.ts.hbs +61 -0
  49. package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/prisma.ts +5 -0
  50. package/templates/nodejs-express/hexagonal/src/config/index.ts +27 -0
  51. package/templates/nodejs-express/hexagonal/src/index.ts.hbs +24 -27
  52. package/templates/nodejs-express/hexagonal/tsconfig.json.hbs +8 -17
  53. package/templates/python-fastapi/clean/app/application/services/__init__.py +0 -0
  54. package/templates/python-fastapi/clean/app/application/services/user_service.py.hbs +20 -0
  55. package/templates/python-fastapi/clean/app/config.py.hbs +24 -0
  56. package/templates/python-fastapi/clean/app/infrastructure/database/models.py.hbs +24 -0
  57. package/templates/python-fastapi/clean/app/infrastructure/database/postgres_repository.py.hbs +62 -0
  58. package/templates/python-fastapi/clean/app/infrastructure/database/session.py.hbs +27 -0
  59. package/templates/python-fastapi/clean/app/infrastructure/http/auth_controller.py.hbs +14 -8
  60. package/templates/python-fastapi/clean/app/main.py.hbs +25 -3
  61. package/templates/python-fastapi/clean/requirements.txt.hbs +3 -1
  62. package/templates/python-fastapi/hexagonal/app/adapters/inbound/http_adapter.py.hbs +41 -17
  63. package/templates/python-fastapi/hexagonal/app/adapters/outbound/postgres_user_repository.py.hbs +50 -0
  64. package/templates/python-fastapi/hexagonal/app/config.py.hbs +20 -0
  65. package/templates/python-fastapi/hexagonal/app/infrastructure/database/models.py.hbs +24 -0
  66. package/templates/python-fastapi/hexagonal/app/infrastructure/database/session.py.hbs +20 -0
  67. package/templates/python-fastapi/hexagonal/app/main.py.hbs +22 -14
  68. package/templates/python-fastapi/hexagonal/requirements.txt.hbs +3 -1
  69. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/InMemoryUserRepository.java.hbs +0 -41
  70. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/infrastructure/persistence/InMemoryUserRepository.java.hbs +0 -41
  71. package/templates/nestjs/clean/src/infrastructure/database/in-memory.repository.ts.hbs +0 -17
  72. package/templates/nodejs-express/clean/src/infrastructure/database/InMemoryUserRepository.ts.hbs +0 -46
  73. package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/InMemoryUserAdapter.ts.hbs +0 -38
  74. /package/templates/python-fastapi/hexagonal/app/core/{ports.py.hbs → ports/ports.py.hbs} +0 -0
@@ -9,16 +9,18 @@
9
9
  "start": "node dist/index.js",
10
10
  "lint": "eslint src --ext .ts",
11
11
  "format": "prettier --write \"src/**/*.ts\"",
12
- "test": "jest"
12
+ "test": "jest",
13
+ "migrate:dev": "prisma migrate dev",
14
+ "migrate:deploy": "prisma migrate deploy",
15
+ "generate": "prisma generate"
13
16
  },
14
17
  "keywords": [
15
18
  "express",
16
19
  "api",
17
20
  "typescript",
18
- "mvc",
19
- "saas",
20
- "stripe",
21
- "auth"
21
+ "hexagonal-architecture",
22
+ "prisma",
23
+ "zod"
22
24
  ],
23
25
  "author": "",
24
26
  "license": "MIT",
@@ -30,7 +32,10 @@
30
32
  "morgan": "^1.10.0",
31
33
  "jsonwebtoken": "^9.0.2",
32
34
  "bcryptjs": "^2.4.3",
33
- "stripe": "^14.14.0"
35
+ "stripe": "^14.14.0",
36
+ "zod": "^3.22.4",
37
+ "express-async-errors": "^3.1.1",
38
+ "@prisma/client": "^5.10.2"
34
39
  },
35
40
  "devDependencies": {
36
41
  "@types/express": "^4.17.21",
@@ -47,9 +52,10 @@
47
52
  "@typescript-eslint/parser": "^6.21.0",
48
53
  "prettier": "^3.2.5",
49
54
  "jest": "^29.7.0",
50
- "@types/jest": "^29.5.12"
55
+ "@types/jest": "^29.5.12",
56
+ "prisma": "^5.10.2"
51
57
  },
52
58
  "engines": {
53
59
  "node": ">=18.0.0"
54
60
  }
55
- }
61
+ }
@@ -0,0 +1,20 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model User {
11
+ id String @id @default(uuid())
12
+ email String @unique
13
+ name String
14
+ password String
15
+ stripeCustomerId String? @map("stripe_customer_id")
16
+ createdAt DateTime @default(now()) @map("created_at")
17
+ updatedAt DateTime @updatedAt @map("updated_at")
18
+
19
+ @@map("users")
20
+ }
@@ -1,48 +1,33 @@
1
- import { Router, Request, Response } from 'express';
2
- import { IAuthPort } from '../../../core/ports/inbound/IAuthPort';
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { IAuthService } from '../../../core/ports/inbound/IAuthService';
3
+ import { z } from 'zod';
3
4
 
4
- /**
5
- * HTTP Auth Controller - Inbound Adapter
6
- * Adapts HTTP requests to the application port
7
- */
8
- export function createAuthController(authService: IAuthPort): Router {
9
- const router = Router();
5
+ const registerSchema = z.object({
6
+ email: z.string().email(),
7
+ name: z.string().min(2),
8
+ password: z.string().min(6),
9
+ });
10
10
 
11
- router.post('/register', async (req: Request, res: Response) => {
12
- try {
13
- const { email, name, password } = req.body;
14
- const result = await authService.register(email, name, password);
15
- res.status(201).json({
16
- token: result.token,
17
- user: { id: result.user.id, email: result.user.email, name: result.user.name },
18
- });
19
- } catch (error: any) {
20
- res.status(400).json({ error: error.message });
21
- }
22
- });
11
+ export class AuthController {
12
+ constructor(private authService: IAuthService) {}
23
13
 
24
- router.post('/login', async (req: Request, res: Response) => {
25
- try {
26
- const { email, password } = req.body;
27
- const result = await authService.login(email, password);
28
- res.json({
29
- token: result.token,
30
- user: { id: result.user.id, email: result.user.email, name: result.user.name },
31
- });
32
- } catch (error: any) {
33
- res.status(401).json({ error: error.message });
34
- }
35
- });
14
+ register = async (req: Request, res: Response, next: NextFunction) => {
15
+ try {
16
+ const { email, name, password } = registerSchema.parse(req.body);
17
+ const result = await this.authService.register(email, name, password);
18
+ res.status(201).json(result);
19
+ } catch (error) {
20
+ next(error);
21
+ }
22
+ };
36
23
 
37
- router.get('/me', async (req: Request, res: Response) => {
38
- const token = req.headers.authorization?.split(' ')[1];
39
- if (!token) return res.status(401).json({ error: 'No token' });
40
-
41
- const user = await authService.validateToken(token);
42
- if (!user) return res.status(401).json({ error: 'Invalid token' });
43
-
44
- res.json({ user: { id: user.id, email: user.email, name: user.name } });
45
- });
46
-
47
- return router;
48
- }
24
+ login = async (req: Request, res: Response, next: NextFunction) => {
25
+ try {
26
+ const { email, password } = req.body;
27
+ const result = await this.authService.login(email, password);
28
+ res.json(result);
29
+ } catch (error) {
30
+ next(error);
31
+ }
32
+ };
33
+ }
@@ -0,0 +1,24 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { ZodError } from 'zod';
3
+
4
+ export function errorHandler(
5
+ err: Error,
6
+ req: Request,
7
+ res: Response,
8
+ next: NextFunction
9
+ ) {
10
+ console.error(err);
11
+
12
+ if (err instanceof ZodError) {
13
+ return res.status(400).json({
14
+ error: 'Validation Error',
15
+ details: err.format(),
16
+ });
17
+ }
18
+
19
+ if (err.message === 'User already exists' || err.message === 'Invalid credentials') {
20
+ return res.status(400).json({ error: err.message });
21
+ }
22
+
23
+ res.status(500).json({ error: 'Internal Server Error' });
24
+ }
@@ -0,0 +1,61 @@
1
+ import { User } from '../../../core/domain/entities/User';
2
+ import { IUserRepositoryPort } from '../../../core/ports/outbound/IUserRepositoryPort';
3
+ import { prisma } from './prisma';
4
+
5
+ export class PrismaUserAdapter implements IUserRepositoryPort {
6
+ async findById(id: string): Promise<User | null> {
7
+ const user = await prisma.user.findUnique({ where: { id } });
8
+ if (!user) return null;
9
+ return User.restore({
10
+ id: user.id,
11
+ email: user.email,
12
+ name: user.name,
13
+ password: user.password,
14
+ stripeCustomerId: user.stripeCustomerId,
15
+ });
16
+ }
17
+
18
+ async findByEmail(email: string): Promise<User | null> {
19
+ const user = await prisma.user.findUnique({ where: { email } });
20
+ if (!user) return null;
21
+ return User.restore({
22
+ id: user.id,
23
+ email: user.email,
24
+ name: user.name,
25
+ password: user.password,
26
+ stripeCustomerId: user.stripeCustomerId,
27
+ });
28
+ }
29
+
30
+ async save(user: User): Promise<User> {
31
+ const data = {
32
+ id: user.id,
33
+ email: user.email,
34
+ name: user.name,
35
+ password: user.password,
36
+ stripeCustomerId: user.stripeCustomerId,
37
+ };
38
+
39
+ const savedUser = await prisma.user.upsert({
40
+ where: { id: user.id || '' },
41
+ update: { ...data },
42
+ create: { ...data },
43
+ });
44
+
45
+ return User.restore({
46
+ id: savedUser.id,
47
+ email: savedUser.email,
48
+ name: savedUser.name,
49
+ password: savedUser.password,
50
+ stripeCustomerId: savedUser.stripeCustomerId,
51
+ });
52
+ }
53
+
54
+ async update(user: User): Promise<User> {
55
+ return this.save(user);
56
+ }
57
+
58
+ async delete(id: string): Promise<void> {
59
+ await prisma.user.delete({ where: { id } });
60
+ }
61
+ }
@@ -0,0 +1,5 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ export const prisma = new PrismaClient({
4
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
5
+ });
@@ -0,0 +1,27 @@
1
+ import dotenv from 'dotenv';
2
+ import { z } from 'zod';
3
+
4
+ dotenv.config();
5
+
6
+ const envSchema = z.object({
7
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
8
+ PORT: z.string().default('3000'),
9
+ DATABASE_URL: z.string(),
10
+ JWT_SECRET: z.string().min(10),
11
+ STRIPE_SECRET_KEY: z.string().optional(),
12
+ });
13
+
14
+ const _env = envSchema.safeParse(process.env);
15
+
16
+ if (!_env.success) {
17
+ console.error('❌ Invalid environment variables:', _env.error.format());
18
+ throw new Error('Invalid environment variables');
19
+ }
20
+
21
+ export const config = {
22
+ env: _env.data.NODE_ENV,
23
+ port: parseInt(_env.data.PORT, 10),
24
+ databaseUrl: _env.data.DATABASE_URL,
25
+ jwtSecret: _env.data.JWT_SECRET,
26
+ stripeSecretKey: _env.data.STRIPE_SECRET_KEY,
27
+ };
@@ -1,41 +1,38 @@
1
+ import 'express-async-errors';
1
2
  import express from 'express';
2
3
  import cors from 'cors';
3
4
  import helmet from 'helmet';
4
5
  import morgan from 'morgan';
5
- import dotenv from 'dotenv';
6
-
7
- // Core
8
- import { AuthService } from './core/AuthService';
9
-
10
- // Adapters
11
- import { InMemoryUserAdapter } from './adapters/outbound/persistence/InMemoryUserAdapter';
12
- import { bcryptAdapter, jwtAdapter } from './adapters/outbound/SecurityAdapters';
13
- import { createAuthController } from './adapters/inbound/http/AuthController';
14
-
15
- dotenv.config();
6
+ import { config } from './config';
7
+ import { PrismaUserAdapter } from './adapters/outbound/persistence/PrismaUserAdapter';
8
+ import { AuthService } from './core/services/AuthService';
9
+ import { AuthController } from './adapters/inbound/http/AuthController';
10
+ import { errorHandler } from './adapters/inbound/http/middlewares/errorHandler';
16
11
 
17
12
  const app = express();
18
13
 
19
- // Middleware
20
- app.use(helmet());
14
+ // Middlewares
15
+ app.use(express.json());
21
16
  app.use(cors());
17
+ app.use(helmet());
22
18
  app.use(morgan('dev'));
23
- app.use(express.json());
24
19
 
25
- // Dependency Injection - Wire up the hexagon
26
- const userRepository = new InMemoryUserAdapter();
27
- const authService = new AuthService(userRepository, bcryptAdapter, jwtAdapter);
20
+ // Dependency Injection
21
+ const userRepository = new PrismaUserAdapter();
22
+ const authService = new AuthService(userRepository);
23
+ const authController = new AuthController(authService);
28
24
 
29
- // Mount adapters
30
- app.use('/api/auth', createAuthController(authService));
25
+ // Routes
26
+ app.post('/api/auth/register', (req, res, next) => authController.register(req, res, next));
27
+ app.post('/api/auth/login', (req, res, next) => authController.login(req, res, next));
31
28
 
32
- // Health check
33
- app.get('/health', (_, res) => res.json({ status: 'ok' }));
29
+ app.get('/health', (req, res) => {
30
+ res.json({ status: 'ok', architecture: 'hexagonal' });
31
+ });
34
32
 
35
- const PORT = process.env.PORT || 3000;
33
+ // Error Handler
34
+ app.use(errorHandler);
36
35
 
37
- app.listen(PORT, () => {
38
- console.log(`🚀 {{pascalCase projectName}} running on port ${PORT}`);
39
- console.log(`⬡ Architecture: Hexagonal (Ports & Adapters)`);
40
- console.log(`🔗 Health check: http://localhost:${PORT}/health`);
41
- });
36
+ app.listen(config.port, () => {
37
+ console.log(`🚀 Server running on port ${config.port}`);
38
+ });
@@ -1,27 +1,18 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2022",
3
+ "target": "es2020",
4
4
  "module": "commonjs",
5
- "lib": [
6
- "ES2022"
7
- ],
5
+ "lib": ["es2020"],
8
6
  "outDir": "./dist",
9
7
  "rootDir": "./src",
10
8
  "strict": true,
11
9
  "esModuleInterop": true,
12
10
  "skipLibCheck": true,
13
11
  "forceConsistentCasingInFileNames": true,
14
- "resolveJsonModule": true,
15
- "moduleResolution": "node",
16
- "types": [
17
- "node"
18
- ]
12
+ "experimentalDecorators": true,
13
+ "emitDecoratorMetadata": true,
14
+ "resolveJsonModule": true
19
15
  },
20
- "include": [
21
- "src/**/*"
22
- ],
23
- "exclude": [
24
- "node_modules",
25
- "dist"
26
- ]
27
- }
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"]
18
+ }
@@ -0,0 +1,20 @@
1
+ from typing import Optional
2
+ from app.domain.entities.user import User
3
+ from app.domain.repositories.user_repository import IUserRepository
4
+ from app.domain.usecases.register_user import RegisterUserUseCase, IPasswordHasher, ITokenGenerator
5
+
6
+ # In Clean Architecture, Services usually orchestrate Use Cases or coordinate cross-domain logic.
7
+ # For simple CRUD, Use Cases might be sufficient, but here is a Service example.
8
+
9
+ class UserService:
10
+ def __init__(self, repository: IUserRepository):
11
+ self.repository = repository
12
+
13
+ async def get_user(self, user_id: str) -> Optional[User]:
14
+ return await self.repository.find_by_id(user_id)
15
+
16
+ async def get_user_by_email(self, email: str) -> Optional[User]:
17
+ return await self.repository.find_by_email(email)
18
+
19
+ # Note: Registration logic is handled by RegisterUserUseCase, aligning with Clean Architecture
20
+ # where specific complex actions are Use Cases.
@@ -0,0 +1,24 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+ from functools import lru_cache
3
+
4
+ class Settings(BaseSettings):
5
+ PROJECT_NAME: str = "{{projectName}}"
6
+ API_V1_STR: str = "/api/v1"
7
+
8
+ # Database
9
+ # Use asyncpg for async SQLAlchemy
10
+ DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/{{projectName}}_db"
11
+
12
+ # Security
13
+ SECRET_KEY: str = "change_this_to_a_secure_random_key"
14
+ ALGORITHM: str = "HS256"
15
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
16
+
17
+ # Stripe
18
+ STRIPE_API_KEY: str | None = None
19
+
20
+ model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)
21
+
22
+ @lru_cache
23
+ def get_settings():
24
+ return Settings()
@@ -0,0 +1,24 @@
1
+ from sqlalchemy import Column, String, DateTime, Boolean
2
+ from sqlalchemy.sql import func
3
+ from app.infrastructure.database.session import Base
4
+ import uuid
5
+
6
+ class UserModel(Base):
7
+ __tablename__ = "users"
8
+
9
+ id = Column(String, primary_key=True, index=True, default=lambda: str(uuid.uuid4()))
10
+ email = Column(String, unique=True, index=True, nullable=False)
11
+ name = Column(String, nullable=False)
12
+ password = Column(String, nullable=False)
13
+ stripe_customer_id = Column(String, nullable=True)
14
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
15
+ is_active = Column(Boolean, default=True)
16
+
17
+ def to_dict(self):
18
+ return {
19
+ "id": self.id,
20
+ "email": self.email,
21
+ "name": self.name,
22
+ "stripe_customer_id": self.stripe_customer_id,
23
+ "created_at": self.created_at
24
+ }
@@ -0,0 +1,62 @@
1
+ from typing import Optional
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, delete
4
+ from app.domain.repositories.user_repository import IUserRepository
5
+ from app.domain.entities.user import User
6
+ from app.infrastructure.database.models import UserModel
7
+
8
+ class PostgresUserRepository(IUserRepository):
9
+ def __init__(self, session: AsyncSession):
10
+ self.session = session
11
+
12
+ def _to_entity(self, model: UserModel) -> User:
13
+ return User(
14
+ id=model.id,
15
+ email=model.email,
16
+ name=model.name,
17
+ password=model.password,
18
+ stripe_customer_id=model.stripe_customer_id,
19
+ created_at=model.created_at
20
+ )
21
+
22
+ async def find_by_id(self, user_id: str) -> Optional[User]:
23
+ result = await self.session.execute(select(UserModel).where(UserModel.id == user_id))
24
+ model = result.scalars().first()
25
+ return self._to_entity(model) if model else None
26
+
27
+ async def find_by_email(self, email: str) -> Optional[User]:
28
+ result = await self.session.execute(select(UserModel).where(UserModel.email == email))
29
+ model = result.scalars().first()
30
+ return self._to_entity(model) if model else None
31
+
32
+ async def save(self, user: User) -> User:
33
+ # Check if exists to decide update vs insert (simplistic approach)
34
+ # In a real app, optimize this or use merge
35
+ existing = await self.find_by_id(user.id)
36
+
37
+ if existing:
38
+ stmt = select(UserModel).where(UserModel.id == user.id)
39
+ result = await self.session.execute(stmt)
40
+ model = result.scalars().first()
41
+ model.email = user.email
42
+ model.name = user.name
43
+ model.password = user.password
44
+ model.stripe_customer_id = user.stripe_customer_id
45
+ else:
46
+ model = UserModel(
47
+ id=user.id,
48
+ email=user.email,
49
+ name=user.name,
50
+ password=user.password,
51
+ stripe_customer_id=user.stripe_customer_id,
52
+ created_at=user.created_at
53
+ )
54
+ self.session.add(model)
55
+
56
+ await self.session.commit()
57
+ await self.session.refresh(model)
58
+ return self._to_entity(model)
59
+
60
+ async def delete(self, user_id: str) -> None:
61
+ await self.session.execute(delete(UserModel).where(UserModel.id == user_id))
62
+ await self.session.commit()
@@ -0,0 +1,27 @@
1
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
2
+ from sqlalchemy.orm import DeclarativeBase
3
+ from app.config import get_settings
4
+
5
+ settings = get_settings()
6
+
7
+ engine = create_async_engine(
8
+ settings.DATABASE_URL,
9
+ echo=True, # Set to False in production
10
+ )
11
+
12
+ AsyncSessionLocal = async_sessionmaker(
13
+ bind=engine,
14
+ class_=AsyncSession,
15
+ expire_on_commit=False,
16
+ autoflush=False,
17
+ )
18
+
19
+ class Base(DeclarativeBase):
20
+ pass
21
+
22
+ async def get_db():
23
+ async with AsyncSessionLocal() as session:
24
+ try:
25
+ yield session
26
+ finally:
27
+ await session.close()
@@ -1,16 +1,19 @@
1
1
  from fastapi import APIRouter, HTTPException, Depends
2
2
  from pydantic import BaseModel, EmailStr
3
+ from sqlalchemy.ext.asyncio import AsyncSession
3
4
  from app.domain.usecases.register_user import RegisterUserUseCase
4
- from app.infrastructure.database.in_memory_repository import InMemoryUserRepository
5
+ from app.infrastructure.database.postgres_repository import PostgresUserRepository
5
6
  from app.infrastructure.security.adapters import BcryptHasher, JwtTokenGenerator
7
+ from app.infrastructure.database.session import get_db
6
8
 
7
9
  router = APIRouter()
8
10
 
9
- # Dependency Injection
10
- repo = InMemoryUserRepository()
11
- hasher = BcryptHasher()
12
- token_gen = JwtTokenGenerator()
13
- register_usecase = RegisterUserUseCase(repo, hasher, token_gen)
11
+ # Dependency Injection Factory
12
+ def get_register_usecase(db: AsyncSession = Depends(get_db)) -> RegisterUserUseCase:
13
+ repo = PostgresUserRepository(db)
14
+ hasher = BcryptHasher()
15
+ token_gen = JwtTokenGenerator()
16
+ return RegisterUserUseCase(repo, hasher, token_gen)
14
17
 
15
18
  class RegisterRequest(BaseModel):
16
19
  email: EmailStr
@@ -18,9 +21,12 @@ class RegisterRequest(BaseModel):
18
21
  password: str
19
22
 
20
23
  @router.post("/register")
21
- async def register(req: RegisterRequest):
24
+ async def register(
25
+ req: RegisterRequest,
26
+ usecase: RegisterUserUseCase = Depends(get_register_usecase)
27
+ ):
22
28
  try:
23
- result = await register_usecase.execute(req.email, req.name, req.password)
29
+ result = await usecase.execute(req.email, req.name, req.password)
24
30
  return result
25
31
  except ValueError as e:
26
32
  raise HTTPException(status_code=400, detail=str(e))
@@ -1,10 +1,32 @@
1
+ from contextlib import asynccontextmanager
1
2
  from fastapi import FastAPI
2
3
  from app.infrastructure.http import auth_controller
4
+ from app.config import get_settings
5
+ from app.infrastructure.database.session import engine, Base
3
6
 
4
- app = FastAPI(title="{{projectName}} - Clean Architecture")
7
+ settings = get_settings()
5
8
 
6
- app.include_router(auth_controller.router, prefix="/api/auth", tags=["Auth"])
9
+ @asynccontextmanager
10
+ async def lifespan(app: FastAPI):
11
+ # Create tables on startup (for development)
12
+ # In production, use Alembic migrations
13
+ async with engine.begin() as conn:
14
+ await conn.run_sync(Base.metadata.create_all)
15
+ yield
16
+ # Cleanup
17
+ await engine.dispose()
18
+
19
+ app = FastAPI(
20
+ title="{{projectName}} - Clean Architecture",
21
+ lifespan=lifespan
22
+ )
23
+
24
+ app.include_router(auth_controller.router, prefix=settings.API_V1_STR + "/auth", tags=["Auth"])
7
25
 
8
26
  @app.get("/health")
9
27
  def health():
10
- return {"status": "ok", "architecture": "clean"}
28
+ return {
29
+ "status": "ok",
30
+ "architecture": "clean",
31
+ "project": settings.PROJECT_NAME
32
+ }
@@ -1,12 +1,14 @@
1
1
  fastapi>=0.109.0
2
2
  uvicorn[standard]>=0.27.0
3
3
  pydantic>=2.5.0
4
+ pydantic-settings>=2.1.0
4
5
  python-dotenv>=1.0.0
5
6
  python-jose[cryptography]>=3.3.0
6
7
  passlib[bcrypt]>=1.7.4
7
8
  stripe>=8.0.0
8
9
  sqlalchemy>=2.0.0
9
10
  alembic>=1.13.0
11
+ asyncpg>=0.29.0
10
12
  psycopg2-binary>=2.9.9
11
13
  pytest>=8.0.0
12
- httpx>=0.26.0
14
+ httpx>=0.26.0