@swizzyweb/swerve-manager 0.1.3

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,87 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import process from "node:process";
4
+ import { mkdirSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ export async function installWebService(appName, importPathOrName, port, expressApp, serviceArgs, gLogger) {
7
+ const packageName = importPathOrName;
8
+ try {
9
+ gLogger.info(`Getting webservice package ${packageName} and will run on port ${port}`);
10
+ gLogger.debug(`Getting tool with path: ${importPathOrName}`);
11
+ const fullPath = importPathOrName; //await getFullImportPath(importPathOrName);
12
+ const tool = await import(fullPath); //require(fullPath); //require(packageName as string);
13
+ gLogger.debug(`Got service with require: ${JSON.stringify(tool)}`);
14
+ gLogger.debug(`Getting web service from tool...`);
15
+ const appDataPath = path.join(serviceArgs.appDataRoot, "appdata", appName);
16
+ mkdirSync(appDataPath, { recursive: true });
17
+ const logger = getLoggerForService(serviceArgs, serviceArgs.appName, port, gLogger);
18
+ gLogger.debug(`serviceArgs for ${packageName}: ${serviceArgs}`);
19
+ const service = await tool.getWebservice({
20
+ appDataPath,
21
+ ...serviceArgs,
22
+ port,
23
+ app: expressApp,
24
+ packageName,
25
+ serviceArgs: { ...serviceArgs },
26
+ logger,
27
+ });
28
+ logger.debug(`Got web service`);
29
+ gLogger.debug(`Installing web service...`);
30
+ await service.install({});
31
+ gLogger.debug(`Installed web service ${packageName}`);
32
+ return service;
33
+ }
34
+ catch (e) {
35
+ const exceptionMessage = `exception: ${e}
36
+ Failed to install web service, is it installed with NPM? Check package exists in node_modules
37
+ To add, run:
38
+ npm install ${packageName ?? "packageName"}
39
+ args:
40
+ packageName: ${packageName}
41
+ port: ${port}
42
+ `;
43
+ // ${getHelpText}`;
44
+ gLogger.error(`Failed to install web service`);
45
+ throw Error(exceptionMessage); //new Error(exceptionMessage);
46
+ }
47
+ }
48
+ export async function getFullImportPath(importPathOrName) {
49
+ const importPath = importPathOrName.startsWith(".")
50
+ ? path.join(process.cwd(), importPathOrName)
51
+ : importPathOrName;
52
+ let fullPath;
53
+ if (importPathOrName === importPath) {
54
+ const fullUrl = import.meta.resolve(importPath, import.meta.url);
55
+ fullPath = fileURLToPath(fullUrl);
56
+ // const pkg = await import(fullUrl, {
57
+ // assert: { type: "json" },
58
+ // });
59
+ // await require.resolve(importPath, {
60
+ // paths: [process.cwd()],
61
+ // });
62
+ }
63
+ else {
64
+ fullPath = importPath;
65
+ }
66
+ return fullPath;
67
+ }
68
+ export function getLoggerForService(serviceArgs, appName, port, gLogger) {
69
+ const logLevel = serviceArgs.logLevel ?? gLogger.getLoggerProps().logLevel;
70
+ const logFileName = serviceArgs.logFileName ?? undefined;
71
+ const ownerName = appName;
72
+ const pid = process.pid;
73
+ const hostName = os.hostname();
74
+ const logDir = serviceArgs.logDir;
75
+ const appDataRoot = serviceArgs.appDataRoot;
76
+ return gLogger.clone({
77
+ port,
78
+ appName,
79
+ appDataRoot,
80
+ logDir,
81
+ hostName,
82
+ pid,
83
+ logLevel,
84
+ ownerName,
85
+ logFileName,
86
+ });
87
+ }
package/jest.config.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ preset: "ts-jest",
3
+ testEnvironment: "node",
4
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@swizzyweb/swerve-manager",
3
+ "version": "0.1.3",
4
+ "description": "swizzy-swerve is a bootstrapper for swizzy web services. This package will bootstrap and run independent swizzy web services.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "server:ts": "ts-node ./src/bootstrap.ts",
11
+ "server": "node dist/bootstrap.js",
12
+ "test": "node --test ./test**/*.spec.ts",
13
+ "coverage": "jest --coverage"
14
+ },
15
+ "author": "Jason Gallagher",
16
+ "license": "Apache-2.0",
17
+ "devDependencies": {
18
+ "@swizzyweb/express": "^4.19.2",
19
+ "@types/jest": "^29.5.14",
20
+ "@types/node": "^22.7.7",
21
+ "jest": "^29.7.0",
22
+ "ts-jest": "^29.3.2",
23
+ "typescript": "^5.6.3"
24
+ },
25
+ "dependencies": {
26
+ "@swizzyweb/swerve-manager": "^0.1.2",
27
+ "@swizzyweb/swizzy-common": "^0.3.2",
28
+ "@swizzyweb/swizzy-web-service": "^0.5.3",
29
+ "bun": "^1.2.15",
30
+ "deno": "^2.3.6",
31
+ "ts-node": "^10.9.2"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/swizzyweb/swerve.git"
36
+ },
37
+ "keywords": [
38
+ "swizzy",
39
+ "dyn",
40
+ "serve",
41
+ "bootsrap",
42
+ "web",
43
+ "service",
44
+ "runner"
45
+ ],
46
+ "bugs": {
47
+ "url": "https://github.com/swizzyweb/swerve/issues"
48
+ },
49
+ "homepage": "https://github.com/swizzyweb/swerve#readme"
50
+ }
@@ -0,0 +1,56 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { IConfig } from "./config.js";
3
+ import path from "node:path";
4
+
5
+ export interface IAsyncConfigParser<CONFIG> {
6
+ parse(path: string): Promise<CONFIG>;
7
+ }
8
+
9
+ export interface ISwerveConfigException {
10
+ message: string;
11
+ stack: any;
12
+ }
13
+
14
+ export class SwerveConfigException implements ISwerveConfigException {
15
+ message: string;
16
+ stack: any;
17
+ constructor(message: string) {
18
+ this.message = message;
19
+ this.stack = new Error(message).stack;
20
+ }
21
+ }
22
+
23
+ export class SwerveConfigParser implements IAsyncConfigParser<IConfig> {
24
+ async parse(configPath: string): Promise<IConfig> {
25
+ let actualPath;
26
+ if (configPath.startsWith(".")) {
27
+ actualPath = path.join(configPath);
28
+ } else {
29
+ actualPath = configPath;
30
+ }
31
+ const content = await readFile(actualPath, {
32
+ encoding: "utf-8",
33
+ });
34
+
35
+ try {
36
+ const config = JSON.parse(content);
37
+ this.validateConfig(config);
38
+ return config;
39
+ } catch (e: any) {
40
+ throw {
41
+ message: `Error parsing config, ${e.message}`,
42
+ stack: e.stack ?? new Error("Error parsing config").stack,
43
+ };
44
+ }
45
+ }
46
+
47
+ validateConfig(config: any) {
48
+ if (typeof config.port !== "number") {
49
+ throw new SwerveConfigException("Invalid port");
50
+ }
51
+
52
+ if (typeof config.services !== "object") {
53
+ throw new SwerveConfigException("Invalid services configuration");
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,15 @@
1
+ export type KeyValue<VALUE> = { [k: string]: VALUE };
2
+
3
+ export interface IService {
4
+ packageName?: string;
5
+ servicePath?: string;
6
+ packageJson?: any;
7
+ logLevel?: string;
8
+ [key: string]: any;
9
+ }
10
+
11
+ export interface IConfig {
12
+ port?: number;
13
+ services: KeyValue<IService>;
14
+ [key: string]: any;
15
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./config.js";
2
+ export * from "./config-parser.js";
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./swerve.js";
2
+ export * from "./config/index.js";
3
+ export * from "./utils/index.js";
package/src/swerve.ts ADDED
@@ -0,0 +1,418 @@
1
+ // @ts-ignore
2
+ import express, { Application } from "@swizzyweb/express";
3
+ import {
4
+ getFullImportPath,
5
+ getLoggerForService,
6
+ installWebService,
7
+ SwerveArgs,
8
+ } from "./utils/index.js";
9
+ import {
10
+ AnyServer,
11
+ IWebService,
12
+ SwizzyWinstonLogger,
13
+ WebService,
14
+ } from "@swizzyweb/swizzy-web-service";
15
+ import os from "node:os";
16
+ import process from "node:process";
17
+ import { ILogger } from "@swizzyweb/swizzy-common";
18
+ import path from "node:path";
19
+ import { mkdirSync } from "node:fs";
20
+
21
+ export interface ISwerveManager {
22
+ run(request: RunRequest): Promise<RunResponse>;
23
+ stop(request: StopRequest): Promise<void>;
24
+ getRunningWebServices(
25
+ props: GetRunningWebServiceRequest,
26
+ ): Promise<GetRunningWebServiceResponse>;
27
+ }
28
+ export interface GetRunningWebServiceRequest {}
29
+
30
+ export interface GetRunningWebServiceResponse {
31
+ webServices: {
32
+ [instanceId: string]: {
33
+ webService: any;
34
+ serviceConfig: any;
35
+ };
36
+ };
37
+ }
38
+ export enum InstanceType {
39
+ webservice = "webservice",
40
+ stack = "stack",
41
+ }
42
+
43
+ export interface InstanceDetails {
44
+ instanceType: InstanceType;
45
+ instanceId: string;
46
+ }
47
+
48
+ export interface StopRequest {
49
+ instanceDetails: InstanceDetails;
50
+ }
51
+
52
+ export interface RunRequest {
53
+ args?: SwerveArgs;
54
+ }
55
+
56
+ export interface RunResponse {
57
+ webServices: WebService<any>[];
58
+ }
59
+
60
+ export type Apps = {
61
+ [key: number]: {
62
+ app: Application;
63
+ server?: AnyServer;
64
+ services: {
65
+ [instanceId: string]: {
66
+ webService: IWebService;
67
+ serviceArgs: SwerveArgs;
68
+ runRequest: RunRequest;
69
+ };
70
+ };
71
+ };
72
+ };
73
+
74
+ export interface SwerveManagerProps {
75
+ apps?: Apps;
76
+ webServices?: WebService<any>[];
77
+ }
78
+
79
+ export interface WebServiceConfiguration {}
80
+
81
+ type WebServiceConfigurations = {
82
+ [instanceId: string]: WebServiceConfiguration;
83
+ };
84
+
85
+ export class SwerveManager implements ISwerveManager {
86
+ apps: Apps;
87
+ webServices: WebService<any>[];
88
+ configurations: WebServiceConfigurations;
89
+ constructor(props: SwerveManagerProps) {
90
+ this.apps = props.apps ?? {};
91
+ this.webServices = props.webServices ?? [];
92
+ }
93
+
94
+ async run(request: RunRequest): Promise<RunResponse> {
95
+ const { args } = request;
96
+ const newWebServices = await this.runWithArgs({
97
+ args,
98
+ });
99
+ this.webServices.push(...newWebServices);
100
+ return { webServices: newWebServices };
101
+ }
102
+
103
+ async runWithArgs(request: RunRequest) {
104
+ const { args } = request;
105
+ const logLevel: string = process.env.LOG_LEVEL ?? args.logLevel ?? "info";
106
+ let gLogger = new SwizzyWinstonLogger({
107
+ port: 0,
108
+ logLevel,
109
+ appDataRoot: args.appDataRoot ?? ".",
110
+ appName: `swerve`,
111
+ hostName: os.hostname(),
112
+ ownerName: "swerve",
113
+ pid: process.pid,
114
+ });
115
+
116
+ try {
117
+ const webServices: WebService<any>[] = [];
118
+ const newApps: { [port: number]: Application } = {};
119
+ for (const serviceEntry of Object.entries(args.services)) {
120
+ const port = serviceEntry[1].port ?? args.port;
121
+ if (!this.apps[`${port}`]) {
122
+ this.apps[`${port}`] = { app: await express(), services: {} };
123
+ newApps[`${port}`] = this.apps[`${port}`];
124
+ }
125
+
126
+ const app = this.apps[`${port}`].app;
127
+
128
+ const service = serviceEntry[1];
129
+ const packageName = serviceEntry[0];
130
+ const importPathOrName = service.servicePath ?? service.packageName;
131
+ gLogger.debug(`importPathOrName ${importPathOrName}`);
132
+ const serviceArgs: SwerveArgs = {
133
+ ...service,
134
+ ...service.serviceConfiguration,
135
+ ...args.serviceArgs,
136
+ };
137
+
138
+ const webservice = await this.installWebService({
139
+ serviceKey: serviceEntry[0],
140
+ packageName,
141
+ importPathOrName,
142
+ port,
143
+ app,
144
+ appDataRoot: args.appDataRoot,
145
+ serviceArgs,
146
+ gLogger,
147
+ });
148
+
149
+ this.apps[`${port}`].services[webservice.instanceId] = {
150
+ webService: webservice,
151
+ serviceArgs,
152
+ runRequest: request,
153
+ };
154
+ webServices.push(webservice);
155
+ }
156
+
157
+ for (const newAppEntry of Object.entries(newApps)) {
158
+ const [port, appRecord] = newAppEntry;
159
+ const newApp = appRecord.app;
160
+ const server = await newApp.listen(port, () => {
161
+ // this.apps[port].services[newApp.instanceId] = {};
162
+ gLogger.debug(`New app listening on port ${port}`);
163
+ });
164
+ this.apps[`${port}`].server = server;
165
+ }
166
+
167
+ for (const webService of webServices) {
168
+ gLogger.info(`${webService.name} running on port ${webService.port}`);
169
+ }
170
+ return webServices;
171
+ } catch (e) {
172
+ gLogger.error(
173
+ `Error occurred initializing service\n ${e.message}\n ${e.stack ?? {}}`,
174
+ );
175
+ }
176
+ }
177
+
178
+ private async runWithApp(props: RunWithAppArgs) {
179
+ const { app, args } = props;
180
+ let gLogger = new SwizzyWinstonLogger({
181
+ port: 0,
182
+ logLevel: process.env.LOG_LEVEL ?? "info",
183
+ appDataRoot: args.appDataRoot,
184
+ appName: `swerve`,
185
+ hostName: os.hostname(),
186
+ pid: process.pid,
187
+ });
188
+
189
+ try {
190
+ gLogger = new SwizzyWinstonLogger({
191
+ logLevel: args.serviceArgs.logLevel ?? process.env.LOG_LEVEL ?? "info",
192
+ port: args.port,
193
+ logDir: args.appDataRoot,
194
+ appName: `swerve`,
195
+ hostName: os.hostname(),
196
+ pid: process.pid,
197
+ });
198
+
199
+ gLogger.debug(`Swerve Args: ${JSON.stringify(args)}`);
200
+
201
+ const PORT = args.port ?? 3005;
202
+ const webServices = [];
203
+ for (const serviceEntry of Object.entries(args.services)) {
204
+ const service = serviceEntry[1];
205
+ const packageName = service.packageName;
206
+ const importPathOrName =
207
+ service.servicePath ?? service.serviceArgs.servicePath ?? packageName;
208
+ const webservice = await this.installWebService({
209
+ serviceKey: serviceEntry[0],
210
+ packageName,
211
+ importPathOrName,
212
+ port: PORT,
213
+ app,
214
+ appDataRoot: args.appDataRoot,
215
+ serviceArgs: {
216
+ ...service,
217
+ ...service.serviceConfiguration,
218
+ ...args.serviceArgs,
219
+ },
220
+ gLogger,
221
+ });
222
+ webServices.push(webservice);
223
+ }
224
+ return webServices;
225
+ } catch (e) {
226
+ gLogger.error(
227
+ `Error occurred initializing service\n ${e.message}\n ${e.stack ?? {}}`,
228
+ );
229
+ }
230
+ }
231
+
232
+ async installWebService(props: {
233
+ //
234
+ serviceKey: string;
235
+ importPathOrName: string;
236
+ app: Application;
237
+ appDataRoot: string;
238
+ packageName: string;
239
+ port: number;
240
+ gLogger: ILogger<any>;
241
+ serviceArgs: { [key: string]: any };
242
+ }) {
243
+ // const packageName = importPathOrName;
244
+ const {
245
+ app,
246
+ appDataRoot,
247
+ packageName,
248
+ port,
249
+ gLogger,
250
+ serviceArgs,
251
+ importPathOrName,
252
+ serviceKey,
253
+ } = props;
254
+
255
+ try {
256
+ gLogger.info(
257
+ `Getting webservice package ${packageName} and will run on port ${port}`,
258
+ );
259
+
260
+ gLogger.debug(`Getting tool with path: ${importPathOrName}`);
261
+
262
+ const fullPath = await getFullImportPath(importPathOrName);
263
+ const tool = await import(fullPath); //require(fullPath); //require(packageName as string);
264
+
265
+ gLogger.debug(`Got service with require: ${JSON.stringify(tool)}`);
266
+ gLogger.debug(`Getting web service from tool...`);
267
+
268
+ const appDataPath = path.join(appDataRoot, "appdata", serviceKey);
269
+ mkdirSync(appDataPath, { recursive: true });
270
+
271
+ const logger = getLoggerForService(
272
+ serviceArgs,
273
+ serviceKey,
274
+ port,
275
+ gLogger,
276
+ );
277
+ gLogger.debug(`serviceArgs for ${packageName}: ${serviceArgs}`);
278
+ const service = await tool.getWebservice({
279
+ appDataPath,
280
+ ...serviceArgs,
281
+ port,
282
+ app,
283
+ packageName,
284
+ serviceArgs: { ...serviceArgs },
285
+ logger,
286
+ });
287
+
288
+ logger.debug(`Got web service`);
289
+
290
+ gLogger.debug(`Installing web service...`);
291
+ await service.install({});
292
+
293
+ gLogger.debug(`Installed web service ${packageName}`);
294
+ return service;
295
+ } catch (e) {
296
+ const exceptionMessage = `exception: ${e}
297
+ Failed to install web service, is it installed with NPM? Check package exists in node_modules
298
+ To add, run:
299
+ npm install ${packageName ?? "packageName"}
300
+ args:
301
+ packageName: ${packageName}
302
+ port: ${port}
303
+ `;
304
+ // ${getHelpText}`;
305
+ gLogger.error(
306
+ `Failed to install web service, error: ${e?.message} stack: ${e?.stack}`,
307
+ );
308
+ throw e; //new Error(exceptionMessage);
309
+ }
310
+ }
311
+
312
+ async stop(request: StopRequest) {
313
+ const { instanceDetails } = request;
314
+ const { instanceId, instanceType } = instanceDetails;
315
+ const instanceIds = [];
316
+ if (!instanceId) {
317
+ throw new Error(`Instance id required to stop web service`);
318
+ }
319
+ if (instanceType === InstanceType.webservice) {
320
+ // console.log(`Matched instance type`);
321
+ instanceIds.push(`${instanceId}`);
322
+ } else if (instanceType === InstanceType.stack) {
323
+ // TODO: implement
324
+ throw new Error(`Stack instance type is not yet supported`);
325
+ } else {
326
+ // this.logger.error(`Invalid instance details ${instanceDetails}`);
327
+ throw new Error(`Invalid instance details provided`);
328
+ }
329
+ // console.log(`instanceIds: ${instanceIds}`);
330
+ const webServices = this.webServices.filter((service) => {
331
+ // console.log(`instanceId ${service.instanceId}`);
332
+ return instanceIds.includes(`${service.instanceId}`);
333
+ });
334
+
335
+ if (!webServices || webServices.length < 1) {
336
+ //console.error(webServices); //this.webServices);
337
+ throw new Error(
338
+ `WebService with instanceId ${instanceId} not found while attempting to stop`,
339
+ );
340
+ }
341
+
342
+ const ports = [];
343
+ for (const webService of webServices) {
344
+ const port = webService.port;
345
+ ports.push(port);
346
+ //console.log(webService);
347
+ await webService.uninstall({});
348
+ const indexes = this.webServices
349
+ .map((val, index, array) => {
350
+ //console.log(
351
+ // `instanceInSwerve: ${val.instanceId} instanceInWebService ${webService.instanceId}`,
352
+ // );
353
+ if (val.instanceId == webService.instanceId) {
354
+ return index;
355
+ }
356
+ return undefined;
357
+ })
358
+ .filter((val) => val != undefined);
359
+ //.filter((val) => val);
360
+ if (indexes.length > 1) {
361
+ throw new Error(
362
+ `Found multiple indexes for webservice instance ${webService.instanceId} ${indexes}`,
363
+ );
364
+ } else if (indexes.length == 0) {
365
+ throw new Error(
366
+ `No indexes for webservice instance ${webService.instanceId}`,
367
+ );
368
+ }
369
+ const index = indexes[0];
370
+ this.webServices.splice(index, 1);
371
+ if (!this.apps[`${port}`]?.services[webService.instanceId]) {
372
+ //console.log(`Apps does not contain service`);
373
+ // TODO: DO SOMETHING, log, throw maybe.
374
+ } else {
375
+ delete this.apps[`${port}`].services[webService.instanceId];
376
+ }
377
+ }
378
+
379
+ //cleanup apps if no more services
380
+
381
+ for (const port of ports) {
382
+ const { services, server, app } = this.apps[`${port}`];
383
+ if (!services || Object.keys(services).length == 0) {
384
+ if (server) {
385
+ server.close();
386
+ } else {
387
+ continue;
388
+ // TODO: do something, log throw etc
389
+ }
390
+ delete this.apps[`${port}`];
391
+ }
392
+ }
393
+ }
394
+
395
+ async getRunningWebServices(
396
+ props: GetRunningWebServiceRequest,
397
+ ): Promise<GetRunningWebServiceResponse> {
398
+ const webservices = {};
399
+ for (const webservice of this.webServices) {
400
+ const instanceId = webservice.instanceId;
401
+ const { runRequest, serviceArgs } =
402
+ this.apps[webservice.port].services[instanceId];
403
+ webservices[instanceId] = {
404
+ webService: webservice.toJson(),
405
+ serviceConfig: runRequest?.args,
406
+ };
407
+ }
408
+
409
+ return {
410
+ webServices: webservices,
411
+ };
412
+ }
413
+ }
414
+
415
+ interface RunWithAppArgs {
416
+ app: Application;
417
+ args: SwerveArgs;
418
+ }