servcraft 0.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.
- package/.dockerignore +45 -0
- package/.env.example +46 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +11 -0
- package/Dockerfile +76 -0
- package/Dockerfile.dev +31 -0
- package/README.md +232 -0
- package/commitlint.config.js +24 -0
- package/dist/cli/index.cjs +3968 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +3945 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +2458 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +828 -0
- package/dist/index.d.ts +828 -0
- package/dist/index.js +2332 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.prod.yml +118 -0
- package/docker-compose.yml +147 -0
- package/eslint.config.js +27 -0
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
- package/npm-cache/_update-notifier-last-checked +0 -0
- package/package.json +112 -0
- package/prisma/schema.prisma +157 -0
- package/src/cli/commands/add-module.ts +422 -0
- package/src/cli/commands/db.ts +137 -0
- package/src/cli/commands/docs.ts +16 -0
- package/src/cli/commands/generate.ts +459 -0
- package/src/cli/commands/init.ts +640 -0
- package/src/cli/index.ts +32 -0
- package/src/cli/templates/controller.ts +67 -0
- package/src/cli/templates/dynamic-prisma.ts +89 -0
- package/src/cli/templates/dynamic-schemas.ts +232 -0
- package/src/cli/templates/dynamic-types.ts +60 -0
- package/src/cli/templates/module-index.ts +33 -0
- package/src/cli/templates/prisma-model.ts +17 -0
- package/src/cli/templates/repository.ts +104 -0
- package/src/cli/templates/routes.ts +70 -0
- package/src/cli/templates/schemas.ts +26 -0
- package/src/cli/templates/service.ts +58 -0
- package/src/cli/templates/types.ts +27 -0
- package/src/cli/utils/docs-generator.ts +47 -0
- package/src/cli/utils/field-parser.ts +315 -0
- package/src/cli/utils/helpers.ts +89 -0
- package/src/config/env.ts +80 -0
- package/src/config/index.ts +97 -0
- package/src/core/index.ts +5 -0
- package/src/core/logger.ts +43 -0
- package/src/core/server.ts +132 -0
- package/src/database/index.ts +7 -0
- package/src/database/prisma.ts +54 -0
- package/src/database/seed.ts +59 -0
- package/src/index.ts +63 -0
- package/src/middleware/error-handler.ts +73 -0
- package/src/middleware/index.ts +3 -0
- package/src/middleware/security.ts +116 -0
- package/src/modules/audit/audit.service.ts +192 -0
- package/src/modules/audit/index.ts +2 -0
- package/src/modules/audit/types.ts +37 -0
- package/src/modules/auth/auth.controller.ts +182 -0
- package/src/modules/auth/auth.middleware.ts +87 -0
- package/src/modules/auth/auth.routes.ts +123 -0
- package/src/modules/auth/auth.service.ts +142 -0
- package/src/modules/auth/index.ts +49 -0
- package/src/modules/auth/schemas.ts +52 -0
- package/src/modules/auth/types.ts +69 -0
- package/src/modules/email/email.service.ts +212 -0
- package/src/modules/email/index.ts +10 -0
- package/src/modules/email/templates.ts +213 -0
- package/src/modules/email/types.ts +57 -0
- package/src/modules/swagger/index.ts +3 -0
- package/src/modules/swagger/schema-builder.ts +263 -0
- package/src/modules/swagger/swagger.service.ts +169 -0
- package/src/modules/swagger/types.ts +68 -0
- package/src/modules/user/index.ts +30 -0
- package/src/modules/user/schemas.ts +49 -0
- package/src/modules/user/types.ts +78 -0
- package/src/modules/user/user.controller.ts +139 -0
- package/src/modules/user/user.repository.ts +156 -0
- package/src/modules/user/user.routes.ts +199 -0
- package/src/modules/user/user.service.ts +145 -0
- package/src/modules/validation/index.ts +18 -0
- package/src/modules/validation/validator.ts +104 -0
- package/src/types/common.ts +61 -0
- package/src/types/index.ts +10 -0
- package/src/utils/errors.ts +66 -0
- package/src/utils/index.ts +33 -0
- package/src/utils/pagination.ts +38 -0
- package/src/utils/response.ts +63 -0
- package/tests/integration/auth.test.ts +59 -0
- package/tests/setup.ts +17 -0
- package/tests/unit/modules/validation.test.ts +88 -0
- package/tests/unit/utils/errors.test.ts +113 -0
- package/tests/unit/utils/pagination.test.ts +82 -0
- package/tsconfig.json +33 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +34 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
|
|
2
|
+
d6bee00ec15bdca9908a56fdc20675409c9ed366 {"key":"pacote:tarball:file:/mnt/d4169edb-8c9f-4e15-9e9d-358608611b0c/Projetcts/Node.js/servcraft","integrity":"sha512-HNADRA1QCgSHYhqtHWQCl4NAaYl2YCBG244k+gPAHubAIsabBYL5aQQtlELuh2rDXAOOlg3UJ9HmIvokuOt9ug==","time":1766018965337,"size":220811}
|
|
3
|
+
9b065a6b5dd7776f840aa247a52a96b098d3280b {"key":"pacote:tarball:file:/mnt/d4169edb-8c9f-4e15-9e9d-358608611b0c/Projetcts/Node.js/servcraft","integrity":"sha512-4BLzYNyTFe5fF4RKDIwjPua/fDCDfEoC6g1Wxhx/erIcDpWOUO0sV8WfmDx2K5MFZ3jJAJsjmP/Cbe8Bg5mbEw==","time":1766019082154,"size":443837}
|
|
4
|
+
793f695e6f45c908cbf359d9bb41e39e78c1827f {"key":"pacote:tarball:file:/mnt/d4169edb-8c9f-4e15-9e9d-358608611b0c/Projetcts/Node.js/servcraft","integrity":"sha512-7bD64RYZAomPTJE8Z9f2zfa+BmWuw7OJucT08KEByh2lm63xtZxOADD1IjAjuNY8/lAcRqMsIMiV1Ps/EcoiMg==","time":1766019259320,"size":888564}
|
|
5
|
+
d817e3cb064934d074e652dfa4e82c5a29df0bda {"key":"pacote:tarball:file:/mnt/d4169edb-8c9f-4e15-9e9d-358608611b0c/Projetcts/Node.js/servcraft","integrity":"sha512-QlUotJPKSRgz5aqw6cMQjSmrPzbCSMqI9F1GMGdPzpEwlZ5WrjCHl6wrYyj6fwmmELlVDtCcuXHQOYdtKT/GnQ==","time":1766019329526,"size":1777318}
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "servcraft",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A modular, production-ready Node.js backend framework",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"servcraft": "dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx watch src/index.ts",
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"test:coverage": "vitest --coverage",
|
|
16
|
+
"lint": "eslint src --ext .ts",
|
|
17
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
18
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
19
|
+
"prepare": "command -v husky >/dev/null 2>&1 && husky install || echo \"husky not installed, skipping hooks\"",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"docs": "tsx src/cli/index.ts docs",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"db:generate": "prisma generate",
|
|
24
|
+
"db:migrate": "prisma migrate dev",
|
|
25
|
+
"db:push": "prisma db push",
|
|
26
|
+
"db:studio": "prisma studio",
|
|
27
|
+
"db:seed": "tsx src/database/seed.ts"
|
|
28
|
+
},
|
|
29
|
+
"prisma": {
|
|
30
|
+
"seed": "tsx src/database/seed.ts"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"backend",
|
|
34
|
+
"framework",
|
|
35
|
+
"nodejs",
|
|
36
|
+
"fastify",
|
|
37
|
+
"typescript",
|
|
38
|
+
"api",
|
|
39
|
+
"rest",
|
|
40
|
+
"modular",
|
|
41
|
+
"authentication",
|
|
42
|
+
"jwt"
|
|
43
|
+
],
|
|
44
|
+
"author": "Le-Sourcier",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@fastify/cors": "^9.0.1",
|
|
51
|
+
"@fastify/helmet": "^11.1.1",
|
|
52
|
+
"@fastify/jwt": "^8.0.1",
|
|
53
|
+
"@fastify/rate-limit": "^9.1.0",
|
|
54
|
+
"@fastify/cookie": "^9.3.1",
|
|
55
|
+
"@fastify/swagger": "^8.15.0",
|
|
56
|
+
"@fastify/swagger-ui": "^4.1.0",
|
|
57
|
+
"@prisma/client": "^5.22.0",
|
|
58
|
+
"fastify": "^4.28.1",
|
|
59
|
+
"pino": "^9.5.0",
|
|
60
|
+
"pino-pretty": "^11.3.0",
|
|
61
|
+
"zod": "^3.23.8",
|
|
62
|
+
"bcryptjs": "^2.4.3",
|
|
63
|
+
"dotenv": "^16.4.5",
|
|
64
|
+
"nodemailer": "^6.9.15",
|
|
65
|
+
"handlebars": "^4.7.8",
|
|
66
|
+
"commander": "^12.1.0",
|
|
67
|
+
"chalk": "^5.3.0",
|
|
68
|
+
"inquirer": "^12.1.0",
|
|
69
|
+
"ora": "^8.1.1"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@types/node": "^22.10.1",
|
|
73
|
+
"@types/bcryptjs": "^2.4.6",
|
|
74
|
+
"@types/nodemailer": "^6.4.17",
|
|
75
|
+
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
|
76
|
+
"@typescript-eslint/parser": "^8.17.0",
|
|
77
|
+
"eslint": "^9.16.0",
|
|
78
|
+
"eslint-config-prettier": "^9.1.0",
|
|
79
|
+
"eslint-plugin-prettier": "^5.2.1",
|
|
80
|
+
"prettier": "^3.4.2",
|
|
81
|
+
"husky": "^9.1.7",
|
|
82
|
+
"lint-staged": "^15.2.10",
|
|
83
|
+
"typescript": "^5.7.2",
|
|
84
|
+
"tsx": "^4.19.2",
|
|
85
|
+
"tsup": "^8.3.5",
|
|
86
|
+
"vitest": "^2.1.8",
|
|
87
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
88
|
+
"prisma": "^5.22.0",
|
|
89
|
+
"@commitlint/cli": "^19.6.0",
|
|
90
|
+
"@commitlint/config-conventional": "^19.6.0",
|
|
91
|
+
"typescript-eslint": "^8.17.0",
|
|
92
|
+
"@eslint/js": "^9.16.0"
|
|
93
|
+
},
|
|
94
|
+
"type": "module",
|
|
95
|
+
"lint-staged": {
|
|
96
|
+
"*.ts": [
|
|
97
|
+
"eslint --fix",
|
|
98
|
+
"prettier --write"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
"directories": {
|
|
102
|
+
"test": "tests"
|
|
103
|
+
},
|
|
104
|
+
"repository": {
|
|
105
|
+
"type": "git",
|
|
106
|
+
"url": "git+https://github.com/Le-Sourcier/servcraft.git"
|
|
107
|
+
},
|
|
108
|
+
"bugs": {
|
|
109
|
+
"url": "https://github.com/Le-Sourcier/servcraft/issues"
|
|
110
|
+
},
|
|
111
|
+
"homepage": "https://github.com/Le-Sourcier/servcraft#readme"
|
|
112
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Servcraft - Prisma Schema
|
|
2
|
+
// Supports: PostgreSQL, MySQL, SQLite
|
|
3
|
+
|
|
4
|
+
generator client {
|
|
5
|
+
provider = "prisma-client-js"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
datasource db {
|
|
9
|
+
provider = env("DATABASE_PROVIDER") // "postgresql", "mysql", or "sqlite"
|
|
10
|
+
url = env("DATABASE_URL")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ==========================================
|
|
14
|
+
// USER & AUTHENTICATION
|
|
15
|
+
// ==========================================
|
|
16
|
+
|
|
17
|
+
model User {
|
|
18
|
+
id String @id @default(uuid())
|
|
19
|
+
email String @unique
|
|
20
|
+
password String
|
|
21
|
+
name String?
|
|
22
|
+
role UserRole @default(USER)
|
|
23
|
+
status UserStatus @default(ACTIVE)
|
|
24
|
+
emailVerified Boolean @default(false)
|
|
25
|
+
lastLoginAt DateTime?
|
|
26
|
+
metadata Json?
|
|
27
|
+
|
|
28
|
+
createdAt DateTime @default(now())
|
|
29
|
+
updatedAt DateTime @updatedAt
|
|
30
|
+
|
|
31
|
+
// Relations
|
|
32
|
+
refreshTokens RefreshToken[]
|
|
33
|
+
sessions Session[]
|
|
34
|
+
auditLogs AuditLog[]
|
|
35
|
+
|
|
36
|
+
@@index([email])
|
|
37
|
+
@@index([status])
|
|
38
|
+
@@index([role])
|
|
39
|
+
@@map("users")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
enum UserRole {
|
|
43
|
+
USER
|
|
44
|
+
MODERATOR
|
|
45
|
+
ADMIN
|
|
46
|
+
SUPER_ADMIN
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
enum UserStatus {
|
|
50
|
+
ACTIVE
|
|
51
|
+
INACTIVE
|
|
52
|
+
SUSPENDED
|
|
53
|
+
BANNED
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
model RefreshToken {
|
|
57
|
+
id String @id @default(uuid())
|
|
58
|
+
token String @unique
|
|
59
|
+
userId String
|
|
60
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
61
|
+
expiresAt DateTime
|
|
62
|
+
createdAt DateTime @default(now())
|
|
63
|
+
|
|
64
|
+
@@index([userId])
|
|
65
|
+
@@index([token])
|
|
66
|
+
@@map("refresh_tokens")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
model Session {
|
|
70
|
+
id String @id @default(uuid())
|
|
71
|
+
userId String
|
|
72
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
73
|
+
userAgent String?
|
|
74
|
+
ipAddress String?
|
|
75
|
+
expiresAt DateTime
|
|
76
|
+
createdAt DateTime @default(now())
|
|
77
|
+
|
|
78
|
+
@@index([userId])
|
|
79
|
+
@@map("sessions")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ==========================================
|
|
83
|
+
// PASSWORD RESET
|
|
84
|
+
// ==========================================
|
|
85
|
+
|
|
86
|
+
model PasswordReset {
|
|
87
|
+
id String @id @default(uuid())
|
|
88
|
+
email String
|
|
89
|
+
token String @unique
|
|
90
|
+
expiresAt DateTime
|
|
91
|
+
used Boolean @default(false)
|
|
92
|
+
createdAt DateTime @default(now())
|
|
93
|
+
|
|
94
|
+
@@index([email])
|
|
95
|
+
@@index([token])
|
|
96
|
+
@@map("password_resets")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ==========================================
|
|
100
|
+
// EMAIL VERIFICATION
|
|
101
|
+
// ==========================================
|
|
102
|
+
|
|
103
|
+
model EmailVerification {
|
|
104
|
+
id String @id @default(uuid())
|
|
105
|
+
email String
|
|
106
|
+
token String @unique
|
|
107
|
+
expiresAt DateTime
|
|
108
|
+
verified Boolean @default(false)
|
|
109
|
+
createdAt DateTime @default(now())
|
|
110
|
+
|
|
111
|
+
@@index([email])
|
|
112
|
+
@@index([token])
|
|
113
|
+
@@map("email_verifications")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ==========================================
|
|
117
|
+
// AUDIT LOGS
|
|
118
|
+
// ==========================================
|
|
119
|
+
|
|
120
|
+
model AuditLog {
|
|
121
|
+
id String @id @default(uuid())
|
|
122
|
+
userId String?
|
|
123
|
+
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
124
|
+
action String
|
|
125
|
+
resource String
|
|
126
|
+
resourceId String?
|
|
127
|
+
oldValue Json?
|
|
128
|
+
newValue Json?
|
|
129
|
+
ipAddress String?
|
|
130
|
+
userAgent String?
|
|
131
|
+
metadata Json?
|
|
132
|
+
createdAt DateTime @default(now())
|
|
133
|
+
|
|
134
|
+
@@index([userId])
|
|
135
|
+
@@index([action])
|
|
136
|
+
@@index([resource])
|
|
137
|
+
@@index([createdAt])
|
|
138
|
+
@@map("audit_logs")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ==========================================
|
|
142
|
+
// SETTINGS / CONFIG
|
|
143
|
+
// ==========================================
|
|
144
|
+
|
|
145
|
+
model Setting {
|
|
146
|
+
id String @id @default(uuid())
|
|
147
|
+
key String @unique
|
|
148
|
+
value Json
|
|
149
|
+
type String @default("string")
|
|
150
|
+
group String @default("general")
|
|
151
|
+
createdAt DateTime @default(now())
|
|
152
|
+
updatedAt DateTime @updatedAt
|
|
153
|
+
|
|
154
|
+
@@index([key])
|
|
155
|
+
@@index([group])
|
|
156
|
+
@@map("settings")
|
|
157
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { ensureDir, writeFile, fileExists, success, error, info, warn, getModulesDir, getSourceDir } from '../utils/helpers.js';
|
|
7
|
+
|
|
8
|
+
// Pre-built modules that can be added
|
|
9
|
+
const AVAILABLE_MODULES = {
|
|
10
|
+
auth: {
|
|
11
|
+
name: 'Authentication',
|
|
12
|
+
description: 'JWT authentication with access/refresh tokens',
|
|
13
|
+
files: ['auth.service', 'auth.controller', 'auth.routes', 'auth.middleware', 'auth.schemas', 'auth.types', 'index'],
|
|
14
|
+
},
|
|
15
|
+
users: {
|
|
16
|
+
name: 'User Management',
|
|
17
|
+
description: 'User CRUD with RBAC (roles & permissions)',
|
|
18
|
+
files: ['user.service', 'user.controller', 'user.repository', 'user.routes', 'user.schemas', 'user.types', 'index'],
|
|
19
|
+
},
|
|
20
|
+
email: {
|
|
21
|
+
name: 'Email Service',
|
|
22
|
+
description: 'SMTP email with templates (Handlebars)',
|
|
23
|
+
files: ['email.service', 'email.templates', 'email.types', 'index'],
|
|
24
|
+
},
|
|
25
|
+
audit: {
|
|
26
|
+
name: 'Audit Logs',
|
|
27
|
+
description: 'Activity logging and audit trail',
|
|
28
|
+
files: ['audit.service', 'audit.types', 'index'],
|
|
29
|
+
},
|
|
30
|
+
upload: {
|
|
31
|
+
name: 'File Upload',
|
|
32
|
+
description: 'File upload with local/S3 storage',
|
|
33
|
+
files: ['upload.service', 'upload.controller', 'upload.routes', 'upload.types', 'index'],
|
|
34
|
+
},
|
|
35
|
+
cache: {
|
|
36
|
+
name: 'Redis Cache',
|
|
37
|
+
description: 'Redis caching service',
|
|
38
|
+
files: ['cache.service', 'cache.types', 'index'],
|
|
39
|
+
},
|
|
40
|
+
notifications: {
|
|
41
|
+
name: 'Notifications',
|
|
42
|
+
description: 'In-app and push notifications',
|
|
43
|
+
files: ['notification.service', 'notification.types', 'index'],
|
|
44
|
+
},
|
|
45
|
+
settings: {
|
|
46
|
+
name: 'Settings',
|
|
47
|
+
description: 'Application settings management',
|
|
48
|
+
files: ['settings.service', 'settings.controller', 'settings.routes', 'settings.types', 'index'],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const addModuleCommand = new Command('add')
|
|
53
|
+
.description('Add a pre-built module to your project')
|
|
54
|
+
.argument('[module]', 'Module to add (auth, users, email, audit, upload, cache, notifications, settings)')
|
|
55
|
+
.option('-l, --list', 'List available modules')
|
|
56
|
+
.action(async (moduleName?: string, options?: { list?: boolean }) => {
|
|
57
|
+
if (options?.list || !moduleName) {
|
|
58
|
+
console.log(chalk.bold('\n📦 Available Modules:\n'));
|
|
59
|
+
|
|
60
|
+
for (const [key, mod] of Object.entries(AVAILABLE_MODULES)) {
|
|
61
|
+
console.log(` ${chalk.cyan(key.padEnd(15))} ${mod.name}`);
|
|
62
|
+
console.log(` ${' '.repeat(15)} ${chalk.gray(mod.description)}\n`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.bold('Usage:'));
|
|
66
|
+
console.log(` ${chalk.yellow('servcraft add auth')} Add authentication module`);
|
|
67
|
+
console.log(` ${chalk.yellow('servcraft add users')} Add user management module`);
|
|
68
|
+
console.log(` ${chalk.yellow('servcraft add email')} Add email service module\n`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const module = AVAILABLE_MODULES[moduleName as keyof typeof AVAILABLE_MODULES];
|
|
73
|
+
|
|
74
|
+
if (!module) {
|
|
75
|
+
error(`Unknown module: ${moduleName}`);
|
|
76
|
+
info('Run "servcraft add --list" to see available modules');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const spinner = ora(`Adding ${module.name} module...`).start();
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const moduleDir = path.join(getModulesDir(), moduleName);
|
|
84
|
+
|
|
85
|
+
// Check if module already exists
|
|
86
|
+
if (await fileExists(moduleDir)) {
|
|
87
|
+
spinner.stop();
|
|
88
|
+
warn(`Module "${moduleName}" already exists`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await ensureDir(moduleDir);
|
|
93
|
+
|
|
94
|
+
// Generate module files based on type
|
|
95
|
+
switch (moduleName) {
|
|
96
|
+
case 'auth':
|
|
97
|
+
await generateAuthModule(moduleDir);
|
|
98
|
+
break;
|
|
99
|
+
case 'users':
|
|
100
|
+
await generateUsersModule(moduleDir);
|
|
101
|
+
break;
|
|
102
|
+
case 'email':
|
|
103
|
+
await generateEmailModule(moduleDir);
|
|
104
|
+
break;
|
|
105
|
+
case 'audit':
|
|
106
|
+
await generateAuditModule(moduleDir);
|
|
107
|
+
break;
|
|
108
|
+
case 'upload':
|
|
109
|
+
await generateUploadModule(moduleDir);
|
|
110
|
+
break;
|
|
111
|
+
case 'cache':
|
|
112
|
+
await generateCacheModule(moduleDir);
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
await generateGenericModule(moduleDir, moduleName);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
spinner.succeed(`${module.name} module added successfully!`);
|
|
119
|
+
|
|
120
|
+
console.log('\n📁 Files created:');
|
|
121
|
+
module.files.forEach((f) => success(` src/modules/${moduleName}/${f}.ts`));
|
|
122
|
+
|
|
123
|
+
console.log('\n📌 Next steps:');
|
|
124
|
+
info(' 1. Register the module in your main app file');
|
|
125
|
+
info(' 2. Configure any required environment variables');
|
|
126
|
+
info(' 3. Run database migrations if needed');
|
|
127
|
+
|
|
128
|
+
} catch (err) {
|
|
129
|
+
spinner.fail('Failed to add module');
|
|
130
|
+
error(err instanceof Error ? err.message : String(err));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
async function generateAuthModule(dir: string): Promise<void> {
|
|
135
|
+
// This would copy from templates or generate inline
|
|
136
|
+
// For now, we'll create placeholder files
|
|
137
|
+
const files = {
|
|
138
|
+
'auth.types.ts': `export interface JwtPayload {
|
|
139
|
+
sub: string;
|
|
140
|
+
email: string;
|
|
141
|
+
role: string;
|
|
142
|
+
type: 'access' | 'refresh';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface TokenPair {
|
|
146
|
+
accessToken: string;
|
|
147
|
+
refreshToken: string;
|
|
148
|
+
expiresIn: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface AuthUser {
|
|
152
|
+
id: string;
|
|
153
|
+
email: string;
|
|
154
|
+
role: string;
|
|
155
|
+
}
|
|
156
|
+
`,
|
|
157
|
+
'auth.schemas.ts': `import { z } from 'zod';
|
|
158
|
+
|
|
159
|
+
export const loginSchema = z.object({
|
|
160
|
+
email: z.string().email(),
|
|
161
|
+
password: z.string().min(1),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export const registerSchema = z.object({
|
|
165
|
+
email: z.string().email(),
|
|
166
|
+
password: z.string().min(8),
|
|
167
|
+
name: z.string().min(2).optional(),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export const refreshTokenSchema = z.object({
|
|
171
|
+
refreshToken: z.string().min(1),
|
|
172
|
+
});
|
|
173
|
+
`,
|
|
174
|
+
'index.ts': `export * from './auth.types.js';
|
|
175
|
+
export * from './auth.schemas.js';
|
|
176
|
+
// Export services, controllers, etc.
|
|
177
|
+
`,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
for (const [name, content] of Object.entries(files)) {
|
|
181
|
+
await writeFile(path.join(dir, name), content);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function generateUsersModule(dir: string): Promise<void> {
|
|
186
|
+
const files = {
|
|
187
|
+
'user.types.ts': `export type UserStatus = 'active' | 'inactive' | 'suspended' | 'banned';
|
|
188
|
+
export type UserRole = 'user' | 'admin' | 'moderator' | 'super_admin';
|
|
189
|
+
|
|
190
|
+
export interface User {
|
|
191
|
+
id: string;
|
|
192
|
+
email: string;
|
|
193
|
+
password: string;
|
|
194
|
+
name?: string;
|
|
195
|
+
role: UserRole;
|
|
196
|
+
status: UserStatus;
|
|
197
|
+
createdAt: Date;
|
|
198
|
+
updatedAt: Date;
|
|
199
|
+
}
|
|
200
|
+
`,
|
|
201
|
+
'user.schemas.ts': `import { z } from 'zod';
|
|
202
|
+
|
|
203
|
+
export const createUserSchema = z.object({
|
|
204
|
+
email: z.string().email(),
|
|
205
|
+
password: z.string().min(8),
|
|
206
|
+
name: z.string().min(2).optional(),
|
|
207
|
+
role: z.enum(['user', 'admin', 'moderator']).optional(),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export const updateUserSchema = z.object({
|
|
211
|
+
email: z.string().email().optional(),
|
|
212
|
+
name: z.string().min(2).optional(),
|
|
213
|
+
role: z.enum(['user', 'admin', 'moderator', 'super_admin']).optional(),
|
|
214
|
+
status: z.enum(['active', 'inactive', 'suspended', 'banned']).optional(),
|
|
215
|
+
});
|
|
216
|
+
`,
|
|
217
|
+
'index.ts': `export * from './user.types.js';
|
|
218
|
+
export * from './user.schemas.js';
|
|
219
|
+
`,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
for (const [name, content] of Object.entries(files)) {
|
|
223
|
+
await writeFile(path.join(dir, name), content);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function generateEmailModule(dir: string): Promise<void> {
|
|
228
|
+
const files = {
|
|
229
|
+
'email.types.ts': `export interface EmailOptions {
|
|
230
|
+
to: string | string[];
|
|
231
|
+
subject: string;
|
|
232
|
+
html?: string;
|
|
233
|
+
text?: string;
|
|
234
|
+
template?: string;
|
|
235
|
+
data?: Record<string, unknown>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface EmailResult {
|
|
239
|
+
success: boolean;
|
|
240
|
+
messageId?: string;
|
|
241
|
+
error?: string;
|
|
242
|
+
}
|
|
243
|
+
`,
|
|
244
|
+
'email.service.ts': `import nodemailer from 'nodemailer';
|
|
245
|
+
import type { EmailOptions, EmailResult } from './email.types.js';
|
|
246
|
+
|
|
247
|
+
export class EmailService {
|
|
248
|
+
private transporter;
|
|
249
|
+
|
|
250
|
+
constructor() {
|
|
251
|
+
this.transporter = nodemailer.createTransport({
|
|
252
|
+
host: process.env.SMTP_HOST,
|
|
253
|
+
port: parseInt(process.env.SMTP_PORT || '587', 10),
|
|
254
|
+
auth: {
|
|
255
|
+
user: process.env.SMTP_USER,
|
|
256
|
+
pass: process.env.SMTP_PASS,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async send(options: EmailOptions): Promise<EmailResult> {
|
|
262
|
+
try {
|
|
263
|
+
const result = await this.transporter.sendMail({
|
|
264
|
+
from: process.env.SMTP_FROM,
|
|
265
|
+
...options,
|
|
266
|
+
});
|
|
267
|
+
return { success: true, messageId: result.messageId };
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return { success: false, error: String(error) };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const emailService = new EmailService();
|
|
275
|
+
`,
|
|
276
|
+
'index.ts': `export * from './email.types.js';
|
|
277
|
+
export { EmailService, emailService } from './email.service.js';
|
|
278
|
+
`,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
for (const [name, content] of Object.entries(files)) {
|
|
282
|
+
await writeFile(path.join(dir, name), content);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function generateAuditModule(dir: string): Promise<void> {
|
|
287
|
+
const files = {
|
|
288
|
+
'audit.types.ts': `export interface AuditLogEntry {
|
|
289
|
+
userId?: string;
|
|
290
|
+
action: string;
|
|
291
|
+
resource: string;
|
|
292
|
+
resourceId?: string;
|
|
293
|
+
oldValue?: Record<string, unknown>;
|
|
294
|
+
newValue?: Record<string, unknown>;
|
|
295
|
+
ipAddress?: string;
|
|
296
|
+
userAgent?: string;
|
|
297
|
+
createdAt: Date;
|
|
298
|
+
}
|
|
299
|
+
`,
|
|
300
|
+
'audit.service.ts': `import type { AuditLogEntry } from './audit.types.js';
|
|
301
|
+
|
|
302
|
+
const logs: AuditLogEntry[] = [];
|
|
303
|
+
|
|
304
|
+
export class AuditService {
|
|
305
|
+
async log(entry: Omit<AuditLogEntry, 'createdAt'>): Promise<void> {
|
|
306
|
+
logs.push({ ...entry, createdAt: new Date() });
|
|
307
|
+
console.log('[AUDIT]', entry.action, entry.resource);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async query(filters: Partial<AuditLogEntry>): Promise<AuditLogEntry[]> {
|
|
311
|
+
return logs.filter((log) => {
|
|
312
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
313
|
+
if (log[key as keyof AuditLogEntry] !== value) return false;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export const auditService = new AuditService();
|
|
321
|
+
`,
|
|
322
|
+
'index.ts': `export * from './audit.types.js';
|
|
323
|
+
export { AuditService, auditService } from './audit.service.js';
|
|
324
|
+
`,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
for (const [name, content] of Object.entries(files)) {
|
|
328
|
+
await writeFile(path.join(dir, name), content);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function generateUploadModule(dir: string): Promise<void> {
|
|
333
|
+
const files = {
|
|
334
|
+
'upload.types.ts': `export interface UploadedFile {
|
|
335
|
+
id: string;
|
|
336
|
+
filename: string;
|
|
337
|
+
originalName: string;
|
|
338
|
+
mimetype: string;
|
|
339
|
+
size: number;
|
|
340
|
+
path: string;
|
|
341
|
+
url: string;
|
|
342
|
+
createdAt: Date;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface UploadOptions {
|
|
346
|
+
maxSize?: number;
|
|
347
|
+
allowedTypes?: string[];
|
|
348
|
+
destination?: string;
|
|
349
|
+
}
|
|
350
|
+
`,
|
|
351
|
+
'index.ts': `export * from './upload.types.js';
|
|
352
|
+
`,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
for (const [name, content] of Object.entries(files)) {
|
|
356
|
+
await writeFile(path.join(dir, name), content);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function generateCacheModule(dir: string): Promise<void> {
|
|
361
|
+
const files = {
|
|
362
|
+
'cache.types.ts': `export interface CacheOptions {
|
|
363
|
+
ttl?: number;
|
|
364
|
+
prefix?: string;
|
|
365
|
+
}
|
|
366
|
+
`,
|
|
367
|
+
'cache.service.ts': `import type { CacheOptions } from './cache.types.js';
|
|
368
|
+
|
|
369
|
+
// In-memory cache (replace with Redis in production)
|
|
370
|
+
const cache = new Map<string, { value: unknown; expiry: number }>();
|
|
371
|
+
|
|
372
|
+
export class CacheService {
|
|
373
|
+
async get<T>(key: string): Promise<T | null> {
|
|
374
|
+
const item = cache.get(key);
|
|
375
|
+
if (!item) return null;
|
|
376
|
+
if (Date.now() > item.expiry) {
|
|
377
|
+
cache.delete(key);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
return item.value as T;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async set(key: string, value: unknown, ttl = 3600): Promise<void> {
|
|
384
|
+
cache.set(key, { value, expiry: Date.now() + ttl * 1000 });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async del(key: string): Promise<void> {
|
|
388
|
+
cache.delete(key);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async clear(): Promise<void> {
|
|
392
|
+
cache.clear();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export const cacheService = new CacheService();
|
|
397
|
+
`,
|
|
398
|
+
'index.ts': `export * from './cache.types.js';
|
|
399
|
+
export { CacheService, cacheService } from './cache.service.js';
|
|
400
|
+
`,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
for (const [name, content] of Object.entries(files)) {
|
|
404
|
+
await writeFile(path.join(dir, name), content);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function generateGenericModule(dir: string, name: string): Promise<void> {
|
|
409
|
+
const files = {
|
|
410
|
+
[`${name}.types.ts`]: `// ${name} types
|
|
411
|
+
export interface ${name.charAt(0).toUpperCase() + name.slice(1)}Data {
|
|
412
|
+
// Define your types here
|
|
413
|
+
}
|
|
414
|
+
`,
|
|
415
|
+
'index.ts': `export * from './${name}.types.js';
|
|
416
|
+
`,
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
for (const [fileName, content] of Object.entries(files)) {
|
|
420
|
+
await writeFile(path.join(dir, fileName), content);
|
|
421
|
+
}
|
|
422
|
+
}
|