@getworkbench/core 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +164 -4
- package/dist/index.js +641 -296
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,297 +1,3 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
import { dirname, join } from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
|
|
5
|
-
// src/api/router.ts
|
|
6
|
-
import { Hono } from "hono";
|
|
7
|
-
function parseSort(sort) {
|
|
8
|
-
if (!sort) return void 0;
|
|
9
|
-
const [field, dir] = sort.split(":");
|
|
10
|
-
if (!field) return void 0;
|
|
11
|
-
return {
|
|
12
|
-
field,
|
|
13
|
-
direction: dir === "asc" ? "asc" : "desc"
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
function createApiRoutes(core) {
|
|
17
|
-
const app = new Hono();
|
|
18
|
-
const qm = core.queueManager;
|
|
19
|
-
app.post("/refresh", async (c) => {
|
|
20
|
-
qm.clearCache();
|
|
21
|
-
return c.json({ success: true });
|
|
22
|
-
});
|
|
23
|
-
app.get("/overview", async (c) => {
|
|
24
|
-
const stats = await qm.getOverview();
|
|
25
|
-
return c.json(stats);
|
|
26
|
-
});
|
|
27
|
-
app.get("/counts", async (c) => {
|
|
28
|
-
const counts = await qm.getQuickCounts();
|
|
29
|
-
return c.json(counts);
|
|
30
|
-
});
|
|
31
|
-
app.get("/runs", async (c) => {
|
|
32
|
-
const limit = Number(c.req.query("limit")) || 50;
|
|
33
|
-
const cursor = c.req.query("cursor");
|
|
34
|
-
const start = cursor ? Number(cursor) : 0;
|
|
35
|
-
const sort = parseSort(c.req.query("sort"));
|
|
36
|
-
const status = c.req.query("status");
|
|
37
|
-
const q = c.req.query("q");
|
|
38
|
-
const from = c.req.query("from");
|
|
39
|
-
const to = c.req.query("to");
|
|
40
|
-
const tagsParam = c.req.query("tags");
|
|
41
|
-
let tags;
|
|
42
|
-
if (tagsParam) {
|
|
43
|
-
try {
|
|
44
|
-
tags = JSON.parse(tagsParam);
|
|
45
|
-
} catch {
|
|
46
|
-
const tagPairs = tagsParam.split(",");
|
|
47
|
-
tags = {};
|
|
48
|
-
for (const pair of tagPairs) {
|
|
49
|
-
const [key, value] = pair.split(":");
|
|
50
|
-
if (key && value) {
|
|
51
|
-
tags[key.trim()] = value.trim();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
let timeRange;
|
|
57
|
-
if (from && to) {
|
|
58
|
-
timeRange = {
|
|
59
|
-
start: Number(from),
|
|
60
|
-
end: Number(to)
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
let text;
|
|
64
|
-
if (q) {
|
|
65
|
-
if (!q.includes(":")) {
|
|
66
|
-
text = q;
|
|
67
|
-
} else {
|
|
68
|
-
const parts = q.split(" ");
|
|
69
|
-
const textParts = parts.filter((p) => !p.includes(":"));
|
|
70
|
-
if (textParts.length > 0) {
|
|
71
|
-
text = textParts.join(" ");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const filters = status || tags || text || timeRange ? {
|
|
76
|
-
status,
|
|
77
|
-
tags,
|
|
78
|
-
text,
|
|
79
|
-
timeRange
|
|
80
|
-
} : void 0;
|
|
81
|
-
const result = await qm.getAllRuns(limit, start, sort, filters);
|
|
82
|
-
return c.json(result);
|
|
83
|
-
});
|
|
84
|
-
app.get("/schedulers", async (c) => {
|
|
85
|
-
const repeatableSort = parseSort(c.req.query("repeatableSort"));
|
|
86
|
-
const delayedSort = parseSort(c.req.query("delayedSort"));
|
|
87
|
-
const result = await qm.getSchedulers(repeatableSort, delayedSort);
|
|
88
|
-
return c.json(result);
|
|
89
|
-
});
|
|
90
|
-
app.post("/test", async (c) => {
|
|
91
|
-
if (core.options.readonly) {
|
|
92
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
93
|
-
}
|
|
94
|
-
const body = await c.req.json();
|
|
95
|
-
if (!body.queueName || !body.jobName) {
|
|
96
|
-
return c.json({ error: "queueName and jobName are required" }, 400);
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
const result = await qm.enqueueJob(body);
|
|
100
|
-
return c.json(result);
|
|
101
|
-
} catch (e) {
|
|
102
|
-
return c.json({ error: e.message }, 400);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
app.get("/queue-names", (c) => {
|
|
106
|
-
const names = qm.getQueueNames();
|
|
107
|
-
return c.json(names);
|
|
108
|
-
});
|
|
109
|
-
app.get("/queues", async (c) => {
|
|
110
|
-
const queues = await qm.getQueues();
|
|
111
|
-
return c.json(queues);
|
|
112
|
-
});
|
|
113
|
-
app.get("/metrics", async (c) => {
|
|
114
|
-
const metrics = await qm.getMetrics();
|
|
115
|
-
return c.json(metrics);
|
|
116
|
-
});
|
|
117
|
-
app.get("/activity", async (c) => {
|
|
118
|
-
const stats = await qm.getActivityStats();
|
|
119
|
-
return c.json(stats);
|
|
120
|
-
});
|
|
121
|
-
app.get("/queues/:name/jobs", async (c) => {
|
|
122
|
-
const { name } = c.req.param();
|
|
123
|
-
const status = c.req.query("status");
|
|
124
|
-
const limit = Number(c.req.query("limit")) || 50;
|
|
125
|
-
const cursor = c.req.query("cursor");
|
|
126
|
-
const start = cursor ? Number(cursor) : 0;
|
|
127
|
-
const sort = parseSort(c.req.query("sort"));
|
|
128
|
-
const result = await qm.getJobs(name, status, limit, start, sort);
|
|
129
|
-
return c.json(result);
|
|
130
|
-
});
|
|
131
|
-
app.get("/jobs/:queue/:id", async (c) => {
|
|
132
|
-
const { queue, id } = c.req.param();
|
|
133
|
-
const job = await qm.getJob(queue, id);
|
|
134
|
-
if (!job) {
|
|
135
|
-
return c.json({ error: "Job not found" }, 404);
|
|
136
|
-
}
|
|
137
|
-
return c.json(job);
|
|
138
|
-
});
|
|
139
|
-
app.post("/jobs/:queue/:id/retry", async (c) => {
|
|
140
|
-
if (core.options.readonly) {
|
|
141
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
142
|
-
}
|
|
143
|
-
const { queue, id } = c.req.param();
|
|
144
|
-
const success = await qm.retryJob(queue, id);
|
|
145
|
-
if (!success) {
|
|
146
|
-
return c.json({ error: "Failed to retry job" }, 400);
|
|
147
|
-
}
|
|
148
|
-
return c.json({ success: true });
|
|
149
|
-
});
|
|
150
|
-
app.post("/jobs/:queue/:id/remove", async (c) => {
|
|
151
|
-
if (core.options.readonly) {
|
|
152
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
153
|
-
}
|
|
154
|
-
const { queue, id } = c.req.param();
|
|
155
|
-
const success = await qm.removeJob(queue, id);
|
|
156
|
-
if (!success) {
|
|
157
|
-
return c.json({ error: "Failed to remove job" }, 400);
|
|
158
|
-
}
|
|
159
|
-
return c.json({ success: true });
|
|
160
|
-
});
|
|
161
|
-
app.post("/jobs/:queue/:id/promote", async (c) => {
|
|
162
|
-
if (core.options.readonly) {
|
|
163
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
164
|
-
}
|
|
165
|
-
const { queue, id } = c.req.param();
|
|
166
|
-
const success = await qm.promoteJob(queue, id);
|
|
167
|
-
if (!success) {
|
|
168
|
-
return c.json({ error: "Failed to promote job" }, 400);
|
|
169
|
-
}
|
|
170
|
-
return c.json({ success: true });
|
|
171
|
-
});
|
|
172
|
-
app.get("/search", async (c) => {
|
|
173
|
-
const query = c.req.query("q") || "";
|
|
174
|
-
const limit = Number(c.req.query("limit")) || 20;
|
|
175
|
-
if (!query) {
|
|
176
|
-
return c.json({ results: [] });
|
|
177
|
-
}
|
|
178
|
-
const results = await qm.search(query, limit);
|
|
179
|
-
return c.json({ results });
|
|
180
|
-
});
|
|
181
|
-
app.get("/tags/:field/values", async (c) => {
|
|
182
|
-
const { field } = c.req.param();
|
|
183
|
-
const limit = Number(c.req.query("limit")) || 50;
|
|
184
|
-
const tagFields = qm.getTagFields();
|
|
185
|
-
if (tagFields.length > 0 && !tagFields.includes(field)) {
|
|
186
|
-
return c.json(
|
|
187
|
-
{ error: `Field "${field}" is not a configured tag field` },
|
|
188
|
-
400
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
const values = await qm.getTagValues(field, limit);
|
|
192
|
-
return c.json({ field, values });
|
|
193
|
-
});
|
|
194
|
-
app.post("/queues/:name/clean", async (c) => {
|
|
195
|
-
if (core.options.readonly) {
|
|
196
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
197
|
-
}
|
|
198
|
-
const { name } = c.req.param();
|
|
199
|
-
const body = await c.req.json();
|
|
200
|
-
const count = await qm.cleanJobs(name, body.status, body.grace || 0);
|
|
201
|
-
return c.json({ removed: count });
|
|
202
|
-
});
|
|
203
|
-
app.post("/bulk/retry", async (c) => {
|
|
204
|
-
if (core.options.readonly) {
|
|
205
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
206
|
-
}
|
|
207
|
-
const body = await c.req.json();
|
|
208
|
-
const result = await qm.bulkRetry(body.jobs);
|
|
209
|
-
return c.json(result);
|
|
210
|
-
});
|
|
211
|
-
app.post("/bulk/delete", async (c) => {
|
|
212
|
-
if (core.options.readonly) {
|
|
213
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
214
|
-
}
|
|
215
|
-
const body = await c.req.json();
|
|
216
|
-
const result = await qm.bulkDelete(body.jobs);
|
|
217
|
-
return c.json(result);
|
|
218
|
-
});
|
|
219
|
-
app.post("/bulk/promote", async (c) => {
|
|
220
|
-
if (core.options.readonly) {
|
|
221
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
222
|
-
}
|
|
223
|
-
const body = await c.req.json();
|
|
224
|
-
const result = await qm.bulkPromote(body.jobs);
|
|
225
|
-
return c.json(result);
|
|
226
|
-
});
|
|
227
|
-
app.post("/queues/:name/pause", async (c) => {
|
|
228
|
-
if (core.options.readonly) {
|
|
229
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
230
|
-
}
|
|
231
|
-
const { name } = c.req.param();
|
|
232
|
-
try {
|
|
233
|
-
await qm.pauseQueue(name);
|
|
234
|
-
return c.json({ success: true, paused: true });
|
|
235
|
-
} catch (error) {
|
|
236
|
-
return c.json(
|
|
237
|
-
{
|
|
238
|
-
error: error instanceof Error ? error.message : "Failed to pause queue"
|
|
239
|
-
},
|
|
240
|
-
404
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
app.post("/queues/:name/resume", async (c) => {
|
|
245
|
-
if (core.options.readonly) {
|
|
246
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
247
|
-
}
|
|
248
|
-
const { name } = c.req.param();
|
|
249
|
-
try {
|
|
250
|
-
await qm.resumeQueue(name);
|
|
251
|
-
return c.json({ success: true, paused: false });
|
|
252
|
-
} catch (error) {
|
|
253
|
-
return c.json(
|
|
254
|
-
{
|
|
255
|
-
error: error instanceof Error ? error.message : "Failed to resume queue"
|
|
256
|
-
},
|
|
257
|
-
404
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
app.get("/flows", async (c) => {
|
|
262
|
-
const limit = Number(c.req.query("limit")) || 50;
|
|
263
|
-
const flows = await qm.getFlows(limit);
|
|
264
|
-
return c.json({ flows });
|
|
265
|
-
});
|
|
266
|
-
app.get("/flows/:queueName/:jobId", async (c) => {
|
|
267
|
-
const { queueName, jobId } = c.req.param();
|
|
268
|
-
const flow = await qm.getFlow(queueName, jobId);
|
|
269
|
-
if (!flow) {
|
|
270
|
-
return c.json({ error: "Flow not found" }, 404);
|
|
271
|
-
}
|
|
272
|
-
return c.json(flow);
|
|
273
|
-
});
|
|
274
|
-
app.post("/flows", async (c) => {
|
|
275
|
-
if (core.options.readonly) {
|
|
276
|
-
return c.json({ error: "Dashboard is in readonly mode" }, 403);
|
|
277
|
-
}
|
|
278
|
-
const body = await c.req.json();
|
|
279
|
-
if (!body.name || !body.queueName || !body.children?.length) {
|
|
280
|
-
return c.json(
|
|
281
|
-
{ error: "name, queueName, and children are required" },
|
|
282
|
-
400
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
const result = await qm.createFlow(body);
|
|
287
|
-
return c.json(result);
|
|
288
|
-
} catch (e) {
|
|
289
|
-
return c.json({ error: e.message }, 400);
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
return app;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
1
|
// src/core/queue-manager.ts
|
|
296
2
|
import { FlowProducer } from "bullmq";
|
|
297
3
|
import { LRUCache } from "lru-cache";
|
|
@@ -2036,12 +1742,651 @@ var WorkbenchCore = class {
|
|
|
2036
1742
|
}
|
|
2037
1743
|
};
|
|
2038
1744
|
|
|
2039
|
-
// src/
|
|
1745
|
+
// src/server/hono-app.ts
|
|
1746
|
+
import { Hono as Hono2 } from "hono";
|
|
1747
|
+
import { basicAuth } from "hono/basic-auth";
|
|
1748
|
+
import { cors } from "hono/cors";
|
|
1749
|
+
|
|
1750
|
+
// src/api/router.ts
|
|
1751
|
+
import { Hono } from "hono";
|
|
1752
|
+
|
|
1753
|
+
// src/api/handlers.ts
|
|
1754
|
+
function parseSort(sort) {
|
|
1755
|
+
if (!sort) return void 0;
|
|
1756
|
+
const [field, dir] = sort.split(":");
|
|
1757
|
+
if (!field) return void 0;
|
|
1758
|
+
return {
|
|
1759
|
+
field,
|
|
1760
|
+
direction: dir === "asc" ? "asc" : "desc"
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
var readonlyError = {
|
|
1764
|
+
status: 403,
|
|
1765
|
+
body: { error: "Dashboard is in readonly mode" }
|
|
1766
|
+
};
|
|
1767
|
+
function buildRouteTable(core) {
|
|
1768
|
+
const qm = core.queueManager;
|
|
1769
|
+
const isReadonly = () => !!core.options.readonly;
|
|
1770
|
+
return [
|
|
1771
|
+
{
|
|
1772
|
+
method: "post",
|
|
1773
|
+
path: "/refresh",
|
|
1774
|
+
handler: async () => {
|
|
1775
|
+
qm.clearCache();
|
|
1776
|
+
return { status: 200, body: { success: true } };
|
|
1777
|
+
}
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
method: "get",
|
|
1781
|
+
path: "/overview",
|
|
1782
|
+
handler: async () => ({
|
|
1783
|
+
status: 200,
|
|
1784
|
+
body: await qm.getOverview()
|
|
1785
|
+
})
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
method: "get",
|
|
1789
|
+
path: "/counts",
|
|
1790
|
+
handler: async () => ({
|
|
1791
|
+
status: 200,
|
|
1792
|
+
body: await qm.getQuickCounts()
|
|
1793
|
+
})
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
method: "get",
|
|
1797
|
+
path: "/runs",
|
|
1798
|
+
handler: async ({ query }) => {
|
|
1799
|
+
const limit = Number(query.limit) || 50;
|
|
1800
|
+
const cursor = query.cursor;
|
|
1801
|
+
const start = cursor ? Number(cursor) : 0;
|
|
1802
|
+
const sort = parseSort(query.sort);
|
|
1803
|
+
const status = query.status;
|
|
1804
|
+
const q = query.q;
|
|
1805
|
+
const from = query.from;
|
|
1806
|
+
const to = query.to;
|
|
1807
|
+
const tagsParam = query.tags;
|
|
1808
|
+
let tags;
|
|
1809
|
+
if (tagsParam) {
|
|
1810
|
+
try {
|
|
1811
|
+
tags = JSON.parse(tagsParam);
|
|
1812
|
+
} catch {
|
|
1813
|
+
const tagPairs = tagsParam.split(",");
|
|
1814
|
+
tags = {};
|
|
1815
|
+
for (const pair of tagPairs) {
|
|
1816
|
+
const [key, value] = pair.split(":");
|
|
1817
|
+
if (key && value) {
|
|
1818
|
+
tags[key.trim()] = value.trim();
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
let timeRange;
|
|
1824
|
+
if (from && to) {
|
|
1825
|
+
timeRange = {
|
|
1826
|
+
start: Number(from),
|
|
1827
|
+
end: Number(to)
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
let text;
|
|
1831
|
+
if (q) {
|
|
1832
|
+
if (!q.includes(":")) {
|
|
1833
|
+
text = q;
|
|
1834
|
+
} else {
|
|
1835
|
+
const parts = q.split(" ");
|
|
1836
|
+
const textParts = parts.filter((p) => !p.includes(":"));
|
|
1837
|
+
if (textParts.length > 0) {
|
|
1838
|
+
text = textParts.join(" ");
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const filters = status || tags || text || timeRange ? {
|
|
1843
|
+
status,
|
|
1844
|
+
tags,
|
|
1845
|
+
text,
|
|
1846
|
+
timeRange
|
|
1847
|
+
} : void 0;
|
|
1848
|
+
return {
|
|
1849
|
+
status: 200,
|
|
1850
|
+
body: await qm.getAllRuns(limit, start, sort, filters)
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
method: "get",
|
|
1856
|
+
path: "/schedulers",
|
|
1857
|
+
handler: async ({ query }) => {
|
|
1858
|
+
const repeatableSort = parseSort(query.repeatableSort);
|
|
1859
|
+
const delayedSort = parseSort(query.delayedSort);
|
|
1860
|
+
return {
|
|
1861
|
+
status: 200,
|
|
1862
|
+
body: await qm.getSchedulers(repeatableSort, delayedSort)
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
method: "post",
|
|
1868
|
+
path: "/test",
|
|
1869
|
+
handler: async ({ body }) => {
|
|
1870
|
+
if (isReadonly()) return readonlyError;
|
|
1871
|
+
const req = body;
|
|
1872
|
+
if (!req?.queueName || !req.jobName) {
|
|
1873
|
+
return {
|
|
1874
|
+
status: 400,
|
|
1875
|
+
body: { error: "queueName and jobName are required" }
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
try {
|
|
1879
|
+
const result = await qm.enqueueJob(req);
|
|
1880
|
+
return { status: 200, body: result };
|
|
1881
|
+
} catch (e) {
|
|
1882
|
+
return { status: 400, body: { error: e.message } };
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
method: "get",
|
|
1888
|
+
path: "/queue-names",
|
|
1889
|
+
handler: async () => ({
|
|
1890
|
+
status: 200,
|
|
1891
|
+
body: qm.getQueueNames()
|
|
1892
|
+
})
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
method: "get",
|
|
1896
|
+
path: "/queues",
|
|
1897
|
+
handler: async () => ({
|
|
1898
|
+
status: 200,
|
|
1899
|
+
body: await qm.getQueues()
|
|
1900
|
+
})
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
method: "get",
|
|
1904
|
+
path: "/metrics",
|
|
1905
|
+
handler: async () => ({
|
|
1906
|
+
status: 200,
|
|
1907
|
+
body: await qm.getMetrics()
|
|
1908
|
+
})
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
method: "get",
|
|
1912
|
+
path: "/activity",
|
|
1913
|
+
handler: async () => ({
|
|
1914
|
+
status: 200,
|
|
1915
|
+
body: await qm.getActivityStats()
|
|
1916
|
+
})
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
method: "get",
|
|
1920
|
+
path: "/queues/:name/jobs",
|
|
1921
|
+
handler: async ({ params, query }) => {
|
|
1922
|
+
const name = params.name;
|
|
1923
|
+
const status = query.status;
|
|
1924
|
+
const limit = Number(query.limit) || 50;
|
|
1925
|
+
const cursor = query.cursor;
|
|
1926
|
+
const start = cursor ? Number(cursor) : 0;
|
|
1927
|
+
const sort = parseSort(query.sort);
|
|
1928
|
+
return {
|
|
1929
|
+
status: 200,
|
|
1930
|
+
body: await qm.getJobs(name, status, limit, start, sort)
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
},
|
|
1934
|
+
{
|
|
1935
|
+
method: "get",
|
|
1936
|
+
path: "/jobs/:queue/:id",
|
|
1937
|
+
handler: async ({ params }) => {
|
|
1938
|
+
const job = await qm.getJob(params.queue, params.id);
|
|
1939
|
+
if (!job) {
|
|
1940
|
+
return { status: 404, body: { error: "Job not found" } };
|
|
1941
|
+
}
|
|
1942
|
+
return { status: 200, body: job };
|
|
1943
|
+
}
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
method: "post",
|
|
1947
|
+
path: "/jobs/:queue/:id/retry",
|
|
1948
|
+
handler: async ({ params }) => {
|
|
1949
|
+
if (isReadonly()) return readonlyError;
|
|
1950
|
+
const success = await qm.retryJob(params.queue, params.id);
|
|
1951
|
+
if (!success) {
|
|
1952
|
+
return { status: 400, body: { error: "Failed to retry job" } };
|
|
1953
|
+
}
|
|
1954
|
+
return { status: 200, body: { success: true } };
|
|
1955
|
+
}
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
method: "post",
|
|
1959
|
+
path: "/jobs/:queue/:id/remove",
|
|
1960
|
+
handler: async ({ params }) => {
|
|
1961
|
+
if (isReadonly()) return readonlyError;
|
|
1962
|
+
const success = await qm.removeJob(params.queue, params.id);
|
|
1963
|
+
if (!success) {
|
|
1964
|
+
return { status: 400, body: { error: "Failed to remove job" } };
|
|
1965
|
+
}
|
|
1966
|
+
return { status: 200, body: { success: true } };
|
|
1967
|
+
}
|
|
1968
|
+
},
|
|
1969
|
+
{
|
|
1970
|
+
method: "post",
|
|
1971
|
+
path: "/jobs/:queue/:id/promote",
|
|
1972
|
+
handler: async ({ params }) => {
|
|
1973
|
+
if (isReadonly()) return readonlyError;
|
|
1974
|
+
const success = await qm.promoteJob(params.queue, params.id);
|
|
1975
|
+
if (!success) {
|
|
1976
|
+
return { status: 400, body: { error: "Failed to promote job" } };
|
|
1977
|
+
}
|
|
1978
|
+
return { status: 200, body: { success: true } };
|
|
1979
|
+
}
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
method: "get",
|
|
1983
|
+
path: "/search",
|
|
1984
|
+
handler: async ({ query }) => {
|
|
1985
|
+
const q = query.q || "";
|
|
1986
|
+
const limit = Number(query.limit) || 20;
|
|
1987
|
+
if (!q) return { status: 200, body: { results: [] } };
|
|
1988
|
+
const results = await qm.search(q, limit);
|
|
1989
|
+
return { status: 200, body: { results } };
|
|
1990
|
+
}
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
method: "get",
|
|
1994
|
+
path: "/tags/:field/values",
|
|
1995
|
+
handler: async ({ params, query }) => {
|
|
1996
|
+
const field = params.field;
|
|
1997
|
+
const limit = Number(query.limit) || 50;
|
|
1998
|
+
const tagFields = qm.getTagFields();
|
|
1999
|
+
if (tagFields.length > 0 && !tagFields.includes(field)) {
|
|
2000
|
+
return {
|
|
2001
|
+
status: 400,
|
|
2002
|
+
body: {
|
|
2003
|
+
error: `Field "${field}" is not a configured tag field`
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
const values = await qm.getTagValues(field, limit);
|
|
2008
|
+
return { status: 200, body: { field, values } };
|
|
2009
|
+
}
|
|
2010
|
+
},
|
|
2011
|
+
{
|
|
2012
|
+
method: "post",
|
|
2013
|
+
path: "/queues/:name/clean",
|
|
2014
|
+
handler: async ({ params, body }) => {
|
|
2015
|
+
if (isReadonly()) return readonlyError;
|
|
2016
|
+
const req = body;
|
|
2017
|
+
if (!req) {
|
|
2018
|
+
return { status: 400, body: { error: "Body required" } };
|
|
2019
|
+
}
|
|
2020
|
+
const count = await qm.cleanJobs(
|
|
2021
|
+
params.name,
|
|
2022
|
+
req.status,
|
|
2023
|
+
req.grace || 0
|
|
2024
|
+
);
|
|
2025
|
+
return { status: 200, body: { removed: count } };
|
|
2026
|
+
}
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
method: "post",
|
|
2030
|
+
path: "/bulk/retry",
|
|
2031
|
+
handler: async ({ body }) => {
|
|
2032
|
+
if (isReadonly()) return readonlyError;
|
|
2033
|
+
const req = body;
|
|
2034
|
+
if (!req?.jobs) {
|
|
2035
|
+
return { status: 400, body: { error: "jobs is required" } };
|
|
2036
|
+
}
|
|
2037
|
+
return { status: 200, body: await qm.bulkRetry(req.jobs) };
|
|
2038
|
+
}
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
method: "post",
|
|
2042
|
+
path: "/bulk/delete",
|
|
2043
|
+
handler: async ({ body }) => {
|
|
2044
|
+
if (isReadonly()) return readonlyError;
|
|
2045
|
+
const req = body;
|
|
2046
|
+
if (!req?.jobs) {
|
|
2047
|
+
return { status: 400, body: { error: "jobs is required" } };
|
|
2048
|
+
}
|
|
2049
|
+
return { status: 200, body: await qm.bulkDelete(req.jobs) };
|
|
2050
|
+
}
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
method: "post",
|
|
2054
|
+
path: "/bulk/promote",
|
|
2055
|
+
handler: async ({ body }) => {
|
|
2056
|
+
if (isReadonly()) return readonlyError;
|
|
2057
|
+
const req = body;
|
|
2058
|
+
if (!req?.jobs) {
|
|
2059
|
+
return { status: 400, body: { error: "jobs is required" } };
|
|
2060
|
+
}
|
|
2061
|
+
return { status: 200, body: await qm.bulkPromote(req.jobs) };
|
|
2062
|
+
}
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
method: "post",
|
|
2066
|
+
path: "/queues/:name/pause",
|
|
2067
|
+
handler: async ({ params }) => {
|
|
2068
|
+
if (isReadonly()) return readonlyError;
|
|
2069
|
+
try {
|
|
2070
|
+
await qm.pauseQueue(params.name);
|
|
2071
|
+
return { status: 200, body: { success: true, paused: true } };
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
return {
|
|
2074
|
+
status: 404,
|
|
2075
|
+
body: {
|
|
2076
|
+
error: error instanceof Error ? error.message : "Failed to pause queue"
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
method: "post",
|
|
2084
|
+
path: "/queues/:name/resume",
|
|
2085
|
+
handler: async ({ params }) => {
|
|
2086
|
+
if (isReadonly()) return readonlyError;
|
|
2087
|
+
try {
|
|
2088
|
+
await qm.resumeQueue(params.name);
|
|
2089
|
+
return { status: 200, body: { success: true, paused: false } };
|
|
2090
|
+
} catch (error) {
|
|
2091
|
+
return {
|
|
2092
|
+
status: 404,
|
|
2093
|
+
body: {
|
|
2094
|
+
error: error instanceof Error ? error.message : "Failed to resume queue"
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
method: "get",
|
|
2102
|
+
path: "/flows",
|
|
2103
|
+
handler: async ({ query }) => {
|
|
2104
|
+
const limit = Number(query.limit) || 50;
|
|
2105
|
+
const flows = await qm.getFlows(limit);
|
|
2106
|
+
return { status: 200, body: { flows } };
|
|
2107
|
+
}
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
method: "get",
|
|
2111
|
+
path: "/flows/:queueName/:jobId",
|
|
2112
|
+
handler: async ({ params }) => {
|
|
2113
|
+
const flow = await qm.getFlow(params.queueName, params.jobId);
|
|
2114
|
+
if (!flow) {
|
|
2115
|
+
return { status: 404, body: { error: "Flow not found" } };
|
|
2116
|
+
}
|
|
2117
|
+
return { status: 200, body: flow };
|
|
2118
|
+
}
|
|
2119
|
+
},
|
|
2120
|
+
{
|
|
2121
|
+
method: "post",
|
|
2122
|
+
path: "/flows",
|
|
2123
|
+
handler: async ({ body }) => {
|
|
2124
|
+
if (isReadonly()) return readonlyError;
|
|
2125
|
+
const req = body;
|
|
2126
|
+
if (!req?.name || !req.queueName || !req.children?.length) {
|
|
2127
|
+
return {
|
|
2128
|
+
status: 400,
|
|
2129
|
+
body: { error: "name, queueName, and children are required" }
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
try {
|
|
2133
|
+
const result = await qm.createFlow(req);
|
|
2134
|
+
return { status: 200, body: result };
|
|
2135
|
+
} catch (e) {
|
|
2136
|
+
return { status: 400, body: { error: e.message } };
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
];
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// src/api/router.ts
|
|
2144
|
+
function createApiRoutes(core) {
|
|
2145
|
+
const app = new Hono();
|
|
2146
|
+
for (const route of buildRouteTable(core)) {
|
|
2147
|
+
app[route.method](route.path, async (c) => {
|
|
2148
|
+
let body;
|
|
2149
|
+
const method = c.req.method;
|
|
2150
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
2151
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
2152
|
+
if (contentType.includes("application/json")) {
|
|
2153
|
+
try {
|
|
2154
|
+
body = await c.req.json();
|
|
2155
|
+
} catch {
|
|
2156
|
+
body = void 0;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const input = {
|
|
2161
|
+
params: c.req.param(),
|
|
2162
|
+
query: c.req.query(),
|
|
2163
|
+
body
|
|
2164
|
+
};
|
|
2165
|
+
const result = await route.handler(input);
|
|
2166
|
+
return new Response(JSON.stringify(result.body), {
|
|
2167
|
+
status: result.status,
|
|
2168
|
+
headers: { "Content-Type": "application/json" }
|
|
2169
|
+
});
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
return app;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/server/base-path.ts
|
|
2176
|
+
var CLIENT_ROUTES = [
|
|
2177
|
+
/\/queues\/[^/]+\/jobs\/[^/]+\/?$/,
|
|
2178
|
+
/\/queues\/[^/]+\/?$/,
|
|
2179
|
+
/\/flows\/[^/]+\/[^/]+\/?$/,
|
|
2180
|
+
/\/schedulers\/?$/,
|
|
2181
|
+
/\/flows\/?$/,
|
|
2182
|
+
/\/metrics\/?$/,
|
|
2183
|
+
/\/test\/?$/
|
|
2184
|
+
];
|
|
2185
|
+
function computeBasePath(pathname) {
|
|
2186
|
+
let basePath = pathname;
|
|
2187
|
+
for (const route of CLIENT_ROUTES) {
|
|
2188
|
+
basePath = basePath.replace(route, "");
|
|
2189
|
+
}
|
|
2190
|
+
if (!basePath.endsWith("/")) {
|
|
2191
|
+
basePath = `${basePath}/`;
|
|
2192
|
+
}
|
|
2193
|
+
return basePath;
|
|
2194
|
+
}
|
|
2195
|
+
function resolveBasePath(override, pathname) {
|
|
2196
|
+
if (override) {
|
|
2197
|
+
return override.endsWith("/") ? override : `${override}/`;
|
|
2198
|
+
}
|
|
2199
|
+
return computeBasePath(pathname);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// src/server/static-assets.ts
|
|
2203
|
+
import { existsSync, readFileSync } from "fs";
|
|
2204
|
+
import { join as join2 } from "path";
|
|
2205
|
+
|
|
2206
|
+
// src/ui-dist.ts
|
|
2207
|
+
import { dirname, join } from "path";
|
|
2208
|
+
import { fileURLToPath } from "url";
|
|
2040
2209
|
var UI_DIST_PATH = join(dirname(fileURLToPath(import.meta.url)), "ui");
|
|
2210
|
+
|
|
2211
|
+
// src/server/static-assets.ts
|
|
2212
|
+
function serveStaticAsset(filename) {
|
|
2213
|
+
const filePath = join2(UI_DIST_PATH, "assets", filename);
|
|
2214
|
+
if (!existsSync(filePath)) {
|
|
2215
|
+
return { status: 404, body: null, contentType: "text/plain" };
|
|
2216
|
+
}
|
|
2217
|
+
const body = readFileSync(filePath);
|
|
2218
|
+
const contentType = filename.endsWith(".js") ? "application/javascript" : filename.endsWith(".css") ? "text/css" : filename.endsWith(".svg") ? "image/svg+xml" : filename.endsWith(".png") ? "image/png" : filename.endsWith(".woff2") ? "font/woff2" : "application/octet-stream";
|
|
2219
|
+
return { status: 200, body, contentType };
|
|
2220
|
+
}
|
|
2221
|
+
function renderIndexHtml(basePath, title) {
|
|
2222
|
+
const indexPath = join2(UI_DIST_PATH, "index.html");
|
|
2223
|
+
if (existsSync(indexPath)) {
|
|
2224
|
+
let html = readFileSync(indexPath, "utf-8");
|
|
2225
|
+
html = html.replace("<head>", `<head>
|
|
2226
|
+
<base href="${basePath}">`);
|
|
2227
|
+
return { body: html, contentType: "text/html; charset=utf-8" };
|
|
2228
|
+
}
|
|
2229
|
+
return {
|
|
2230
|
+
body: fallbackHtml(title, basePath),
|
|
2231
|
+
contentType: "text/html; charset=utf-8"
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
function fallbackHtml(title, basePath) {
|
|
2235
|
+
return `<!DOCTYPE html>
|
|
2236
|
+
<html lang="en">
|
|
2237
|
+
<head>
|
|
2238
|
+
<base href="${basePath}">
|
|
2239
|
+
<meta charset="UTF-8" />
|
|
2240
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2241
|
+
<title>${title}</title>
|
|
2242
|
+
<style>
|
|
2243
|
+
body {
|
|
2244
|
+
font-family: system-ui, sans-serif;
|
|
2245
|
+
display: flex;
|
|
2246
|
+
align-items: center;
|
|
2247
|
+
justify-content: center;
|
|
2248
|
+
min-height: 100vh;
|
|
2249
|
+
margin: 0;
|
|
2250
|
+
background: #0a0a0a;
|
|
2251
|
+
color: #fafafa;
|
|
2252
|
+
}
|
|
2253
|
+
.message {
|
|
2254
|
+
text-align: center;
|
|
2255
|
+
padding: 2rem;
|
|
2256
|
+
}
|
|
2257
|
+
code {
|
|
2258
|
+
background: #1a1a1a;
|
|
2259
|
+
padding: 0.5rem 1rem;
|
|
2260
|
+
border-radius: 0.5rem;
|
|
2261
|
+
display: block;
|
|
2262
|
+
margin-top: 1rem;
|
|
2263
|
+
}
|
|
2264
|
+
</style>
|
|
2265
|
+
</head>
|
|
2266
|
+
<body>
|
|
2267
|
+
<div class="message">
|
|
2268
|
+
<h1>${title}</h1>
|
|
2269
|
+
<p>UI assets not found. Build @getworkbench/core first:</p>
|
|
2270
|
+
<code>bun run --filter=@getworkbench/core build</code>
|
|
2271
|
+
</div>
|
|
2272
|
+
</body>
|
|
2273
|
+
</html>`;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// src/server/hono-app.ts
|
|
2277
|
+
function buildWorkbenchApp(core) {
|
|
2278
|
+
const app = new Hono2();
|
|
2279
|
+
app.use("/api/*", cors());
|
|
2280
|
+
if (core.requiresAuth()) {
|
|
2281
|
+
app.use(
|
|
2282
|
+
"*",
|
|
2283
|
+
basicAuth({
|
|
2284
|
+
username: core.options.auth.username,
|
|
2285
|
+
password: core.options.auth.password
|
|
2286
|
+
})
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
app.route("/api", createApiRoutes(core));
|
|
2290
|
+
app.get("/config", (c) => c.json(core.getConfig()));
|
|
2291
|
+
app.get("/assets/:file", (c) => {
|
|
2292
|
+
const fileName = c.req.param("file");
|
|
2293
|
+
const asset = serveStaticAsset(fileName);
|
|
2294
|
+
if (asset.status === 404 || !asset.body) {
|
|
2295
|
+
return c.text("Not found", 404);
|
|
2296
|
+
}
|
|
2297
|
+
return new Response(new Uint8Array(asset.body), {
|
|
2298
|
+
status: 200,
|
|
2299
|
+
headers: { "Content-Type": asset.contentType }
|
|
2300
|
+
});
|
|
2301
|
+
});
|
|
2302
|
+
app.get("*", (c) => {
|
|
2303
|
+
const url = new URL(c.req.url);
|
|
2304
|
+
const basePath = resolveBasePath(core.options.basePath, url.pathname);
|
|
2305
|
+
const html = renderIndexHtml(basePath, core.options.title || "Workbench");
|
|
2306
|
+
return c.html(html.body);
|
|
2307
|
+
});
|
|
2308
|
+
return app;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// src/api/fetch-handler.ts
|
|
2312
|
+
function createFetchHandler(options) {
|
|
2313
|
+
const core = new WorkbenchCore(options);
|
|
2314
|
+
const app = buildWorkbenchApp(core);
|
|
2315
|
+
const basePath = normalizeBasePath(core.options.basePath);
|
|
2316
|
+
const fetchHandler = async (req) => {
|
|
2317
|
+
if (basePath) {
|
|
2318
|
+
const url = new URL(req.url);
|
|
2319
|
+
if (url.pathname === basePath || url.pathname.startsWith(`${basePath}/`)) {
|
|
2320
|
+
const rewritten = url.pathname.slice(basePath.length) || "/";
|
|
2321
|
+
url.pathname = rewritten;
|
|
2322
|
+
const init = {
|
|
2323
|
+
method: req.method,
|
|
2324
|
+
headers: req.headers,
|
|
2325
|
+
body: req.method === "GET" || req.method === "HEAD" ? void 0 : req.body,
|
|
2326
|
+
// @ts-expect-error duplex is required for streaming bodies in Node 18+
|
|
2327
|
+
duplex: "half",
|
|
2328
|
+
redirect: req.redirect
|
|
2329
|
+
};
|
|
2330
|
+
return app.fetch(new Request(url.toString(), init));
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
return app.fetch(req);
|
|
2334
|
+
};
|
|
2335
|
+
return {
|
|
2336
|
+
fetch: fetchHandler,
|
|
2337
|
+
core
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
function normalizeBasePath(value) {
|
|
2341
|
+
if (!value) return null;
|
|
2342
|
+
const trimmed = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
2343
|
+
if (trimmed === "" || trimmed === "/") return null;
|
|
2344
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// src/server/basic-auth.ts
|
|
2348
|
+
function checkBasicAuth(authHeader, username, password) {
|
|
2349
|
+
if (!authHeader) return false;
|
|
2350
|
+
const [scheme, encoded] = authHeader.split(" ");
|
|
2351
|
+
if (!scheme || scheme.toLowerCase() !== "basic" || !encoded) return false;
|
|
2352
|
+
let decoded;
|
|
2353
|
+
try {
|
|
2354
|
+
decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
|
2355
|
+
} catch {
|
|
2356
|
+
return false;
|
|
2357
|
+
}
|
|
2358
|
+
const idx = decoded.indexOf(":");
|
|
2359
|
+
if (idx === -1) return false;
|
|
2360
|
+
const user = decoded.slice(0, idx);
|
|
2361
|
+
const pass = decoded.slice(idx + 1);
|
|
2362
|
+
return safeEqual(user, username) && safeEqual(pass, password);
|
|
2363
|
+
}
|
|
2364
|
+
function safeEqual(a, b) {
|
|
2365
|
+
if (a.length !== b.length) return false;
|
|
2366
|
+
let diff = 0;
|
|
2367
|
+
for (let i = 0; i < a.length; i++) {
|
|
2368
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2369
|
+
}
|
|
2370
|
+
return diff === 0;
|
|
2371
|
+
}
|
|
2372
|
+
var BASIC_AUTH_CHALLENGE = {
|
|
2373
|
+
status: 401,
|
|
2374
|
+
headers: { "WWW-Authenticate": 'Basic realm="Workbench"' },
|
|
2375
|
+
body: "Unauthorized"
|
|
2376
|
+
};
|
|
2041
2377
|
export {
|
|
2378
|
+
BASIC_AUTH_CHALLENGE,
|
|
2042
2379
|
QueueManager,
|
|
2043
2380
|
UI_DIST_PATH,
|
|
2044
2381
|
WorkbenchCore,
|
|
2045
|
-
|
|
2382
|
+
buildRouteTable,
|
|
2383
|
+
buildWorkbenchApp,
|
|
2384
|
+
checkBasicAuth,
|
|
2385
|
+
computeBasePath,
|
|
2386
|
+
createApiRoutes,
|
|
2387
|
+
createFetchHandler,
|
|
2388
|
+
renderIndexHtml,
|
|
2389
|
+
resolveBasePath,
|
|
2390
|
+
serveStaticAsset
|
|
2046
2391
|
};
|
|
2047
2392
|
//# sourceMappingURL=index.js.map
|