UNPKG

arvox-backend

Version:

Un framework backend moderne et modulaire basé sur Hono, TypeScript et l'architecture hexagonale avec authentification Better Auth + Drizzle intégrée

547 lines (475 loc) 16.1 kB
#!/usr/bin/env node import { Command } from 'commander'; import fs from 'fs/promises'; import path from 'path'; import { spawn } from 'child_process'; const program = new Command(); program .name('create-arvox-app') .description('CLI pour créer des projets avec arvox-backend') .version('1.0.0'); program .command('init <project-name>') .description('Créer un nouveau projet') .option('-p, --package-manager <pm>', 'Package manager à utiliser (npm, bun, pnpm)', 'npm') .action(async (projectName, options) => { await createProject(projectName, options.packageManager); }); async function createProject(projectName, packageManager) { console.log(`🚀 Création du projet ${projectName}...`); const projectDir = path.join(process.cwd(), projectName); try { // Créer le répertoire du projet await fs.mkdir(projectDir, { recursive: true }); // Générer les fichiers du template await generateBasicTemplate(projectDir, projectName); // Générer les configs de qualité projet (prettier, eslint, commitlint, hooks) await generateProjectConfigs(projectDir, packageManager); // Générer auth et schema systématiquement console.log('🔑 Génération de la configuration auth (Better Auth + Drizzle)...'); await generateAuthFiles(projectDir); console.log(`📦 Installation des dépendances avec ${packageManager}...`); // Installer les dépendances await installDependencies(projectDir, packageManager); console.log(`✅ Projet ${projectName} créé avec succès !`); console.log('\n📋 Prochaines étapes :'); console.log(` cd ${projectName}`); console.log(` ${packageManager} run dev`); } catch (error) { console.error('❌ Erreur lors de la création du projet :', error.message); process.exit(1); } } // Génère tous les fichiers nécessaires pour Better Auth + Drizzle async function generateAuthFiles(projectDir) { // Générer drizzle.config.ts à la racine du projet const drizzleConfig = `import type { Config } from 'drizzle-kit'; export default { schema: './src/infrastructure/database/schema.ts', out: './src/infrastructure/database/migrations', driver: 'pg', dbCredentials: { connectionString: process.env.DATABASE_URL || '' } } satisfies Config; `; await fs.writeFile(path.join(projectDir, 'drizzle.config.ts'), drizzleConfig, 'utf-8'); const join = path.join; const dbDir = join(projectDir, 'src', 'infrastructure', 'database'); const configDir = join(projectDir, 'src', 'infrastructure', 'config'); // 1. Générer un schema Drizzle minimal const schemaTs = `import { pgTable, text, boolean, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: text('id').primaryKey(), email: text('email').notNull().unique(), password: text('password'), firstname: text('firstname'), lastname: text('lastname'), isAdmin: boolean('is_admin').default(false), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow() }); export const sessions = pgTable('sessions', { id: text('id').primaryKey(), userId: text('user_id').notNull(), expiresAt: timestamp('expires_at').notNull() }); export const accounts = pgTable('accounts', { id: text('id').primaryKey(), userId: text('user_id').notNull(), provider: text('provider').notNull(), providerAccountId: text('provider_account_id').notNull() }); export const verifications = pgTable('verifications', { id: text('id').primaryKey(), userId: text('user_id').notNull(), token: text('token').notNull(), expiresAt: timestamp('expires_at').notNull() }); `; await fs.mkdir(dbDir, { recursive: true }); await fs.writeFile(join(dbDir, 'schema.ts'), schemaTs, 'utf-8'); // 2. Générer un client db minimal const dbTs = `import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; import * as schema from './schema'; const client = postgres(process.env.DATABASE_URL); export const db = drizzle(client, { schema }); `; await fs.writeFile(join(dbDir, 'db.ts'), dbTs, 'utf-8'); // 3. Générer le template minimal Better Auth config const authConfigTs = `import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { emailOTP } from 'better-auth/plugins'; import { Hono } from 'hono'; import { eq } from 'drizzle-orm'; import { db } from '../database/db'; import { users } from '../database/schema'; // Remplacez ceci par votre propre fonction d'envoi d'email OTP async function sendOTPEmail(params) { // params: { email, otp } // TODO: Intégrez votre service d'email ici } export const auth = betterAuth({ plugins: [ emailOTP({ expiresIn: 300, // 5 minutes otpLength: 6, async sendVerificationOTP({ email, otp }) { await sendOTPEmail({ email, otp }); } }) ], database: drizzleAdapter(db, { provider: 'pg' }), baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000', trustedOrigins: [ process.env.BETTER_AUTH_URL || 'http://localhost:3000', process.env.REACT_APP_URL || 'http://localhost:5173' ], user: { modelName: 'users', additionalFields: { firstname: { type: 'string', default: '', returned: true }, lastname: { type: 'string', default: '', returned: true }, isAdmin: { type: 'boolean', default: false, returned: true } } }, session: { modelName: 'sessions' }, account: { modelName: 'accounts' }, verification: { modelName: 'verifications' }, emailAndPassword: { enabled: true, minPasswordLength: 8, requireEmailVerification: false } }); // Router Better Auth avec update lastLoginAt const router = new Hono({ strict: false }); router.on(['POST', 'GET'], '/auth/*', async (c) => { const path = c.req.path; const response = await auth.handler(c.req.raw); if ( c.req.method === 'POST' && (path === '/api/auth/sign-in/email' || path === '/api/auth/sign-in/email-otp') ) { try { const body = await response.text(); const data = JSON.parse(body); if (data?.user?.id) { const now = new Date(); await db .update(users) .set({ lastLoginAt: now, updatedAt: now }) .where(eq(users.id, data.user.id)) .returning({ lastLoginAt: users.lastLoginAt }); } return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }); } catch (error) { console.error('Failed to update last login date:', error); } } return response; }); export default router; `; await fs.mkdir(configDir, { recursive: true }); await fs.writeFile(join(configDir, 'auth.config.ts'), authConfigTs, 'utf-8'); // 4. Générer un .env.example const envExample = `DATABASE_URL=postgresql://postgres:password@localhost:5432/default_db?search_path=public BETTER_AUTH_SECRET=ZAyWnPtauC0eytcpaueedNSvosqAVdDe BETTER_AUTH_URL=http://localhost:3000 NODE_ENV="development" REACT_APP_URL=http://localhost:5173 `; await fs.writeFile(join(projectDir, '.env.example'), envExample, 'utf-8'); console.log('✅ Auth (Better Auth + Drizzle) généré dans le projet.'); } async function generateBasicTemplate(projectDir, projectName) { // package.json const packageJson = { name: projectName, version: '1.0.0', description: 'API créée avec arvox-backend', main: 'dist/index.js', scripts: { dev: 'tsx watch src/index.ts', build: 'tsc', start: 'node dist/index.js', }, dependencies: { 'arvox-backend': '^1.1.6', '@hono/node-server': '^1.8.2', 'drizzle-orm': '^0.43.1', 'dotenv': '^16.5.0', "better-auth": "^1.2.7", 'hono': '^4.7.5', 'postgres': '^3.4.5', }, devDependencies: { '@commitlint/cli': '^19.8.0', '@commitlint/config-conventional': '^19.8.0', '@kolhe/eslint-config': '2.2.6', '@types/node': '^20.0.0', 'tsx': '^4.0.0', 'typescript': '^5.0.0', 'drizzle-kit': '^0.31.1', 'tsc-alias': '^1.8.13', 'prettier': '^3.5.3', 'eslint': '^9.23.0', 'simple-git-hooks': '^2.12.1', 'eslint-plugin-prettier': '^5.2.5', } }; await fs.writeFile( path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2) ); // tsconfig.json const tsConfig = { compilerOptions: { target: 'ES2022', module: 'ESNext', moduleResolution: 'node', outDir: './dist', rootDir: './src', strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, declaration: true, declarationMap: true, sourceMap: true }, include: ['src/**/*'], exclude: ['node_modules', 'dist'] }; await fs.writeFile( path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2) ); // Créer le dossier src, controllers, modules await fs.mkdir(path.join(projectDir, 'src'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'application', 'services'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'application', 'use-cases'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'domain', 'models'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'domain', 'repositories'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'domain', 'types'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'infrastructure', 'config'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'infrastructure', 'controllers'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'infrastructure', 'database'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'infrastructure', 'middlewares'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'infrastructure', 'repositories'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'src', 'infrastructure', 'modules'), { recursive: true }); // index.ts principal const indexTs = `import { serve } from '@hono/node-server'; import { ArvoxFramework } from 'arvox-backend'; import { HealthModule } from './infrastructure/modules/health.module'; import router from './infrastructure/config/auth.config'; const app = new ArvoxFramework({ appName: '${projectName} API', version: '1.0.0', description: 'API créée avec arvox-backend', router, // Utiliser le router Better Auth }); // Enregistrer le module Health app.registerModule(new HealthModule()); app.start(); `; await fs.writeFile(path.join(projectDir, 'src', 'index.ts'), indexTs); // HealthController const healthController = `import { BaseController } from 'arvox-backend'; import { z } from 'zod'; export class HealthController extends BaseController { initRoutes() { this.createListRoute( '/health', { response: z.object({ status: z.string(), timestamp: z.string(), uptime: z.number() }), tag: 'Health', summary: "Vérification de l'état du serveur", description: 'Retourne le statut de santé du serveur' }, async (c) => { return c.json({ success: true, data: { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime() } }); } ); } } `; await fs.writeFile( path.join(projectDir, 'src', 'infrastructure', 'controllers', 'health.controller.ts'), healthController ); // HealthModule const healthModule = `import { IModule } from 'arvox-backend'; import { HealthController } from '../controllers/health.controller'; export class HealthModule implements IModule { private controller: HealthController; constructor() { this.controller = new HealthController(); } getName() { return 'health'; } async initialize() { // Initialisation éventuelle } registerRoutes(app:any) { app.route('/health', this.controller.controller); } async cleanup() { // Nettoyage éventuel } async healthCheck() { return { healthy: true }; } } `; await fs.writeFile( path.join(projectDir, 'src', 'infrastructure', 'modules', 'health.module.ts'), healthModule ); // README.md const readme = `# ${projectName} API créée avec [arvox-backend](https://github.com/armelgeek/arvox-backend). ## Démarrage rapide \`\`\`bash npm run dev \`\`\` L'API sera disponible sur http://localhost:3000 ## Documentation - Health check : GET /health - Documentation OpenAPI : GET /doc ## Scripts - \`npm run dev\` : Démarrer en mode développement - \`npm run build\` : Compiler le projet - \`npm run start\` : Démarrer en mode production `; await fs.writeFile(path.join(projectDir, 'README.md'), readme); } // Génère prettier, eslint, commitlint, simple-git-hooks dans le projet async function generateProjectConfigs(projectDir, packageManager) { const join = path.join; // prettier.config.js const prettierConfig = `export default { semi: false, trailingComma: 'none', singleQuote: true, printWidth: 120, tabWidth: 2 } `; await fs.writeFile(join(projectDir, 'prettier.config.js'), prettierConfig, 'utf-8'); // eslint.config.js const eslintConfig = `import { config } from '@kolhe/eslint-config' export default config( [ { files: ['src/**/*.ts'], rules: { 'import/no-default-export': 'off' } }, { files: ['db/**/*'], rules: { 'unicorn/filename-case': 'off', 'no-console': 'off' } }, { files: ['**/*.test.ts'], rules: { 'unicorn/filename-case': 'off', 'no-console': 'off', 'import/no-default-export': 'off' } } ], { prettier: true, markdown: true, ignorePatterns: ['docs', 'db/**', '.github'] } ) `; await fs.writeFile(join(projectDir, 'eslint.config.js'), eslintConfig, 'utf-8'); // commitlint.config.js const commitlintConfig = `export default { extends: ['@commitlint/config-conventional'] } `; await fs.writeFile(join(projectDir, 'commitlint.config.js'), commitlintConfig, 'utf-8'); // Ajout simple-git-hooks et scripts dans package.json const pkgPath = join(projectDir, 'package.json'); let pkgRaw; try { pkgRaw = await fs.readFile(pkgPath, 'utf-8'); } catch { pkgRaw = null; } if (pkgRaw) { const pkg = JSON.parse(pkgRaw); // Scripts adaptés au gestionnaire de paquets const pm = packageManager; pkg.scripts = { ...pkg.scripts, build: 'tsc --noEmitOnError false && tsc-alias', format: `${pm} run prettier --write "./**/*.{js,ts,json}"`, lint: `${pm} run eslint .`, 'lint:fix': `${pm} run lint --fix`, 'db:generate': `${pm} run drizzle-kit generate`, 'db:check': 'npx drizzle-kit check', 'db:migrate': 'tsx ./db/migrate.ts', 'db:studio': `${pm} run drizzle-kit studio`, 'db:push': `${pm} run drizzle-kit push`, 'db:drop': 'tsx ./db/reset.ts', 'db:seed': `${pm} run ./db/seed.js`, 'db:reset': 'tsx ./db/reset.ts', 'db:update': `${pm} run db:generate && ${pm} run db:migrate` }; pkg['simple-git-hooks'] = { 'pre-commit': `${pm} run lint && ${pm} run format`, 'commit-msg': `${pm} run commitlint --edit $1` }; await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8'); } } function installDependencies(projectDir, packageManager) { return new Promise((resolve, reject) => { const child = spawn(packageManager, ['install'], { cwd: projectDir, stdio: 'inherit' }); child.on('close', (code) => { if (code !== 0) { reject(new Error(`${packageManager} install failed with code ${code}`)); } else { resolve(); } }); }); } program.parse();