bullpane 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
- var flowsQuerySchema = z4.object({ sample: z4.coerce.number().int().min(10).max(2e3).default(200) });
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 nanoid2 } from "nanoid";
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 = nanoid2();
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 and3, asc, desc as desc2, eq as eq3, gte, lt as lt3, lte, or, sql } from "drizzle-orm";
7667
- import { nanoid as nanoid3 } from "nanoid";
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: nanoid3(),
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(eq3(auditLog3.actorId, query.actorId));
7784
- if (query.action) filters.push(eq3(auditLog3.action, query.action));
7785
- if (query.connectionId) filters.push(eq3(auditLog3.connectionId, query.connectionId));
7786
- if (query.queueName) filters.push(eq3(auditLog3.queueName, query.queueName));
7787
- if (query.jobId) filters.push(eq3(auditLog3.jobId, query.jobId));
7788
- if (query.result) filters.push(eq3(auditLog3.result, query.result));
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), and3(eq3(auditLog3.createdAt, c.createdAt), lt3(auditLog3.id, c.id)));
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] : and3(...filters);
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 and4, eq as eq4, inArray } from "drizzle-orm";
7864
- import { nanoid as nanoid4 } from "nanoid";
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(eq4(connections3.id, id3)).limit(1);
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 = nanoid4();
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(eq4(connections3.id, id3));
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(eq4(connections3.id, id3));
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(eq4(folderQueues3.connectionId, id3));
7999
- await this.db.delete(hiddenQueues3).where(eq4(hiddenQueues3.connectionId, id3));
8000
- await this.db.delete(alerts3).where(eq4(alerts3.connectionId, id3));
8001
- await this.db.delete(flowEdges3).where(eq4(flowEdges3.connectionId, id3));
8002
- await this.db.delete(connections3).where(eq4(connections3.id, id3));
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(eq4(hiddenQueues3.connectionId, connectionId));
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(eq4(hiddenQueues3.connectionId, connectionId)).orderBy(hiddenQueues3.hiddenAt);
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(and4(eq4(hiddenQueues3.connectionId, connectionId), eq4(hiddenQueues3.queueName, queueName))).limit(1);
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(and4(eq4(hiddenQueues3.connectionId, connectionId), eq4(hiddenQueues3.queueName, queueName)));
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(and4(eq4(hiddenQueues3.connectionId, connectionId), eq4(hiddenQueues3.queueName, queueName))).limit(1);
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(and4(eq4(connections3.name, name))).limit(1);
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 eq5 } from "drizzle-orm";
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(eq5(settings3.key, key)).limit(1);
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(eq5(settings3.key, key));
9103
+ await this.db.delete(settings3).where(eq6(settings3.key, key));
8759
9104
  }
8760
9105
  };
8761
9106
 
8762
- // apps/server/src/ee/services/flows.ts
8763
- import { and as and5, eq as eq6 } from "drizzle-orm";
8764
- import { nanoid as nanoid5 } from "nanoid";
8765
- var FLOW_CACHE_TTL_MS = 3e4;
8766
- var FLOW_CONCURRENCY = 5;
8767
- function detectedEdgeId(from, to) {
8768
- return `d:${from}->${to}`;
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 toFlowEdgeDto(row) {
8771
- return { id: row.id, from: row.fromQueue, to: row.toQueue, source: "manual", evidence: 0, label: row.label ?? null };
9172
+ function toDetectedMapEdge(connectionId, e) {
9173
+ const from = flowMapNodeId({ connectionId, queueName: e.from });
9174
+ const to = flowMapNodeId({ connectionId, queueName: e.to });
9175
+ return { id: detectedEdgeId(from, to), from, to, source: "detected", label: null, evidence: e.evidence };
8772
9176
  }
8773
- async function mapWithConcurrency(items, limit, fn) {
8774
- const results = new Array(items.length);
8775
- let next = 0;
8776
- const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
8777
- while (next < items.length) {
8778
- const index3 = next++;
8779
- results[index3] = await fn(items[index3]);
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
- var FlowsService = class {
8786
- constructor(db, connections5) {
8787
- this.db = db;
8788
- this.connections = connections5;
8789
- }
8790
- cache = /* @__PURE__ */ new Map();
8791
- async getGraph(connectionId, sample) {
8792
- const row = await this.connections.getRow(connectionId);
8793
- const [detected, manual] = await Promise.all([this.detected(row, sample), this.manualEdges(connectionId)]);
8794
- const nodeIds = new Set(detected.nodes.map((n) => n.id));
8795
- const nodes = [...detected.nodes];
8796
- for (const edge of manual) {
8797
- for (const name of [edge.from, edge.to]) {
8798
- if (!nodeIds.has(name)) {
8799
- nodeIds.add(name);
8800
- nodes.push({ id: name, queueName: name, counts: { ...EMPTY_COUNTS }, isPaused: false });
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
- connectionId,
8806
- nodes,
8807
- edges: [...detected.edges, ...manual],
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
- invalidate(connectionId) {
8812
- this.cache.delete(connectionId);
8813
- }
8814
- detected(row, sample) {
8815
- const cached = this.cache.get(row.id);
8816
- const now = Date.now();
8817
- if (cached && cached.sample === sample && now - cached.at < FLOW_CACHE_TTL_MS) return cached.value;
8818
- const value = withRedis(() => this.computeDetected(this.connections.inspectorFor(row), sample));
8819
- this.cache.set(row.id, { at: now, sample, value });
8820
- value.catch(() => this.cache.delete(row.id));
8821
- return value;
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
- async computeDetected(inspector, sample) {
8824
- const names = await inspector.discoverQueues();
8825
- if (names.length === 0) return { nodes: [], edges: [], sampledJobs: 0 };
8826
- const stats = await inspector.getQueueStats(names);
8827
- const nodes = names.map((name) => ({
8828
- id: name,
8829
- queueName: name,
8830
- counts: stats[name]?.counts ?? { ...EMPTY_COUNTS },
8831
- isPaused: stats[name]?.isPaused ?? false
8832
- }));
8833
- const samples = await mapWithConcurrency(
8834
- names,
8835
- FLOW_CONCURRENCY,
8836
- (name) => inspector.sampleFlowEdges(name, { sample })
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 edgeMap = /* @__PURE__ */ new Map();
8839
- let sampledJobs = 0;
8840
- for (const result of samples) {
8841
- sampledJobs += result.sampled;
8842
- for (const e of result.edges) {
8843
- const id3 = detectedEdgeId(e.childQueue, e.parentQueue);
8844
- const existing = edgeMap.get(id3);
8845
- if (existing) {
8846
- existing.evidence += e.count;
8847
- } else {
8848
- edgeMap.set(id3, {
8849
- id: id3,
8850
- from: e.childQueue,
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 manualEdges(connectionId) {
8862
- const rows = await this.db.select().from(flowEdges3).where(eq6(flowEdges3.connectionId, connectionId));
8863
- return rows.map(toFlowEdgeDto);
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
- async createManualEdge(input) {
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
- if (input.from === input.to) throw conflict("A queue cannot flow into itself");
8868
- const existing = await this.db.select({ id: flowEdges3.id }).from(flowEdges3).where(
8869
- and5(
8870
- eq6(flowEdges3.connectionId, input.connectionId),
8871
- eq6(flowEdges3.fromQueue, input.from),
8872
- eq6(flowEdges3.toQueue, input.to)
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
- ).limit(1);
8875
- if (existing[0]) throw conflict("This edge already exists");
8876
- const id3 = nanoid5();
8877
- await this.db.insert(flowEdges3).values({
8878
- id: id3,
8879
- connectionId: input.connectionId,
8880
- fromQueue: input.from,
8881
- toQueue: input.to,
8882
- label: input.label ?? null,
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
- const rows = await this.db.select().from(flowEdges3).where(eq6(flowEdges3.id, id3)).limit(1);
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 toFlowEdgeDto(row);
9555
+ if (!row) throw notFound("Flow map edge");
9556
+ return row;
8889
9557
  }
8890
- async deleteManualEdge(id3) {
8891
- const rows = await this.db.select({ id: flowEdges3.id }).from(flowEdges3).where(eq6(flowEdges3.id, id3)).limit(1);
8892
- if (!rows[0]) throw notFound("Flow edge");
8893
- await this.db.delete(flowEdges3).where(eq6(flowEdges3.id, id3));
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 eq7, inArray as inArray2 } from "drizzle-orm";
8899
- import { nanoid as nanoid6 } from "nanoid";
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
- inArray2(
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(eq7(folders3.id, id3)).limit(1);
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(eq7(folderQueues3.folderId, id3));
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 = nanoid6();
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(eq7(folders3.id, id3));
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(eq7(folders3.parentId, id3));
8966
- await this.db.delete(folderQueues3).where(eq7(folderQueues3.folderId, id3));
8967
- await this.db.delete(folders3).where(eq7(folders3.id, id3));
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(eq7(folderQueues3.folderId, id3));
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 eq8 } from "drizzle-orm";
9188
- import { nanoid as nanoid7 } from "nanoid";
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(eq8(ssoProviders3.id, id3)).limit(1);
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 = nanoid7();
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(eq8(ssoProviders3.id, id3));
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(eq8(ssoProviders3.enabled, true))).filter((r) => r.id !== id3);
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(eq8(ssoProviders3.id, id3));
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(eq8(ssoProviders3.enabled, true));
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(eq8(ssoProviders3.enabled, true)).orderBy(asc2(ssoProviders3.createdAt));
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(eq8(users3.email, normalised)).limit(1);
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 and6, desc as desc3, eq as eq9, lt as lt4, notInArray } from "drizzle-orm";
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(eq9(mcpClients3.id, id3)).limit(1);
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(and6(lt4(mcpClients3.createdAt, before), notInArray(mcpClients3.id, used)));
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(eq9(mcpAuthCodes3.codeHash, codeHash)).limit(1);
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(eq9(mcpAuthCodes3.codeHash, codeHash));
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(eq9(mcpGrants3.refreshHash, refreshHash)).limit(1);
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(eq9(mcpGrants3.prevRefreshHash, refreshHash)).limit(1);
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(and6(eq9(mcpGrants3.id, grantId), eq9(mcpGrants3.refreshHash, oldHash)));
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, eq9(mcpGrants3.userId, users3.id)).innerJoin(mcpClients3, eq9(mcpGrants3.clientId, mcpClients3.id)).where(eq9(mcpGrants3.id, grantId)).limit(1);
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(eq9(mcpGrants3.id, grantId));
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(eq9(mcpGrants3.id, grantId));
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(eq9(mcpGrants3.userId, userId));
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, eq9(mcpGrants3.userId, users3.id)).innerJoin(mcpClients3, eq9(mcpGrants3.clientId, mcpClients3.id));
10005
- const rows = await (userId ? q.where(eq9(mcpGrants3.userId, userId)) : q).orderBy(desc3(mcpGrants3.createdAt)).limit(500);
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 and7, eq as eq10, isNull as isNull2 } from "drizzle-orm";
10012
- import { nanoid as nanoid8 } from "nanoid";
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(eq10(users3.id, id3)).limit(1);
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(eq10(users3.email, email.toLowerCase())).limit(1);
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 = nanoid8();
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(eq10(users3.id, id3));
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(eq10(users3.id, id3));
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(and7(eq10(users3.role, "admin"), isNull2(users3.disabledAt)))).length;
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,