viojs-core 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.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # viojs-core
2
+
3
+ High-performance Node.js framework core built on uWebSockets.js.
4
+
5
+ ## Features
6
+ - uWebSockets.js integration
7
+ - Koa-style middleware
8
+ - TypeScript Decorators
9
+ - IoC / Dependency Injection
10
+ - WebSocket Hub
11
+
12
+ ## Installation
13
+ ```bash
14
+ npm install viojs-core
15
+ ```
@@ -0,0 +1,16 @@
1
+ import { us_listen_socket } from 'uWebSockets.js';
2
+ import { Middleware } from './Context';
3
+ export declare class Application {
4
+ private app;
5
+ private middlewares;
6
+ private router;
7
+ constructor();
8
+ use(middleware: Middleware): this;
9
+ publish(topic: string, message: string | ArrayBuffer, isBinary?: boolean, compress?: boolean): void;
10
+ registerWsRoute(pattern: string, behavior: any): void;
11
+ registerRoutes(controllers: any[]): void;
12
+ listen(port: number, cb?: (token: us_listen_socket) => void): Promise<void>;
13
+ private writeHeaders;
14
+ private sendResponse;
15
+ private compose;
16
+ }
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Application = void 0;
37
+ const uWebSockets_js_1 = require("uWebSockets.js");
38
+ const Context_1 = require("./Context");
39
+ const Router_1 = require("./Router");
40
+ const Container_1 = require("./Container");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ class Application {
44
+ app;
45
+ middlewares = [];
46
+ router;
47
+ constructor() {
48
+ this.app = (0, uWebSockets_js_1.App)();
49
+ this.router = new Router_1.Router(this);
50
+ // Bind the current Application instance to the DI container for global retrieval
51
+ Container_1.container.instances.set(Application, this);
52
+ }
53
+ use(middleware) {
54
+ this.middlewares.push(middleware);
55
+ return this;
56
+ }
57
+ // Global fast broadcast
58
+ publish(topic, message, isBinary = false, compress = false) {
59
+ this.app.publish(topic, message, isBinary, compress);
60
+ }
61
+ // Register a WebSocket namespace
62
+ registerWsRoute(pattern, behavior) {
63
+ // pattern must be uWS format, e.g. /* or /room/*
64
+ this.app.ws(pattern, behavior);
65
+ }
66
+ // Register router routes
67
+ registerRoutes(controllers) {
68
+ console.error("Application.registerRoutes called with", controllers.length, "controllers");
69
+ this.router.register(controllers);
70
+ this.use(this.router.middleware());
71
+ }
72
+ async listen(port, cb) {
73
+ this.app.any('/*', async (res, req) => {
74
+ const isUpgrade = req.getHeader('upgrade') === 'websocket';
75
+ if (isUpgrade) {
76
+ res.setYield(true);
77
+ return;
78
+ }
79
+ console.error("DEBUG ENTERED HANDLER");
80
+ const url = req.getUrl();
81
+ console.error("URL:", url);
82
+ let contextAborted = false;
83
+ try {
84
+ // Create context
85
+ const ctx = new Context_1.BaseContext(res, req);
86
+ console.log(`Incoming request: ${ctx.method} ${ctx.url}`);
87
+ // Keep track of abort safely beyond context setup since we are going async heavily
88
+ res.onAborted(() => {
89
+ ctx.aborted = true;
90
+ contextAborted = true;
91
+ });
92
+ // Execute middleware chain
93
+ try {
94
+ if (contextAborted)
95
+ return;
96
+ const composed = this.compose(this.middlewares);
97
+ await composed(ctx);
98
+ if (!contextAborted && !ctx.responded) {
99
+ this.sendResponse(ctx, contextAborted);
100
+ }
101
+ }
102
+ catch (err) {
103
+ console.error('Middleware error:', err);
104
+ if (!contextAborted) {
105
+ res.writeStatus('500 Internal Server Error').end('Internal Server Error');
106
+ }
107
+ }
108
+ }
109
+ catch (err) {
110
+ console.error('Context creation error:', err);
111
+ if (!contextAborted) {
112
+ res.writeStatus('500 Internal Server Error').end('Context Error');
113
+ }
114
+ }
115
+ });
116
+ this.app.listen(port, (token) => {
117
+ if (cb)
118
+ cb(token);
119
+ else {
120
+ if (token) {
121
+ console.log('Listening to port ' + port);
122
+ }
123
+ else {
124
+ console.log('Failed to listen to port ' + port);
125
+ }
126
+ }
127
+ });
128
+ }
129
+ // Extracted headers writer
130
+ writeHeaders(ctx) {
131
+ const { res, status, responseHeaders } = ctx;
132
+ const statusMap = {
133
+ 200: '200 OK',
134
+ 201: '201 Created',
135
+ 204: '204 No Content',
136
+ 400: '400 Bad Request',
137
+ 401: '401 Unauthorized',
138
+ 403: '403 Forbidden',
139
+ 404: '404 Not Found',
140
+ 500: '500 Internal Server Error'
141
+ };
142
+ const statusText = statusMap[status] || `${status} Status`;
143
+ res.writeStatus(statusText);
144
+ for (const [key, value] of Object.entries(responseHeaders)) {
145
+ if (Array.isArray(value)) {
146
+ value.forEach(v => res.writeHeader(key, v));
147
+ }
148
+ else {
149
+ res.writeHeader(key, value);
150
+ }
151
+ }
152
+ }
153
+ sendResponse(ctx, contextAborted) {
154
+ if (contextAborted)
155
+ return;
156
+ const { res, body, _filePath } = ctx;
157
+ // --- BRANCH A: FILE STREAMING ---
158
+ if (_filePath) {
159
+ fs.stat(_filePath, (err, stats) => {
160
+ if (err || !stats.isFile() || contextAborted || ctx.aborted) {
161
+ if (!contextAborted && !ctx.aborted) {
162
+ ctx.status = 404;
163
+ res.cork(() => {
164
+ this.writeHeaders(ctx);
165
+ res.end('File Not Found');
166
+ });
167
+ }
168
+ return;
169
+ }
170
+ const totalSize = stats.size;
171
+ // Basic Mime type guess if not set
172
+ if (!ctx.responseHeaders['Content-Type']) {
173
+ const ext = path.extname(_filePath).toLowerCase();
174
+ const mimeTypes = {
175
+ '.json': 'application/json',
176
+ '.png': 'image/png',
177
+ '.atlas': 'text/plain',
178
+ '.txt': 'text/plain',
179
+ '.jpg': 'image/jpeg',
180
+ '.jpeg': 'image/jpeg'
181
+ };
182
+ ctx.responseHeaders['Content-Type'] = mimeTypes[ext] || 'application/octet-stream';
183
+ }
184
+ res.cork(() => {
185
+ this.writeHeaders(ctx);
186
+ });
187
+ let readStream = fs.createReadStream(_filePath);
188
+ readStream.on('data', (chunk) => {
189
+ if (typeof chunk === 'string') {
190
+ chunk = Buffer.from(chunk);
191
+ }
192
+ // Try to send chunk via uWebSockets
193
+ const chunkArrayBuffer = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
194
+ let ok = false;
195
+ let done = false;
196
+ res.cork(() => {
197
+ const result = res.tryEnd(chunkArrayBuffer, totalSize);
198
+ ok = result[0];
199
+ done = result[1];
200
+ });
201
+ if (done) {
202
+ // Current chunk ended successfully and stream completed (if it was the last piece)
203
+ }
204
+ else if (ok) {
205
+ // Chunk successfully buffered to C++ space
206
+ }
207
+ else {
208
+ // **Backpressure activated!**
209
+ // C++ buffer is full (Client network is slow) -> Pause reading from Disk!
210
+ readStream.pause();
211
+ // Wait for C++ buffer to drain
212
+ res.onWritable((offset) => {
213
+ // C++ buffer drained, we can resume reading from Disk
214
+ readStream.resume();
215
+ // Important: You must return true explicitly to tell uWebSockets the stream is still alive
216
+ return true;
217
+ });
218
+ }
219
+ });
220
+ readStream.on('error', () => {
221
+ if (!contextAborted && !ctx.aborted) {
222
+ res.cork(() => {
223
+ res.end();
224
+ });
225
+ }
226
+ });
227
+ });
228
+ ctx.responded = true;
229
+ return;
230
+ }
231
+ // --- BRANCH B: NORMAL JSON / TEXT PAYLOAD ---
232
+ res.cork(() => {
233
+ this.writeHeaders(ctx);
234
+ if (typeof body === 'object') {
235
+ res.writeHeader('Content-Type', 'application/json');
236
+ res.end(JSON.stringify(body));
237
+ }
238
+ else if (body) {
239
+ res.end(String(body));
240
+ }
241
+ else {
242
+ // Default 404 behavior if no body is set
243
+ res.writeStatus('404 Not Found');
244
+ res.writeHeader('Content-Type', 'application/json');
245
+ res.end(JSON.stringify({ error: "Not Found", url: ctx.url, method: ctx.method }));
246
+ }
247
+ });
248
+ ctx.responded = true;
249
+ }
250
+ compose(middlewares) {
251
+ return function (ctx, next) {
252
+ let index = -1;
253
+ return dispatch(0);
254
+ function dispatch(i) {
255
+ if (i <= index)
256
+ return Promise.reject(new Error('next() called multiple times'));
257
+ index = i;
258
+ let fn = middlewares[i];
259
+ if (i === middlewares.length)
260
+ fn = next; // End of chain
261
+ if (!fn)
262
+ return Promise.resolve();
263
+ try {
264
+ return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
265
+ }
266
+ catch (err) {
267
+ return Promise.reject(err);
268
+ }
269
+ }
270
+ };
271
+ }
272
+ }
273
+ exports.Application = Application;
@@ -0,0 +1,6 @@
1
+ import 'reflect-metadata';
2
+ export declare class Container {
3
+ instances: Map<any, any>;
4
+ resolve<T>(target: any): T;
5
+ }
6
+ export declare const container: Container;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.container = exports.Container = void 0;
4
+ require("reflect-metadata");
5
+ const decorators_1 = require("../decorators");
6
+ class Container {
7
+ instances = new Map();
8
+ resolve(target) {
9
+ // Find if target has been marked as injectable (explicitly or via Controller)
10
+ const isInjectable = Reflect.getMetadata(decorators_1.INJECTABLE_METADATA, target) || target.name !== 'Object';
11
+ if (!isInjectable) {
12
+ throw new Error(`Cannot resolve ${target.name || target}. Make sure it has @Injectable() or @Controller() decorator.`);
13
+ }
14
+ // Return singleton if already instantiated
15
+ if (this.instances.has(target)) {
16
+ return this.instances.get(target);
17
+ }
18
+ // Check constructor dependencies
19
+ const tokens = Reflect.getMetadata('design:paramtypes', target) || [];
20
+ let injections = [];
21
+ if (tokens.length > 0) {
22
+ injections = tokens.map((token) => {
23
+ // Circular dependency or undefined typescript output guard
24
+ if (!token || token === target) {
25
+ throw new Error(`Failed to resolve dependencies for ${target.name}. Circular dependency detected or bad type.`);
26
+ }
27
+ return this.resolve(token);
28
+ });
29
+ }
30
+ // Instantiate
31
+ const instance = new target(...injections);
32
+ this.instances.set(target, instance);
33
+ return instance;
34
+ }
35
+ }
36
+ exports.Container = Container;
37
+ exports.container = new Container();
@@ -0,0 +1,27 @@
1
+ export type Next = () => Promise<void>;
2
+ export type Middleware<TContext extends BaseContext = BaseContext> = (ctx: TContext, next: Next) => Promise<void>;
3
+ import { HttpRequest, HttpResponse } from 'uWebSockets.js';
4
+ export declare class BaseContext {
5
+ req: HttpRequest;
6
+ res: HttpResponse;
7
+ aborted: boolean;
8
+ responded: boolean;
9
+ method: string;
10
+ url: string;
11
+ query: string;
12
+ headers: Record<string, string>;
13
+ params: Record<string, string>;
14
+ body: any;
15
+ status: number;
16
+ responseHeaders: Record<string, string | string[]>;
17
+ _filePath?: string;
18
+ private bodyPromise;
19
+ constructor(res: HttpResponse, req: HttpRequest);
20
+ json(): Promise<any>;
21
+ text(): Promise<string>;
22
+ private _parsedQuery?;
23
+ get queries(): Record<string, string>;
24
+ throw(status: number, message?: string): never;
25
+ redirect(url: string, status?: number): void;
26
+ sendFile(path: string): void;
27
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseContext = void 0;
4
+ class BaseContext {
5
+ req;
6
+ res;
7
+ aborted = false;
8
+ responded = false; // Tracks if response has been sent to uWebSockets
9
+ // Request properties
10
+ method;
11
+ url;
12
+ query;
13
+ headers;
14
+ params;
15
+ // Response properties
16
+ body;
17
+ status = 200;
18
+ responseHeaders = {};
19
+ // File streaming properties
20
+ _filePath;
21
+ bodyPromise;
22
+ constructor(res, req) {
23
+ this.res = res;
24
+ this.req = req;
25
+ this.method = req.getMethod().toUpperCase();
26
+ this.url = req.getUrl();
27
+ // query params
28
+ this.query = req.getQuery();
29
+ this.headers = {};
30
+ req.forEach((k, v) => {
31
+ this.headers[k] = v;
32
+ });
33
+ this.params = {};
34
+ // Important: Handle abort
35
+ res.onAborted(() => {
36
+ this.aborted = true;
37
+ });
38
+ // Check if we expect a body
39
+ const contentLength = this.headers['content-length'];
40
+ const transferEncoding = this.headers['transfer-encoding'];
41
+ const hasBody = (contentLength && parseInt(contentLength) > 0) || (transferEncoding && transferEncoding.includes('chunked'));
42
+ if (hasBody) {
43
+ this.bodyPromise = new Promise((resolve, reject) => {
44
+ let buffer;
45
+ res.onData((chunk, isLast) => {
46
+ const chunkBuffer = Buffer.from(chunk);
47
+ if (isLast) {
48
+ if (buffer) {
49
+ resolve(Buffer.concat([buffer, chunkBuffer]));
50
+ }
51
+ else {
52
+ resolve(chunkBuffer);
53
+ }
54
+ }
55
+ else {
56
+ if (buffer) {
57
+ buffer = Buffer.concat([buffer, chunkBuffer]);
58
+ }
59
+ else {
60
+ buffer = Buffer.concat([chunkBuffer]);
61
+ }
62
+ }
63
+ });
64
+ });
65
+ }
66
+ else {
67
+ this.bodyPromise = Promise.resolve(Buffer.alloc(0));
68
+ }
69
+ }
70
+ // Helper to read body (async because we might need to buffer)
71
+ async json() {
72
+ const buffer = await this.bodyPromise;
73
+ if (!buffer || buffer.length === 0)
74
+ return {};
75
+ try {
76
+ return JSON.parse(buffer.toString());
77
+ }
78
+ catch (e) {
79
+ return {};
80
+ }
81
+ }
82
+ async text() {
83
+ const buffer = await this.bodyPromise;
84
+ return buffer ? buffer.toString() : '';
85
+ }
86
+ // Lazy initialization for query parameters
87
+ _parsedQuery;
88
+ get queries() {
89
+ if (!this._parsedQuery) {
90
+ this._parsedQuery = {};
91
+ if (this.query) {
92
+ const searchParams = new URLSearchParams(this.query);
93
+ for (const [key, value] of searchParams.entries()) {
94
+ this._parsedQuery[key] = value;
95
+ }
96
+ }
97
+ }
98
+ return this._parsedQuery;
99
+ }
100
+ throw(status, message) {
101
+ this.status = status;
102
+ throw new Error(message || `Error ${status}`);
103
+ }
104
+ redirect(url, status = 302) {
105
+ this.status = status;
106
+ this.responseHeaders['Location'] = url;
107
+ this.body = '';
108
+ }
109
+ sendFile(path) {
110
+ this._filePath = path;
111
+ }
112
+ }
113
+ exports.BaseContext = BaseContext;
@@ -0,0 +1,13 @@
1
+ import { Middleware } from './Context';
2
+ import { Application } from './Application';
3
+ export declare class Router {
4
+ private app;
5
+ private routes;
6
+ constructor(app: Application);
7
+ register(controllers: any[]): void;
8
+ private registerWs;
9
+ private compose;
10
+ private pathToRegexp;
11
+ middleware(): Middleware;
12
+ private findRoute;
13
+ }
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Router = void 0;
4
+ const decorators_1 = require("../decorators");
5
+ const WsContext_1 = require("./WsContext");
6
+ const validation_1 = require("../validation");
7
+ const Container_1 = require("./Container");
8
+ class Router {
9
+ app;
10
+ routes = [];
11
+ constructor(app) {
12
+ this.app = app;
13
+ }
14
+ register(controllers) {
15
+ console.log("HELLO FROM ROUTER REGISTER");
16
+ controllers.forEach(controllerClass => {
17
+ // Instantiate securely with DI Container!
18
+ const instance = Container_1.container.resolve(controllerClass);
19
+ // Check if this is a WebSocket Controller
20
+ const wsPrefix = Reflect.getMetadata(decorators_1.WS_CONTROLLER_METADATA, controllerClass);
21
+ if (wsPrefix !== undefined) {
22
+ this.registerWs(wsPrefix, controllerClass, instance);
23
+ return; // Skip normal HTTP routing
24
+ }
25
+ const prefix = Reflect.getMetadata(decorators_1.CONTROLLER_METADATA, controllerClass);
26
+ const routes = Reflect.getMetadata(decorators_1.ROUTE_METADATA, controllerClass);
27
+ const controllerMiddlewares = Reflect.getMetadata(decorators_1.MIDDLEWARE_METADATA, controllerClass) || [];
28
+ if (!routes)
29
+ return;
30
+ routes.forEach(route => {
31
+ const fullPath = (prefix + route.path).replace(/\/+/g, '/');
32
+ // Convert path to regex /users/:id -> /^\/users\/([^/]+)$/
33
+ const { re, keys } = this.pathToRegexp(fullPath);
34
+ const routeMiddlewares = Reflect.getMetadata(decorators_1.MIDDLEWARE_METADATA, controllerClass.prototype, route.methodName) || [];
35
+ const paramsMetadata = Reflect.getOwnMetadata(decorators_1.PARAM_METADATA, controllerClass.prototype, route.methodName) || [];
36
+ // Compose: Controller Middlewares -> Route Middlewares -> Handler
37
+ const allMiddlewares = [...controllerMiddlewares, ...routeMiddlewares];
38
+ console.log(`Registering route: ${route.method.toUpperCase()} ${fullPath} -> regex: ${re}`);
39
+ this.routes.push({
40
+ method: route.method.toUpperCase(),
41
+ path: re,
42
+ paramNames: keys,
43
+ handler: async (ctx) => {
44
+ const handler = instance[route.methodName].bind(instance);
45
+ console.log("Handler loaded for", route.methodName);
46
+ // Create a mini-chain for this route
47
+ const chain = [...allMiddlewares, async (ctx, next) => {
48
+ let args = [ctx]; // Default backwards-compatible fallback array
49
+ // If parameter decorators are used, we switch into reflection injection mode
50
+ if (paramsMetadata.length > 0) {
51
+ const maxIndex = Math.max(...paramsMetadata.map(p => p.index));
52
+ args = new Array(maxIndex + 1).fill(undefined);
53
+ const hasBody = paramsMetadata.some(p => p.type === 'body');
54
+ let bodyData = null;
55
+ if (hasBody) {
56
+ bodyData = await ctx.json();
57
+ }
58
+ for (const meta of paramsMetadata) {
59
+ let value;
60
+ switch (meta.type) {
61
+ case 'ctx':
62
+ value = ctx;
63
+ break;
64
+ case 'param':
65
+ value = meta.name ? ctx.params[meta.name] : ctx.params;
66
+ break;
67
+ case 'query':
68
+ value = meta.name ? ctx.queries[meta.name] : ctx.queries;
69
+ break;
70
+ case 'body':
71
+ value = meta.name ? (bodyData ? bodyData[meta.name] : undefined) : bodyData;
72
+ break;
73
+ }
74
+ // Simple auto-casting for strongly typed metadata via typescript design:paramtypes
75
+ if (value !== undefined && meta.designType) {
76
+ if (meta.designType === Number) {
77
+ const parsed = Number(value);
78
+ if (isNaN(parsed))
79
+ throw new validation_1.ValidationError([`Parameter '${meta.name}' must be a number`]);
80
+ value = parsed;
81
+ }
82
+ else if (meta.designType === Boolean) {
83
+ value = value === 'true' || value === '1' || value === true;
84
+ }
85
+ else if (meta.designType === String) {
86
+ value = String(value);
87
+ }
88
+ else if (meta.designType.name !== 'Object' && meta.designType.name !== 'Array') {
89
+ // It's a potential DTO class! Execute native Validation layer
90
+ value = (0, validation_1.validateDTO)(meta.designType, value);
91
+ }
92
+ }
93
+ else if (value === undefined && meta.designType && meta.designType.name !== 'Object' && meta.designType.name !== 'String' && meta.designType.name !== 'Number' && meta.designType.name !== 'Boolean') {
94
+ // Body was undefined/null, let validation engine check if @IsRequired complains!
95
+ value = (0, validation_1.validateDTO)(meta.designType, {});
96
+ }
97
+ args[meta.index] = value;
98
+ }
99
+ }
100
+ // Execute handler with injected args (passing undefined will trigger ES6 default arguments)
101
+ const result = await handler(...args);
102
+ console.log("Handler returned:", result);
103
+ if (result !== undefined) {
104
+ ctx.body = result;
105
+ }
106
+ }];
107
+ const composed = this.compose(chain);
108
+ await composed(ctx);
109
+ }
110
+ });
111
+ });
112
+ });
113
+ }
114
+ registerWs(prefix, controllerClass, instance) {
115
+ const fullPath = prefix.replace(/\/+/g, '/');
116
+ const openMethod = Reflect.getMetadata(decorators_1.WS_ON_OPEN_METADATA, controllerClass);
117
+ const messageMethod = Reflect.getMetadata(decorators_1.WS_ON_MESSAGE_METADATA, controllerClass);
118
+ const closeMethod = Reflect.getMetadata(decorators_1.WS_ON_CLOSE_METADATA, controllerClass);
119
+ const drainMethod = Reflect.getMetadata(decorators_1.WS_ON_DRAIN_METADATA, controllerClass);
120
+ console.log(`Registering WS route: ${fullPath}`);
121
+ this.app.registerWsRoute(fullPath, {
122
+ idleTimeout: 32,
123
+ maxBackpressure: 1024 * 1024 * 10,
124
+ maxPayloadLength: 1024 * 1024 * 16,
125
+ upgrade: (res, req, context) => {
126
+ // Must read req here safely because req is invalidated outside
127
+ const url = req.getUrl();
128
+ const queryStr = req.getQuery();
129
+ // Extremely simple query parser
130
+ const query = {};
131
+ if (queryStr) {
132
+ queryStr.split('&').forEach((pair) => {
133
+ const [k, v] = pair.split('=');
134
+ query[k] = decodeURIComponent(v || '');
135
+ });
136
+ }
137
+ res.upgrade({ url, query }, req.getHeader('sec-websocket-key'), req.getHeader('sec-websocket-protocol'), req.getHeader('sec-websocket-extensions'), context);
138
+ },
139
+ open: (ws) => {
140
+ const ctx = new WsContext_1.WsContext(ws);
141
+ ws.customCtx = ctx;
142
+ if (openMethod) {
143
+ instance[openMethod](ctx);
144
+ }
145
+ },
146
+ message: (ws, message, isBinary) => {
147
+ if (messageMethod) {
148
+ let data = message;
149
+ if (!isBinary && message instanceof ArrayBuffer) {
150
+ data = Buffer.from(message).toString('utf8');
151
+ }
152
+ instance[messageMethod](ws.customCtx, data, isBinary);
153
+ }
154
+ },
155
+ close: (ws, code, message) => {
156
+ if (closeMethod) {
157
+ instance[closeMethod](ws.customCtx, code, message);
158
+ }
159
+ },
160
+ drain: (ws) => {
161
+ if (drainMethod) {
162
+ instance[drainMethod](ws.customCtx);
163
+ }
164
+ }
165
+ });
166
+ }
167
+ // Helper to compose middlewares (similar to Application.compose but specific for this scope if needed)
168
+ // Actually we can reuse or duplicate it. Let's duplicate for simplicity to keep Router standalone.
169
+ compose(middlewares) {
170
+ return function (ctx, next) {
171
+ let index = -1;
172
+ return dispatch(0);
173
+ function dispatch(i) {
174
+ if (i <= index)
175
+ return Promise.reject(new Error('next() called multiple times'));
176
+ index = i;
177
+ let fn = middlewares[i];
178
+ if (i === middlewares.length)
179
+ fn = next;
180
+ if (!fn)
181
+ return Promise.resolve();
182
+ try {
183
+ return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
184
+ }
185
+ catch (err) {
186
+ return Promise.reject(err);
187
+ }
188
+ }
189
+ };
190
+ }
191
+ pathToRegexp(path) {
192
+ const keys = [];
193
+ // Escape special chars except :
194
+ let pattern = path.replace(/([.+*?^$(){}|[\]\\])/g, '\\$1');
195
+ // Replace :param with capture group
196
+ pattern = pattern.replace(/:(\w+)/g, (_, key) => {
197
+ keys.push(key);
198
+ return '([^/]+)';
199
+ });
200
+ // Add start/end anchors
201
+ return { re: new RegExp(`^${pattern}$`), keys };
202
+ }
203
+ middleware() {
204
+ return async (ctx, next) => {
205
+ // Find matching route
206
+ const matched = this.findRoute(ctx.method, ctx.url);
207
+ if (matched) {
208
+ console.log(`Route matched: ${ctx.method} ${ctx.url}`);
209
+ ctx.params = matched.params;
210
+ await matched.handler(ctx);
211
+ }
212
+ else {
213
+ console.log(`No route matched for: ${ctx.method} ${ctx.url}`);
214
+ await next();
215
+ }
216
+ };
217
+ }
218
+ findRoute(method, url) {
219
+ for (const route of this.routes) {
220
+ // console.log(`Checking route: ${route.method} ${route.path} vs ${method} ${url}`);
221
+ if (route.method !== method && route.method !== 'ANY')
222
+ continue;
223
+ const match = route.path.exec(url); // url in uWS does not include query string
224
+ if (match) {
225
+ const params = {};
226
+ route.paramNames.forEach((name, index) => {
227
+ params[name] = decodeURIComponent(match[index + 1] || '');
228
+ });
229
+ return { handler: route.handler, params };
230
+ }
231
+ }
232
+ return null;
233
+ }
234
+ }
235
+ exports.Router = Router;
@@ -0,0 +1,32 @@
1
+ import { WebSocket } from 'uWebSockets.js';
2
+ export declare class WsContext {
3
+ private readonly ws;
4
+ data: Record<string, any>;
5
+ query: Record<string, string>;
6
+ url: string;
7
+ constructor(ws: WebSocket<any>);
8
+ /**
9
+ * Extremely fast corked C++ network send
10
+ */
11
+ send(message: string | ArrayBuffer, isBinary?: boolean): boolean;
12
+ /**
13
+ * Subscribe to a High-Performance C++ Pub/Sub topic
14
+ */
15
+ subscribe(topic: string): boolean;
16
+ /**
17
+ * Unsubscribe from a topic
18
+ */
19
+ unsubscribe(topic: string): boolean;
20
+ /**
21
+ * Broadcast to all sockets subscribed to this topic globally.
22
+ */
23
+ publish(topic: string, message: string | ArrayBuffer, isBinary?: boolean): boolean;
24
+ /**
25
+ * Close the connection securely
26
+ */
27
+ close(code?: number, shortMessage?: string | ArrayBuffer): void;
28
+ /**
29
+ * Extract Player IP
30
+ */
31
+ getRemoteAddressAsText(): string;
32
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WsContext = void 0;
4
+ class WsContext {
5
+ ws;
6
+ data = {};
7
+ query = {};
8
+ url = '';
9
+ constructor(ws) {
10
+ this.ws = ws;
11
+ // Retrieve custom data injected during Upgrade
12
+ const userData = ws.getUserData();
13
+ if (userData) {
14
+ this.url = userData.url || '';
15
+ this.query = userData.query || {};
16
+ // Copy everything else so businesses can use it
17
+ Object.assign(this.data, userData);
18
+ }
19
+ }
20
+ /**
21
+ * Extremely fast corked C++ network send
22
+ */
23
+ send(message, isBinary = false) {
24
+ let result = false;
25
+ // Wrapped in cork to prevent socket chunk fragmentation warning during concurrent IO
26
+ this.ws.cork(() => {
27
+ // Returns: 0 = Backpressure | 1 = Success | 2 = Dropped Max Connections
28
+ result = this.ws.send(message, isBinary, false) === 1;
29
+ });
30
+ return result;
31
+ }
32
+ /**
33
+ * Subscribe to a High-Performance C++ Pub/Sub topic
34
+ */
35
+ subscribe(topic) {
36
+ return this.ws.subscribe(topic);
37
+ }
38
+ /**
39
+ * Unsubscribe from a topic
40
+ */
41
+ unsubscribe(topic) {
42
+ return this.ws.unsubscribe(topic);
43
+ }
44
+ /**
45
+ * Broadcast to all sockets subscribed to this topic globally.
46
+ */
47
+ publish(topic, message, isBinary = false) {
48
+ let result = false;
49
+ try {
50
+ // C++ underlying publish. If the socket is detached during @OnClose this will throw.
51
+ // Safely swallow here so Node doesn't crash from wild user attempts.
52
+ this.ws.cork(() => {
53
+ result = this.ws.publish(topic, message, isBinary);
54
+ });
55
+ }
56
+ catch (e) {
57
+ console.warn('[WsContext] Local socket publish failed (likely closed). Use Application.publish for global broadcasts.');
58
+ }
59
+ return result;
60
+ }
61
+ /**
62
+ * Close the connection securely
63
+ */
64
+ close(code, shortMessage) {
65
+ if (code) {
66
+ this.ws.end(code, shortMessage);
67
+ }
68
+ else {
69
+ this.ws.close();
70
+ }
71
+ }
72
+ /**
73
+ * Extract Player IP
74
+ */
75
+ getRemoteAddressAsText() {
76
+ try {
77
+ const buffer = this.ws.getRemoteAddressAsText();
78
+ return Buffer.from(buffer).toString('utf-8');
79
+ }
80
+ catch (e) {
81
+ return 'Unknown';
82
+ }
83
+ }
84
+ }
85
+ exports.WsContext = WsContext;
@@ -0,0 +1,41 @@
1
+ import 'reflect-metadata';
2
+ export declare const CONTROLLER_METADATA = "CONTROLLER_METADATA";
3
+ export declare const ROUTE_METADATA = "ROUTE_METADATA";
4
+ export declare const INJECTABLE_METADATA = "INJECTABLE_METADATA";
5
+ export declare function Injectable(): ClassDecorator;
6
+ export declare function Controller(prefix?: string): ClassDecorator;
7
+ export interface RouteDefinition {
8
+ path: string;
9
+ method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options';
10
+ methodName: string | symbol;
11
+ }
12
+ export declare const MIDDLEWARE_METADATA = "MIDDLEWARE_METADATA";
13
+ export declare function Use(...middlewares: Function[]): ClassDecorator & MethodDecorator;
14
+ export declare const Get: (path?: string) => MethodDecorator;
15
+ export declare const Post: (path?: string) => MethodDecorator;
16
+ export declare const Put: (path?: string) => MethodDecorator;
17
+ export declare const Delete: (path?: string) => MethodDecorator;
18
+ export declare const Patch: (path?: string) => MethodDecorator;
19
+ export declare const Head: (path?: string) => MethodDecorator;
20
+ export declare const Options: (path?: string) => MethodDecorator;
21
+ export declare const PARAM_METADATA = "PARAM_METADATA";
22
+ export interface ParamDefinition {
23
+ index: number;
24
+ type: 'param' | 'query' | 'body' | 'ctx';
25
+ name?: string;
26
+ designType?: any;
27
+ }
28
+ export declare const Param: (name?: string) => ParameterDecorator;
29
+ export declare const Query: (name?: string) => ParameterDecorator;
30
+ export declare const Body: (name?: string) => ParameterDecorator;
31
+ export declare const Ctx: () => ParameterDecorator;
32
+ export declare const WS_CONTROLLER_METADATA = "WS_CONTROLLER_METADATA";
33
+ export declare const WS_ON_OPEN_METADATA = "WS_ON_OPEN_METADATA";
34
+ export declare const WS_ON_MESSAGE_METADATA = "WS_ON_MESSAGE_METADATA";
35
+ export declare const WS_ON_CLOSE_METADATA = "WS_ON_CLOSE_METADATA";
36
+ export declare const WS_ON_DRAIN_METADATA = "WS_ON_DRAIN_METADATA";
37
+ export declare function WebSocketController(path?: string): ClassDecorator;
38
+ export declare const OnOpen: () => MethodDecorator;
39
+ export declare const OnMessage: () => MethodDecorator;
40
+ export declare const OnClose: () => MethodDecorator;
41
+ export declare const OnDrain: () => MethodDecorator;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OnDrain = exports.OnClose = exports.OnMessage = exports.OnOpen = exports.WS_ON_DRAIN_METADATA = exports.WS_ON_CLOSE_METADATA = exports.WS_ON_MESSAGE_METADATA = exports.WS_ON_OPEN_METADATA = exports.WS_CONTROLLER_METADATA = exports.Ctx = exports.Body = exports.Query = exports.Param = exports.PARAM_METADATA = exports.Options = exports.Head = exports.Patch = exports.Delete = exports.Put = exports.Post = exports.Get = exports.MIDDLEWARE_METADATA = exports.INJECTABLE_METADATA = exports.ROUTE_METADATA = exports.CONTROLLER_METADATA = void 0;
4
+ exports.Injectable = Injectable;
5
+ exports.Controller = Controller;
6
+ exports.Use = Use;
7
+ exports.WebSocketController = WebSocketController;
8
+ require("reflect-metadata");
9
+ exports.CONTROLLER_METADATA = 'CONTROLLER_METADATA';
10
+ exports.ROUTE_METADATA = 'ROUTE_METADATA';
11
+ exports.INJECTABLE_METADATA = 'INJECTABLE_METADATA';
12
+ function Injectable() {
13
+ return (target) => {
14
+ Reflect.defineMetadata(exports.INJECTABLE_METADATA, true, target);
15
+ };
16
+ }
17
+ function Controller(prefix = '') {
18
+ return (target) => {
19
+ Reflect.defineMetadata(exports.CONTROLLER_METADATA, prefix, target);
20
+ // Controllers are automatically considered injectable
21
+ Reflect.defineMetadata(exports.INJECTABLE_METADATA, true, target);
22
+ };
23
+ }
24
+ function createRouteDecorator(method) {
25
+ return (path = '/') => {
26
+ return (target, propertyKey, descriptor) => {
27
+ console.log(`@${method} called for ${String(propertyKey)}`);
28
+ if (!Reflect.hasMetadata(exports.ROUTE_METADATA, target.constructor)) {
29
+ Reflect.defineMetadata(exports.ROUTE_METADATA, [], target.constructor);
30
+ }
31
+ const routes = Reflect.getMetadata(exports.ROUTE_METADATA, target.constructor);
32
+ routes.push({
33
+ method: method,
34
+ path,
35
+ methodName: propertyKey,
36
+ });
37
+ Reflect.defineMetadata(exports.ROUTE_METADATA, routes, target.constructor);
38
+ };
39
+ };
40
+ }
41
+ exports.MIDDLEWARE_METADATA = 'MIDDLEWARE_METADATA';
42
+ function Use(...middlewares) {
43
+ return (target, propertyKey, descriptor) => {
44
+ try {
45
+ console.log(`@Use called for ${String(propertyKey || target.name)}`);
46
+ if (propertyKey) {
47
+ // Method decorator
48
+ // target is prototype for instance members
49
+ if (!Reflect.hasMetadata(exports.MIDDLEWARE_METADATA, target, propertyKey)) {
50
+ Reflect.defineMetadata(exports.MIDDLEWARE_METADATA, [], target, propertyKey);
51
+ }
52
+ const existing = Reflect.getMetadata(exports.MIDDLEWARE_METADATA, target, propertyKey);
53
+ Reflect.defineMetadata(exports.MIDDLEWARE_METADATA, [...existing, ...middlewares], target, propertyKey);
54
+ }
55
+ else {
56
+ // Class decorator
57
+ if (!Reflect.hasMetadata(exports.MIDDLEWARE_METADATA, target)) {
58
+ Reflect.defineMetadata(exports.MIDDLEWARE_METADATA, [], target);
59
+ }
60
+ const existing = Reflect.getMetadata(exports.MIDDLEWARE_METADATA, target);
61
+ Reflect.defineMetadata(exports.MIDDLEWARE_METADATA, [...existing, ...middlewares], target);
62
+ }
63
+ }
64
+ catch (e) {
65
+ console.error(`Error in @Use decorator:`, e);
66
+ throw e;
67
+ }
68
+ };
69
+ }
70
+ exports.Get = createRouteDecorator('get');
71
+ exports.Post = createRouteDecorator('post');
72
+ exports.Put = createRouteDecorator('put');
73
+ exports.Delete = createRouteDecorator('delete');
74
+ exports.Patch = createRouteDecorator('patch');
75
+ exports.Head = createRouteDecorator('head');
76
+ exports.Options = createRouteDecorator('options');
77
+ exports.PARAM_METADATA = 'PARAM_METADATA';
78
+ function createParamDecorator(type) {
79
+ return (name) => {
80
+ return (target, propertyKey, parameterIndex) => {
81
+ if (!propertyKey)
82
+ return;
83
+ const params = Reflect.getOwnMetadata(exports.PARAM_METADATA, target, propertyKey) || [];
84
+ // Extract the strong type info. "design:paramtypes" is generated by TS when emitDecoratorMetadata is true.
85
+ const designTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey);
86
+ const designType = designTypes ? designTypes[parameterIndex] : undefined;
87
+ params.push({
88
+ index: parameterIndex,
89
+ type,
90
+ name,
91
+ designType
92
+ });
93
+ Reflect.defineMetadata(exports.PARAM_METADATA, params, target, propertyKey);
94
+ };
95
+ };
96
+ }
97
+ exports.Param = createParamDecorator('param');
98
+ exports.Query = createParamDecorator('query');
99
+ exports.Body = createParamDecorator('body');
100
+ const Ctx = () => createParamDecorator('ctx')();
101
+ exports.Ctx = Ctx;
102
+ // --- WebSocket Decorators ---
103
+ exports.WS_CONTROLLER_METADATA = 'WS_CONTROLLER_METADATA';
104
+ exports.WS_ON_OPEN_METADATA = 'WS_ON_OPEN_METADATA';
105
+ exports.WS_ON_MESSAGE_METADATA = 'WS_ON_MESSAGE_METADATA';
106
+ exports.WS_ON_CLOSE_METADATA = 'WS_ON_CLOSE_METADATA';
107
+ exports.WS_ON_DRAIN_METADATA = 'WS_ON_DRAIN_METADATA';
108
+ function WebSocketController(path = '/*') {
109
+ return (target) => {
110
+ Reflect.defineMetadata(exports.WS_CONTROLLER_METADATA, path, target);
111
+ // WS Controllers are also injectable
112
+ Reflect.defineMetadata(exports.INJECTABLE_METADATA, true, target);
113
+ };
114
+ }
115
+ function createWsMethodDecorator(metadataKey) {
116
+ return (target, propertyKey) => {
117
+ Reflect.defineMetadata(metadataKey, propertyKey, target.constructor);
118
+ };
119
+ }
120
+ const OnOpen = () => createWsMethodDecorator(exports.WS_ON_OPEN_METADATA);
121
+ exports.OnOpen = OnOpen;
122
+ const OnMessage = () => createWsMethodDecorator(exports.WS_ON_MESSAGE_METADATA);
123
+ exports.OnMessage = OnMessage;
124
+ const OnClose = () => createWsMethodDecorator(exports.WS_ON_CLOSE_METADATA);
125
+ exports.OnClose = OnClose;
126
+ const OnDrain = () => createWsMethodDecorator(exports.WS_ON_DRAIN_METADATA);
127
+ exports.OnDrain = OnDrain;
@@ -0,0 +1,8 @@
1
+ export * from './core/Context';
2
+ export * from './core/WsContext';
3
+ export * from './core/Router';
4
+ export * from './core/Application';
5
+ export * from './core/Container';
6
+ export * from './decorators';
7
+ export * from './validation';
8
+ export type { Middleware, Next } from './core/Context';
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./core/Context"), exports);
18
+ __exportStar(require("./core/WsContext"), exports);
19
+ __exportStar(require("./core/Router"), exports);
20
+ __exportStar(require("./core/Application"), exports);
21
+ __exportStar(require("./core/Container"), exports);
22
+ __exportStar(require("./decorators"), exports);
23
+ __exportStar(require("./validation"), exports);
@@ -0,0 +1,16 @@
1
+ import { BaseContext, Next } from '../core/Context';
2
+ export interface CookieOptions {
3
+ maxAge?: number;
4
+ path?: string;
5
+ domain?: string;
6
+ secure?: boolean;
7
+ httpOnly?: boolean;
8
+ sameSite?: 'Strict' | 'Lax' | 'None';
9
+ }
10
+ declare module '../core/Context' {
11
+ interface BaseContext {
12
+ getCookie(name: string): string | undefined;
13
+ setCookie(name: string, value: string, options?: CookieOptions): void;
14
+ }
15
+ }
16
+ export declare const cookieMiddleware: () => (ctx: BaseContext, next: Next) => Promise<void>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cookieMiddleware = void 0;
4
+ const cookieMiddleware = () => {
5
+ return async (ctx, next) => {
6
+ let parsedCookies;
7
+ ctx.getCookie = (name) => {
8
+ if (!parsedCookies) {
9
+ parsedCookies = {};
10
+ const cookieHeader = ctx.headers['cookie'];
11
+ if (cookieHeader) {
12
+ cookieHeader.split(';').forEach(c => {
13
+ const [k, v] = c.split('=');
14
+ if (k && v) {
15
+ parsedCookies[k.trim()] = decodeURIComponent(v.trim());
16
+ }
17
+ });
18
+ }
19
+ }
20
+ return parsedCookies[name];
21
+ };
22
+ ctx.setCookie = (name, value, options = {}) => {
23
+ let cookieStr = `${name}=${encodeURIComponent(value)}`;
24
+ if (options.maxAge !== undefined)
25
+ cookieStr += `; Max-Age=${options.maxAge}`;
26
+ if (options.path)
27
+ cookieStr += `; Path=${options.path}`;
28
+ if (options.domain)
29
+ cookieStr += `; Domain=${options.domain}`;
30
+ if (options.secure)
31
+ cookieStr += `; Secure`;
32
+ if (options.httpOnly)
33
+ cookieStr += `; HttpOnly`;
34
+ if (options.sameSite)
35
+ cookieStr += `; SameSite=${options.sameSite}`;
36
+ if (!ctx.responseHeaders['Set-Cookie']) {
37
+ ctx.responseHeaders['Set-Cookie'] = [];
38
+ }
39
+ else if (typeof ctx.responseHeaders['Set-Cookie'] === 'string') {
40
+ ctx.responseHeaders['Set-Cookie'] = [ctx.responseHeaders['Set-Cookie']];
41
+ }
42
+ ctx.responseHeaders['Set-Cookie'].push(cookieStr);
43
+ };
44
+ await next();
45
+ };
46
+ };
47
+ exports.cookieMiddleware = cookieMiddleware;
@@ -0,0 +1,20 @@
1
+ import 'reflect-metadata';
2
+ export declare const VALIDATION_METADATA_KEY = "VIO:VALIDATION";
3
+ export interface ValidationRule {
4
+ property: string;
5
+ type: string;
6
+ value?: any;
7
+ message?: string;
8
+ }
9
+ export declare class ValidationError extends Error {
10
+ errors: string[];
11
+ constructor(errors: string[]);
12
+ }
13
+ export declare function IsRequired(message?: string): PropertyDecorator;
14
+ export declare function IsString(message?: string): PropertyDecorator;
15
+ export declare function IsNumber(message?: string): PropertyDecorator;
16
+ export declare function IsInt(message?: string): PropertyDecorator;
17
+ export declare function IsBoolean(message?: string): PropertyDecorator;
18
+ export declare function Min(val: number, message?: string): PropertyDecorator;
19
+ export declare function Max(val: number, message?: string): PropertyDecorator;
20
+ export declare function validateDTO(dtoClass: any, plainObject: any): any;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ValidationError = exports.VALIDATION_METADATA_KEY = void 0;
4
+ exports.IsRequired = IsRequired;
5
+ exports.IsString = IsString;
6
+ exports.IsNumber = IsNumber;
7
+ exports.IsInt = IsInt;
8
+ exports.IsBoolean = IsBoolean;
9
+ exports.Min = Min;
10
+ exports.Max = Max;
11
+ exports.validateDTO = validateDTO;
12
+ require("reflect-metadata");
13
+ exports.VALIDATION_METADATA_KEY = 'VIO:VALIDATION';
14
+ function addValidationRule(target, propertyKey, rule) {
15
+ const rules = Reflect.getMetadata(exports.VALIDATION_METADATA_KEY, target.constructor) || [];
16
+ rules.push({ property: propertyKey, ...rule });
17
+ Reflect.defineMetadata(exports.VALIDATION_METADATA_KEY, rules, target.constructor);
18
+ }
19
+ // Validation Error Exception class
20
+ class ValidationError extends Error {
21
+ errors;
22
+ constructor(errors) {
23
+ super(errors.join(' | ')); // Joined by pipe for error message output
24
+ this.errors = errors;
25
+ this.name = 'ValidationError';
26
+ }
27
+ }
28
+ exports.ValidationError = ValidationError;
29
+ // Built-in Validation Property Decorators
30
+ function IsRequired(message) {
31
+ return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'required', message });
32
+ }
33
+ function IsString(message) {
34
+ return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'string', message });
35
+ }
36
+ function IsNumber(message) {
37
+ return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'number', message });
38
+ }
39
+ function IsInt(message) {
40
+ return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'int', message });
41
+ }
42
+ function IsBoolean(message) {
43
+ return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'boolean', message });
44
+ }
45
+ function Min(val, message) {
46
+ return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'min', value: val, message });
47
+ }
48
+ function Max(val, message) {
49
+ return (target, propertyKey) => addValidationRule(target, propertyKey, { type: 'max', value: val, message });
50
+ }
51
+ // High performance native DTO Validator Engine
52
+ function validateDTO(dtoClass, plainObject) {
53
+ const rules = Reflect.getMetadata(exports.VALIDATION_METADATA_KEY, dtoClass) || [];
54
+ // No decorators, immediately return transparent wrapper
55
+ if (rules.length === 0) {
56
+ if (plainObject && typeof plainObject === 'object') {
57
+ const temp = new dtoClass();
58
+ Object.assign(temp, plainObject);
59
+ return temp;
60
+ }
61
+ return plainObject;
62
+ }
63
+ const errors = [];
64
+ const instance = new dtoClass();
65
+ // Copy arbitrary fields ignoring safety for now to support nested objects
66
+ if (plainObject && typeof plainObject === 'object') {
67
+ Object.assign(instance, plainObject);
68
+ }
69
+ for (const rule of rules) {
70
+ const val = instance[rule.property];
71
+ const isMissing = (val === undefined || val === null || val === '');
72
+ if (rule.type === 'required' && isMissing) {
73
+ errors.push(rule.message || `Parameter [${rule.property}] is required.`);
74
+ continue;
75
+ }
76
+ if (!isMissing) {
77
+ if (rule.type === 'string' && typeof val !== 'string') {
78
+ errors.push(rule.message || `Parameter [${rule.property}] must be a valid string.`);
79
+ }
80
+ else if (rule.type === 'number') {
81
+ const num = Number(val);
82
+ if (isNaN(num))
83
+ errors.push(rule.message || `Parameter [${rule.property}] must be a number.`);
84
+ else
85
+ instance[rule.property] = num;
86
+ }
87
+ else if (rule.type === 'int') {
88
+ const num = Number(val);
89
+ if (isNaN(num) || !Number.isInteger(num))
90
+ errors.push(rule.message || `Parameter [${rule.property}] must be an integer.`);
91
+ else
92
+ instance[rule.property] = num;
93
+ }
94
+ else if (rule.type === 'boolean') {
95
+ if (val !== 'true' && val !== 'false' && val !== true && val !== false && val !== 1 && val !== 0 && val !== '1' && val !== '0') {
96
+ errors.push(rule.message || `Parameter [${rule.property}] must be a boolean.`);
97
+ }
98
+ else
99
+ instance[rule.property] = (val === 'true' || val === '1' || val === true || val === 1);
100
+ }
101
+ else if (rule.type === 'min') {
102
+ if (Number(val) < rule.value)
103
+ errors.push(rule.message || `Parameter [${rule.property}] must be >= ${rule.value}.`);
104
+ }
105
+ else if (rule.type === 'max') {
106
+ if (Number(val) > rule.value)
107
+ errors.push(rule.message || `Parameter [${rule.property}] must be <= ${rule.value}.`);
108
+ }
109
+ }
110
+ }
111
+ if (errors.length > 0) {
112
+ throw new ValidationError(errors);
113
+ }
114
+ return instance; // Cleanly parsed and casted Data Transfer Object (DTO)
115
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "viojs-core",
3
+ "version": "1.0.0",
4
+ "description": "Core functionality for vio framework",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "package.json",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch"
15
+ },
16
+ "dependencies": {
17
+ "mime-types": "^3.0.2",
18
+ "reflect-metadata": "^0.2.1",
19
+ "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.58.0"
20
+ },
21
+ "devDependencies": {
22
+ "@swc/core": "^1.15.11",
23
+ "@types/mime-types": "^3.0.1",
24
+ "@types/node": "^20.11.19",
25
+ "tsup": "^8.5.1",
26
+ "typescript": "^5.3.3"
27
+ },
28
+ "peerDependencies": {
29
+ "reflect-metadata": "^0.2.1"
30
+ }
31
+ }