@noxfly/noxus 1.1.9 → 1.1.10

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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "typescript.tsdk": "node_modules\\typescript\\lib"
3
+ }
package/dist/noxus.d.mts CHANGED
@@ -86,7 +86,11 @@ interface IRouteMetadata {
86
86
  /**
87
87
  * The different HTTP methods that can be used in the application.
88
88
  */
89
- type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
89
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'BATCH';
90
+ /**
91
+ * Atomic HTTP verbs supported by controllers. BATCH is handled at the router level only.
92
+ */
93
+ type AtomicHttpMethod = Exclude<HttpMethod, 'BATCH'>;
90
94
  /**
91
95
  * Gets the route metadata for a given target class.
92
96
  * This metadata includes the HTTP method, path, handler, and guards defined by the route decorators.
@@ -152,24 +156,36 @@ declare class Request {
152
156
  * It includes properties for the sender ID, request ID, path, method, and an optional body.
153
157
  * This interface is used to standardize the request data across the application.
154
158
  */
155
- interface IRequest<T = any> {
159
+ interface IRequest<TBody = unknown> {
156
160
  senderId: number;
157
161
  requestId: string;
158
162
  path: string;
159
163
  method: HttpMethod;
160
- body?: T;
164
+ body?: TBody;
165
+ }
166
+ interface IBatchRequestItem<TBody = unknown> {
167
+ requestId?: string;
168
+ path: string;
169
+ method: AtomicHttpMethod;
170
+ body?: TBody;
171
+ }
172
+ interface IBatchRequestPayload {
173
+ requests: IBatchRequestItem[];
161
174
  }
162
175
  /**
163
176
  * Creates a Request object from the IPC event data.
164
177
  * This function extracts the necessary information from the IPC event and constructs a Request instance.
165
178
  */
166
- interface IResponse<T = any> {
179
+ interface IResponse<TBody = unknown> {
167
180
  requestId: string;
168
181
  status: number;
169
- body?: T;
182
+ body?: TBody;
170
183
  error?: string;
171
184
  stack?: string;
172
185
  }
186
+ interface IBatchResponsePayload {
187
+ responses: IResponse[];
188
+ }
173
189
 
174
190
 
175
191
  /**
@@ -291,6 +307,10 @@ declare class Router {
291
307
  * @param channelSenderId - The ID of the sender channel to shut down.
292
308
  */
293
309
  handle(request: Request): Promise<IResponse>;
310
+ private handleAtomic;
311
+ private handleBatch;
312
+ private normalizeBatchPayload;
313
+ private normalizeBatchItem;
294
314
  /**
295
315
  * Finds the route definition for a given request.
296
316
  * This method searches the routing tree for a matching route based on the request's path and method.
@@ -650,4 +670,4 @@ declare namespace Logger {
650
670
  };
651
671
  }
652
672
 
653
- export { AppInjector, Authorize, BadGatewayException, BadRequestException, CONTROLLER_METADATA_KEY, ConflictException, Controller, type ControllerAction, Delete, ForbiddenException, GatewayTimeoutException, Get, type HttpMethod, HttpVersionNotSupportedException, type IApp, type IBinding, type IControllerMetadata, type IGuard, type IMiddleware, type IModuleMetadata, INJECTABLE_METADATA_KEY, type IRequest, type IResponse, type IRouteDefinition, type IRouteMetadata, Injectable, InsufficientStorageException, InternalServerException, type Lifetime, type LogLevel, Logger, LoopDetectedException, MODULE_METADATA_KEY, type MaybeAsync, MethodNotAllowedException, Module, NetworkAuthenticationRequiredException, NetworkConnectTimeoutException, type NextFunction, NotAcceptableException, NotExtendedException, NotFoundException, NotImplementedException, NoxApp, Patch, PaymentRequiredException, Post, Put, ROUTE_METADATA_KEY, Request, RequestTimeoutException, ResponseException, RootInjector, Router, ServiceUnavailableException, TooManyRequestsException, type Type, UnauthorizedException, UpgradeRequiredException, UseMiddlewares, VariantAlsoNegotiatesException, bootstrapApplication, getControllerMetadata, getGuardForController, getGuardForControllerAction, getInjectableMetadata, getMiddlewaresForController, getMiddlewaresForControllerAction, getModuleMetadata, getRouteMetadata, inject };
673
+ export { AppInjector, type AtomicHttpMethod, Authorize, BadGatewayException, BadRequestException, CONTROLLER_METADATA_KEY, ConflictException, Controller, type ControllerAction, Delete, ForbiddenException, GatewayTimeoutException, Get, type HttpMethod, HttpVersionNotSupportedException, type IApp, type IBatchRequestItem, type IBatchRequestPayload, type IBatchResponsePayload, type IBinding, type IControllerMetadata, type IGuard, type IMiddleware, type IModuleMetadata, INJECTABLE_METADATA_KEY, type IRequest, type IResponse, type IRouteDefinition, type IRouteMetadata, Injectable, InsufficientStorageException, InternalServerException, type Lifetime, type LogLevel, Logger, LoopDetectedException, MODULE_METADATA_KEY, type MaybeAsync, MethodNotAllowedException, Module, NetworkAuthenticationRequiredException, NetworkConnectTimeoutException, type NextFunction, NotAcceptableException, NotExtendedException, NotFoundException, NotImplementedException, NoxApp, Patch, PaymentRequiredException, Post, Put, ROUTE_METADATA_KEY, Request, RequestTimeoutException, ResponseException, RootInjector, Router, ServiceUnavailableException, TooManyRequestsException, type Type, UnauthorizedException, UpgradeRequiredException, UseMiddlewares, VariantAlsoNegotiatesException, bootstrapApplication, getControllerMetadata, getGuardForController, getGuardForControllerAction, getInjectableMetadata, getMiddlewaresForController, getMiddlewaresForControllerAction, getModuleMetadata, getRouteMetadata, inject };
package/dist/noxus.d.ts CHANGED
@@ -86,7 +86,11 @@ interface IRouteMetadata {
86
86
  /**
87
87
  * The different HTTP methods that can be used in the application.
88
88
  */
89
- type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
89
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'BATCH';
90
+ /**
91
+ * Atomic HTTP verbs supported by controllers. BATCH is handled at the router level only.
92
+ */
93
+ type AtomicHttpMethod = Exclude<HttpMethod, 'BATCH'>;
90
94
  /**
91
95
  * Gets the route metadata for a given target class.
92
96
  * This metadata includes the HTTP method, path, handler, and guards defined by the route decorators.
@@ -152,24 +156,36 @@ declare class Request {
152
156
  * It includes properties for the sender ID, request ID, path, method, and an optional body.
153
157
  * This interface is used to standardize the request data across the application.
154
158
  */
155
- interface IRequest<T = any> {
159
+ interface IRequest<TBody = unknown> {
156
160
  senderId: number;
157
161
  requestId: string;
158
162
  path: string;
159
163
  method: HttpMethod;
160
- body?: T;
164
+ body?: TBody;
165
+ }
166
+ interface IBatchRequestItem<TBody = unknown> {
167
+ requestId?: string;
168
+ path: string;
169
+ method: AtomicHttpMethod;
170
+ body?: TBody;
171
+ }
172
+ interface IBatchRequestPayload {
173
+ requests: IBatchRequestItem[];
161
174
  }
162
175
  /**
163
176
  * Creates a Request object from the IPC event data.
164
177
  * This function extracts the necessary information from the IPC event and constructs a Request instance.
165
178
  */
166
- interface IResponse<T = any> {
179
+ interface IResponse<TBody = unknown> {
167
180
  requestId: string;
168
181
  status: number;
169
- body?: T;
182
+ body?: TBody;
170
183
  error?: string;
171
184
  stack?: string;
172
185
  }
186
+ interface IBatchResponsePayload {
187
+ responses: IResponse[];
188
+ }
173
189
 
174
190
 
175
191
  /**
@@ -291,6 +307,10 @@ declare class Router {
291
307
  * @param channelSenderId - The ID of the sender channel to shut down.
292
308
  */
293
309
  handle(request: Request): Promise<IResponse>;
310
+ private handleAtomic;
311
+ private handleBatch;
312
+ private normalizeBatchPayload;
313
+ private normalizeBatchItem;
294
314
  /**
295
315
  * Finds the route definition for a given request.
296
316
  * This method searches the routing tree for a matching route based on the request's path and method.
@@ -650,4 +670,4 @@ declare namespace Logger {
650
670
  };
651
671
  }
652
672
 
653
- export { AppInjector, Authorize, BadGatewayException, BadRequestException, CONTROLLER_METADATA_KEY, ConflictException, Controller, type ControllerAction, Delete, ForbiddenException, GatewayTimeoutException, Get, type HttpMethod, HttpVersionNotSupportedException, type IApp, type IBinding, type IControllerMetadata, type IGuard, type IMiddleware, type IModuleMetadata, INJECTABLE_METADATA_KEY, type IRequest, type IResponse, type IRouteDefinition, type IRouteMetadata, Injectable, InsufficientStorageException, InternalServerException, type Lifetime, type LogLevel, Logger, LoopDetectedException, MODULE_METADATA_KEY, type MaybeAsync, MethodNotAllowedException, Module, NetworkAuthenticationRequiredException, NetworkConnectTimeoutException, type NextFunction, NotAcceptableException, NotExtendedException, NotFoundException, NotImplementedException, NoxApp, Patch, PaymentRequiredException, Post, Put, ROUTE_METADATA_KEY, Request, RequestTimeoutException, ResponseException, RootInjector, Router, ServiceUnavailableException, TooManyRequestsException, type Type, UnauthorizedException, UpgradeRequiredException, UseMiddlewares, VariantAlsoNegotiatesException, bootstrapApplication, getControllerMetadata, getGuardForController, getGuardForControllerAction, getInjectableMetadata, getMiddlewaresForController, getMiddlewaresForControllerAction, getModuleMetadata, getRouteMetadata, inject };
673
+ export { AppInjector, type AtomicHttpMethod, Authorize, BadGatewayException, BadRequestException, CONTROLLER_METADATA_KEY, ConflictException, Controller, type ControllerAction, Delete, ForbiddenException, GatewayTimeoutException, Get, type HttpMethod, HttpVersionNotSupportedException, type IApp, type IBatchRequestItem, type IBatchRequestPayload, type IBatchResponsePayload, type IBinding, type IControllerMetadata, type IGuard, type IMiddleware, type IModuleMetadata, INJECTABLE_METADATA_KEY, type IRequest, type IResponse, type IRouteDefinition, type IRouteMetadata, Injectable, InsufficientStorageException, InternalServerException, type Lifetime, type LogLevel, Logger, LoopDetectedException, MODULE_METADATA_KEY, type MaybeAsync, MethodNotAllowedException, Module, NetworkAuthenticationRequiredException, NetworkConnectTimeoutException, type NextFunction, NotAcceptableException, NotExtendedException, NotFoundException, NotImplementedException, NoxApp, Patch, PaymentRequiredException, Post, Put, ROUTE_METADATA_KEY, Request, RequestTimeoutException, ResponseException, RootInjector, Router, ServiceUnavailableException, TooManyRequestsException, type Type, UnauthorizedException, UpgradeRequiredException, UseMiddlewares, VariantAlsoNegotiatesException, bootstrapApplication, getControllerMetadata, getGuardForController, getGuardForControllerAction, getInjectableMetadata, getMiddlewaresForController, getMiddlewaresForControllerAction, getModuleMetadata, getRouteMetadata, inject };
package/dist/noxus.js CHANGED
@@ -359,7 +359,7 @@ __name(inject, "inject");
359
359
  var RootInjector = new AppInjector("root");
360
360
 
361
361
  // src/router.ts
362
- var import_reflect_metadata2 = require("reflect-metadata");
362
+ var import_reflect_metadata3 = require("reflect-metadata");
363
363
 
364
364
  // src/decorators/guards.decorator.ts
365
365
  function Authorize(...guardClasses) {
@@ -699,6 +699,28 @@ function getMiddlewaresForControllerAction(controllerName, actionName) {
699
699
  __name(getMiddlewaresForControllerAction, "getMiddlewaresForControllerAction");
700
700
  var middlewares = /* @__PURE__ */ new Map();
701
701
 
702
+ // src/request.ts
703
+ var import_reflect_metadata2 = require("reflect-metadata");
704
+ var _Request = class _Request {
705
+ constructor(event, id, method, path, body) {
706
+ __publicField(this, "event");
707
+ __publicField(this, "id");
708
+ __publicField(this, "method");
709
+ __publicField(this, "path");
710
+ __publicField(this, "body");
711
+ __publicField(this, "context", RootInjector.createScope());
712
+ __publicField(this, "params", {});
713
+ this.event = event;
714
+ this.id = id;
715
+ this.method = method;
716
+ this.path = path;
717
+ this.body = body;
718
+ this.path = path.replace(/^\/|\/$/g, "");
719
+ }
720
+ };
721
+ __name(_Request, "Request");
722
+ var Request = _Request;
723
+
702
724
  // src/utils/radix-tree.ts
703
725
  var _a;
704
726
  var RadixNode = (_a = class {
@@ -864,6 +886,17 @@ function _ts_decorate(decorators, target, key, desc) {
864
886
  return c > 3 && r && Object.defineProperty(target, key, r), r;
865
887
  }
866
888
  __name(_ts_decorate, "_ts_decorate");
889
+ var ATOMIC_HTTP_METHODS = /* @__PURE__ */ new Set([
890
+ "GET",
891
+ "POST",
892
+ "PUT",
893
+ "PATCH",
894
+ "DELETE"
895
+ ]);
896
+ function isAtomicHttpMethod(method) {
897
+ return typeof method === "string" && ATOMIC_HTTP_METHODS.has(method);
898
+ }
899
+ __name(isAtomicHttpMethod, "isAtomicHttpMethod");
867
900
  var _Router = class _Router {
868
901
  constructor() {
869
902
  __publicField(this, "routes", new RadixTree());
@@ -932,6 +965,12 @@ var _Router = class _Router {
932
965
  * @param channelSenderId - The ID of the sender channel to shut down.
933
966
  */
934
967
  async handle(request) {
968
+ if (request.method === "BATCH") {
969
+ return this.handleBatch(request);
970
+ }
971
+ return this.handleAtomic(request);
972
+ }
973
+ async handleAtomic(request) {
935
974
  Logger.comment(`> ${request.method} /${request.path}`);
936
975
  const t0 = performance.now();
937
976
  const response = {
@@ -975,6 +1014,94 @@ var _Router = class _Router {
975
1014
  return response;
976
1015
  }
977
1016
  }
1017
+ async handleBatch(request) {
1018
+ Logger.comment(`> ${request.method} /${request.path}`);
1019
+ const t0 = performance.now();
1020
+ const response = {
1021
+ requestId: request.id,
1022
+ status: 200,
1023
+ body: {
1024
+ responses: []
1025
+ }
1026
+ };
1027
+ try {
1028
+ const payload = this.normalizeBatchPayload(request.body);
1029
+ const batchResponses = [];
1030
+ for (const [index, item] of payload.requests.entries()) {
1031
+ const subRequestId = item.requestId ?? `${request.id}:${index}`;
1032
+ const atomicRequest = new Request(request.event, subRequestId, item.method, item.path, item.body);
1033
+ batchResponses.push(await this.handleAtomic(atomicRequest));
1034
+ }
1035
+ response.body.responses = batchResponses;
1036
+ } catch (error) {
1037
+ response.body = void 0;
1038
+ if (error instanceof ResponseException) {
1039
+ response.status = error.status;
1040
+ response.error = error.message;
1041
+ response.stack = error.stack;
1042
+ } else if (error instanceof Error) {
1043
+ response.status = 500;
1044
+ response.error = error.message || "Internal Server Error";
1045
+ response.stack = error.stack || "No stack trace available";
1046
+ } else {
1047
+ response.status = 500;
1048
+ response.error = "Unknown error occurred";
1049
+ response.stack = "No stack trace available";
1050
+ }
1051
+ } finally {
1052
+ const t1 = performance.now();
1053
+ const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
1054
+ if (response.status < 400) Logger.log(message);
1055
+ else if (response.status < 500) Logger.warn(message);
1056
+ else Logger.error(message);
1057
+ if (response.error !== void 0) {
1058
+ Logger.error(response.error);
1059
+ if (response.stack !== void 0) {
1060
+ Logger.errorStack(response.stack);
1061
+ }
1062
+ }
1063
+ return response;
1064
+ }
1065
+ }
1066
+ normalizeBatchPayload(body) {
1067
+ if (body === null || typeof body !== "object") {
1068
+ throw new BadRequestException("Batch payload must be an object containing a requests array.");
1069
+ }
1070
+ const possiblePayload = body;
1071
+ const { requests } = possiblePayload;
1072
+ if (!Array.isArray(requests)) {
1073
+ throw new BadRequestException("Batch payload must define a requests array.");
1074
+ }
1075
+ const normalizedRequests = requests.map((entry, index) => this.normalizeBatchItem(entry, index));
1076
+ return {
1077
+ requests: normalizedRequests
1078
+ };
1079
+ }
1080
+ normalizeBatchItem(entry, index) {
1081
+ if (entry === null || typeof entry !== "object") {
1082
+ throw new BadRequestException(`Batch request at index ${index} must be an object.`);
1083
+ }
1084
+ const { requestId, path, method, body } = entry;
1085
+ if (requestId !== void 0 && typeof requestId !== "string") {
1086
+ throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
1087
+ }
1088
+ if (typeof path !== "string" || path.length === 0) {
1089
+ throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
1090
+ }
1091
+ if (typeof method !== "string") {
1092
+ throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
1093
+ }
1094
+ const normalizedMethod = method.toUpperCase();
1095
+ if (!isAtomicHttpMethod(normalizedMethod)) {
1096
+ throw new BadRequestException(`Batch request at index ${index} uses the unsupported method ${method}.`);
1097
+ }
1098
+ return {
1099
+ requestId,
1100
+ path,
1101
+ method: normalizedMethod,
1102
+ body
1103
+ };
1104
+ }
978
1105
  /**
979
1106
  * Finds the route definition for a given request.
980
1107
  * This method searches the routing tree for a matching route based on the request's path and method.
@@ -1111,30 +1238,6 @@ Router = _ts_decorate([
1111
1238
 
1112
1239
  // src/app.ts
1113
1240
  var import_main = require("electron/main");
1114
-
1115
- // src/request.ts
1116
- var import_reflect_metadata3 = require("reflect-metadata");
1117
- var _Request = class _Request {
1118
- constructor(event, id, method, path, body) {
1119
- __publicField(this, "event");
1120
- __publicField(this, "id");
1121
- __publicField(this, "method");
1122
- __publicField(this, "path");
1123
- __publicField(this, "body");
1124
- __publicField(this, "context", RootInjector.createScope());
1125
- __publicField(this, "params", {});
1126
- this.event = event;
1127
- this.id = id;
1128
- this.method = method;
1129
- this.path = path;
1130
- this.body = body;
1131
- this.path = path.replace(/^\/|\/$/g, "");
1132
- }
1133
- };
1134
- __name(_Request, "Request");
1135
- var Request = _Request;
1136
-
1137
- // src/app.ts
1138
1241
  function _ts_decorate2(decorators, target, key, desc) {
1139
1242
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1140
1243
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
package/dist/noxus.mjs CHANGED
@@ -622,6 +622,28 @@ function getMiddlewaresForControllerAction(controllerName, actionName) {
622
622
  __name(getMiddlewaresForControllerAction, "getMiddlewaresForControllerAction");
623
623
  var middlewares = /* @__PURE__ */ new Map();
624
624
 
625
+ // src/request.ts
626
+ import "reflect-metadata";
627
+ var _Request = class _Request {
628
+ constructor(event, id, method, path, body) {
629
+ __publicField(this, "event");
630
+ __publicField(this, "id");
631
+ __publicField(this, "method");
632
+ __publicField(this, "path");
633
+ __publicField(this, "body");
634
+ __publicField(this, "context", RootInjector.createScope());
635
+ __publicField(this, "params", {});
636
+ this.event = event;
637
+ this.id = id;
638
+ this.method = method;
639
+ this.path = path;
640
+ this.body = body;
641
+ this.path = path.replace(/^\/|\/$/g, "");
642
+ }
643
+ };
644
+ __name(_Request, "Request");
645
+ var Request = _Request;
646
+
625
647
  // src/utils/radix-tree.ts
626
648
  var _a;
627
649
  var RadixNode = (_a = class {
@@ -787,6 +809,17 @@ function _ts_decorate(decorators, target, key, desc) {
787
809
  return c > 3 && r && Object.defineProperty(target, key, r), r;
788
810
  }
789
811
  __name(_ts_decorate, "_ts_decorate");
812
+ var ATOMIC_HTTP_METHODS = /* @__PURE__ */ new Set([
813
+ "GET",
814
+ "POST",
815
+ "PUT",
816
+ "PATCH",
817
+ "DELETE"
818
+ ]);
819
+ function isAtomicHttpMethod(method) {
820
+ return typeof method === "string" && ATOMIC_HTTP_METHODS.has(method);
821
+ }
822
+ __name(isAtomicHttpMethod, "isAtomicHttpMethod");
790
823
  var _Router = class _Router {
791
824
  constructor() {
792
825
  __publicField(this, "routes", new RadixTree());
@@ -855,6 +888,12 @@ var _Router = class _Router {
855
888
  * @param channelSenderId - The ID of the sender channel to shut down.
856
889
  */
857
890
  async handle(request) {
891
+ if (request.method === "BATCH") {
892
+ return this.handleBatch(request);
893
+ }
894
+ return this.handleAtomic(request);
895
+ }
896
+ async handleAtomic(request) {
858
897
  Logger.comment(`> ${request.method} /${request.path}`);
859
898
  const t0 = performance.now();
860
899
  const response = {
@@ -898,6 +937,94 @@ var _Router = class _Router {
898
937
  return response;
899
938
  }
900
939
  }
940
+ async handleBatch(request) {
941
+ Logger.comment(`> ${request.method} /${request.path}`);
942
+ const t0 = performance.now();
943
+ const response = {
944
+ requestId: request.id,
945
+ status: 200,
946
+ body: {
947
+ responses: []
948
+ }
949
+ };
950
+ try {
951
+ const payload = this.normalizeBatchPayload(request.body);
952
+ const batchResponses = [];
953
+ for (const [index, item] of payload.requests.entries()) {
954
+ const subRequestId = item.requestId ?? `${request.id}:${index}`;
955
+ const atomicRequest = new Request(request.event, subRequestId, item.method, item.path, item.body);
956
+ batchResponses.push(await this.handleAtomic(atomicRequest));
957
+ }
958
+ response.body.responses = batchResponses;
959
+ } catch (error) {
960
+ response.body = void 0;
961
+ if (error instanceof ResponseException) {
962
+ response.status = error.status;
963
+ response.error = error.message;
964
+ response.stack = error.stack;
965
+ } else if (error instanceof Error) {
966
+ response.status = 500;
967
+ response.error = error.message || "Internal Server Error";
968
+ response.stack = error.stack || "No stack trace available";
969
+ } else {
970
+ response.status = 500;
971
+ response.error = "Unknown error occurred";
972
+ response.stack = "No stack trace available";
973
+ }
974
+ } finally {
975
+ const t1 = performance.now();
976
+ const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
977
+ if (response.status < 400) Logger.log(message);
978
+ else if (response.status < 500) Logger.warn(message);
979
+ else Logger.error(message);
980
+ if (response.error !== void 0) {
981
+ Logger.error(response.error);
982
+ if (response.stack !== void 0) {
983
+ Logger.errorStack(response.stack);
984
+ }
985
+ }
986
+ return response;
987
+ }
988
+ }
989
+ normalizeBatchPayload(body) {
990
+ if (body === null || typeof body !== "object") {
991
+ throw new BadRequestException("Batch payload must be an object containing a requests array.");
992
+ }
993
+ const possiblePayload = body;
994
+ const { requests } = possiblePayload;
995
+ if (!Array.isArray(requests)) {
996
+ throw new BadRequestException("Batch payload must define a requests array.");
997
+ }
998
+ const normalizedRequests = requests.map((entry, index) => this.normalizeBatchItem(entry, index));
999
+ return {
1000
+ requests: normalizedRequests
1001
+ };
1002
+ }
1003
+ normalizeBatchItem(entry, index) {
1004
+ if (entry === null || typeof entry !== "object") {
1005
+ throw new BadRequestException(`Batch request at index ${index} must be an object.`);
1006
+ }
1007
+ const { requestId, path, method, body } = entry;
1008
+ if (requestId !== void 0 && typeof requestId !== "string") {
1009
+ throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
1010
+ }
1011
+ if (typeof path !== "string" || path.length === 0) {
1012
+ throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
1013
+ }
1014
+ if (typeof method !== "string") {
1015
+ throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
1016
+ }
1017
+ const normalizedMethod = method.toUpperCase();
1018
+ if (!isAtomicHttpMethod(normalizedMethod)) {
1019
+ throw new BadRequestException(`Batch request at index ${index} uses the unsupported method ${method}.`);
1020
+ }
1021
+ return {
1022
+ requestId,
1023
+ path,
1024
+ method: normalizedMethod,
1025
+ body
1026
+ };
1027
+ }
901
1028
  /**
902
1029
  * Finds the route definition for a given request.
903
1030
  * This method searches the routing tree for a matching route based on the request's path and method.
@@ -1034,30 +1161,6 @@ Router = _ts_decorate([
1034
1161
 
1035
1162
  // src/app.ts
1036
1163
  import { app, BrowserWindow, ipcMain, MessageChannelMain } from "electron/main";
1037
-
1038
- // src/request.ts
1039
- import "reflect-metadata";
1040
- var _Request = class _Request {
1041
- constructor(event, id, method, path, body) {
1042
- __publicField(this, "event");
1043
- __publicField(this, "id");
1044
- __publicField(this, "method");
1045
- __publicField(this, "path");
1046
- __publicField(this, "body");
1047
- __publicField(this, "context", RootInjector.createScope());
1048
- __publicField(this, "params", {});
1049
- this.event = event;
1050
- this.id = id;
1051
- this.method = method;
1052
- this.path = path;
1053
- this.body = body;
1054
- this.path = path.replace(/^\/|\/$/g, "");
1055
- }
1056
- };
1057
- __name(_Request, "Request");
1058
- var Request = _Request;
1059
-
1060
- // src/app.ts
1061
1164
  function _ts_decorate2(decorators, target, key, desc) {
1062
1165
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1063
1166
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noxfly/noxus",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "main": "dist/noxus.js",
5
5
  "types": "dist/noxus.d.ts",
6
6
  "scripts": {
@@ -23,7 +23,12 @@ export interface IRouteMetadata {
23
23
  /**
24
24
  * The different HTTP methods that can be used in the application.
25
25
  */
26
- export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
26
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'BATCH';
27
+
28
+ /**
29
+ * Atomic HTTP verbs supported by controllers. BATCH is handled at the router level only.
30
+ */
31
+ export type AtomicHttpMethod = Exclude<HttpMethod, 'BATCH'>;
27
32
 
28
33
  /**
29
34
  * The configuration that waits a route's decorator.
package/src/request.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import 'reflect-metadata';
8
- import { HttpMethod } from 'src/decorators/method.decorator';
8
+ import { AtomicHttpMethod, HttpMethod } from 'src/decorators/method.decorator';
9
9
  import { AppInjector, RootInjector } from 'src/DI/app-injector';
10
10
 
11
11
  /**
@@ -34,22 +34,37 @@ export class Request {
34
34
  * It includes properties for the sender ID, request ID, path, method, and an optional body.
35
35
  * This interface is used to standardize the request data across the application.
36
36
  */
37
- export interface IRequest<T = any> {
37
+ export interface IRequest<TBody = unknown> {
38
38
  senderId: number;
39
39
  requestId: string;
40
40
  path: string;
41
41
  method: HttpMethod;
42
- body?: T;
42
+ body?: TBody;
43
+ }
44
+
45
+ export interface IBatchRequestItem<TBody = unknown> {
46
+ requestId?: string;
47
+ path: string;
48
+ method: AtomicHttpMethod;
49
+ body?: TBody;
50
+ }
51
+
52
+ export interface IBatchRequestPayload {
53
+ requests: IBatchRequestItem[];
43
54
  }
44
55
 
45
56
  /**
46
57
  * Creates a Request object from the IPC event data.
47
58
  * This function extracts the necessary information from the IPC event and constructs a Request instance.
48
59
  */
49
- export interface IResponse<T = any> {
60
+ export interface IResponse<TBody = unknown> {
50
61
  requestId: string;
51
62
  status: number;
52
- body?: T;
63
+ body?: TBody;
53
64
  error?: string;
54
65
  stack?: string;
55
66
  }
67
+
68
+ export interface IBatchResponsePayload {
69
+ responses: IResponse[];
70
+ }
package/src/router.ts CHANGED
@@ -8,14 +8,20 @@ import 'reflect-metadata';
8
8
  import { getControllerMetadata } from 'src/decorators/controller.decorator';
9
9
  import { getGuardForController, getGuardForControllerAction, IGuard } from 'src/decorators/guards.decorator';
10
10
  import { Injectable } from 'src/decorators/injectable.decorator';
11
- import { getRouteMetadata } from 'src/decorators/method.decorator';
11
+ import { AtomicHttpMethod, getRouteMetadata } from 'src/decorators/method.decorator';
12
12
  import { getMiddlewaresForController, getMiddlewaresForControllerAction, IMiddleware, NextFunction } from 'src/decorators/middleware.decorator';
13
- import { MethodNotAllowedException, NotFoundException, ResponseException, UnauthorizedException } from 'src/exceptions';
14
- import { IResponse, Request } from 'src/request';
13
+ import { BadRequestException, MethodNotAllowedException, NotFoundException, ResponseException, UnauthorizedException } from 'src/exceptions';
14
+ import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from 'src/request';
15
15
  import { Logger } from 'src/utils/logger';
16
16
  import { RadixTree } from 'src/utils/radix-tree';
17
17
  import { Type } from 'src/utils/types';
18
18
 
19
+ const ATOMIC_HTTP_METHODS: ReadonlySet<AtomicHttpMethod> = new Set<AtomicHttpMethod>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
20
+
21
+ function isAtomicHttpMethod(method: unknown): method is AtomicHttpMethod {
22
+ return typeof method === 'string' && ATOMIC_HTTP_METHODS.has(method as AtomicHttpMethod);
23
+ }
24
+
19
25
  /**
20
26
  * IRouteDefinition interface defines the structure of a route in the application.
21
27
  * It includes the HTTP method, path, controller class, handler method name,
@@ -121,6 +127,14 @@ export class Router {
121
127
  * @param channelSenderId - The ID of the sender channel to shut down.
122
128
  */
123
129
  public async handle(request: Request): Promise<IResponse> {
130
+ if(request.method === 'BATCH') {
131
+ return this.handleBatch(request);
132
+ }
133
+
134
+ return this.handleAtomic(request);
135
+ }
136
+
137
+ private async handleAtomic(request: Request): Promise<IResponse> {
124
138
  Logger.comment(`> ${request.method} /${request.path}`);
125
139
 
126
140
  const t0 = performance.now();
@@ -182,6 +196,122 @@ export class Router {
182
196
  }
183
197
  }
184
198
 
199
+ private async handleBatch(request: Request): Promise<IResponse> {
200
+ Logger.comment(`> ${request.method} /${request.path}`);
201
+
202
+ const t0 = performance.now();
203
+
204
+ const response: IResponse<IBatchResponsePayload> = {
205
+ requestId: request.id,
206
+ status: 200,
207
+ body: { responses: [] },
208
+ };
209
+
210
+ try {
211
+ const payload = this.normalizeBatchPayload(request.body);
212
+ const batchResponses: IResponse[] = [];
213
+
214
+ for(const [index, item] of payload.requests.entries()) {
215
+ const subRequestId = item.requestId ?? `${request.id}:${index}`;
216
+ const atomicRequest = new Request(request.event, subRequestId, item.method, item.path, item.body);
217
+ batchResponses.push(await this.handleAtomic(atomicRequest));
218
+ }
219
+
220
+ response.body!.responses = batchResponses;
221
+ }
222
+ catch(error: unknown) {
223
+ response.body = undefined;
224
+
225
+ if(error instanceof ResponseException) {
226
+ response.status = error.status;
227
+ response.error = error.message;
228
+ response.stack = error.stack;
229
+ }
230
+ else if(error instanceof Error) {
231
+ response.status = 500;
232
+ response.error = error.message || 'Internal Server Error';
233
+ response.stack = error.stack || 'No stack trace available';
234
+ }
235
+ else {
236
+ response.status = 500;
237
+ response.error = 'Unknown error occurred';
238
+ response.stack = 'No stack trace available';
239
+ }
240
+ }
241
+ finally {
242
+ const t1 = performance.now();
243
+
244
+ const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
245
+
246
+ if(response.status < 400)
247
+ Logger.log(message);
248
+ else if(response.status < 500)
249
+ Logger.warn(message);
250
+ else
251
+ Logger.error(message);
252
+
253
+ if(response.error !== undefined) {
254
+ Logger.error(response.error);
255
+
256
+ if(response.stack !== undefined) {
257
+ Logger.errorStack(response.stack);
258
+ }
259
+ }
260
+
261
+ return response;
262
+ }
263
+ }
264
+
265
+ private normalizeBatchPayload(body: unknown): IBatchRequestPayload {
266
+ if(body === null || typeof body !== 'object') {
267
+ throw new BadRequestException('Batch payload must be an object containing a requests array.');
268
+ }
269
+
270
+ const possiblePayload = body as Partial<IBatchRequestPayload>;
271
+ const { requests } = possiblePayload;
272
+
273
+ if(!Array.isArray(requests)) {
274
+ throw new BadRequestException('Batch payload must define a requests array.');
275
+ }
276
+
277
+ const normalizedRequests = requests.map((entry, index) => this.normalizeBatchItem(entry, index));
278
+
279
+ return { requests: normalizedRequests };
280
+ }
281
+
282
+ private normalizeBatchItem(entry: unknown, index: number): IBatchRequestItem {
283
+ if(entry === null || typeof entry !== 'object') {
284
+ throw new BadRequestException(`Batch request at index ${index} must be an object.`);
285
+ }
286
+
287
+ const { requestId, path, method, body } = entry as Partial<IBatchRequestItem> & { method?: unknown };
288
+
289
+ if(requestId !== undefined && typeof requestId !== 'string') {
290
+ throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
291
+ }
292
+
293
+ if(typeof path !== 'string' || path.length === 0) {
294
+ throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
295
+ }
296
+
297
+ if(typeof method !== 'string') {
298
+ throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
299
+ }
300
+
301
+ const normalizedMethod = method.toUpperCase();
302
+
303
+ if(!isAtomicHttpMethod(normalizedMethod)) {
304
+ throw new BadRequestException(`Batch request at index ${index} uses the unsupported method ${method}.`);
305
+ }
306
+
307
+ return {
308
+ requestId,
309
+ path,
310
+ method: normalizedMethod as AtomicHttpMethod,
311
+ body,
312
+ };
313
+ }
314
+
185
315
  /**
186
316
  * Finds the route definition for a given request.
187
317
  * This method searches the routing tree for a matching route based on the request's path and method.