@bestoneconsulting/sap-b1-bridge 1.1.0 → 1.2.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/README.md CHANGED
@@ -66,8 +66,8 @@ Before the bridge can talk to your on-premise systems, you need to register an a
66
66
 
67
67
  ```typescript
68
68
  const agent = await bridge.registerAgent({
69
- name: "Production Server",
70
- serverName: "SQLPROD01",
69
+ name: "Main Office",
70
+ serverName: "Production ERP", // A friendly label — not a hostname or connection string
71
71
  capabilities: ["sql_server", "sap_service_layer", "sap_di_api"],
72
72
  });
73
73
 
@@ -75,6 +75,8 @@ console.log("Agent API Key:", agent.apiKey);
75
75
  // Give this API key to the agent software running on your Windows server
76
76
  ```
77
77
 
78
+ The `serverName` field is a descriptive label to help you identify which server or environment this agent represents (e.g. "Production ERP", "US Warehouse", "Dev Server"). It is not used as a connection string.
79
+
78
80
  Contact [Best One Consulting](https://www.bestoneconsulting.com/) to obtain the Windows agent installer.
79
81
 
80
82
  ## Built-in Test Console
@@ -89,7 +91,7 @@ bridge.mountUI("/bridge");
89
91
  The test console includes:
90
92
 
91
93
  - **Query Console** - Execute SQL queries, SAP Service Layer requests, and DI API operations with a form-based interface. Select your target type and agent, enter your query or request details, and see results in real time.
92
- - **Agents** - Register new agents, view connection status, copy API keys, and delete agents.
94
+ - **Agents** - Register new agents, view connection status, copy API keys, and delete agents. Displays the **WebSocket URL** that agents need to connect to, with a one-click copy button.
93
95
  - **History** - View recent query history with status, execution time, and row counts.
94
96
  - **Live WebSocket** - Real-time connection status indicator and automatic result updates via WebSocket.
95
97
 
@@ -335,6 +337,126 @@ const bridge = await createSAPB1Bridge(options, new MyDatabaseStorage());
335
337
 
336
338
  See the full [`IBridgeStorage` interface](https://github.com/bestoneconsulting/sap-b1-bridge/blob/main/bridge/storage.ts) for method signatures and return types.
337
339
 
340
+ ### Built-in PostgreSQL Storage (Drizzle ORM)
341
+
342
+ The package includes a ready-to-use PostgreSQL storage adapter built on [Drizzle ORM](https://orm.drizzle.team/). This gives you persistent storage without writing any database code.
343
+
344
+ **1. Install Drizzle dependencies:**
345
+
346
+ ```bash
347
+ npm install drizzle-orm @neondatabase/serverless drizzle-kit
348
+ ```
349
+
350
+ Or if you use a standard PostgreSQL server (not Neon):
351
+
352
+ ```bash
353
+ npm install drizzle-orm pg drizzle-kit
354
+ npm install -D @types/pg
355
+ ```
356
+
357
+ **2. Set up the database connection and push the schema:**
358
+
359
+ Create a `drizzle.config.ts` in your project root:
360
+
361
+ ```typescript
362
+ import { defineConfig } from "drizzle-kit";
363
+
364
+ export default defineConfig({
365
+ out: "./drizzle",
366
+ schema: "./src/db/schema.ts",
367
+ dialect: "postgresql",
368
+ dbCredentials: {
369
+ url: process.env.DATABASE_URL!,
370
+ },
371
+ });
372
+ ```
373
+
374
+ Create `src/db/schema.ts` that re-exports the bridge schema (and any of your own tables):
375
+
376
+ ```typescript
377
+ export { bridgeCompanies, bridgeAgents, bridgeQueries } from "@bestoneconsulting/sap-b1-bridge";
378
+
379
+ // Add your own application tables here if needed
380
+ ```
381
+
382
+ Push the schema to your database:
383
+
384
+ ```bash
385
+ npx drizzle-kit push
386
+ ```
387
+
388
+ This creates three tables: `bridge_companies`, `bridge_agents`, and `bridge_queries`. The `bridge_` prefix avoids conflicts with your own application tables.
389
+
390
+ **3. Create the Drizzle client and pass it to the bridge:**
391
+
392
+ For Neon serverless PostgreSQL:
393
+
394
+ ```typescript
395
+ import express from "express";
396
+ import { createServer } from "http";
397
+ import { neon } from "@neondatabase/serverless";
398
+ import { drizzle } from "drizzle-orm/neon-http";
399
+ import { createSAPB1Bridge, createDrizzleBridgeStorage } from "@bestoneconsulting/sap-b1-bridge";
400
+
401
+ const sql = neon(process.env.DATABASE_URL!);
402
+ const db = drizzle(sql);
403
+
404
+ const app = express();
405
+ app.use(express.json());
406
+ const server = createServer(app);
407
+
408
+ const storage = createDrizzleBridgeStorage(db);
409
+
410
+ const bridge = await createSAPB1Bridge(
411
+ { app, server, appName: "My SAP App" },
412
+ storage
413
+ );
414
+
415
+ bridge.mountUI("/bridge");
416
+ server.listen(3000);
417
+ ```
418
+
419
+ For standard PostgreSQL (using `pg`):
420
+
421
+ ```typescript
422
+ import express from "express";
423
+ import { createServer } from "http";
424
+ import { Pool } from "pg";
425
+ import { drizzle } from "drizzle-orm/node-postgres";
426
+ import { createSAPB1Bridge, createDrizzleBridgeStorage } from "@bestoneconsulting/sap-b1-bridge";
427
+
428
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
429
+ const db = drizzle(pool);
430
+
431
+ const app = express();
432
+ app.use(express.json());
433
+ const server = createServer(app);
434
+
435
+ const storage = createDrizzleBridgeStorage(db);
436
+
437
+ const bridge = await createSAPB1Bridge(
438
+ { app, server, appName: "My SAP App" },
439
+ storage
440
+ );
441
+
442
+ bridge.mountUI("/bridge");
443
+ server.listen(3000);
444
+ ```
445
+
446
+ **Schema details:**
447
+
448
+ | Table | Purpose |
449
+ |-------|---------|
450
+ | `bridge_companies` | Organization grouping for agents |
451
+ | `bridge_agents` | Registered agents with API keys and connection status |
452
+ | `bridge_queries` | Query history with status, results, and execution metrics |
453
+
454
+ The Drizzle schema objects are exported for advanced use cases (custom queries, migrations, extending):
455
+
456
+ ```typescript
457
+ import { bridgeCompanies, bridgeAgents, bridgeQueries } from "@bestoneconsulting/sap-b1-bridge";
458
+ ```
459
+
338
460
  ## Configuration Options
339
461
 
340
462
  | Option | Type | Default | Description |
@@ -0,0 +1,2 @@
1
+ import type { IBridgeStorage } from "./storage";
2
+ export declare function createAppBridgeStorage(dbStorage: any): IBridgeStorage;
@@ -0,0 +1,61 @@
1
+ export function createAppBridgeStorage(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
+ }
@@ -1,2 +1,6 @@
1
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import type { NeonHttpDatabase } from "drizzle-orm/neon-http";
1
3
  import type { IBridgeStorage } from "./storage";
2
- export declare function createDrizzleBridgeStorage(dbStorage: any): IBridgeStorage;
4
+ type DrizzleDB = NodePgDatabase<any> | NeonHttpDatabase<any>;
5
+ export declare function createDrizzleBridgeStorage(db: DrizzleDB): IBridgeStorage;
6
+ export {};
@@ -1,61 +1,89 @@
1
- export function createDrizzleBridgeStorage(dbStorage) {
1
+ import { eq, desc, and, asc } from "drizzle-orm";
2
+ import { randomUUID } from "crypto";
3
+ import { bridgeCompanies, bridgeAgents, bridgeQueries } from "./schema";
4
+ export function createDrizzleBridgeStorage(db) {
2
5
  return {
3
6
  async getCompanies() {
4
- return dbStorage.getCompanies();
7
+ return db.select().from(bridgeCompanies);
5
8
  },
6
9
  async getCompany(id) {
7
- return dbStorage.getCompany(id);
10
+ const rows = await db.select().from(bridgeCompanies).where(eq(bridgeCompanies.id, id));
11
+ return rows[0];
8
12
  },
9
13
  async createCompany(name, description) {
10
- return dbStorage.createCompany({ name, description });
14
+ const rows = await db.insert(bridgeCompanies).values({ name, description }).returning();
15
+ return rows[0];
11
16
  },
12
17
  async updateCompany(id, updates) {
13
- return dbStorage.updateCompany(id, updates);
18
+ await db.update(bridgeCompanies).set(updates).where(eq(bridgeCompanies.id, id));
14
19
  },
15
20
  async deleteCompany(id) {
16
- return dbStorage.deleteCompany(id);
21
+ await db.delete(bridgeCompanies).where(eq(bridgeCompanies.id, id));
17
22
  },
18
23
  async getAgents() {
19
- return dbStorage.getAgents();
24
+ return db.select().from(bridgeAgents);
20
25
  },
21
26
  async getAgent(id) {
22
- return dbStorage.getAgent(id);
27
+ const rows = await db.select().from(bridgeAgents).where(eq(bridgeAgents.id, id));
28
+ return rows[0];
23
29
  },
24
30
  async getAgentByApiKey(apiKey) {
25
- return dbStorage.getAgentByApiKey(apiKey);
31
+ const rows = await db.select().from(bridgeAgents).where(eq(bridgeAgents.apiKey, apiKey));
32
+ return rows[0];
26
33
  },
27
34
  async createAgent(data) {
28
- return dbStorage.createAgent(data);
35
+ const apiKey = `agent_${randomUUID().replace(/-/g, "")}`;
36
+ const rows = await db.insert(bridgeAgents).values({
37
+ name: data.name,
38
+ serverName: data.serverName,
39
+ capabilities: data.capabilities || ["sql_server"],
40
+ companyId: data.companyId || null,
41
+ apiKey,
42
+ status: "offline",
43
+ }).returning();
44
+ return rows[0];
29
45
  },
30
46
  async updateAgent(id, updates) {
31
- return dbStorage.updateAgent(id, updates);
47
+ await db.update(bridgeAgents).set(updates).where(eq(bridgeAgents.id, id));
32
48
  },
33
49
  async updateAgentStatus(id, status, lastHeartbeat) {
34
- return dbStorage.updateAgentStatus(id, status, lastHeartbeat);
50
+ await db.update(bridgeAgents).set({ status, lastHeartbeat }).where(eq(bridgeAgents.id, id));
35
51
  },
36
52
  async deleteAgent(id) {
37
- return dbStorage.deleteAgent(id);
53
+ await db.delete(bridgeQueries).where(eq(bridgeQueries.agentId, id));
54
+ await db.delete(bridgeAgents).where(eq(bridgeAgents.id, id));
38
55
  },
39
56
  async getQueries() {
40
- return dbStorage.getQueries();
57
+ return db.select().from(bridgeQueries).orderBy(desc(bridgeQueries.submittedAt));
41
58
  },
42
59
  async getQuery(id) {
43
- return dbStorage.getQuery(id);
60
+ const rows = await db.select().from(bridgeQueries).where(eq(bridgeQueries.id, id));
61
+ return rows[0];
44
62
  },
45
63
  async getRecentQueries(limit) {
46
- return dbStorage.getRecentQueries(limit);
64
+ return db.select().from(bridgeQueries).orderBy(desc(bridgeQueries.submittedAt)).limit(limit);
47
65
  },
48
66
  async getPendingQueriesForAgent(agentId) {
49
- return dbStorage.getPendingQueriesForAgent(agentId);
67
+ return db.select().from(bridgeQueries)
68
+ .where(and(eq(bridgeQueries.agentId, agentId), eq(bridgeQueries.status, "pending")))
69
+ .orderBy(asc(bridgeQueries.submittedAt));
50
70
  },
51
71
  async getActiveQuery() {
52
- return dbStorage.getActiveQuery();
72
+ const rows = await db.select().from(bridgeQueries).orderBy(desc(bridgeQueries.submittedAt)).limit(1);
73
+ return rows[0] || null;
53
74
  },
54
75
  async createQuery(data) {
55
- return dbStorage.createQuery(data);
76
+ const rows = await db.insert(bridgeQueries).values({
77
+ agentId: data.agentId || null,
78
+ targetType: data.targetType || "sql_server",
79
+ sqlQuery: data.sqlQuery,
80
+ requestPayload: data.requestPayload || null,
81
+ status: "pending",
82
+ }).returning();
83
+ return rows[0];
56
84
  },
57
85
  async updateQueryStatus(id, status, updates) {
58
- return dbStorage.updateQueryStatus(id, status, updates);
86
+ await db.update(bridgeQueries).set({ status, ...updates }).where(eq(bridgeQueries.id, id));
59
87
  },
60
88
  };
61
89
  }
package/dist/index.d.ts CHANGED
@@ -3,5 +3,7 @@ import { type IBridgeStorage } from "./storage";
3
3
  export declare function createSAPB1Bridge(options: SAPB1BridgeOptions, storage?: IBridgeStorage): Promise<SAPB1BridgeAPI>;
4
4
  export { SAPB1Bridge } from "./core";
5
5
  export { MemBridgeStorage } from "./storage";
6
- export type { IBridgeStorage } from "./storage";
6
+ export { createDrizzleBridgeStorage } from "./drizzle-storage";
7
+ export { bridgeCompanies, bridgeAgents, bridgeQueries } from "./schema";
8
+ export type { IBridgeStorage, BridgeQuery } from "./storage";
7
9
  export type { SAPB1BridgeOptions, SAPB1BridgeAPI, AgentInfo, QueryResult, ServiceLayerRequest, DiApiRequest, CapabilityStatus, AgentCapabilities, CompanyInfo, RegisterAgentOptions, } from "./types";
package/dist/index.js CHANGED
@@ -4,3 +4,5 @@ export async function createSAPB1Bridge(options, storage) {
4
4
  }
5
5
  export { SAPB1Bridge } from "./core";
6
6
  export { MemBridgeStorage } from "./storage";
7
+ export { createDrizzleBridgeStorage } from "./drizzle-storage";
8
+ export { bridgeCompanies, bridgeAgents, bridgeQueries } from "./schema";
@@ -0,0 +1,499 @@
1
+ export declare const bridgeCompanies: import("drizzle-orm/pg-core").PgTableWithColumns<{
2
+ name: "bridge_companies";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/pg-core").PgColumn<{
6
+ name: "id";
7
+ tableName: "bridge_companies";
8
+ dataType: "string";
9
+ columnType: "PgVarchar";
10
+ data: string;
11
+ driverParam: string;
12
+ notNull: true;
13
+ hasDefault: true;
14
+ isPrimaryKey: true;
15
+ isAutoincrement: false;
16
+ hasRuntimeDefault: false;
17
+ enumValues: [string, ...string[]];
18
+ baseColumn: never;
19
+ identity: undefined;
20
+ generated: undefined;
21
+ }, {}, {
22
+ length: number | undefined;
23
+ }>;
24
+ name: import("drizzle-orm/pg-core").PgColumn<{
25
+ name: "name";
26
+ tableName: "bridge_companies";
27
+ dataType: "string";
28
+ columnType: "PgText";
29
+ data: string;
30
+ driverParam: string;
31
+ notNull: true;
32
+ hasDefault: false;
33
+ isPrimaryKey: false;
34
+ isAutoincrement: false;
35
+ hasRuntimeDefault: false;
36
+ enumValues: [string, ...string[]];
37
+ baseColumn: never;
38
+ identity: undefined;
39
+ generated: undefined;
40
+ }, {}, {}>;
41
+ description: import("drizzle-orm/pg-core").PgColumn<{
42
+ name: "description";
43
+ tableName: "bridge_companies";
44
+ dataType: "string";
45
+ columnType: "PgText";
46
+ data: string;
47
+ driverParam: string;
48
+ notNull: false;
49
+ hasDefault: false;
50
+ isPrimaryKey: false;
51
+ isAutoincrement: false;
52
+ hasRuntimeDefault: false;
53
+ enumValues: [string, ...string[]];
54
+ baseColumn: never;
55
+ identity: undefined;
56
+ generated: undefined;
57
+ }, {}, {}>;
58
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
59
+ name: "created_at";
60
+ tableName: "bridge_companies";
61
+ dataType: "date";
62
+ columnType: "PgTimestamp";
63
+ data: Date;
64
+ driverParam: string;
65
+ notNull: false;
66
+ hasDefault: true;
67
+ isPrimaryKey: false;
68
+ isAutoincrement: false;
69
+ hasRuntimeDefault: false;
70
+ enumValues: undefined;
71
+ baseColumn: never;
72
+ identity: undefined;
73
+ generated: undefined;
74
+ }, {}, {}>;
75
+ };
76
+ dialect: "pg";
77
+ }>;
78
+ export declare const bridgeAgents: import("drizzle-orm/pg-core").PgTableWithColumns<{
79
+ name: "bridge_agents";
80
+ schema: undefined;
81
+ columns: {
82
+ id: import("drizzle-orm/pg-core").PgColumn<{
83
+ name: "id";
84
+ tableName: "bridge_agents";
85
+ dataType: "string";
86
+ columnType: "PgVarchar";
87
+ data: string;
88
+ driverParam: string;
89
+ notNull: true;
90
+ hasDefault: true;
91
+ isPrimaryKey: true;
92
+ isAutoincrement: false;
93
+ hasRuntimeDefault: false;
94
+ enumValues: [string, ...string[]];
95
+ baseColumn: never;
96
+ identity: undefined;
97
+ generated: undefined;
98
+ }, {}, {
99
+ length: number | undefined;
100
+ }>;
101
+ name: import("drizzle-orm/pg-core").PgColumn<{
102
+ name: "name";
103
+ tableName: "bridge_agents";
104
+ dataType: "string";
105
+ columnType: "PgText";
106
+ data: string;
107
+ driverParam: string;
108
+ notNull: true;
109
+ hasDefault: false;
110
+ isPrimaryKey: false;
111
+ isAutoincrement: false;
112
+ hasRuntimeDefault: false;
113
+ enumValues: [string, ...string[]];
114
+ baseColumn: never;
115
+ identity: undefined;
116
+ generated: undefined;
117
+ }, {}, {}>;
118
+ serverName: import("drizzle-orm/pg-core").PgColumn<{
119
+ name: "server_name";
120
+ tableName: "bridge_agents";
121
+ dataType: "string";
122
+ columnType: "PgText";
123
+ data: string;
124
+ driverParam: string;
125
+ notNull: true;
126
+ hasDefault: false;
127
+ isPrimaryKey: false;
128
+ isAutoincrement: false;
129
+ hasRuntimeDefault: false;
130
+ enumValues: [string, ...string[]];
131
+ baseColumn: never;
132
+ identity: undefined;
133
+ generated: undefined;
134
+ }, {}, {}>;
135
+ companyId: import("drizzle-orm/pg-core").PgColumn<{
136
+ name: "company_id";
137
+ tableName: "bridge_agents";
138
+ dataType: "string";
139
+ columnType: "PgVarchar";
140
+ data: string;
141
+ driverParam: string;
142
+ notNull: false;
143
+ hasDefault: false;
144
+ isPrimaryKey: false;
145
+ isAutoincrement: false;
146
+ hasRuntimeDefault: false;
147
+ enumValues: [string, ...string[]];
148
+ baseColumn: never;
149
+ identity: undefined;
150
+ generated: undefined;
151
+ }, {}, {
152
+ length: number | undefined;
153
+ }>;
154
+ status: import("drizzle-orm/pg-core").PgColumn<{
155
+ name: "status";
156
+ tableName: "bridge_agents";
157
+ dataType: "string";
158
+ columnType: "PgText";
159
+ data: string;
160
+ driverParam: string;
161
+ notNull: true;
162
+ hasDefault: true;
163
+ isPrimaryKey: false;
164
+ isAutoincrement: false;
165
+ hasRuntimeDefault: false;
166
+ enumValues: [string, ...string[]];
167
+ baseColumn: never;
168
+ identity: undefined;
169
+ generated: undefined;
170
+ }, {}, {}>;
171
+ lastHeartbeat: import("drizzle-orm/pg-core").PgColumn<{
172
+ name: "last_heartbeat";
173
+ tableName: "bridge_agents";
174
+ dataType: "date";
175
+ columnType: "PgTimestamp";
176
+ data: Date;
177
+ driverParam: string;
178
+ notNull: false;
179
+ hasDefault: false;
180
+ isPrimaryKey: false;
181
+ isAutoincrement: false;
182
+ hasRuntimeDefault: false;
183
+ enumValues: undefined;
184
+ baseColumn: never;
185
+ identity: undefined;
186
+ generated: undefined;
187
+ }, {}, {}>;
188
+ version: import("drizzle-orm/pg-core").PgColumn<{
189
+ name: "version";
190
+ tableName: "bridge_agents";
191
+ dataType: "string";
192
+ columnType: "PgText";
193
+ data: string;
194
+ driverParam: string;
195
+ notNull: false;
196
+ hasDefault: false;
197
+ isPrimaryKey: false;
198
+ isAutoincrement: false;
199
+ hasRuntimeDefault: false;
200
+ enumValues: [string, ...string[]];
201
+ baseColumn: never;
202
+ identity: undefined;
203
+ generated: undefined;
204
+ }, {}, {}>;
205
+ apiKey: import("drizzle-orm/pg-core").PgColumn<{
206
+ name: "api_key";
207
+ tableName: "bridge_agents";
208
+ dataType: "string";
209
+ columnType: "PgText";
210
+ data: string;
211
+ driverParam: string;
212
+ notNull: true;
213
+ hasDefault: false;
214
+ isPrimaryKey: false;
215
+ isAutoincrement: false;
216
+ hasRuntimeDefault: false;
217
+ enumValues: [string, ...string[]];
218
+ baseColumn: never;
219
+ identity: undefined;
220
+ generated: undefined;
221
+ }, {}, {}>;
222
+ capabilities: import("drizzle-orm/pg-core").PgColumn<{
223
+ name: "capabilities";
224
+ tableName: "bridge_agents";
225
+ dataType: "array";
226
+ columnType: "PgArray";
227
+ data: string[];
228
+ driverParam: string | string[];
229
+ notNull: true;
230
+ hasDefault: true;
231
+ isPrimaryKey: false;
232
+ isAutoincrement: false;
233
+ hasRuntimeDefault: false;
234
+ enumValues: [string, ...string[]];
235
+ baseColumn: import("drizzle-orm").Column<{
236
+ name: "capabilities";
237
+ tableName: "bridge_agents";
238
+ dataType: "string";
239
+ columnType: "PgText";
240
+ data: string;
241
+ driverParam: string;
242
+ notNull: false;
243
+ hasDefault: false;
244
+ isPrimaryKey: false;
245
+ isAutoincrement: false;
246
+ hasRuntimeDefault: false;
247
+ enumValues: [string, ...string[]];
248
+ baseColumn: never;
249
+ identity: undefined;
250
+ generated: undefined;
251
+ }, {}, {}>;
252
+ identity: undefined;
253
+ generated: undefined;
254
+ }, {}, {
255
+ baseBuilder: import("drizzle-orm/pg-core").PgColumnBuilder<{
256
+ name: "capabilities";
257
+ dataType: "string";
258
+ columnType: "PgText";
259
+ data: string;
260
+ enumValues: [string, ...string[]];
261
+ driverParam: string;
262
+ }, {}, {}, import("drizzle-orm").ColumnBuilderExtraConfig>;
263
+ size: undefined;
264
+ }>;
265
+ };
266
+ dialect: "pg";
267
+ }>;
268
+ export declare const bridgeQueries: import("drizzle-orm/pg-core").PgTableWithColumns<{
269
+ name: "bridge_queries";
270
+ schema: undefined;
271
+ columns: {
272
+ id: import("drizzle-orm/pg-core").PgColumn<{
273
+ name: "id";
274
+ tableName: "bridge_queries";
275
+ dataType: "string";
276
+ columnType: "PgVarchar";
277
+ data: string;
278
+ driverParam: string;
279
+ notNull: true;
280
+ hasDefault: true;
281
+ isPrimaryKey: true;
282
+ isAutoincrement: false;
283
+ hasRuntimeDefault: false;
284
+ enumValues: [string, ...string[]];
285
+ baseColumn: never;
286
+ identity: undefined;
287
+ generated: undefined;
288
+ }, {}, {
289
+ length: number | undefined;
290
+ }>;
291
+ agentId: import("drizzle-orm/pg-core").PgColumn<{
292
+ name: "agent_id";
293
+ tableName: "bridge_queries";
294
+ dataType: "string";
295
+ columnType: "PgVarchar";
296
+ data: string;
297
+ driverParam: string;
298
+ notNull: false;
299
+ hasDefault: false;
300
+ isPrimaryKey: false;
301
+ isAutoincrement: false;
302
+ hasRuntimeDefault: false;
303
+ enumValues: [string, ...string[]];
304
+ baseColumn: never;
305
+ identity: undefined;
306
+ generated: undefined;
307
+ }, {}, {
308
+ length: number | undefined;
309
+ }>;
310
+ targetType: import("drizzle-orm/pg-core").PgColumn<{
311
+ name: "target_type";
312
+ tableName: "bridge_queries";
313
+ dataType: "string";
314
+ columnType: "PgText";
315
+ data: string;
316
+ driverParam: string;
317
+ notNull: true;
318
+ hasDefault: true;
319
+ isPrimaryKey: false;
320
+ isAutoincrement: false;
321
+ hasRuntimeDefault: false;
322
+ enumValues: [string, ...string[]];
323
+ baseColumn: never;
324
+ identity: undefined;
325
+ generated: undefined;
326
+ }, {}, {}>;
327
+ sqlQuery: import("drizzle-orm/pg-core").PgColumn<{
328
+ name: "sql_query";
329
+ tableName: "bridge_queries";
330
+ dataType: "string";
331
+ columnType: "PgText";
332
+ data: string;
333
+ driverParam: string;
334
+ notNull: true;
335
+ hasDefault: false;
336
+ isPrimaryKey: false;
337
+ isAutoincrement: false;
338
+ hasRuntimeDefault: false;
339
+ enumValues: [string, ...string[]];
340
+ baseColumn: never;
341
+ identity: undefined;
342
+ generated: undefined;
343
+ }, {}, {}>;
344
+ requestPayload: import("drizzle-orm/pg-core").PgColumn<{
345
+ name: "request_payload";
346
+ tableName: "bridge_queries";
347
+ dataType: "json";
348
+ columnType: "PgJsonb";
349
+ data: unknown;
350
+ driverParam: unknown;
351
+ notNull: false;
352
+ hasDefault: false;
353
+ isPrimaryKey: false;
354
+ isAutoincrement: false;
355
+ hasRuntimeDefault: false;
356
+ enumValues: undefined;
357
+ baseColumn: never;
358
+ identity: undefined;
359
+ generated: undefined;
360
+ }, {}, {}>;
361
+ status: import("drizzle-orm/pg-core").PgColumn<{
362
+ name: "status";
363
+ tableName: "bridge_queries";
364
+ dataType: "string";
365
+ columnType: "PgText";
366
+ data: string;
367
+ driverParam: string;
368
+ notNull: true;
369
+ hasDefault: true;
370
+ isPrimaryKey: false;
371
+ isAutoincrement: false;
372
+ hasRuntimeDefault: false;
373
+ enumValues: [string, ...string[]];
374
+ baseColumn: never;
375
+ identity: undefined;
376
+ generated: undefined;
377
+ }, {}, {}>;
378
+ submittedAt: import("drizzle-orm/pg-core").PgColumn<{
379
+ name: "submitted_at";
380
+ tableName: "bridge_queries";
381
+ dataType: "date";
382
+ columnType: "PgTimestamp";
383
+ data: Date;
384
+ driverParam: string;
385
+ notNull: false;
386
+ hasDefault: true;
387
+ isPrimaryKey: false;
388
+ isAutoincrement: false;
389
+ hasRuntimeDefault: false;
390
+ enumValues: undefined;
391
+ baseColumn: never;
392
+ identity: undefined;
393
+ generated: undefined;
394
+ }, {}, {}>;
395
+ executedAt: import("drizzle-orm/pg-core").PgColumn<{
396
+ name: "executed_at";
397
+ tableName: "bridge_queries";
398
+ dataType: "date";
399
+ columnType: "PgTimestamp";
400
+ data: Date;
401
+ driverParam: string;
402
+ notNull: false;
403
+ hasDefault: false;
404
+ isPrimaryKey: false;
405
+ isAutoincrement: false;
406
+ hasRuntimeDefault: false;
407
+ enumValues: undefined;
408
+ baseColumn: never;
409
+ identity: undefined;
410
+ generated: undefined;
411
+ }, {}, {}>;
412
+ completedAt: import("drizzle-orm/pg-core").PgColumn<{
413
+ name: "completed_at";
414
+ tableName: "bridge_queries";
415
+ dataType: "date";
416
+ columnType: "PgTimestamp";
417
+ data: Date;
418
+ driverParam: string;
419
+ notNull: false;
420
+ hasDefault: false;
421
+ isPrimaryKey: false;
422
+ isAutoincrement: false;
423
+ hasRuntimeDefault: false;
424
+ enumValues: undefined;
425
+ baseColumn: never;
426
+ identity: undefined;
427
+ generated: undefined;
428
+ }, {}, {}>;
429
+ rowCount: import("drizzle-orm/pg-core").PgColumn<{
430
+ name: "row_count";
431
+ tableName: "bridge_queries";
432
+ dataType: "number";
433
+ columnType: "PgInteger";
434
+ data: number;
435
+ driverParam: string | number;
436
+ notNull: false;
437
+ hasDefault: false;
438
+ isPrimaryKey: false;
439
+ isAutoincrement: false;
440
+ hasRuntimeDefault: false;
441
+ enumValues: undefined;
442
+ baseColumn: never;
443
+ identity: undefined;
444
+ generated: undefined;
445
+ }, {}, {}>;
446
+ executionTime: import("drizzle-orm/pg-core").PgColumn<{
447
+ name: "execution_time";
448
+ tableName: "bridge_queries";
449
+ dataType: "number";
450
+ columnType: "PgInteger";
451
+ data: number;
452
+ driverParam: string | number;
453
+ notNull: false;
454
+ hasDefault: false;
455
+ isPrimaryKey: false;
456
+ isAutoincrement: false;
457
+ hasRuntimeDefault: false;
458
+ enumValues: undefined;
459
+ baseColumn: never;
460
+ identity: undefined;
461
+ generated: undefined;
462
+ }, {}, {}>;
463
+ error: import("drizzle-orm/pg-core").PgColumn<{
464
+ name: "error";
465
+ tableName: "bridge_queries";
466
+ dataType: "string";
467
+ columnType: "PgText";
468
+ data: string;
469
+ driverParam: string;
470
+ notNull: false;
471
+ hasDefault: false;
472
+ isPrimaryKey: false;
473
+ isAutoincrement: false;
474
+ hasRuntimeDefault: false;
475
+ enumValues: [string, ...string[]];
476
+ baseColumn: never;
477
+ identity: undefined;
478
+ generated: undefined;
479
+ }, {}, {}>;
480
+ result: import("drizzle-orm/pg-core").PgColumn<{
481
+ name: "result";
482
+ tableName: "bridge_queries";
483
+ dataType: "json";
484
+ columnType: "PgJsonb";
485
+ data: unknown;
486
+ driverParam: unknown;
487
+ notNull: false;
488
+ hasDefault: false;
489
+ isPrimaryKey: false;
490
+ isAutoincrement: false;
491
+ hasRuntimeDefault: false;
492
+ enumValues: undefined;
493
+ baseColumn: never;
494
+ identity: undefined;
495
+ generated: undefined;
496
+ }, {}, {}>;
497
+ };
498
+ dialect: "pg";
499
+ }>;
package/dist/schema.js ADDED
@@ -0,0 +1,34 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { pgTable, text, varchar, timestamp, jsonb, integer } from "drizzle-orm/pg-core";
3
+ export const bridgeCompanies = pgTable("bridge_companies", {
4
+ id: varchar("id").primaryKey().default(sql `gen_random_uuid()`),
5
+ name: text("name").notNull(),
6
+ description: text("description"),
7
+ createdAt: timestamp("created_at").defaultNow(),
8
+ });
9
+ export const bridgeAgents = pgTable("bridge_agents", {
10
+ id: varchar("id").primaryKey().default(sql `gen_random_uuid()`),
11
+ name: text("name").notNull(),
12
+ serverName: text("server_name").notNull(),
13
+ companyId: varchar("company_id").references(() => bridgeCompanies.id),
14
+ status: text("status").notNull().default("offline"),
15
+ lastHeartbeat: timestamp("last_heartbeat"),
16
+ version: text("version"),
17
+ apiKey: text("api_key").notNull().unique(),
18
+ capabilities: text("capabilities").array().notNull().default(sql `ARRAY['sql_server']::text[]`),
19
+ });
20
+ export const bridgeQueries = pgTable("bridge_queries", {
21
+ id: varchar("id").primaryKey().default(sql `gen_random_uuid()`),
22
+ agentId: varchar("agent_id").references(() => bridgeAgents.id),
23
+ targetType: text("target_type").notNull().default("sql_server"),
24
+ sqlQuery: text("sql_query").notNull(),
25
+ requestPayload: jsonb("request_payload"),
26
+ status: text("status").notNull().default("pending"),
27
+ submittedAt: timestamp("submitted_at").defaultNow(),
28
+ executedAt: timestamp("executed_at"),
29
+ completedAt: timestamp("completed_at"),
30
+ rowCount: integer("row_count"),
31
+ executionTime: integer("execution_time"),
32
+ error: text("error"),
33
+ result: jsonb("result"),
34
+ });
package/dist/test-ui.js CHANGED
@@ -266,16 +266,31 @@ export function getTestUIHTML(options) {
266
266
  <div class="page" id="page-agents">
267
267
  <div class="page-title">Agents</div>
268
268
 
269
+ <div class="card" style="margin-bottom:20px; background: var(--surface-2); border-color: var(--primary); border-left: 3px solid var(--primary);">
270
+ <div class="card-title" style="display:flex; align-items:center; gap:8px;">
271
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
272
+ Agent Connection Info
273
+ </div>
274
+ <div style="font-size:13px; color: var(--text-dim); margin-bottom:8px;">
275
+ Configure your Windows agent software to connect to this WebSocket URL:
276
+ </div>
277
+ <div style="display:flex; align-items:center; gap:8px;">
278
+ <code id="wsUrlDisplay" data-testid="text-ws-url" style="flex:1; background:var(--bg); padding:8px 12px; border-radius:var(--radius); font-family:var(--mono); font-size:13px; border:1px solid var(--border); word-break:break-all;"></code>
279
+ <button class="btn btn-ghost btn-sm" onclick="copyWsUrl()" data-testid="button-copy-ws-url">Copy</button>
280
+ </div>
281
+ </div>
282
+
269
283
  <div class="card" style="margin-bottom:20px">
270
284
  <div class="card-title">Register New Agent</div>
271
285
  <div class="form-row">
272
286
  <div class="form-group">
273
287
  <label class="form-label">Agent Name</label>
274
- <input id="newAgentName" placeholder="Production Server" data-testid="input-agent-name" />
288
+ <input id="newAgentName" placeholder="e.g. Main Office" data-testid="input-agent-name" />
275
289
  </div>
276
290
  <div class="form-group">
277
- <label class="form-label">Server Name</label>
278
- <input id="newAgentServer" placeholder="SQLPROD01" data-testid="input-agent-server" />
291
+ <label class="form-label">Server Description</label>
292
+ <input id="newAgentServer" placeholder="e.g. Production ERP" data-testid="input-agent-server" />
293
+ <div style="font-size:11px; color:var(--text-dim); margin-top:4px;">A friendly label to identify this server (not a hostname or connection string)</div>
279
294
  </div>
280
295
  </div>
281
296
  <button class="btn btn-primary" id="registerBtn" data-testid="button-register-agent">Register Agent</button>
@@ -344,6 +359,16 @@ export function getTestUIHTML(options) {
344
359
  }
345
360
  connectWS();
346
361
 
362
+ // WebSocket URL display
363
+ const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
364
+ const wsUrl = wsProto + '//' + location.host + WS_PATH;
365
+ const wsUrlEl = document.getElementById('wsUrlDisplay');
366
+ if (wsUrlEl) wsUrlEl.textContent = wsUrl;
367
+
368
+ window.copyWsUrl = function() {
369
+ navigator.clipboard.writeText(wsUrl).then(() => alert('WebSocket URL copied!'));
370
+ };
371
+
347
372
  // API helpers
348
373
  async function api(path, opts) {
349
374
  const res = await fetch(API + path, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bestoneconsulting/sap-b1-bridge",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "SAP Business One & SQL Server bridge for secure database and ERP access behind firewalls via WebSocket agents",
@@ -27,7 +27,13 @@
27
27
  "peerDependencies": {
28
28
  "express": ">=4.0.0",
29
29
  "ws": ">=8.0.0",
30
- "zod": ">=3.0.0"
30
+ "zod": ">=3.0.0",
31
+ "drizzle-orm": ">=0.30.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "drizzle-orm": {
35
+ "optional": true
36
+ }
31
37
  },
32
38
  "scripts": {
33
39
  "build": "tsc -p ../tsconfig.bridge.json",