@bestoneconsulting/sap-b1-bridge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.d.ts +44 -0
- package/dist/core.js +587 -0
- package/dist/drizzle-storage.d.ts +2 -0
- package/dist/drizzle-storage.js +61 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/storage.d.ts +87 -0
- package/dist/storage.js +123 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +1 -0
- package/package.json +36 -0
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
import type { SAPB1BridgeOptions, SAPB1BridgeAPI, AgentInfo, QueryResult, ServiceLayerRequest, DiApiRequest, AgentCapabilities, CompanyInfo } from "./types";
|
|
3
|
+
import { type IBridgeStorage } from "./storage";
|
|
4
|
+
export declare class SAPB1Bridge implements SAPB1BridgeAPI {
|
|
5
|
+
private app;
|
|
6
|
+
private server;
|
|
7
|
+
private storage;
|
|
8
|
+
private appName;
|
|
9
|
+
private wsPath;
|
|
10
|
+
private apiPrefix;
|
|
11
|
+
private wss;
|
|
12
|
+
private connectedAgents;
|
|
13
|
+
private agentCaps;
|
|
14
|
+
private uiClients;
|
|
15
|
+
private pendingPings;
|
|
16
|
+
private pendingQueries;
|
|
17
|
+
constructor(options: SAPB1BridgeOptions, storageInstance?: IBridgeStorage);
|
|
18
|
+
private broadcastToUI;
|
|
19
|
+
private setupWebSocket;
|
|
20
|
+
private setupAPIRoutes;
|
|
21
|
+
executeQuery(agentId: string, sql: string): Promise<QueryResult>;
|
|
22
|
+
callServiceLayer(agentId: string, request: ServiceLayerRequest): Promise<QueryResult>;
|
|
23
|
+
callDiApi(agentId: string, request: DiApiRequest): Promise<QueryResult>;
|
|
24
|
+
private waitForResult;
|
|
25
|
+
getAgents(): Promise<AgentInfo[]>;
|
|
26
|
+
getAgent(id: string): Promise<AgentInfo | undefined>;
|
|
27
|
+
registerAgent(options: {
|
|
28
|
+
name: string;
|
|
29
|
+
serverName: string;
|
|
30
|
+
capabilities?: string[];
|
|
31
|
+
companyId?: string;
|
|
32
|
+
}): Promise<AgentInfo>;
|
|
33
|
+
deleteAgent(id: string): Promise<void>;
|
|
34
|
+
getCompanies(): Promise<CompanyInfo[]>;
|
|
35
|
+
createCompany(name: string, description?: string): Promise<CompanyInfo>;
|
|
36
|
+
deleteCompany(id: string): Promise<void>;
|
|
37
|
+
getAgentCapabilities(agentId: string): AgentCapabilities | null;
|
|
38
|
+
refreshAgentCapabilities(agentId: string): Promise<boolean>;
|
|
39
|
+
mountUI(mountPath?: string, staticDir?: string): void;
|
|
40
|
+
getConnectedAgentIds(): string[];
|
|
41
|
+
isAgentConnected(agentId: string): boolean;
|
|
42
|
+
getWebSocketServer(): WebSocketServer;
|
|
43
|
+
getStorage(): IBridgeStorage;
|
|
44
|
+
}
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { MemBridgeStorage } from "./storage";
|
|
6
|
+
const capabilityStatusSchema = z.object({
|
|
7
|
+
enabled: z.boolean(),
|
|
8
|
+
available: z.boolean(),
|
|
9
|
+
reason: z.string().nullable(),
|
|
10
|
+
});
|
|
11
|
+
const agentCapabilitiesSchema = z.object({
|
|
12
|
+
sql: capabilityStatusSchema,
|
|
13
|
+
serviceLayer: capabilityStatusSchema,
|
|
14
|
+
diApi: capabilityStatusSchema,
|
|
15
|
+
});
|
|
16
|
+
const sapServiceLayerPayloadSchema = z.object({
|
|
17
|
+
method: z.enum(["GET", "POST", "PATCH", "PUT", "DELETE"]),
|
|
18
|
+
endpoint: z.string().min(1),
|
|
19
|
+
body: z.any().optional(),
|
|
20
|
+
queryParams: z.record(z.string()).optional(),
|
|
21
|
+
});
|
|
22
|
+
const sapDiApiPayloadSchema = z.object({
|
|
23
|
+
version: z.string().optional().default("1.0"),
|
|
24
|
+
operation: z.enum(["di_get", "di_add", "di_update", "di_action", "di_query"]),
|
|
25
|
+
object: z.string().min(1),
|
|
26
|
+
key: z.record(z.union([z.string(), z.number()])).optional(),
|
|
27
|
+
sapXml: z.string().optional(),
|
|
28
|
+
action: z.string().optional(),
|
|
29
|
+
options: z.object({
|
|
30
|
+
dryRun: z.boolean().optional(),
|
|
31
|
+
transaction: z.boolean().optional(),
|
|
32
|
+
returnXml: z.boolean().optional(),
|
|
33
|
+
allowDiQuery: z.boolean().optional(),
|
|
34
|
+
}).optional(),
|
|
35
|
+
});
|
|
36
|
+
const TARGET_TO_CAPABILITY_KEY = {
|
|
37
|
+
sql_server: "sql",
|
|
38
|
+
sap_service_layer: "serviceLayer",
|
|
39
|
+
sap_di_api: "diApi",
|
|
40
|
+
};
|
|
41
|
+
export class SAPB1Bridge {
|
|
42
|
+
constructor(options, storageInstance) {
|
|
43
|
+
this.connectedAgents = new Map();
|
|
44
|
+
this.agentCaps = new Map();
|
|
45
|
+
this.uiClients = new Set();
|
|
46
|
+
this.pendingPings = new Map();
|
|
47
|
+
this.pendingQueries = new Map();
|
|
48
|
+
this.app = options.app;
|
|
49
|
+
this.server = options.server;
|
|
50
|
+
this.storage = storageInstance || new MemBridgeStorage();
|
|
51
|
+
this.appName = options.appName || "SAP B1 Bridge";
|
|
52
|
+
this.wsPath = options.wsPath || "/ws";
|
|
53
|
+
this.apiPrefix = options.apiPrefix || "/api";
|
|
54
|
+
this.wss = new WebSocketServer({ server: this.server, path: this.wsPath });
|
|
55
|
+
this.setupWebSocket();
|
|
56
|
+
this.setupAPIRoutes();
|
|
57
|
+
}
|
|
58
|
+
broadcastToUI(message) {
|
|
59
|
+
const payload = JSON.stringify(message);
|
|
60
|
+
this.uiClients.forEach((client) => {
|
|
61
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
62
|
+
client.send(payload);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
setupWebSocket() {
|
|
67
|
+
this.wss.on("connection", (ws, req) => {
|
|
68
|
+
let agentId = null;
|
|
69
|
+
let isUIClient = false;
|
|
70
|
+
const requestHost = req?.headers?.host;
|
|
71
|
+
ws.on("message", async (message) => {
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(message.toString());
|
|
74
|
+
if (data.type === "ui_connect") {
|
|
75
|
+
isUIClient = true;
|
|
76
|
+
this.uiClients.add(ws);
|
|
77
|
+
ws.send(JSON.stringify({ type: "ui_connected" }));
|
|
78
|
+
}
|
|
79
|
+
else if (data.type === "auth") {
|
|
80
|
+
const agent = await this.storage.getAgentByApiKey(data.apiKey);
|
|
81
|
+
if (agent) {
|
|
82
|
+
agentId = agent.id;
|
|
83
|
+
this.connectedAgents.set(agent.id, ws);
|
|
84
|
+
await this.storage.updateAgentStatus(agent.id, "online", new Date());
|
|
85
|
+
ws.send(JSON.stringify({
|
|
86
|
+
type: "auth_success",
|
|
87
|
+
agentId: agent.id,
|
|
88
|
+
connectionName: agent.name,
|
|
89
|
+
appName: this.appName,
|
|
90
|
+
appUrl: process.env.REPLIT_DEV_DOMAIN
|
|
91
|
+
? `https://${process.env.REPLIT_DEV_DOMAIN}`
|
|
92
|
+
: requestHost
|
|
93
|
+
? `https://${requestHost}`
|
|
94
|
+
: undefined,
|
|
95
|
+
}));
|
|
96
|
+
this.broadcastToUI({
|
|
97
|
+
type: "agent_status_update",
|
|
98
|
+
agentId: agent.id,
|
|
99
|
+
status: "online",
|
|
100
|
+
});
|
|
101
|
+
console.log(`[SAP B1 Bridge] Agent authenticated: ${agent.name}`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
ws.send(JSON.stringify({ type: "auth_failed" }));
|
|
105
|
+
ws.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (data.type === "heartbeat" && agentId) {
|
|
109
|
+
await this.storage.updateAgentStatus(agentId, "online", new Date());
|
|
110
|
+
ws.send(JSON.stringify({ type: "heartbeat_ack" }));
|
|
111
|
+
}
|
|
112
|
+
else if (data.type === "capabilities" && agentId) {
|
|
113
|
+
try {
|
|
114
|
+
const caps = agentCapabilitiesSchema.parse(data.capabilities);
|
|
115
|
+
this.agentCaps.set(agentId, caps);
|
|
116
|
+
this.broadcastToUI({
|
|
117
|
+
type: "agent_capabilities_update",
|
|
118
|
+
agentId,
|
|
119
|
+
capabilities: caps,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
console.error(`[SAP B1 Bridge] Invalid capabilities from agent: ${agentId}`, e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (data.type === "query_result" && agentId) {
|
|
127
|
+
if (!data.result) {
|
|
128
|
+
data.result = data.results || data.rows || data.data || data.recordset;
|
|
129
|
+
}
|
|
130
|
+
const resultData = data.result || data.results;
|
|
131
|
+
if (resultData && resultData.capabilities && typeof data.queryId === "string" && data.queryId.startsWith("cap-")) {
|
|
132
|
+
try {
|
|
133
|
+
const caps = agentCapabilitiesSchema.parse(resultData.capabilities);
|
|
134
|
+
this.agentCaps.set(agentId, caps);
|
|
135
|
+
this.broadcastToUI({
|
|
136
|
+
type: "agent_capabilities_update",
|
|
137
|
+
agentId,
|
|
138
|
+
capabilities: caps,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.error("[SAP B1 Bridge] Invalid capabilities in refresh response:", e);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const { queryId, result, rowCount, executionTime, error } = data;
|
|
147
|
+
const pending = this.pendingQueries.get(queryId);
|
|
148
|
+
if (pending) {
|
|
149
|
+
clearTimeout(pending.timer);
|
|
150
|
+
this.pendingQueries.delete(queryId);
|
|
151
|
+
if (error) {
|
|
152
|
+
pending.resolve({ id: queryId, status: "error", error });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
pending.resolve({ id: queryId, status: "completed", result, rowCount, executionTime });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (error) {
|
|
159
|
+
await this.storage.updateQueryStatus(queryId, "error", {
|
|
160
|
+
error,
|
|
161
|
+
completedAt: new Date(),
|
|
162
|
+
});
|
|
163
|
+
this.broadcastToUI({ type: "query_completed", queryId, status: "error", error });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await this.storage.updateQueryStatus(queryId, "completed", {
|
|
167
|
+
result,
|
|
168
|
+
rowCount,
|
|
169
|
+
executionTime,
|
|
170
|
+
completedAt: new Date(),
|
|
171
|
+
});
|
|
172
|
+
this.broadcastToUI({ type: "query_completed", queryId, status: "completed", rowCount, executionTime });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (data.type === "pong" && data.pingId) {
|
|
176
|
+
const pending = this.pendingPings.get(data.pingId);
|
|
177
|
+
if (pending) {
|
|
178
|
+
clearTimeout(pending.timer);
|
|
179
|
+
this.pendingPings.delete(data.pingId);
|
|
180
|
+
pending.resolve(true);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (data.type === "poll" && agentId) {
|
|
184
|
+
const pendingQueries = await this.storage.getPendingQueriesForAgent(agentId);
|
|
185
|
+
if (pendingQueries.length > 0) {
|
|
186
|
+
const query = pendingQueries[0];
|
|
187
|
+
await this.storage.updateQueryStatus(query.id, "executing", { executedAt: new Date() });
|
|
188
|
+
if (query.targetType === "sql_server") {
|
|
189
|
+
ws.send(JSON.stringify({
|
|
190
|
+
type: "execute_query",
|
|
191
|
+
queryId: query.id,
|
|
192
|
+
sqlQuery: query.sqlQuery,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const payload = query.requestPayload || {};
|
|
197
|
+
if (payload.method && !payload.operation) {
|
|
198
|
+
payload.operation = payload.method;
|
|
199
|
+
}
|
|
200
|
+
ws.send(JSON.stringify({
|
|
201
|
+
type: "execute_request",
|
|
202
|
+
queryId: query.id,
|
|
203
|
+
targetType: query.targetType,
|
|
204
|
+
requestPayload: payload,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
this.broadcastToUI({ type: "query_executing", queryId: query.id });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
ws.send(JSON.stringify({ type: "no_queries" }));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error("[SAP B1 Bridge] WebSocket message error:", error);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
ws.on("close", async () => {
|
|
219
|
+
if (agentId) {
|
|
220
|
+
this.connectedAgents.delete(agentId);
|
|
221
|
+
this.agentCaps.delete(agentId);
|
|
222
|
+
await this.storage.updateAgentStatus(agentId, "offline", new Date());
|
|
223
|
+
this.broadcastToUI({ type: "agent_status_update", agentId, status: "offline" });
|
|
224
|
+
this.broadcastToUI({ type: "agent_capabilities_update", agentId, capabilities: null });
|
|
225
|
+
}
|
|
226
|
+
if (isUIClient) {
|
|
227
|
+
this.uiClients.delete(ws);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
setupAPIRoutes() {
|
|
233
|
+
const prefix = this.apiPrefix;
|
|
234
|
+
const app = this.app;
|
|
235
|
+
app.get(`${prefix}/config`, (_req, res) => {
|
|
236
|
+
res.json({ appName: this.appName });
|
|
237
|
+
});
|
|
238
|
+
app.post(`${prefix}/agents/:id/test`, async (req, res) => {
|
|
239
|
+
try {
|
|
240
|
+
const agent = await this.storage.getAgent(req.params.id);
|
|
241
|
+
if (!agent)
|
|
242
|
+
return res.status(404).json({ error: "Agent not found" });
|
|
243
|
+
const ws = this.connectedAgents.get(agent.id);
|
|
244
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
245
|
+
return res.json({ success: false, message: "Agent is not connected" });
|
|
246
|
+
}
|
|
247
|
+
const pingId = `ping_${Date.now()}`;
|
|
248
|
+
const result = await new Promise((resolve) => {
|
|
249
|
+
const timer = setTimeout(() => { this.pendingPings.delete(pingId); resolve(false); }, 5000);
|
|
250
|
+
this.pendingPings.set(pingId, { resolve, timer });
|
|
251
|
+
ws.send(JSON.stringify({ type: "ping", pingId }));
|
|
252
|
+
});
|
|
253
|
+
res.json(result
|
|
254
|
+
? { success: true, message: "Agent responded successfully" }
|
|
255
|
+
: { success: false, message: "Agent did not respond within 5 seconds" });
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
res.status(500).json({ error: "Failed to test connection" });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
app.get(`${prefix}/agents/capabilities`, async (_req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
const result = {};
|
|
264
|
+
const allAgents = await this.storage.getAgents();
|
|
265
|
+
for (const agent of allAgents) {
|
|
266
|
+
result[agent.id] = this.agentCaps.get(agent.id) || null;
|
|
267
|
+
}
|
|
268
|
+
res.json(result);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
res.status(500).json({ error: "Failed to fetch capabilities" });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
app.post(`${prefix}/agents/:id/refresh-capabilities`, async (req, res) => {
|
|
275
|
+
try {
|
|
276
|
+
const agent = await this.storage.getAgent(req.params.id);
|
|
277
|
+
if (!agent)
|
|
278
|
+
return res.status(404).json({ error: "Agent not found" });
|
|
279
|
+
const ws = this.connectedAgents.get(agent.id);
|
|
280
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
281
|
+
return res.json({ success: false, message: "Agent is not connected" });
|
|
282
|
+
}
|
|
283
|
+
const queryId = `cap-${Date.now()}`;
|
|
284
|
+
ws.send(JSON.stringify({
|
|
285
|
+
type: "execute_request",
|
|
286
|
+
queryId,
|
|
287
|
+
targetType: "agent",
|
|
288
|
+
requestPayload: { operation: "agent_capabilities" },
|
|
289
|
+
}));
|
|
290
|
+
res.json({ success: true, message: "Capability refresh requested" });
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
res.status(500).json({ error: "Failed to refresh capabilities" });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
app.get(`${prefix}/agents`, async (_req, res) => {
|
|
297
|
+
try {
|
|
298
|
+
res.json(await this.storage.getAgents());
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
res.status(500).json({ error: "Failed to fetch agents" });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
app.post(`${prefix}/agents`, async (req, res) => {
|
|
305
|
+
try {
|
|
306
|
+
const agent = await this.storage.createAgent(req.body);
|
|
307
|
+
res.json(agent);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
res.status(500).json({ error: "Failed to create agent" });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
app.patch(`${prefix}/agents/:id`, async (req, res) => {
|
|
314
|
+
try {
|
|
315
|
+
await this.storage.updateAgent(req.params.id, req.body);
|
|
316
|
+
const agent = await this.storage.getAgent(req.params.id);
|
|
317
|
+
res.json(agent);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
res.status(500).json({ error: "Failed to update agent" });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
app.delete(`${prefix}/agents/:id`, async (req, res) => {
|
|
324
|
+
try {
|
|
325
|
+
await this.storage.deleteAgent(req.params.id);
|
|
326
|
+
res.json({ success: true });
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
res.status(500).json({ error: "Failed to delete agent" });
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
app.get(`${prefix}/companies`, async (_req, res) => {
|
|
333
|
+
try {
|
|
334
|
+
res.json(await this.storage.getCompanies());
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
res.status(500).json({ error: "Failed to fetch companies" });
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
app.post(`${prefix}/companies`, async (req, res) => {
|
|
341
|
+
try {
|
|
342
|
+
const company = await this.storage.createCompany(req.body.name, req.body.description);
|
|
343
|
+
res.json(company);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
res.status(500).json({ error: "Failed to create company" });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
app.patch(`${prefix}/companies/:id`, async (req, res) => {
|
|
350
|
+
try {
|
|
351
|
+
await this.storage.updateCompany(req.params.id, req.body);
|
|
352
|
+
const company = await this.storage.getCompany(req.params.id);
|
|
353
|
+
res.json(company);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
res.status(500).json({ error: "Failed to update company" });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
app.delete(`${prefix}/companies/:id`, async (req, res) => {
|
|
360
|
+
try {
|
|
361
|
+
const allAgents = await this.storage.getAgents();
|
|
362
|
+
for (const agent of allAgents.filter((a) => a.companyId === req.params.id)) {
|
|
363
|
+
await this.storage.updateAgent(agent.id, { companyId: null });
|
|
364
|
+
}
|
|
365
|
+
await this.storage.deleteCompany(req.params.id);
|
|
366
|
+
res.json({ success: true });
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
res.status(500).json({ error: "Failed to delete company" });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
app.get(`${prefix}/queries`, async (_req, res) => {
|
|
373
|
+
try {
|
|
374
|
+
res.json(await this.storage.getQueries());
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
res.status(500).json({ error: "Failed to fetch queries" });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
app.get(`${prefix}/queries/recent`, async (_req, res) => {
|
|
381
|
+
try {
|
|
382
|
+
res.json(await this.storage.getRecentQueries(10));
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
res.status(500).json({ error: "Failed to fetch recent queries" });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
app.get(`${prefix}/queries/active`, async (_req, res) => {
|
|
389
|
+
try {
|
|
390
|
+
res.json(await this.storage.getActiveQuery());
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
res.status(500).json({ error: "Failed to fetch active query" });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
app.post(`${prefix}/queries`, async (req, res) => {
|
|
397
|
+
try {
|
|
398
|
+
const data = req.body;
|
|
399
|
+
if (data.agentId) {
|
|
400
|
+
const caps = this.agentCaps.get(data.agentId);
|
|
401
|
+
if (caps) {
|
|
402
|
+
const capKey = TARGET_TO_CAPABILITY_KEY[data.targetType || "sql_server"];
|
|
403
|
+
if (capKey) {
|
|
404
|
+
const capStatus = caps[capKey];
|
|
405
|
+
if (capStatus && !capStatus.available) {
|
|
406
|
+
return res.status(400).json({
|
|
407
|
+
success: false,
|
|
408
|
+
code: "capability_unavailable",
|
|
409
|
+
capability: capKey,
|
|
410
|
+
reason: capStatus.reason || "Capability is not available on this agent",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (data.targetType === "sap_service_layer") {
|
|
417
|
+
if (!data.requestPayload)
|
|
418
|
+
return res.status(400).json({ error: "requestPayload is required for SAP Service Layer requests" });
|
|
419
|
+
sapServiceLayerPayloadSchema.parse(data.requestPayload);
|
|
420
|
+
if (!data.sqlQuery || !data.sqlQuery.trim()) {
|
|
421
|
+
const payload = data.requestPayload;
|
|
422
|
+
data.sqlQuery = `${payload.method || "GET"} ${payload.endpoint || "/"}`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (data.targetType === "sap_di_api") {
|
|
426
|
+
if (!data.requestPayload)
|
|
427
|
+
return res.status(400).json({ error: "requestPayload is required for SAP DI API requests" });
|
|
428
|
+
sapDiApiPayloadSchema.parse(data.requestPayload);
|
|
429
|
+
if (!data.sqlQuery || !data.sqlQuery.trim()) {
|
|
430
|
+
const payload = data.requestPayload;
|
|
431
|
+
data.sqlQuery = `DI:${payload.operation || "unknown"} ${payload.object || "unknown"}`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const query = await this.storage.createQuery(data);
|
|
435
|
+
if (data.agentId && this.connectedAgents.has(data.agentId)) {
|
|
436
|
+
const ws = this.connectedAgents.get(data.agentId);
|
|
437
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
438
|
+
ws.send(JSON.stringify({ type: "new_query", queryId: query.id }));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
this.broadcastToUI({ type: "query_submitted", queryId: query.id, agentId: data.agentId });
|
|
442
|
+
res.json(query);
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
if (error instanceof z.ZodError) {
|
|
446
|
+
res.status(400).json({ error: error.errors });
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
res.status(500).json({ error: "Failed to create query" });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async executeQuery(agentId, sql) {
|
|
455
|
+
const query = await this.storage.createQuery({ agentId, targetType: "sql_server", sqlQuery: sql });
|
|
456
|
+
const ws = this.connectedAgents.get(agentId);
|
|
457
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
458
|
+
await this.storage.updateQueryStatus(query.id, "error", { error: "Agent is not connected" });
|
|
459
|
+
return { id: query.id, status: "error", error: "Agent is not connected" };
|
|
460
|
+
}
|
|
461
|
+
await this.storage.updateQueryStatus(query.id, "executing", { executedAt: new Date() });
|
|
462
|
+
ws.send(JSON.stringify({ type: "execute_query", queryId: query.id, sqlQuery: sql }));
|
|
463
|
+
this.broadcastToUI({ type: "query_executing", queryId: query.id });
|
|
464
|
+
return this.waitForResult(query.id);
|
|
465
|
+
}
|
|
466
|
+
async callServiceLayer(agentId, request) {
|
|
467
|
+
sapServiceLayerPayloadSchema.parse(request);
|
|
468
|
+
const sqlQuery = `${request.method} ${request.endpoint}`;
|
|
469
|
+
const query = await this.storage.createQuery({
|
|
470
|
+
agentId,
|
|
471
|
+
targetType: "sap_service_layer",
|
|
472
|
+
sqlQuery,
|
|
473
|
+
requestPayload: request,
|
|
474
|
+
});
|
|
475
|
+
const ws = this.connectedAgents.get(agentId);
|
|
476
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
477
|
+
await this.storage.updateQueryStatus(query.id, "error", { error: "Agent is not connected" });
|
|
478
|
+
return { id: query.id, status: "error", error: "Agent is not connected" };
|
|
479
|
+
}
|
|
480
|
+
await this.storage.updateQueryStatus(query.id, "executing", { executedAt: new Date() });
|
|
481
|
+
const payload = { ...request };
|
|
482
|
+
if (payload.method && !payload.operation)
|
|
483
|
+
payload.operation = payload.method;
|
|
484
|
+
ws.send(JSON.stringify({
|
|
485
|
+
type: "execute_request",
|
|
486
|
+
queryId: query.id,
|
|
487
|
+
targetType: "sap_service_layer",
|
|
488
|
+
requestPayload: payload,
|
|
489
|
+
}));
|
|
490
|
+
this.broadcastToUI({ type: "query_executing", queryId: query.id });
|
|
491
|
+
return this.waitForResult(query.id);
|
|
492
|
+
}
|
|
493
|
+
async callDiApi(agentId, request) {
|
|
494
|
+
sapDiApiPayloadSchema.parse(request);
|
|
495
|
+
const sqlQuery = `DI:${request.operation} ${request.object}`;
|
|
496
|
+
const query = await this.storage.createQuery({
|
|
497
|
+
agentId,
|
|
498
|
+
targetType: "sap_di_api",
|
|
499
|
+
sqlQuery,
|
|
500
|
+
requestPayload: request,
|
|
501
|
+
});
|
|
502
|
+
const ws = this.connectedAgents.get(agentId);
|
|
503
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
504
|
+
await this.storage.updateQueryStatus(query.id, "error", { error: "Agent is not connected" });
|
|
505
|
+
return { id: query.id, status: "error", error: "Agent is not connected" };
|
|
506
|
+
}
|
|
507
|
+
await this.storage.updateQueryStatus(query.id, "executing", { executedAt: new Date() });
|
|
508
|
+
ws.send(JSON.stringify({
|
|
509
|
+
type: "execute_request",
|
|
510
|
+
queryId: query.id,
|
|
511
|
+
targetType: "sap_di_api",
|
|
512
|
+
requestPayload: request,
|
|
513
|
+
}));
|
|
514
|
+
this.broadcastToUI({ type: "query_executing", queryId: query.id });
|
|
515
|
+
return this.waitForResult(query.id);
|
|
516
|
+
}
|
|
517
|
+
waitForResult(queryId, timeoutMs = 300000) {
|
|
518
|
+
return new Promise((resolve) => {
|
|
519
|
+
const timer = setTimeout(() => {
|
|
520
|
+
this.pendingQueries.delete(queryId);
|
|
521
|
+
this.storage.updateQueryStatus(queryId, "error", { error: "Query timed out" });
|
|
522
|
+
resolve({ id: queryId, status: "error", error: "Query timed out" });
|
|
523
|
+
}, timeoutMs);
|
|
524
|
+
this.pendingQueries.set(queryId, { resolve, timer });
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async getAgents() {
|
|
528
|
+
return this.storage.getAgents();
|
|
529
|
+
}
|
|
530
|
+
async getAgent(id) {
|
|
531
|
+
return this.storage.getAgent(id);
|
|
532
|
+
}
|
|
533
|
+
async registerAgent(options) {
|
|
534
|
+
return this.storage.createAgent(options);
|
|
535
|
+
}
|
|
536
|
+
async deleteAgent(id) {
|
|
537
|
+
return this.storage.deleteAgent(id);
|
|
538
|
+
}
|
|
539
|
+
async getCompanies() {
|
|
540
|
+
return this.storage.getCompanies();
|
|
541
|
+
}
|
|
542
|
+
async createCompany(name, description) {
|
|
543
|
+
return this.storage.createCompany(name, description);
|
|
544
|
+
}
|
|
545
|
+
async deleteCompany(id) {
|
|
546
|
+
return this.storage.deleteCompany(id);
|
|
547
|
+
}
|
|
548
|
+
getAgentCapabilities(agentId) {
|
|
549
|
+
return this.agentCaps.get(agentId) || null;
|
|
550
|
+
}
|
|
551
|
+
async refreshAgentCapabilities(agentId) {
|
|
552
|
+
const ws = this.connectedAgents.get(agentId);
|
|
553
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
554
|
+
return false;
|
|
555
|
+
const queryId = `cap-${Date.now()}`;
|
|
556
|
+
ws.send(JSON.stringify({
|
|
557
|
+
type: "execute_request",
|
|
558
|
+
queryId,
|
|
559
|
+
targetType: "agent",
|
|
560
|
+
requestPayload: { operation: "agent_capabilities" },
|
|
561
|
+
}));
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
mountUI(mountPath = "/bridge", staticDir) {
|
|
565
|
+
const clientDistPath = staticDir
|
|
566
|
+
? path.resolve(staticDir)
|
|
567
|
+
: path.resolve(__dirname, "../dist/client");
|
|
568
|
+
this.app.use(mountPath, express.static(clientDistPath));
|
|
569
|
+
this.app.get(`${mountPath}/*`, (_req, res) => {
|
|
570
|
+
res.sendFile(path.join(clientDistPath, "index.html"));
|
|
571
|
+
});
|
|
572
|
+
console.log(`[SAP B1 Bridge] UI mounted at ${mountPath} (serving from ${clientDistPath})`);
|
|
573
|
+
}
|
|
574
|
+
getConnectedAgentIds() {
|
|
575
|
+
return Array.from(this.connectedAgents.keys());
|
|
576
|
+
}
|
|
577
|
+
isAgentConnected(agentId) {
|
|
578
|
+
const ws = this.connectedAgents.get(agentId);
|
|
579
|
+
return !!ws && ws.readyState === WebSocket.OPEN;
|
|
580
|
+
}
|
|
581
|
+
getWebSocketServer() {
|
|
582
|
+
return this.wss;
|
|
583
|
+
}
|
|
584
|
+
getStorage() {
|
|
585
|
+
return this.storage;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function createDrizzleBridgeStorage(dbStorage) {
|
|
2
|
+
return {
|
|
3
|
+
async getCompanies() {
|
|
4
|
+
return dbStorage.getCompanies();
|
|
5
|
+
},
|
|
6
|
+
async getCompany(id) {
|
|
7
|
+
return dbStorage.getCompany(id);
|
|
8
|
+
},
|
|
9
|
+
async createCompany(name, description) {
|
|
10
|
+
return dbStorage.createCompany({ name, description });
|
|
11
|
+
},
|
|
12
|
+
async updateCompany(id, updates) {
|
|
13
|
+
return dbStorage.updateCompany(id, updates);
|
|
14
|
+
},
|
|
15
|
+
async deleteCompany(id) {
|
|
16
|
+
return dbStorage.deleteCompany(id);
|
|
17
|
+
},
|
|
18
|
+
async getAgents() {
|
|
19
|
+
return dbStorage.getAgents();
|
|
20
|
+
},
|
|
21
|
+
async getAgent(id) {
|
|
22
|
+
return dbStorage.getAgent(id);
|
|
23
|
+
},
|
|
24
|
+
async getAgentByApiKey(apiKey) {
|
|
25
|
+
return dbStorage.getAgentByApiKey(apiKey);
|
|
26
|
+
},
|
|
27
|
+
async createAgent(data) {
|
|
28
|
+
return dbStorage.createAgent(data);
|
|
29
|
+
},
|
|
30
|
+
async updateAgent(id, updates) {
|
|
31
|
+
return dbStorage.updateAgent(id, updates);
|
|
32
|
+
},
|
|
33
|
+
async updateAgentStatus(id, status, lastHeartbeat) {
|
|
34
|
+
return dbStorage.updateAgentStatus(id, status, lastHeartbeat);
|
|
35
|
+
},
|
|
36
|
+
async deleteAgent(id) {
|
|
37
|
+
return dbStorage.deleteAgent(id);
|
|
38
|
+
},
|
|
39
|
+
async getQueries() {
|
|
40
|
+
return dbStorage.getQueries();
|
|
41
|
+
},
|
|
42
|
+
async getQuery(id) {
|
|
43
|
+
return dbStorage.getQuery(id);
|
|
44
|
+
},
|
|
45
|
+
async getRecentQueries(limit) {
|
|
46
|
+
return dbStorage.getRecentQueries(limit);
|
|
47
|
+
},
|
|
48
|
+
async getPendingQueriesForAgent(agentId) {
|
|
49
|
+
return dbStorage.getPendingQueriesForAgent(agentId);
|
|
50
|
+
},
|
|
51
|
+
async getActiveQuery() {
|
|
52
|
+
return dbStorage.getActiveQuery();
|
|
53
|
+
},
|
|
54
|
+
async createQuery(data) {
|
|
55
|
+
return dbStorage.createQuery(data);
|
|
56
|
+
},
|
|
57
|
+
async updateQueryStatus(id, status, updates) {
|
|
58
|
+
return dbStorage.updateQueryStatus(id, status, updates);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SAPB1BridgeOptions, SAPB1BridgeAPI } from "./types";
|
|
2
|
+
import { type IBridgeStorage } from "./storage";
|
|
3
|
+
export declare function createSAPB1Bridge(options: SAPB1BridgeOptions, storage?: IBridgeStorage): Promise<SAPB1BridgeAPI>;
|
|
4
|
+
export { SAPB1Bridge } from "./core";
|
|
5
|
+
export { MemBridgeStorage } from "./storage";
|
|
6
|
+
export type { IBridgeStorage } from "./storage";
|
|
7
|
+
export type { SAPB1BridgeOptions, SAPB1BridgeAPI, AgentInfo, QueryResult, ServiceLayerRequest, DiApiRequest, CapabilityStatus, AgentCapabilities, CompanyInfo, RegisterAgentOptions, } from "./types";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AgentInfo, CompanyInfo } from "./types";
|
|
2
|
+
export interface BridgeQuery {
|
|
3
|
+
id: string;
|
|
4
|
+
agentId: string | null;
|
|
5
|
+
targetType: string;
|
|
6
|
+
sqlQuery: string;
|
|
7
|
+
requestPayload: any;
|
|
8
|
+
status: string;
|
|
9
|
+
submittedAt: Date | null;
|
|
10
|
+
executedAt: Date | null;
|
|
11
|
+
completedAt: Date | null;
|
|
12
|
+
rowCount: number | null;
|
|
13
|
+
executionTime: number | null;
|
|
14
|
+
error: string | null;
|
|
15
|
+
result: any;
|
|
16
|
+
}
|
|
17
|
+
export interface IBridgeStorage {
|
|
18
|
+
getCompanies(): Promise<CompanyInfo[]>;
|
|
19
|
+
getCompany(id: string): Promise<CompanyInfo | undefined>;
|
|
20
|
+
createCompany(name: string, description?: string): Promise<CompanyInfo>;
|
|
21
|
+
updateCompany(id: string, updates: Partial<{
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
}>): Promise<void>;
|
|
25
|
+
deleteCompany(id: string): Promise<void>;
|
|
26
|
+
getAgents(): Promise<AgentInfo[]>;
|
|
27
|
+
getAgent(id: string): Promise<AgentInfo | undefined>;
|
|
28
|
+
getAgentByApiKey(apiKey: string): Promise<AgentInfo | undefined>;
|
|
29
|
+
createAgent(data: {
|
|
30
|
+
name: string;
|
|
31
|
+
serverName: string;
|
|
32
|
+
capabilities?: string[];
|
|
33
|
+
companyId?: string;
|
|
34
|
+
}): Promise<AgentInfo>;
|
|
35
|
+
updateAgent(id: string, updates: Partial<AgentInfo>): Promise<void>;
|
|
36
|
+
updateAgentStatus(id: string, status: string, lastHeartbeat: Date): Promise<void>;
|
|
37
|
+
deleteAgent(id: string): Promise<void>;
|
|
38
|
+
getQueries(): Promise<BridgeQuery[]>;
|
|
39
|
+
getQuery(id: string): Promise<BridgeQuery | undefined>;
|
|
40
|
+
getRecentQueries(limit: number): Promise<BridgeQuery[]>;
|
|
41
|
+
getPendingQueriesForAgent(agentId: string): Promise<BridgeQuery[]>;
|
|
42
|
+
getActiveQuery(): Promise<BridgeQuery | null>;
|
|
43
|
+
createQuery(data: {
|
|
44
|
+
agentId?: string;
|
|
45
|
+
targetType?: string;
|
|
46
|
+
sqlQuery: string;
|
|
47
|
+
requestPayload?: any;
|
|
48
|
+
}): Promise<BridgeQuery>;
|
|
49
|
+
updateQueryStatus(id: string, status: string, updates?: Partial<BridgeQuery>): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export declare class MemBridgeStorage implements IBridgeStorage {
|
|
52
|
+
private companies;
|
|
53
|
+
private agents;
|
|
54
|
+
private queries;
|
|
55
|
+
getCompanies(): Promise<CompanyInfo[]>;
|
|
56
|
+
getCompany(id: string): Promise<CompanyInfo | undefined>;
|
|
57
|
+
createCompany(name: string, description?: string): Promise<CompanyInfo>;
|
|
58
|
+
updateCompany(id: string, updates: Partial<{
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
}>): Promise<void>;
|
|
62
|
+
deleteCompany(id: string): Promise<void>;
|
|
63
|
+
getAgents(): Promise<AgentInfo[]>;
|
|
64
|
+
getAgent(id: string): Promise<AgentInfo | undefined>;
|
|
65
|
+
getAgentByApiKey(apiKey: string): Promise<AgentInfo | undefined>;
|
|
66
|
+
createAgent(data: {
|
|
67
|
+
name: string;
|
|
68
|
+
serverName: string;
|
|
69
|
+
capabilities?: string[];
|
|
70
|
+
companyId?: string;
|
|
71
|
+
}): Promise<AgentInfo>;
|
|
72
|
+
updateAgent(id: string, updates: Partial<AgentInfo>): Promise<void>;
|
|
73
|
+
updateAgentStatus(id: string, status: string, lastHeartbeat: Date): Promise<void>;
|
|
74
|
+
deleteAgent(id: string): Promise<void>;
|
|
75
|
+
getQueries(): Promise<BridgeQuery[]>;
|
|
76
|
+
getQuery(id: string): Promise<BridgeQuery | undefined>;
|
|
77
|
+
getRecentQueries(limit: number): Promise<BridgeQuery[]>;
|
|
78
|
+
getPendingQueriesForAgent(agentId: string): Promise<BridgeQuery[]>;
|
|
79
|
+
getActiveQuery(): Promise<BridgeQuery | null>;
|
|
80
|
+
createQuery(data: {
|
|
81
|
+
agentId?: string;
|
|
82
|
+
targetType?: string;
|
|
83
|
+
sqlQuery: string;
|
|
84
|
+
requestPayload?: any;
|
|
85
|
+
}): Promise<BridgeQuery>;
|
|
86
|
+
updateQueryStatus(id: string, status: string, updates?: Partial<BridgeQuery>): Promise<void>;
|
|
87
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
export class MemBridgeStorage {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.companies = new Map();
|
|
5
|
+
this.agents = new Map();
|
|
6
|
+
this.queries = new Map();
|
|
7
|
+
}
|
|
8
|
+
async getCompanies() {
|
|
9
|
+
return Array.from(this.companies.values());
|
|
10
|
+
}
|
|
11
|
+
async getCompany(id) {
|
|
12
|
+
return this.companies.get(id);
|
|
13
|
+
}
|
|
14
|
+
async createCompany(name, description) {
|
|
15
|
+
const id = randomUUID();
|
|
16
|
+
const company = { id, name, description: description || null, createdAt: new Date() };
|
|
17
|
+
this.companies.set(id, company);
|
|
18
|
+
return company;
|
|
19
|
+
}
|
|
20
|
+
async updateCompany(id, updates) {
|
|
21
|
+
const company = this.companies.get(id);
|
|
22
|
+
if (company)
|
|
23
|
+
Object.assign(company, updates);
|
|
24
|
+
}
|
|
25
|
+
async deleteCompany(id) {
|
|
26
|
+
this.companies.delete(id);
|
|
27
|
+
}
|
|
28
|
+
async getAgents() {
|
|
29
|
+
return Array.from(this.agents.values());
|
|
30
|
+
}
|
|
31
|
+
async getAgent(id) {
|
|
32
|
+
return this.agents.get(id);
|
|
33
|
+
}
|
|
34
|
+
async getAgentByApiKey(apiKey) {
|
|
35
|
+
return Array.from(this.agents.values()).find((a) => a.apiKey === apiKey);
|
|
36
|
+
}
|
|
37
|
+
async createAgent(data) {
|
|
38
|
+
const id = randomUUID();
|
|
39
|
+
const apiKey = `agent_${randomUUID().replace(/-/g, "")}`;
|
|
40
|
+
const agent = {
|
|
41
|
+
id,
|
|
42
|
+
name: data.name,
|
|
43
|
+
serverName: data.serverName,
|
|
44
|
+
status: "offline",
|
|
45
|
+
apiKey,
|
|
46
|
+
capabilities: data.capabilities || ["sql_server"],
|
|
47
|
+
companyId: data.companyId || null,
|
|
48
|
+
lastHeartbeat: null,
|
|
49
|
+
version: null,
|
|
50
|
+
};
|
|
51
|
+
this.agents.set(id, agent);
|
|
52
|
+
return agent;
|
|
53
|
+
}
|
|
54
|
+
async updateAgent(id, updates) {
|
|
55
|
+
const agent = this.agents.get(id);
|
|
56
|
+
if (agent)
|
|
57
|
+
Object.assign(agent, updates);
|
|
58
|
+
}
|
|
59
|
+
async updateAgentStatus(id, status, lastHeartbeat) {
|
|
60
|
+
const agent = this.agents.get(id);
|
|
61
|
+
if (agent) {
|
|
62
|
+
agent.status = status;
|
|
63
|
+
agent.lastHeartbeat = lastHeartbeat;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async deleteAgent(id) {
|
|
67
|
+
this.agents.delete(id);
|
|
68
|
+
}
|
|
69
|
+
async getQueries() {
|
|
70
|
+
return Array.from(this.queries.values()).sort((a, b) => {
|
|
71
|
+
const at = a.submittedAt ? a.submittedAt.getTime() : 0;
|
|
72
|
+
const bt = b.submittedAt ? b.submittedAt.getTime() : 0;
|
|
73
|
+
return bt - at;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async getQuery(id) {
|
|
77
|
+
return this.queries.get(id);
|
|
78
|
+
}
|
|
79
|
+
async getRecentQueries(limit) {
|
|
80
|
+
return (await this.getQueries()).slice(0, limit);
|
|
81
|
+
}
|
|
82
|
+
async getPendingQueriesForAgent(agentId) {
|
|
83
|
+
return Array.from(this.queries.values())
|
|
84
|
+
.filter((q) => q.agentId === agentId && q.status === "pending")
|
|
85
|
+
.sort((a, b) => {
|
|
86
|
+
const at = a.submittedAt ? a.submittedAt.getTime() : 0;
|
|
87
|
+
const bt = b.submittedAt ? b.submittedAt.getTime() : 0;
|
|
88
|
+
return at - bt;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async getActiveQuery() {
|
|
92
|
+
const sorted = await this.getQueries();
|
|
93
|
+
return sorted[0] || null;
|
|
94
|
+
}
|
|
95
|
+
async createQuery(data) {
|
|
96
|
+
const id = randomUUID();
|
|
97
|
+
const query = {
|
|
98
|
+
id,
|
|
99
|
+
agentId: data.agentId || null,
|
|
100
|
+
targetType: data.targetType || "sql_server",
|
|
101
|
+
sqlQuery: data.sqlQuery,
|
|
102
|
+
requestPayload: data.requestPayload || null,
|
|
103
|
+
status: "pending",
|
|
104
|
+
submittedAt: new Date(),
|
|
105
|
+
executedAt: null,
|
|
106
|
+
completedAt: null,
|
|
107
|
+
rowCount: null,
|
|
108
|
+
executionTime: null,
|
|
109
|
+
error: null,
|
|
110
|
+
result: null,
|
|
111
|
+
};
|
|
112
|
+
this.queries.set(id, query);
|
|
113
|
+
return query;
|
|
114
|
+
}
|
|
115
|
+
async updateQueryStatus(id, status, updates) {
|
|
116
|
+
const query = this.queries.get(id);
|
|
117
|
+
if (query) {
|
|
118
|
+
query.status = status;
|
|
119
|
+
if (updates)
|
|
120
|
+
Object.assign(query, updates);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Server } from "http";
|
|
2
|
+
import type { Express } from "express";
|
|
3
|
+
export interface SAPB1BridgeOptions {
|
|
4
|
+
app: Express;
|
|
5
|
+
server: Server;
|
|
6
|
+
databaseUrl?: string;
|
|
7
|
+
appName?: string;
|
|
8
|
+
wsPath?: string;
|
|
9
|
+
apiPrefix?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AgentInfo {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
serverName: string;
|
|
15
|
+
status: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
capabilities: string[];
|
|
18
|
+
companyId: string | null;
|
|
19
|
+
lastHeartbeat: Date | null;
|
|
20
|
+
version: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface RegisterAgentOptions {
|
|
23
|
+
name: string;
|
|
24
|
+
serverName: string;
|
|
25
|
+
capabilities?: string[];
|
|
26
|
+
companyId?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface QueryResult {
|
|
29
|
+
id: string;
|
|
30
|
+
status: string;
|
|
31
|
+
result?: any;
|
|
32
|
+
rowCount?: number | null;
|
|
33
|
+
executionTime?: number | null;
|
|
34
|
+
error?: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface ServiceLayerRequest {
|
|
37
|
+
method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
38
|
+
endpoint: string;
|
|
39
|
+
body?: any;
|
|
40
|
+
queryParams?: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
export interface DiApiRequest {
|
|
43
|
+
version?: string;
|
|
44
|
+
operation: "di_get" | "di_add" | "di_update" | "di_action" | "di_query";
|
|
45
|
+
object: string;
|
|
46
|
+
key?: Record<string, string | number>;
|
|
47
|
+
sapXml?: string;
|
|
48
|
+
action?: string;
|
|
49
|
+
options?: {
|
|
50
|
+
dryRun?: boolean;
|
|
51
|
+
transaction?: boolean;
|
|
52
|
+
returnXml?: boolean;
|
|
53
|
+
allowDiQuery?: boolean;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface CapabilityStatus {
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
available: boolean;
|
|
59
|
+
reason: string | null;
|
|
60
|
+
}
|
|
61
|
+
export interface AgentCapabilities {
|
|
62
|
+
sql: CapabilityStatus;
|
|
63
|
+
serviceLayer: CapabilityStatus;
|
|
64
|
+
diApi: CapabilityStatus;
|
|
65
|
+
}
|
|
66
|
+
export interface CompanyInfo {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
description: string | null;
|
|
70
|
+
createdAt: Date | null;
|
|
71
|
+
}
|
|
72
|
+
export interface SAPB1BridgeAPI {
|
|
73
|
+
executeQuery(agentId: string, sql: string): Promise<QueryResult>;
|
|
74
|
+
callServiceLayer(agentId: string, request: ServiceLayerRequest): Promise<QueryResult>;
|
|
75
|
+
callDiApi(agentId: string, request: DiApiRequest): Promise<QueryResult>;
|
|
76
|
+
getAgents(): Promise<AgentInfo[]>;
|
|
77
|
+
getAgent(id: string): Promise<AgentInfo | undefined>;
|
|
78
|
+
registerAgent(options: RegisterAgentOptions): Promise<AgentInfo>;
|
|
79
|
+
deleteAgent(id: string): Promise<void>;
|
|
80
|
+
getCompanies(): Promise<CompanyInfo[]>;
|
|
81
|
+
createCompany(name: string, description?: string): Promise<CompanyInfo>;
|
|
82
|
+
deleteCompany(id: string): Promise<void>;
|
|
83
|
+
getAgentCapabilities(agentId: string): AgentCapabilities | null;
|
|
84
|
+
refreshAgentCapabilities(agentId: string): Promise<boolean>;
|
|
85
|
+
mountUI(path?: string, staticDir?: string): void;
|
|
86
|
+
getConnectedAgentIds(): string[];
|
|
87
|
+
isAgentConnected(agentId: string): boolean;
|
|
88
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bestoneconsulting/sap-b1-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "SAP Business One & SQL Server bridge for secure database and ERP access behind firewalls via WebSocket agents",
|
|
7
|
+
"author": "Best One Consulting <https://www.bestoneconsulting.com/>",
|
|
8
|
+
"homepage": "https://www.bestoneconsulting.com/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/bestoneconsulting/sap-b1-bridge"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["sap", "business-one", "b1", "sql-server", "bridge", "websocket", "agent", "erp", "di-api", "service-layer"],
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"express": ">=4.0.0",
|
|
29
|
+
"ws": ">=8.0.0",
|
|
30
|
+
"zod": ">=3.0.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p ../tsconfig.bridge.json",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
}
|
|
36
|
+
}
|