@joystick.js/node-canary 0.0.0-canary.52 → 0.0.0-canary.54
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/dist/app/index.js +673 -0
- package/package.json +1 -1
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import aws from "aws-sdk";
|
|
3
|
+
import EventEmitter from "events";
|
|
4
|
+
import * as WebSocket from "ws";
|
|
5
|
+
import queryString from "query-string";
|
|
6
|
+
import multer from "multer";
|
|
7
|
+
import cron from "node-cron";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import initExpress from "./initExpress.js";
|
|
10
|
+
import handleProcessErrors from "./handleProcessErrors";
|
|
11
|
+
import registerGetters from "./registerGetters.js";
|
|
12
|
+
import registerSetters from "./registerSetters.js";
|
|
13
|
+
import connectMongoDB from "./databases/mongodb/index.js";
|
|
14
|
+
import connectPostgreSQL from "./databases/postgresql/index.js";
|
|
15
|
+
import accounts from "./accounts";
|
|
16
|
+
import formatAPIError from "../lib/formatAPIError";
|
|
17
|
+
import hasLoginTokenExpired from "./accounts/hasLoginTokenExpired.js";
|
|
18
|
+
import { isObject } from "../validation/lib/typeValidators.js";
|
|
19
|
+
import supportedHTTPMethods from "../lib/supportedHTTPMethods.js";
|
|
20
|
+
import getAPIURLComponent from "./getAPIURLComponent";
|
|
21
|
+
import validateUploaderOptions from "./validateUploaderOptions.js";
|
|
22
|
+
import log from "../lib/log.js";
|
|
23
|
+
import validateUploads from "./validateUploads";
|
|
24
|
+
import runUploader from "./runUploader";
|
|
25
|
+
import generateId from "../lib/generateId.js";
|
|
26
|
+
import getOutput from "./getOutput.js";
|
|
27
|
+
import defaultUserOutputFields from "./accounts/defaultUserOutputFields.js";
|
|
28
|
+
import createPostgreSQLAccountsTables from "./databases/postgresql/createAccountsTables";
|
|
29
|
+
import createPostgreSQLAccountsIndexes from "./databases/postgresql/createAccountsIndexes";
|
|
30
|
+
import loadSettings from "../settings/load.js";
|
|
31
|
+
import Queue from "./queues/index.js";
|
|
32
|
+
import readDirectory from "../lib/readDirectory.js";
|
|
33
|
+
import getBuildPath from "../lib/getBuildPath.js";
|
|
34
|
+
import generateMachineId from "../lib/generateMachineId.js";
|
|
35
|
+
import importFile from "../lib/importFile.js";
|
|
36
|
+
import emitWebsocketEvent from "../websockets/emitWebsocketEvent.js";
|
|
37
|
+
import getTargetDatabaseConnection from "./databases/getTargetDatabaseConnection.js";
|
|
38
|
+
process.setMaxListeners(0);
|
|
39
|
+
class App {
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.setMachineId();
|
|
42
|
+
this.setJoystickProcessId();
|
|
43
|
+
handleProcessErrors(options?.events);
|
|
44
|
+
const HMRSessions = JSON.parse(process.env.HMR_SESSIONS || "{}");
|
|
45
|
+
this.sessions = new Map(HMRSessions ? Object.entries(HMRSessions) : []);
|
|
46
|
+
this.databases = [];
|
|
47
|
+
this.express = {};
|
|
48
|
+
this.options = options || {};
|
|
49
|
+
}
|
|
50
|
+
async start(options = {}) {
|
|
51
|
+
await this.invalidateCache();
|
|
52
|
+
this.databases = await this.loadDatabases();
|
|
53
|
+
this.express = initExpress(this.onStartApp, options, this);
|
|
54
|
+
this.initWebsockets(options?.websockets || {});
|
|
55
|
+
this.initDevelopmentRoutes();
|
|
56
|
+
this.initAccounts();
|
|
57
|
+
this.initTests();
|
|
58
|
+
this.initDeploy();
|
|
59
|
+
this.initAPI(options?.api);
|
|
60
|
+
this.initRoutes(options?.routes);
|
|
61
|
+
this.initUploaders(options?.uploaders);
|
|
62
|
+
this.initFixtures(options?.fixtures);
|
|
63
|
+
this.initQueues(options?.queues);
|
|
64
|
+
this.initCronJobs(options?.cronJobs);
|
|
65
|
+
if (process.env.NODE_ENV === "development") {
|
|
66
|
+
process.on("message", (message) => {
|
|
67
|
+
const parsedMessage = typeof message === "string" ? JSON.parse(message) : message;
|
|
68
|
+
if (parsedMessage?.type === " RESTART_SERVER") {
|
|
69
|
+
this.express?.server?.close();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async invalidateCache() {
|
|
75
|
+
const uiFiles = fs.existsSync(`${getBuildPath()}ui`) ? await readDirectory(`${getBuildPath()}ui`) : [];
|
|
76
|
+
const cacheFiles = uiFiles?.filter((filePath) => filePath?.includes("_cache"));
|
|
77
|
+
for (let i = 0; i < cacheFiles?.length; i += 1) {
|
|
78
|
+
await fs.promises.unlink(cacheFiles[i]);
|
|
79
|
+
}
|
|
80
|
+
return Promise.resolve();
|
|
81
|
+
}
|
|
82
|
+
async loadDatabases(callback) {
|
|
83
|
+
const settings = loadSettings();
|
|
84
|
+
const hasUsersDatabase = settings?.config?.databases?.some((database = {}) => {
|
|
85
|
+
return !!database?.users;
|
|
86
|
+
});
|
|
87
|
+
const hasQueuesDatabase = settings?.config?.databases?.some((database = {}) => {
|
|
88
|
+
return !!database?.queues;
|
|
89
|
+
});
|
|
90
|
+
const hasPostgreSQLUsersDatabase = settings?.config?.databases?.some((database = {}) => {
|
|
91
|
+
return database?.provider === "postgresql" && database?.users;
|
|
92
|
+
});
|
|
93
|
+
const databases = settings?.config?.databases?.map((database) => {
|
|
94
|
+
return {
|
|
95
|
+
provider: database?.provider,
|
|
96
|
+
settings: database
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
for (let databaseIndex = 0; databaseIndex < databases?.length; databaseIndex += 1) {
|
|
100
|
+
const database = databases[databaseIndex];
|
|
101
|
+
const hasMultipleOfProvider = databases?.filter((database2) => database2?.provider === database2?.provider)?.length > 1;
|
|
102
|
+
const databasePort = parseInt(process.env.PORT, 10) + 10 + databaseIndex;
|
|
103
|
+
if (database?.provider === "mongodb") {
|
|
104
|
+
const mongodb = await connectMongoDB(database?.settings, databasePort);
|
|
105
|
+
process.databases = {
|
|
106
|
+
...process.databases || {},
|
|
107
|
+
mongodb: !hasMultipleOfProvider ? mongodb : {
|
|
108
|
+
...process?.databases?.mongodb || {},
|
|
109
|
+
[database?.settings?.name || `mongodb_${databasePort}`]: mongodb
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (database?.provider === "postgresql") {
|
|
114
|
+
const postgresql = await connectPostgreSQL(database?.settings, databasePort);
|
|
115
|
+
process.databases = {
|
|
116
|
+
...process.databases || {},
|
|
117
|
+
postgresql: !hasMultipleOfProvider ? {
|
|
118
|
+
...postgresql?.pool,
|
|
119
|
+
query: postgresql?.query
|
|
120
|
+
} : {
|
|
121
|
+
...process?.databases?.postgresql || {},
|
|
122
|
+
[database?.settings?.name || `postgresql_${databasePort}`]: {
|
|
123
|
+
...postgresql?.pool,
|
|
124
|
+
query: postgresql?.query
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (hasUsersDatabase) {
|
|
131
|
+
process.databases._users = getTargetDatabaseConnection("users")?.connection;
|
|
132
|
+
}
|
|
133
|
+
if (hasQueuesDatabase) {
|
|
134
|
+
process.databases._queues = getTargetDatabaseConnection("queues")?.connection;
|
|
135
|
+
}
|
|
136
|
+
if (hasPostgreSQLUsersDatabase) {
|
|
137
|
+
await createPostgreSQLAccountsTables();
|
|
138
|
+
await createPostgreSQLAccountsIndexes();
|
|
139
|
+
}
|
|
140
|
+
return process.databases;
|
|
141
|
+
}
|
|
142
|
+
onStartApp(express = {}) {
|
|
143
|
+
process.on("message", (message) => {
|
|
144
|
+
if (typeof message === "string") {
|
|
145
|
+
process.BUILD_ERROR = JSON.parse(message);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (process.env.NODE_ENV !== "test") {
|
|
149
|
+
console.log(`App running at: http://localhost:${express.port}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
setMachineId() {
|
|
153
|
+
generateMachineId();
|
|
154
|
+
}
|
|
155
|
+
setJoystickProcessId() {
|
|
156
|
+
if (!fs.existsSync("./.joystick")) {
|
|
157
|
+
fs.mkdirSync("./.joystick");
|
|
158
|
+
}
|
|
159
|
+
if (!fs.existsSync("./.joystick/PROCESS_ID")) {
|
|
160
|
+
fs.writeFileSync("./.joystick/PROCESS_ID", `${generateId(32)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
initTests() {
|
|
164
|
+
if (process.env.NODE_ENV === "test") {
|
|
165
|
+
this.express.app.get("/api/_test/bootstrap", async (req, res) => {
|
|
166
|
+
const Component = req?.query?.pathToComponent ? await importFile(`./.joystick/build/${req?.query?.pathToComponent}`) : null;
|
|
167
|
+
if (Component) {
|
|
168
|
+
const componentInstance = Component();
|
|
169
|
+
console.log(componentInstance);
|
|
170
|
+
}
|
|
171
|
+
res.status(200).send({
|
|
172
|
+
ping: "pong",
|
|
173
|
+
query: req?.query
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
initDeploy() {
|
|
179
|
+
if (process.env.NODE_ENV === "production" && process.env.IS_PUSH_DEPLOYED) {
|
|
180
|
+
this.express.app.get("/api/_push/pre-version", async (req, res) => {
|
|
181
|
+
const instanceToken = fs.readFileSync("/root/token.txt", "utf-8");
|
|
182
|
+
if (req?.headers["x-instance-token"] === instanceToken?.replace("\n", "")) {
|
|
183
|
+
if (this.options?.events?.onBeforeDeployment && typeof this.options?.events?.onBeforeDeployment === "function") {
|
|
184
|
+
await this.options.events.onBeforeDeployment(req?.query?.instance || "", req?.query?.version);
|
|
185
|
+
return res.status(200).send("ok");
|
|
186
|
+
}
|
|
187
|
+
return res.status(200).send("ok");
|
|
188
|
+
}
|
|
189
|
+
return res.status(403).send("Sorry, you must pass a valid instance token to access this endpoint.");
|
|
190
|
+
});
|
|
191
|
+
this.express.app.get("/api/_push/health", async (req, res) => {
|
|
192
|
+
const instanceToken = fs.readFileSync("/root/token.txt", "utf-8");
|
|
193
|
+
if (req?.headers["x-instance-token"] === instanceToken?.replace("\n", "")) {
|
|
194
|
+
return res.status(200).send("ok");
|
|
195
|
+
}
|
|
196
|
+
return res.status(403).send("Sorry, you must pass a valid instance token to access this endpoint.");
|
|
197
|
+
});
|
|
198
|
+
this.express.app.get("/api/_push/logs", async (req, res) => {
|
|
199
|
+
const instanceToken = fs.readFileSync("/root/token.txt", "utf-8");
|
|
200
|
+
if (req?.headers["x-instance-token"] === instanceToken?.replace("\n", "")) {
|
|
201
|
+
const logs = execSync(`export NODE_ENV=production && instance logs${req?.query?.before ? ` --before ${req?.query?.before}` : ""}${req?.query?.after ? ` --after ${req?.query?.after}` : ""}`);
|
|
202
|
+
return res.status(200).send(logs);
|
|
203
|
+
}
|
|
204
|
+
return res.status(403).send("Sorry, you must pass a valid instance token to access this endpoint.");
|
|
205
|
+
});
|
|
206
|
+
this.express.app.get("/api/_push/metrics", async (req, res) => {
|
|
207
|
+
const instanceToken = fs.readFileSync("/root/token.txt", "utf-8");
|
|
208
|
+
if (req?.headers["x-instance-token"] === instanceToken?.replace("\n", "")) {
|
|
209
|
+
const metrics = execSync(`export NODE_ENV=production && instance metrics`);
|
|
210
|
+
return res.status(200).send(metrics);
|
|
211
|
+
}
|
|
212
|
+
return res.status(403).send("Sorry, you must pass a valid instance token to access this endpoint.");
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
initAPI(api = {}) {
|
|
217
|
+
const getters = api?.getters;
|
|
218
|
+
const setters = api?.setters;
|
|
219
|
+
const options = api?.options;
|
|
220
|
+
const context = api?.context;
|
|
221
|
+
if (getters && isObject(getters) && Object.keys(getters).length > 0) {
|
|
222
|
+
registerGetters(this.express, Object.entries(getters), context, options, this);
|
|
223
|
+
}
|
|
224
|
+
if (setters && isObject(setters) && Object.keys(setters).length > 0) {
|
|
225
|
+
registerSetters(this.express, Object.entries(setters), context, options, this);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
initRoutes(routes = {}) {
|
|
229
|
+
Object.entries(routes).forEach(([path, callback]) => {
|
|
230
|
+
const isObjectBasedRoute = path && callback && typeof callback === "object";
|
|
231
|
+
const isFunctionBasedRoute = path && callback && typeof callback === "function";
|
|
232
|
+
const method = callback?.method?.toLowerCase();
|
|
233
|
+
const methods = callback?.methods?.map((method2) => method2?.toLowerCase());
|
|
234
|
+
const methodsForRoute = method ? [method] : methods;
|
|
235
|
+
const invalidMethods = isObjectBasedRoute ? methodsForRoute.filter((method2) => !supportedHTTPMethods.includes(method2)) : [];
|
|
236
|
+
const isValidMethod = Array.isArray(methodsForRoute) && invalidMethods.length === 0;
|
|
237
|
+
const isValidHandler = isFunctionBasedRoute && typeof callback === "function" || isObjectBasedRoute && callback && callback.handler && typeof callback.handler === "function";
|
|
238
|
+
if (isFunctionBasedRoute && !isValidHandler) {
|
|
239
|
+
log(`Cannot register route ${path}. When defining a route using the function-based pattern, route must be set to a function.`, {
|
|
240
|
+
level: "danger",
|
|
241
|
+
docs: "https://github.com/cheatcode/joystick#defining-routes"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (isObjectBasedRoute && !isValidHandler) {
|
|
245
|
+
log(`Cannot register route ${path}. When defining a route using the object-based pattern, the handler property must be set to a function.`, {
|
|
246
|
+
level: "danger",
|
|
247
|
+
docs: "https://github.com/cheatcode/joystick#defining-routes-for-specific-http-methods"
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (isObjectBasedRoute && invalidMethods.length > 0) {
|
|
251
|
+
log(`Cannot register route ${path}. When defining a route using the object-based pattern, you can only set method (single HTTP method as a string) or methods (array of HTTP methods as strings), not both.`, {
|
|
252
|
+
level: "danger",
|
|
253
|
+
docs: "https://github.com/cheatcode/joystick#defining-routes-for-specific-http-methods"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (isObjectBasedRoute && method && methods) {
|
|
257
|
+
log(`Cannot register route ${path}. When defining a route using the object-based pattern, you can only set method (single HTTP method as a string) or methods (array of HTTP methods as strings), not both.`, {
|
|
258
|
+
level: "danger",
|
|
259
|
+
docs: "https://github.com/cheatcode/joystick#defining-routes-for-specific-http-methods"
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (isObjectBasedRoute && !method && !methods) {
|
|
263
|
+
log(`Cannot register route ${path}. When defining a route using the object-based pattern, you must pass a method (single HTTP method as a string) or methods (array of HTTP methods as strings) for the route.`, {
|
|
264
|
+
level: "danger",
|
|
265
|
+
docs: "https://github.com/cheatcode/joystick#defining-routes-for-specific-http-methods"
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (isObjectBasedRoute && method && !isValidMethod) {
|
|
269
|
+
log(`Cannot register route ${path}. When defining a route using the object-based pattern, the method property must be set to a valid HTTP method: ${supportedHTTPMethods.join(", ")}.`, {
|
|
270
|
+
level: "danger",
|
|
271
|
+
docs: "https://github.com/cheatcode/joystick#defining-routes-for-specific-http-methods"
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (isObjectBasedRoute && methods && !isValidMethod) {
|
|
275
|
+
log(`Cannot register route ${path}. When defining a route using the object-based pattern, the methods property must be set to an array of valid HTTP methods: ${supportedHTTPMethods.join(", ")}.`, {
|
|
276
|
+
level: "danger",
|
|
277
|
+
docs: "https://github.com/cheatcode/joystick#defining-routes-for-specific-http-methods"
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (isObjectBasedRoute && callback && !callback.handler) {
|
|
281
|
+
log(`Cannot register route ${path}. When defining a route using the object-based pattern, the handler property must be set to a function.`, {
|
|
282
|
+
level: "danger",
|
|
283
|
+
docs: "https://github.com/cheatcode/joystick#defining-routes-for-specific-http-methods"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (isObjectBasedRoute && methodsForRoute && isValidMethod && callback && callback.handler) {
|
|
287
|
+
methodsForRoute.forEach((method2) => {
|
|
288
|
+
this.express.app[method2](path, ...[
|
|
289
|
+
...Array.isArray(callback?.middleware) ? callback?.middleware : [],
|
|
290
|
+
async (req, res, next) => {
|
|
291
|
+
callback.handler(Object.assign(req, {
|
|
292
|
+
context: {
|
|
293
|
+
...req?.context || {},
|
|
294
|
+
ifLoggedIn: (redirectPath = "", callback2 = null) => {
|
|
295
|
+
if (!!req?.context?.user && redirectPath) {
|
|
296
|
+
return res.redirect(redirectPath);
|
|
297
|
+
}
|
|
298
|
+
if (callback2) {
|
|
299
|
+
return callback2();
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
ifNotLoggedIn: (redirectPath = "", callback2 = null) => {
|
|
303
|
+
if (!req?.context?.user && redirectPath) {
|
|
304
|
+
return res.redirect(redirectPath);
|
|
305
|
+
}
|
|
306
|
+
if (callback2) {
|
|
307
|
+
return callback2();
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
...process.databases || {}
|
|
311
|
+
}
|
|
312
|
+
}), res, next);
|
|
313
|
+
}
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (isFunctionBasedRoute) {
|
|
318
|
+
this.express.app.get(path, (req, res, next) => {
|
|
319
|
+
callback(Object.assign(req, {
|
|
320
|
+
context: {
|
|
321
|
+
...req?.context || {},
|
|
322
|
+
ifLoggedIn: (redirectPath = "", callback2 = null) => {
|
|
323
|
+
if (!!req?.context?.user && redirectPath) {
|
|
324
|
+
return res.redirect(redirectPath);
|
|
325
|
+
}
|
|
326
|
+
if (callback2) {
|
|
327
|
+
return callback2();
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
ifNotLoggedIn: (redirectPath = "", callback2 = null) => {
|
|
331
|
+
if (!req?.context?.user && redirectPath) {
|
|
332
|
+
return res.redirect(redirectPath);
|
|
333
|
+
}
|
|
334
|
+
if (callback2) {
|
|
335
|
+
return callback2();
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
...process.databases || {}
|
|
339
|
+
}
|
|
340
|
+
}), res, next);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
initWebsockets(userWebsockets = {}) {
|
|
346
|
+
const websocketServers = {
|
|
347
|
+
uploaders: {
|
|
348
|
+
server: new WebSocket.WebSocketServer({
|
|
349
|
+
noServer: true,
|
|
350
|
+
path: "/api/_websockets/uploaders"
|
|
351
|
+
})
|
|
352
|
+
},
|
|
353
|
+
...Object.entries(userWebsockets).reduce((definitions = {}, [userWebsocketName, userWebsocketDefinition]) => {
|
|
354
|
+
definitions[userWebsocketName] = {
|
|
355
|
+
server: new WebSocket.WebSocketServer({
|
|
356
|
+
noServer: true,
|
|
357
|
+
path: `/api/_websockets/${userWebsocketName}`
|
|
358
|
+
}),
|
|
359
|
+
onOpen: userWebsocketDefinition?.onOpen || null,
|
|
360
|
+
onMessage: userWebsocketDefinition?.onMessage || null,
|
|
361
|
+
onClose: userWebsocketDefinition?.onClose || null
|
|
362
|
+
};
|
|
363
|
+
return definitions;
|
|
364
|
+
}, {})
|
|
365
|
+
};
|
|
366
|
+
Object.entries(websocketServers).forEach(([websocketName, websocketDefinition]) => {
|
|
367
|
+
websocketDefinition.server.on("connection", function connection(websocketConnection, connectionRequest) {
|
|
368
|
+
try {
|
|
369
|
+
const [_path, params] = connectionRequest?.url?.split("?");
|
|
370
|
+
const connectionParams = queryString.parse(params);
|
|
371
|
+
const emitter = new EventEmitter();
|
|
372
|
+
const emitterId = connectionParams?.id ? `${websocketName}_${connectionParams?.id}` : websocketName;
|
|
373
|
+
if (joystick?.emitters[emitterId]) {
|
|
374
|
+
joystick.emitters[emitterId].push(emitter);
|
|
375
|
+
} else {
|
|
376
|
+
joystick.emitters = {
|
|
377
|
+
...joystick?.emitters || {},
|
|
378
|
+
[emitterId]: [emitter]
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const connection2 = Object.assign(websocketConnection, {
|
|
382
|
+
params: connectionParams
|
|
383
|
+
});
|
|
384
|
+
if (websocketDefinition?.onOpen) {
|
|
385
|
+
websocketDefinition.onOpen(connection2);
|
|
386
|
+
}
|
|
387
|
+
websocketConnection.on("message", (message) => {
|
|
388
|
+
const parsedMessage = JSON.parse(message);
|
|
389
|
+
if (websocketDefinition.onMessage) {
|
|
390
|
+
websocketDefinition.onMessage(parsedMessage, connection2);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
websocketConnection.on("close", (code = 0, reason = "") => {
|
|
394
|
+
if (websocketDefinition?.onClose) {
|
|
395
|
+
websocketDefinition.onClose(code, reason?.toString(), connection2);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
emitter.on("message", (message = {}) => {
|
|
399
|
+
websocketConnection.send(JSON.stringify(message));
|
|
400
|
+
});
|
|
401
|
+
emitter.on("progress", (progress = {}) => {
|
|
402
|
+
websocketConnection.send(JSON.stringify({ type: "PROGRESS", ...progress }));
|
|
403
|
+
});
|
|
404
|
+
} catch (exception) {
|
|
405
|
+
console.warn(exception);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
this.express.server.on("upgrade", (request, socket, head) => {
|
|
410
|
+
if (!request?.url?.includes("/api/_websockets/")) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const websocketName = (request?.url?.replace("/api/_websockets/", "").split("?") || [])[0];
|
|
414
|
+
const websocket = websocketServers[websocketName];
|
|
415
|
+
if (websocket) {
|
|
416
|
+
websocket.server.handleUpgrade(request, socket, head, (socket2) => {
|
|
417
|
+
websocket.server.emit("connection", socket2, request);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
initDevelopmentRoutes() {
|
|
423
|
+
if (["development", "test"].includes(process.env.NODE_ENV)) {
|
|
424
|
+
this.express.app.get("/api/_joystick/sessions", async (req, res) => {
|
|
425
|
+
const sessions = Array.from(this.sessions.entries())?.reduce((acc = {}, [key, value]) => {
|
|
426
|
+
acc[key] = value;
|
|
427
|
+
return acc;
|
|
428
|
+
}, {});
|
|
429
|
+
res.status(200).send(JSON.stringify(sessions));
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
initAccounts() {
|
|
434
|
+
this.express.app.get("/api/_accounts/authenticated", async (req, res) => {
|
|
435
|
+
const loginTokenHasExpired = await hasLoginTokenExpired(res, req?.cookies?.joystickLoginToken, req?.cookies?.joystickLoginTokenExpiresAt);
|
|
436
|
+
const status = !loginTokenHasExpired ? 200 : 401;
|
|
437
|
+
return res.status(status).send(JSON.stringify({ status, authenticated: !loginTokenHasExpired }));
|
|
438
|
+
});
|
|
439
|
+
this.express.app.get("/api/_accounts/user", async (req, res) => {
|
|
440
|
+
const loginTokenHasExpired = await hasLoginTokenExpired(res, req?.cookies?.joystickLoginToken, req?.cookies?.joystickLoginTokenExpiresAt);
|
|
441
|
+
const status = !loginTokenHasExpired ? 200 : 401;
|
|
442
|
+
const user = getOutput(req?.context?.user, req?.body?.output || defaultUserOutputFields);
|
|
443
|
+
return res.status(status).send(JSON.stringify({ status, user }));
|
|
444
|
+
});
|
|
445
|
+
this.express.app.post("/api/_accounts/signup", async (req, res) => {
|
|
446
|
+
try {
|
|
447
|
+
const signup = await accounts.signup({
|
|
448
|
+
emailAddress: req?.body?.emailAddress,
|
|
449
|
+
password: req?.body?.password,
|
|
450
|
+
metadata: req?.body?.metadata,
|
|
451
|
+
output: req?.body?.output || defaultUserOutputFields
|
|
452
|
+
});
|
|
453
|
+
accounts._setAuthenticationCookie(res, {
|
|
454
|
+
token: signup?.token,
|
|
455
|
+
tokenExpiresAt: signup?.tokenExpiresAt
|
|
456
|
+
});
|
|
457
|
+
res.status(200).send(JSON.stringify(signup?.user || {}));
|
|
458
|
+
} catch (exception) {
|
|
459
|
+
console.log(exception);
|
|
460
|
+
return res.status(500).send(JSON.stringify({
|
|
461
|
+
errors: [formatAPIError(exception, "server")]
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
this.express.app.post("/api/_accounts/login", async (req, res) => {
|
|
466
|
+
try {
|
|
467
|
+
const login = await accounts.login({
|
|
468
|
+
emailAddress: req?.body?.emailAddress,
|
|
469
|
+
username: req?.body?.username,
|
|
470
|
+
password: req?.body?.password,
|
|
471
|
+
output: req?.body?.output || defaultUserOutputFields
|
|
472
|
+
});
|
|
473
|
+
accounts._setAuthenticationCookie(res, {
|
|
474
|
+
token: login?.token,
|
|
475
|
+
tokenExpiresAt: login?.tokenExpiresAt
|
|
476
|
+
});
|
|
477
|
+
res.status(200).send(JSON.stringify(login?.user || {}));
|
|
478
|
+
} catch (exception) {
|
|
479
|
+
console.log(exception);
|
|
480
|
+
return res.status(500).send(JSON.stringify({
|
|
481
|
+
errors: [formatAPIError(exception, "server")]
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
this.express.app.post("/api/_accounts/logout", async (req, res) => {
|
|
486
|
+
try {
|
|
487
|
+
this.sessions.delete(req?.context?.user?._id || req?.context?.user?.user_id);
|
|
488
|
+
accounts._unsetAuthenticationCookie(res);
|
|
489
|
+
res.status(200).send(JSON.stringify({}));
|
|
490
|
+
} catch (exception) {
|
|
491
|
+
console.log(exception);
|
|
492
|
+
return res.status(500).send(JSON.stringify({
|
|
493
|
+
errors: [formatAPIError(exception, "server")]
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
this.express.app.post("/api/_accounts/recover-password", async (req, res) => {
|
|
498
|
+
try {
|
|
499
|
+
await accounts.recoverPassword({
|
|
500
|
+
emailAddress: req?.body?.emailAddress
|
|
501
|
+
});
|
|
502
|
+
res.status(200).send(JSON.stringify({}));
|
|
503
|
+
} catch (exception) {
|
|
504
|
+
console.log(exception);
|
|
505
|
+
return res.status(500).send(JSON.stringify({
|
|
506
|
+
errors: [formatAPIError(exception, "server")]
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
this.express.app.post("/api/_accounts/reset-password", async (req, res) => {
|
|
511
|
+
try {
|
|
512
|
+
const reset = await accounts.resetPassword({
|
|
513
|
+
token: req?.body?.token,
|
|
514
|
+
password: req?.body?.password,
|
|
515
|
+
output: req?.body?.output || defaultUserOutputFields
|
|
516
|
+
});
|
|
517
|
+
accounts._setAuthenticationCookie(res, {
|
|
518
|
+
token: reset?.token,
|
|
519
|
+
tokenExpiresAt: reset?.tokenExpiresAt
|
|
520
|
+
});
|
|
521
|
+
res.status(200).send(JSON.stringify(reset?.user || {}));
|
|
522
|
+
} catch (exception) {
|
|
523
|
+
console.log(exception);
|
|
524
|
+
return res.status(500).send(JSON.stringify({
|
|
525
|
+
errors: [formatAPIError(exception, "server")]
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
this.express.app.post("/api/_accounts/verify-email", async (req, res) => {
|
|
530
|
+
try {
|
|
531
|
+
await accounts.verifyEmail({
|
|
532
|
+
token: req?.query?.token
|
|
533
|
+
});
|
|
534
|
+
res.redirect("/");
|
|
535
|
+
} catch (exception) {
|
|
536
|
+
console.log(exception);
|
|
537
|
+
return res.status(500).send(JSON.stringify({
|
|
538
|
+
errors: [formatAPIError(exception, "server")]
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
initUploaders(uploaders = {}) {
|
|
544
|
+
const { app } = this.express;
|
|
545
|
+
Object.entries(uploaders).forEach(([uploaderName, uploaderOptions]) => {
|
|
546
|
+
const errors = validateUploaderOptions(uploaderOptions);
|
|
547
|
+
if (errors?.length > 0) {
|
|
548
|
+
log(errors, {
|
|
549
|
+
level: "danger",
|
|
550
|
+
docs: "https://github.com/cheatcode/joystick#uploaders"
|
|
551
|
+
});
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (errors?.length === 0) {
|
|
555
|
+
const formattedUploaderName = getAPIURLComponent(uploaderName);
|
|
556
|
+
const upload = multer();
|
|
557
|
+
const multerMiddleware = upload.array("files", 12);
|
|
558
|
+
app.post(`/api/_uploaders/${formattedUploaderName}`, (req, res, next) => {
|
|
559
|
+
let progress = 0;
|
|
560
|
+
const fileSize = parseInt(req.headers["content-length"], 10);
|
|
561
|
+
const providers = uploaderOptions?.providers?.includes("local") ? uploaderOptions?.providers.length : uploaderOptions?.providers?.length + 1;
|
|
562
|
+
const totalFileSizeAllProviders = fileSize * providers;
|
|
563
|
+
req.on("data", (chunk) => {
|
|
564
|
+
progress += chunk.length;
|
|
565
|
+
const percentage = Math.round(progress / totalFileSizeAllProviders * 100);
|
|
566
|
+
emitWebsocketEvent(`uploaders_${req?.headers["x-joystick-upload-id"]}`, "progress", { provider: "uploadToServer", progress: percentage });
|
|
567
|
+
});
|
|
568
|
+
next();
|
|
569
|
+
}, multerMiddleware, async (req, res) => {
|
|
570
|
+
const input = req?.headers["x-joystick-upload-input"] ? JSON.parse(req?.headers["x-joystick-upload-input"]) : {};
|
|
571
|
+
validateUploads({
|
|
572
|
+
files: req?.files,
|
|
573
|
+
input,
|
|
574
|
+
uploaderName,
|
|
575
|
+
uploaderOptions
|
|
576
|
+
}).then(async (validatedUploads = []) => {
|
|
577
|
+
if (typeof uploaderOptions?.onBeforeUpload === "function") {
|
|
578
|
+
await uploaderOptions?.onBeforeUpload({
|
|
579
|
+
input,
|
|
580
|
+
req,
|
|
581
|
+
uploads: validatedUploads
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
const fileSize = parseInt(req.headers["content-length"], 10);
|
|
585
|
+
const providers = uploaderOptions?.providers?.includes("local") ? uploaderOptions?.providers.length : uploaderOptions?.providers?.length + 1;
|
|
586
|
+
const totalFileSizeAllProviders = fileSize * providers;
|
|
587
|
+
const uploads = await runUploader({
|
|
588
|
+
alreadyUploaded: fileSize,
|
|
589
|
+
totalFileSizeAllProviders,
|
|
590
|
+
uploads: validatedUploads,
|
|
591
|
+
input,
|
|
592
|
+
req
|
|
593
|
+
});
|
|
594
|
+
if (typeof uploaderOptions?.onAfterUpload === "function") {
|
|
595
|
+
await uploaderOptions?.onAfterUpload({
|
|
596
|
+
input,
|
|
597
|
+
req,
|
|
598
|
+
uploads
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
res.status(200).send(JSON.stringify({
|
|
602
|
+
status: 200,
|
|
603
|
+
uploads
|
|
604
|
+
}));
|
|
605
|
+
}).catch((errors2) => {
|
|
606
|
+
if (typeof errors2 === "string") {
|
|
607
|
+
return res.status(403).send(JSON.stringify({
|
|
608
|
+
errors: [formatAPIError(new Error(errors2))]
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
611
|
+
if (errors2 instanceof Error) {
|
|
612
|
+
return res.status(403).send(JSON.stringify({
|
|
613
|
+
errors: [formatAPIError(errors2)]
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
return res.status(403).send(JSON.stringify({
|
|
617
|
+
errors: (errors2 || []).map((error) => {
|
|
618
|
+
return formatAPIError(new Error(error?.message, "validation"));
|
|
619
|
+
})
|
|
620
|
+
}));
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
initFixtures(fixtures = null) {
|
|
627
|
+
if (fixtures && typeof fixtures === "function") {
|
|
628
|
+
fixtures();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
initQueues(queues = null) {
|
|
632
|
+
if (queues && typeof queues === "object" && !Array.isArray(queues)) {
|
|
633
|
+
const queueDefinitions = Object.entries(queues);
|
|
634
|
+
for (let i = 0; i < queueDefinitions.length; i += 1) {
|
|
635
|
+
const [queueName, queueOptions] = queueDefinitions[i];
|
|
636
|
+
process.queues = {
|
|
637
|
+
...process.queues || {},
|
|
638
|
+
[queueName]: new Queue(queueName, queueOptions)
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
initCronJobs(cronJobs = null) {
|
|
644
|
+
if (cronJobs && typeof cronJobs === "object" && !Array.isArray(cronJobs)) {
|
|
645
|
+
const cronJobDefinitions = Object.entries(cronJobs);
|
|
646
|
+
for (let i = 0; i < cronJobDefinitions.length; i += 1) {
|
|
647
|
+
const [cronJobName, cronJobOptions] = cronJobDefinitions[i];
|
|
648
|
+
if (cronJobOptions?.schedule && cronJobOptions?.run && typeof cronJobOptions?.run === "function") {
|
|
649
|
+
process.cron = {
|
|
650
|
+
...process.queues || {},
|
|
651
|
+
[cronJobName]: cron.schedule(cronJobOptions?.schedule, () => {
|
|
652
|
+
if (cronJobOptions?.logAtRunTime && typeof cronJobOptions?.logAtRunTime === "string") {
|
|
653
|
+
console.log(cronJobOptions.logAtRunTime);
|
|
654
|
+
}
|
|
655
|
+
cronJobOptions.run();
|
|
656
|
+
})
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
var app_default = (options = {}) => {
|
|
664
|
+
return new Promise(async (resolve) => {
|
|
665
|
+
const app = new App(options);
|
|
666
|
+
await app.start(options);
|
|
667
|
+
return resolve(app.express);
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
export {
|
|
671
|
+
App,
|
|
672
|
+
app_default as default
|
|
673
|
+
};
|