@magek/server 0.0.1
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/index.d.ts +7 -0
- package/dist/index.js +95 -0
- package/dist/infrastructure/controllers/graphql.d.ts +7 -0
- package/dist/infrastructure/controllers/graphql.js +20 -0
- package/dist/infrastructure/controllers/health-controller.d.ts +7 -0
- package/dist/infrastructure/controllers/health-controller.js +28 -0
- package/dist/infrastructure/http.d.ts +8 -0
- package/dist/infrastructure/http.js +20 -0
- package/dist/infrastructure/scheduler.d.ts +2 -0
- package/dist/infrastructure/scheduler.js +25 -0
- package/dist/infrastructure/test-helper/local-queries.d.ts +5 -0
- package/dist/infrastructure/test-helper/local-queries.js +15 -0
- package/dist/infrastructure/test-helper/local-test-helper.d.ts +17 -0
- package/dist/infrastructure/test-helper/local-test-helper.js +37 -0
- package/dist/infrastructure/websocket-registry.d.ts +27 -0
- package/dist/infrastructure/websocket-registry.js +49 -0
- package/dist/library/api-adapter.d.ts +13 -0
- package/dist/library/api-adapter.js +33 -0
- package/dist/library/graphql-adapter.d.ts +11 -0
- package/dist/library/graphql-adapter.js +92 -0
- package/dist/library/health-adapter.d.ts +9 -0
- package/dist/library/health-adapter.js +70 -0
- package/dist/library/rocket-adapter.d.ts +2 -0
- package/dist/library/rocket-adapter.js +11 -0
- package/dist/library/scheduled-adapter.d.ts +6 -0
- package/dist/library/scheduled-adapter.js +14 -0
- package/dist/library/searcher-adapter.d.ts +17 -0
- package/dist/library/searcher-adapter.js +90 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.js +14 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +197 -0
- package/dist/services/graphql-service.d.ts +9 -0
- package/dist/services/graphql-service.js +15 -0
- package/dist/services/health-service.d.ts +7 -0
- package/dist/services/health-service.js +12 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +5 -0
- package/package.json +70 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ProviderLibrary } from '@magek/common';
|
|
2
|
+
export * from './paths';
|
|
3
|
+
export * from './services';
|
|
4
|
+
export * from './library/graphql-adapter';
|
|
5
|
+
export { createServer, getWebSocketRegistry, sendWebSocketMessage } from './server';
|
|
6
|
+
export type { ServerOptions } from './server';
|
|
7
|
+
export declare const Provider: () => ProviderLibrary;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Provider = exports.sendWebSocketMessage = exports.getWebSocketRegistry = exports.createServer = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@magek/common");
|
|
6
|
+
const api_adapter_1 = require("./library/api-adapter");
|
|
7
|
+
const graphql_adapter_1 = require("./library/graphql-adapter");
|
|
8
|
+
const scheduled_adapter_1 = require("./library/scheduled-adapter");
|
|
9
|
+
const health_adapter_1 = require("./library/health-adapter");
|
|
10
|
+
tslib_1.__exportStar(require("./paths"), exports);
|
|
11
|
+
tslib_1.__exportStar(require("./services"), exports);
|
|
12
|
+
tslib_1.__exportStar(require("./library/graphql-adapter"), exports);
|
|
13
|
+
var server_1 = require("./server");
|
|
14
|
+
Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_1.createServer; } });
|
|
15
|
+
Object.defineProperty(exports, "getWebSocketRegistry", { enumerable: true, get: function () { return server_1.getWebSocketRegistry; } });
|
|
16
|
+
Object.defineProperty(exports, "sendWebSocketMessage", { enumerable: true, get: function () { return server_1.sendWebSocketMessage; } });
|
|
17
|
+
const Provider = () => ({
|
|
18
|
+
// ProviderGraphQLLibrary
|
|
19
|
+
graphQL: {
|
|
20
|
+
rawToEnvelope: graphql_adapter_1.rawGraphQLRequestToEnvelope,
|
|
21
|
+
handleResult: api_adapter_1.requestSucceeded,
|
|
22
|
+
},
|
|
23
|
+
// ProviderAPIHandling
|
|
24
|
+
api: {
|
|
25
|
+
requestSucceeded: api_adapter_1.requestSucceeded,
|
|
26
|
+
requestFailed: api_adapter_1.requestFailed,
|
|
27
|
+
healthRequestResult: api_adapter_1.healthRequestResult,
|
|
28
|
+
},
|
|
29
|
+
// ProviderMessagingLibrary
|
|
30
|
+
messaging: {
|
|
31
|
+
sendMessage: async (config, connectionID, data) => {
|
|
32
|
+
// Use the global WebSocket registry for message sending
|
|
33
|
+
const globalRegistry = global.webSocketRegistry;
|
|
34
|
+
if (globalRegistry && typeof globalRegistry.sendMessage === 'function') {
|
|
35
|
+
globalRegistry.sendMessage(connectionID, data);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const logger = (0, common_1.getLogger)(config, 'ServerProvider');
|
|
39
|
+
logger.warn(`WebSocket registry not available. Message not sent to connection ${connectionID}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
// ScheduledCommandsLibrary
|
|
44
|
+
scheduled: {
|
|
45
|
+
rawToEnvelope: scheduled_adapter_1.rawScheduledInputToEnvelope,
|
|
46
|
+
},
|
|
47
|
+
sensor: {
|
|
48
|
+
databaseEventsHealthDetails: (config) => {
|
|
49
|
+
var _a;
|
|
50
|
+
// Delegate to event store adapter health check if available
|
|
51
|
+
if ((_a = config.eventStoreAdapter) === null || _a === void 0 ? void 0 : _a.healthCheck) {
|
|
52
|
+
return config.eventStoreAdapter.healthCheck.details(config);
|
|
53
|
+
}
|
|
54
|
+
throw new Error('No event store adapter configured for health checks');
|
|
55
|
+
},
|
|
56
|
+
databaseReadModelsHealthDetails: (config) => {
|
|
57
|
+
var _a;
|
|
58
|
+
// Delegate to read model store adapter health check if available
|
|
59
|
+
if ((_a = config.readModelStoreAdapter) === null || _a === void 0 ? void 0 : _a.healthCheck) {
|
|
60
|
+
return config.readModelStoreAdapter.healthCheck.details(config);
|
|
61
|
+
}
|
|
62
|
+
throw new Error('No read model store adapter configured for health checks');
|
|
63
|
+
},
|
|
64
|
+
isDatabaseEventUp: (config) => {
|
|
65
|
+
var _a;
|
|
66
|
+
// Delegate to event store adapter health check if available
|
|
67
|
+
if ((_a = config.eventStoreAdapter) === null || _a === void 0 ? void 0 : _a.healthCheck) {
|
|
68
|
+
return config.eventStoreAdapter.healthCheck.isUp(config);
|
|
69
|
+
}
|
|
70
|
+
return Promise.resolve(false);
|
|
71
|
+
},
|
|
72
|
+
areDatabaseReadModelsUp: (config) => {
|
|
73
|
+
var _a;
|
|
74
|
+
// Delegate to read model store adapter health check if available
|
|
75
|
+
if ((_a = config.readModelStoreAdapter) === null || _a === void 0 ? void 0 : _a.healthCheck) {
|
|
76
|
+
return config.readModelStoreAdapter.healthCheck.isUp(config);
|
|
77
|
+
}
|
|
78
|
+
return Promise.resolve(false);
|
|
79
|
+
},
|
|
80
|
+
databaseUrls: (config) => {
|
|
81
|
+
var _a, _b, _c, _d, _e, _f;
|
|
82
|
+
// Get URLs from both event store and read model store adapters
|
|
83
|
+
const eventUrls = (_c = (_b = (_a = config.eventStoreAdapter) === null || _a === void 0 ? void 0 : _a.healthCheck) === null || _b === void 0 ? void 0 : _b.urls(config)) !== null && _c !== void 0 ? _c : Promise.resolve([]);
|
|
84
|
+
const readModelUrls = (_f = (_e = (_d = config.readModelStoreAdapter) === null || _d === void 0 ? void 0 : _d.healthCheck) === null || _e === void 0 ? void 0 : _e.urls(config)) !== null && _f !== void 0 ? _f : Promise.resolve([]);
|
|
85
|
+
return Promise.all([eventUrls, readModelUrls]).then(([events, readModels]) => [
|
|
86
|
+
...events,
|
|
87
|
+
...readModels
|
|
88
|
+
]);
|
|
89
|
+
},
|
|
90
|
+
isGraphQLFunctionUp: health_adapter_1.isGraphQLFunctionUp,
|
|
91
|
+
graphQLFunctionUrl: health_adapter_1.graphqlFunctionUrl,
|
|
92
|
+
rawRequestToHealthEnvelope: health_adapter_1.rawRequestToSensorHealth,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
exports.Provider = Provider;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
+
import { GraphQLService } from '../../services';
|
|
3
|
+
export declare class GraphQLController {
|
|
4
|
+
readonly graphQLService: GraphQLService;
|
|
5
|
+
constructor(graphQLService: GraphQLService);
|
|
6
|
+
handleGraphQL(request: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GraphQLController = void 0;
|
|
4
|
+
const http_1 = require("../http");
|
|
5
|
+
class GraphQLController {
|
|
6
|
+
constructor(graphQLService) {
|
|
7
|
+
this.graphQLService = graphQLService;
|
|
8
|
+
}
|
|
9
|
+
async handleGraphQL(request, reply) {
|
|
10
|
+
try {
|
|
11
|
+
const response = await this.graphQLService.handleGraphQLRequest(request);
|
|
12
|
+
reply.status(http_1.HttpCodes.Ok).send(response.result);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
await (0, http_1.requestFailed)(e, reply);
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.GraphQLController = GraphQLController;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
+
import { HealthService } from '../../services';
|
|
3
|
+
export declare class HealthController {
|
|
4
|
+
readonly healthService: HealthService;
|
|
5
|
+
constructor(healthService: HealthService);
|
|
6
|
+
handleHealth(request: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HealthController = void 0;
|
|
4
|
+
const http_1 = require("../http");
|
|
5
|
+
class HealthController {
|
|
6
|
+
constructor(healthService) {
|
|
7
|
+
this.healthService = healthService;
|
|
8
|
+
}
|
|
9
|
+
async handleHealth(request, reply) {
|
|
10
|
+
try {
|
|
11
|
+
const response = await this.healthService.handleHealthRequest(request);
|
|
12
|
+
if (response.status === 'success') {
|
|
13
|
+
reply.status(http_1.HttpCodes.Ok).send(response.result);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
reply.status(response.code).send({
|
|
17
|
+
title: response.title,
|
|
18
|
+
reason: response.message,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
await (0, http_1.requestFailed)(e, reply);
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.HealthController = HealthController;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpCodes = void 0;
|
|
4
|
+
exports.requestFailed = requestFailed;
|
|
5
|
+
const common_1 = require("@magek/common");
|
|
6
|
+
var HttpCodes;
|
|
7
|
+
(function (HttpCodes) {
|
|
8
|
+
HttpCodes[HttpCodes["Ok"] = 200] = "Ok";
|
|
9
|
+
HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest";
|
|
10
|
+
HttpCodes[HttpCodes["NotAuthorized"] = 403] = "NotAuthorized";
|
|
11
|
+
HttpCodes[HttpCodes["InternalError"] = 500] = "InternalError";
|
|
12
|
+
})(HttpCodes || (exports.HttpCodes = HttpCodes = {}));
|
|
13
|
+
// Wrapper to return a failed request through GraphQL
|
|
14
|
+
async function requestFailed(error, reply) {
|
|
15
|
+
const statusCode = (0, common_1.httpStatusCodeFor)(error);
|
|
16
|
+
await reply.status(statusCode).send({
|
|
17
|
+
title: (0, common_1.toClassTitle)(error),
|
|
18
|
+
reason: error.message,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configureScheduler = configureScheduler;
|
|
4
|
+
const scheduler = require("node-schedule");
|
|
5
|
+
function configureScheduler(config, userProject) {
|
|
6
|
+
const triggerScheduledCommands = userProject['triggerScheduledCommands'];
|
|
7
|
+
Object.keys(config.scheduledCommandHandlers)
|
|
8
|
+
.map((scheduledCommandName) => buildScheduledCommandInfo(config, scheduledCommandName))
|
|
9
|
+
.filter((scheduledCommandInfo) => scheduledCommandInfo.metadata.scheduledOn)
|
|
10
|
+
.forEach((scheduledCommandInfo) => {
|
|
11
|
+
scheduler.scheduleJob(scheduledCommandInfo.name, createCronExpression(scheduledCommandInfo.metadata), () => {
|
|
12
|
+
triggerScheduledCommands({ typeName: scheduledCommandInfo.name });
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function createCronExpression(scheduledCommandMetadata) {
|
|
17
|
+
const { minute = '*', hour = '*', day = '*', month = '*', weekDay = '*' } = scheduledCommandMetadata.scheduledOn;
|
|
18
|
+
return `${minute} ${hour} ${day} ${month} ${weekDay}`;
|
|
19
|
+
}
|
|
20
|
+
function buildScheduledCommandInfo(config, scheduledCommandName) {
|
|
21
|
+
return {
|
|
22
|
+
name: scheduledCommandName,
|
|
23
|
+
metadata: config.scheduledCommandHandlers[scheduledCommandName],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalQueries = void 0;
|
|
4
|
+
class LocalQueries {
|
|
5
|
+
constructor() { }
|
|
6
|
+
async events(primaryKey, latestFirst = true) {
|
|
7
|
+
// TODO implement method
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
async readModels(primaryKey, readModelName, latestFirst = true) {
|
|
11
|
+
// TODO implement method
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.LocalQueries = LocalQueries;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LocalQueries } from './local-queries';
|
|
2
|
+
interface ApplicationOutputs {
|
|
3
|
+
graphqlURL: string;
|
|
4
|
+
websocketURL: string;
|
|
5
|
+
healthURL: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class LocalTestHelper {
|
|
8
|
+
readonly outputs: ApplicationOutputs;
|
|
9
|
+
readonly queries: LocalQueries;
|
|
10
|
+
private constructor();
|
|
11
|
+
static build(appName: string): Promise<LocalTestHelper>;
|
|
12
|
+
private static ensureProviderIsReady;
|
|
13
|
+
private static graphqlURL;
|
|
14
|
+
private static healthURL;
|
|
15
|
+
private static websocketURL;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalTestHelper = void 0;
|
|
4
|
+
const local_queries_1 = require("./local-queries");
|
|
5
|
+
class LocalTestHelper {
|
|
6
|
+
constructor(outputs, queries) {
|
|
7
|
+
this.outputs = outputs;
|
|
8
|
+
this.queries = queries;
|
|
9
|
+
}
|
|
10
|
+
static async build(appName) {
|
|
11
|
+
await this.ensureProviderIsReady();
|
|
12
|
+
return new LocalTestHelper({
|
|
13
|
+
graphqlURL: await this.graphqlURL(),
|
|
14
|
+
websocketURL: await this.websocketURL(),
|
|
15
|
+
healthURL: await this.healthURL(),
|
|
16
|
+
}, new local_queries_1.LocalQueries());
|
|
17
|
+
}
|
|
18
|
+
static async ensureProviderIsReady() {
|
|
19
|
+
const url = await this.healthURL();
|
|
20
|
+
const response = await fetch(url);
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new Error(`Provider is not ready: ${response.status} ${response.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
static async graphqlURL() {
|
|
26
|
+
const url = 'http://localhost:3000/graphql';
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
static async healthURL() {
|
|
30
|
+
return 'http://localhost:3000/sensor/health/';
|
|
31
|
+
}
|
|
32
|
+
static async websocketURL() {
|
|
33
|
+
const url = 'ws://localhost:3000/websocket';
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.LocalTestHelper = LocalTestHelper;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WebSocket } from '@fastify/websocket';
|
|
2
|
+
/**
|
|
3
|
+
* Registry to manage active WebSocket connections
|
|
4
|
+
*/
|
|
5
|
+
export declare class WebSocketRegistry {
|
|
6
|
+
private connections;
|
|
7
|
+
/**
|
|
8
|
+
* Add a connection to the registry
|
|
9
|
+
*/
|
|
10
|
+
addConnection(connectionId: string, socket: WebSocket): void;
|
|
11
|
+
/**
|
|
12
|
+
* Remove a connection from the registry
|
|
13
|
+
*/
|
|
14
|
+
removeConnection(connectionId: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Send a message to a specific connection
|
|
17
|
+
*/
|
|
18
|
+
sendMessage(connectionId: string, data: unknown): void;
|
|
19
|
+
/**
|
|
20
|
+
* Check if a connection exists
|
|
21
|
+
*/
|
|
22
|
+
hasConnection(connectionId: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Get the number of active connections
|
|
25
|
+
*/
|
|
26
|
+
getConnectionCount(): number;
|
|
27
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebSocketRegistry = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Registry to manage active WebSocket connections
|
|
6
|
+
*/
|
|
7
|
+
class WebSocketRegistry {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.connections = new Map();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Add a connection to the registry
|
|
13
|
+
*/
|
|
14
|
+
addConnection(connectionId, socket) {
|
|
15
|
+
this.connections.set(connectionId, socket);
|
|
16
|
+
// Clean up when connection closes
|
|
17
|
+
socket.on('close', () => {
|
|
18
|
+
this.connections.delete(connectionId);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Remove a connection from the registry
|
|
23
|
+
*/
|
|
24
|
+
removeConnection(connectionId) {
|
|
25
|
+
this.connections.delete(connectionId);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Send a message to a specific connection
|
|
29
|
+
*/
|
|
30
|
+
sendMessage(connectionId, data) {
|
|
31
|
+
const connection = this.connections.get(connectionId);
|
|
32
|
+
if (connection && connection.readyState === connection.OPEN) {
|
|
33
|
+
connection.send(JSON.stringify(data));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if a connection exists
|
|
38
|
+
*/
|
|
39
|
+
hasConnection(connectionId) {
|
|
40
|
+
return this.connections.has(connectionId);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the number of active connections
|
|
44
|
+
*/
|
|
45
|
+
getConnectionCount() {
|
|
46
|
+
return this.connections.size;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.WebSocketRegistry = WebSocketRegistry;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type APIResult = {
|
|
2
|
+
status: 'success';
|
|
3
|
+
result: unknown;
|
|
4
|
+
headers?: Record<string, number | string | ReadonlyArray<string>>;
|
|
5
|
+
} | {
|
|
6
|
+
status: 'failure';
|
|
7
|
+
code: number;
|
|
8
|
+
title: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function requestSucceeded(body?: any, headers?: Record<string, number | string | ReadonlyArray<string>>): Promise<APIResult>;
|
|
12
|
+
export declare function requestFailed(error: Error): Promise<APIResult>;
|
|
13
|
+
export declare function healthRequestResult(body: unknown, isHealthy: boolean): Promise<APIResult>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestSucceeded = requestSucceeded;
|
|
4
|
+
exports.requestFailed = requestFailed;
|
|
5
|
+
exports.healthRequestResult = healthRequestResult;
|
|
6
|
+
const common_1 = require("@magek/common");
|
|
7
|
+
async function requestSucceeded(body, headers) {
|
|
8
|
+
return {
|
|
9
|
+
status: 'success',
|
|
10
|
+
result: body,
|
|
11
|
+
headers: {
|
|
12
|
+
...headers,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function requestFailed(error) {
|
|
17
|
+
const statusCode = (0, common_1.httpStatusCodeFor)(error);
|
|
18
|
+
return {
|
|
19
|
+
status: 'failure',
|
|
20
|
+
code: statusCode,
|
|
21
|
+
title: (0, common_1.toClassTitle)(error),
|
|
22
|
+
reason: error.message,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async function healthRequestResult(body, isHealthy) {
|
|
26
|
+
return {
|
|
27
|
+
status: 'success',
|
|
28
|
+
result: body,
|
|
29
|
+
headers: {
|
|
30
|
+
'status-code': isHealthy ? 200 : 503,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MagekConfig, GraphQLRequestEnvelope, GraphQLRequestEnvelopeError } from '@magek/common';
|
|
2
|
+
import { FastifyRequest } from 'fastify';
|
|
3
|
+
export interface WebSocketMessage {
|
|
4
|
+
connectionContext: {
|
|
5
|
+
connectionId: string;
|
|
6
|
+
eventType: 'CONNECT' | 'MESSAGE' | 'DISCONNECT';
|
|
7
|
+
};
|
|
8
|
+
data?: any;
|
|
9
|
+
incomingMessage?: any;
|
|
10
|
+
}
|
|
11
|
+
export declare function rawGraphQLRequestToEnvelope(config: MagekConfig, request: FastifyRequest | WebSocketMessage): Promise<GraphQLRequestEnvelope | GraphQLRequestEnvelopeError>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rawGraphQLRequestToEnvelope = rawGraphQLRequestToEnvelope;
|
|
4
|
+
const common_1 = require("@magek/common");
|
|
5
|
+
async function rawGraphQLRequestToEnvelope(config, request) {
|
|
6
|
+
const requestID = common_1.UUID.generate();
|
|
7
|
+
return isWebSocketMessage(request)
|
|
8
|
+
? webSocketMessageToEnvelope(config, request, requestID)
|
|
9
|
+
: httpMessageToEnvelope(config, request, requestID);
|
|
10
|
+
}
|
|
11
|
+
function webSocketMessageToEnvelope(config, webSocketRequest, requestID) {
|
|
12
|
+
const logger = (0, common_1.getLogger)(config, 'graphql-adapter#webSocketMessageToEnvelope');
|
|
13
|
+
logger.debug('Received WebSocket GraphQL request: ', webSocketRequest);
|
|
14
|
+
let eventType = 'MESSAGE';
|
|
15
|
+
const incomingMessage = webSocketRequest.incomingMessage;
|
|
16
|
+
const headers = incomingMessage === null || incomingMessage === void 0 ? void 0 : incomingMessage.headers;
|
|
17
|
+
const data = webSocketRequest.data;
|
|
18
|
+
try {
|
|
19
|
+
const connectionContext = webSocketRequest.connectionContext;
|
|
20
|
+
eventType = connectionContext === null || connectionContext === void 0 ? void 0 : connectionContext.eventType;
|
|
21
|
+
return {
|
|
22
|
+
requestID,
|
|
23
|
+
eventType,
|
|
24
|
+
connectionID: connectionContext === null || connectionContext === void 0 ? void 0 : connectionContext.connectionId.toString(),
|
|
25
|
+
token: Array.isArray(headers === null || headers === void 0 ? void 0 : headers.authorization) ? headers.authorization[0] : headers === null || headers === void 0 ? void 0 : headers.authorization,
|
|
26
|
+
value: data,
|
|
27
|
+
context: {
|
|
28
|
+
request: {
|
|
29
|
+
headers: headers,
|
|
30
|
+
body: data,
|
|
31
|
+
},
|
|
32
|
+
rawContext: webSocketRequest,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return {
|
|
38
|
+
error: e,
|
|
39
|
+
requestID,
|
|
40
|
+
connectionID: undefined,
|
|
41
|
+
eventType: eventType,
|
|
42
|
+
context: {
|
|
43
|
+
request: {
|
|
44
|
+
headers: headers,
|
|
45
|
+
body: data,
|
|
46
|
+
},
|
|
47
|
+
rawContext: webSocketRequest,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function httpMessageToEnvelope(config, httpRequest, requestId) {
|
|
53
|
+
const logger = (0, common_1.getLogger)(config, 'graphql-adapter#httpMessageToEnvelope');
|
|
54
|
+
const eventType = 'MESSAGE';
|
|
55
|
+
const headers = httpRequest.headers;
|
|
56
|
+
const data = httpRequest.body;
|
|
57
|
+
try {
|
|
58
|
+
logger.debug('Received GraphQL request: \n- Headers: ', headers, '\n- Body: ', data);
|
|
59
|
+
return {
|
|
60
|
+
connectionID: undefined,
|
|
61
|
+
requestID: requestId,
|
|
62
|
+
eventType: eventType,
|
|
63
|
+
token: Array.isArray(headers === null || headers === void 0 ? void 0 : headers.authorization) ? headers.authorization[0] : headers === null || headers === void 0 ? void 0 : headers.authorization,
|
|
64
|
+
value: data,
|
|
65
|
+
context: {
|
|
66
|
+
request: {
|
|
67
|
+
headers: headers,
|
|
68
|
+
body: data,
|
|
69
|
+
},
|
|
70
|
+
rawContext: httpRequest,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return {
|
|
76
|
+
error: e,
|
|
77
|
+
requestID: requestId,
|
|
78
|
+
connectionID: undefined,
|
|
79
|
+
eventType: eventType,
|
|
80
|
+
context: {
|
|
81
|
+
request: {
|
|
82
|
+
headers: headers,
|
|
83
|
+
body: data,
|
|
84
|
+
},
|
|
85
|
+
rawContext: httpRequest,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function isWebSocketMessage(request) {
|
|
91
|
+
return 'connectionContext' in request;
|
|
92
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { HealthEnvelope } from '@magek/common';
|
|
2
|
+
import { FastifyRequest } from 'fastify';
|
|
3
|
+
import Nedb from '@seald-io/nedb';
|
|
4
|
+
export declare function databaseUrl(): Promise<Array<string>>;
|
|
5
|
+
export declare function countAll(database: Nedb): Promise<number>;
|
|
6
|
+
export declare function graphqlFunctionUrl(): Promise<string>;
|
|
7
|
+
export declare function areDatabaseReadModelsUp(): Promise<boolean>;
|
|
8
|
+
export declare function isGraphQLFunctionUp(): Promise<boolean>;
|
|
9
|
+
export declare function rawRequestToSensorHealth(rawRequest: FastifyRequest): HealthEnvelope;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.databaseUrl = databaseUrl;
|
|
4
|
+
exports.countAll = countAll;
|
|
5
|
+
exports.graphqlFunctionUrl = graphqlFunctionUrl;
|
|
6
|
+
exports.areDatabaseReadModelsUp = areDatabaseReadModelsUp;
|
|
7
|
+
exports.isGraphQLFunctionUp = isGraphQLFunctionUp;
|
|
8
|
+
exports.rawRequestToSensorHealth = rawRequestToSensorHealth;
|
|
9
|
+
const paths_1 = require("../paths");
|
|
10
|
+
const common_1 = require("@magek/common");
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
async function databaseUrl() {
|
|
13
|
+
return [paths_1.eventsDatabase, paths_1.readModelsDatabase];
|
|
14
|
+
}
|
|
15
|
+
async function countAll(database) {
|
|
16
|
+
await database.loadDatabaseAsync();
|
|
17
|
+
const count = await database.countAsync({});
|
|
18
|
+
return count !== null && count !== void 0 ? count : 0;
|
|
19
|
+
}
|
|
20
|
+
async function graphqlFunctionUrl() {
|
|
21
|
+
try {
|
|
22
|
+
const port = (0, common_1.localPort)();
|
|
23
|
+
return `http://localhost:${port}/graphql`;
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function areDatabaseReadModelsUp() {
|
|
30
|
+
return (0, fs_1.existsSync)(paths_1.readModelsDatabase);
|
|
31
|
+
}
|
|
32
|
+
async function isGraphQLFunctionUp() {
|
|
33
|
+
try {
|
|
34
|
+
const url = await graphqlFunctionUrl();
|
|
35
|
+
const response = await (0, common_1.request)(url, 'POST', JSON.stringify({
|
|
36
|
+
query: 'query { __typename }',
|
|
37
|
+
}));
|
|
38
|
+
return response.status === 200;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function rawRequestToSensorHealthComponentPath(rawRequest) {
|
|
45
|
+
// For health requests, the component path is typically in the URL path
|
|
46
|
+
// Since we don't have a direct url property, we'll construct it from params
|
|
47
|
+
const params = rawRequest.params;
|
|
48
|
+
if (params && Object.keys(params).length > 0) {
|
|
49
|
+
// If there are path parameters, join them to create the component path
|
|
50
|
+
return Object.values(params).filter(Boolean).join('/');
|
|
51
|
+
}
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
function rawRequestToSensorHealth(rawRequest) {
|
|
55
|
+
const componentPath = rawRequestToSensorHealthComponentPath(rawRequest);
|
|
56
|
+
const requestID = common_1.UUID.generate();
|
|
57
|
+
const headers = rawRequest.headers;
|
|
58
|
+
return {
|
|
59
|
+
requestID: requestID,
|
|
60
|
+
context: {
|
|
61
|
+
request: {
|
|
62
|
+
headers: headers,
|
|
63
|
+
body: rawRequest.body || {},
|
|
64
|
+
},
|
|
65
|
+
rawContext: rawRequest,
|
|
66
|
+
},
|
|
67
|
+
componentPath: componentPath,
|
|
68
|
+
token: Array.isArray(headers === null || headers === void 0 ? void 0 : headers.authorization) ? headers.authorization[0] : headers === null || headers === void 0 ? void 0 : headers.authorization,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rawRocketInputToEnvelope = rawRocketInputToEnvelope;
|
|
4
|
+
const common_1 = require("@magek/common");
|
|
5
|
+
function rawRocketInputToEnvelope(config, request) {
|
|
6
|
+
const idFromRequest = request[common_1.rocketFunctionIDEnvVar];
|
|
7
|
+
const id = idFromRequest !== null && idFromRequest !== void 0 ? idFromRequest : process.env[common_1.rocketFunctionIDEnvVar];
|
|
8
|
+
return {
|
|
9
|
+
rocketId: id,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { MagekConfig, ScheduledCommandEnvelope } from '@magek/common';
|
|
2
|
+
interface LocalScheduleCommandEnvelope {
|
|
3
|
+
typeName: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function rawScheduledInputToEnvelope(config: MagekConfig, input: Partial<LocalScheduleCommandEnvelope>): Promise<ScheduledCommandEnvelope>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rawScheduledInputToEnvelope = rawScheduledInputToEnvelope;
|
|
4
|
+
const common_1 = require("@magek/common");
|
|
5
|
+
async function rawScheduledInputToEnvelope(config, input) {
|
|
6
|
+
const logger = (0, common_1.getLogger)(config, 'rawScheduledInputToEnvelope');
|
|
7
|
+
logger.debug('Received LocalScheduleCommand request: ', input);
|
|
8
|
+
if (!input.typeName)
|
|
9
|
+
throw new Error(`typeName is not defined or empty, scheduled command envelope should have the structure {typeName: string }, but you gave ${JSON.stringify(input)}`);
|
|
10
|
+
return {
|
|
11
|
+
requestID: common_1.UUID.generate(),
|
|
12
|
+
typeName: input.typeName,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FilterFor } from '@magek/common';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a query record out of the read mode name and
|
|
4
|
+
* the GraphQL filters, ready to be passed into the `query`
|
|
5
|
+
* method of the read model registry.
|
|
6
|
+
*/
|
|
7
|
+
export declare function queryRecordFor(filters: FilterFor<any>, nested?: string, queryFromFilters?: Record<string, object>): Record<string, QueryOperation<QueryValue>>;
|
|
8
|
+
export type QueryValue = number | string | boolean;
|
|
9
|
+
export type QueryOperation<TValue> = TValue | {
|
|
10
|
+
[TKey in '$lt' | '$lte' | '$gt' | '$gte' | '$ne' | '$exists']?: TValue;
|
|
11
|
+
} | {
|
|
12
|
+
[TKey in '$in' | '$nin']?: Array<TValue>;
|
|
13
|
+
} | {
|
|
14
|
+
[TKey in '$regex' | '$nin']?: RegExp;
|
|
15
|
+
} | {
|
|
16
|
+
[TKey in '$elemMatch']?: TValue;
|
|
17
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.queryRecordFor = queryRecordFor;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a query record out of the read mode name and
|
|
6
|
+
* the GraphQL filters, ready to be passed into the `query`
|
|
7
|
+
* method of the read model registry.
|
|
8
|
+
*/
|
|
9
|
+
function queryRecordFor(filters, nested, queryFromFilters = {}) {
|
|
10
|
+
if (Object.keys(filters).length != 0) {
|
|
11
|
+
for (const key in filters) {
|
|
12
|
+
const propName = nested ? `${nested}.${key}` : key;
|
|
13
|
+
const filter = filters[key];
|
|
14
|
+
switch (key) {
|
|
15
|
+
case 'not':
|
|
16
|
+
queryFromFilters[`$${propName}`] = queryRecordFor(filter);
|
|
17
|
+
break;
|
|
18
|
+
case 'or':
|
|
19
|
+
case 'and':
|
|
20
|
+
queryFromFilters[`$${propName}`] = filters[key].map((filter) => queryRecordFor(filter));
|
|
21
|
+
break;
|
|
22
|
+
default:
|
|
23
|
+
if (!Object.keys(queryOperatorTable).includes(Object.keys(filter)[0])) {
|
|
24
|
+
queryRecordFor(filter, propName, queryFromFilters);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
queryFromFilters[`value.${propName}`] = filterToQuery(filter);
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { ...queryFromFilters };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Transforms a GraphQL Magek filter into an neDB query
|
|
37
|
+
*/
|
|
38
|
+
function filterToQuery(filter) {
|
|
39
|
+
const [query] = Object.entries(filter).map(([propName, filter]) => {
|
|
40
|
+
const query = queryOperatorTable[propName];
|
|
41
|
+
const queryFilter = Array.isArray(filter) ? filter : [filter];
|
|
42
|
+
return query(queryFilter);
|
|
43
|
+
});
|
|
44
|
+
return query;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Table of equivalences between a GraphQL operation and the NeDB
|
|
48
|
+
* query operator.
|
|
49
|
+
*
|
|
50
|
+
* It is needed that we pass the values, because of the special case
|
|
51
|
+
* of `=`, in which the operator is the value itself.
|
|
52
|
+
*/
|
|
53
|
+
const queryOperatorTable = {
|
|
54
|
+
eq: (values) => values[0],
|
|
55
|
+
ne: (values) => ({ $ne: values[0] }),
|
|
56
|
+
lt: (values) => ({ $lt: values[0] }),
|
|
57
|
+
gt: (values) => ({ $gt: values[0] }),
|
|
58
|
+
lte: (values) => ({ $lte: values[0] }),
|
|
59
|
+
gte: (values) => ({ $gte: values[0] }),
|
|
60
|
+
in: (values) => ({ $in: values }),
|
|
61
|
+
isDefined: (values) => ({ $exists: values[0] }),
|
|
62
|
+
contains: buildRegexQuery.bind(null, 'contains'),
|
|
63
|
+
beginsWith: buildRegexQuery.bind(null, 'begins-with'),
|
|
64
|
+
includes: buildIncludes.bind(null, 'contains'),
|
|
65
|
+
regex: buildRegexQuery.bind(null, 'regex'),
|
|
66
|
+
iRegex: buildRegexQuery.bind(null, 'iRegex'),
|
|
67
|
+
};
|
|
68
|
+
function buildIncludes(operation, values) {
|
|
69
|
+
const matcher = values[0];
|
|
70
|
+
if (typeof matcher === 'string') {
|
|
71
|
+
return { $regex: new RegExp(matcher) };
|
|
72
|
+
}
|
|
73
|
+
return { $elemMatch: matcher };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Builds a regex out of string GraphQL queries
|
|
77
|
+
*/
|
|
78
|
+
function buildRegexQuery(operation, values) {
|
|
79
|
+
const matcher = values[0];
|
|
80
|
+
if (typeof matcher != 'string') {
|
|
81
|
+
throw new Error(`Attempted to perform a ${operation} operation on a non-string`);
|
|
82
|
+
}
|
|
83
|
+
if (operation === 'begins-with') {
|
|
84
|
+
return { $regex: new RegExp(`^${matcher}`) };
|
|
85
|
+
}
|
|
86
|
+
if (operation === 'iRegex') {
|
|
87
|
+
return { $regex: new RegExp(matcher, 'i') };
|
|
88
|
+
}
|
|
89
|
+
return { $regex: new RegExp(matcher) };
|
|
90
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const registeredUsersDatabase: string;
|
|
2
|
+
export declare const authenticatedUsersDatabase: string;
|
|
3
|
+
export declare const eventsDatabase: string;
|
|
4
|
+
export declare const readModelsDatabase: string;
|
|
5
|
+
export declare const connectionsDatabase: string;
|
|
6
|
+
export declare const subscriptionDatabase: string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.subscriptionDatabase = exports.connectionsDatabase = exports.readModelsDatabase = exports.eventsDatabase = exports.authenticatedUsersDatabase = exports.registeredUsersDatabase = void 0;
|
|
4
|
+
// Paths used by the local provider internally
|
|
5
|
+
const path = require("path");
|
|
6
|
+
exports.registeredUsersDatabase = internalPath('registered_users.json');
|
|
7
|
+
exports.authenticatedUsersDatabase = internalPath('authenticated_users.json');
|
|
8
|
+
exports.eventsDatabase = internalPath('events.json');
|
|
9
|
+
exports.readModelsDatabase = internalPath('read_models.json');
|
|
10
|
+
exports.connectionsDatabase = internalPath('connections.json');
|
|
11
|
+
exports.subscriptionDatabase = internalPath('subscriptions.json');
|
|
12
|
+
function internalPath(filename) {
|
|
13
|
+
return path.normalize(path.join('.', '.magek', filename));
|
|
14
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { FastifyInstance } from 'fastify';
|
|
2
|
+
import { UserApp } from '@magek/common';
|
|
3
|
+
import { WebSocketRegistry } from './infrastructure/websocket-registry';
|
|
4
|
+
/**
|
|
5
|
+
* Get the global WebSocket registry instance
|
|
6
|
+
*/
|
|
7
|
+
export declare function getWebSocketRegistry(): WebSocketRegistry;
|
|
8
|
+
/**
|
|
9
|
+
* Send a message to a WebSocket connection
|
|
10
|
+
*/
|
|
11
|
+
export declare function sendWebSocketMessage(connectionId: string, data: unknown): void;
|
|
12
|
+
export interface ServerOptions {
|
|
13
|
+
/** Enable Fastify logging. Default: true */
|
|
14
|
+
logger?: boolean;
|
|
15
|
+
/** Maximum request body size in bytes. Default: 6MB */
|
|
16
|
+
bodyLimit?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a configured Fastify server instance for a Magek application.
|
|
20
|
+
*
|
|
21
|
+
* @param userApp - The user's Magek application module
|
|
22
|
+
* @param options - Optional server configuration
|
|
23
|
+
* @returns A configured Fastify instance ready to listen
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { createServer } from '@magek/server'
|
|
28
|
+
* import * as myApp from './dist'
|
|
29
|
+
*
|
|
30
|
+
* const server = await createServer(myApp)
|
|
31
|
+
* await server.listen({ port: 3000 })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function createServer(userApp: UserApp, options?: ServerOptions): Promise<FastifyInstance>;
|
|
35
|
+
declare module 'http' {
|
|
36
|
+
interface IncomingMessage {
|
|
37
|
+
rawBody: Buffer;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
declare module 'fastify' {
|
|
41
|
+
interface FastifyRequest {
|
|
42
|
+
rawBody: Buffer;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getWebSocketRegistry = getWebSocketRegistry;
|
|
4
|
+
exports.sendWebSocketMessage = sendWebSocketMessage;
|
|
5
|
+
exports.createServer = createServer;
|
|
6
|
+
const fastify_1 = require("fastify");
|
|
7
|
+
const websocket_1 = require("@fastify/websocket");
|
|
8
|
+
const fastify_sse_v2_1 = require("fastify-sse-v2");
|
|
9
|
+
const cors_1 = require("@fastify/cors");
|
|
10
|
+
const services_1 = require("./services");
|
|
11
|
+
const graphql_1 = require("./infrastructure/controllers/graphql");
|
|
12
|
+
const health_controller_1 = require("./infrastructure/controllers/health-controller");
|
|
13
|
+
const websocket_registry_1 = require("./infrastructure/websocket-registry");
|
|
14
|
+
const http_1 = require("./infrastructure/http");
|
|
15
|
+
const scheduler_1 = require("./infrastructure/scheduler");
|
|
16
|
+
// Global WebSocket registry instance
|
|
17
|
+
let globalWebSocketRegistry;
|
|
18
|
+
/**
|
|
19
|
+
* Get the global WebSocket registry instance
|
|
20
|
+
*/
|
|
21
|
+
function getWebSocketRegistry() {
|
|
22
|
+
if (!globalWebSocketRegistry) {
|
|
23
|
+
throw new Error('WebSocket registry not initialized. Make sure the server is started.');
|
|
24
|
+
}
|
|
25
|
+
return globalWebSocketRegistry;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Send a message to a WebSocket connection
|
|
29
|
+
*/
|
|
30
|
+
function sendWebSocketMessage(connectionId, data) {
|
|
31
|
+
const registry = getWebSocketRegistry();
|
|
32
|
+
registry.sendMessage(connectionId, data);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Default error handling for Fastify requests.
|
|
36
|
+
*/
|
|
37
|
+
async function defaultErrorHandler(error, request, reply) {
|
|
38
|
+
if (reply.sent) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.error(error);
|
|
42
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
43
|
+
await (0, http_1.requestFailed)(err, reply);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a configured Fastify server instance for a Magek application.
|
|
47
|
+
*
|
|
48
|
+
* @param userApp - The user's Magek application module
|
|
49
|
+
* @param options - Optional server configuration
|
|
50
|
+
* @returns A configured Fastify instance ready to listen
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* import { createServer } from '@magek/server'
|
|
55
|
+
* import * as myApp from './dist'
|
|
56
|
+
*
|
|
57
|
+
* const server = await createServer(myApp)
|
|
58
|
+
* await server.listen({ port: 3000 })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async function createServer(userApp, options = {}) {
|
|
62
|
+
const { logger = true, bodyLimit = 6 * 1024 * 1024 } = options;
|
|
63
|
+
// Initialize WebSocket registry
|
|
64
|
+
globalWebSocketRegistry = new websocket_registry_1.WebSocketRegistry();
|
|
65
|
+
global.webSocketRegistry = globalWebSocketRegistry;
|
|
66
|
+
const fastify = (0, fastify_1.default)({
|
|
67
|
+
logger,
|
|
68
|
+
bodyLimit,
|
|
69
|
+
});
|
|
70
|
+
// Register plugins
|
|
71
|
+
await fastify.register(cors_1.default, {
|
|
72
|
+
origin: true,
|
|
73
|
+
credentials: true,
|
|
74
|
+
});
|
|
75
|
+
await fastify.register(websocket_1.default);
|
|
76
|
+
await fastify.register(fastify_sse_v2_1.FastifySSEPlugin);
|
|
77
|
+
// Add raw body support for GraphQL requests
|
|
78
|
+
fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {
|
|
79
|
+
try {
|
|
80
|
+
const rawBody = body;
|
|
81
|
+
req.rawBody = rawBody;
|
|
82
|
+
const json = JSON.parse(rawBody.toString());
|
|
83
|
+
done(null, json);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
done(err, undefined);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const graphQLService = new services_1.GraphQLService(userApp);
|
|
90
|
+
const healthService = new services_1.HealthService(userApp);
|
|
91
|
+
// Register GraphQL endpoint
|
|
92
|
+
const graphQLController = new graphql_1.GraphQLController(graphQLService);
|
|
93
|
+
await fastify.register((instance) => {
|
|
94
|
+
instance.post('/graphql', graphQLController.handleGraphQL.bind(graphQLController));
|
|
95
|
+
});
|
|
96
|
+
// Register Health endpoint
|
|
97
|
+
const healthController = new health_controller_1.HealthController(healthService);
|
|
98
|
+
await fastify.register((instance) => {
|
|
99
|
+
instance.get('/sensor/health/*', healthController.handleHealth.bind(healthController));
|
|
100
|
+
});
|
|
101
|
+
// Register WebSocket endpoint
|
|
102
|
+
await fastify.register((instance) => {
|
|
103
|
+
instance.get('/websocket', { websocket: true }, (connection, req) => {
|
|
104
|
+
const connectionId = req.connectionId || `conn_${Date.now()}_${Math.random()}`;
|
|
105
|
+
globalWebSocketRegistry.addConnection(connectionId, connection.socket);
|
|
106
|
+
connection.socket.on('message', async (message) => {
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(message.toString());
|
|
109
|
+
const webSocketRequest = {
|
|
110
|
+
connectionContext: {
|
|
111
|
+
connectionId,
|
|
112
|
+
eventType: 'MESSAGE',
|
|
113
|
+
},
|
|
114
|
+
data,
|
|
115
|
+
incomingMessage: req.raw,
|
|
116
|
+
};
|
|
117
|
+
await graphQLService.handleGraphQLRequest(webSocketRequest);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error('WebSocket message error:', error);
|
|
121
|
+
connection.socket.send(JSON.stringify({
|
|
122
|
+
type: 'error',
|
|
123
|
+
payload: { message: 'Failed to process message' },
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
connection.socket.on('close', async () => {
|
|
128
|
+
const webSocketRequest = {
|
|
129
|
+
connectionContext: {
|
|
130
|
+
connectionId,
|
|
131
|
+
eventType: 'DISCONNECT',
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
await graphQLService.handleGraphQLRequest(webSocketRequest);
|
|
135
|
+
});
|
|
136
|
+
const webSocketRequest = {
|
|
137
|
+
connectionContext: {
|
|
138
|
+
connectionId,
|
|
139
|
+
eventType: 'CONNECT',
|
|
140
|
+
},
|
|
141
|
+
incomingMessage: req.raw,
|
|
142
|
+
};
|
|
143
|
+
void graphQLService.handleGraphQLRequest(webSocketRequest);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// Register SSE endpoint
|
|
147
|
+
await fastify.register((instance) => {
|
|
148
|
+
instance.get('/sse', (request, reply) => {
|
|
149
|
+
const connectionId = `sse_${Date.now()}_${Math.random()}`;
|
|
150
|
+
reply.sse({
|
|
151
|
+
id: connectionId,
|
|
152
|
+
event: 'connection',
|
|
153
|
+
data: JSON.stringify({
|
|
154
|
+
type: 'connection_init',
|
|
155
|
+
payload: { connectionId },
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
const webSocketRequest = {
|
|
159
|
+
connectionContext: {
|
|
160
|
+
connectionId,
|
|
161
|
+
eventType: 'CONNECT',
|
|
162
|
+
},
|
|
163
|
+
incomingMessage: request.raw,
|
|
164
|
+
};
|
|
165
|
+
void graphQLService.handleGraphQLRequest(webSocketRequest);
|
|
166
|
+
const pingInterval = setInterval(() => {
|
|
167
|
+
if (!reply.sent) {
|
|
168
|
+
reply.sse({
|
|
169
|
+
id: Date.now().toString(),
|
|
170
|
+
event: 'ping',
|
|
171
|
+
data: JSON.stringify({ type: 'ping', timestamp: Date.now() }),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}, 30000);
|
|
175
|
+
request.raw.on('close', async () => {
|
|
176
|
+
clearInterval(pingInterval);
|
|
177
|
+
const disconnectRequest = {
|
|
178
|
+
connectionContext: {
|
|
179
|
+
connectionId,
|
|
180
|
+
eventType: 'DISCONNECT',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
await graphQLService.handleGraphQLRequest(disconnectRequest);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
fastify.setErrorHandler(async (error, request, reply) => {
|
|
188
|
+
await defaultErrorHandler(error, request, reply);
|
|
189
|
+
});
|
|
190
|
+
// Configure scheduled commands
|
|
191
|
+
const config = userApp.Magek.config;
|
|
192
|
+
if (config) {
|
|
193
|
+
(0, scheduler_1.configureScheduler)(config, userApp);
|
|
194
|
+
}
|
|
195
|
+
await fastify.ready();
|
|
196
|
+
return fastify;
|
|
197
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReadModelEnvelope, UserApp } from '@magek/common';
|
|
2
|
+
import { FastifyRequest } from 'fastify';
|
|
3
|
+
import { WebSocketMessage } from '../library/graphql-adapter';
|
|
4
|
+
export declare class GraphQLService {
|
|
5
|
+
readonly userApp: UserApp;
|
|
6
|
+
constructor(userApp: UserApp);
|
|
7
|
+
handleGraphQLRequest(request: FastifyRequest | WebSocketMessage): Promise<any>;
|
|
8
|
+
handleNotificationSubscription(request: Array<ReadModelEnvelope>): Promise<unknown>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GraphQLService = void 0;
|
|
4
|
+
class GraphQLService {
|
|
5
|
+
constructor(userApp) {
|
|
6
|
+
this.userApp = userApp;
|
|
7
|
+
}
|
|
8
|
+
async handleGraphQLRequest(request) {
|
|
9
|
+
return await this.userApp.graphQLDispatcher(request);
|
|
10
|
+
}
|
|
11
|
+
async handleNotificationSubscription(request) {
|
|
12
|
+
return await this.userApp.notifySubscribers(request);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.GraphQLService = GraphQLService;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HealthService = void 0;
|
|
4
|
+
class HealthService {
|
|
5
|
+
constructor(userApp) {
|
|
6
|
+
this.userApp = userApp;
|
|
7
|
+
}
|
|
8
|
+
async handleHealthRequest(request) {
|
|
9
|
+
return await this.userApp.health(request);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.HealthService = HealthService;
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magek/server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Debug your Magek projects locally",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"server"
|
|
7
|
+
],
|
|
8
|
+
"author": "Boosterin Labs SLU",
|
|
9
|
+
"homepage": "https://magek.ai",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/theam/magek.git"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.0.0 <23.0.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@magek/common": "^0.0.1",
|
|
30
|
+
"@fastify/cors": "11.2.0",
|
|
31
|
+
"@fastify/websocket": "11.2.0",
|
|
32
|
+
"@seald-io/nedb": "4.1.2",
|
|
33
|
+
"fastify": "5.6.2",
|
|
34
|
+
"fastify-sse-v2": "4.2.1",
|
|
35
|
+
"node-schedule": "2.1.1",
|
|
36
|
+
"tslib": "2.8.1"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/theam/magek/issues"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@magek/eslint-config": "^0.0.1",
|
|
43
|
+
"@types/chai": "5.2.3",
|
|
44
|
+
"@types/chai-as-promised": "8.0.2",
|
|
45
|
+
"@types/mocha": "10.0.10",
|
|
46
|
+
"@types/node": "22.19.3",
|
|
47
|
+
"@types/node-schedule": "2.1.8",
|
|
48
|
+
"@types/sinon": "21.0.0",
|
|
49
|
+
"@types/sinon-chai": "4.0.0",
|
|
50
|
+
"chai": "6.2.2",
|
|
51
|
+
"chai-as-promised": "8.0.2",
|
|
52
|
+
"@faker-js/faker": "10.2.0",
|
|
53
|
+
"mocha": "11.7.5",
|
|
54
|
+
"c8": "^10.1.3",
|
|
55
|
+
"rimraf": "6.1.2",
|
|
56
|
+
"sinon": "21.0.1",
|
|
57
|
+
"sinon-chai": "4.0.1",
|
|
58
|
+
"tsx": "^4.19.2",
|
|
59
|
+
"typescript": "5.9.3"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"format": "prettier --write --ext '.js,.ts' **/*.ts **/*/*.ts",
|
|
63
|
+
"lint:check": "eslint \"**/*.ts\"",
|
|
64
|
+
"lint:fix": "eslint --quiet --fix \"**/*.ts\"",
|
|
65
|
+
"build": "tsc -b tsconfig.json",
|
|
66
|
+
"clean": "rimraf ./dist tsconfig.tsbuildinfo",
|
|
67
|
+
"test:provider-local": "npm run test",
|
|
68
|
+
"test": "tsc --noEmit -p tsconfig.test.json && MAGEK_ENV=test c8 mocha --forbid-only \"test/**/*.test.ts\""
|
|
69
|
+
}
|
|
70
|
+
}
|