@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,348 @@
1
+ import nodemailer from 'nodemailer';
2
+ import ejs from 'ejs';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { LanguageDict } from '../core/i18n';
6
+
7
+ // ── Types ────────────────────────────────────────────────────────────────────
8
+
9
+ export interface EmailConfig {
10
+ host: string;
11
+ port: number;
12
+ secure?: boolean;
13
+ user: string;
14
+ password: string;
15
+ from?: string;
16
+ name?: string;
17
+ }
18
+
19
+ export interface EmailAttachment {
20
+ filename?: string;
21
+ content?: string | Buffer;
22
+ path?: string;
23
+ href?: string;
24
+ contentType?: string;
25
+ encoding?: string;
26
+ cid?: string;
27
+ }
28
+
29
+ export interface EmailOptions {
30
+ to: string | string[];
31
+ subject: string;
32
+ html?: string;
33
+ text?: string;
34
+ cc?: string | string[];
35
+ bcc?: string | string[];
36
+ replyTo?: string;
37
+ attachments?: EmailAttachment[];
38
+ headers?: Record<string, string>;
39
+ }
40
+
41
+ export interface EmailResult {
42
+ success: boolean;
43
+ messageId?: string;
44
+ error?: Error;
45
+ }
46
+
47
+ // ── Configuration ────────────────────────────────────────────────────────────
48
+
49
+ let _config: { emails?: Record<string, EmailConfig> } = { emails: {} };
50
+ let _configLoaded = false;
51
+
52
+ function getConfig(): { emails?: Record<string, EmailConfig> } {
53
+ if (!_configLoaded) {
54
+ try {
55
+ _config = require(path.join(process.cwd(), 'config', 'app.json'));
56
+ } catch {
57
+ _config = { emails: {} };
58
+ }
59
+ _configLoaded = true;
60
+ }
61
+ return _config;
62
+ }
63
+
64
+ function getEmailConfig(configKey: string): EmailConfig {
65
+ const config = getConfig();
66
+
67
+ if (!config.emails || typeof config.emails !== 'object') {
68
+ throw new Error('Email configuration not found in config/app.json');
69
+ }
70
+
71
+ const emailConfig = config.emails[configKey];
72
+ if (!emailConfig) {
73
+ const available = Object.keys(config.emails).join(', ') || 'none';
74
+ throw new Error(`Email config '${configKey}' not found. Available: ${available}`);
75
+ }
76
+
77
+ const required: (keyof EmailConfig)[] = ['host', 'port', 'user', 'password'];
78
+ const missing = required.filter(field => !emailConfig[field]);
79
+
80
+ if (missing.length > 0) {
81
+ throw new Error(`Email config '${configKey}' missing required fields: ${missing.join(', ')}`);
82
+ }
83
+
84
+ return emailConfig;
85
+ }
86
+
87
+ function createTransporter(config: EmailConfig) {
88
+ return nodemailer.createTransport({
89
+ host: config.host,
90
+ port: config.port,
91
+ secure: config.secure ?? config.port === 465,
92
+ auth: {
93
+ user: config.user,
94
+ pass: config.password
95
+ },
96
+ tls: {
97
+ rejectUnauthorized: process.env.NODE_ENV === 'production'
98
+ },
99
+ pool: true,
100
+ maxConnections: 5,
101
+ maxMessages: 100
102
+ });
103
+ }
104
+
105
+ // ── Template Rendering ──────────────────────────────────────────────────────
106
+
107
+ const TEMPLATES_PATH = path.join(process.cwd(), 'templates', 'email');
108
+ const LAYOUTS_PATH = path.join(process.cwd(), 'templates', 'layouts');
109
+ const templateCache = new Map<string, string>();
110
+
111
+ function loadTemplate(name: string): string {
112
+ if (templateCache.has(name)) {
113
+ return templateCache.get(name)!;
114
+ }
115
+
116
+ const filePath = path.join(TEMPLATES_PATH, `${name}.ejs`);
117
+ if (!fs.existsSync(filePath)) {
118
+ throw new Error(`Email template '${name}' not found at ${filePath}`);
119
+ }
120
+
121
+ const content = fs.readFileSync(filePath, 'utf-8');
122
+ templateCache.set(name, content);
123
+ return content;
124
+ }
125
+
126
+ function loadLayout(name: string): string | null {
127
+ const cacheKey = `layout:${name}`;
128
+ if (templateCache.has(cacheKey)) {
129
+ return templateCache.get(cacheKey)!;
130
+ }
131
+
132
+ const filePath = path.join(LAYOUTS_PATH, `${name}.ejs`);
133
+ if (!fs.existsSync(filePath)) {
134
+ return null;
135
+ }
136
+
137
+ const content = fs.readFileSync(filePath, 'utf-8');
138
+ templateCache.set(cacheKey, content);
139
+ return content;
140
+ }
141
+
142
+ function renderTemplate(name: string, data: Record<string, unknown> = {}, layout: string | false = 'email', language?: string): string {
143
+ const template = loadTemplate(name);
144
+
145
+ // Inject __() translation helper into template data
146
+ const lang = language || (data.language as string) || undefined;
147
+ const templateData = {
148
+ ...data,
149
+ __: (key: string, params?: Record<string, unknown>) => LanguageDict.get(key, params || null, lang || null),
150
+ };
151
+
152
+ const body = ejs.render(template, templateData, { filename: path.join(TEMPLATES_PATH, `${name}.ejs`) });
153
+
154
+ // Wrap in layout (default: 'email', pass false to skip)
155
+ if (layout !== false) {
156
+ const layoutContent = loadLayout(layout);
157
+ if (layoutContent) {
158
+ return ejs.render(layoutContent, { ...templateData, body }, { filename: path.join(LAYOUTS_PATH, `${layout}.ejs`) });
159
+ }
160
+ }
161
+
162
+ return body;
163
+ }
164
+
165
+ // ── Validation ──────────────────────────────────────────────────────────────
166
+
167
+ function validateEmail(email: string): boolean {
168
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
169
+ }
170
+
171
+ function validateOptions(options: EmailOptions): void {
172
+ if (!options.to) {
173
+ throw new Error('Recipient (to) is required');
174
+ }
175
+
176
+ if (!options.subject) {
177
+ throw new Error('Subject is required');
178
+ }
179
+
180
+ if (!options.html && !options.text) {
181
+ throw new Error('Email body (html or text) is required');
182
+ }
183
+
184
+ const recipients = Array.isArray(options.to) ? options.to : [options.to];
185
+ for (const email of recipients) {
186
+ if (!validateEmail(email)) {
187
+ throw new Error(`Invalid email address: ${email}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ // ── Main API ─────────────────────────────────────────────────────────────────
193
+
194
+ /**
195
+ * Config-driven email client with multiple SMTP profiles.
196
+ *
197
+ * @example
198
+ * // Simple email
199
+ * await Mailer.send('default', {
200
+ * to: 'user@example.com',
201
+ * subject: 'Hello',
202
+ * html: '<h1>Welcome!</h1>'
203
+ * });
204
+ *
205
+ * @example
206
+ * // With attachments
207
+ * await Mailer.send('support', {
208
+ * to: ['user1@example.com', 'user2@example.com'],
209
+ * subject: 'Invoice',
210
+ * html: '<p>Please find your invoice attached.</p>',
211
+ * attachments: [
212
+ * { filename: 'invoice.pdf', path: './invoice.pdf' }
213
+ * ]
214
+ * });
215
+ *
216
+ * @example
217
+ * // Render EJS template and send
218
+ * await Mailer.sendTemplate('default', 'confirmation', {
219
+ * to: 'user@example.com',
220
+ * subject: 'Confirm your email',
221
+ * data: { name: 'John', confirmationUrl: 'https://...' }
222
+ * });
223
+ *
224
+ * @example
225
+ * // Render template without sending
226
+ * const html = Mailer.render('resetPassword', { name: 'John', resetUrl: '...' });
227
+ */
228
+ export const Mailer = {
229
+ /**
230
+ * Send a single email using a configured SMTP profile
231
+ */
232
+ async send(configKey: string, options: EmailOptions): Promise<EmailResult> {
233
+ try {
234
+ const config = getEmailConfig(configKey);
235
+ validateOptions(options);
236
+
237
+ const transporter = createTransporter(config);
238
+
239
+ const mailOptions = {
240
+ from: config.from || `"${config.name || 'No Reply'}" <${config.user}>`,
241
+ to: options.to,
242
+ subject: options.subject,
243
+ html: options.html,
244
+ text: options.text,
245
+ cc: options.cc,
246
+ bcc: options.bcc,
247
+ replyTo: options.replyTo,
248
+ attachments: options.attachments,
249
+ headers: options.headers
250
+ };
251
+
252
+ const info = await transporter.sendMail(mailOptions);
253
+ transporter.close();
254
+
255
+ return {
256
+ success: true,
257
+ messageId: info.messageId
258
+ };
259
+ } catch (error) {
260
+ return {
261
+ success: false,
262
+ error: error as Error
263
+ };
264
+ }
265
+ },
266
+
267
+ /**
268
+ * Send multiple emails in parallel
269
+ */
270
+ async sendBatch(
271
+ configKey: string,
272
+ emails: EmailOptions[]
273
+ ): Promise<EmailResult[]> {
274
+ const results = await Promise.allSettled(
275
+ emails.map(options => this.send(configKey, options))
276
+ );
277
+
278
+ return results.map((result) => {
279
+ if (result.status === 'fulfilled') {
280
+ return result.value;
281
+ }
282
+ return {
283
+ success: false,
284
+ error: result.reason
285
+ };
286
+ });
287
+ },
288
+
289
+ /**
290
+ * Verify SMTP connection for a configuration
291
+ */
292
+ async verify(configKey: string): Promise<boolean> {
293
+ try {
294
+ const config = getEmailConfig(configKey);
295
+ const transporter = createTransporter(config);
296
+
297
+ await transporter.verify();
298
+ transporter.close();
299
+
300
+ return true;
301
+ } catch {
302
+ return false;
303
+ }
304
+ },
305
+
306
+ /**
307
+ * Render an EJS email template from templates/email/
308
+ * Templates have access to __() for translations: <%= __('signIn') %>
309
+ * @param layout - Layout name from templates/layouts/ (default: 'email'), or false to skip layout
310
+ * @param language - Language code for translations (e.g. 'de_DE'), defaults to 'en_US'
311
+ */
312
+ render(template: string, data: Record<string, unknown> = {}, layout: string | false = 'email', language?: string): string {
313
+ return renderTemplate(template, data, layout, language);
314
+ },
315
+
316
+ /**
317
+ * Render an EJS template and send it as an email
318
+ * Templates have access to __() for translations: <%= __('signIn') %>
319
+ * @param options.layout - Layout name from templates/layouts/ (default: 'email'), or false to skip layout
320
+ * @param options.language - Language code for translations (e.g. 'de_DE'), defaults to 'en_US'
321
+ */
322
+ async sendTemplate(
323
+ configKey: string,
324
+ template: string,
325
+ options: { to: string | string[]; subject: string; data?: Record<string, unknown>; layout?: string | false; language?: string } & Omit<EmailOptions, 'html'>
326
+ ): Promise<EmailResult> {
327
+ const { data = {}, layout = 'email', language, ...emailOptions } = options;
328
+ const html = renderTemplate(template, { ...data, subject: options.subject }, layout, language);
329
+ return this.send(configKey, { ...emailOptions, html });
330
+ },
331
+
332
+ /**
333
+ * Clear the template cache (e.g. after editing templates in dev)
334
+ */
335
+ clearTemplateCache(): void {
336
+ templateCache.clear();
337
+ },
338
+
339
+ /**
340
+ * List available email configurations
341
+ */
342
+ listConfigs(): string[] {
343
+ const config = getConfig();
344
+ return Object.keys(config.emails || {});
345
+ }
346
+ };
347
+
348
+ export default Mailer;
@@ -0,0 +1,25 @@
1
+ export { ApiClient, ApiClientError } from './ApiClient';
2
+ export type { ServiceConfig, EndpointConfig, AuthConfig, RequestOptions, ApiResponse } from './ApiClient';
3
+
4
+ export { Mailer } from './Mailer';
5
+ export type { EmailConfig, EmailOptions, EmailAttachment, EmailResult } from './Mailer';
6
+
7
+ export const env = {
8
+ isProduction: () => process.env.NODE_ENV === 'production',
9
+ isDevelopment: () => __filename.endsWith('.ts') || process.env.NODE_ENV === 'development',
10
+ isTest: () => process.env.NODE_ENV === 'test',
11
+ current: () => process.env.NODE_ENV || 'development',
12
+
13
+ get: <T extends string | number | boolean>(key: string, defaultValue: T): T => {
14
+ const value = process.env[key];
15
+ if (value === undefined) return defaultValue;
16
+
17
+ if (typeof defaultValue === 'number') {
18
+ return parseInt(value, 10) as T;
19
+ }
20
+ if (typeof defaultValue === 'boolean') {
21
+ return (value.toLowerCase() === 'true') as T;
22
+ }
23
+ return value as T;
24
+ }
25
+ };
@@ -0,0 +1,17 @@
1
+ <p>Hi <%= name %>,</p>
2
+
3
+ <p>Welcome to <strong><%= appName %></strong>. Your account has been created successfully.</p>
4
+
5
+ <% if (locals.activationLink) { %>
6
+ <p>Please verify your email address to get started:</p>
7
+ <p style="text-align: center; margin: 32px 0;">
8
+ <a href="<%= activationLink %>" class="btn">Verify Email</a>
9
+ </p>
10
+ <% } %>
11
+
12
+ <p>If you have any questions, feel free to reach out to our support team.</p>
13
+
14
+ <p>
15
+ Best regards,<br>
16
+ The <%= appName %> Team
17
+ </p>
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= subject %></title>
7
+ <style>
8
+ body { margin: 0; padding: 0; background-color: #f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }
9
+ .container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
10
+ .card { background: #ffffff; border-radius: 8px; padding: 40px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
11
+ .header { text-align: center; margin-bottom: 32px; }
12
+ .header h1 { margin: 0; font-size: 24px; color: #1a1a2e; }
13
+ .content { color: #333; font-size: 16px; line-height: 1.6; }
14
+ .content p { margin: 0 0 16px; }
15
+ .btn { display: inline-block; padding: 12px 32px; background-color: #1a1a2e; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 14px; }
16
+ .footer { text-align: center; margin-top: 32px; font-size: 13px; color: #999; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div class="container">
21
+ <div class="card">
22
+ <div class="header">
23
+ <% if (locals.logoUrl) { %><img src="<%= logoUrl %>" alt="" height="40" style="margin-bottom: 16px;"><% } %>
24
+ <h1><%= subject %></h1>
25
+ </div>
26
+ <div class="content">
27
+ <%- body %>
28
+ </div>
29
+ </div>
30
+ <div class="footer">
31
+ <% if (locals.footerText) { %><%= footerText %><% } %>
32
+ </div>
33
+ </div>
34
+ </body>
35
+ </html>
package/tsconfig.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "lib": ["ES2024"],
7
+ "outDir": "dist",
8
+ "rootDir": ".",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "resolveJsonModule": true,
17
+ "noUnusedLocals": false,
18
+ "noUnusedParameters": false,
19
+ "noImplicitReturns": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ },
22
+ "include": [
23
+ "src/**/*.ts",
24
+ "main.ts",
25
+ "routes/**/*.ts"
26
+ ],
27
+ "exclude": [
28
+ "node_modules",
29
+ "dist",
30
+ "__test__",
31
+ "**/*.js"
32
+ ]
33
+ }