@clairejs/server 3.15.2 → 3.16.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/.mocharc.json +3 -0
- package/dist/common/AbstractController.js +3 -0
- package/dist/common/ControllerMetadata.js +1 -0
- package/dist/common/FileOperation.js +6 -0
- package/dist/common/ServerModelMetadata.js +1 -0
- package/dist/common/Transactionable.js +17 -0
- package/dist/common/auth/AbstractPrincipalResolver.js +2 -0
- package/dist/common/auth/IPrincipal.js +1 -0
- package/dist/common/constants.js +7 -0
- package/dist/common/decorator.d.ts +2 -2
- package/dist/common/decorator.js +6 -0
- package/dist/common/request/EndpointMetadata.js +1 -0
- package/dist/common/request/HttpData.js +1 -0
- package/dist/common/request/HttpEndpoint.js +1 -0
- package/dist/common/request/JobData.js +1 -0
- package/dist/common/request/MountedEndpointInfo.js +1 -0
- package/dist/common/request/RequestOptions.js +1 -0
- package/dist/common/request/SocketData.js +1 -0
- package/dist/common/request/types.d.ts +1 -1
- package/dist/common/request/types.js +1 -0
- package/dist/controllers/FileManageController.js +90 -0
- package/dist/controllers/FileUploadController.js +64 -0
- package/dist/controllers/dto/system.js +14 -0
- package/dist/controllers/dto/upload.js +205 -0
- package/dist/http/auth/AbstractHttpAuthorizer.js +2 -0
- package/dist/http/common/HttpRequest.js +72 -0
- package/dist/http/common/HttpResponse.js +62 -0
- package/dist/http/controller/AbstractHttpController.js +21 -0
- package/dist/http/controller/AbstractHttpMiddleware.js +2 -0
- package/dist/http/controller/AbstractHttpRequestHandler.js +69 -0
- package/dist/http/controller/CrudHttpController.js +302 -0
- package/dist/http/controller/DefaultHttpRequestHandler.js +143 -0
- package/dist/http/decorators.d.ts +1 -1
- package/dist/http/decorators.js +86 -0
- package/dist/http/file-upload/AbstractFileUploadHandler.js +2 -0
- package/dist/http/file-upload/FileUploadHandler.js +41 -0
- package/dist/http/file-upload/types.d.ts +1 -1
- package/dist/http/file-upload/types.js +1 -0
- package/dist/http/repository/AbstractRepository.js +26 -0
- package/dist/http/repository/DtoRepository.d.ts +3 -3
- package/dist/http/repository/DtoRepository.js +204 -0
- package/dist/http/repository/ICrudRepository.js +1 -0
- package/dist/http/repository/ModelRepository.js +696 -0
- package/dist/http/security/AbstractAccessCondition.js +2 -0
- package/dist/http/security/access-conditions/FilterModelFieldAccessCondition.js +30 -0
- package/dist/http/security/access-conditions/MaximumQueryLimit.js +31 -0
- package/dist/http/security/cors.js +1 -0
- package/dist/http/utils.js +32 -0
- package/dist/index.js +75 -1
- package/dist/job/AbstractJobController.js +9 -0
- package/dist/job/AbstractJobRepository.js +2 -0
- package/dist/job/AbstractJobScheduler.js +48 -0
- package/dist/job/AwsJobScheduler.js +405 -0
- package/dist/job/LocalJobScheduler.js +273 -0
- package/dist/job/decorators.js +57 -0
- package/dist/job/interfaces.js +10 -0
- package/dist/logging/FileLogMedium.js +44 -0
- package/dist/services/AbstractFileService.js +28 -0
- package/dist/services/AbstractMailService.js +2 -0
- package/dist/services/AbstractService.js +3 -0
- package/dist/services/AbstractSmsService.js +2 -0
- package/dist/services/implementations/LocalFileService.js +42 -0
- package/dist/services/implementations/LocalMailService.js +27 -0
- package/dist/services/implementations/LocalSmsService.js +17 -0
- package/dist/services/implementations/S3FileService.js +107 -0
- package/dist/services/implementations/SesMailService.js +64 -0
- package/dist/socket/AbstractServerSocket.js +44 -0
- package/dist/socket/AbstractServerSocketManager.d.ts +1 -1
- package/dist/socket/AbstractServerSocketManager.js +348 -0
- package/dist/socket/AbstractSocketConnectionHandler.js +2 -0
- package/dist/socket/AbstractSocketController.d.ts +3 -3
- package/dist/socket/AbstractSocketController.js +12 -0
- package/dist/socket/AwsSocketManager.d.ts +2 -2
- package/dist/socket/AwsSocketManager.js +160 -0
- package/dist/socket/IServerSocket.js +1 -0
- package/dist/socket/LocalSocketManager.js +292 -0
- package/dist/system/ClaireServer.js +78 -0
- package/dist/system/ExpressWrapper.js +122 -0
- package/dist/system/LambdaWrapper.js +151 -0
- package/dist/system/ServerGlobalStore.js +1 -0
- package/dist/system/lamba-request-mapper.js +49 -0
- package/dist/system/locale/LocaleEntry.js +13 -0
- package/dist/system/locale/LocaleTranslation.js +47 -0
- package/dist/system/locale/decorators.js +14 -0
- package/package.json +13 -20
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { AbstractLogger, Initable, LogContext, MessageType, SocketMethod } from "@clairejs/core";
|
|
11
|
+
import WebSocket from "ws";
|
|
12
|
+
import parseurl from "parseurl";
|
|
13
|
+
import queryString from "query-string";
|
|
14
|
+
import Redis from "ioredis";
|
|
15
|
+
import { AbstractServerSocketManager } from "./AbstractServerSocketManager";
|
|
16
|
+
import { AbstractServerSocket } from "./AbstractServerSocket";
|
|
17
|
+
import { AbstractRequestAuthorizer } from "../http/auth/AbstractHttpAuthorizer";
|
|
18
|
+
import { AbstractPrincipalResolver } from "../common/auth/AbstractPrincipalResolver";
|
|
19
|
+
var RedisSocketCommand;
|
|
20
|
+
(function (RedisSocketCommand) {
|
|
21
|
+
RedisSocketCommand["SEND"] = "SEND";
|
|
22
|
+
RedisSocketCommand["CLOSE"] = "CLOSE";
|
|
23
|
+
RedisSocketCommand["SAVE"] = "SAVE";
|
|
24
|
+
})(RedisSocketCommand || (RedisSocketCommand = {}));
|
|
25
|
+
class RedisSocket extends AbstractServerSocket {
|
|
26
|
+
socketChannelName;
|
|
27
|
+
redisClient;
|
|
28
|
+
socketInfo;
|
|
29
|
+
constructor(socketChannelName, redisClient, socketInfo) {
|
|
30
|
+
super(socketInfo);
|
|
31
|
+
this.socketChannelName = socketChannelName;
|
|
32
|
+
this.redisClient = redisClient;
|
|
33
|
+
this.socketInfo = socketInfo;
|
|
34
|
+
}
|
|
35
|
+
async physicSend(data) {
|
|
36
|
+
await this.redisClient.publish(this.socketChannelName, JSON.stringify({
|
|
37
|
+
type: RedisSocketCommand.SEND,
|
|
38
|
+
data: data,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
async disconnect(err) {
|
|
42
|
+
await this.redisClient.publish(this.socketChannelName, JSON.stringify({
|
|
43
|
+
type: RedisSocketCommand.CLOSE,
|
|
44
|
+
data: err,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
async saveSocketInfo() {
|
|
48
|
+
await this.redisClient.publish(this.socketChannelName, JSON.stringify({
|
|
49
|
+
type: RedisSocketCommand.SAVE,
|
|
50
|
+
data: this.socketInfo,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class ExpressSocket extends AbstractServerSocket {
|
|
55
|
+
socketManager;
|
|
56
|
+
socket;
|
|
57
|
+
socketInfo;
|
|
58
|
+
infoSaver;
|
|
59
|
+
constructor(socketManager, socket, socketInfo, infoSaver) {
|
|
60
|
+
super(socketInfo);
|
|
61
|
+
this.socketManager = socketManager;
|
|
62
|
+
this.socket = socket;
|
|
63
|
+
this.socketInfo = socketInfo;
|
|
64
|
+
this.infoSaver = infoSaver;
|
|
65
|
+
}
|
|
66
|
+
async saveSocketInfo() {
|
|
67
|
+
//-- save to redis
|
|
68
|
+
await this.infoSaver(this.socketInfo);
|
|
69
|
+
}
|
|
70
|
+
async physicSend(data) {
|
|
71
|
+
this.socket.send(JSON.stringify(data));
|
|
72
|
+
}
|
|
73
|
+
async disconnect(err) {
|
|
74
|
+
this.socket.close(1000, err && JSON.stringify(err));
|
|
75
|
+
await this.socketManager.removeSocket(this.getId());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let LocalSocketManager = class LocalSocketManager extends AbstractServerSocketManager {
|
|
79
|
+
authenticationRequired;
|
|
80
|
+
subClient;
|
|
81
|
+
channelSubClient;
|
|
82
|
+
sendClient;
|
|
83
|
+
logger;
|
|
84
|
+
principalResolver;
|
|
85
|
+
requestAuthorizer;
|
|
86
|
+
allSockets = [];
|
|
87
|
+
subscribedChannels = [];
|
|
88
|
+
constructor(authenticationRequired, subClient, //-- use to subscribe to the socket-self channels
|
|
89
|
+
channelSubClient, //-- use to subscribe to channels
|
|
90
|
+
sendClient, //-- use to publish data
|
|
91
|
+
logger, principalResolver, requestAuthorizer) {
|
|
92
|
+
super(authenticationRequired, logger, sendClient, principalResolver, requestAuthorizer);
|
|
93
|
+
this.authenticationRequired = authenticationRequired;
|
|
94
|
+
this.subClient = subClient;
|
|
95
|
+
this.channelSubClient = channelSubClient;
|
|
96
|
+
this.sendClient = sendClient;
|
|
97
|
+
this.logger = logger;
|
|
98
|
+
this.principalResolver = principalResolver;
|
|
99
|
+
this.requestAuthorizer = requestAuthorizer;
|
|
100
|
+
}
|
|
101
|
+
async init() {
|
|
102
|
+
this.logger.info("LocalSocketManager init");
|
|
103
|
+
this.channelSubClient.on("message", (channel, message) => {
|
|
104
|
+
this.logger.debug("channelSubClient: ", message, channel);
|
|
105
|
+
this.channelMessageListenner(message, channel);
|
|
106
|
+
});
|
|
107
|
+
this.subClient.on("message", (channel, message) => {
|
|
108
|
+
this.logger.debug("subClient: ", message, channel);
|
|
109
|
+
this.socketMessageListener(message, channel);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
exit() {
|
|
113
|
+
this.logger.info("LocalSocketManager exit");
|
|
114
|
+
}
|
|
115
|
+
channelMessageListenner(message, channel) {
|
|
116
|
+
this.logger.debug("Receiving message from channel", channel, message);
|
|
117
|
+
const socketMessage = JSON.parse(message);
|
|
118
|
+
//-- only broadcast plain message
|
|
119
|
+
if (socketMessage.type === MessageType.PLAIN) {
|
|
120
|
+
this.allSockets.forEach((socket) => {
|
|
121
|
+
//-- socket.send will check for necessary permission
|
|
122
|
+
socket.send(socketMessage.data.message, socketMessage.data.channel);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
socketMessageListener(message, channel) {
|
|
127
|
+
this.logger.debug("Receiving message from remote socket channel", channel, message);
|
|
128
|
+
const socketId = channel.substring(this.getSocketChannelKeyPrefix().length);
|
|
129
|
+
//-- find the socket
|
|
130
|
+
const socket = this.allSockets.find((s) => s.getId() === socketId);
|
|
131
|
+
if (socket) {
|
|
132
|
+
//-- handle message
|
|
133
|
+
const command = JSON.parse(message);
|
|
134
|
+
switch (command.type) {
|
|
135
|
+
case RedisSocketCommand.SEND:
|
|
136
|
+
socket.physicSend(command.data);
|
|
137
|
+
break;
|
|
138
|
+
case RedisSocketCommand.SAVE:
|
|
139
|
+
socket.socketInfo.channels = command.data;
|
|
140
|
+
socket.saveSocketInfo();
|
|
141
|
+
break;
|
|
142
|
+
case RedisSocketCommand.CLOSE:
|
|
143
|
+
socket.disconnect(command.data);
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
//-- ignore
|
|
147
|
+
this.logger.debug("Invalid remote command", command);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
getUniqueDataChannelName(channel) {
|
|
153
|
+
return `CHANNEL_DATA:${channel}`;
|
|
154
|
+
}
|
|
155
|
+
getSocketChannelKeyPrefix() {
|
|
156
|
+
return "SOCKET_CHANNEL:";
|
|
157
|
+
}
|
|
158
|
+
getSocketChannelKey(socketId) {
|
|
159
|
+
return `${this.getSocketChannelKeyPrefix()}${socketId}`;
|
|
160
|
+
}
|
|
161
|
+
async broadcastToChannel(channel, data) {
|
|
162
|
+
const formattedData = this.formatBroadcastData(channel, data);
|
|
163
|
+
await this.redisClient.publish(this.getUniqueDataChannelName(channel), JSON.stringify(formattedData));
|
|
164
|
+
}
|
|
165
|
+
async addSocketToChannel(socketId, channelInfos) {
|
|
166
|
+
const socket = await super.addSocketToChannel(socketId, channelInfos);
|
|
167
|
+
const notYetSubscribed = channelInfos.map((c) => c.name).filter((c) => !this.subscribedChannels.includes(c));
|
|
168
|
+
if (notYetSubscribed.length) {
|
|
169
|
+
await Promise.all(notYetSubscribed.map((channelName) => {
|
|
170
|
+
return this.channelSubClient.subscribe(this.getUniqueDataChannelName(channelName));
|
|
171
|
+
}));
|
|
172
|
+
this.subscribedChannels.push(...notYetSubscribed);
|
|
173
|
+
}
|
|
174
|
+
return socket;
|
|
175
|
+
}
|
|
176
|
+
async removeSocketFromChannel(socketId, channels, persist = true) {
|
|
177
|
+
await super.removeSocketFromChannel(socketId, channels, persist);
|
|
178
|
+
await Promise.all(channels.map((channel) => this.channelSubClient.unsubscribe(this.getUniqueChannelName(channel))));
|
|
179
|
+
this.subscribedChannels = this.subscribedChannels.filter((c) => !channels.includes(c));
|
|
180
|
+
}
|
|
181
|
+
async addSocket(socketInfo, { socket }) {
|
|
182
|
+
const expressSocket = new ExpressSocket(this, socket, socketInfo, async (socketInfo) => {
|
|
183
|
+
await this.redisClient.hset(this.getSocketInfoHashKey(), socketInfo.id, JSON.stringify(socketInfo));
|
|
184
|
+
});
|
|
185
|
+
//-- create a channel on redis for cross-instance communication
|
|
186
|
+
const channelKey = this.getSocketChannelKey(socketInfo.id);
|
|
187
|
+
//-- subscribe to that channel
|
|
188
|
+
await this.subClient.subscribe(channelKey);
|
|
189
|
+
this.allSockets.push(expressSocket);
|
|
190
|
+
return expressSocket;
|
|
191
|
+
}
|
|
192
|
+
async removeSocket(socketId, physicRemove = true) {
|
|
193
|
+
await super.removeSocket(socketId, physicRemove);
|
|
194
|
+
//-- unsubscribe from that channel
|
|
195
|
+
const channelKey = this.getSocketChannelKey(socketId);
|
|
196
|
+
await this.subClient.unsubscribe(channelKey);
|
|
197
|
+
//-- filter out socket
|
|
198
|
+
this.allSockets = this.allSockets.filter((s) => s.getId() !== socketId);
|
|
199
|
+
}
|
|
200
|
+
async getById(socketId) {
|
|
201
|
+
let socket = this.allSockets.find((s) => s.getId() === socketId);
|
|
202
|
+
if (!socket) {
|
|
203
|
+
//-- check redis
|
|
204
|
+
const socketInfo = await new Promise((resolve) => this.redisClient
|
|
205
|
+
.hget(this.getSocketInfoHashKey(), socketId)
|
|
206
|
+
.then((data) => resolve(data ? JSON.parse(data) : undefined))
|
|
207
|
+
.catch((err) => {
|
|
208
|
+
this.logger.error(err);
|
|
209
|
+
resolve(undefined);
|
|
210
|
+
}));
|
|
211
|
+
//-- if found then create redis socket
|
|
212
|
+
if (socketInfo) {
|
|
213
|
+
socket = new RedisSocket(this.getSocketChannelKey(socketId), this.sendClient, socketInfo);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return socket;
|
|
217
|
+
}
|
|
218
|
+
async getSocketsByChannel(channel) {
|
|
219
|
+
//-- get socket ids
|
|
220
|
+
const ids = await this.getSocketIdsOfChannel(channel);
|
|
221
|
+
const sockets = await Promise.all(ids.map((id) => this.getById(id)));
|
|
222
|
+
return sockets.filter((s) => !!s);
|
|
223
|
+
}
|
|
224
|
+
configure(httpServer) {
|
|
225
|
+
if (!httpServer) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.logger.debug("Local socket manager configure");
|
|
229
|
+
const socketServer = new WebSocket.Server({ server: httpServer });
|
|
230
|
+
socketServer.on("connection", (socket, req) => {
|
|
231
|
+
const socketId = req.headers["sec-websocket-key"];
|
|
232
|
+
let socketReady = false;
|
|
233
|
+
let bufferMessages = [];
|
|
234
|
+
//-- call handle connection
|
|
235
|
+
const result = parseurl({ url: req.url });
|
|
236
|
+
const query = queryString.parse(decodeURIComponent(result.search || ""));
|
|
237
|
+
this.handle({
|
|
238
|
+
method: SocketMethod.CONNECT,
|
|
239
|
+
socketId: socketId,
|
|
240
|
+
data: { queries: query, socket },
|
|
241
|
+
})
|
|
242
|
+
.then(() => {
|
|
243
|
+
socketReady = true;
|
|
244
|
+
for (const m of bufferMessages) {
|
|
245
|
+
this.handle(m).catch((err) => {
|
|
246
|
+
this.logger.error(err);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
.catch((err) => {
|
|
251
|
+
//-- free up buffered messages
|
|
252
|
+
bufferMessages = [];
|
|
253
|
+
//-- close with code 1000
|
|
254
|
+
socket.close(1000, `${err.name}:${err.message}`);
|
|
255
|
+
});
|
|
256
|
+
socket.onmessage = (event) => {
|
|
257
|
+
const data = {
|
|
258
|
+
method: SocketMethod.MESSAGE,
|
|
259
|
+
socketId: socketId,
|
|
260
|
+
data: event.data && JSON.parse(event.data),
|
|
261
|
+
};
|
|
262
|
+
if (socketReady) {
|
|
263
|
+
this.handle(data).catch((err) => {
|
|
264
|
+
this.logger.error(err);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
bufferMessages.push(data);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
socket.onclose = () => {
|
|
272
|
+
this.handle({
|
|
273
|
+
method: SocketMethod.DISCONNECT,
|
|
274
|
+
socketId: socketId,
|
|
275
|
+
}).catch((err) => {
|
|
276
|
+
this.logger.error(err);
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
LocalSocketManager = __decorate([
|
|
283
|
+
Initable(),
|
|
284
|
+
LogContext(),
|
|
285
|
+
__metadata("design:paramtypes", [Boolean, Redis,
|
|
286
|
+
Redis,
|
|
287
|
+
Redis,
|
|
288
|
+
AbstractLogger,
|
|
289
|
+
AbstractPrincipalResolver,
|
|
290
|
+
AbstractRequestAuthorizer])
|
|
291
|
+
], LocalSocketManager);
|
|
292
|
+
export { LocalSocketManager };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { AbstractLogger, ClaireApp, LogContext, getGlobalStore } from "@clairejs/core";
|
|
11
|
+
import { ExitCode } from "../common/constants";
|
|
12
|
+
import { AbstractHttpRequestHandler } from "../http/controller/AbstractHttpRequestHandler";
|
|
13
|
+
import { AbstractServerSocketManager } from "../socket/AbstractServerSocketManager";
|
|
14
|
+
import { getEndpointId } from "../http/utils";
|
|
15
|
+
let ClaireServer = class ClaireServer extends ClaireApp {
|
|
16
|
+
logger;
|
|
17
|
+
httpRequestHandler;
|
|
18
|
+
socketManager;
|
|
19
|
+
booted = false;
|
|
20
|
+
constructor(logger, httpRequestHandler, socketManager) {
|
|
21
|
+
super();
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
this.httpRequestHandler = httpRequestHandler;
|
|
24
|
+
this.socketManager = socketManager;
|
|
25
|
+
}
|
|
26
|
+
async init() {
|
|
27
|
+
if (this.booted) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const mountedEndpointInfo = [];
|
|
31
|
+
if (this.httpRequestHandler) {
|
|
32
|
+
const infos = await this.httpRequestHandler.getMountedEndpointInfo();
|
|
33
|
+
mountedEndpointInfo.push(...infos);
|
|
34
|
+
}
|
|
35
|
+
if (this.socketManager) {
|
|
36
|
+
const infos = await this.socketManager.getMountedEndpointInfo();
|
|
37
|
+
mountedEndpointInfo.push(...infos);
|
|
38
|
+
}
|
|
39
|
+
//-- save mounted endpoint info in server store to prevent circular dependency when resolving http request handler in controller
|
|
40
|
+
for (const endpoint of mountedEndpointInfo) {
|
|
41
|
+
this.logger.info(`Mounting: ${getEndpointId(endpoint.endpoint)}`);
|
|
42
|
+
}
|
|
43
|
+
getGlobalStore().mountedEndpointInfo = mountedEndpointInfo;
|
|
44
|
+
this.logger.debug("Claire server initing");
|
|
45
|
+
//-- handle exceptions
|
|
46
|
+
this.logger.debug(`Setting up exception handlers`);
|
|
47
|
+
process.on("SIGTERM", () => {
|
|
48
|
+
this.logger.warn("SIGTERM interrupt signal");
|
|
49
|
+
return this.stop(ExitCode.SIGTERM_INTERUPTION);
|
|
50
|
+
});
|
|
51
|
+
process.on("SIGINT", () => {
|
|
52
|
+
this.logger.warn("SIGINT interrupt signal");
|
|
53
|
+
return this.stop(ExitCode.SIGTERM_INTERUPTION);
|
|
54
|
+
});
|
|
55
|
+
process.on("uncaughtException", (err) => {
|
|
56
|
+
this.logger.error("uncaughtException", err.name, err.stack);
|
|
57
|
+
});
|
|
58
|
+
process.on("unhandledRejection", (err) => {
|
|
59
|
+
this.logger.error("unhandledRejection", err.name, err.stack);
|
|
60
|
+
});
|
|
61
|
+
this.booted = true;
|
|
62
|
+
}
|
|
63
|
+
exit() {
|
|
64
|
+
super.exit();
|
|
65
|
+
}
|
|
66
|
+
stop(code) {
|
|
67
|
+
this.logger.debug("Server is shutting down");
|
|
68
|
+
this.exit();
|
|
69
|
+
process.exit(code);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
ClaireServer = __decorate([
|
|
73
|
+
LogContext(),
|
|
74
|
+
__metadata("design:paramtypes", [AbstractLogger,
|
|
75
|
+
AbstractHttpRequestHandler,
|
|
76
|
+
AbstractServerSocketManager])
|
|
77
|
+
], ClaireServer);
|
|
78
|
+
export { ClaireServer };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Errors, getServiceProvider, SocketMethod } from "@clairejs/core";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import cookieParser from "cookie-parser";
|
|
5
|
+
import expressFileUpload from "express-fileupload";
|
|
6
|
+
import cors from "cors";
|
|
7
|
+
import { AbstractServerSocketManager } from "../socket/AbstractServerSocketManager";
|
|
8
|
+
import { AbstractHttpRequestHandler } from "../http/controller/AbstractHttpRequestHandler";
|
|
9
|
+
export class ExpressWrapper {
|
|
10
|
+
server;
|
|
11
|
+
config;
|
|
12
|
+
injector = getServiceProvider().getInjector();
|
|
13
|
+
socketManager;
|
|
14
|
+
httpRequestHandler;
|
|
15
|
+
httpServer;
|
|
16
|
+
constructor(server, config) {
|
|
17
|
+
this.server = server;
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.socketManager = this.injector.resolveOptional(AbstractServerSocketManager);
|
|
20
|
+
this.httpRequestHandler = this.injector.resolveOptional(AbstractHttpRequestHandler);
|
|
21
|
+
}
|
|
22
|
+
close() {
|
|
23
|
+
this.httpServer?.close();
|
|
24
|
+
this.server.exit();
|
|
25
|
+
}
|
|
26
|
+
async listen(listenPort) {
|
|
27
|
+
await this.server.init();
|
|
28
|
+
await this.injector.initInstances();
|
|
29
|
+
const app = express();
|
|
30
|
+
//-- setup http listener
|
|
31
|
+
const httpServer = http.createServer(app);
|
|
32
|
+
this.socketManager?.configure(httpServer);
|
|
33
|
+
if (this.httpRequestHandler?.corsConfig) {
|
|
34
|
+
app.use(cors({
|
|
35
|
+
allowedHeaders: this.httpRequestHandler.corsConfig.headers,
|
|
36
|
+
credentials: this.httpRequestHandler.corsConfig.credentials,
|
|
37
|
+
origin: this.httpRequestHandler.corsConfig.origins,
|
|
38
|
+
methods: this.httpRequestHandler.corsConfig.methods,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
const requestLimitBytes = (this.config?.requestSetting?.maxBodySizeKB || 10240) * 1024;
|
|
42
|
+
app.use(cookieParser());
|
|
43
|
+
app.use(express.json({ limit: requestLimitBytes }));
|
|
44
|
+
app.use(express.urlencoded({ extended: false, limit: requestLimitBytes }));
|
|
45
|
+
app.use(expressFileUpload());
|
|
46
|
+
app.use((req, _res, next) => {
|
|
47
|
+
if (req.files) {
|
|
48
|
+
if (!req.body) {
|
|
49
|
+
req.body = {};
|
|
50
|
+
}
|
|
51
|
+
Object.keys(req.files).forEach((file) => {
|
|
52
|
+
req.body[file] = req.files[file];
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
next();
|
|
56
|
+
});
|
|
57
|
+
app.use(express.raw({
|
|
58
|
+
limit: requestLimitBytes,
|
|
59
|
+
type: (req) => !["application/json", "application/x-www-form-urlencoded"].find((type) => req.headers["content-type"]?.includes(type)),
|
|
60
|
+
}));
|
|
61
|
+
app.all("*", (req, res) => {
|
|
62
|
+
(async () => {
|
|
63
|
+
const isAwsGatewaySocket = !!req.headers["x-amzn-apigateway-api-id"];
|
|
64
|
+
if (isAwsGatewaySocket) {
|
|
65
|
+
if (!this.socketManager) {
|
|
66
|
+
throw Errors.SYSTEM_ERROR("Socket manager not found");
|
|
67
|
+
}
|
|
68
|
+
const socketData = {
|
|
69
|
+
socketId: req.body.connectionId,
|
|
70
|
+
data: req.body.data,
|
|
71
|
+
method: req.body.type.toLowerCase(),
|
|
72
|
+
};
|
|
73
|
+
//-- if connect method then send url for authentication parsing
|
|
74
|
+
if (socketData.method === SocketMethod.CONNECT) {
|
|
75
|
+
//-- parse querystring, the query string has format of: "{p1=v1, p2=v2}"
|
|
76
|
+
const queryString = req.body.data.querystring;
|
|
77
|
+
const queries = queryString
|
|
78
|
+
.substring(1, queryString.length - 1)
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((pair) => pair.trim().split("="))
|
|
81
|
+
.reduce((collector, pair) => Object.assign(collector, { [pair[0]]: pair[1] }), {});
|
|
82
|
+
socketData.data = { queries };
|
|
83
|
+
}
|
|
84
|
+
await this.socketManager.handle(socketData);
|
|
85
|
+
//-- send success response
|
|
86
|
+
res.status(200).send();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (!this.httpRequestHandler) {
|
|
90
|
+
throw Errors.SYSTEM_ERROR("Http request handler not found");
|
|
91
|
+
}
|
|
92
|
+
const httpData = {
|
|
93
|
+
clientIP: req.ip || req.headers["x-forwarded-for"],
|
|
94
|
+
fullPath: req.url,
|
|
95
|
+
method: req.method.toLowerCase(),
|
|
96
|
+
headers: req.headers,
|
|
97
|
+
body: req.body,
|
|
98
|
+
cookies: req.cookies,
|
|
99
|
+
};
|
|
100
|
+
//-- handle options
|
|
101
|
+
const result = await this.httpRequestHandler.handle(httpData);
|
|
102
|
+
if (result.headers) {
|
|
103
|
+
Object.keys(result.headers).forEach((key) => {
|
|
104
|
+
res.set(key, result.headers[key]);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (result.cookies) {
|
|
108
|
+
Object.keys(result.cookies).forEach((key) => {
|
|
109
|
+
res.cookie(key, result.cookies[key].value, result.cookies[key].options || {});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
res.status(result.code || 200).send(result.value);
|
|
113
|
+
}
|
|
114
|
+
})().catch((err) => {
|
|
115
|
+
res.status(err.code || 500).json({ name: err.name, message: err.message, code: err.code || 500 });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
this.httpServer = httpServer.listen(listenPort, resolve).on("error", reject);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { getServiceProvider, HttpMethod, SocketMethod, Errors } from "@clairejs/core";
|
|
2
|
+
import { AbstractHttpRequestHandler } from "../http/controller/AbstractHttpRequestHandler";
|
|
3
|
+
import { AwsJobScheduler } from "../job/AwsJobScheduler";
|
|
4
|
+
import { CRON_REQUEST_METHOD, INTERVAL_REQUEST_METHOD } from "../job/interfaces";
|
|
5
|
+
import { AbstractServerSocketManager } from "../socket/AbstractServerSocketManager";
|
|
6
|
+
const convertCorsToHeaders = (origin, cors) => {
|
|
7
|
+
const headers = {};
|
|
8
|
+
if (cors?.credentials) {
|
|
9
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
10
|
+
}
|
|
11
|
+
if (cors?.headers) {
|
|
12
|
+
headers["Access-Control-Allow-Headers"] =
|
|
13
|
+
typeof cors.headers === "string" ? cors.headers : cors.headers.join(", ");
|
|
14
|
+
}
|
|
15
|
+
if (cors?.methods) {
|
|
16
|
+
headers["Access-Control-Allow-Methods"] =
|
|
17
|
+
typeof cors.methods === "string" ? cors.methods : cors.methods.join(", ");
|
|
18
|
+
}
|
|
19
|
+
if (cors?.origins) {
|
|
20
|
+
headers["Access-Control-Allow-Origin"] =
|
|
21
|
+
typeof cors.origins === "string"
|
|
22
|
+
? cors.origins
|
|
23
|
+
: cors.origins.find((org) => origin.match(org))
|
|
24
|
+
? origin
|
|
25
|
+
: "";
|
|
26
|
+
}
|
|
27
|
+
return headers;
|
|
28
|
+
};
|
|
29
|
+
const toApiGatewayFormat = (response) => ({
|
|
30
|
+
statusCode: response.code || 200,
|
|
31
|
+
body: response.value && JSON.stringify(response.value),
|
|
32
|
+
headers: response.headers,
|
|
33
|
+
cookies: Object.keys(response.cookies || {}).map((key) => {
|
|
34
|
+
const cookieValue = response.cookies[key];
|
|
35
|
+
let base = `${key}=${cookieValue.value}`;
|
|
36
|
+
if (cookieValue.options?.maxAge) {
|
|
37
|
+
base += `; MaxAge=${cookieValue.options.maxAge}`;
|
|
38
|
+
}
|
|
39
|
+
if (cookieValue.options?.expires) {
|
|
40
|
+
base += `; Expires=${cookieValue.options.expires.toUTCString()}`;
|
|
41
|
+
}
|
|
42
|
+
if (cookieValue.options?.httpOnly) {
|
|
43
|
+
base += `; HttpOnly`;
|
|
44
|
+
}
|
|
45
|
+
if (cookieValue.options?.path) {
|
|
46
|
+
base += `; Path=${cookieValue.options?.path}`;
|
|
47
|
+
}
|
|
48
|
+
if (cookieValue.options?.domain) {
|
|
49
|
+
base += `; Domain=${cookieValue.options?.domain}`;
|
|
50
|
+
}
|
|
51
|
+
if (cookieValue.options?.secure) {
|
|
52
|
+
base += `; Secure`;
|
|
53
|
+
}
|
|
54
|
+
if (cookieValue.options?.sameSite) {
|
|
55
|
+
if (typeof cookieValue.options?.sameSite === "boolean") {
|
|
56
|
+
base += `; SameSite`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
base += `; SameSite=${cookieValue.options?.sameSite}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return base;
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
export class LambdaWrapper {
|
|
66
|
+
serverFactory;
|
|
67
|
+
requestMapper;
|
|
68
|
+
config;
|
|
69
|
+
injector = getServiceProvider().getInjector();
|
|
70
|
+
httpRequestHandler;
|
|
71
|
+
socketManager;
|
|
72
|
+
jobScheduler;
|
|
73
|
+
_server;
|
|
74
|
+
constructor(serverFactory, requestMapper, config) {
|
|
75
|
+
this.serverFactory = serverFactory;
|
|
76
|
+
this.requestMapper = requestMapper;
|
|
77
|
+
this.config = config;
|
|
78
|
+
this.handler = this.handler.bind(this);
|
|
79
|
+
this.socketManager = this.injector.resolveOptional(AbstractServerSocketManager);
|
|
80
|
+
this.httpRequestHandler = this.injector.resolveOptional(AbstractHttpRequestHandler);
|
|
81
|
+
this.jobScheduler = this.injector.resolveOptional(AwsJobScheduler);
|
|
82
|
+
}
|
|
83
|
+
async bootServer() {
|
|
84
|
+
if (!this._server) {
|
|
85
|
+
this._server = await this.serverFactory();
|
|
86
|
+
await this.injector.initInstances();
|
|
87
|
+
this.socketManager?.configure();
|
|
88
|
+
await this._server.init();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async handler(event) {
|
|
92
|
+
await this.bootServer();
|
|
93
|
+
//-- handle http otherwise
|
|
94
|
+
const requestOptions = this.requestMapper(event);
|
|
95
|
+
if (!requestOptions) {
|
|
96
|
+
throw Errors.SYSTEM_ERROR("Cannot resolve event");
|
|
97
|
+
}
|
|
98
|
+
const corsHeaders = convertCorsToHeaders(requestOptions.headers?.origin || "", this.httpRequestHandler?.corsConfig);
|
|
99
|
+
if (requestOptions.method === HttpMethod.OPTIONS) {
|
|
100
|
+
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
101
|
+
}
|
|
102
|
+
if (requestOptions.method === INTERVAL_REQUEST_METHOD) {
|
|
103
|
+
await this.jobScheduler?.handleInterval(requestOptions.body);
|
|
104
|
+
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
105
|
+
}
|
|
106
|
+
if (requestOptions.method === CRON_REQUEST_METHOD) {
|
|
107
|
+
await this.jobScheduler?.handleCron(requestOptions.body);
|
|
108
|
+
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
if (Object.values(HttpMethod).includes(requestOptions.method)) {
|
|
112
|
+
if (!this.httpRequestHandler) {
|
|
113
|
+
throw Errors.SYSTEM_ERROR("Http request handler not found");
|
|
114
|
+
}
|
|
115
|
+
const httpData = {
|
|
116
|
+
clientIP: requestOptions.clientIP,
|
|
117
|
+
fullPath: requestOptions.rawPath,
|
|
118
|
+
method: requestOptions.method,
|
|
119
|
+
headers: requestOptions.headers,
|
|
120
|
+
cookies: requestOptions.cookies,
|
|
121
|
+
body: requestOptions.body,
|
|
122
|
+
};
|
|
123
|
+
const response = await this.httpRequestHandler.handle(httpData);
|
|
124
|
+
return toApiGatewayFormat({ ...response, headers: { ...corsHeaders, ...response.headers } });
|
|
125
|
+
}
|
|
126
|
+
else if (Object.values(SocketMethod).includes(requestOptions.method)) {
|
|
127
|
+
if (!this.socketManager) {
|
|
128
|
+
throw Errors.SYSTEM_ERROR("Socket manager not found");
|
|
129
|
+
}
|
|
130
|
+
const socketData = {
|
|
131
|
+
socketId: requestOptions.rawPath,
|
|
132
|
+
method: requestOptions.method,
|
|
133
|
+
data: requestOptions.body,
|
|
134
|
+
};
|
|
135
|
+
await this.socketManager.handle(socketData);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
throw Errors.HTTP_REQUEST_ERROR("Not allow request method: " + requestOptions.method);
|
|
139
|
+
}
|
|
140
|
+
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
return toApiGatewayFormat({
|
|
144
|
+
code: err.code || 500,
|
|
145
|
+
value: { name: err.name, message: err.message, code: err.code || 500 },
|
|
146
|
+
headers: corsHeaders,
|
|
147
|
+
cookies: {},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|