@rapidd/core 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 (59) hide show
  1. package/.dockerignore +71 -0
  2. package/.env.example +70 -0
  3. package/.gitignore +11 -0
  4. package/LICENSE +15 -0
  5. package/README.md +231 -0
  6. package/bin/cli.js +145 -0
  7. package/config/app.json +166 -0
  8. package/config/rate-limit.json +12 -0
  9. package/dist/main.js +26 -0
  10. package/dockerfile +57 -0
  11. package/locales/ar_SA.json +179 -0
  12. package/locales/de_DE.json +179 -0
  13. package/locales/en_US.json +180 -0
  14. package/locales/es_ES.json +179 -0
  15. package/locales/fr_FR.json +179 -0
  16. package/locales/it_IT.json +179 -0
  17. package/locales/ja_JP.json +179 -0
  18. package/locales/pt_BR.json +179 -0
  19. package/locales/ru_RU.json +179 -0
  20. package/locales/tr_TR.json +179 -0
  21. package/main.ts +25 -0
  22. package/package.json +126 -0
  23. package/prisma/schema.prisma +9 -0
  24. package/prisma.config.ts +12 -0
  25. package/public/static/favicon.ico +0 -0
  26. package/public/static/image/logo.png +0 -0
  27. package/routes/api/v1/index.ts +113 -0
  28. package/src/app.ts +197 -0
  29. package/src/auth/Auth.ts +446 -0
  30. package/src/auth/stores/ISessionStore.ts +19 -0
  31. package/src/auth/stores/MemoryStore.ts +70 -0
  32. package/src/auth/stores/RedisStore.ts +92 -0
  33. package/src/auth/stores/index.ts +149 -0
  34. package/src/config/acl.ts +9 -0
  35. package/src/config/rls.ts +38 -0
  36. package/src/core/dmmf.ts +226 -0
  37. package/src/core/env.ts +183 -0
  38. package/src/core/errors.ts +87 -0
  39. package/src/core/i18n.ts +144 -0
  40. package/src/core/middleware.ts +123 -0
  41. package/src/core/prisma.ts +236 -0
  42. package/src/index.ts +112 -0
  43. package/src/middleware/model.ts +61 -0
  44. package/src/orm/Model.ts +881 -0
  45. package/src/orm/QueryBuilder.ts +2078 -0
  46. package/src/plugins/auth.ts +162 -0
  47. package/src/plugins/language.ts +79 -0
  48. package/src/plugins/rateLimit.ts +210 -0
  49. package/src/plugins/response.ts +80 -0
  50. package/src/plugins/rls.ts +51 -0
  51. package/src/plugins/security.ts +23 -0
  52. package/src/plugins/upload.ts +299 -0
  53. package/src/types.ts +308 -0
  54. package/src/utils/ApiClient.ts +526 -0
  55. package/src/utils/Mailer.ts +348 -0
  56. package/src/utils/index.ts +25 -0
  57. package/templates/email/example.ejs +17 -0
  58. package/templates/layouts/email.ejs +35 -0
  59. package/tsconfig.json +33 -0
@@ -0,0 +1,149 @@
1
+ import { ISessionStore } from './ISessionStore';
2
+ import { MemoryStore } from './MemoryStore';
3
+ import { RedisStore } from './RedisStore';
4
+
5
+ const builtInStores: Record<string, new (options?: any) => ISessionStore> = {
6
+ memory: MemoryStore,
7
+ redis: RedisStore,
8
+ };
9
+
10
+ /**
11
+ * Central session store manager with automatic fallback.
12
+ * Handles store failures transparently and switches to fallback.
13
+ */
14
+ export class SessionStoreManager extends ISessionStore {
15
+ private ttl: number;
16
+ private storeName: string;
17
+ private healthCheckInterval: number;
18
+ private _primaryStore: ISessionStore | null = null;
19
+ private _fallbackStore: ISessionStore | null = null;
20
+ private _usingFallback = false;
21
+ private _initialized = false;
22
+ private _healthCheckTimer: ReturnType<typeof setInterval> | null = null;
23
+
24
+ constructor(options: { ttl?: number; store?: string; healthCheckInterval?: number } = {}) {
25
+ super();
26
+ this.ttl = options.ttl || parseInt(process.env.AUTH_SESSION_TTL || '86400', 10);
27
+ this.storeName = (options.store || process.env.AUTH_SESSION_STORAGE || 'redis').toLowerCase();
28
+ this.healthCheckInterval = options.healthCheckInterval || 30000;
29
+ }
30
+
31
+ private async _ensureInitialized(): Promise<void> {
32
+ if (this._initialized) return;
33
+
34
+ this._fallbackStore = new MemoryStore({ ttl: this.ttl });
35
+
36
+ if (this.storeName === 'memory') {
37
+ this._primaryStore = this._fallbackStore;
38
+ this._initialized = true;
39
+ return;
40
+ }
41
+
42
+ const StoreClass = builtInStores[this.storeName];
43
+ if (!StoreClass) {
44
+ console.warn(`[SessionStore] Unknown store "${this.storeName}", using memory`);
45
+ this._primaryStore = this._fallbackStore;
46
+ this._initialized = true;
47
+ return;
48
+ }
49
+
50
+ try {
51
+ this._primaryStore = new StoreClass({ ttl: this.ttl });
52
+ } catch (err) {
53
+ console.warn(`[SessionStore] Failed to create ${this.storeName}: ${(err as Error).message}, using memory`);
54
+ this._primaryStore = this._fallbackStore;
55
+ }
56
+
57
+ this._initialized = true;
58
+
59
+ if (this._primaryStore !== this._fallbackStore) {
60
+ this._startHealthCheck();
61
+ }
62
+ }
63
+
64
+ private _startHealthCheck(): void {
65
+ if (this._healthCheckTimer) return;
66
+ this._healthCheckTimer = setInterval(() => this._checkPrimaryHealth(), this.healthCheckInterval);
67
+ }
68
+
69
+ private async _checkPrimaryHealth(): Promise<void> {
70
+ try {
71
+ const isHealthy = await this._primaryStore!.isHealthy();
72
+ if (isHealthy && this._usingFallback) {
73
+ console.log(`[SessionStore] ${this.storeName} recovered, switching back from memory`);
74
+ this._usingFallback = false;
75
+ } else if (!isHealthy && !this._usingFallback) {
76
+ console.warn(`[SessionStore] ${this.storeName} unavailable, switching to memory`);
77
+ this._usingFallback = true;
78
+ }
79
+ } catch {
80
+ if (!this._usingFallback) {
81
+ console.warn(`[SessionStore] ${this.storeName} health check failed, switching to memory`);
82
+ this._usingFallback = true;
83
+ }
84
+ }
85
+ }
86
+
87
+ private async _execute<T>(operation: string, ...args: any[]): Promise<T> {
88
+ await this._ensureInitialized();
89
+
90
+ if (this._primaryStore === this._fallbackStore) {
91
+ return (this._primaryStore as any)[operation](...args);
92
+ }
93
+
94
+ if (this._usingFallback) {
95
+ return (this._fallbackStore as any)[operation](...args);
96
+ }
97
+
98
+ try {
99
+ return await (this._primaryStore as any)[operation](...args);
100
+ } catch (err) {
101
+ console.warn(`[SessionStore] ${this.storeName}.${operation} failed: ${(err as Error).message}, switching to memory`);
102
+ this._usingFallback = true;
103
+ return (this._fallbackStore as any)[operation](...args);
104
+ }
105
+ }
106
+
107
+ async create(sessionId: string, data: Record<string, unknown>): Promise<void> {
108
+ return this._execute('create', sessionId, data);
109
+ }
110
+
111
+ async get(sessionId: string): Promise<Record<string, unknown> | null> {
112
+ return this._execute('get', sessionId);
113
+ }
114
+
115
+ async delete(sessionId: string): Promise<void> {
116
+ return this._execute('delete', sessionId);
117
+ }
118
+
119
+ async refresh(sessionId: string): Promise<void> {
120
+ return this._execute('refresh', sessionId);
121
+ }
122
+
123
+ async isHealthy(): Promise<boolean> {
124
+ await this._ensureInitialized();
125
+ const store = this._usingFallback ? this._fallbackStore! : this._primaryStore!;
126
+ return store.isHealthy();
127
+ }
128
+
129
+ destroy(): void {
130
+ if (this._healthCheckTimer) {
131
+ clearInterval(this._healthCheckTimer);
132
+ this._healthCheckTimer = null;
133
+ }
134
+ }
135
+
136
+ getStatus(): { configured: string; active: string; usingFallback: boolean } {
137
+ return {
138
+ configured: this.storeName,
139
+ active: this._usingFallback ? 'memory' : this.storeName,
140
+ usingFallback: this._usingFallback,
141
+ };
142
+ }
143
+ }
144
+
145
+ export function createStore(options: { ttl?: number; store?: string } = {}): SessionStoreManager {
146
+ return new SessionStoreManager(options);
147
+ }
148
+
149
+ export { ISessionStore, MemoryStore, RedisStore };
@@ -0,0 +1,9 @@
1
+ import type { AclConfig, RapiddUser } from '../types';
2
+
3
+ const acl: AclConfig = {
4
+ model: {
5
+
6
+ },
7
+ };
8
+
9
+ export default acl;
@@ -0,0 +1,38 @@
1
+ import type { RlsContextFn } from '../types';
2
+
3
+ /**
4
+ * RLS (Row-Level Security) variable mapping.
5
+ *
6
+ * Return the SQL session variables to set before each database query.
7
+ * Keys become variable names (e.g. `app.current_user_id`), values are set per-request.
8
+ * Return null for a key to skip it. Return an empty object to disable RLS.
9
+ *
10
+ * The `request` parameter is the full Fastify request object — you can read
11
+ * from request.user, request.headers, request.body, or any custom property.
12
+ *
13
+ * @example
14
+ * // User-based isolation
15
+ * const rlsContext: RlsContextFn = (request) => ({
16
+ * current_user_id: request.user?.id ?? null,
17
+ * current_user_role: request.user?.role ?? null,
18
+ * });
19
+ *
20
+ * @example
21
+ * // Multi-tenant from header
22
+ * const rlsContext: RlsContextFn = (request) => ({
23
+ * current_tenant_id: request.headers['x-tenant-id'] ?? null,
24
+ * });
25
+ *
26
+ * @example
27
+ * // Composite: tenant + user + department
28
+ * const rlsContext: RlsContextFn = (request) => ({
29
+ * current_user_id: request.user?.id ?? null,
30
+ * current_tenant_id: request.user?.tenantId ?? null,
31
+ * current_department_id: request.user?.departmentId ?? null,
32
+ * });
33
+ */
34
+ const rlsContext: RlsContextFn = (request) => {
35
+ return {};
36
+ };
37
+
38
+ export default rlsContext;
@@ -0,0 +1,226 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import type { DMMF, DMMFModel, DMMFField, RelationConfig } from '../types';
4
+
5
+ let _dmmf: DMMF | null = null;
6
+ let _dmmfPromise: Promise<DMMF> | null = null;
7
+
8
+ /**
9
+ * Load and cache the full DMMF from the Prisma schema.
10
+ * Uses @prisma/internals for complete DMMF including:
11
+ * isId, isList, isRequired, relationFromFields, relationToFields, primaryKey
12
+ */
13
+ export async function loadDMMF(): Promise<DMMF> {
14
+ if (_dmmf) return _dmmf;
15
+ if (_dmmfPromise) return _dmmfPromise;
16
+
17
+ _dmmfPromise = (async () => {
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const { getDMMF } = require('@prisma/internals');
20
+ const schemaPath = path.join(process.cwd(), 'prisma', 'schema.prisma');
21
+ const schema = fs.readFileSync(schemaPath, 'utf8');
22
+ _dmmf = await getDMMF({ datamodel: schema });
23
+ return _dmmf!;
24
+ })();
25
+
26
+ return _dmmfPromise;
27
+ }
28
+
29
+ /**
30
+ * Get cached DMMF synchronously (must call loadDMMF first)
31
+ */
32
+ export function getDMMFSync(): DMMF | null {
33
+ return _dmmf;
34
+ }
35
+
36
+ /**
37
+ * Get a model from the DMMF by name
38
+ */
39
+ export function getModel(modelName: string): DMMFModel | undefined {
40
+ if (!_dmmf) {
41
+ throw new Error('DMMF not loaded. Call loadDMMF() first.');
42
+ }
43
+ return _dmmf.datamodel.models.find((m: DMMFModel) => m.name === modelName);
44
+ }
45
+
46
+ /**
47
+ * Get all fields for a model (including relation fields)
48
+ */
49
+ export function getFields(modelName: string): Record<string, DMMFField> {
50
+ const model = getModel(modelName);
51
+ if (!model) return {};
52
+ return model.fields.reduce((acc: Record<string, DMMFField>, field: DMMFField) => {
53
+ acc[field.name] = field;
54
+ return acc;
55
+ }, {});
56
+ }
57
+
58
+ /**
59
+ * Get only scalar fields for a model (excludes relation fields)
60
+ */
61
+ export function getScalarFields(modelName: string): Record<string, DMMFField> {
62
+ const model = getModel(modelName);
63
+ if (!model) return {};
64
+ return model.fields
65
+ .filter((field: DMMFField) => field.kind !== 'object')
66
+ .reduce((acc: Record<string, DMMFField>, field: DMMFField) => {
67
+ acc[field.name] = field;
68
+ return acc;
69
+ }, {});
70
+ }
71
+
72
+ /**
73
+ * Get the primary key field(s) for a model
74
+ */
75
+ export function getPrimaryKey(modelName: string): string | string[] {
76
+ const model = getModel(modelName);
77
+ if (!model) return 'id';
78
+
79
+ if (model.primaryKey?.fields && model.primaryKey.fields.length > 0) {
80
+ return model.primaryKey.fields.length === 1
81
+ ? model.primaryKey.fields[0]
82
+ : model.primaryKey.fields;
83
+ }
84
+
85
+ const idField = model.fields.find((f: DMMFField) => f.isId);
86
+ return idField ? idField.name : 'id';
87
+ }
88
+
89
+ /**
90
+ * Get all relation fields for a model
91
+ */
92
+ export function getRelations(modelName: string): DMMFField[] {
93
+ const model = getModel(modelName);
94
+ if (!model) return [];
95
+ return model.fields.filter((f: DMMFField) => f.kind === 'object');
96
+ }
97
+
98
+ /**
99
+ * Check if a field is a list relation
100
+ */
101
+ export function isListRelation(modelName: string, fieldName: string): boolean {
102
+ const model = getModel(modelName);
103
+ if (!model) return false;
104
+ const field = model.fields.find((f: DMMFField) => f.name === fieldName);
105
+ return field?.isList === true;
106
+ }
107
+
108
+ /**
109
+ * Get relationship info for a relation field
110
+ */
111
+ export function getRelationInfo(
112
+ modelName: string,
113
+ relationName: string
114
+ ): { name: string; targetModel: string; isList: boolean; fromFields: string[]; toFields: string[]; onDelete?: string } | null {
115
+ const model = getModel(modelName);
116
+ if (!model) return null;
117
+
118
+ const field = model.fields.find((f: DMMFField) => f.name === relationName && f.kind === 'object');
119
+ if (!field) return null;
120
+
121
+ return {
122
+ name: field.name,
123
+ targetModel: field.type,
124
+ isList: field.isList,
125
+ fromFields: field.relationFromFields || [],
126
+ toFields: field.relationToFields || [],
127
+ onDelete: field.relationOnDelete as string | undefined,
128
+ };
129
+ }
130
+
131
+ // =====================================================
132
+ // AUTO-DETECTION HELPERS
133
+ // =====================================================
134
+
135
+ const USER_MODEL_NAMES = ['user', 'users', 'account', 'accounts'];
136
+ const PASSWORD_FIELD_NAMES = ['password', 'hash', 'passwordhash', 'password_hash', 'passwd', 'pwd', 'hashed_password'];
137
+
138
+ /**
139
+ * Find a user-like model by common naming conventions
140
+ */
141
+ export function findUserModel(): DMMFModel | null {
142
+ if (!_dmmf) return null;
143
+ for (const name of USER_MODEL_NAMES) {
144
+ const model = _dmmf.datamodel.models.find(
145
+ (m: DMMFModel) => m.name.toLowerCase() === name
146
+ );
147
+ if (model) return model;
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Find unique scalar string fields suitable as login identifiers
154
+ */
155
+ export function findIdentifierFields(modelName: string): string[] {
156
+ const fields = getScalarFields(modelName);
157
+ const result: string[] = [];
158
+
159
+ for (const [name, field] of Object.entries(fields)) {
160
+ if (field.isUnique && field.type === 'String' && !field.isId) {
161
+ const lower = name.toLowerCase();
162
+ if (!PASSWORD_FIELD_NAMES.includes(lower)) {
163
+ result.push(name);
164
+ }
165
+ }
166
+ }
167
+
168
+ return result.length > 0 ? result : ['email'];
169
+ }
170
+
171
+ /**
172
+ * Find the password field by common naming conventions
173
+ */
174
+ export function findPasswordField(modelName: string): string | null {
175
+ const fields = getScalarFields(modelName);
176
+
177
+ for (const [name] of Object.entries(fields)) {
178
+ if (PASSWORD_FIELD_NAMES.includes(name.toLowerCase())) {
179
+ return name;
180
+ }
181
+ }
182
+
183
+ return null;
184
+ }
185
+
186
+ // =====================================================
187
+ // RELATIONSHIP BUILDING
188
+ // =====================================================
189
+
190
+ /**
191
+ * Build relationships configuration for a model (replaces relationships.json)
192
+ */
193
+ export function buildRelationships(modelName: string): RelationConfig[] {
194
+ const relations = getRelations(modelName);
195
+
196
+ return relations.map((rel: DMMFField) => {
197
+ const config: RelationConfig = {
198
+ name: rel.name,
199
+ object: rel.type,
200
+ isList: rel.isList,
201
+ };
202
+
203
+ if (rel.relationFromFields && rel.relationFromFields.length > 0) {
204
+ config.field = rel.relationFromFields[0];
205
+ config.foreignKey = rel.relationToFields?.[0] || 'id';
206
+
207
+ if (rel.relationFromFields.length > 1) {
208
+ config.fields = rel.relationFromFields;
209
+ config.foreignKeys = rel.relationToFields;
210
+ }
211
+ }
212
+
213
+ const targetRelations = getRelations(rel.type);
214
+ if (targetRelations.length > 0) {
215
+ config.relation = targetRelations.map((nested: DMMFField) => ({
216
+ name: nested.name,
217
+ object: nested.type,
218
+ isList: nested.isList,
219
+ field: nested.relationFromFields?.[0],
220
+ foreignKey: nested.relationToFields?.[0] || 'id',
221
+ }));
222
+ }
223
+
224
+ return config;
225
+ });
226
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Environment variable validation and access
3
+ * Validates required variables at startup and provides typed access
4
+ */
5
+
6
+ export interface EnvConfig {
7
+ // Required
8
+ DATABASE_URL: string;
9
+
10
+ // Optional (auto-generated if auth is enabled)
11
+ JWT_SECRET?: string;
12
+ JWT_REFRESH_SECRET?: string;
13
+
14
+ // Optional with defaults
15
+ NODE_ENV: 'development' | 'production' | 'test';
16
+ PORT: number;
17
+ HOST: string;
18
+ DATABASE_URL_ADMIN?: string;
19
+ DATABASE_PROVIDER?: 'postgresql' | 'mysql';
20
+ COOKIE_SECRET?: string;
21
+ ALLOWED_ORIGINS?: string;
22
+
23
+ // Auth
24
+ AUTH_SESSION_STORAGE: 'redis' | 'memory';
25
+ AUTH_SESSION_TTL: number;
26
+ AUTH_SALT_ROUNDS: number;
27
+ AUTH_ACCESS_TOKEN_EXPIRY: string;
28
+ AUTH_REFRESH_TOKEN_EXPIRY: string;
29
+ DB_USER_TABLE: string;
30
+
31
+ // API
32
+ API_RESULT_LIMIT: number;
33
+ REQUEST_TIMEOUT: number;
34
+ API_MAX_RETRIES: number;
35
+
36
+ // Rate limiting
37
+ RATE_LIMIT_ENABLED: boolean;
38
+ RATE_LIMIT_WINDOW_MS: number;
39
+ RATE_LIMIT_MAX_REQUESTS: number;
40
+
41
+ // Redis
42
+ REDIS_HOST?: string;
43
+ REDIS_PORT: number;
44
+ REDIS_PASSWORD?: string;
45
+ REDIS_DB_RATE_LIMIT: number;
46
+ REDIS_DB_AUTH: number;
47
+
48
+ // RLS
49
+ RLS_ENABLED?: boolean;
50
+ RLS_NAMESPACE: string;
51
+
52
+ // Proxy
53
+ TRUST_PROXY?: boolean;
54
+ }
55
+
56
+ const REQUIRED_VARS = [
57
+ 'DATABASE_URL'
58
+ ] as const;
59
+
60
+ const DEFAULTS: Partial<Record<keyof EnvConfig, string | number | boolean>> = {
61
+ NODE_ENV: 'development',
62
+ PORT: 3000,
63
+ HOST: '0.0.0.0',
64
+ AUTH_SESSION_STORAGE: 'redis',
65
+ AUTH_SESSION_TTL: 86400,
66
+ AUTH_SALT_ROUNDS: 10,
67
+ AUTH_ACCESS_TOKEN_EXPIRY: '1d',
68
+ AUTH_REFRESH_TOKEN_EXPIRY: '7d',
69
+ DB_USER_TABLE: 'users',
70
+ API_RESULT_LIMIT: 500,
71
+ REQUEST_TIMEOUT: 10000,
72
+ API_MAX_RETRIES: 2,
73
+ RATE_LIMIT_ENABLED: true,
74
+ RATE_LIMIT_WINDOW_MS: 900000,
75
+ RATE_LIMIT_MAX_REQUESTS: 100,
76
+ REDIS_PORT: 6379,
77
+ REDIS_DB_RATE_LIMIT: 0,
78
+ REDIS_DB_AUTH: 1,
79
+ RLS_NAMESPACE: 'app'
80
+ };
81
+
82
+ /**
83
+ * Validate that all required environment variables are set
84
+ * @throws Error if required variables are missing
85
+ */
86
+ export function validateEnv(): void {
87
+ const missing: string[] = [];
88
+
89
+ for (const key of REQUIRED_VARS) {
90
+ if (!process.env[key]) {
91
+ missing.push(key);
92
+ }
93
+ }
94
+
95
+ if (missing.length > 0) {
96
+ throw new Error(
97
+ `Missing required environment variables: ${missing.join(', ')}\n` +
98
+ `Please check your .env file or environment configuration.`
99
+ );
100
+ }
101
+
102
+ // Validate DATABASE_URL format
103
+ const dbUrl = process.env.DATABASE_URL!;
104
+ if (!dbUrl.startsWith('postgresql://') && !dbUrl.startsWith('postgres://') &&
105
+ !dbUrl.startsWith('mysql://') && !dbUrl.startsWith('mariadb://')) {
106
+ throw new Error(
107
+ `Invalid DATABASE_URL format. Must start with postgresql://, postgres://, mysql://, or mariadb://`
108
+ );
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get an environment variable with type coercion
114
+ */
115
+ export function getEnv<K extends keyof EnvConfig>(key: K): EnvConfig[K] {
116
+ const value = process.env[key];
117
+ const defaultValue = DEFAULTS[key];
118
+
119
+ if (value === undefined || value === '') {
120
+ if (defaultValue !== undefined) {
121
+ return defaultValue as EnvConfig[K];
122
+ }
123
+ return undefined as EnvConfig[K];
124
+ }
125
+
126
+ // Type coercion based on default value type
127
+ if (typeof defaultValue === 'number') {
128
+ return parseInt(value, 10) as EnvConfig[K];
129
+ }
130
+ if (typeof defaultValue === 'boolean') {
131
+ return (value.toLowerCase() === 'true') as EnvConfig[K];
132
+ }
133
+
134
+ return value as EnvConfig[K];
135
+ }
136
+
137
+ /**
138
+ * Get all environment configuration
139
+ */
140
+ export function getAllEnv(): Partial<EnvConfig> {
141
+ const config: Partial<EnvConfig> = {};
142
+
143
+ for (const key of Object.keys(DEFAULTS) as (keyof EnvConfig)[]) {
144
+ (config as any)[key] = getEnv(key);
145
+ }
146
+
147
+ // Add required vars
148
+ for (const key of REQUIRED_VARS) {
149
+ (config as any)[key] = process.env[key];
150
+ }
151
+
152
+ return config;
153
+ }
154
+
155
+ /**
156
+ * Check if running in production
157
+ */
158
+ export function isProduction(): boolean {
159
+ return getEnv('NODE_ENV') === 'production';
160
+ }
161
+
162
+ /**
163
+ * Check if running in development
164
+ */
165
+ export function isDevelopment(): boolean {
166
+ return getEnv('NODE_ENV') === 'development';
167
+ }
168
+
169
+ /**
170
+ * Check if running in test
171
+ */
172
+ export function isTest(): boolean {
173
+ return getEnv('NODE_ENV') === 'test';
174
+ }
175
+
176
+ export default {
177
+ validate: validateEnv,
178
+ get: getEnv,
179
+ getAll: getAllEnv,
180
+ isProduction,
181
+ isDevelopment,
182
+ isTest
183
+ };
@@ -0,0 +1,87 @@
1
+ import { LanguageDict } from './i18n';
2
+
3
+ /**
4
+ * Basic error response with HTTP status code
5
+ */
6
+ export class ErrorBasicResponse extends Error {
7
+ status_code: number;
8
+
9
+ constructor(message: string, status_code: number = 500) {
10
+ super(message);
11
+ this.status_code = status_code;
12
+ }
13
+
14
+ toJSON(): { status_code: number; message: string } {
15
+ return {
16
+ status_code: this.status_code,
17
+ message: this.message,
18
+ };
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Localized error response with i18n support via LanguageDict.
24
+ * Constructor takes (status_code, message_key, data?) — NOT the standard Error(message) order.
25
+ */
26
+ export class ErrorResponse extends ErrorBasicResponse {
27
+ data: Record<string, unknown> | null;
28
+
29
+ constructor(status_code: number, message: string, data: Record<string, unknown> | null = null) {
30
+ super(message, status_code);
31
+ this.data = data;
32
+ }
33
+
34
+ toJSON(language: string = 'en_US'): { status_code: number; message: string } {
35
+ return {
36
+ status_code: this.status_code,
37
+ message: LanguageDict.get(this.message, this.data, language),
38
+ };
39
+ }
40
+
41
+ static badRequest(key: string, data: Record<string, unknown> | null = null): ErrorResponse {
42
+ return new ErrorResponse(400, key, data);
43
+ }
44
+
45
+ static unauthorized(key: string, data: Record<string, unknown> | null = null): ErrorResponse {
46
+ return new ErrorResponse(401, key, data);
47
+ }
48
+
49
+ static forbidden(key: string, data: Record<string, unknown> | null = null): ErrorResponse {
50
+ return new ErrorResponse(403, key, data);
51
+ }
52
+
53
+ static notFound(key: string, data: Record<string, unknown> | null = null): ErrorResponse {
54
+ return new ErrorResponse(404, key, data);
55
+ }
56
+
57
+ static conflict(key: string, data: Record<string, unknown> | null = null): ErrorResponse {
58
+ return new ErrorResponse(409, key, data);
59
+ }
60
+
61
+ static tooManyRequests(key: string, data: Record<string, unknown> | null = null): ErrorResponse {
62
+ return new ErrorResponse(429, key, data);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Success response with i18n support
68
+ */
69
+ export class Response {
70
+ status_code: number;
71
+ message: string;
72
+ data: Record<string, unknown> | null;
73
+
74
+ constructor(status_code: number, message: string, data: Record<string, unknown> | null = null) {
75
+ this.status_code = status_code;
76
+ this.message = message;
77
+ this.data = data;
78
+ }
79
+
80
+ toJSON(language: string = 'en_US'): { status_code: number; message: string; data: Record<string, unknown> | null } {
81
+ return {
82
+ status_code: this.status_code,
83
+ message: LanguageDict.get(this.message, this.data, language),
84
+ data: this.data,
85
+ };
86
+ }
87
+ }