bullpane 0.6.3 → 0.7.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/server.mjs +1073 -190
- package/migrations/mysql/0011_flow_maps.sql +53 -0
- package/migrations/sqlite/0004_flow_maps.sql +37 -0
- package/package.json +1 -1
- package/web/assets/FlowsPage-BOHg_j5s.js +26 -0
- package/web/assets/FlowsPage-BRUBQF1y.css +1 -0
- package/web/assets/{JobTreePage-BZbwFo2w.js → JobTreePage-DWE60jpd.js} +2 -2
- package/web/assets/McpConsentPage-YpDqETHG.js +1 -0
- package/web/assets/index-BuIZl_Bc.css +1 -0
- package/web/assets/{index-DS8k2gIv.js → index-CIHznD1e.js} +115 -115
- package/web/assets/style-B5JqANGk.js +23 -0
- package/web/index.html +2 -2
- package/web/assets/FlowsPage-BVM22MG9.js +0 -6
- package/web/assets/McpConsentPage-o4BOZhJd.js +0 -1
- package/web/assets/flowLayout-Bwc9MVRI.js +0 -23
- package/web/assets/index-B5JQkwxp.css +0 -1
- /package/web/assets/{flowLayout-BnuhLJ6X.css → style-BnuhLJ6X.css} +0 -0
package/dist/server.mjs
CHANGED
|
@@ -451,6 +451,38 @@ var createFlowEdgeSchema = z.object({
|
|
|
451
451
|
to: z.string().min(1),
|
|
452
452
|
label: z.string().max(120).nullable().optional()
|
|
453
453
|
});
|
|
454
|
+
function flowMapNodeId(ref) {
|
|
455
|
+
return `${ref.connectionId}:${ref.queueName}`;
|
|
456
|
+
}
|
|
457
|
+
var flowMapNodeRefSchema = z.object({
|
|
458
|
+
connectionId: z.string().min(1).max(64),
|
|
459
|
+
queueName: z.string().min(1).max(255)
|
|
460
|
+
});
|
|
461
|
+
var createFlowMapSchema = z.object({
|
|
462
|
+
name: z.string().trim().min(1).max(80),
|
|
463
|
+
description: z.string().max(500).nullable().optional(),
|
|
464
|
+
parentId: z.string().nullable().optional()
|
|
465
|
+
});
|
|
466
|
+
var updateFlowMapSchema = z.object({
|
|
467
|
+
name: z.string().trim().min(1).max(80).optional(),
|
|
468
|
+
description: z.string().max(500).nullable().optional(),
|
|
469
|
+
/** null moves the map to the root */
|
|
470
|
+
parentId: z.string().nullable().optional(),
|
|
471
|
+
position: z.number().int().min(0).optional()
|
|
472
|
+
});
|
|
473
|
+
var addFlowMapNodeSchema = flowMapNodeRefSchema.extend({
|
|
474
|
+
x: z.number().finite().nullable().optional(),
|
|
475
|
+
y: z.number().finite().nullable().optional()
|
|
476
|
+
});
|
|
477
|
+
var saveFlowMapLayoutSchema = z.object({
|
|
478
|
+
positions: z.array(z.object({ nodeId: z.string().min(1).max(320), x: z.number().finite(), y: z.number().finite() })).max(2e3)
|
|
479
|
+
});
|
|
480
|
+
var createFlowMapEdgeSchema = z.object({
|
|
481
|
+
from: flowMapNodeRefSchema,
|
|
482
|
+
to: flowMapNodeRefSchema,
|
|
483
|
+
label: z.string().max(120).nullable().optional()
|
|
484
|
+
});
|
|
485
|
+
var updateFlowMapEdgeSchema = z.object({ label: z.string().max(120).nullable() });
|
|
454
486
|
var jobTreeQuerySchema = z.object({
|
|
455
487
|
/** hard cap on jobs read; the walk is breadth-first so the cap keeps the top of the tree */
|
|
456
488
|
maxNodes: z.coerce.number().int().min(1).max(500).default(200),
|
|
@@ -4005,6 +4037,9 @@ __export(schema_mysql_exports, {
|
|
|
4005
4037
|
auditLog: () => auditLog,
|
|
4006
4038
|
connections: () => connections,
|
|
4007
4039
|
flowEdges: () => flowEdges,
|
|
4040
|
+
flowMapEdges: () => flowMapEdges,
|
|
4041
|
+
flowMapNodes: () => flowMapNodes,
|
|
4042
|
+
flowMaps: () => flowMaps,
|
|
4008
4043
|
folderQueues: () => folderQueues,
|
|
4009
4044
|
folders: () => folders,
|
|
4010
4045
|
hiddenQueues: () => hiddenQueues,
|
|
@@ -4201,6 +4236,48 @@ var flowEdges = mysqlTable(
|
|
|
4201
4236
|
},
|
|
4202
4237
|
(t) => [uniqueIndex("flow_edges_unique").on(t.connectionId, t.fromQueue, t.toQueue)]
|
|
4203
4238
|
);
|
|
4239
|
+
var flowMaps = mysqlTable(
|
|
4240
|
+
"flow_maps",
|
|
4241
|
+
{
|
|
4242
|
+
id: id(),
|
|
4243
|
+
name: varchar("name", { length: 80 }).notNull(),
|
|
4244
|
+
description: varchar("description", { length: 500 }),
|
|
4245
|
+
parentId: varchar("parent_id", { length: 36 }),
|
|
4246
|
+
position: int("position").notNull().default(0),
|
|
4247
|
+
createdAt: createdAt(),
|
|
4248
|
+
updatedAt: datetime("updated_at", { mode: "date", fsp: 3 }).notNull()
|
|
4249
|
+
},
|
|
4250
|
+
(t) => [index("flow_maps_parent_id_idx").on(t.parentId)]
|
|
4251
|
+
);
|
|
4252
|
+
var flowMapNodes = mysqlTable(
|
|
4253
|
+
"flow_map_nodes",
|
|
4254
|
+
{
|
|
4255
|
+
mapId: varchar("map_id", { length: 36 }).notNull(),
|
|
4256
|
+
connectionId: varchar("connection_id", { length: 36 }).notNull(),
|
|
4257
|
+
queueName: varchar("queue_name", { length: 255 }).notNull(),
|
|
4258
|
+
x: double("x"),
|
|
4259
|
+
y: double("y")
|
|
4260
|
+
},
|
|
4261
|
+
(t) => [primaryKey({ columns: [t.mapId, t.connectionId, t.queueName] }), index("flow_map_nodes_connection_idx").on(t.connectionId)]
|
|
4262
|
+
);
|
|
4263
|
+
var flowMapEdges = mysqlTable(
|
|
4264
|
+
"flow_map_edges",
|
|
4265
|
+
{
|
|
4266
|
+
id: id(),
|
|
4267
|
+
mapId: varchar("map_id", { length: 36 }).notNull(),
|
|
4268
|
+
fromConnectionId: varchar("from_connection_id", { length: 36 }).notNull(),
|
|
4269
|
+
fromQueue: varchar("from_queue", { length: 255 }).notNull(),
|
|
4270
|
+
toConnectionId: varchar("to_connection_id", { length: 36 }).notNull(),
|
|
4271
|
+
toQueue: varchar("to_queue", { length: 255 }).notNull(),
|
|
4272
|
+
label: varchar("label", { length: 120 }),
|
|
4273
|
+
createdAt: createdAt()
|
|
4274
|
+
},
|
|
4275
|
+
(t) => [
|
|
4276
|
+
uniqueIndex("flow_map_edges_unique").on(t.mapId, t.fromConnectionId, t.fromQueue, t.toConnectionId, t.toQueue),
|
|
4277
|
+
index("flow_map_edges_from_conn_idx").on(t.fromConnectionId),
|
|
4278
|
+
index("flow_map_edges_to_conn_idx").on(t.toConnectionId)
|
|
4279
|
+
]
|
|
4280
|
+
);
|
|
4204
4281
|
var mcpClients = mysqlTable(
|
|
4205
4282
|
"mcp_clients",
|
|
4206
4283
|
{
|
|
@@ -4256,6 +4333,9 @@ __export(schema_sqlite_exports, {
|
|
|
4256
4333
|
auditLog: () => auditLog2,
|
|
4257
4334
|
connections: () => connections2,
|
|
4258
4335
|
flowEdges: () => flowEdges2,
|
|
4336
|
+
flowMapEdges: () => flowMapEdges2,
|
|
4337
|
+
flowMapNodes: () => flowMapNodes2,
|
|
4338
|
+
flowMaps: () => flowMaps2,
|
|
4259
4339
|
folderQueues: () => folderQueues2,
|
|
4260
4340
|
folders: () => folders2,
|
|
4261
4341
|
hiddenQueues: () => hiddenQueues2,
|
|
@@ -4471,6 +4551,48 @@ var settings2 = sqliteTable("settings", {
|
|
|
4471
4551
|
key: text2("key").primaryKey(),
|
|
4472
4552
|
value: text2("value").notNull()
|
|
4473
4553
|
});
|
|
4554
|
+
var flowMaps2 = sqliteTable(
|
|
4555
|
+
"flow_maps",
|
|
4556
|
+
{
|
|
4557
|
+
id: id2(),
|
|
4558
|
+
name: text2("name").notNull(),
|
|
4559
|
+
description: text2("description"),
|
|
4560
|
+
parentId: text2("parent_id"),
|
|
4561
|
+
position: integer("position").notNull().default(0),
|
|
4562
|
+
createdAt: createdAt2(),
|
|
4563
|
+
updatedAt: timestamp("updated_at").notNull()
|
|
4564
|
+
},
|
|
4565
|
+
(t) => [index2("flow_maps_parent_id_idx").on(t.parentId)]
|
|
4566
|
+
);
|
|
4567
|
+
var flowMapNodes2 = sqliteTable(
|
|
4568
|
+
"flow_map_nodes",
|
|
4569
|
+
{
|
|
4570
|
+
mapId: text2("map_id").notNull(),
|
|
4571
|
+
connectionId: text2("connection_id").notNull(),
|
|
4572
|
+
queueName: text2("queue_name").notNull(),
|
|
4573
|
+
x: real("x"),
|
|
4574
|
+
y: real("y")
|
|
4575
|
+
},
|
|
4576
|
+
(t) => [primaryKey2({ columns: [t.mapId, t.connectionId, t.queueName] }), index2("flow_map_nodes_connection_idx").on(t.connectionId)]
|
|
4577
|
+
);
|
|
4578
|
+
var flowMapEdges2 = sqliteTable(
|
|
4579
|
+
"flow_map_edges",
|
|
4580
|
+
{
|
|
4581
|
+
id: id2(),
|
|
4582
|
+
mapId: text2("map_id").notNull(),
|
|
4583
|
+
fromConnectionId: text2("from_connection_id").notNull(),
|
|
4584
|
+
fromQueue: text2("from_queue").notNull(),
|
|
4585
|
+
toConnectionId: text2("to_connection_id").notNull(),
|
|
4586
|
+
toQueue: text2("to_queue").notNull(),
|
|
4587
|
+
label: text2("label"),
|
|
4588
|
+
createdAt: createdAt2()
|
|
4589
|
+
},
|
|
4590
|
+
(t) => [
|
|
4591
|
+
uniqueIndex2("flow_map_edges_unique").on(t.mapId, t.fromConnectionId, t.fromQueue, t.toConnectionId, t.toQueue),
|
|
4592
|
+
index2("flow_map_edges_from_conn_idx").on(t.fromConnectionId),
|
|
4593
|
+
index2("flow_map_edges_to_conn_idx").on(t.toConnectionId)
|
|
4594
|
+
]
|
|
4595
|
+
);
|
|
4474
4596
|
|
|
4475
4597
|
// apps/server/src/db/schema.ts
|
|
4476
4598
|
var users3 = users;
|
|
@@ -4488,6 +4610,9 @@ var settings3 = settings;
|
|
|
4488
4610
|
var mcpClients3 = mcpClients;
|
|
4489
4611
|
var mcpAuthCodes3 = mcpAuthCodes;
|
|
4490
4612
|
var mcpGrants3 = mcpGrants;
|
|
4613
|
+
var flowMaps3 = flowMaps;
|
|
4614
|
+
var flowMapNodes3 = flowMapNodes;
|
|
4615
|
+
var flowMapEdges3 = flowMapEdges;
|
|
4491
4616
|
var current = "mysql";
|
|
4492
4617
|
function schemaDialect() {
|
|
4493
4618
|
return current;
|
|
@@ -4509,6 +4634,9 @@ function useSchema(dialect) {
|
|
|
4509
4634
|
mcpClients3 = s.mcpClients;
|
|
4510
4635
|
mcpAuthCodes3 = s.mcpAuthCodes;
|
|
4511
4636
|
mcpGrants3 = s.mcpGrants;
|
|
4637
|
+
flowMaps3 = s.flowMaps;
|
|
4638
|
+
flowMapNodes3 = s.flowMapNodes;
|
|
4639
|
+
flowMapEdges3 = s.flowMapEdges;
|
|
4512
4640
|
current = dialect;
|
|
4513
4641
|
}
|
|
4514
4642
|
|
|
@@ -5590,7 +5718,17 @@ var UNAUDITED = /* @__PURE__ */ new Set([
|
|
|
5590
5718
|
"POST /api/connections/test",
|
|
5591
5719
|
"POST /api/setup",
|
|
5592
5720
|
"POST /api/alerts/:id/test",
|
|
5593
|
-
"POST /api/license/refresh"
|
|
5721
|
+
"POST /api/license/refresh",
|
|
5722
|
+
"POST /api/flow-maps",
|
|
5723
|
+
"PATCH /api/flow-maps/:id",
|
|
5724
|
+
"DELETE /api/flow-maps/:id",
|
|
5725
|
+
"POST /api/flow-maps/:id/nodes",
|
|
5726
|
+
"DELETE /api/flow-maps/:id/nodes/:nodeId",
|
|
5727
|
+
"PUT /api/flow-maps/:id/layout",
|
|
5728
|
+
"POST /api/flow-maps/:id/edges",
|
|
5729
|
+
"PATCH /api/flow-maps/:id/edges/:edgeId",
|
|
5730
|
+
"DELETE /api/flow-maps/:id/edges/:edgeId",
|
|
5731
|
+
"POST /api/flow-maps/:id/copy"
|
|
5594
5732
|
]);
|
|
5595
5733
|
function actionFor(method, routePattern) {
|
|
5596
5734
|
if (!routePattern) return null;
|
|
@@ -6652,9 +6790,213 @@ async function connectionRoutes(app) {
|
|
|
6652
6790
|
});
|
|
6653
6791
|
}
|
|
6654
6792
|
|
|
6793
|
+
// apps/server/src/ee/routes/flowMaps.ts
|
|
6794
|
+
var copyFlowMapSchema = createFlowMapSchema.omit({ description: true }).partial({ name: true });
|
|
6795
|
+
async function flowMapRoutes(app) {
|
|
6796
|
+
const gate = requireFeature("flows");
|
|
6797
|
+
const viewer = [requireAuth, gate, requireRole("viewer")];
|
|
6798
|
+
const operator = [requireAuth, gate, requireRole("operator")];
|
|
6799
|
+
app.get("/flow-maps", { preHandler: viewer }, async () => app.ctx.flowMaps.list());
|
|
6800
|
+
app.post("/flow-maps", { preHandler: operator }, async (request, reply) => {
|
|
6801
|
+
const map = await app.ctx.flowMaps.create(createFlowMapSchema.parse(request.body));
|
|
6802
|
+
reply.status(201);
|
|
6803
|
+
return map;
|
|
6804
|
+
});
|
|
6805
|
+
app.get("/flow-maps/:id", { preHandler: viewer }, async (request) => app.ctx.flowMaps.get(request.params.id));
|
|
6806
|
+
app.patch(
|
|
6807
|
+
"/flow-maps/:id",
|
|
6808
|
+
{ preHandler: operator },
|
|
6809
|
+
async (request) => app.ctx.flowMaps.update(request.params.id, updateFlowMapSchema.parse(request.body ?? {}))
|
|
6810
|
+
);
|
|
6811
|
+
app.delete("/flow-maps/:id", { preHandler: operator }, async (request) => {
|
|
6812
|
+
await app.ctx.flowMaps.remove(request.params.id);
|
|
6813
|
+
return { ok: true };
|
|
6814
|
+
});
|
|
6815
|
+
app.post(
|
|
6816
|
+
"/flow-maps/:id/nodes",
|
|
6817
|
+
{ preHandler: operator },
|
|
6818
|
+
async (request) => app.ctx.flowMaps.addNode(request.params.id, addFlowMapNodeSchema.parse(request.body))
|
|
6819
|
+
);
|
|
6820
|
+
app.delete(
|
|
6821
|
+
"/flow-maps/:id/nodes/:nodeId",
|
|
6822
|
+
{ preHandler: operator },
|
|
6823
|
+
async (request) => app.ctx.flowMaps.removeNode(request.params.id, request.params.nodeId)
|
|
6824
|
+
);
|
|
6825
|
+
app.put("/flow-maps/:id/layout", { preHandler: operator }, async (request) => {
|
|
6826
|
+
await app.ctx.flowMaps.saveLayout(request.params.id, saveFlowMapLayoutSchema.parse(request.body));
|
|
6827
|
+
return { ok: true };
|
|
6828
|
+
});
|
|
6829
|
+
app.post("/flow-maps/:id/edges", { preHandler: operator }, async (request, reply) => {
|
|
6830
|
+
const map = await app.ctx.flowMaps.addEdge(request.params.id, createFlowMapEdgeSchema.parse(request.body));
|
|
6831
|
+
reply.status(201);
|
|
6832
|
+
return map;
|
|
6833
|
+
});
|
|
6834
|
+
app.patch(
|
|
6835
|
+
"/flow-maps/:id/edges/:edgeId",
|
|
6836
|
+
{ preHandler: operator },
|
|
6837
|
+
async (request) => app.ctx.flowMaps.updateEdge(request.params.id, request.params.edgeId, updateFlowMapEdgeSchema.parse(request.body))
|
|
6838
|
+
);
|
|
6839
|
+
app.delete(
|
|
6840
|
+
"/flow-maps/:id/edges/:edgeId",
|
|
6841
|
+
{ preHandler: operator },
|
|
6842
|
+
async (request) => app.ctx.flowMaps.removeEdge(request.params.id, request.params.edgeId)
|
|
6843
|
+
);
|
|
6844
|
+
app.post("/flow-maps/:id/copy", { preHandler: operator }, async (request, reply) => {
|
|
6845
|
+
const map = await app.ctx.flowMaps.copy(request.params.id, copyFlowMapSchema.parse(request.body ?? {}));
|
|
6846
|
+
reply.status(201);
|
|
6847
|
+
return map;
|
|
6848
|
+
});
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6655
6851
|
// apps/server/src/ee/routes/flows.ts
|
|
6656
6852
|
import { z as z4 } from "zod";
|
|
6657
|
-
|
|
6853
|
+
|
|
6854
|
+
// apps/server/src/ee/services/flows.ts
|
|
6855
|
+
import { and as and3, eq as eq3 } from "drizzle-orm";
|
|
6856
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
6857
|
+
var FLOW_CACHE_TTL_MS = 3e4;
|
|
6858
|
+
var FLOW_CONCURRENCY = 5;
|
|
6859
|
+
var DEFAULT_FLOW_SAMPLE = 200;
|
|
6860
|
+
function detectedEdgeId(from, to) {
|
|
6861
|
+
return `d:${from}->${to}`;
|
|
6862
|
+
}
|
|
6863
|
+
function toFlowEdgeDto(row) {
|
|
6864
|
+
return { id: row.id, from: row.fromQueue, to: row.toQueue, source: "manual", evidence: 0, label: row.label ?? null };
|
|
6865
|
+
}
|
|
6866
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
6867
|
+
const results = new Array(items.length);
|
|
6868
|
+
let next = 0;
|
|
6869
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
6870
|
+
while (next < items.length) {
|
|
6871
|
+
const index3 = next++;
|
|
6872
|
+
results[index3] = await fn(items[index3]);
|
|
6873
|
+
}
|
|
6874
|
+
});
|
|
6875
|
+
await Promise.all(workers);
|
|
6876
|
+
return results;
|
|
6877
|
+
}
|
|
6878
|
+
var FlowsService = class {
|
|
6879
|
+
constructor(db, connections5) {
|
|
6880
|
+
this.db = db;
|
|
6881
|
+
this.connections = connections5;
|
|
6882
|
+
}
|
|
6883
|
+
cache = /* @__PURE__ */ new Map();
|
|
6884
|
+
async getGraph(connectionId, sample) {
|
|
6885
|
+
const row = await this.connections.getRow(connectionId);
|
|
6886
|
+
const [detected, manual] = await Promise.all([this.detected(row, sample), this.manualEdges(connectionId)]);
|
|
6887
|
+
const nodeIds = new Set(detected.nodes.map((n) => n.id));
|
|
6888
|
+
const nodes = [...detected.nodes];
|
|
6889
|
+
for (const edge of manual) {
|
|
6890
|
+
for (const name of [edge.from, edge.to]) {
|
|
6891
|
+
if (!nodeIds.has(name)) {
|
|
6892
|
+
nodeIds.add(name);
|
|
6893
|
+
nodes.push({ id: name, queueName: name, counts: { ...EMPTY_COUNTS }, isPaused: false });
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6896
|
+
}
|
|
6897
|
+
return {
|
|
6898
|
+
connectionId,
|
|
6899
|
+
nodes,
|
|
6900
|
+
edges: [...detected.edges, ...manual],
|
|
6901
|
+
sampledJobs: detected.sampledJobs
|
|
6902
|
+
};
|
|
6903
|
+
}
|
|
6904
|
+
/**
|
|
6905
|
+
* The detected (FlowProducer) edges of one connection, from the same 30 s
|
|
6906
|
+
* cache as getGraph, so flow maps and the /flows page share one sample.
|
|
6907
|
+
* Edge ends are queue names. Rejects when the connection is down.
|
|
6908
|
+
*/
|
|
6909
|
+
async detectedEdges(row, sample = DEFAULT_FLOW_SAMPLE) {
|
|
6910
|
+
return (await this.detected(row, sample)).edges;
|
|
6911
|
+
}
|
|
6912
|
+
invalidate(connectionId) {
|
|
6913
|
+
this.cache.delete(connectionId);
|
|
6914
|
+
}
|
|
6915
|
+
detected(row, sample) {
|
|
6916
|
+
const cached = this.cache.get(row.id);
|
|
6917
|
+
const now = Date.now();
|
|
6918
|
+
if (cached && cached.sample === sample && now - cached.at < FLOW_CACHE_TTL_MS) return cached.value;
|
|
6919
|
+
const value = withRedis(() => this.computeDetected(this.connections.inspectorFor(row), sample));
|
|
6920
|
+
this.cache.set(row.id, { at: now, sample, value });
|
|
6921
|
+
value.catch(() => this.cache.delete(row.id));
|
|
6922
|
+
return value;
|
|
6923
|
+
}
|
|
6924
|
+
async computeDetected(inspector, sample) {
|
|
6925
|
+
const names = await inspector.discoverQueues();
|
|
6926
|
+
if (names.length === 0) return { nodes: [], edges: [], sampledJobs: 0 };
|
|
6927
|
+
const stats = await inspector.getQueueStats(names);
|
|
6928
|
+
const nodes = names.map((name) => ({
|
|
6929
|
+
id: name,
|
|
6930
|
+
queueName: name,
|
|
6931
|
+
counts: stats[name]?.counts ?? { ...EMPTY_COUNTS },
|
|
6932
|
+
isPaused: stats[name]?.isPaused ?? false
|
|
6933
|
+
}));
|
|
6934
|
+
const samples = await mapWithConcurrency(
|
|
6935
|
+
names,
|
|
6936
|
+
FLOW_CONCURRENCY,
|
|
6937
|
+
(name) => inspector.sampleFlowEdges(name, { sample })
|
|
6938
|
+
);
|
|
6939
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
6940
|
+
let sampledJobs = 0;
|
|
6941
|
+
for (const result of samples) {
|
|
6942
|
+
sampledJobs += result.sampled;
|
|
6943
|
+
for (const e of result.edges) {
|
|
6944
|
+
const id3 = detectedEdgeId(e.childQueue, e.parentQueue);
|
|
6945
|
+
const existing = edgeMap.get(id3);
|
|
6946
|
+
if (existing) {
|
|
6947
|
+
existing.evidence += e.count;
|
|
6948
|
+
} else {
|
|
6949
|
+
edgeMap.set(id3, {
|
|
6950
|
+
id: id3,
|
|
6951
|
+
from: e.childQueue,
|
|
6952
|
+
to: e.parentQueue,
|
|
6953
|
+
source: "detected",
|
|
6954
|
+
evidence: e.count,
|
|
6955
|
+
label: null
|
|
6956
|
+
});
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
return { nodes, edges: [...edgeMap.values()], sampledJobs };
|
|
6961
|
+
}
|
|
6962
|
+
async manualEdges(connectionId) {
|
|
6963
|
+
const rows = await this.db.select().from(flowEdges3).where(eq3(flowEdges3.connectionId, connectionId));
|
|
6964
|
+
return rows.map(toFlowEdgeDto);
|
|
6965
|
+
}
|
|
6966
|
+
async createManualEdge(input) {
|
|
6967
|
+
await this.connections.getRow(input.connectionId);
|
|
6968
|
+
if (input.from === input.to) throw conflict("A queue cannot flow into itself");
|
|
6969
|
+
const existing = await this.db.select({ id: flowEdges3.id }).from(flowEdges3).where(
|
|
6970
|
+
and3(
|
|
6971
|
+
eq3(flowEdges3.connectionId, input.connectionId),
|
|
6972
|
+
eq3(flowEdges3.fromQueue, input.from),
|
|
6973
|
+
eq3(flowEdges3.toQueue, input.to)
|
|
6974
|
+
)
|
|
6975
|
+
).limit(1);
|
|
6976
|
+
if (existing[0]) throw conflict("This edge already exists");
|
|
6977
|
+
const id3 = nanoid2();
|
|
6978
|
+
await this.db.insert(flowEdges3).values({
|
|
6979
|
+
id: id3,
|
|
6980
|
+
connectionId: input.connectionId,
|
|
6981
|
+
fromQueue: input.from,
|
|
6982
|
+
toQueue: input.to,
|
|
6983
|
+
label: input.label ?? null,
|
|
6984
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
6985
|
+
});
|
|
6986
|
+
const rows = await this.db.select().from(flowEdges3).where(eq3(flowEdges3.id, id3)).limit(1);
|
|
6987
|
+
const row = rows[0];
|
|
6988
|
+
if (!row) throw notFound("Flow edge");
|
|
6989
|
+
return toFlowEdgeDto(row);
|
|
6990
|
+
}
|
|
6991
|
+
async deleteManualEdge(id3) {
|
|
6992
|
+
const rows = await this.db.select({ id: flowEdges3.id }).from(flowEdges3).where(eq3(flowEdges3.id, id3)).limit(1);
|
|
6993
|
+
if (!rows[0]) throw notFound("Flow edge");
|
|
6994
|
+
await this.db.delete(flowEdges3).where(eq3(flowEdges3.id, id3));
|
|
6995
|
+
}
|
|
6996
|
+
};
|
|
6997
|
+
|
|
6998
|
+
// apps/server/src/ee/routes/flows.ts
|
|
6999
|
+
var flowsQuerySchema = z4.object({ sample: z4.coerce.number().int().min(10).max(2e3).default(DEFAULT_FLOW_SAMPLE) });
|
|
6658
7000
|
async function flowRoutes(app) {
|
|
6659
7001
|
const gate = requireFeature("flows");
|
|
6660
7002
|
const viewer = [requireAuth, gate, requireRole("viewer")];
|
|
@@ -7148,7 +7490,7 @@ async function ssoRoutes(app) {
|
|
|
7148
7490
|
|
|
7149
7491
|
// apps/server/src/ee/mcp/service.ts
|
|
7150
7492
|
import { createHash as createHash2, createHmac, hkdfSync as hkdfSync2, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
7151
|
-
import { nanoid as
|
|
7493
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
7152
7494
|
var MCP_SETTINGS_KEY = "mcp.max_access";
|
|
7153
7495
|
var MCP_SCOPES = { read: "queues:read", write: "queues:write" };
|
|
7154
7496
|
var ACCESS_TTL_S = 60 * 60;
|
|
@@ -7431,7 +7773,7 @@ var McpService = class {
|
|
|
7431
7773
|
const challenge = b64url(createHash2("sha256").update(p.code_verifier).digest());
|
|
7432
7774
|
if (!safeEqual2(challenge, row.codeChallenge)) throw new OAuthError(400, "invalid_grant", "PKCE verification failed");
|
|
7433
7775
|
const refresh = random("bprt_");
|
|
7434
|
-
const grantId =
|
|
7776
|
+
const grantId = nanoid3();
|
|
7435
7777
|
await this.store.insertGrant({
|
|
7436
7778
|
id: grantId,
|
|
7437
7779
|
clientId: row.clientId,
|
|
@@ -7654,6 +7996,7 @@ async function apiPlugin(app) {
|
|
|
7654
7996
|
await app.register(jobRoutes);
|
|
7655
7997
|
await app.register(groupRoutes);
|
|
7656
7998
|
await app.register(flowRoutes);
|
|
7999
|
+
await app.register(flowMapRoutes);
|
|
7657
8000
|
await app.register(folderRoutes);
|
|
7658
8001
|
await app.register(alertRoutes);
|
|
7659
8002
|
await app.register(userRoutes);
|
|
@@ -7663,8 +8006,8 @@ async function apiPlugin(app) {
|
|
|
7663
8006
|
}
|
|
7664
8007
|
|
|
7665
8008
|
// apps/server/src/ee/services/audit.ts
|
|
7666
|
-
import { and as
|
|
7667
|
-
import { nanoid as
|
|
8009
|
+
import { and as and4, asc, desc as desc2, eq as eq4, gte, lt as lt3, lte, or, sql } from "drizzle-orm";
|
|
8010
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
7668
8011
|
var MAX_ERROR = 500;
|
|
7669
8012
|
var MAX_UA = 255;
|
|
7670
8013
|
var MAX_JOB_ID = 255;
|
|
@@ -7748,7 +8091,7 @@ var AuditService = class {
|
|
|
7748
8091
|
async record(input) {
|
|
7749
8092
|
try {
|
|
7750
8093
|
await this.db.insert(auditLog3).values({
|
|
7751
|
-
id:
|
|
8094
|
+
id: nanoid4(),
|
|
7752
8095
|
createdAt: input.createdAt ?? /* @__PURE__ */ new Date(),
|
|
7753
8096
|
actorId: input.actor?.id ?? null,
|
|
7754
8097
|
// Denormalised on purpose: the row must still name the person after the
|
|
@@ -7780,22 +8123,22 @@ var AuditService = class {
|
|
|
7780
8123
|
*/
|
|
7781
8124
|
async list(query) {
|
|
7782
8125
|
const filters = [];
|
|
7783
|
-
if (query.actorId) filters.push(
|
|
7784
|
-
if (query.action) filters.push(
|
|
7785
|
-
if (query.connectionId) filters.push(
|
|
7786
|
-
if (query.queueName) filters.push(
|
|
7787
|
-
if (query.jobId) filters.push(
|
|
7788
|
-
if (query.result) filters.push(
|
|
8126
|
+
if (query.actorId) filters.push(eq4(auditLog3.actorId, query.actorId));
|
|
8127
|
+
if (query.action) filters.push(eq4(auditLog3.action, query.action));
|
|
8128
|
+
if (query.connectionId) filters.push(eq4(auditLog3.connectionId, query.connectionId));
|
|
8129
|
+
if (query.queueName) filters.push(eq4(auditLog3.queueName, query.queueName));
|
|
8130
|
+
if (query.jobId) filters.push(eq4(auditLog3.jobId, query.jobId));
|
|
8131
|
+
if (query.result) filters.push(eq4(auditLog3.result, query.result));
|
|
7789
8132
|
if (query.from) filters.push(gte(auditLog3.createdAt, new Date(query.from)));
|
|
7790
8133
|
if (query.to) filters.push(lt3(auditLog3.createdAt, new Date(query.to)));
|
|
7791
8134
|
if (query.cursor) {
|
|
7792
8135
|
const c = decodeCursor(query.cursor);
|
|
7793
8136
|
if (c) {
|
|
7794
|
-
const keyset = or(lt3(auditLog3.createdAt, c.createdAt),
|
|
8137
|
+
const keyset = or(lt3(auditLog3.createdAt, c.createdAt), and4(eq4(auditLog3.createdAt, c.createdAt), lt3(auditLog3.id, c.id)));
|
|
7795
8138
|
if (keyset) filters.push(keyset);
|
|
7796
8139
|
}
|
|
7797
8140
|
}
|
|
7798
|
-
const where = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] :
|
|
8141
|
+
const where = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : and4(...filters);
|
|
7799
8142
|
const rows = await this.db.select().from(auditLog3).where(where).orderBy(desc2(auditLog3.createdAt), desc2(auditLog3.id)).limit(query.limit + 1);
|
|
7800
8143
|
const page = rows.slice(0, query.limit);
|
|
7801
8144
|
const last = page[page.length - 1];
|
|
@@ -7860,8 +8203,8 @@ var AuditService = class {
|
|
|
7860
8203
|
};
|
|
7861
8204
|
|
|
7862
8205
|
// apps/server/src/services/connections.ts
|
|
7863
|
-
import { and as
|
|
7864
|
-
import { nanoid as
|
|
8206
|
+
import { and as and5, eq as eq5, inArray, or as or2 } from "drizzle-orm";
|
|
8207
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
7865
8208
|
var STATUS_TTL_MS = 1e4;
|
|
7866
8209
|
var SCHEDULER_FANOUT_CONCURRENCY = 6;
|
|
7867
8210
|
var SCHEDULERS_PER_QUEUE_CAP = 500;
|
|
@@ -7914,7 +8257,7 @@ var ConnectionsService = class {
|
|
|
7914
8257
|
return this.db.select().from(connections3).orderBy(connections3.position, connections3.createdAt);
|
|
7915
8258
|
}
|
|
7916
8259
|
async getRow(id3) {
|
|
7917
|
-
const rows = await this.db.select().from(connections3).where(
|
|
8260
|
+
const rows = await this.db.select().from(connections3).where(eq5(connections3.id, id3)).limit(1);
|
|
7918
8261
|
const row = rows[0];
|
|
7919
8262
|
if (!row) throw notFound("Connection");
|
|
7920
8263
|
return row;
|
|
@@ -7929,7 +8272,7 @@ var ConnectionsService = class {
|
|
|
7929
8272
|
return Promise.all(rows.map(async (row) => toConnectionDto(row, await this.getStatus(row))));
|
|
7930
8273
|
}
|
|
7931
8274
|
async create(input) {
|
|
7932
|
-
const id3 =
|
|
8275
|
+
const id3 = nanoid5();
|
|
7933
8276
|
const siblings = await this.db.select({ id: connections3.id }).from(connections3);
|
|
7934
8277
|
await this.db.insert(connections3).values({
|
|
7935
8278
|
id: id3,
|
|
@@ -7966,7 +8309,7 @@ var ConnectionsService = class {
|
|
|
7966
8309
|
const ordered = [...ids, ...rows.filter((r) => !ids.includes(r.id)).map((r) => r.id)];
|
|
7967
8310
|
await this.db.transaction(async (tx) => {
|
|
7968
8311
|
for (const [index3, id3] of ordered.entries()) {
|
|
7969
|
-
await tx.update(connections3).set({ position: index3 }).where(
|
|
8312
|
+
await tx.update(connections3).set({ position: index3 }).where(eq5(connections3.id, id3));
|
|
7970
8313
|
}
|
|
7971
8314
|
});
|
|
7972
8315
|
return this.list();
|
|
@@ -7982,7 +8325,7 @@ var ConnectionsService = class {
|
|
|
7982
8325
|
if (input.cluster !== void 0) patch.cluster = input.cluster;
|
|
7983
8326
|
if (input.queueFilter !== void 0) patch.queueFilter = input.queueFilter;
|
|
7984
8327
|
if (Object.keys(patch).length > 0) {
|
|
7985
|
-
await this.db.update(connections3).set(patch).where(
|
|
8328
|
+
await this.db.update(connections3).set(patch).where(eq5(connections3.id, id3));
|
|
7986
8329
|
}
|
|
7987
8330
|
await this.pool.evict(id3);
|
|
7988
8331
|
this.notifyEvict(id3);
|
|
@@ -7995,11 +8338,13 @@ var ConnectionsService = class {
|
|
|
7995
8338
|
await this.pool.evict(id3);
|
|
7996
8339
|
this.notifyEvict(id3);
|
|
7997
8340
|
this.statusCache.delete(id3);
|
|
7998
|
-
await this.db.delete(folderQueues3).where(
|
|
7999
|
-
await this.db.delete(hiddenQueues3).where(
|
|
8000
|
-
await this.db.delete(alerts3).where(
|
|
8001
|
-
await this.db.delete(flowEdges3).where(
|
|
8002
|
-
await this.db.delete(
|
|
8341
|
+
await this.db.delete(folderQueues3).where(eq5(folderQueues3.connectionId, id3));
|
|
8342
|
+
await this.db.delete(hiddenQueues3).where(eq5(hiddenQueues3.connectionId, id3));
|
|
8343
|
+
await this.db.delete(alerts3).where(eq5(alerts3.connectionId, id3));
|
|
8344
|
+
await this.db.delete(flowEdges3).where(eq5(flowEdges3.connectionId, id3));
|
|
8345
|
+
await this.db.delete(flowMapEdges3).where(or2(eq5(flowMapEdges3.fromConnectionId, id3), eq5(flowMapEdges3.toConnectionId, id3)));
|
|
8346
|
+
await this.db.delete(flowMapNodes3).where(eq5(flowMapNodes3.connectionId, id3));
|
|
8347
|
+
await this.db.delete(connections3).where(eq5(connections3.id, id3));
|
|
8003
8348
|
}
|
|
8004
8349
|
inspectorFor(row) {
|
|
8005
8350
|
return this.pool.get({
|
|
@@ -8056,12 +8401,12 @@ var ConnectionsService = class {
|
|
|
8056
8401
|
// -------------------------------------------------------------------------
|
|
8057
8402
|
/** Names hidden on this connection, as a Set for O(1) filtering. */
|
|
8058
8403
|
async hiddenQueueNames(connectionId) {
|
|
8059
|
-
const rows = await this.db.select({ queueName: hiddenQueues3.queueName }).from(hiddenQueues3).where(
|
|
8404
|
+
const rows = await this.db.select({ queueName: hiddenQueues3.queueName }).from(hiddenQueues3).where(eq5(hiddenQueues3.connectionId, connectionId));
|
|
8060
8405
|
return new Set(rows.map((r) => r.queueName));
|
|
8061
8406
|
}
|
|
8062
8407
|
/** The hidden list for the UI, newest first, with the display name of who hid it. */
|
|
8063
8408
|
async listHiddenQueues(connectionId) {
|
|
8064
|
-
const rows = await this.db.select().from(hiddenQueues3).where(
|
|
8409
|
+
const rows = await this.db.select().from(hiddenQueues3).where(eq5(hiddenQueues3.connectionId, connectionId)).orderBy(hiddenQueues3.hiddenAt);
|
|
8065
8410
|
const ids = [...new Set(rows.map((r) => r.hiddenBy).filter((v) => !!v))];
|
|
8066
8411
|
const names = /* @__PURE__ */ new Map();
|
|
8067
8412
|
if (ids.length > 0) {
|
|
@@ -8080,7 +8425,7 @@ var ConnectionsService = class {
|
|
|
8080
8425
|
/** Idempotent: hiding an already hidden queue is a no-op, not a 409. */
|
|
8081
8426
|
async hideQueue(connectionId, queueName, userId) {
|
|
8082
8427
|
await this.getRow(connectionId);
|
|
8083
|
-
const existing = await this.db.select({ queueName: hiddenQueues3.queueName }).from(hiddenQueues3).where(
|
|
8428
|
+
const existing = await this.db.select({ queueName: hiddenQueues3.queueName }).from(hiddenQueues3).where(and5(eq5(hiddenQueues3.connectionId, connectionId), eq5(hiddenQueues3.queueName, queueName))).limit(1);
|
|
8084
8429
|
if (existing.length === 0) {
|
|
8085
8430
|
await this.db.insert(hiddenQueues3).values({ connectionId, queueName, hiddenAt: /* @__PURE__ */ new Date(), hiddenBy: userId });
|
|
8086
8431
|
}
|
|
@@ -8089,11 +8434,11 @@ var ConnectionsService = class {
|
|
|
8089
8434
|
/** Idempotent too: unhiding something that is not hidden succeeds. */
|
|
8090
8435
|
async unhideQueue(connectionId, queueName) {
|
|
8091
8436
|
await this.getRow(connectionId);
|
|
8092
|
-
await this.db.delete(hiddenQueues3).where(
|
|
8437
|
+
await this.db.delete(hiddenQueues3).where(and5(eq5(hiddenQueues3.connectionId, connectionId), eq5(hiddenQueues3.queueName, queueName)));
|
|
8093
8438
|
return this.listHiddenQueues(connectionId);
|
|
8094
8439
|
}
|
|
8095
8440
|
async isQueueHidden(connectionId, queueName) {
|
|
8096
|
-
const rows = await this.db.select({ queueName: hiddenQueues3.queueName }).from(hiddenQueues3).where(
|
|
8441
|
+
const rows = await this.db.select({ queueName: hiddenQueues3.queueName }).from(hiddenQueues3).where(and5(eq5(hiddenQueues3.connectionId, connectionId), eq5(hiddenQueues3.queueName, queueName))).limit(1);
|
|
8097
8442
|
return rows.length > 0;
|
|
8098
8443
|
}
|
|
8099
8444
|
/**
|
|
@@ -8193,7 +8538,7 @@ var ConnectionsService = class {
|
|
|
8193
8538
|
}
|
|
8194
8539
|
/** Used by seeding: does a connection with this name exist? */
|
|
8195
8540
|
async findByName(name) {
|
|
8196
|
-
const rows = await this.db.select().from(connections3).where(
|
|
8541
|
+
const rows = await this.db.select().from(connections3).where(and5(eq5(connections3.name, name))).limit(1);
|
|
8197
8542
|
return rows[0] ?? null;
|
|
8198
8543
|
}
|
|
8199
8544
|
};
|
|
@@ -8731,13 +9076,13 @@ var AttentionService = class {
|
|
|
8731
9076
|
};
|
|
8732
9077
|
|
|
8733
9078
|
// apps/server/src/services/settings-store.ts
|
|
8734
|
-
import { eq as
|
|
9079
|
+
import { eq as eq6 } from "drizzle-orm";
|
|
8735
9080
|
var DrizzleSettingsStore = class {
|
|
8736
9081
|
constructor(db) {
|
|
8737
9082
|
this.db = db;
|
|
8738
9083
|
}
|
|
8739
9084
|
async get(key) {
|
|
8740
|
-
const rows = await this.db.select().from(settings3).where(
|
|
9085
|
+
const rows = await this.db.select().from(settings3).where(eq6(settings3.key, key)).limit(1);
|
|
8741
9086
|
const value = rows[0]?.value;
|
|
8742
9087
|
return value === void 0 || value === "" ? null : value;
|
|
8743
9088
|
}
|
|
@@ -8755,148 +9100,512 @@ var DrizzleSettingsStore = class {
|
|
|
8755
9100
|
await this.db.insert(settings3).values({ key, value }).onDuplicateKeyUpdate({ set: { value } });
|
|
8756
9101
|
}
|
|
8757
9102
|
async delete(key) {
|
|
8758
|
-
await this.db.delete(settings3).where(
|
|
9103
|
+
await this.db.delete(settings3).where(eq6(settings3.key, key));
|
|
8759
9104
|
}
|
|
8760
9105
|
};
|
|
8761
9106
|
|
|
8762
|
-
// apps/server/src/ee/services/
|
|
8763
|
-
import { and as
|
|
8764
|
-
import { nanoid as
|
|
8765
|
-
var
|
|
8766
|
-
var
|
|
8767
|
-
|
|
8768
|
-
|
|
9107
|
+
// apps/server/src/ee/services/flowMaps.ts
|
|
9108
|
+
import { and as and6, eq as eq7, inArray as inArray2, isNull as isNull2, or as or3 } from "drizzle-orm";
|
|
9109
|
+
import { nanoid as nanoid6 } from "nanoid";
|
|
9110
|
+
var DETECTED_MAP_PREFIX = "detected:";
|
|
9111
|
+
var DETECTED_WAIT_MS = 3e3;
|
|
9112
|
+
var NAME_MAX = 80;
|
|
9113
|
+
var detectedMapReadOnly = () => new HttpError(409, "detected_map_read_only", "Detected maps are computed from BullMQ flows and cannot be edited. Copy it to edit it.");
|
|
9114
|
+
function isDetectedMapId(id3) {
|
|
9115
|
+
return id3.startsWith(DETECTED_MAP_PREFIX);
|
|
9116
|
+
}
|
|
9117
|
+
function detectedMapId(connectionId, rootQueue) {
|
|
9118
|
+
return `${DETECTED_MAP_PREFIX}${connectionId}:${rootQueue}`;
|
|
9119
|
+
}
|
|
9120
|
+
function parseNodeId(nodeId) {
|
|
9121
|
+
const i = nodeId.indexOf(":");
|
|
9122
|
+
if (i <= 0 || i === nodeId.length - 1) return null;
|
|
9123
|
+
return { connectionId: nodeId.slice(0, i), queueName: nodeId.slice(i + 1) };
|
|
9124
|
+
}
|
|
9125
|
+
function detectedComponents(edges) {
|
|
9126
|
+
const parent = /* @__PURE__ */ new Map();
|
|
9127
|
+
const find = (q) => {
|
|
9128
|
+
let r = q;
|
|
9129
|
+
while (parent.get(r) !== r) r = parent.get(r);
|
|
9130
|
+
let c = q;
|
|
9131
|
+
while (c !== r) {
|
|
9132
|
+
const next = parent.get(c);
|
|
9133
|
+
parent.set(c, r);
|
|
9134
|
+
c = next;
|
|
9135
|
+
}
|
|
9136
|
+
return r;
|
|
9137
|
+
};
|
|
9138
|
+
for (const e of edges) {
|
|
9139
|
+
for (const q of [e.from, e.to]) if (!parent.has(q)) parent.set(q, q);
|
|
9140
|
+
const a = find(e.from);
|
|
9141
|
+
const b = find(e.to);
|
|
9142
|
+
if (a !== b) parent.set(a, b);
|
|
9143
|
+
}
|
|
9144
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9145
|
+
for (const q of parent.keys()) {
|
|
9146
|
+
const r = find(q);
|
|
9147
|
+
const g = groups.get(r) ?? { queues: [], edges: [] };
|
|
9148
|
+
g.queues.push(q);
|
|
9149
|
+
groups.set(r, g);
|
|
9150
|
+
}
|
|
9151
|
+
for (const e of edges) groups.get(find(e.from))?.edges.push(e);
|
|
9152
|
+
const out = [];
|
|
9153
|
+
for (const g of groups.values()) {
|
|
9154
|
+
if (g.queues.length < 2) continue;
|
|
9155
|
+
const outgoing = new Set(g.edges.map((e) => e.from));
|
|
9156
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
9157
|
+
for (const e of g.edges) incoming.set(e.to, (incoming.get(e.to) ?? 0) + e.evidence);
|
|
9158
|
+
const sinks = g.queues.filter((q) => !outgoing.has(q));
|
|
9159
|
+
const candidates = sinks.length > 0 ? sinks : g.queues;
|
|
9160
|
+
candidates.sort((a, b) => (incoming.get(b) ?? 0) - (incoming.get(a) ?? 0) || a.localeCompare(b));
|
|
9161
|
+
out.push({ root: candidates[0], queues: g.queues.sort((a, b) => a.localeCompare(b)), edges: g.edges });
|
|
9162
|
+
}
|
|
9163
|
+
return out.sort((a, b) => a.root.localeCompare(b.root));
|
|
9164
|
+
}
|
|
9165
|
+
function within(promise, ms) {
|
|
9166
|
+
let timer;
|
|
9167
|
+
const timeout = new Promise((_, reject) => {
|
|
9168
|
+
timer = setTimeout(() => reject(new Error("still sampling")), ms);
|
|
9169
|
+
});
|
|
9170
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
8769
9171
|
}
|
|
8770
|
-
function
|
|
8771
|
-
|
|
9172
|
+
function toDetectedMapEdge(connectionId, e) {
|
|
9173
|
+
const from = flowMapNodeId({ connectionId, queueName: e.to });
|
|
9174
|
+
const to = flowMapNodeId({ connectionId, queueName: e.from });
|
|
9175
|
+
return { id: detectedEdgeId(from, to), from, to, source: "detected", label: null, evidence: e.evidence };
|
|
8772
9176
|
}
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
}
|
|
8782
|
-
await Promise.all(workers);
|
|
8783
|
-
return results;
|
|
9177
|
+
function toManualMapEdge(row) {
|
|
9178
|
+
return {
|
|
9179
|
+
id: row.id,
|
|
9180
|
+
from: flowMapNodeId({ connectionId: row.fromConnectionId, queueName: row.fromQueue }),
|
|
9181
|
+
to: flowMapNodeId({ connectionId: row.toConnectionId, queueName: row.toQueue }),
|
|
9182
|
+
source: "manual",
|
|
9183
|
+
label: row.label ?? null,
|
|
9184
|
+
evidence: 0
|
|
9185
|
+
};
|
|
8784
9186
|
}
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
9187
|
+
function manualSummary(row) {
|
|
9188
|
+
return {
|
|
9189
|
+
id: row.id,
|
|
9190
|
+
kind: "manual",
|
|
9191
|
+
name: row.name,
|
|
9192
|
+
description: row.description ?? null,
|
|
9193
|
+
parentId: row.parentId ?? null,
|
|
9194
|
+
position: row.position,
|
|
9195
|
+
connectionId: null
|
|
9196
|
+
};
|
|
9197
|
+
}
|
|
9198
|
+
function sameRef(a, b) {
|
|
9199
|
+
return a.connectionId === b.connectionId && a.queueName === b.queueName;
|
|
9200
|
+
}
|
|
9201
|
+
var FlowMapsService = class {
|
|
9202
|
+
constructor(db, connections5, flows) {
|
|
9203
|
+
this.db = db;
|
|
9204
|
+
this.connections = connections5;
|
|
9205
|
+
this.flows = flows;
|
|
9206
|
+
}
|
|
9207
|
+
// ---------------------------------------------------------------------------
|
|
9208
|
+
// reads
|
|
9209
|
+
// ---------------------------------------------------------------------------
|
|
9210
|
+
async list() {
|
|
9211
|
+
const rows = await this.db.select().from(flowMaps3);
|
|
9212
|
+
rows.sort(
|
|
9213
|
+
(a, b) => (a.parentId ?? "").localeCompare(b.parentId ?? "") || a.position - b.position || a.name.localeCompare(b.name) || a.id.localeCompare(b.id)
|
|
9214
|
+
);
|
|
9215
|
+
const nodeCount = /* @__PURE__ */ new Map();
|
|
9216
|
+
const edgeCount = /* @__PURE__ */ new Map();
|
|
9217
|
+
if (rows.length > 0) {
|
|
9218
|
+
const ids = rows.map((r) => r.id);
|
|
9219
|
+
for (const n of await this.db.select({ mapId: flowMapNodes3.mapId }).from(flowMapNodes3).where(inArray2(flowMapNodes3.mapId, ids))) {
|
|
9220
|
+
nodeCount.set(n.mapId, (nodeCount.get(n.mapId) ?? 0) + 1);
|
|
9221
|
+
}
|
|
9222
|
+
for (const e of await this.db.select({ mapId: flowMapEdges3.mapId }).from(flowMapEdges3).where(inArray2(flowMapEdges3.mapId, ids))) {
|
|
9223
|
+
edgeCount.set(e.mapId, (edgeCount.get(e.mapId) ?? 0) + 1);
|
|
8802
9224
|
}
|
|
8803
9225
|
}
|
|
9226
|
+
const maps = rows.map((r) => ({ ...manualSummary(r), nodeCount: nodeCount.get(r.id) ?? 0, edgeCount: edgeCount.get(r.id) ?? 0 }));
|
|
9227
|
+
let detectedComplete = true;
|
|
9228
|
+
const perConnection = await Promise.all(
|
|
9229
|
+
(await this.connections.listRows()).map(async (row) => {
|
|
9230
|
+
try {
|
|
9231
|
+
return { row, components: detectedComponents(await within(this.flows.detectedEdges(row), DETECTED_WAIT_MS)) };
|
|
9232
|
+
} catch {
|
|
9233
|
+
detectedComplete = false;
|
|
9234
|
+
return { row, components: [] };
|
|
9235
|
+
}
|
|
9236
|
+
})
|
|
9237
|
+
);
|
|
9238
|
+
for (const { row, components } of perConnection) {
|
|
9239
|
+
components.forEach((c, position) => {
|
|
9240
|
+
maps.push({
|
|
9241
|
+
id: detectedMapId(row.id, c.root),
|
|
9242
|
+
kind: "detected",
|
|
9243
|
+
name: c.root,
|
|
9244
|
+
description: null,
|
|
9245
|
+
parentId: null,
|
|
9246
|
+
position,
|
|
9247
|
+
connectionId: row.id,
|
|
9248
|
+
nodeCount: c.queues.length,
|
|
9249
|
+
edgeCount: c.edges.length
|
|
9250
|
+
});
|
|
9251
|
+
});
|
|
9252
|
+
}
|
|
9253
|
+
return { maps, detectedComplete };
|
|
9254
|
+
}
|
|
9255
|
+
async get(id3) {
|
|
9256
|
+
return this.build(await this.source(id3));
|
|
9257
|
+
}
|
|
9258
|
+
/** What a map is made of (no Redis counts): the table rows, or the detected component. */
|
|
9259
|
+
async source(id3) {
|
|
9260
|
+
if (isDetectedMapId(id3)) return this.detectedSource(id3);
|
|
9261
|
+
const row = await this.getRow(id3);
|
|
9262
|
+
const nodes = await this.db.select().from(flowMapNodes3).where(eq7(flowMapNodes3.mapId, id3));
|
|
9263
|
+
const manualEdges = await this.db.select().from(flowMapEdges3).where(eq7(flowMapEdges3.mapId, id3));
|
|
9264
|
+
nodes.sort((a, b) => a.connectionId.localeCompare(b.connectionId) || a.queueName.localeCompare(b.queueName));
|
|
9265
|
+
manualEdges.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
8804
9266
|
return {
|
|
8805
|
-
|
|
8806
|
-
nodes,
|
|
8807
|
-
|
|
8808
|
-
sampledJobs: detected.sampledJobs
|
|
9267
|
+
summary: manualSummary(row),
|
|
9268
|
+
nodes: nodes.map((n) => ({ connectionId: n.connectionId, queueName: n.queueName, x: n.x ?? null, y: n.y ?? null })),
|
|
9269
|
+
manualEdges
|
|
8809
9270
|
};
|
|
8810
9271
|
}
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
9272
|
+
async detectedSource(id3) {
|
|
9273
|
+
const ref = parseNodeId(id3.slice(DETECTED_MAP_PREFIX.length));
|
|
9274
|
+
if (!ref) throw notFound("Flow map");
|
|
9275
|
+
const row = await this.connections.getRow(ref.connectionId);
|
|
9276
|
+
let edges;
|
|
9277
|
+
try {
|
|
9278
|
+
edges = await this.flows.detectedEdges(row);
|
|
9279
|
+
} catch {
|
|
9280
|
+
throw notFound("Flow map");
|
|
9281
|
+
}
|
|
9282
|
+
const components = detectedComponents(edges);
|
|
9283
|
+
const position = components.findIndex((c) => c.root === ref.queueName);
|
|
9284
|
+
const component = components[position];
|
|
9285
|
+
if (!component) throw notFound("Flow map");
|
|
9286
|
+
return {
|
|
9287
|
+
summary: { id: id3, kind: "detected", name: component.root, description: null, parentId: null, position, connectionId: row.id },
|
|
9288
|
+
nodes: component.queues.map((queueName) => ({ connectionId: row.id, queueName, x: null, y: null })),
|
|
9289
|
+
manualEdges: [],
|
|
9290
|
+
detectedEdges: component.edges
|
|
9291
|
+
};
|
|
8822
9292
|
}
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
9293
|
+
/**
|
|
9294
|
+
* Live counts + detected edges for a map's nodes: per connection, one
|
|
9295
|
+
* getQueueStats for only the queues on the map. Anything that cannot be read
|
|
9296
|
+
* (connection deleted or down, queue not discovered) is a `missing` node,
|
|
9297
|
+
* never an error: a map is a drawing first.
|
|
9298
|
+
*/
|
|
9299
|
+
async build(src) {
|
|
9300
|
+
const byConnection = /* @__PURE__ */ new Map();
|
|
9301
|
+
for (const n of src.nodes) {
|
|
9302
|
+
const list = byConnection.get(n.connectionId) ?? [];
|
|
9303
|
+
list.push(n);
|
|
9304
|
+
byConnection.set(n.connectionId, list);
|
|
9305
|
+
}
|
|
9306
|
+
const rows = new Map((await this.connections.listRows()).map((r) => [r.id, r]));
|
|
9307
|
+
const live = /* @__PURE__ */ new Map();
|
|
9308
|
+
const detectedEdges = [];
|
|
9309
|
+
await Promise.all(
|
|
9310
|
+
[...byConnection.entries()].map(async ([connectionId, nodes2]) => {
|
|
9311
|
+
const row = rows.get(connectionId);
|
|
9312
|
+
if (!row) return;
|
|
9313
|
+
await Promise.all([this.readCounts(row, nodes2, live), this.readDetected(row, src, detectedEdges)]);
|
|
9314
|
+
})
|
|
8837
9315
|
);
|
|
8838
|
-
const
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
to: e.parentQueue,
|
|
8852
|
-
source: "detected",
|
|
8853
|
-
evidence: e.count,
|
|
8854
|
-
label: null
|
|
8855
|
-
});
|
|
9316
|
+
const nodes = src.nodes.map((n) => {
|
|
9317
|
+
const id3 = flowMapNodeId(n);
|
|
9318
|
+
return {
|
|
9319
|
+
id: id3,
|
|
9320
|
+
connectionId: n.connectionId,
|
|
9321
|
+
queueName: n.queueName,
|
|
9322
|
+
x: n.x,
|
|
9323
|
+
y: n.y,
|
|
9324
|
+
...live.get(id3) ?? {
|
|
9325
|
+
connectionName: rows.get(n.connectionId)?.name ?? n.connectionId,
|
|
9326
|
+
counts: { ...EMPTY_COUNTS },
|
|
9327
|
+
isPaused: false,
|
|
9328
|
+
missing: true
|
|
8856
9329
|
}
|
|
9330
|
+
};
|
|
9331
|
+
});
|
|
9332
|
+
const onMap = new Set(nodes.map((n) => n.id));
|
|
9333
|
+
const manual = src.manualEdges.map(toManualMapEdge);
|
|
9334
|
+
const drawn = new Set(manual.map((e) => `${e.from}->${e.to}`));
|
|
9335
|
+
const edges = [
|
|
9336
|
+
...manual,
|
|
9337
|
+
...detectedEdges.filter((e) => onMap.has(e.from) && onMap.has(e.to) && !drawn.has(`${e.from}->${e.to}`))
|
|
9338
|
+
];
|
|
9339
|
+
return { ...src.summary, nodeCount: nodes.length, edgeCount: edges.length, nodes, edges };
|
|
9340
|
+
}
|
|
9341
|
+
async readCounts(row, nodes, live) {
|
|
9342
|
+
try {
|
|
9343
|
+
const inspector = this.connections.inspectorFor(row);
|
|
9344
|
+
const discovered = new Set(await withRedis(() => inspector.discoverQueues()));
|
|
9345
|
+
const present = [...new Set(nodes.map((n) => n.queueName))].filter((q) => discovered.has(q));
|
|
9346
|
+
if (present.length === 0) return;
|
|
9347
|
+
const stats = await withRedis(() => inspector.getQueueStats(present));
|
|
9348
|
+
for (const q of present) {
|
|
9349
|
+
const s = stats[q];
|
|
9350
|
+
if (!s) continue;
|
|
9351
|
+
live.set(flowMapNodeId({ connectionId: row.id, queueName: q }), {
|
|
9352
|
+
connectionName: row.name,
|
|
9353
|
+
counts: s.counts,
|
|
9354
|
+
isPaused: s.isPaused,
|
|
9355
|
+
missing: false
|
|
9356
|
+
});
|
|
8857
9357
|
}
|
|
9358
|
+
} catch {
|
|
8858
9359
|
}
|
|
8859
|
-
return { nodes, edges: [...edgeMap.values()], sampledJobs };
|
|
8860
9360
|
}
|
|
8861
|
-
async
|
|
8862
|
-
|
|
8863
|
-
|
|
9361
|
+
async readDetected(row, src, out) {
|
|
9362
|
+
if (src.detectedEdges) {
|
|
9363
|
+
if (src.summary.connectionId === row.id) out.push(...src.detectedEdges.map((e) => toDetectedMapEdge(row.id, e)));
|
|
9364
|
+
return;
|
|
9365
|
+
}
|
|
9366
|
+
try {
|
|
9367
|
+
const edges = await within(this.flows.detectedEdges(row), DETECTED_WAIT_MS);
|
|
9368
|
+
out.push(...edges.map((e) => toDetectedMapEdge(row.id, e)));
|
|
9369
|
+
} catch {
|
|
9370
|
+
}
|
|
8864
9371
|
}
|
|
8865
|
-
|
|
9372
|
+
// ---------------------------------------------------------------------------
|
|
9373
|
+
// writes (manual maps only, except copy)
|
|
9374
|
+
// ---------------------------------------------------------------------------
|
|
9375
|
+
async getRow(id3) {
|
|
9376
|
+
const rows = await this.db.select().from(flowMaps3).where(eq7(flowMaps3.id, id3)).limit(1);
|
|
9377
|
+
const row = rows[0];
|
|
9378
|
+
if (!row) throw notFound("Flow map");
|
|
9379
|
+
return row;
|
|
9380
|
+
}
|
|
9381
|
+
/** The row of a map a write may touch: 409 for a detected id, 404 for an unknown one. */
|
|
9382
|
+
async writable(id3) {
|
|
9383
|
+
if (isDetectedMapId(id3)) throw detectedMapReadOnly();
|
|
9384
|
+
return this.getRow(id3);
|
|
9385
|
+
}
|
|
9386
|
+
async touch(id3) {
|
|
9387
|
+
await this.db.update(flowMaps3).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq7(flowMaps3.id, id3));
|
|
9388
|
+
}
|
|
9389
|
+
/** New maps go to the bottom of their level. */
|
|
9390
|
+
async nextPosition(parentId) {
|
|
9391
|
+
const siblings = await this.db.select({ id: flowMaps3.id }).from(flowMaps3).where(parentId ? eq7(flowMaps3.parentId, parentId) : isNull2(flowMaps3.parentId));
|
|
9392
|
+
return siblings.length;
|
|
9393
|
+
}
|
|
9394
|
+
async insertMap(input) {
|
|
9395
|
+
const parentId = input.parentId ?? null;
|
|
9396
|
+
if (parentId) await this.getRow(parentId);
|
|
9397
|
+
const id3 = nanoid6();
|
|
9398
|
+
const now = /* @__PURE__ */ new Date();
|
|
9399
|
+
await this.db.insert(flowMaps3).values({
|
|
9400
|
+
id: id3,
|
|
9401
|
+
name: input.name,
|
|
9402
|
+
description: input.description ?? null,
|
|
9403
|
+
parentId,
|
|
9404
|
+
position: await this.nextPosition(parentId),
|
|
9405
|
+
createdAt: now,
|
|
9406
|
+
updatedAt: now
|
|
9407
|
+
});
|
|
9408
|
+
return id3;
|
|
9409
|
+
}
|
|
9410
|
+
async create(input) {
|
|
9411
|
+
return this.get(await this.insertMap(input));
|
|
9412
|
+
}
|
|
9413
|
+
async update(id3, input) {
|
|
9414
|
+
await this.writable(id3);
|
|
9415
|
+
if (input.parentId) await this.assertNoCycle(id3, input.parentId);
|
|
9416
|
+
const patch = { updatedAt: /* @__PURE__ */ new Date() };
|
|
9417
|
+
if (input.name !== void 0) patch.name = input.name;
|
|
9418
|
+
if (input.description !== void 0) patch.description = input.description;
|
|
9419
|
+
if (input.parentId !== void 0) patch.parentId = input.parentId;
|
|
9420
|
+
if (input.position !== void 0) patch.position = input.position;
|
|
9421
|
+
await this.db.update(flowMaps3).set(patch).where(eq7(flowMaps3.id, id3));
|
|
9422
|
+
return this.get(id3);
|
|
9423
|
+
}
|
|
9424
|
+
/** Moving `id` under `parentId` must not put it under itself or one of its descendants. */
|
|
9425
|
+
async assertNoCycle(id3, parentId) {
|
|
9426
|
+
if (isDetectedMapId(parentId)) throw notFound("Flow map");
|
|
9427
|
+
const all = await this.db.select({ id: flowMaps3.id, parentId: flowMaps3.parentId }).from(flowMaps3);
|
|
9428
|
+
const parentOf = new Map(all.map((r) => [r.id, r.parentId ?? null]));
|
|
9429
|
+
if (!parentOf.has(parentId)) throw notFound("Flow map");
|
|
9430
|
+
let cursor = parentId;
|
|
9431
|
+
for (let steps = 0; cursor !== null && steps <= all.length; steps++) {
|
|
9432
|
+
if (cursor === id3) throw conflict("A flow map cannot be moved under itself or one of its own maps");
|
|
9433
|
+
cursor = parentOf.get(cursor) ?? null;
|
|
9434
|
+
}
|
|
9435
|
+
}
|
|
9436
|
+
async remove(id3) {
|
|
9437
|
+
const row = await this.writable(id3);
|
|
9438
|
+
await this.db.transaction(async (tx) => {
|
|
9439
|
+
await tx.update(flowMaps3).set({ parentId: row.parentId ?? null, updatedAt: /* @__PURE__ */ new Date() }).where(eq7(flowMaps3.parentId, id3));
|
|
9440
|
+
await tx.delete(flowMapEdges3).where(eq7(flowMapEdges3.mapId, id3));
|
|
9441
|
+
await tx.delete(flowMapNodes3).where(eq7(flowMapNodes3.mapId, id3));
|
|
9442
|
+
await tx.delete(flowMaps3).where(eq7(flowMaps3.id, id3));
|
|
9443
|
+
});
|
|
9444
|
+
}
|
|
9445
|
+
async findNode(mapId, ref) {
|
|
9446
|
+
const rows = await this.db.select().from(flowMapNodes3).where(and6(eq7(flowMapNodes3.mapId, mapId), eq7(flowMapNodes3.connectionId, ref.connectionId), eq7(flowMapNodes3.queueName, ref.queueName))).limit(1);
|
|
9447
|
+
return rows[0] ?? null;
|
|
9448
|
+
}
|
|
9449
|
+
/** Puts a queue on the map if it is not there yet. Returns true when it was added. */
|
|
9450
|
+
async ensureNode(mapId, ref, pos = {}) {
|
|
9451
|
+
if (await this.findNode(mapId, ref)) return false;
|
|
9452
|
+
await this.db.insert(flowMapNodes3).values({
|
|
9453
|
+
mapId,
|
|
9454
|
+
connectionId: ref.connectionId,
|
|
9455
|
+
queueName: ref.queueName,
|
|
9456
|
+
x: pos.x ?? null,
|
|
9457
|
+
y: pos.y ?? null
|
|
9458
|
+
});
|
|
9459
|
+
return true;
|
|
9460
|
+
}
|
|
9461
|
+
async addNode(id3, input) {
|
|
9462
|
+
await this.writable(id3);
|
|
8866
9463
|
await this.connections.getRow(input.connectionId);
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
9464
|
+
const added = await this.ensureNode(id3, input, input);
|
|
9465
|
+
if (!added && (input.x !== void 0 || input.y !== void 0)) {
|
|
9466
|
+
const patch = {};
|
|
9467
|
+
if (input.x !== void 0) patch.x = input.x;
|
|
9468
|
+
if (input.y !== void 0) patch.y = input.y;
|
|
9469
|
+
await this.db.update(flowMapNodes3).set(patch).where(and6(eq7(flowMapNodes3.mapId, id3), eq7(flowMapNodes3.connectionId, input.connectionId), eq7(flowMapNodes3.queueName, input.queueName)));
|
|
9470
|
+
}
|
|
9471
|
+
await this.touch(id3);
|
|
9472
|
+
return this.get(id3);
|
|
9473
|
+
}
|
|
9474
|
+
async removeNode(id3, nodeId) {
|
|
9475
|
+
await this.writable(id3);
|
|
9476
|
+
const ref = parseNodeId(nodeId);
|
|
9477
|
+
if (!ref || !await this.findNode(id3, ref)) throw notFound("Queue on this flow map");
|
|
9478
|
+
await this.db.transaction(async (tx) => {
|
|
9479
|
+
await tx.delete(flowMapEdges3).where(
|
|
9480
|
+
and6(
|
|
9481
|
+
eq7(flowMapEdges3.mapId, id3),
|
|
9482
|
+
or3(
|
|
9483
|
+
and6(eq7(flowMapEdges3.fromConnectionId, ref.connectionId), eq7(flowMapEdges3.fromQueue, ref.queueName)),
|
|
9484
|
+
and6(eq7(flowMapEdges3.toConnectionId, ref.connectionId), eq7(flowMapEdges3.toQueue, ref.queueName))
|
|
9485
|
+
)
|
|
9486
|
+
)
|
|
9487
|
+
);
|
|
9488
|
+
await tx.delete(flowMapNodes3).where(and6(eq7(flowMapNodes3.mapId, id3), eq7(flowMapNodes3.connectionId, ref.connectionId), eq7(flowMapNodes3.queueName, ref.queueName)));
|
|
9489
|
+
});
|
|
9490
|
+
await this.touch(id3);
|
|
9491
|
+
return this.get(id3);
|
|
9492
|
+
}
|
|
9493
|
+
async saveLayout(id3, input) {
|
|
9494
|
+
await this.writable(id3);
|
|
9495
|
+
const known = new Set(
|
|
9496
|
+
(await this.db.select({ connectionId: flowMapNodes3.connectionId, queueName: flowMapNodes3.queueName }).from(flowMapNodes3).where(eq7(flowMapNodes3.mapId, id3))).map(
|
|
9497
|
+
(n) => flowMapNodeId(n)
|
|
8873
9498
|
)
|
|
8874
|
-
)
|
|
8875
|
-
|
|
8876
|
-
const
|
|
8877
|
-
await this.db.
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
9499
|
+
);
|
|
9500
|
+
const positions = /* @__PURE__ */ new Map();
|
|
9501
|
+
for (const p of input.positions) if (known.has(p.nodeId)) positions.set(p.nodeId, { x: p.x, y: p.y });
|
|
9502
|
+
await this.db.transaction(async (tx) => {
|
|
9503
|
+
for (const [nodeId, pos] of positions) {
|
|
9504
|
+
const ref = parseNodeId(nodeId);
|
|
9505
|
+
await tx.update(flowMapNodes3).set(pos).where(and6(eq7(flowMapNodes3.mapId, id3), eq7(flowMapNodes3.connectionId, ref.connectionId), eq7(flowMapNodes3.queueName, ref.queueName)));
|
|
9506
|
+
}
|
|
9507
|
+
await tx.update(flowMaps3).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq7(flowMaps3.id, id3));
|
|
8884
9508
|
});
|
|
8885
|
-
|
|
9509
|
+
}
|
|
9510
|
+
async findEdge(mapId, from, to) {
|
|
9511
|
+
const rows = await this.db.select().from(flowMapEdges3).where(
|
|
9512
|
+
and6(
|
|
9513
|
+
eq7(flowMapEdges3.mapId, mapId),
|
|
9514
|
+
eq7(flowMapEdges3.fromConnectionId, from.connectionId),
|
|
9515
|
+
eq7(flowMapEdges3.fromQueue, from.queueName),
|
|
9516
|
+
eq7(flowMapEdges3.toConnectionId, to.connectionId),
|
|
9517
|
+
eq7(flowMapEdges3.toQueue, to.queueName)
|
|
9518
|
+
)
|
|
9519
|
+
).limit(1);
|
|
9520
|
+
return rows[0] ?? null;
|
|
9521
|
+
}
|
|
9522
|
+
/**
|
|
9523
|
+
* Idempotent on the full (from, to) refs, connection included: the same queue
|
|
9524
|
+
* name on two connections is two different nodes, and an edge may go from one
|
|
9525
|
+
* connection to another (Redis → Postgres included).
|
|
9526
|
+
*/
|
|
9527
|
+
async addEdge(id3, input) {
|
|
9528
|
+
await this.writable(id3);
|
|
9529
|
+
if (sameRef(input.from, input.to)) throw conflict("A queue cannot flow into itself");
|
|
9530
|
+
await this.connections.getRow(input.from.connectionId);
|
|
9531
|
+
if (input.to.connectionId !== input.from.connectionId) await this.connections.getRow(input.to.connectionId);
|
|
9532
|
+
await this.ensureNode(id3, input.from);
|
|
9533
|
+
await this.ensureNode(id3, input.to);
|
|
9534
|
+
const existing = await this.findEdge(id3, input.from, input.to);
|
|
9535
|
+
if (existing) {
|
|
9536
|
+
if (input.label !== void 0) await this.db.update(flowMapEdges3).set({ label: input.label }).where(eq7(flowMapEdges3.id, existing.id));
|
|
9537
|
+
} else {
|
|
9538
|
+
await this.db.insert(flowMapEdges3).values({
|
|
9539
|
+
id: nanoid6(),
|
|
9540
|
+
mapId: id3,
|
|
9541
|
+
fromConnectionId: input.from.connectionId,
|
|
9542
|
+
fromQueue: input.from.queueName,
|
|
9543
|
+
toConnectionId: input.to.connectionId,
|
|
9544
|
+
toQueue: input.to.queueName,
|
|
9545
|
+
label: input.label ?? null,
|
|
9546
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
9547
|
+
});
|
|
9548
|
+
}
|
|
9549
|
+
await this.touch(id3);
|
|
9550
|
+
return this.get(id3);
|
|
9551
|
+
}
|
|
9552
|
+
async edgeOf(mapId, edgeId) {
|
|
9553
|
+
const rows = await this.db.select().from(flowMapEdges3).where(and6(eq7(flowMapEdges3.mapId, mapId), eq7(flowMapEdges3.id, edgeId))).limit(1);
|
|
8886
9554
|
const row = rows[0];
|
|
8887
|
-
if (!row) throw notFound("Flow edge");
|
|
8888
|
-
return
|
|
9555
|
+
if (!row) throw notFound("Flow map edge");
|
|
9556
|
+
return row;
|
|
8889
9557
|
}
|
|
8890
|
-
async
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
await this.db.
|
|
9558
|
+
async updateEdge(id3, edgeId, input) {
|
|
9559
|
+
await this.writable(id3);
|
|
9560
|
+
await this.edgeOf(id3, edgeId);
|
|
9561
|
+
await this.db.update(flowMapEdges3).set({ label: input.label }).where(eq7(flowMapEdges3.id, edgeId));
|
|
9562
|
+
await this.touch(id3);
|
|
9563
|
+
return this.get(id3);
|
|
9564
|
+
}
|
|
9565
|
+
async removeEdge(id3, edgeId) {
|
|
9566
|
+
await this.writable(id3);
|
|
9567
|
+
await this.edgeOf(id3, edgeId);
|
|
9568
|
+
await this.db.delete(flowMapEdges3).where(eq7(flowMapEdges3.id, edgeId));
|
|
9569
|
+
await this.touch(id3);
|
|
9570
|
+
return this.get(id3);
|
|
9571
|
+
}
|
|
9572
|
+
/**
|
|
9573
|
+
* A new manual map with the same queues and positions. Works on detected maps
|
|
9574
|
+
* too (that is how one becomes editable). A manual map's drawn edges come
|
|
9575
|
+
* along; detected edges are not turned into manual ones, they keep being drawn
|
|
9576
|
+
* from the live sample.
|
|
9577
|
+
*/
|
|
9578
|
+
async copy(id3, input) {
|
|
9579
|
+
const src = await this.source(id3);
|
|
9580
|
+
const name = input.name ?? `${src.summary.name} (copy)`.slice(0, NAME_MAX);
|
|
9581
|
+
const newId = await this.insertMap({ name, description: src.summary.description, parentId: input.parentId ?? null });
|
|
9582
|
+
await this.db.transaction(async (tx) => {
|
|
9583
|
+
if (src.nodes.length > 0) {
|
|
9584
|
+
await tx.insert(flowMapNodes3).values(src.nodes.map((n) => ({ mapId: newId, connectionId: n.connectionId, queueName: n.queueName, x: n.x, y: n.y })));
|
|
9585
|
+
}
|
|
9586
|
+
if (src.manualEdges.length > 0) {
|
|
9587
|
+
const now = /* @__PURE__ */ new Date();
|
|
9588
|
+
await tx.insert(flowMapEdges3).values(
|
|
9589
|
+
src.manualEdges.map((e) => ({
|
|
9590
|
+
id: nanoid6(),
|
|
9591
|
+
mapId: newId,
|
|
9592
|
+
fromConnectionId: e.fromConnectionId,
|
|
9593
|
+
fromQueue: e.fromQueue,
|
|
9594
|
+
toConnectionId: e.toConnectionId,
|
|
9595
|
+
toQueue: e.toQueue,
|
|
9596
|
+
label: e.label ?? null,
|
|
9597
|
+
createdAt: now
|
|
9598
|
+
}))
|
|
9599
|
+
);
|
|
9600
|
+
}
|
|
9601
|
+
});
|
|
9602
|
+
return this.get(newId);
|
|
8894
9603
|
}
|
|
8895
9604
|
};
|
|
8896
9605
|
|
|
8897
9606
|
// apps/server/src/ee/services/folders.ts
|
|
8898
|
-
import { eq as
|
|
8899
|
-
import { nanoid as
|
|
9607
|
+
import { eq as eq8, inArray as inArray3 } from "drizzle-orm";
|
|
9608
|
+
import { nanoid as nanoid7 } from "nanoid";
|
|
8900
9609
|
var FoldersService = class {
|
|
8901
9610
|
constructor(db) {
|
|
8902
9611
|
this.db = db;
|
|
@@ -8908,7 +9617,7 @@ var FoldersService = class {
|
|
|
8908
9617
|
const rows = await this.db.select().from(folders3).orderBy(folders3.position, folders3.name);
|
|
8909
9618
|
if (rows.length === 0) return [];
|
|
8910
9619
|
const refs = await this.db.select().from(folderQueues3).where(
|
|
8911
|
-
|
|
9620
|
+
inArray3(
|
|
8912
9621
|
folderQueues3.folderId,
|
|
8913
9622
|
rows.map((r) => r.id)
|
|
8914
9623
|
)
|
|
@@ -8922,10 +9631,10 @@ var FoldersService = class {
|
|
|
8922
9631
|
return rows.map((row) => toFolderDto(row, byFolder.get(row.id) ?? []));
|
|
8923
9632
|
}
|
|
8924
9633
|
async get(id3) {
|
|
8925
|
-
const rows = await this.db.select().from(folders3).where(
|
|
9634
|
+
const rows = await this.db.select().from(folders3).where(eq8(folders3.id, id3)).limit(1);
|
|
8926
9635
|
const row = rows[0];
|
|
8927
9636
|
if (!row) throw notFound("Folder");
|
|
8928
|
-
const refs = await this.db.select().from(folderQueues3).where(
|
|
9637
|
+
const refs = await this.db.select().from(folderQueues3).where(eq8(folderQueues3.folderId, id3));
|
|
8929
9638
|
return toFolderDto(
|
|
8930
9639
|
row,
|
|
8931
9640
|
refs.map((r) => ({ connectionId: r.connectionId, queueName: r.queueName }))
|
|
@@ -8933,7 +9642,7 @@ var FoldersService = class {
|
|
|
8933
9642
|
}
|
|
8934
9643
|
async create(input) {
|
|
8935
9644
|
if (input.parentId) await this.get(input.parentId);
|
|
8936
|
-
const id3 =
|
|
9645
|
+
const id3 = nanoid7();
|
|
8937
9646
|
const siblings = await this.db.select({ id: folders3.id }).from(folders3);
|
|
8938
9647
|
await this.db.insert(folders3).values({
|
|
8939
9648
|
id: id3,
|
|
@@ -8956,21 +9665,21 @@ var FoldersService = class {
|
|
|
8956
9665
|
if (input.parentId !== void 0) patch.parentId = input.parentId;
|
|
8957
9666
|
if (input.position !== void 0) patch.position = input.position;
|
|
8958
9667
|
if (Object.keys(patch).length > 0) {
|
|
8959
|
-
await this.db.update(folders3).set(patch).where(
|
|
9668
|
+
await this.db.update(folders3).set(patch).where(eq8(folders3.id, id3));
|
|
8960
9669
|
}
|
|
8961
9670
|
return this.get(id3);
|
|
8962
9671
|
}
|
|
8963
9672
|
async remove(id3) {
|
|
8964
9673
|
await this.get(id3);
|
|
8965
|
-
await this.db.update(folders3).set({ parentId: null }).where(
|
|
8966
|
-
await this.db.delete(folderQueues3).where(
|
|
8967
|
-
await this.db.delete(folders3).where(
|
|
9674
|
+
await this.db.update(folders3).set({ parentId: null }).where(eq8(folders3.parentId, id3));
|
|
9675
|
+
await this.db.delete(folderQueues3).where(eq8(folderQueues3.folderId, id3));
|
|
9676
|
+
await this.db.delete(folders3).where(eq8(folders3.id, id3));
|
|
8968
9677
|
}
|
|
8969
9678
|
async setQueues(id3, queues) {
|
|
8970
9679
|
await this.get(id3);
|
|
8971
9680
|
const unique = /* @__PURE__ */ new Map();
|
|
8972
9681
|
for (const q of queues) unique.set(`${q.connectionId}|${q.queueName}`, q);
|
|
8973
|
-
await this.db.delete(folderQueues3).where(
|
|
9682
|
+
await this.db.delete(folderQueues3).where(eq8(folderQueues3.folderId, id3));
|
|
8974
9683
|
if (unique.size > 0) {
|
|
8975
9684
|
await this.db.insert(folderQueues3).values([...unique.values()].map((q) => ({ folderId: id3, connectionId: q.connectionId, queueName: q.queueName })));
|
|
8976
9685
|
}
|
|
@@ -9184,8 +9893,8 @@ function buildPostgresWarnings(info, error) {
|
|
|
9184
9893
|
}
|
|
9185
9894
|
|
|
9186
9895
|
// apps/server/src/ee/services/sso.ts
|
|
9187
|
-
import { asc as asc2, eq as
|
|
9188
|
-
import { nanoid as
|
|
9896
|
+
import { asc as asc2, eq as eq9 } from "drizzle-orm";
|
|
9897
|
+
import { nanoid as nanoid8 } from "nanoid";
|
|
9189
9898
|
var REQUIRE_SSO_KEY = "sso.require_sso";
|
|
9190
9899
|
var AUTO_PROVISION_KEY = "sso.auto_provision";
|
|
9191
9900
|
var AUTO_PROVISION_DOMAINS_KEY = "sso.auto_provision_domains";
|
|
@@ -9227,7 +9936,7 @@ var SsoService = class {
|
|
|
9227
9936
|
return rows.map((r) => this.toDto(r));
|
|
9228
9937
|
}
|
|
9229
9938
|
async getRow(id3) {
|
|
9230
|
-
const rows = await this.db.select().from(ssoProviders3).where(
|
|
9939
|
+
const rows = await this.db.select().from(ssoProviders3).where(eq9(ssoProviders3.id, id3)).limit(1);
|
|
9231
9940
|
const row = rows[0];
|
|
9232
9941
|
if (!row) throw notFound("SSO provider");
|
|
9233
9942
|
return row;
|
|
@@ -9236,7 +9945,7 @@ var SsoService = class {
|
|
|
9236
9945
|
return this.toDto(await this.getRow(id3));
|
|
9237
9946
|
}
|
|
9238
9947
|
async create(input) {
|
|
9239
|
-
const id3 =
|
|
9948
|
+
const id3 = nanoid8();
|
|
9240
9949
|
const now = /* @__PURE__ */ new Date();
|
|
9241
9950
|
const { config, secretEnc } = this.splitSecret(input.kind, input.config);
|
|
9242
9951
|
await this.db.insert(ssoProviders3).values({
|
|
@@ -9265,19 +9974,19 @@ var SsoService = class {
|
|
|
9265
9974
|
}
|
|
9266
9975
|
if (Object.keys(patch).length > 0) {
|
|
9267
9976
|
patch.updatedAt = /* @__PURE__ */ new Date();
|
|
9268
|
-
await this.db.update(ssoProviders3).set(patch).where(
|
|
9977
|
+
await this.db.update(ssoProviders3).set(patch).where(eq9(ssoProviders3.id, id3));
|
|
9269
9978
|
}
|
|
9270
9979
|
return this.get(id3);
|
|
9271
9980
|
}
|
|
9272
9981
|
async remove(id3) {
|
|
9273
9982
|
const row = await this.getRow(id3);
|
|
9274
9983
|
if (row.enabled && await this.requireSso()) {
|
|
9275
|
-
const others = (await this.db.select().from(ssoProviders3).where(
|
|
9984
|
+
const others = (await this.db.select().from(ssoProviders3).where(eq9(ssoProviders3.enabled, true))).filter((r) => r.id !== id3);
|
|
9276
9985
|
if (others.length === 0) {
|
|
9277
9986
|
throw conflict('This is the only enabled SSO provider and "require SSO" is on. Turn that off before deleting it.');
|
|
9278
9987
|
}
|
|
9279
9988
|
}
|
|
9280
|
-
await this.db.delete(ssoProviders3).where(
|
|
9989
|
+
await this.db.delete(ssoProviders3).where(eq9(ssoProviders3.id, id3));
|
|
9281
9990
|
}
|
|
9282
9991
|
/**
|
|
9283
9992
|
* Pulls the write-only secret out of a config object, encrypts it, and
|
|
@@ -9312,7 +10021,7 @@ var SsoService = class {
|
|
|
9312
10021
|
}
|
|
9313
10022
|
async setRequireSso(value) {
|
|
9314
10023
|
if (value) {
|
|
9315
|
-
const enabled = await this.db.select().from(ssoProviders3).where(
|
|
10024
|
+
const enabled = await this.db.select().from(ssoProviders3).where(eq9(ssoProviders3.enabled, true));
|
|
9316
10025
|
if (enabled.length === 0) {
|
|
9317
10026
|
throw conflict("Add and enable an SSO provider before requiring SSO, or nobody will be able to sign in.");
|
|
9318
10027
|
}
|
|
@@ -9375,7 +10084,7 @@ var SsoService = class {
|
|
|
9375
10084
|
* IdP: the button label is enough to start the flow.
|
|
9376
10085
|
*/
|
|
9377
10086
|
async loginOptions() {
|
|
9378
|
-
const rows = await this.db.select().from(ssoProviders3).where(
|
|
10087
|
+
const rows = await this.db.select().from(ssoProviders3).where(eq9(ssoProviders3.enabled, true)).orderBy(asc2(ssoProviders3.createdAt));
|
|
9379
10088
|
const providers = rows.map((r) => ({ id: r.id, kind: r.kind, name: r.name }));
|
|
9380
10089
|
const requireSso = providers.length > 0 && await this.requireSso();
|
|
9381
10090
|
return {
|
|
@@ -9393,7 +10102,7 @@ var SsoService = class {
|
|
|
9393
10102
|
async resolveUser(email) {
|
|
9394
10103
|
const normalised = email.trim().toLowerCase();
|
|
9395
10104
|
if (!normalised) return null;
|
|
9396
|
-
const rows = await this.db.select().from(users3).where(
|
|
10105
|
+
const rows = await this.db.select().from(users3).where(eq9(users3.email, normalised)).limit(1);
|
|
9397
10106
|
const row = rows[0];
|
|
9398
10107
|
return row ? toUserDto(row) : null;
|
|
9399
10108
|
}
|
|
@@ -9422,6 +10131,9 @@ function textResult(value, isError = false) {
|
|
|
9422
10131
|
}
|
|
9423
10132
|
async function forward(ctx, req) {
|
|
9424
10133
|
const { status, body } = await ctx.call(req);
|
|
10134
|
+
return apiResult(status, body);
|
|
10135
|
+
}
|
|
10136
|
+
function apiResult(status, body) {
|
|
9425
10137
|
if (status >= 400) {
|
|
9426
10138
|
const b = body ?? {};
|
|
9427
10139
|
const hint = status === 403 ? " (MCP calls act with your role, capped to the access this connection was given: read = viewer, write = operator)" : "";
|
|
@@ -9444,6 +10156,23 @@ var zConn = z10.object({ connection_id: z10.string().min(1) });
|
|
|
9444
10156
|
var zQueue = zConn.extend({ queue: z10.string().min(1) });
|
|
9445
10157
|
var zJob = zQueue.extend({ job_id: z10.string().min(1) });
|
|
9446
10158
|
var zState = z10.enum(JOB_STATES);
|
|
10159
|
+
var zMap = z10.object({ map_id: z10.string().min(1) });
|
|
10160
|
+
var mapPath = (a) => `/api/flow-maps/${seg(a.map_id)}`;
|
|
10161
|
+
async function connectionOrDefault(ctx, given) {
|
|
10162
|
+
if (given) return { id: given };
|
|
10163
|
+
const res = await ctx.call({ method: "GET", url: "/api/connections" });
|
|
10164
|
+
if (res.status >= 400) return { error: apiResult(res.status, res.body) };
|
|
10165
|
+
const list = Array.isArray(res.body) ? res.body : [];
|
|
10166
|
+
if (list.length === 1) return { id: list[0].id };
|
|
10167
|
+
if (list.length === 0) return { error: textResult("validation: Bullpane has no connection yet; add one in the dashboard first.", true) };
|
|
10168
|
+
const names = list.map((c) => `${c.name} (${c.id})`).join(", ");
|
|
10169
|
+
return {
|
|
10170
|
+
error: textResult(
|
|
10171
|
+
`validation: connection_id is required: this installation has ${list.length} connections (${names}). Pass the connection of each queue; list_connections shows them.`,
|
|
10172
|
+
true
|
|
10173
|
+
)
|
|
10174
|
+
};
|
|
10175
|
+
}
|
|
9447
10176
|
var ro = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
9448
10177
|
var rw = (destructive, idempotent) => ({ readOnlyHint: false, destructiveHint: destructive, idempotentHint: idempotent, openWorldHint: false });
|
|
9449
10178
|
function tool(def) {
|
|
@@ -9718,6 +10447,158 @@ var MCP_TOOLS = [
|
|
|
9718
10447
|
schema: zQueue.extend({ group_id: z10.string().min(1).max(200) }),
|
|
9719
10448
|
run: (a, ctx) => forward(ctx, { method: "POST", url: `${queuePath(a)}/groups/${seg(a.group_id)}/resume` })
|
|
9720
10449
|
}),
|
|
10450
|
+
// ----- flow maps (Pro): drawings of a process, read and write -----
|
|
10451
|
+
tool({
|
|
10452
|
+
name: "list_flow_maps",
|
|
10453
|
+
title: "List flow maps",
|
|
10454
|
+
description: "Lists the flow maps: named diagrams of the queues one process goes through. kind manual = drawn by people (nested by parentId, like folders); kind detected = computed, read-only, one per group of queues linked by BullMQ FlowProducer parents. Use get_flow_map for a map's queues, arrows and live counts.",
|
|
10455
|
+
access: "read",
|
|
10456
|
+
inputSchema: object({}, []),
|
|
10457
|
+
annotations: ro,
|
|
10458
|
+
schema: z10.object({}),
|
|
10459
|
+
run: (_a, ctx) => forward(ctx, { method: "GET", url: "/api/flow-maps" })
|
|
10460
|
+
}),
|
|
10461
|
+
tool({
|
|
10462
|
+
name: "get_flow_map",
|
|
10463
|
+
title: "Get flow map",
|
|
10464
|
+
description: "One flow map: its queues (node id = connectionId:queueName) with live job counts, and its arrows (edges, from \u2192 to). source manual = drawn, with an edge id for remove_flow_edge; source detected = seen in BullMQ flows, not removable. missing: true means the queue was not found on its connection.",
|
|
10465
|
+
access: "read",
|
|
10466
|
+
inputSchema: object({ map_id: str4("Flow map id, from list_flow_maps") }, ["map_id"]),
|
|
10467
|
+
annotations: ro,
|
|
10468
|
+
schema: zMap,
|
|
10469
|
+
run: (a, ctx) => forward(ctx, { method: "GET", url: mapPath(a) })
|
|
10470
|
+
}),
|
|
10471
|
+
tool({
|
|
10472
|
+
name: "create_flow_map",
|
|
10473
|
+
title: "Create flow map",
|
|
10474
|
+
description: "Creates an empty flow map to draw a process on. Then call add_flow_edge once per hop (work goes from_queue \u2192 to_queue): queues not on the map yet are added automatically, so the edges alone draw the whole process. Use add_flow_queue only for a queue with no arrow. parent_id nests it under another map.",
|
|
10475
|
+
access: "write",
|
|
10476
|
+
inputSchema: object(
|
|
10477
|
+
{ name: str4("Map name, e.g. the process: Checkout", { maxLength: 80 }), description: str4("What the process does", { maxLength: 500 }), parent_id: str4("Manual map to nest it under") },
|
|
10478
|
+
["name"]
|
|
10479
|
+
),
|
|
10480
|
+
annotations: rw(false, false),
|
|
10481
|
+
schema: z10.object({ name: z10.string().trim().min(1).max(80), description: z10.string().max(500).optional(), parent_id: z10.string().min(1).optional() }),
|
|
10482
|
+
run: (a, ctx) => forward(ctx, { method: "POST", url: "/api/flow-maps", body: { name: a.name, description: a.description, parentId: a.parent_id } })
|
|
10483
|
+
}),
|
|
10484
|
+
tool({
|
|
10485
|
+
name: "update_flow_map",
|
|
10486
|
+
title: "Rename or move flow map",
|
|
10487
|
+
description: "Renames a manual flow map, changes its description, or moves it under another map (parent_id; null moves it to the top level). Detected maps are read-only: copy_flow_map them first.",
|
|
10488
|
+
access: "write",
|
|
10489
|
+
inputSchema: object(
|
|
10490
|
+
{
|
|
10491
|
+
map_id: str4("Flow map id"),
|
|
10492
|
+
name: str4("New name", { maxLength: 80 }),
|
|
10493
|
+
description: { type: ["string", "null"], description: "New description; null clears it" },
|
|
10494
|
+
parent_id: { type: ["string", "null"], description: "Map to move it under; null = top level" }
|
|
10495
|
+
},
|
|
10496
|
+
["map_id"]
|
|
10497
|
+
),
|
|
10498
|
+
annotations: rw(false, true),
|
|
10499
|
+
schema: zMap.extend({ name: z10.string().trim().min(1).max(80).optional(), description: z10.string().max(500).nullable().optional(), parent_id: z10.string().min(1).nullable().optional() }),
|
|
10500
|
+
run: (a, ctx) => forward(ctx, { method: "PATCH", url: mapPath(a), body: { name: a.name, description: a.description, parentId: a.parent_id } })
|
|
10501
|
+
}),
|
|
10502
|
+
tool({
|
|
10503
|
+
name: "delete_flow_map",
|
|
10504
|
+
title: "Delete flow map",
|
|
10505
|
+
description: "Deletes a manual flow map and its drawing. Only the drawing: no queue or job is touched. Maps nested under it move up one level.",
|
|
10506
|
+
access: "write",
|
|
10507
|
+
inputSchema: object({ map_id: str4("Flow map id") }, ["map_id"]),
|
|
10508
|
+
annotations: rw(true, true),
|
|
10509
|
+
schema: zMap,
|
|
10510
|
+
run: (a, ctx) => forward(ctx, { method: "DELETE", url: mapPath(a) })
|
|
10511
|
+
}),
|
|
10512
|
+
tool({
|
|
10513
|
+
name: "add_flow_queue",
|
|
10514
|
+
title: "Add queue to flow map",
|
|
10515
|
+
description: "Puts a queue on a manual flow map. Idempotent. Not needed for queues that have an arrow: add_flow_edge adds both ends. The queue does not have to exist yet (it shows as missing). connection_id may be omitted when Bullpane has exactly one connection.",
|
|
10516
|
+
access: "write",
|
|
10517
|
+
inputSchema: object({ map_id: str4("Flow map id"), queue: str4("Queue name"), connection_id: str4("Connection of the queue, from list_connections (optional with a single connection)") }, ["map_id", "queue"]),
|
|
10518
|
+
annotations: rw(false, true),
|
|
10519
|
+
schema: zMap.extend({ queue: z10.string().min(1).max(255), connection_id: z10.string().min(1).optional() }),
|
|
10520
|
+
run: async (a, ctx) => {
|
|
10521
|
+
const conn = await connectionOrDefault(ctx, a.connection_id);
|
|
10522
|
+
if ("error" in conn) return conn.error;
|
|
10523
|
+
return forward(ctx, { method: "POST", url: `${mapPath(a)}/nodes`, body: { connectionId: conn.id, queueName: a.queue } });
|
|
10524
|
+
}
|
|
10525
|
+
}),
|
|
10526
|
+
tool({
|
|
10527
|
+
name: "remove_flow_queue",
|
|
10528
|
+
title: "Remove queue from flow map",
|
|
10529
|
+
description: "Takes a queue off a manual flow map, with the drawn arrows touching it. Only the drawing: the queue itself is untouched. connection_id may be omitted when Bullpane has exactly one connection.",
|
|
10530
|
+
access: "write",
|
|
10531
|
+
inputSchema: object({ map_id: str4("Flow map id"), queue: str4("Queue name"), connection_id: str4("Connection of the queue (optional with a single connection)") }, ["map_id", "queue"]),
|
|
10532
|
+
annotations: rw(true, true),
|
|
10533
|
+
schema: zMap.extend({ queue: z10.string().min(1).max(255), connection_id: z10.string().min(1).optional() }),
|
|
10534
|
+
run: async (a, ctx) => {
|
|
10535
|
+
const conn = await connectionOrDefault(ctx, a.connection_id);
|
|
10536
|
+
if ("error" in conn) return conn.error;
|
|
10537
|
+
return forward(ctx, { method: "DELETE", url: `${mapPath(a)}/nodes/${seg(flowMapNodeId({ connectionId: conn.id, queueName: a.queue }))}` });
|
|
10538
|
+
}
|
|
10539
|
+
}),
|
|
10540
|
+
tool({
|
|
10541
|
+
name: "add_flow_edge",
|
|
10542
|
+
title: "Draw an arrow on a flow map",
|
|
10543
|
+
description: "Draws one hop of a process on a manual flow map: work goes from from_queue to to_queue (the producer side to the consumer side). Call it once per hop; queues not on the map yet are added automatically. Idempotent: drawing the same arrow again only updates its label. The two queues may be on different connections (another Redis, or Postgres): pass from_connection_id and to_connection_id. Each may be omitted only when Bullpane has exactly one connection.",
|
|
10544
|
+
access: "write",
|
|
10545
|
+
inputSchema: object(
|
|
10546
|
+
{
|
|
10547
|
+
map_id: str4("Flow map id"),
|
|
10548
|
+
from_queue: str4("Queue the work comes from"),
|
|
10549
|
+
to_queue: str4("Queue the work goes to"),
|
|
10550
|
+
label: str4("Short text on the arrow, e.g. 'per item' or 'on failure'", { maxLength: 120 }),
|
|
10551
|
+
from_connection_id: str4("Connection of from_queue, from list_connections (optional with a single connection)"),
|
|
10552
|
+
to_connection_id: str4("Connection of to_queue (optional with a single connection)")
|
|
10553
|
+
},
|
|
10554
|
+
["map_id", "from_queue", "to_queue"]
|
|
10555
|
+
),
|
|
10556
|
+
annotations: rw(false, true),
|
|
10557
|
+
schema: zMap.extend({
|
|
10558
|
+
from_queue: z10.string().min(1).max(255),
|
|
10559
|
+
to_queue: z10.string().min(1).max(255),
|
|
10560
|
+
label: z10.string().max(120).optional(),
|
|
10561
|
+
from_connection_id: z10.string().min(1).optional(),
|
|
10562
|
+
to_connection_id: z10.string().min(1).optional()
|
|
10563
|
+
}),
|
|
10564
|
+
run: async (a, ctx) => {
|
|
10565
|
+
let fallback = null;
|
|
10566
|
+
const pick = async (given) => {
|
|
10567
|
+
if (given) return { id: given };
|
|
10568
|
+
fallback ??= await connectionOrDefault(ctx, void 0);
|
|
10569
|
+
return fallback;
|
|
10570
|
+
};
|
|
10571
|
+
const from = await pick(a.from_connection_id);
|
|
10572
|
+
if ("error" in from) return from.error;
|
|
10573
|
+
const to = await pick(a.to_connection_id);
|
|
10574
|
+
if ("error" in to) return to.error;
|
|
10575
|
+
return forward(ctx, {
|
|
10576
|
+
method: "POST",
|
|
10577
|
+
url: `${mapPath(a)}/edges`,
|
|
10578
|
+
body: { from: { connectionId: from.id, queueName: a.from_queue }, to: { connectionId: to.id, queueName: a.to_queue }, label: a.label }
|
|
10579
|
+
});
|
|
10580
|
+
}
|
|
10581
|
+
}),
|
|
10582
|
+
tool({
|
|
10583
|
+
name: "remove_flow_edge",
|
|
10584
|
+
title: "Remove an arrow from a flow map",
|
|
10585
|
+
description: "Removes a drawn arrow (source manual) from a manual flow map; its edge id is in get_flow_map. The queues stay on the map. Detected arrows cannot be removed.",
|
|
10586
|
+
access: "write",
|
|
10587
|
+
inputSchema: object({ map_id: str4("Flow map id"), edge_id: str4("Edge id, from get_flow_map") }, ["map_id", "edge_id"]),
|
|
10588
|
+
annotations: rw(true, true),
|
|
10589
|
+
schema: zMap.extend({ edge_id: z10.string().min(1) }),
|
|
10590
|
+
run: (a, ctx) => forward(ctx, { method: "DELETE", url: `${mapPath(a)}/edges/${seg(a.edge_id)}` })
|
|
10591
|
+
}),
|
|
10592
|
+
tool({
|
|
10593
|
+
name: "copy_flow_map",
|
|
10594
|
+
title: "Copy flow map",
|
|
10595
|
+
description: "Copies a flow map into a new manual one with the same queues and positions (and drawn arrows). This is how a detected map becomes editable; its detected arrows keep being shown live, they are not copied.",
|
|
10596
|
+
access: "write",
|
|
10597
|
+
inputSchema: object({ map_id: str4("Flow map id to copy (manual or detected)"), name: str4("Name of the copy (default: '<name> (copy)')", { maxLength: 80 }), parent_id: str4("Manual map to nest the copy under") }, ["map_id"]),
|
|
10598
|
+
annotations: rw(false, false),
|
|
10599
|
+
schema: zMap.extend({ name: z10.string().trim().min(1).max(80).optional(), parent_id: z10.string().min(1).optional() }),
|
|
10600
|
+
run: (a, ctx) => forward(ctx, { method: "POST", url: `${mapPath(a)}/copy`, body: { name: a.name, parentId: a.parent_id } })
|
|
10601
|
+
}),
|
|
9721
10602
|
// ----- destructive: a link, never an action -----
|
|
9722
10603
|
tool({
|
|
9723
10604
|
name: "request_destructive_action",
|
|
@@ -9933,7 +10814,7 @@ This connection has ${caller.access} access as ${caller.user.email}.`
|
|
|
9933
10814
|
}
|
|
9934
10815
|
|
|
9935
10816
|
// apps/server/src/ee/mcp/store.ts
|
|
9936
|
-
import { and as
|
|
10817
|
+
import { and as and7, desc as desc3, eq as eq10, lt as lt4, notInArray } from "drizzle-orm";
|
|
9937
10818
|
function affectedRows(result) {
|
|
9938
10819
|
const header = Array.isArray(result) ? result[0] : result;
|
|
9939
10820
|
if (typeof header !== "object" || header === null) return 0;
|
|
@@ -9949,7 +10830,7 @@ var DrizzleMcpStore = class {
|
|
|
9949
10830
|
await this.db.insert(mcpClients3).values(row);
|
|
9950
10831
|
}
|
|
9951
10832
|
async getClient(id3) {
|
|
9952
|
-
const rows = await this.db.select().from(mcpClients3).where(
|
|
10833
|
+
const rows = await this.db.select().from(mcpClients3).where(eq10(mcpClients3.id, id3)).limit(1);
|
|
9953
10834
|
return rows[0] ?? null;
|
|
9954
10835
|
}
|
|
9955
10836
|
async countClients() {
|
|
@@ -9957,16 +10838,16 @@ var DrizzleMcpStore = class {
|
|
|
9957
10838
|
}
|
|
9958
10839
|
async purgeUnusedClients(before) {
|
|
9959
10840
|
const used = this.db.selectDistinct({ id: mcpGrants3.clientId }).from(mcpGrants3);
|
|
9960
|
-
await this.db.delete(mcpClients3).where(
|
|
10841
|
+
await this.db.delete(mcpClients3).where(and7(lt4(mcpClients3.createdAt, before), notInArray(mcpClients3.id, used)));
|
|
9961
10842
|
}
|
|
9962
10843
|
async insertCode(row) {
|
|
9963
10844
|
await this.db.insert(mcpAuthCodes3).values(row);
|
|
9964
10845
|
}
|
|
9965
10846
|
async consumeCode(codeHash) {
|
|
9966
|
-
const rows = await this.db.select().from(mcpAuthCodes3).where(
|
|
10847
|
+
const rows = await this.db.select().from(mcpAuthCodes3).where(eq10(mcpAuthCodes3.codeHash, codeHash)).limit(1);
|
|
9967
10848
|
const row = rows[0];
|
|
9968
10849
|
if (!row) return null;
|
|
9969
|
-
const res = await this.db.delete(mcpAuthCodes3).where(
|
|
10850
|
+
const res = await this.db.delete(mcpAuthCodes3).where(eq10(mcpAuthCodes3.codeHash, codeHash));
|
|
9970
10851
|
return affectedRows(res) === 1 ? row : null;
|
|
9971
10852
|
}
|
|
9972
10853
|
async purgeExpiredCodes(now) {
|
|
@@ -9976,40 +10857,40 @@ var DrizzleMcpStore = class {
|
|
|
9976
10857
|
await this.db.insert(mcpGrants3).values(row);
|
|
9977
10858
|
}
|
|
9978
10859
|
async findGrantByRefresh(refreshHash) {
|
|
9979
|
-
const rows = await this.db.select().from(mcpGrants3).where(
|
|
10860
|
+
const rows = await this.db.select().from(mcpGrants3).where(eq10(mcpGrants3.refreshHash, refreshHash)).limit(1);
|
|
9980
10861
|
return rows[0] ?? null;
|
|
9981
10862
|
}
|
|
9982
10863
|
async findGrantByPrevRefresh(refreshHash) {
|
|
9983
|
-
const rows = await this.db.select().from(mcpGrants3).where(
|
|
10864
|
+
const rows = await this.db.select().from(mcpGrants3).where(eq10(mcpGrants3.prevRefreshHash, refreshHash)).limit(1);
|
|
9984
10865
|
return rows[0] ?? null;
|
|
9985
10866
|
}
|
|
9986
10867
|
async rotateRefresh(grantId, oldHash, newHash, expiresAt) {
|
|
9987
|
-
const res = await this.db.update(mcpGrants3).set({ refreshHash: newHash, prevRefreshHash: oldHash, refreshExpiresAt: expiresAt }).where(
|
|
10868
|
+
const res = await this.db.update(mcpGrants3).set({ refreshHash: newHash, prevRefreshHash: oldHash, refreshExpiresAt: expiresAt }).where(and7(eq10(mcpGrants3.id, grantId), eq10(mcpGrants3.refreshHash, oldHash)));
|
|
9988
10869
|
return affectedRows(res) === 1;
|
|
9989
10870
|
}
|
|
9990
10871
|
async getGrantWithUser(grantId) {
|
|
9991
|
-
const rows = await this.db.select({ grant: mcpGrants3, user: users3, clientName: mcpClients3.name, redirectUris: mcpClients3.redirectUris }).from(mcpGrants3).innerJoin(users3,
|
|
10872
|
+
const rows = await this.db.select({ grant: mcpGrants3, user: users3, clientName: mcpClients3.name, redirectUris: mcpClients3.redirectUris }).from(mcpGrants3).innerJoin(users3, eq10(mcpGrants3.userId, users3.id)).innerJoin(mcpClients3, eq10(mcpGrants3.clientId, mcpClients3.id)).where(eq10(mcpGrants3.id, grantId)).limit(1);
|
|
9992
10873
|
return rows[0] ?? null;
|
|
9993
10874
|
}
|
|
9994
10875
|
async touchGrant(grantId, at) {
|
|
9995
|
-
await this.db.update(mcpGrants3).set({ lastUsedAt: at }).where(
|
|
10876
|
+
await this.db.update(mcpGrants3).set({ lastUsedAt: at }).where(eq10(mcpGrants3.id, grantId));
|
|
9996
10877
|
}
|
|
9997
10878
|
async deleteGrant(grantId) {
|
|
9998
|
-
await this.db.delete(mcpGrants3).where(
|
|
10879
|
+
await this.db.delete(mcpGrants3).where(eq10(mcpGrants3.id, grantId));
|
|
9999
10880
|
}
|
|
10000
10881
|
async deleteGrantsForUser(userId) {
|
|
10001
|
-
await this.db.delete(mcpGrants3).where(
|
|
10882
|
+
await this.db.delete(mcpGrants3).where(eq10(mcpGrants3.userId, userId));
|
|
10002
10883
|
}
|
|
10003
10884
|
async listGrants(userId) {
|
|
10004
|
-
const q = this.db.select({ grant: mcpGrants3, user: users3, clientName: mcpClients3.name, redirectUris: mcpClients3.redirectUris }).from(mcpGrants3).innerJoin(users3,
|
|
10005
|
-
const rows = await (userId ? q.where(
|
|
10885
|
+
const q = this.db.select({ grant: mcpGrants3, user: users3, clientName: mcpClients3.name, redirectUris: mcpClients3.redirectUris }).from(mcpGrants3).innerJoin(users3, eq10(mcpGrants3.userId, users3.id)).innerJoin(mcpClients3, eq10(mcpGrants3.clientId, mcpClients3.id));
|
|
10886
|
+
const rows = await (userId ? q.where(eq10(mcpGrants3.userId, userId)) : q).orderBy(desc3(mcpGrants3.createdAt)).limit(500);
|
|
10006
10887
|
return rows;
|
|
10007
10888
|
}
|
|
10008
10889
|
};
|
|
10009
10890
|
|
|
10010
10891
|
// apps/server/src/services/users.ts
|
|
10011
|
-
import { and as
|
|
10012
|
-
import { nanoid as
|
|
10892
|
+
import { and as and8, eq as eq11, isNull as isNull3 } from "drizzle-orm";
|
|
10893
|
+
import { nanoid as nanoid9 } from "nanoid";
|
|
10013
10894
|
var UsersService = class {
|
|
10014
10895
|
constructor(db) {
|
|
10015
10896
|
this.db = db;
|
|
@@ -10022,13 +10903,13 @@ var UsersService = class {
|
|
|
10022
10903
|
return rows.map(toUserDto);
|
|
10023
10904
|
}
|
|
10024
10905
|
async getRow(id3) {
|
|
10025
|
-
const rows = await this.db.select().from(users3).where(
|
|
10906
|
+
const rows = await this.db.select().from(users3).where(eq11(users3.id, id3)).limit(1);
|
|
10026
10907
|
const row = rows[0];
|
|
10027
10908
|
if (!row) throw notFound("User");
|
|
10028
10909
|
return row;
|
|
10029
10910
|
}
|
|
10030
10911
|
async findByEmail(email) {
|
|
10031
|
-
const rows = await this.db.select().from(users3).where(
|
|
10912
|
+
const rows = await this.db.select().from(users3).where(eq11(users3.email, email.toLowerCase())).limit(1);
|
|
10032
10913
|
return rows[0] ?? null;
|
|
10033
10914
|
}
|
|
10034
10915
|
async create(input) {
|
|
@@ -10037,7 +10918,7 @@ var UsersService = class {
|
|
|
10037
10918
|
if (existing) {
|
|
10038
10919
|
throw conflict(existing.disabledAt ? "A disabled user has this email. Re-enable that account instead of creating a new one." : "A user with this email already exists");
|
|
10039
10920
|
}
|
|
10040
|
-
const id3 =
|
|
10921
|
+
const id3 = nanoid9();
|
|
10041
10922
|
await this.db.insert(users3).values({
|
|
10042
10923
|
id: id3,
|
|
10043
10924
|
email,
|
|
@@ -10069,16 +10950,16 @@ var UsersService = class {
|
|
|
10069
10950
|
patch.disabledAt = input.disabled ? /* @__PURE__ */ new Date() : null;
|
|
10070
10951
|
}
|
|
10071
10952
|
if (Object.keys(patch).length > 0) {
|
|
10072
|
-
await this.db.update(users3).set(patch).where(
|
|
10953
|
+
await this.db.update(users3).set(patch).where(eq11(users3.id, id3));
|
|
10073
10954
|
}
|
|
10074
10955
|
return toUserDto(await this.getRow(id3));
|
|
10075
10956
|
}
|
|
10076
10957
|
async touchLogin(id3, at = /* @__PURE__ */ new Date()) {
|
|
10077
|
-
await this.db.update(users3).set({ lastLoginAt: at }).where(
|
|
10958
|
+
await this.db.update(users3).set({ lastLoginAt: at }).where(eq11(users3.id, id3));
|
|
10078
10959
|
}
|
|
10079
10960
|
/** Active admins only: a disabled admin cannot log in, so it does not keep the install manageable. */
|
|
10080
10961
|
async adminCount() {
|
|
10081
|
-
return (await this.db.select({ id: users3.id }).from(users3).where(
|
|
10962
|
+
return (await this.db.select({ id: users3.id }).from(users3).where(and8(eq11(users3.role, "admin"), isNull3(users3.disabledAt)))).length;
|
|
10082
10963
|
}
|
|
10083
10964
|
};
|
|
10084
10965
|
|
|
@@ -10118,6 +10999,7 @@ async function buildApp(opts) {
|
|
|
10118
10999
|
const health = new HealthService(connections5);
|
|
10119
11000
|
connections5.onEvict((id3) => health.evict(id3));
|
|
10120
11001
|
const flows = new FlowsService(db, connections5);
|
|
11002
|
+
const flowMaps4 = new FlowMapsService(db, connections5, flows);
|
|
10121
11003
|
const alerts4 = new AlertsService(db, connections5, folders4);
|
|
10122
11004
|
const audit = new AuditService(db, app.log);
|
|
10123
11005
|
const attention = new AttentionService(new DrizzleSettingsStore(db));
|
|
@@ -10134,6 +11016,7 @@ async function buildApp(opts) {
|
|
|
10134
11016
|
folders: folders4,
|
|
10135
11017
|
health,
|
|
10136
11018
|
flows,
|
|
11019
|
+
flowMaps: flowMaps4,
|
|
10137
11020
|
alerts: alerts4,
|
|
10138
11021
|
alertsEngine,
|
|
10139
11022
|
audit,
|