voltjs-framework 1.0.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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,347 @@
1
+ /**
2
+ * VoltJS Mail Module
3
+ *
4
+ * Send emails via SMTP with zero external dependencies.
5
+ * Uses raw TCP sockets for SMTP communication.
6
+ *
7
+ * @example
8
+ * const { Mail } = require('voltjs');
9
+ *
10
+ * const mailer = new Mail({
11
+ * host: 'smtp.gmail.com',
12
+ * port: 587,
13
+ * secure: false,
14
+ * user: 'you@gmail.com',
15
+ * password: 'your-app-password',
16
+ * from: 'My App <you@gmail.com>',
17
+ * });
18
+ *
19
+ * await mailer.send({
20
+ * to: 'user@example.com',
21
+ * subject: 'Welcome!',
22
+ * html: '<h1>Welcome to our app!</h1>',
23
+ * });
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ const net = require('net');
29
+ const tls = require('tls');
30
+ const crypto = require('crypto');
31
+
32
+ class Mail {
33
+ constructor(config = {}) {
34
+ this._config = {
35
+ host: config.host || process.env.MAIL_HOST || 'localhost',
36
+ port: config.port || parseInt(process.env.MAIL_PORT) || 587,
37
+ secure: config.secure !== undefined ? config.secure : false,
38
+ user: config.user || process.env.MAIL_USER || null,
39
+ password: config.password || process.env.MAIL_PASSWORD || null,
40
+ from: config.from || process.env.MAIL_FROM || 'VoltJS <noreply@voltjs.dev>',
41
+ timeout: config.timeout || 10000,
42
+ };
43
+ this._templates = new Map();
44
+ }
45
+
46
+ /** Send an email */
47
+ async send(options) {
48
+ const message = {
49
+ from: options.from || this._config.from,
50
+ to: Array.isArray(options.to) ? options.to : [options.to],
51
+ cc: options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]) : [],
52
+ bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]) : [],
53
+ subject: options.subject || '(no subject)',
54
+ text: options.text || '',
55
+ html: options.html || '',
56
+ attachments: options.attachments || [],
57
+ replyTo: options.replyTo || null,
58
+ headers: options.headers || {},
59
+ };
60
+
61
+ // If template is specified, render it
62
+ if (options.template) {
63
+ const rendered = this._renderTemplate(options.template, options.data || {});
64
+ if (!message.html) message.html = rendered;
65
+ }
66
+
67
+ return this._sendSMTP(message);
68
+ }
69
+
70
+ /** Send email using a template */
71
+ async sendTemplate(templateName, to, data = {}, options = {}) {
72
+ const html = this._renderTemplate(templateName, data);
73
+ return this.send({
74
+ to,
75
+ subject: options.subject || data.subject || templateName,
76
+ html,
77
+ ...options,
78
+ });
79
+ }
80
+
81
+ /** Register an email template */
82
+ template(name, htmlTemplate) {
83
+ this._templates.set(name, htmlTemplate);
84
+ return this;
85
+ }
86
+
87
+ /** Send to multiple recipients */
88
+ async sendBulk(recipients, options) {
89
+ const results = [];
90
+ for (const recipient of recipients) {
91
+ try {
92
+ const to = typeof recipient === 'string' ? recipient : recipient.email;
93
+ const data = typeof recipient === 'object' ? recipient : {};
94
+ await this.send({ ...options, to, data: { ...options.data, ...data } });
95
+ results.push({ to, status: 'sent' });
96
+ } catch (err) {
97
+ results.push({ to: recipient, status: 'failed', error: err.message });
98
+ }
99
+ }
100
+ return results;
101
+ }
102
+
103
+ /** Verify SMTP connection */
104
+ async verify() {
105
+ try {
106
+ const socket = await this._connect();
107
+ await this._readResponse(socket);
108
+ await this._command(socket, `EHLO ${this._config.host}`);
109
+ socket.destroy();
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ // ===== SMTP IMPLEMENTATION =====
117
+
118
+ async _sendSMTP(message) {
119
+ const socket = await this._connect();
120
+
121
+ try {
122
+ // Read greeting
123
+ await this._readResponse(socket);
124
+
125
+ // EHLO
126
+ await this._command(socket, `EHLO ${this._config.host}`);
127
+
128
+ // STARTTLS if not already secure
129
+ if (!this._config.secure && this._config.port !== 465) {
130
+ try {
131
+ await this._command(socket, 'STARTTLS');
132
+ // Upgrade to TLS — in production, you'd upgrade the socket here
133
+ } catch {
134
+ // STARTTLS not supported, continue unencrypted
135
+ }
136
+ }
137
+
138
+ // AUTH
139
+ if (this._config.user && this._config.password) {
140
+ await this._command(socket, 'AUTH LOGIN');
141
+ await this._command(socket, Buffer.from(this._config.user).toString('base64'));
142
+ await this._command(socket, Buffer.from(this._config.password).toString('base64'));
143
+ }
144
+
145
+ // MAIL FROM
146
+ const fromAddr = this._extractEmail(message.from);
147
+ await this._command(socket, `MAIL FROM:<${fromAddr}>`);
148
+
149
+ // RCPT TO
150
+ const allRecipients = [...message.to, ...message.cc, ...message.bcc];
151
+ for (const to of allRecipients) {
152
+ const toAddr = this._extractEmail(to);
153
+ await this._command(socket, `RCPT TO:<${toAddr}>`);
154
+ }
155
+
156
+ // DATA
157
+ await this._command(socket, 'DATA');
158
+
159
+ // Build message
160
+ const emailData = this._buildMessage(message);
161
+ await this._command(socket, emailData + '\r\n.');
162
+
163
+ // QUIT
164
+ await this._command(socket, 'QUIT');
165
+
166
+ socket.destroy();
167
+ return { success: true, messageId: this._generateMessageId() };
168
+ } catch (err) {
169
+ socket.destroy();
170
+ throw new Error(`Failed to send email: ${err.message}`);
171
+ }
172
+ }
173
+
174
+ _connect() {
175
+ return new Promise((resolve, reject) => {
176
+ const options = {
177
+ host: this._config.host,
178
+ port: this._config.port,
179
+ timeout: this._config.timeout,
180
+ };
181
+
182
+ const socket = this._config.secure
183
+ ? tls.connect(options, () => resolve(socket))
184
+ : net.connect(options, () => resolve(socket));
185
+
186
+ socket.on('error', reject);
187
+ socket.on('timeout', () => {
188
+ socket.destroy();
189
+ reject(new Error('SMTP connection timed out'));
190
+ });
191
+ });
192
+ }
193
+
194
+ _command(socket, data) {
195
+ return new Promise((resolve, reject) => {
196
+ socket.write(data + '\r\n', (err) => {
197
+ if (err) return reject(err);
198
+ this._readResponse(socket).then(resolve).catch(reject);
199
+ });
200
+ });
201
+ }
202
+
203
+ _readResponse(socket) {
204
+ return new Promise((resolve, reject) => {
205
+ const timeout = setTimeout(() => reject(new Error('SMTP response timeout')), this._config.timeout);
206
+
207
+ socket.once('data', (data) => {
208
+ clearTimeout(timeout);
209
+ const response = data.toString();
210
+ const code = parseInt(response.substring(0, 3));
211
+ if (code >= 400) {
212
+ reject(new Error(`SMTP error ${code}: ${response}`));
213
+ } else {
214
+ resolve(response);
215
+ }
216
+ });
217
+ });
218
+ }
219
+
220
+ _buildMessage(message) {
221
+ const boundary = `----VoltJS_${crypto.randomBytes(16).toString('hex')}`;
222
+ const messageId = this._generateMessageId();
223
+
224
+ let msg = '';
225
+ msg += `Message-ID: <${messageId}>\r\n`;
226
+ msg += `Date: ${new Date().toUTCString()}\r\n`;
227
+ msg += `From: ${message.from}\r\n`;
228
+ msg += `To: ${message.to.join(', ')}\r\n`;
229
+ if (message.cc.length > 0) msg += `Cc: ${message.cc.join(', ')}\r\n`;
230
+ msg += `Subject: ${this._encodeSubject(message.subject)}\r\n`;
231
+ if (message.replyTo) msg += `Reply-To: ${message.replyTo}\r\n`;
232
+ msg += 'MIME-Version: 1.0\r\n';
233
+ msg += `X-Mailer: VoltJS/1.0\r\n`;
234
+
235
+ // Custom headers
236
+ for (const [key, value] of Object.entries(message.headers)) {
237
+ msg += `${key}: ${value}\r\n`;
238
+ }
239
+
240
+ if (message.attachments.length > 0) {
241
+ msg += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n`;
242
+
243
+ // Body part
244
+ msg += `--${boundary}\r\n`;
245
+ if (message.html) {
246
+ msg += 'Content-Type: text/html; charset=UTF-8\r\n\r\n';
247
+ msg += message.html;
248
+ } else {
249
+ msg += 'Content-Type: text/plain; charset=UTF-8\r\n\r\n';
250
+ msg += message.text;
251
+ }
252
+ msg += '\r\n';
253
+
254
+ // Attachments
255
+ for (const att of message.attachments) {
256
+ msg += `--${boundary}\r\n`;
257
+ msg += `Content-Type: ${att.type || 'application/octet-stream'}; name="${att.name}"\r\n`;
258
+ msg += `Content-Disposition: attachment; filename="${att.name}"\r\n`;
259
+ msg += 'Content-Transfer-Encoding: base64\r\n\r\n';
260
+ msg += (att.data instanceof Buffer ? att.data : Buffer.from(att.data)).toString('base64');
261
+ msg += '\r\n';
262
+ }
263
+ msg += `--${boundary}--`;
264
+ } else if (message.html) {
265
+ msg += 'Content-Type: text/html; charset=UTF-8\r\n\r\n';
266
+ msg += message.html;
267
+ } else {
268
+ msg += 'Content-Type: text/plain; charset=UTF-8\r\n\r\n';
269
+ msg += message.text;
270
+ }
271
+
272
+ return msg;
273
+ }
274
+
275
+ _extractEmail(str) {
276
+ const match = String(str).match(/<(.+?)>/);
277
+ return match ? match[1] : str.trim();
278
+ }
279
+
280
+ _encodeSubject(subject) {
281
+ // Encode non-ASCII subjects
282
+ if (/[^\x00-\x7F]/.test(subject)) {
283
+ return `=?UTF-8?B?${Buffer.from(subject).toString('base64')}?=`;
284
+ }
285
+ return subject;
286
+ }
287
+
288
+ _generateMessageId() {
289
+ return `${Date.now()}.${crypto.randomBytes(8).toString('hex')}@voltjs.dev`;
290
+ }
291
+
292
+ _renderTemplate(name, data) {
293
+ let template = this._templates.get(name);
294
+ if (!template) {
295
+ throw new Error(`Email template "${name}" not found`);
296
+ }
297
+
298
+ // Simple template rendering: {{variable}}
299
+ return template.replace(/\{\{\s*(.+?)\s*\}\}/g, (_, key) => {
300
+ const keys = key.split('.');
301
+ let value = data;
302
+ for (const k of keys) {
303
+ value = value?.[k];
304
+ }
305
+ return value !== undefined ? String(value) : '';
306
+ });
307
+ }
308
+
309
+ // ===== STATIC HELPERS =====
310
+
311
+ /** Quick send without instantiation */
312
+ static async quickSend(config, options) {
313
+ const mailer = new Mail(config);
314
+ return mailer.send(options);
315
+ }
316
+
317
+ /** Create a beautiful HTML email from a simple template */
318
+ static htmlTemplate(title, body, options = {}) {
319
+ const primaryColor = options.primaryColor || '#667eea';
320
+ const footerText = options.footer || 'Sent with VoltJS';
321
+
322
+ return `
323
+ <!DOCTYPE html>
324
+ <html>
325
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
326
+ <body style="margin:0;padding:0;background:#f4f4f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
327
+ <table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f7;padding:40px 0;">
328
+ <tr><td align="center">
329
+ <table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
330
+ <tr><td style="background:${primaryColor};padding:30px;text-align:center;">
331
+ <h1 style="margin:0;color:#ffffff;font-size:24px;">${title}</h1>
332
+ </td></tr>
333
+ <tr><td style="padding:30px;color:#333;font-size:16px;line-height:1.6;">
334
+ ${body}
335
+ </td></tr>
336
+ <tr><td style="padding:20px 30px;text-align:center;color:#888;font-size:12px;border-top:1px solid #eee;">
337
+ ${footerText}
338
+ </td></tr>
339
+ </table>
340
+ </td></tr>
341
+ </table>
342
+ </body>
343
+ </html>`;
344
+ }
345
+ }
346
+
347
+ module.exports = { Mail };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * VoltJS Paginator
3
+ *
4
+ * Pagination helper for database results and arrays.
5
+ *
6
+ * @example
7
+ * const { Paginator } = require('voltjs');
8
+ *
9
+ * const result = Paginator.paginate(allItems, { page: 2, perPage: 20 });
10
+ * // {
11
+ * // data: [...],
12
+ * // meta: { page: 2, perPage: 20, total: 150, totalPages: 8, hasNext: true, hasPrev: true }
13
+ * // }
14
+ *
15
+ * // As middleware:
16
+ * app.get('/users', async (req, res) => {
17
+ * const page = Paginator.fromRequest(req);
18
+ * const users = await User.paginate(page.page, page.perPage);
19
+ * res.json(users);
20
+ * });
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ class Paginator {
26
+ /** Paginate an array of items */
27
+ static paginate(items, options = {}) {
28
+ const page = Math.max(1, parseInt(options.page) || 1);
29
+ const perPage = Math.min(Math.max(1, parseInt(options.perPage) || 20), options.maxPerPage || 100);
30
+ const total = items.length;
31
+ const totalPages = Math.ceil(total / perPage);
32
+ const offset = (page - 1) * perPage;
33
+
34
+ const data = items.slice(offset, offset + perPage);
35
+
36
+ return {
37
+ data,
38
+ meta: {
39
+ page,
40
+ perPage,
41
+ total,
42
+ totalPages,
43
+ from: total > 0 ? offset + 1 : 0,
44
+ to: Math.min(offset + perPage, total),
45
+ hasNext: page < totalPages,
46
+ hasPrev: page > 1,
47
+ nextPage: page < totalPages ? page + 1 : null,
48
+ prevPage: page > 1 ? page - 1 : null,
49
+ },
50
+ };
51
+ }
52
+
53
+ /** Create pagination from pre-sliced data (database query) */
54
+ static fromQuery(data, total, options = {}) {
55
+ const page = Math.max(1, parseInt(options.page) || 1);
56
+ const perPage = Math.max(1, parseInt(options.perPage) || 20);
57
+ const totalPages = Math.ceil(total / perPage);
58
+ const offset = (page - 1) * perPage;
59
+
60
+ return {
61
+ data,
62
+ meta: {
63
+ page,
64
+ perPage,
65
+ total,
66
+ totalPages,
67
+ from: total > 0 ? offset + 1 : 0,
68
+ to: Math.min(offset + perPage, total),
69
+ hasNext: page < totalPages,
70
+ hasPrev: page > 1,
71
+ nextPage: page < totalPages ? page + 1 : null,
72
+ prevPage: page > 1 ? page - 1 : null,
73
+ },
74
+ };
75
+ }
76
+
77
+ /** Extract pagination params from request */
78
+ static fromRequest(req, defaults = {}) {
79
+ const query = req.query || {};
80
+ return {
81
+ page: parseInt(query.page) || defaults.page || 1,
82
+ perPage: Math.min(
83
+ parseInt(query.per_page || query.perPage || query.limit) || defaults.perPage || 20,
84
+ defaults.maxPerPage || 100
85
+ ),
86
+ sort: query.sort || defaults.sort || null,
87
+ order: (query.order || defaults.order || 'asc').toLowerCase(),
88
+ };
89
+ }
90
+
91
+ /** Generate SQL LIMIT/OFFSET clause */
92
+ static sql(options = {}) {
93
+ const page = Math.max(1, parseInt(options.page) || 1);
94
+ const perPage = Math.max(1, parseInt(options.perPage) || 20);
95
+ const offset = (page - 1) * perPage;
96
+ return { limit: perPage, offset, sql: `LIMIT ${perPage} OFFSET ${offset}` };
97
+ }
98
+
99
+ /** Generate pagination links (for API responses) */
100
+ static links(baseURL, meta) {
101
+ const links = {};
102
+ const separator = baseURL.includes('?') ? '&' : '?';
103
+
104
+ links.first = `${baseURL}${separator}page=1&per_page=${meta.perPage}`;
105
+ links.last = `${baseURL}${separator}page=${meta.totalPages}&per_page=${meta.perPage}`;
106
+
107
+ if (meta.hasNext) {
108
+ links.next = `${baseURL}${separator}page=${meta.nextPage}&per_page=${meta.perPage}`;
109
+ }
110
+ if (meta.hasPrev) {
111
+ links.prev = `${baseURL}${separator}page=${meta.prevPage}&per_page=${meta.perPage}`;
112
+ }
113
+
114
+ return links;
115
+ }
116
+
117
+ /** Generate page numbers for UI rendering */
118
+ static pageNumbers(currentPage, totalPages, maxVisible = 7) {
119
+ const pages = [];
120
+
121
+ if (totalPages <= maxVisible) {
122
+ for (let i = 1; i <= totalPages; i++) pages.push(i);
123
+ return pages;
124
+ }
125
+
126
+ const half = Math.floor(maxVisible / 2);
127
+ let start = Math.max(1, currentPage - half);
128
+ let end = Math.min(totalPages, start + maxVisible - 1);
129
+
130
+ if (end - start < maxVisible - 1) {
131
+ start = Math.max(1, end - maxVisible + 1);
132
+ }
133
+
134
+ if (start > 1) {
135
+ pages.push(1);
136
+ if (start > 2) pages.push('...');
137
+ }
138
+
139
+ for (let i = start; i <= end; i++) pages.push(i);
140
+
141
+ if (end < totalPages) {
142
+ if (end < totalPages - 1) pages.push('...');
143
+ pages.push(totalPages);
144
+ }
145
+
146
+ return pages;
147
+ }
148
+
149
+ /** Cursor-based pagination */
150
+ static cursorPaginate(items, options = {}) {
151
+ const perPage = Math.max(1, parseInt(options.perPage) || 20);
152
+ const cursor = options.cursor || null;
153
+ const cursorField = options.cursorField || 'id';
154
+
155
+ let filtered = items;
156
+ if (cursor) {
157
+ const cursorIndex = items.findIndex(item => String(item[cursorField]) === String(cursor));
158
+ if (cursorIndex >= 0) {
159
+ filtered = items.slice(cursorIndex + 1);
160
+ }
161
+ }
162
+
163
+ const data = filtered.slice(0, perPage);
164
+ const hasMore = filtered.length > perPage;
165
+ const nextCursor = data.length > 0 ? data[data.length - 1][cursorField] : null;
166
+
167
+ return {
168
+ data,
169
+ meta: {
170
+ perPage,
171
+ hasMore,
172
+ nextCursor: hasMore ? nextCursor : null,
173
+ prevCursor: cursor,
174
+ },
175
+ };
176
+ }
177
+ }
178
+
179
+ module.exports = { Paginator };