servcraft 0.1.0 → 0.1.1
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/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- 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 +0 -5
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Configuration
|
|
3
|
+
* Centralized database configuration management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DatabaseConfig } from '../database/interfaces/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load database configuration from environment variables
|
|
10
|
+
*/
|
|
11
|
+
export function loadDatabaseConfig(): DatabaseConfig {
|
|
12
|
+
const orm = (process.env.DATABASE_ORM || 'prisma') as DatabaseConfig['orm'];
|
|
13
|
+
const database = (process.env.DATABASE_TYPE || 'postgresql') as DatabaseConfig['database'];
|
|
14
|
+
|
|
15
|
+
// Primary configuration: DATABASE_URL
|
|
16
|
+
if (process.env.DATABASE_URL) {
|
|
17
|
+
return {
|
|
18
|
+
orm,
|
|
19
|
+
database,
|
|
20
|
+
url: process.env.DATABASE_URL,
|
|
21
|
+
logging: process.env.DATABASE_LOGGING === 'true',
|
|
22
|
+
ssl: process.env.DATABASE_SSL === 'true',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fallback: individual environment variables
|
|
27
|
+
const config: DatabaseConfig = {
|
|
28
|
+
orm,
|
|
29
|
+
database,
|
|
30
|
+
host: process.env.DATABASE_HOST || 'localhost',
|
|
31
|
+
port: process.env.DATABASE_PORT ? parseInt(process.env.DATABASE_PORT) : undefined,
|
|
32
|
+
username: process.env.DATABASE_USER,
|
|
33
|
+
password: process.env.DATABASE_PASSWORD,
|
|
34
|
+
databaseName: process.env.DATABASE_NAME,
|
|
35
|
+
logging: process.env.DATABASE_LOGGING === 'true',
|
|
36
|
+
ssl: process.env.DATABASE_SSL === 'true',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// MongoDB specific URL fallback
|
|
40
|
+
if (database === 'mongodb' && process.env.MONGODB_URI) {
|
|
41
|
+
config.url = process.env.MONGODB_URI;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Set default ports if not specified
|
|
45
|
+
if (!config.port && !config.url) {
|
|
46
|
+
switch (database) {
|
|
47
|
+
case 'postgresql':
|
|
48
|
+
config.port = 5432;
|
|
49
|
+
break;
|
|
50
|
+
case 'mysql':
|
|
51
|
+
case 'mariadb':
|
|
52
|
+
config.port = 3306;
|
|
53
|
+
break;
|
|
54
|
+
case 'mongodb':
|
|
55
|
+
config.port = 27017;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate database configuration
|
|
65
|
+
* @throws Error if configuration is invalid
|
|
66
|
+
*/
|
|
67
|
+
export function validateDatabaseConfig(config: DatabaseConfig): void {
|
|
68
|
+
// Check required fields
|
|
69
|
+
if (!config.orm) {
|
|
70
|
+
throw new Error('DATABASE_ORM is required');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!config.database) {
|
|
74
|
+
throw new Error('DATABASE_TYPE is required');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate ORM compatibility with database
|
|
78
|
+
const compatibility: Record<DatabaseConfig['orm'], DatabaseConfig['database'][]> = {
|
|
79
|
+
prisma: ['postgresql', 'mysql', 'sqlite', 'mongodb'],
|
|
80
|
+
mongoose: ['mongodb'],
|
|
81
|
+
sequelize: ['postgresql', 'mysql', 'sqlite', 'mariadb'],
|
|
82
|
+
typeorm: ['postgresql', 'mysql', 'sqlite', 'mongodb', 'mariadb'],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (!compatibility[config.orm]?.includes(config.database)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`ORM '${config.orm}' does not support database '${config.database}'. ` +
|
|
88
|
+
`Supported: ${compatibility[config.orm]?.join(', ') || 'none'}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check connection details
|
|
93
|
+
if (!config.url) {
|
|
94
|
+
if (config.database !== 'sqlite') {
|
|
95
|
+
if (!config.host) {
|
|
96
|
+
throw new Error('DATABASE_HOST or DATABASE_URL is required');
|
|
97
|
+
}
|
|
98
|
+
if (!config.databaseName) {
|
|
99
|
+
throw new Error('DATABASE_NAME or DATABASE_URL is required');
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
// SQLite needs at least a database name/path
|
|
103
|
+
if (!config.databaseName) {
|
|
104
|
+
config.databaseName = './dev.db';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate Mongoose specific
|
|
110
|
+
if (config.orm === 'mongoose' && config.database !== 'mongodb') {
|
|
111
|
+
throw new Error('Mongoose can only be used with MongoDB');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Validate MongoDB specific
|
|
115
|
+
if (
|
|
116
|
+
config.database === 'mongodb' &&
|
|
117
|
+
config.orm !== 'mongoose' &&
|
|
118
|
+
config.orm !== 'prisma' &&
|
|
119
|
+
config.orm !== 'typeorm'
|
|
120
|
+
) {
|
|
121
|
+
throw new Error(`MongoDB requires Mongoose, Prisma, or TypeORM (got ${config.orm})`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get recommended ORM for a database
|
|
127
|
+
*/
|
|
128
|
+
export function getRecommendedOrm(database: DatabaseConfig['database']): DatabaseConfig['orm'] {
|
|
129
|
+
switch (database) {
|
|
130
|
+
case 'mongodb':
|
|
131
|
+
return 'mongoose';
|
|
132
|
+
case 'postgresql':
|
|
133
|
+
case 'mysql':
|
|
134
|
+
case 'sqlite':
|
|
135
|
+
return 'prisma';
|
|
136
|
+
case 'mariadb':
|
|
137
|
+
return 'sequelize';
|
|
138
|
+
default:
|
|
139
|
+
return 'prisma';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get supported databases for an ORM
|
|
145
|
+
*/
|
|
146
|
+
export function getSupportedDatabases(orm: DatabaseConfig['orm']): DatabaseConfig['database'][] {
|
|
147
|
+
const support: Record<DatabaseConfig['orm'], DatabaseConfig['database'][]> = {
|
|
148
|
+
prisma: ['postgresql', 'mysql', 'sqlite', 'mongodb'],
|
|
149
|
+
mongoose: ['mongodb'],
|
|
150
|
+
sequelize: ['postgresql', 'mysql', 'sqlite', 'mariadb'],
|
|
151
|
+
typeorm: ['postgresql', 'mysql', 'sqlite', 'mongodb', 'mariadb'],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return support[orm] || [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Example .env template generator
|
|
159
|
+
*/
|
|
160
|
+
export function generateEnvTemplate(config: DatabaseConfig): string {
|
|
161
|
+
const lines: string[] = [
|
|
162
|
+
'# Database Configuration',
|
|
163
|
+
`DATABASE_ORM=${config.orm}`,
|
|
164
|
+
`DATABASE_TYPE=${config.database}`,
|
|
165
|
+
'',
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
if (config.url) {
|
|
169
|
+
lines.push(`DATABASE_URL=${config.url}`);
|
|
170
|
+
} else {
|
|
171
|
+
lines.push(
|
|
172
|
+
`DATABASE_HOST=${config.host || 'localhost'}`,
|
|
173
|
+
`DATABASE_PORT=${config.port || ''}`,
|
|
174
|
+
`DATABASE_USER=${config.username || ''}`,
|
|
175
|
+
`DATABASE_PASSWORD=${config.password || ''}`,
|
|
176
|
+
`DATABASE_NAME=${config.databaseName || 'mydb'}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push('', '# Optional', 'DATABASE_LOGGING=false', 'DATABASE_SSL=false');
|
|
181
|
+
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
package/src/config/env.ts
CHANGED
|
@@ -34,16 +34,6 @@ const envSchema = z.object({
|
|
|
34
34
|
// Redis (optional)
|
|
35
35
|
REDIS_URL: z.string().optional(),
|
|
36
36
|
|
|
37
|
-
// Swagger/OpenAPI
|
|
38
|
-
SWAGGER_ENABLED: z
|
|
39
|
-
.union([z.literal('true'), z.literal('false')])
|
|
40
|
-
.default('true')
|
|
41
|
-
.transform((val) => val === 'true'),
|
|
42
|
-
SWAGGER_ROUTE: z.string().default('/docs'),
|
|
43
|
-
SWAGGER_TITLE: z.string().default('Servcraft API'),
|
|
44
|
-
SWAGGER_DESCRIPTION: z.string().default('API documentation'),
|
|
45
|
-
SWAGGER_VERSION: z.string().default('1.0.0'),
|
|
46
|
-
|
|
47
37
|
// Logging
|
|
48
38
|
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
|
49
39
|
});
|
package/src/config/index.ts
CHANGED
|
@@ -32,13 +32,6 @@ export interface AppConfig {
|
|
|
32
32
|
redis: {
|
|
33
33
|
url?: string;
|
|
34
34
|
};
|
|
35
|
-
swagger: {
|
|
36
|
-
enabled: boolean;
|
|
37
|
-
route: string;
|
|
38
|
-
title: string;
|
|
39
|
-
description: string;
|
|
40
|
-
version: string;
|
|
41
|
-
};
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
function parseCorsOrigin(origin: string): string | string[] {
|
|
@@ -81,13 +74,6 @@ export function createConfig(): AppConfig {
|
|
|
81
74
|
redis: {
|
|
82
75
|
url: env.REDIS_URL,
|
|
83
76
|
},
|
|
84
|
-
swagger: {
|
|
85
|
-
enabled: env.SWAGGER_ENABLED,
|
|
86
|
-
route: env.SWAGGER_ROUTE,
|
|
87
|
-
title: env.SWAGGER_TITLE,
|
|
88
|
-
description: env.SWAGGER_DESCRIPTION,
|
|
89
|
-
version: env.SWAGGER_VERSION,
|
|
90
|
-
},
|
|
91
77
|
};
|
|
92
78
|
}
|
|
93
79
|
|
package/src/core/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
2
|
import type { FastifyInstance, FastifyServerOptions } from 'fastify';
|
|
3
|
-
import { logger
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
4
|
import type { Logger } from './logger.js';
|
|
5
5
|
|
|
6
6
|
export interface ServerConfig {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongoose Database Adapter
|
|
3
|
+
* Implements IDatabaseAdapter for Mongoose ORM (MongoDB)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import mongoose from 'mongoose';
|
|
7
|
+
import { logger } from '../../core/logger.js';
|
|
8
|
+
import type { IDatabaseAdapter, DatabaseConfig } from '../interfaces/index.js';
|
|
9
|
+
import { getDatabaseUrl } from '../connection.js';
|
|
10
|
+
|
|
11
|
+
export class MongooseAdapter implements IDatabaseAdapter {
|
|
12
|
+
private connection: typeof mongoose | null = null;
|
|
13
|
+
private config: DatabaseConfig;
|
|
14
|
+
private connected: boolean = false;
|
|
15
|
+
|
|
16
|
+
constructor(config: DatabaseConfig) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
|
|
19
|
+
if (config.database !== 'mongodb') {
|
|
20
|
+
throw new Error('MongooseAdapter only supports MongoDB');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Connect to MongoDB using Mongoose
|
|
26
|
+
*/
|
|
27
|
+
async connect(): Promise<void> {
|
|
28
|
+
if (this.connected && this.connection) {
|
|
29
|
+
logger.warn('Mongoose already connected');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const url = this.config.url || getDatabaseUrl(this.config);
|
|
35
|
+
|
|
36
|
+
this.connection = await mongoose.connect(url, {
|
|
37
|
+
// Modern Mongoose options
|
|
38
|
+
maxPoolSize: this.config.pool?.max || 10,
|
|
39
|
+
minPoolSize: this.config.pool?.min || 2,
|
|
40
|
+
socketTimeoutMS: 45000,
|
|
41
|
+
serverSelectionTimeoutMS: 5000,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.connected = true;
|
|
45
|
+
|
|
46
|
+
logger.info({ host: this.connection.connection.host }, 'Mongoose connected to MongoDB');
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.error({ err: error }, 'Failed to connect Mongoose');
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Mongoose connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Disconnect from MongoDB
|
|
57
|
+
*/
|
|
58
|
+
async disconnect(): Promise<void> {
|
|
59
|
+
if (!this.connection) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await mongoose.disconnect();
|
|
65
|
+
this.connection = null;
|
|
66
|
+
this.connected = false;
|
|
67
|
+
|
|
68
|
+
logger.info('Mongoose disconnected from MongoDB');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error({ err: error }, 'Error disconnecting Mongoose');
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Health check
|
|
77
|
+
*/
|
|
78
|
+
async healthCheck(): Promise<boolean> {
|
|
79
|
+
if (!this.connection || !this.connected) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Check connection state
|
|
85
|
+
return mongoose.connection.readyState === 1; // 1 = connected
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get ORM type
|
|
93
|
+
*/
|
|
94
|
+
getType(): 'mongoose' {
|
|
95
|
+
return 'mongoose';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get database type
|
|
100
|
+
*/
|
|
101
|
+
getDatabaseType(): 'mongodb' {
|
|
102
|
+
return 'mongodb';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get raw Mongoose instance
|
|
107
|
+
* Use with caution - breaks abstraction
|
|
108
|
+
*/
|
|
109
|
+
getRawClient(): typeof mongoose {
|
|
110
|
+
if (!this.connection) {
|
|
111
|
+
throw new Error('Mongoose not initialized. Call connect() first.');
|
|
112
|
+
}
|
|
113
|
+
return this.connection;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get Mongoose instance (typed helper)
|
|
118
|
+
*/
|
|
119
|
+
getMongoose(): typeof mongoose {
|
|
120
|
+
return this.getRawClient();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get native MongoDB connection
|
|
125
|
+
*/
|
|
126
|
+
getConnection(): mongoose.Connection {
|
|
127
|
+
if (!this.connection) {
|
|
128
|
+
throw new Error('Mongoose not initialized. Call connect() first.');
|
|
129
|
+
}
|
|
130
|
+
return this.connection.connection;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma Database Adapter
|
|
3
|
+
* Implements IDatabaseAdapter for Prisma ORM
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PrismaClient } from '@prisma/client';
|
|
7
|
+
import { logger } from '../../core/logger.js';
|
|
8
|
+
import type { IDatabaseAdapter, DatabaseConfig } from '../interfaces/index.js';
|
|
9
|
+
|
|
10
|
+
export class PrismaAdapter implements IDatabaseAdapter {
|
|
11
|
+
private client: PrismaClient | null = null;
|
|
12
|
+
private config: DatabaseConfig;
|
|
13
|
+
private connected: boolean = false;
|
|
14
|
+
|
|
15
|
+
constructor(config: DatabaseConfig) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Connect to database using Prisma
|
|
21
|
+
*/
|
|
22
|
+
async connect(): Promise<void> {
|
|
23
|
+
if (this.connected && this.client) {
|
|
24
|
+
logger.warn('Prisma client already connected');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
this.client = new PrismaClient({
|
|
30
|
+
datasources: {
|
|
31
|
+
db: {
|
|
32
|
+
url: this.config.url,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
log: this.config.logging ? ['query', 'info', 'warn', 'error'] : ['error'],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await this.client.$connect();
|
|
39
|
+
this.connected = true;
|
|
40
|
+
|
|
41
|
+
logger.info({ database: this.config.database }, 'Prisma connected successfully');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.error({ err: error }, 'Failed to connect Prisma');
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Prisma connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Disconnect from database
|
|
52
|
+
*/
|
|
53
|
+
async disconnect(): Promise<void> {
|
|
54
|
+
if (!this.client) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await this.client.$disconnect();
|
|
60
|
+
this.client = null;
|
|
61
|
+
this.connected = false;
|
|
62
|
+
|
|
63
|
+
logger.info('Prisma disconnected');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.error({ err: error }, 'Error disconnecting Prisma');
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Health check
|
|
72
|
+
*/
|
|
73
|
+
async healthCheck(): Promise<boolean> {
|
|
74
|
+
if (!this.client || !this.connected) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Try a simple query
|
|
80
|
+
await this.client.$queryRaw`SELECT 1`;
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get ORM type
|
|
89
|
+
*/
|
|
90
|
+
getType(): 'prisma' {
|
|
91
|
+
return 'prisma';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get database type
|
|
96
|
+
*/
|
|
97
|
+
getDatabaseType(): DatabaseConfig['database'] {
|
|
98
|
+
return this.config.database;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get raw Prisma client
|
|
103
|
+
* Use with caution - breaks abstraction
|
|
104
|
+
*/
|
|
105
|
+
getRawClient(): PrismaClient {
|
|
106
|
+
if (!this.client) {
|
|
107
|
+
throw new Error('Prisma client not initialized. Call connect() first.');
|
|
108
|
+
}
|
|
109
|
+
return this.client;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get Prisma client (typed helper)
|
|
114
|
+
*/
|
|
115
|
+
getClient(): PrismaClient {
|
|
116
|
+
return this.getRawClient();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Connection Factory
|
|
3
|
+
* Creates appropriate adapter based on configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../core/logger.js';
|
|
7
|
+
import type { IDatabaseAdapter, DatabaseConfig } from './interfaces/index.js';
|
|
8
|
+
|
|
9
|
+
// Lazy imports to avoid loading unused ORMs
|
|
10
|
+
let PrismaAdapter: any;
|
|
11
|
+
let MongooseAdapter: any;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Database Factory
|
|
15
|
+
* Singleton factory for creating and managing database adapters
|
|
16
|
+
*/
|
|
17
|
+
export class DatabaseFactory {
|
|
18
|
+
private static adapter: IDatabaseAdapter | null = null;
|
|
19
|
+
private static config: DatabaseConfig | null = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create database adapter based on configuration
|
|
23
|
+
* @param config - Database configuration
|
|
24
|
+
* @returns Database adapter instance
|
|
25
|
+
*/
|
|
26
|
+
static async createAdapter(config: DatabaseConfig): Promise<IDatabaseAdapter> {
|
|
27
|
+
this.config = config;
|
|
28
|
+
|
|
29
|
+
logger.info({ orm: config.orm, database: config.database }, 'Creating database adapter');
|
|
30
|
+
|
|
31
|
+
switch (config.orm) {
|
|
32
|
+
case 'prisma':
|
|
33
|
+
return this.createPrismaAdapter(config);
|
|
34
|
+
|
|
35
|
+
case 'mongoose':
|
|
36
|
+
return this.createMongooseAdapter(config);
|
|
37
|
+
|
|
38
|
+
case 'sequelize':
|
|
39
|
+
throw new Error('Sequelize support coming soon! Use Prisma or Mongoose for now.');
|
|
40
|
+
|
|
41
|
+
case 'typeorm':
|
|
42
|
+
throw new Error('TypeORM support coming soon! Use Prisma or Mongoose for now.');
|
|
43
|
+
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`Unsupported ORM: ${config.orm}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create Prisma adapter
|
|
51
|
+
*/
|
|
52
|
+
private static async createPrismaAdapter(config: DatabaseConfig): Promise<IDatabaseAdapter> {
|
|
53
|
+
// Validate Prisma supports this database
|
|
54
|
+
if (!['postgresql', 'mysql', 'sqlite', 'mongodb'].includes(config.database)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Prisma does not support ${config.database}. Use PostgreSQL, MySQL, SQLite, or MongoDB.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Lazy load Prisma adapter
|
|
61
|
+
if (!PrismaAdapter) {
|
|
62
|
+
const module = await import('./adapters/prisma.adapter.js');
|
|
63
|
+
PrismaAdapter = module.PrismaAdapter;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new PrismaAdapter(config);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create Mongoose adapter
|
|
71
|
+
*/
|
|
72
|
+
private static async createMongooseAdapter(config: DatabaseConfig): Promise<IDatabaseAdapter> {
|
|
73
|
+
// Validate Mongoose only supports MongoDB
|
|
74
|
+
if (config.database !== 'mongodb') {
|
|
75
|
+
throw new Error('Mongoose only supports MongoDB database.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Lazy load Mongoose adapter
|
|
79
|
+
if (!MongooseAdapter) {
|
|
80
|
+
const module = await import('./adapters/mongoose.adapter.js');
|
|
81
|
+
MongooseAdapter = module.MongooseAdapter;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return new MongooseAdapter(config);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get current adapter instance (singleton)
|
|
89
|
+
* @throws Error if no adapter initialized
|
|
90
|
+
*/
|
|
91
|
+
static getAdapter(): IDatabaseAdapter {
|
|
92
|
+
if (!this.adapter) {
|
|
93
|
+
throw new Error('Database adapter not initialized. Call DatabaseFactory.initialize() first.');
|
|
94
|
+
}
|
|
95
|
+
return this.adapter;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Initialize database connection
|
|
100
|
+
* @param config - Database configuration
|
|
101
|
+
*/
|
|
102
|
+
static async initialize(config: DatabaseConfig): Promise<IDatabaseAdapter> {
|
|
103
|
+
if (this.adapter) {
|
|
104
|
+
logger.warn('Database adapter already initialized. Disconnecting previous connection.');
|
|
105
|
+
await this.disconnect();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.adapter = await this.createAdapter(config);
|
|
109
|
+
await this.adapter.connect();
|
|
110
|
+
|
|
111
|
+
logger.info({ orm: config.orm, database: config.database }, 'Database initialized');
|
|
112
|
+
|
|
113
|
+
return this.adapter;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Disconnect from database
|
|
118
|
+
*/
|
|
119
|
+
static async disconnect(): Promise<void> {
|
|
120
|
+
if (this.adapter) {
|
|
121
|
+
await this.adapter.disconnect();
|
|
122
|
+
this.adapter = null;
|
|
123
|
+
logger.info('Database disconnected');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check database health
|
|
129
|
+
*/
|
|
130
|
+
static async healthCheck(): Promise<boolean> {
|
|
131
|
+
if (!this.adapter) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return this.adapter.healthCheck();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get current configuration
|
|
139
|
+
*/
|
|
140
|
+
static getConfig(): DatabaseConfig | null {
|
|
141
|
+
return this.config;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Helper function to get database connection URL from config
|
|
147
|
+
*/
|
|
148
|
+
export function getDatabaseUrl(config: DatabaseConfig): string {
|
|
149
|
+
if (config.url) {
|
|
150
|
+
return config.url;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Build URL from components
|
|
154
|
+
const { database, host, port, username, password, databaseName } = config;
|
|
155
|
+
|
|
156
|
+
switch (database) {
|
|
157
|
+
case 'postgresql':
|
|
158
|
+
return `postgresql://${username}:${password}@${host}:${port || 5432}/${databaseName}`;
|
|
159
|
+
|
|
160
|
+
case 'mysql':
|
|
161
|
+
case 'mariadb':
|
|
162
|
+
return `mysql://${username}:${password}@${host}:${port || 3306}/${databaseName}`;
|
|
163
|
+
|
|
164
|
+
case 'sqlite':
|
|
165
|
+
return `file:${databaseName || './dev.db'}`;
|
|
166
|
+
|
|
167
|
+
case 'mongodb':
|
|
168
|
+
return `mongodb://${host}:${port || 27017}/${databaseName}`;
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
throw new Error(`Cannot build URL for database: ${database}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validate ORM and database compatibility
|
|
177
|
+
*/
|
|
178
|
+
export function validateCompatibility(
|
|
179
|
+
orm: DatabaseConfig['orm'],
|
|
180
|
+
database: DatabaseConfig['database']
|
|
181
|
+
): boolean {
|
|
182
|
+
const compatibility: Record<DatabaseConfig['orm'], DatabaseConfig['database'][]> = {
|
|
183
|
+
prisma: ['postgresql', 'mysql', 'sqlite', 'mongodb'],
|
|
184
|
+
mongoose: ['mongodb'],
|
|
185
|
+
sequelize: ['postgresql', 'mysql', 'sqlite', 'mariadb'],
|
|
186
|
+
typeorm: ['postgresql', 'mysql', 'sqlite', 'mongodb', 'mariadb'],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return compatibility[orm]?.includes(database) || false;
|
|
190
|
+
}
|