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,204 @@
1
+ /**
2
+ * VoltJS REST API Builder
3
+ *
4
+ * Rapid REST API scaffolding with automatic CRUD, resource routing,
5
+ * versioning, and response formatting.
6
+ *
7
+ * @example
8
+ * const { RestAPI } = require('voltjs');
9
+ *
10
+ * const api = new RestAPI(app, { prefix: '/api/v1' });
11
+ *
12
+ * api.resource('users', {
13
+ * async index(req, res) { res.json(await User.all()); },
14
+ * async show(req, res) { res.json(await User.find(req.params.id)); },
15
+ * async store(req, res) { res.json(await User.create(req.body), 201); },
16
+ * async update(req, res) { res.json(await User.updateById(req.params.id, req.body)); },
17
+ * async destroy(req, res) { await User.deleteById(req.params.id); res.noContent(); },
18
+ * });
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ class RestAPI {
24
+ constructor(app, options = {}) {
25
+ this.app = app;
26
+ this.prefix = options.prefix || '/api';
27
+ this.version = options.version || null;
28
+ this.middleware = options.middleware || [];
29
+ this.resources = new Map();
30
+
31
+ // Default response transformer
32
+ this.transformer = options.transformer || null;
33
+ }
34
+
35
+ /** Define a full REST resource */
36
+ resource(name, handlers, options = {}) {
37
+ const base = `${this.prefix}/${name}`;
38
+ const middleware = [...this.middleware, ...(options.middleware || [])];
39
+
40
+ const routes = {
41
+ index: { method: 'get', path: base, handler: handlers.index },
42
+ store: { method: 'post', path: base, handler: handlers.store || handlers.create },
43
+ show: { method: 'get', path: `${base}/:id`, handler: handlers.show },
44
+ update: { method: 'put', path: `${base}/:id`, handler: handlers.update },
45
+ patch: { method: 'patch', path: `${base}/:id`, handler: handlers.patch || handlers.update },
46
+ destroy: { method: 'delete', path: `${base}/:id`, handler: handlers.destroy || handlers.delete },
47
+ };
48
+
49
+ for (const [action, route] of Object.entries(routes)) {
50
+ if (route.handler) {
51
+ const only = options.only;
52
+ const except = options.except;
53
+ if (only && !only.includes(action)) continue;
54
+ if (except && except.includes(action)) continue;
55
+
56
+ this.app[route.method](route.path, ...middleware, this._wrap(route.handler));
57
+ }
58
+ }
59
+
60
+ // Custom actions
61
+ if (handlers.custom) {
62
+ for (const [method, path, handler] of handlers.custom) {
63
+ this.app[method](`${base}${path}`, ...middleware, this._wrap(handler));
64
+ }
65
+ }
66
+
67
+ this.resources.set(name, { base, routes });
68
+ return this;
69
+ }
70
+
71
+ /** Define a single route */
72
+ route(method, path, handler, ...middleware) {
73
+ const fullPath = `${this.prefix}${path}`;
74
+ this.app[method](fullPath, ...this.middleware, ...middleware, this._wrap(handler));
75
+ return this;
76
+ }
77
+
78
+ /** GET shorthand */
79
+ get(path, handler, ...mw) { return this.route('get', path, handler, ...mw); }
80
+ /** POST shorthand */
81
+ post(path, handler, ...mw) { return this.route('post', path, handler, ...mw); }
82
+ /** PUT shorthand */
83
+ put(path, handler, ...mw) { return this.route('put', path, handler, ...mw); }
84
+ /** PATCH shorthand */
85
+ patch(path, handler, ...mw) { return this.route('patch', path, handler, ...mw); }
86
+ /** DELETE shorthand */
87
+ delete(path, handler, ...mw) { return this.route('delete', path, handler, ...mw); }
88
+
89
+ /** Create an API group with shared prefix and middleware */
90
+ group(prefix, middleware, fn) {
91
+ if (typeof middleware === 'function') {
92
+ fn = middleware;
93
+ middleware = [];
94
+ }
95
+ const sub = new RestAPI(this.app, {
96
+ prefix: `${this.prefix}${prefix}`,
97
+ middleware: [...this.middleware, ...middleware],
98
+ transformer: this.transformer,
99
+ });
100
+ fn(sub);
101
+ return this;
102
+ }
103
+
104
+ /** API versioning */
105
+ version(v, fn) {
106
+ return this.group(`/v${v}`, fn);
107
+ }
108
+
109
+ /** Standard success response */
110
+ static success(res, data, statusCode = 200, meta = {}) {
111
+ const response = {
112
+ success: true,
113
+ data,
114
+ ...(Object.keys(meta).length > 0 ? { meta } : {}),
115
+ };
116
+ res.json(response, statusCode);
117
+ }
118
+
119
+ /** Standard error response */
120
+ static error(res, message, statusCode = 400, errors = null) {
121
+ const response = {
122
+ success: false,
123
+ error: { message, code: statusCode },
124
+ ...(errors ? { errors } : {}),
125
+ };
126
+ res.json(response, statusCode);
127
+ }
128
+
129
+ /** Paginated response */
130
+ static paginated(res, data, meta) {
131
+ res.json({
132
+ success: true,
133
+ data,
134
+ meta: {
135
+ page: meta.page,
136
+ perPage: meta.perPage,
137
+ total: meta.total,
138
+ totalPages: meta.totalPages,
139
+ hasNext: meta.hasNext,
140
+ hasPrev: meta.hasPrev,
141
+ },
142
+ });
143
+ }
144
+
145
+ /** List all registered API routes */
146
+ listRoutes() {
147
+ const routes = [];
148
+ for (const [name, resource] of this.resources) {
149
+ for (const [action, route] of Object.entries(resource.routes)) {
150
+ if (route.handler) {
151
+ routes.push({
152
+ resource: name,
153
+ action,
154
+ method: route.method.toUpperCase(),
155
+ path: route.path,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ return routes;
161
+ }
162
+
163
+ // ===== INTERNAL =====
164
+
165
+ _wrap(handler) {
166
+ return async (req, res) => {
167
+ try {
168
+ const result = await handler(req, res);
169
+ // Auto-send if handler returns a value but didn't respond
170
+ if (result !== undefined && !res.writableEnded) {
171
+ if (this.transformer) {
172
+ res.json(this.transformer(result));
173
+ } else {
174
+ res.json(result);
175
+ }
176
+ }
177
+ } catch (error) {
178
+ if (!res.writableEnded) {
179
+ const status = error.statusCode || error.status || 500;
180
+ RestAPI.error(res, error.message || 'Internal Server Error', status);
181
+ }
182
+ }
183
+ };
184
+ }
185
+ }
186
+
187
+ /** API Error helper */
188
+ class ApiError extends Error {
189
+ constructor(message, statusCode = 400) {
190
+ super(message);
191
+ this.statusCode = statusCode;
192
+ this.name = 'ApiError';
193
+ }
194
+
195
+ static badRequest(msg = 'Bad Request') { return new ApiError(msg, 400); }
196
+ static unauthorized(msg = 'Unauthorized') { return new ApiError(msg, 401); }
197
+ static forbidden(msg = 'Forbidden') { return new ApiError(msg, 403); }
198
+ static notFound(msg = 'Not Found') { return new ApiError(msg, 404); }
199
+ static conflict(msg = 'Conflict') { return new ApiError(msg, 409); }
200
+ static validation(msg = 'Validation Failed') { return new ApiError(msg, 422); }
201
+ static internal(msg = 'Internal Server Error') { return new ApiError(msg, 500); }
202
+ }
203
+
204
+ module.exports = { RestAPI, ApiError };
@@ -0,0 +1,285 @@
1
+ /**
2
+ * VoltJS WebSocket Server
3
+ *
4
+ * Real-time WebSocket server with rooms, broadcasting, authentication,
5
+ * and automatic heartbeat.
6
+ *
7
+ * @example
8
+ * const { WebSocketServer } = require('voltjs');
9
+ *
10
+ * const wss = new WebSocketServer(app, { path: '/ws' });
11
+ *
12
+ * wss.on('connection', (client) => {
13
+ * client.join('lobby');
14
+ * client.on('chat', (data) => {
15
+ * wss.to('lobby').emit('chat', { user: client.id, ...data });
16
+ * });
17
+ * });
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const crypto = require('crypto');
23
+ const { EventEmitter } = require('events');
24
+
25
+ class WebSocketServer extends EventEmitter {
26
+ constructor(app, options = {}) {
27
+ super();
28
+ this.path = options.path || '/ws';
29
+ this.clients = new Map();
30
+ this.rooms = new Map();
31
+ this.heartbeatInterval = options.heartbeat || 30000;
32
+ this.authHandler = options.auth || null;
33
+ this._wss = null;
34
+
35
+ if (app && app._server) {
36
+ this._attachToServer(app._server);
37
+ } else if (app) {
38
+ // Defer attachment until server starts
39
+ const originalListen = app.listen?.bind(app);
40
+ if (originalListen) {
41
+ app.listen = (...args) => {
42
+ const server = originalListen(...args);
43
+ this._attachToServer(server);
44
+ return server;
45
+ };
46
+ }
47
+ }
48
+ }
49
+
50
+ _attachToServer(server) {
51
+ try {
52
+ const { WebSocketServer: WSS } = require('ws');
53
+ this._wss = new WSS({ server, path: this.path });
54
+
55
+ this._wss.on('connection', (ws, req) => this._onConnection(ws, req));
56
+
57
+ // Heartbeat timer
58
+ this._heartbeatTimer = setInterval(() => this._heartbeat(), this.heartbeatInterval);
59
+ if (this._heartbeatTimer.unref) this._heartbeatTimer.unref();
60
+ } catch (e) {
61
+ console.warn('[Volt] WebSocket: "ws" package not installed. Run: npm install ws');
62
+ }
63
+ }
64
+
65
+ async _onConnection(ws, req) {
66
+ const client = new WebSocketClient(ws, req, this);
67
+
68
+ // Authentication
69
+ if (this.authHandler) {
70
+ try {
71
+ const authResult = await this.authHandler(req, client);
72
+ if (authResult === false) {
73
+ ws.close(4001, 'Unauthorized');
74
+ return;
75
+ }
76
+ if (typeof authResult === 'object') {
77
+ client.user = authResult;
78
+ }
79
+ } catch (err) {
80
+ ws.close(4001, 'Auth failed');
81
+ return;
82
+ }
83
+ }
84
+
85
+ this.clients.set(client.id, client);
86
+
87
+ // Message handling
88
+ ws.on('message', (raw) => {
89
+ try {
90
+ const message = JSON.parse(raw.toString());
91
+ const event = message.event || 'message';
92
+ const data = message.data || message;
93
+
94
+ client.emit(event, data);
95
+ this.emit(event, data, client);
96
+ } catch {
97
+ client.emit('message', raw.toString());
98
+ }
99
+ });
100
+
101
+ ws.on('close', () => {
102
+ // Leave all rooms
103
+ for (const [roomName, members] of this.rooms) {
104
+ members.delete(client.id);
105
+ if (members.size === 0) this.rooms.delete(roomName);
106
+ }
107
+ this.clients.delete(client.id);
108
+ this.emit('disconnect', client);
109
+ });
110
+
111
+ ws.on('error', (err) => {
112
+ this.emit('error', err, client);
113
+ });
114
+
115
+ ws.isAlive = true;
116
+ ws.on('pong', () => { ws.isAlive = true; });
117
+
118
+ this.emit('connection', client);
119
+ }
120
+
121
+ /** Broadcast to all connected clients */
122
+ broadcast(event, data, exclude = null) {
123
+ const payload = JSON.stringify({ event, data });
124
+ for (const [id, client] of this.clients) {
125
+ if (exclude && (exclude === id || exclude === client)) continue;
126
+ client._send(payload);
127
+ }
128
+ }
129
+
130
+ /** Send to a specific room */
131
+ to(room) {
132
+ return {
133
+ emit: (event, data) => {
134
+ const members = this.rooms.get(room);
135
+ if (!members) return;
136
+ const payload = JSON.stringify({ event, data });
137
+ for (const clientId of members) {
138
+ const client = this.clients.get(clientId);
139
+ if (client) client._send(payload);
140
+ }
141
+ },
142
+ clients: () => {
143
+ const members = this.rooms.get(room);
144
+ if (!members) return [];
145
+ return [...members].map(id => this.clients.get(id)).filter(Boolean);
146
+ },
147
+ count: () => {
148
+ return this.rooms.get(room)?.size || 0;
149
+ },
150
+ };
151
+ }
152
+
153
+ /** Get a client by ID */
154
+ client(id) {
155
+ return this.clients.get(id);
156
+ }
157
+
158
+ /** Get all connected client IDs */
159
+ get clientIds() {
160
+ return [...this.clients.keys()];
161
+ }
162
+
163
+ /** Get connected client count */
164
+ get clientCount() {
165
+ return this.clients.size;
166
+ }
167
+
168
+ /** Get all room names */
169
+ get roomNames() {
170
+ return [...this.rooms.keys()];
171
+ }
172
+
173
+ /** Require authentication */
174
+ requireAuth(handler) {
175
+ this.authHandler = handler;
176
+ return this;
177
+ }
178
+
179
+ /** Close the WebSocket server */
180
+ close() {
181
+ if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
182
+ if (this._wss) this._wss.close();
183
+ this.clients.clear();
184
+ this.rooms.clear();
185
+ }
186
+
187
+ _heartbeat() {
188
+ if (!this._wss) return;
189
+ for (const [id, client] of this.clients) {
190
+ if (!client._ws.isAlive) {
191
+ client._ws.terminate();
192
+ this.clients.delete(id);
193
+ continue;
194
+ }
195
+ client._ws.isAlive = false;
196
+ client._ws.ping();
197
+ }
198
+ }
199
+ }
200
+
201
+ class WebSocketClient extends EventEmitter {
202
+ constructor(ws, req, server) {
203
+ super();
204
+ this._ws = ws;
205
+ this._server = server;
206
+ this.id = crypto.randomBytes(8).toString('hex');
207
+ this.ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress;
208
+ this.user = null;
209
+ this.data = {}; // Custom per-client data
210
+
211
+ // Parse URL params
212
+ const url = new URL(req.url, `http://${req.headers.host}`);
213
+ this.query = Object.fromEntries(url.searchParams);
214
+ }
215
+
216
+ /** Send event+data to this client */
217
+ emit(event, data) {
218
+ if (event === 'newListener' || event === 'removeListener') {
219
+ return super.emit(event, data);
220
+ }
221
+
222
+ // If there are listeners, fire them (internal event)
223
+ if (this.listenerCount(event) > 0) {
224
+ super.emit(event, data);
225
+ return this;
226
+ }
227
+
228
+ // Otherwise, send to the WebSocket
229
+ this._send(JSON.stringify({ event, data }));
230
+ return this;
231
+ }
232
+
233
+ /** Send raw data */
234
+ send(data) {
235
+ this._send(typeof data === 'string' ? data : JSON.stringify(data));
236
+ return this;
237
+ }
238
+
239
+ /** Join a room */
240
+ join(room) {
241
+ if (!this._server.rooms.has(room)) {
242
+ this._server.rooms.set(room, new Set());
243
+ }
244
+ this._server.rooms.get(room).add(this.id);
245
+ return this;
246
+ }
247
+
248
+ /** Leave a room */
249
+ leave(room) {
250
+ const members = this._server.rooms.get(room);
251
+ if (members) {
252
+ members.delete(this.id);
253
+ if (members.size === 0) this._server.rooms.delete(room);
254
+ }
255
+ return this;
256
+ }
257
+
258
+ /** Get rooms this client is in */
259
+ get rooms() {
260
+ const rooms = [];
261
+ for (const [name, members] of this._server.rooms) {
262
+ if (members.has(this.id)) rooms.push(name);
263
+ }
264
+ return rooms;
265
+ }
266
+
267
+ /** Broadcast to all except this client */
268
+ broadcast(event, data) {
269
+ this._server.broadcast(event, data, this.id);
270
+ return this;
271
+ }
272
+
273
+ /** Disconnect this client */
274
+ disconnect(code = 1000, reason = '') {
275
+ this._ws.close(code, reason);
276
+ }
277
+
278
+ _send(payload) {
279
+ if (this._ws.readyState === 1) { // OPEN
280
+ this._ws.send(payload);
281
+ }
282
+ }
283
+ }
284
+
285
+ module.exports = { WebSocketServer, WebSocketClient };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * VoltJS CLI — `volt build`
3
+ * Prepares the application for production.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const c = {
12
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
13
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m',
14
+ };
15
+
16
+ module.exports = function build(args) {
17
+ const cwd = process.cwd();
18
+ const distDir = path.join(cwd, 'dist');
19
+
20
+ console.log(`${c.cyan}${c.bold}⚡ VoltJS Build${c.reset}\n`);
21
+
22
+ // Clean dist
23
+ if (fs.existsSync(distDir)) {
24
+ fs.rmSync(distDir, { recursive: true, force: true });
25
+ }
26
+ fs.mkdirSync(distDir, { recursive: true });
27
+
28
+ // Copy relevant files to dist
29
+ const copyDirs = ['pages', 'api', 'views', 'components', 'models', 'middleware', 'config', 'public', 'database'];
30
+ const copyFiles = ['app.js', 'index.js', 'server.js', 'volt.config.js', 'package.json'];
31
+
32
+ let fileCount = 0;
33
+
34
+ for (const dir of copyDirs) {
35
+ const src = path.join(cwd, dir);
36
+ if (fs.existsSync(src)) {
37
+ copyRecursive(src, path.join(distDir, dir));
38
+ console.log(` ${c.green}✓${c.reset} ${dir}/`);
39
+ fileCount++;
40
+ }
41
+ }
42
+
43
+ for (const file of copyFiles) {
44
+ const src = path.join(cwd, file);
45
+ if (fs.existsSync(src)) {
46
+ fs.copyFileSync(src, path.join(distDir, file));
47
+ console.log(` ${c.green}✓${c.reset} ${file}`);
48
+ fileCount++;
49
+ }
50
+ }
51
+
52
+ // Create production-ready package.json
53
+ const pkgPath = path.join(cwd, 'package.json');
54
+ if (fs.existsSync(pkgPath)) {
55
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
56
+ const prodPkg = {
57
+ name: pkg.name,
58
+ version: pkg.version,
59
+ private: true,
60
+ scripts: { start: 'node app.js' },
61
+ dependencies: pkg.dependencies || {},
62
+ };
63
+ fs.writeFileSync(path.join(distDir, 'package.json'), JSON.stringify(prodPkg, null, 2));
64
+ }
65
+
66
+ // Minify CSS files
67
+ const cssDir = path.join(distDir, 'public', 'css');
68
+ if (fs.existsSync(cssDir)) {
69
+ const cssFiles = fs.readdirSync(cssDir).filter(f => f.endsWith('.css'));
70
+ for (const file of cssFiles) {
71
+ const content = fs.readFileSync(path.join(cssDir, file), 'utf-8');
72
+ const minified = content
73
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
74
+ .replace(/\s+/g, ' ') // Collapse whitespace
75
+ .replace(/\s*([{}:;,])\s*/g, '$1') // Remove spaces around symbols
76
+ .trim();
77
+ fs.writeFileSync(path.join(cssDir, file), minified);
78
+ }
79
+ console.log(` ${c.green}✓${c.reset} CSS minified`);
80
+ }
81
+
82
+ // Create necessary dirs
83
+ for (const dir of ['logs', 'storage']) {
84
+ fs.mkdirSync(path.join(distDir, dir), { recursive: true });
85
+ }
86
+
87
+ console.log(`
88
+ ${c.green}${c.bold}✓ Build complete!${c.reset} ${c.dim}(${fileCount} items)${c.reset}
89
+
90
+ ${c.bold}To deploy:${c.reset}
91
+ ${c.cyan}cd dist${c.reset}
92
+ ${c.cyan}npm install --production${c.reset}
93
+ ${c.cyan}npm start${c.reset}
94
+ `);
95
+ };
96
+
97
+ function copyRecursive(src, dest) {
98
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
99
+
100
+ const entries = fs.readdirSync(src, { withFileTypes: true });
101
+ for (const entry of entries) {
102
+ const srcPath = path.join(src, entry.name);
103
+ const destPath = path.join(dest, entry.name);
104
+
105
+ if (entry.isDirectory()) {
106
+ copyRecursive(srcPath, destPath);
107
+ } else {
108
+ fs.copyFileSync(srcPath, destPath);
109
+ }
110
+ }
111
+ }