@vorplex/api 0.0.4 → 0.0.9

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 (35) hide show
  1. package/dist/client/hub-client.mode.d.ts +9 -0
  2. package/dist/client/hub-client.mode.js +51 -0
  3. package/dist/client/index.d.ts +2 -0
  4. package/dist/client/index.js +3 -0
  5. package/dist/client/parse-jwt.function.d.ts +1 -0
  6. package/dist/client/parse-jwt.function.js +8 -0
  7. package/dist/client/tsconfig.build.tsbuildinfo +1 -0
  8. package/dist/server/index.d.ts +12 -0
  9. package/dist/server/index.js +13 -0
  10. package/dist/server/jwt.util.d.ts +8 -0
  11. package/dist/server/jwt.util.js +42 -0
  12. package/dist/server/server/controller/controller.interface.d.ts +15 -0
  13. package/dist/server/server/controller/controller.interface.js +0 -0
  14. package/dist/server/server/controller/handler.interface.d.ts +28 -0
  15. package/dist/server/server/controller/handler.interface.js +0 -0
  16. package/dist/server/server/http/error.model.d.ts +9 -0
  17. package/dist/server/server/http/error.model.js +16 -0
  18. package/dist/server/server/http/reader.util.d.ts +5 -0
  19. package/dist/server/server/http/reader.util.js +37 -0
  20. package/dist/server/server/http/request-method.enum.d.ts +11 -0
  21. package/dist/server/server/http/request-method.enum.js +10 -0
  22. package/dist/server/server/http/responder.util.d.ts +37 -0
  23. package/dist/server/server/http/responder.util.js +66 -0
  24. package/dist/server/server/http/response-codes.enum.d.ts +16 -0
  25. package/dist/server/server/http/response-codes.enum.js +15 -0
  26. package/dist/server/server/http/response.interface.d.ts +23 -0
  27. package/dist/server/server/http/response.interface.js +0 -0
  28. package/dist/server/server/hub/action.interface.d.ts +15 -0
  29. package/dist/server/server/hub/action.interface.js +0 -0
  30. package/dist/server/server/hub/hub.interface.d.ts +5 -0
  31. package/dist/server/server/hub/hub.interface.js +0 -0
  32. package/dist/server/server/server.model.d.ts +78 -0
  33. package/dist/server/server/server.model.js +313 -0
  34. package/dist/server/tsconfig.build.tsbuildinfo +1 -0
  35. package/package.json +2 -2
@@ -0,0 +1,15 @@
1
+ export const HttpResponseCodes = {
2
+ OK: 200,
3
+ NoContent: 204,
4
+ PartialContent: 206,
5
+ Redirect: 302,
6
+ BadRequest: 400,
7
+ Unauthorized: 401,
8
+ Forbidden: 403,
9
+ NotFound: 404,
10
+ MethodNotAllowed: 405,
11
+ UnsupportedMediaType: 415,
12
+ Locked: 423,
13
+ InternalServerError: 500,
14
+ NotImplemented: 501
15
+ };
@@ -0,0 +1,23 @@
1
+ import { MimeType } from '@vorplex/core';
2
+ import { HttpResponseCodes } from './response-codes.enum';
3
+ export interface HttpResponse {
4
+ type?: string;
5
+ code?: HttpResponseCodes;
6
+ status?: string;
7
+ headers?: Record<string, string>;
8
+ }
9
+ export interface HttpJsonResponse<T = any> extends HttpResponse {
10
+ type: 'json';
11
+ value: T;
12
+ }
13
+ export interface HttpFileResponse extends HttpResponse {
14
+ type: 'file';
15
+ file: string;
16
+ mimeType: MimeType;
17
+ }
18
+ export interface HttpRedirectResponse extends HttpResponse {
19
+ type: 'redirect';
20
+ url: string;
21
+ code?: HttpResponseCodes;
22
+ }
23
+ export type HttpResponses = HttpJsonResponse | HttpFileResponse | HttpRedirectResponse;
File without changes
@@ -0,0 +1,15 @@
1
+ import { Awaitable, Injector, Task, TsonObjectDefinition, WebClient } from '@vorplex/core';
2
+ export interface ActionParams<T = any> {
3
+ injector: Injector;
4
+ client: WebClient;
5
+ task: Task;
6
+ packet: {
7
+ id: string;
8
+ data: T;
9
+ };
10
+ }
11
+ export interface Action<T = any> {
12
+ name: string;
13
+ schema?: TsonObjectDefinition<T>;
14
+ callback: (params: ActionParams<T>) => Awaitable<void>;
15
+ }
File without changes
@@ -0,0 +1,5 @@
1
+ import { Action } from './action.interface';
2
+ export interface Hub {
3
+ name: string;
4
+ actions: Action[];
5
+ }
File without changes
@@ -0,0 +1,78 @@
1
+ import { Awaitable, Injector, Logger, Subscribable, WebClient } from '@vorplex/core';
2
+ import { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'http';
3
+ import { Server as NodeHttpsServer } from 'https';
4
+ import { WebSocketServer } from 'ws';
5
+ import { Controller } from './controller/controller.interface';
6
+ import { Handler } from './controller/handler.interface';
7
+ import { Action } from './hub/action.interface';
8
+ import { Hub } from './hub/hub.interface';
9
+ export declare class Api {
10
+ static controller(controller: Controller): Controller;
11
+ static hub(hub: Hub): Hub;
12
+ static action<T>(action: Action<T>): Action<T>;
13
+ static actionResult(result: {
14
+ id: string;
15
+ data: any;
16
+ }): {
17
+ id: string;
18
+ data: any;
19
+ };
20
+ static actionError(result: {
21
+ id: string;
22
+ error: {
23
+ message: string;
24
+ data?: any;
25
+ };
26
+ }): {
27
+ id: string;
28
+ error: {
29
+ message: string;
30
+ data?: any;
31
+ };
32
+ };
33
+ static handler<TRoute extends string, TParameters extends string[]>(handler: Handler<TRoute, TParameters>): Handler<TRoute, TParameters>;
34
+ }
35
+ export interface HttpServerOptions {
36
+ port: number;
37
+ injector?: Injector;
38
+ hostname?: string;
39
+ logger?: Logger;
40
+ certificate?: {
41
+ crt: string;
42
+ key: string;
43
+ };
44
+ }
45
+ export declare class Server {
46
+ private _disposables;
47
+ private _clients;
48
+ private readonly _connection;
49
+ private readonly _disconnection;
50
+ private readonly _requests;
51
+ private readonly _packets;
52
+ injector: Injector;
53
+ options: HttpServerOptions;
54
+ httpServer: NodeHttpServer | NodeHttpsServer;
55
+ webServer: WebSocketServer;
56
+ controllers: Controller[];
57
+ hubs: Hub[];
58
+ get connection(): Subscribable<{
59
+ client: WebClient;
60
+ request: IncomingMessage;
61
+ }>;
62
+ get disconnection(): Subscribable<WebClient>;
63
+ get clients(): ReadonlyArray<WebClient>;
64
+ get requests(): Subscribable<{
65
+ request: IncomingMessage;
66
+ response: ServerResponse;
67
+ }>;
68
+ get packets(): Subscribable<{
69
+ client: WebClient;
70
+ packet: any;
71
+ }>;
72
+ constructor(options: HttpServerOptions);
73
+ route(routes: string | string[], callback: (request: IncomingMessage, response: ServerResponse) => Awaitable<void>): void;
74
+ start(): void;
75
+ private initHttpServer;
76
+ private initWebServer;
77
+ stop(): void;
78
+ }
@@ -0,0 +1,313 @@
1
+ import { $Array, $Router, $String, $Tson, Emitter, Injector, MimeType, Task, TsonError, WebClient } from '@vorplex/core';
2
+ import * as fs from 'fs';
3
+ import { Server as NodeHttpServer } from 'http';
4
+ import { Server as NodeHttpsServer } from 'https';
5
+ import { WebSocketServer } from 'ws';
6
+ import { HttpError, WebError } from './http/error.model';
7
+ import { $HttpResponder } from './http/responder.util';
8
+ import { HttpResponseCodes } from './http/response-codes.enum';
9
+ export class Api {
10
+ static controller(controller) {
11
+ return controller;
12
+ }
13
+ static hub(hub) {
14
+ return hub;
15
+ }
16
+ static action(action) {
17
+ return action;
18
+ }
19
+ static actionResult(result) {
20
+ return result;
21
+ }
22
+ static actionError(result) {
23
+ return result;
24
+ }
25
+ static handler(handler) {
26
+ return handler;
27
+ }
28
+ }
29
+ export class Server {
30
+ _disposables = [];
31
+ _clients = [];
32
+ _connection = new Emitter();
33
+ _disconnection = new Emitter();
34
+ _requests = new Emitter();
35
+ _packets = new Emitter();
36
+ injector;
37
+ options;
38
+ httpServer;
39
+ webServer;
40
+ controllers = [];
41
+ hubs = [];
42
+ get connection() { return this._connection; }
43
+ get disconnection() { return this._disconnection; }
44
+ get clients() { return this._clients; }
45
+ get requests() { return this._requests; }
46
+ get packets() { return this._packets; }
47
+ constructor(options) {
48
+ this.options = options;
49
+ this.injector = options.injector ?? new Injector();
50
+ this.injector.addInstance(this);
51
+ }
52
+ route(routes, callback) {
53
+ routes = Array.isArray(routes) ? routes : [routes];
54
+ this.requests.subscribe(async ({ request, response }) => {
55
+ for (const route of routes) {
56
+ const match = $Router.match(route, request.url);
57
+ if (match) {
58
+ await callback(request, response);
59
+ }
60
+ }
61
+ });
62
+ }
63
+ start() {
64
+ this.options.logger?.log('Starting Server');
65
+ this.stop();
66
+ this.initHttpServer();
67
+ this.initWebServer();
68
+ }
69
+ initHttpServer() {
70
+ const listener = (request, response) => {
71
+ response.setHeader('Access-Control-Allow-Origin', '*');
72
+ response.setHeader('Access-Control-Allow-Methods', '*');
73
+ response.setHeader('Access-Control-Allow-Headers', '*');
74
+ if (request.method === 'OPTIONS') {
75
+ response.end();
76
+ return;
77
+ }
78
+ this._requests.emit({ request, response });
79
+ };
80
+ const subscription = this.requests.subscribe(async ({ request, response }) => {
81
+ const task = new Task(`Request: ${request.method} ${request.url}`);
82
+ try {
83
+ task.log('Routing request');
84
+ for (const controller of this.controllers) {
85
+ for (const handler of controller.handlers) {
86
+ if (handler.method === request.method) {
87
+ const match = $Router.match(`${controller.route}${handler.route}`, request.url);
88
+ if (match) {
89
+ const query = $Router.getQueryParameters(request.url);
90
+ task.log(`Forwarding request to controller (${controller.route}) handler (${handler.route})`);
91
+ for (const guard of controller.guards ?? []) {
92
+ const authorized = await guard({
93
+ injector: this.injector,
94
+ server: this,
95
+ request,
96
+ response
97
+ });
98
+ if (!authorized)
99
+ throw new HttpError(HttpResponseCodes.Unauthorized);
100
+ }
101
+ task.log(`${request.method} ${request.url}`, {
102
+ attachments: {
103
+ parameters: { type: 'json', value: JSON.stringify(query, null, 4) },
104
+ headers: { type: 'json', value: JSON.stringify(request.headers, null, 4) }
105
+ }
106
+ });
107
+ for (const parameter of handler.parameters ?? []) {
108
+ if (!parameter.endsWith('?') && $String.isNullOrEmpty(query[parameter]))
109
+ throw new HttpError(HttpResponseCodes.BadRequest, `Missing required query parameter (${parameter})`);
110
+ }
111
+ const result = await handler.callback({
112
+ injector: this.injector,
113
+ server: this,
114
+ task,
115
+ parameters: {
116
+ route: match,
117
+ query
118
+ },
119
+ request,
120
+ response
121
+ });
122
+ if (result) {
123
+ if (response.writable) {
124
+ if (result.code)
125
+ response.statusCode = result.code;
126
+ if (result.status)
127
+ response.statusMessage = result.status;
128
+ if (result.headers)
129
+ response.setHeaders(new Map(Object.entries(result.headers)));
130
+ switch (result.type) {
131
+ case 'json':
132
+ response
133
+ .setHeader('Content-Type', MimeType.json)
134
+ .write(result.value === undefined ? undefined : JSON.stringify(result.value));
135
+ break;
136
+ case 'file':
137
+ await new Promise((resolve, reject) => {
138
+ response.setHeader('Content-Type', result.mimeType);
139
+ response.once('finish', () => resolve());
140
+ response.once('error', (error) => reject(error));
141
+ fs.createReadStream(result.file).pipe(response, { end: true });
142
+ });
143
+ break;
144
+ case 'redirect':
145
+ response.statusCode = 302;
146
+ response.setHeader('Location', result.url);
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ if (!response.writableEnded)
152
+ response.end();
153
+ return;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ task.fail(`No route matches URL`);
159
+ if (response.writable) {
160
+ response.writeHead(HttpResponseCodes.BadRequest, 'Route Not Found');
161
+ response.end();
162
+ }
163
+ }
164
+ catch (error) {
165
+ const message = error instanceof Error ? error.stack : String(error);
166
+ task.fail(message);
167
+ if (error instanceof HttpError) {
168
+ response.statusCode = error.code;
169
+ if (error.message)
170
+ response.statusMessage = error.message;
171
+ response.end();
172
+ }
173
+ else
174
+ $HttpResponder.internalServerError(response);
175
+ }
176
+ finally {
177
+ task.complete();
178
+ this.options.logger?.log(task.toConsoleLog());
179
+ }
180
+ });
181
+ this.httpServer = this.options.certificate ? new NodeHttpsServer({ key: this.options.certificate.key, cert: this.options.certificate.crt, rejectUnauthorized: false }, listener) : new NodeHttpServer(listener);
182
+ this.httpServer.listen(this.options.port, this.options.hostname);
183
+ this.options.logger?.log(`HTTP server started listening on ${this.httpServer instanceof NodeHttpServer ? 'http' : 'https'}://${this.options.hostname ?? '0.0.0.0'}:${this.options.port}`);
184
+ this._disposables.push(() => {
185
+ subscription.unsubscribe();
186
+ this.httpServer.close();
187
+ });
188
+ }
189
+ initWebServer() {
190
+ this.webServer = new WebSocketServer({ server: this.httpServer });
191
+ this.options.logger?.log(`WS server started listening on ${this.httpServer instanceof NodeHttpServer ? 'ws' : 'wss'}://${this.options.hostname ?? '0.0.0.0'}:${this.options.port}`);
192
+ this.webServer.on('connection', (ws, request) => {
193
+ const forwarded = request.headers['x-forwarded-for'];
194
+ const client = new WebClient(ws, forwarded ? forwarded.split(',')[0] : request.socket.remoteAddress.replace('::ffff:', ''));
195
+ this._clients.push(client);
196
+ this._connection.emit({ client, request });
197
+ ws.on('message', packet => {
198
+ try {
199
+ const json = packet.toString();
200
+ packet = JSON.parse(json);
201
+ this._packets.emit({ client, packet });
202
+ }
203
+ catch (error) { }
204
+ });
205
+ ws.on('close', () => {
206
+ this._clients = $Array.remove(this._clients, client);
207
+ this._disconnection.emit(client);
208
+ });
209
+ });
210
+ const subscription = this.packets.subscribe(async ({ client, packet }) => {
211
+ const definition = $Tson.object({
212
+ properties: {
213
+ id: $Tson.string(),
214
+ hub: $Tson.string(),
215
+ action: $Tson.string(),
216
+ data: $Tson.any({ default: null })
217
+ }
218
+ });
219
+ const schema = $Tson.parse(definition);
220
+ let parsedPacket;
221
+ try {
222
+ parsedPacket = schema.parse(packet);
223
+ }
224
+ catch (error) {
225
+ if (error instanceof TsonError) {
226
+ client.send({
227
+ error: {
228
+ message: error.message,
229
+ path: error.path,
230
+ schema: error.schema
231
+ },
232
+ schema
233
+ });
234
+ return;
235
+ }
236
+ throw error;
237
+ }
238
+ const task = new Task(`[${parsedPacket.id}] Packet`);
239
+ try {
240
+ for (const hub of this.hubs) {
241
+ if (hub.name === parsedPacket.hub) {
242
+ for (const action of hub.actions) {
243
+ if (action.name === parsedPacket.action) {
244
+ task.log(`Forwarding packet to hub (${hub.name}) with action (${action.name})`, {
245
+ attachments: { packet: { type: 'json', value: JSON.stringify(packet, null, 4) } }
246
+ });
247
+ let data = parsedPacket.data;
248
+ if (action.schema) {
249
+ try {
250
+ data = $Tson.parse(action.schema).parse(parsedPacket.data);
251
+ }
252
+ catch (error) {
253
+ if (error instanceof TsonError) {
254
+ throw new WebError(`Failed to parse packet data for hub (${hub.name}) with action (${action.name})`, {
255
+ message: error.message,
256
+ path: error.path,
257
+ schema: error.schema,
258
+ value: error.value
259
+ });
260
+ }
261
+ else
262
+ throw error;
263
+ }
264
+ }
265
+ await action.callback({
266
+ injector: this.injector,
267
+ client,
268
+ task,
269
+ packet: {
270
+ id: parsedPacket.id,
271
+ data
272
+ }
273
+ });
274
+ return;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ throw new WebError(`No hub (${parsedPacket.hub}) was found with action (${parsedPacket.action})`);
280
+ }
281
+ catch (error) {
282
+ task.fail(error);
283
+ if (error instanceof WebError) {
284
+ client.send(Api.actionError({
285
+ id: parsedPacket.id,
286
+ error: {
287
+ message: error.message,
288
+ data: error.data
289
+ }
290
+ }));
291
+ }
292
+ throw error;
293
+ }
294
+ finally {
295
+ task.complete();
296
+ this.options.logger?.log(task.toConsoleLog());
297
+ }
298
+ ;
299
+ });
300
+ this._disposables.push(() => {
301
+ subscription.unsubscribe();
302
+ this.webServer.close();
303
+ });
304
+ }
305
+ stop() {
306
+ for (const dispose of this._disposables) {
307
+ dispose();
308
+ }
309
+ this._disposables = [];
310
+ this.httpServer = null;
311
+ this.webServer = null;
312
+ }
313
+ }