@maydotinc/q-studio 0.1.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.d.ts CHANGED
@@ -1,433 +1 @@
1
- import { Queue } from 'bullmq';
2
- import { Q as QueueGroup, i as QueueGroupInfo, j as QueueInfo, O as OverviewStats, M as MetricsResponse, a as ActivityStatsResponse, g as JobStatus, r as SortOptions, P as PaginatedResponse, J as JobInfo, f as JobLogsResponse, o as SearchResult, m as RunInfoList, S as SchedulerInfo, D as DelayedJobInfo, T as TestJobRequest, e as FlowSummary, d as FlowNode, b as CreateFlowRequest, W as WorkbenchOptions } from './types-DhcUr9Xm.js';
3
- export { A as ActivityBucket, C as CreateFlowChildRequest, c as DelayedSortField, F as FailingJobType, H as HourlyBucket, h as JobTags, k as QueueMetrics, R as RepeatableSortField, l as RunInfo, n as RunSortField, p as SlowestJob, q as SortDirection, s as WorkerInfo } from './types-DhcUr9Xm.js';
4
-
5
- /**
6
- * Manages queue operations for the Workbench dashboard
7
- */
8
- declare class QueueManager {
9
- private queues;
10
- private queueGroupByName;
11
- private queueGroups;
12
- private tagFields;
13
- private flowProducer;
14
- private cache;
15
- private readonly CACHE_TTL;
16
- constructor(queues: Queue[], tagFields?: string[], queueGroups?: QueueGroup[]);
17
- /**
18
- * Get cached value or compute and cache
19
- */
20
- private cached;
21
- /**
22
- * Execute a promise with a timeout
23
- */
24
- private withTimeout;
25
- /**
26
- * Get jobs by time range using Redis sorted sets (ZRANGEBYSCORE)
27
- * This is more efficient than fetching all jobs and filtering in memory
28
- */
29
- private getJobsByTimeRange;
30
- /**
31
- * Cache for job state lookups to avoid repeated Redis calls
32
- */
33
- private jobStateCache;
34
- /**
35
- * Cache for job counts to avoid repeated Redis calls
36
- * Short TTL since counts change frequently but are expensive to fetch
37
- */
38
- private countCache;
39
- /**
40
- * Get job counts with caching
41
- */
42
- private getCachedJobCounts;
43
- /**
44
- * Invalidate caches related to a job or queue
45
- */
46
- private invalidateJobCache;
47
- /**
48
- * Clear cache (useful after mutations)
49
- */
50
- clearCache(prefix?: string): void;
51
- /**
52
- * Get quick job counts across all queues (lightweight, for smart polling)
53
- * Returns total counts per status - cached and very fast
54
- */
55
- getQuickCounts(): Promise<{
56
- waiting: number;
57
- active: number;
58
- completed: number;
59
- failed: number;
60
- delayed: number;
61
- total: number;
62
- timestamp: number;
63
- }>;
64
- /**
65
- * Get configured tag field names
66
- */
67
- getTagFields(): string[];
68
- /**
69
- * Get just queue names (very fast, no Redis calls)
70
- * Used for sidebar initial render
71
- */
72
- getQueueNames(): string[];
73
- /**
74
- * Get configured queue groups (very fast, no Redis calls)
75
- */
76
- getQueueGroups(): QueueGroupInfo[];
77
- /**
78
- * Get a queue by name
79
- */
80
- getQueue(name: string): Queue | undefined;
81
- /**
82
- * Get information for all queues (cached)
83
- */
84
- getQueues(): Promise<QueueInfo[]>;
85
- /**
86
- * Get overview statistics (cached)
87
- */
88
- getOverview(): Promise<OverviewStats>;
89
- /**
90
- * Pause a queue - stops processing new jobs
91
- */
92
- pauseQueue(queueName: string): Promise<void>;
93
- /**
94
- * Resume a paused queue
95
- */
96
- resumeQueue(queueName: string): Promise<void>;
97
- /**
98
- * Check if a queue is paused
99
- */
100
- isQueuePaused(queueName: string): Promise<boolean>;
101
- /**
102
- * Get metrics for the last 24 hours (cached - expensive operation)
103
- */
104
- getMetrics(): Promise<MetricsResponse>;
105
- /**
106
- * Get activity stats for the last 7 days (cached)
107
- * Returns 4-hour buckets for the activity timeline
108
- */
109
- getActivityStats(): Promise<ActivityStatsResponse>;
110
- /**
111
- * Get jobs for a specific queue with pagination and sorting
112
- */
113
- getJobs(queueName: string, status?: JobStatus, limit?: number, start?: number, sort?: SortOptions): Promise<PaginatedResponse<JobInfo>>;
114
- /**
115
- * Get a single job by ID
116
- */
117
- getJob(queueName: string, jobId: string): Promise<JobInfo | null>;
118
- /**
119
- * Get BullMQ log lines for a job.
120
- */
121
- getJobLogs(queueName: string, jobId: string, start?: number, end?: number, asc?: boolean): Promise<JobLogsResponse | null>;
122
- /**
123
- * Retry a failed job
124
- */
125
- retryJob(queueName: string, jobId: string): Promise<boolean>;
126
- /**
127
- * Remove a job
128
- */
129
- removeJob(queueName: string, jobId: string): Promise<boolean>;
130
- /**
131
- * Promote a delayed job to waiting
132
- */
133
- promoteJob(queueName: string, jobId: string): Promise<boolean>;
134
- /**
135
- * Parse search query for field:value filters
136
- * Returns { filters: { field: value }, text: remainingText }
137
- */
138
- private parseSearchQuery;
139
- /**
140
- * Check if a raw job matches all provided filters (before conversion)
141
- * This is more efficient than converting to JobInfo first
142
- */
143
- private jobMatchesAllFilters;
144
- /**
145
- * Check if a job matches the given tag filters
146
- */
147
- private jobMatchesFilters;
148
- /**
149
- * Search jobs across all queues
150
- * Supports field:value syntax (e.g., "teamId:abc-123 invoice")
151
- * Optimized with parallel processing, early exits, and count checks
152
- */
153
- search(query: string, limit?: number): Promise<SearchResult[]>;
154
- /**
155
- * Clean jobs from a queue
156
- */
157
- cleanJobs(queueName: string, status: "completed" | "failed", grace?: number): Promise<number>;
158
- /**
159
- * FAST PATH: Get latest runs without filters
160
- * Optimized for the common case of viewing newest jobs (timestamp desc, no filters)
161
- * - Single getJobs call per queue (not per status type)
162
- * - No count checks needed
163
- * - Minimal Redis round-trips
164
- */
165
- private getLatestRuns;
166
- /**
167
- * Get all runs (jobs) across all queues with sorting and filtering
168
- * Uses fast path for common case (no filters, timestamp desc)
169
- */
170
- getAllRuns(limit?: number, start?: number, sort?: SortOptions, filters?: {
171
- status?: JobStatus;
172
- tags?: Record<string, string>;
173
- text?: string;
174
- timeRange?: {
175
- start: number;
176
- end: number;
177
- };
178
- }): Promise<PaginatedResponse<RunInfoList>>;
179
- /**
180
- * Get all schedulers (repeatable and delayed jobs) with sorting
181
- */
182
- getSchedulers(repeatableSort?: SortOptions, delayedSort?: SortOptions): Promise<{
183
- repeatable: SchedulerInfo[];
184
- delayed: DelayedJobInfo[];
185
- }>;
186
- /**
187
- * Enqueue a new job (for testing)
188
- */
189
- enqueueJob(request: TestJobRequest): Promise<{
190
- id: string;
191
- }>;
192
- /**
193
- * Extract tag values from job data based on configured tag fields
194
- */
195
- private extractTags;
196
- /**
197
- * Get unique values for a specific tag field across all jobs
198
- */
199
- getTagValues(field: string, limit?: number): Promise<{
200
- value: string;
201
- count: number;
202
- }[]>;
203
- /**
204
- * Get sortable value from JobInfo/RunInfo
205
- */
206
- private getSortValue;
207
- /**
208
- * Get sortable value from RunInfoList (lightweight version)
209
- */
210
- private getSortValueForList;
211
- /**
212
- * Get sortable value from SchedulerInfo
213
- */
214
- private getSchedulerSortValue;
215
- /**
216
- * Get sortable value from DelayedJobInfo
217
- */
218
- private getDelayedSortValue;
219
- /**
220
- * Convert a BullMQ Job to JobInfo or RunInfoList
221
- * @param job - The BullMQ job to convert
222
- * @param fields - "list" for lightweight list view, "full" for complete job details
223
- * @param knownState - Optional: skip getState() call if state is already known from fetch
224
- */
225
- private jobToInfo;
226
- /**
227
- * Retry multiple jobs across queues
228
- * Processed in parallel for better performance
229
- */
230
- bulkRetry(jobs: {
231
- queueName: string;
232
- jobId: string;
233
- }[]): Promise<{
234
- success: number;
235
- failed: number;
236
- }>;
237
- /**
238
- * Delete multiple jobs across queues
239
- * Processed in parallel for better performance
240
- */
241
- bulkDelete(jobs: {
242
- queueName: string;
243
- jobId: string;
244
- }[]): Promise<{
245
- success: number;
246
- failed: number;
247
- }>;
248
- /**
249
- * Promote multiple delayed jobs across queues (move to waiting)
250
- * Processed in parallel for better performance
251
- */
252
- bulkPromote(jobs: {
253
- queueName: string;
254
- jobId: string;
255
- }[]): Promise<{
256
- success: number;
257
- failed: number;
258
- }>;
259
- /**
260
- * Get all flows (jobs that have children or are part of a flow) - cached
261
- * Optimized to focus on waiting-children type first and early exit
262
- */
263
- getFlows(limit?: number): Promise<FlowSummary[]>;
264
- /**
265
- * Get a single flow tree by root job ID
266
- */
267
- getFlow(queueName: string, jobId: string): Promise<FlowNode | null>;
268
- /**
269
- * Create a new flow
270
- */
271
- createFlow(request: CreateFlowRequest): Promise<{
272
- id: string;
273
- }>;
274
- /**
275
- * Build a FlowJob from CreateFlowRequest or CreateFlowChildRequest
276
- */
277
- private buildFlowJob;
278
- /**
279
- * Convert BullMQ flow tree to our FlowNode structure
280
- */
281
- private convertFlowTree;
282
- /**
283
- * Count statistics for a flow tree
284
- */
285
- private countFlowStats;
286
- }
287
-
288
- /**
289
- * Core Workbench class that manages the dashboard
290
- */
291
- declare class WorkbenchCore {
292
- readonly options: Required<Pick<WorkbenchOptions, "title" | "readonly">> & WorkbenchOptions;
293
- readonly queueManager: QueueManager;
294
- constructor(options: WorkbenchOptions | Queue[]);
295
- /**
296
- * Get the queue manager instance
297
- */
298
- getQueueManager(): QueueManager;
299
- /**
300
- * Check if authentication is required
301
- */
302
- requiresAuth(): boolean;
303
- /**
304
- * Validate authentication credentials
305
- */
306
- validateAuth(username: string, password: string): boolean;
307
- /**
308
- * Get dashboard configuration for the UI
309
- */
310
- getConfig(): {
311
- title: string;
312
- logo: string | undefined;
313
- readonly: boolean;
314
- queues: string[];
315
- queueGroups: QueueGroupInfo[];
316
- tags: string[];
317
- };
318
- }
319
-
320
- /**
321
- * Framework-agnostic HTTP method.
322
- */
323
- type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
324
- /**
325
- * Normalized input passed to every handler. Adapters are responsible for
326
- * mapping their framework-specific request shape to this.
327
- */
328
- interface HandlerInput {
329
- params: Record<string, string>;
330
- query: Record<string, string | undefined>;
331
- body?: unknown;
332
- }
333
- /**
334
- * Normalized output returned by every handler. Adapters serialize this
335
- * onto their framework-specific response object.
336
- */
337
- interface HandlerResult {
338
- status: number;
339
- body: unknown;
340
- }
341
- /**
342
- * A framework-agnostic route handler. Closes over a `WorkbenchCore` and
343
- * takes a normalized request envelope.
344
- */
345
- type Handler = (input: HandlerInput) => Promise<HandlerResult>;
346
- /**
347
- * A framework-agnostic route definition.
348
- *
349
- * `path` uses `:param` syntax compatible with Hono, Express, and Fastify.
350
- * Paths are relative to `/api` — adapters mount them under that prefix.
351
- */
352
- interface RouteDef {
353
- method: HttpMethod;
354
- path: string;
355
- handler: Handler;
356
- }
357
- /**
358
- * Build the framework-agnostic route table for the Workbench API.
359
- *
360
- * Adapters iterate this list and register each route on their host framework.
361
- * Paths are relative to `/api`.
362
- */
363
- declare function buildRouteTable(core: WorkbenchCore): RouteDef[];
364
-
365
- declare function computeBasePath(pathname: string): string;
366
- /**
367
- * Resolve the dashboard's base path, preferring an explicit override.
368
- *
369
- * Adapters where the host framework preserves the mount prefix on the
370
- * incoming URL (Hono `.route()`, Express `req.originalUrl`, Next.js route
371
- * files) can rely on auto-detection. Adapters where the prefix is stripped
372
- * before the handler runs (Elysia `.mount()`) require the user to pass
373
- * `basePath` so the dashboard's HTML still references assets under the
374
- * correct prefix.
375
- */
376
- declare function resolveBasePath(override: string | undefined, pathname: string): string;
377
-
378
- /**
379
- * Parse a `Basic` Authorization header and check it against the configured
380
- * credentials. Uses constant-time comparison to avoid leaking timing info
381
- * about which character mismatched.
382
- *
383
- * Returns `true` when credentials are valid, `false` otherwise. Both inputs
384
- * being undefined or empty count as a failed check — adapters should only
385
- * call this when `core.requiresAuth()` is true.
386
- */
387
- declare function checkBasicAuth(authHeader: string | undefined, username: string, password: string): boolean;
388
- /**
389
- * Standard 401 response body + header for an unauthenticated Basic auth
390
- * request. Adapters use this when `checkBasicAuth` returns false.
391
- */
392
- declare const BASIC_AUTH_CHALLENGE: {
393
- status: 401;
394
- headers: {
395
- "WWW-Authenticate": string;
396
- };
397
- body: string;
398
- };
399
-
400
- interface StaticAssetResult {
401
- status: 200 | 404;
402
- body: Buffer | null;
403
- contentType: string;
404
- }
405
- /**
406
- * Read a bundled UI asset from `UI_DIST_PATH/assets/<filename>`.
407
- *
408
- * Returns a uniform `{ status, body, contentType }` shape so each adapter
409
- * can serialize it onto its framework-native response without re-implementing
410
- * the file lookup or content-type sniffing.
411
- */
412
- declare function serveStaticAsset(filename: string): StaticAssetResult;
413
- interface IndexHtmlResult {
414
- body: string;
415
- contentType: "text/html; charset=utf-8";
416
- }
417
- /**
418
- * Read the bundled `index.html`, inject a `<base href>` matching the request's
419
- * mount path so client-side asset URLs resolve correctly, and return it.
420
- *
421
- * Falls back to a tiny "UI assets not found" stub when the core package has
422
- * not been built yet — useful for `pnpm dev` against a fresh checkout.
423
- */
424
- declare function renderIndexHtml(basePath: string, title: string): IndexHtmlResult;
425
-
426
- /**
427
- * Absolute filesystem path to the bundled UI assets (index.html + /assets).
428
- * Adapters that don't go through {@link createFetchHandler} serve static
429
- * files from this directory directly.
430
- */
431
- declare const UI_DIST_PATH: string;
432
-
433
- export { ActivityStatsResponse, BASIC_AUTH_CHALLENGE, CreateFlowRequest, DelayedJobInfo, FlowNode, FlowSummary, type Handler, type HandlerInput, type HandlerResult, type HttpMethod, type IndexHtmlResult, JobInfo, JobStatus, MetricsResponse, OverviewStats, PaginatedResponse, QueueGroup, QueueGroupInfo, QueueInfo, QueueManager, type RouteDef, RunInfoList, SchedulerInfo, SearchResult, SortOptions, type StaticAssetResult, TestJobRequest, UI_DIST_PATH, WorkbenchCore, WorkbenchOptions, buildRouteTable, checkBasicAuth, computeBasePath, renderIndexHtml, resolveBasePath, serveStaticAsset };
1
+ export * from '@getworkbench/core';
package/dist/core.js CHANGED
@@ -1,25 +1,41 @@
1
1
  import {
2
+ AlertManager,
2
3
  BASIC_AUTH_CHALLENGE,
4
+ MemoryAlertStore,
3
5
  QueueManager,
6
+ RedisAlertStore,
4
7
  UI_DIST_PATH,
5
8
  WorkbenchCore,
6
9
  buildRouteTable,
7
10
  checkBasicAuth,
8
11
  computeBasePath,
12
+ createAlertStore,
13
+ createFetchHandler,
14
+ discoverQueues,
9
15
  renderIndexHtml,
10
16
  resolveBasePath,
11
- serveStaticAsset
12
- } from "./chunk-L36RXNVW.js";
17
+ serveStaticAsset,
18
+ serveUiFile,
19
+ toPublicContactPoint
20
+ } from "./chunk-YGPMHRL3.js";
13
21
  export {
22
+ AlertManager,
14
23
  BASIC_AUTH_CHALLENGE,
24
+ MemoryAlertStore,
15
25
  QueueManager,
26
+ RedisAlertStore,
16
27
  UI_DIST_PATH,
17
28
  WorkbenchCore,
18
29
  buildRouteTable,
19
30
  checkBasicAuth,
20
31
  computeBasePath,
32
+ createAlertStore,
33
+ createFetchHandler,
34
+ discoverQueues,
21
35
  renderIndexHtml,
22
36
  resolveBasePath,
23
- serveStaticAsset
37
+ serveStaticAsset,
38
+ serveUiFile,
39
+ toPublicContactPoint
24
40
  };
25
41
  //# sourceMappingURL=core.js.map
package/dist/index.d.ts CHANGED
@@ -1,7 +1,2 @@
1
- import { Queue } from 'bullmq';
2
- import { W as WorkbenchOptions } from './types-DhcUr9Xm.js';
3
- import { Router } from 'express';
4
-
5
- declare function qStudio(options: WorkbenchOptions | Queue[]): Router;
6
-
7
- export { WorkbenchOptions, qStudio };
1
+ export { workbench as qStudio, workbench } from '@getworkbench/express';
2
+ export { WorkbenchOptions } from '@getworkbench/core';
package/dist/index.js CHANGED
@@ -5,12 +5,13 @@ import {
5
5
  checkBasicAuth,
6
6
  renderIndexHtml,
7
7
  resolveBasePath,
8
- serveStaticAsset
9
- } from "./chunk-L36RXNVW.js";
8
+ serveStaticAsset,
9
+ serveUiFile
10
+ } from "./chunk-YGPMHRL3.js";
10
11
 
11
- // src/index.ts
12
+ // ../express/dist/index.js
12
13
  import express from "express";
13
- function qStudio(options) {
14
+ function workbench(options) {
14
15
  const core = new WorkbenchCore(options);
15
16
  const router = express.Router();
16
17
  if (core.requiresAuth()) {
@@ -53,16 +54,16 @@ function qStudio(options) {
53
54
  router.get("/config", (_req, res) => {
54
55
  res.json(core.getConfig());
55
56
  });
56
- router.get("/favicon.ico", (_req, res) => {
57
- const asset = serveStaticAsset("favicon.ico");
57
+ router.get("/assets/:file", (req, res) => {
58
+ const asset = serveStaticAsset(req.params.file);
58
59
  if (asset.status === 404 || !asset.body) {
59
60
  res.status(404).type("text/plain").send("Not found");
60
61
  return;
61
62
  }
62
63
  res.status(200).type(asset.contentType).send(asset.body);
63
64
  });
64
- router.get("/assets/:file", (req, res) => {
65
- const asset = serveStaticAsset(req.params.file);
65
+ router.get("/app-icon.svg", (_req, res) => {
66
+ const asset = serveUiFile("app-icon.svg");
66
67
  if (asset.status === 404 || !asset.body) {
67
68
  res.status(404).type("text/plain").send("Not found");
68
69
  return;
@@ -82,6 +83,7 @@ function qStudio(options) {
82
83
  return router;
83
84
  }
84
85
  export {
85
- qStudio
86
+ workbench as qStudio,
87
+ workbench
86
88
  };
87
89
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n BASIC_AUTH_CHALLENGE,\n buildRouteTable,\n checkBasicAuth,\n renderIndexHtml,\n resolveBasePath,\n serveStaticAsset,\n WorkbenchCore,\n type WorkbenchOptions,\n} from \"./core\";\nimport type { Queue } from \"bullmq\";\nimport express, { type Router } from \"express\";\n\nexport function qStudio(options: WorkbenchOptions | Queue[]): Router {\n const core = new WorkbenchCore(options);\n const router = express.Router();\n\n if (core.requiresAuth()) {\n router.use((req, res, next) => {\n if (\n !checkBasicAuth(\n req.headers.authorization,\n core.options.auth!.username,\n core.options.auth!.password,\n )\n ) {\n res.set(BASIC_AUTH_CHALLENGE.headers);\n res.status(BASIC_AUTH_CHALLENGE.status).send(BASIC_AUTH_CHALLENGE.body);\n return;\n }\n next();\n });\n }\n\n router.use(\"/api\", (_req, res, next) => {\n res.set(\"Access-Control-Allow-Origin\", \"*\");\n res.set(\"Access-Control-Allow-Methods\", \"GET,HEAD,PUT,PATCH,POST,DELETE\");\n res.set(\"Access-Control-Allow-Headers\", \"*\");\n next();\n });\n\n router.use(\"/api\", express.json());\n\n for (const route of buildRouteTable(core)) {\n router[route.method](`/api${route.path}`, async (req, res) => {\n try {\n const result = await route.handler({\n params: req.params as Record<string, string>,\n query: req.query as Record<string, string | undefined>,\n body: req.body,\n });\n res.status(result.status).json(result.body);\n } catch (error) {\n res.status(500).json({\n error:\n error instanceof Error ? error.message : \"Internal server error\",\n });\n }\n });\n }\n\n router.get(\"/config\", (_req, res) => {\n res.json(core.getConfig());\n });\n\n router.get(\"/favicon.ico\", (_req, res) => {\n const asset = serveStaticAsset(\"favicon.ico\");\n if (asset.status === 404 || !asset.body) {\n res.status(404).type(\"text/plain\").send(\"Not found\");\n return;\n }\n\n res.status(200).type(asset.contentType).send(asset.body);\n });\n\n router.get(\"/assets/:file\", (req, res) => {\n const asset = serveStaticAsset(req.params.file as string);\n if (asset.status === 404 || !asset.body) {\n res.status(404).type(\"text/plain\").send(\"Not found\");\n return;\n }\n res.status(200).type(asset.contentType).send(asset.body);\n });\n\n router.use((req, res, next) => {\n if (req.method !== \"GET\") {\n next();\n return;\n }\n const pathname = (req.originalUrl ?? req.url).split(\"?\")[0] ?? \"/\";\n const basePath = resolveBasePath(core.options.basePath, pathname);\n const html = renderIndexHtml(basePath, core.options.title || \"Workbench\");\n res.status(200).type(\"text/html; charset=utf-8\").send(html.body);\n });\n\n return router;\n}\n\nexport type { WorkbenchOptions } from \"./core\";\n"],"mappings":";;;;;;;;;;;AAWA,OAAO,aAA8B;AAE9B,SAAS,QAAQ,SAA6C;AACnE,QAAM,OAAO,IAAI,cAAc,OAAO;AACtC,QAAM,SAAS,QAAQ,OAAO;AAE9B,MAAI,KAAK,aAAa,GAAG;AACvB,WAAO,IAAI,CAAC,KAAK,KAAK,SAAS;AAC7B,UACE,CAAC;AAAA,QACC,IAAI,QAAQ;AAAA,QACZ,KAAK,QAAQ,KAAM;AAAA,QACnB,KAAK,QAAQ,KAAM;AAAA,MACrB,GACA;AACA,YAAI,IAAI,qBAAqB,OAAO;AACpC,YAAI,OAAO,qBAAqB,MAAM,EAAE,KAAK,qBAAqB,IAAI;AACtE;AAAA,MACF;AACA,WAAK;AAAA,IACP,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS;AACtC,QAAI,IAAI,+BAA+B,GAAG;AAC1C,QAAI,IAAI,gCAAgC,gCAAgC;AACxE,QAAI,IAAI,gCAAgC,GAAG;AAC3C,SAAK;AAAA,EACP,CAAC;AAED,SAAO,IAAI,QAAQ,QAAQ,KAAK,CAAC;AAEjC,aAAW,SAAS,gBAAgB,IAAI,GAAG;AACzC,WAAO,MAAM,MAAM,EAAE,OAAO,MAAM,IAAI,IAAI,OAAO,KAAK,QAAQ;AAC5D,UAAI;AACF,cAAM,SAAS,MAAM,MAAM,QAAQ;AAAA,UACjC,QAAQ,IAAI;AAAA,UACZ,OAAO,IAAI;AAAA,UACX,MAAM,IAAI;AAAA,QACZ,CAAC;AACD,YAAI,OAAO,OAAO,MAAM,EAAE,KAAK,OAAO,IAAI;AAAA,MAC5C,SAAS,OAAO;AACd,YAAI,OAAO,GAAG,EAAE,KAAK;AAAA,UACnB,OACE,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAC7C,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO,IAAI,WAAW,CAAC,MAAM,QAAQ;AACnC,QAAI,KAAK,KAAK,UAAU,CAAC;AAAA,EAC3B,CAAC;AAED,SAAO,IAAI,gBAAgB,CAAC,MAAM,QAAQ;AACxC,UAAM,QAAQ,iBAAiB,aAAa;AAC5C,QAAI,MAAM,WAAW,OAAO,CAAC,MAAM,MAAM;AACvC,UAAI,OAAO,GAAG,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW;AACnD;AAAA,IACF;AAEA,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,WAAW,EAAE,KAAK,MAAM,IAAI;AAAA,EACzD,CAAC;AAED,SAAO,IAAI,iBAAiB,CAAC,KAAK,QAAQ;AACxC,UAAM,QAAQ,iBAAiB,IAAI,OAAO,IAAc;AACxD,QAAI,MAAM,WAAW,OAAO,CAAC,MAAM,MAAM;AACvC,UAAI,OAAO,GAAG,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW;AACnD;AAAA,IACF;AACA,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,WAAW,EAAE,KAAK,MAAM,IAAI;AAAA,EACzD,CAAC;AAED,SAAO,IAAI,CAAC,KAAK,KAAK,SAAS;AAC7B,QAAI,IAAI,WAAW,OAAO;AACxB,WAAK;AACL;AAAA,IACF;AACA,UAAM,YAAY,IAAI,eAAe,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK;AAC/D,UAAM,WAAW,gBAAgB,KAAK,QAAQ,UAAU,QAAQ;AAChE,UAAM,OAAO,gBAAgB,UAAU,KAAK,QAAQ,SAAS,WAAW;AACxE,QAAI,OAAO,GAAG,EAAE,KAAK,0BAA0B,EAAE,KAAK,KAAK,IAAI;AAAA,EACjE,CAAC;AAED,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../express/src/index.ts"],"sourcesContent":["import {\n BASIC_AUTH_CHALLENGE,\n buildRouteTable,\n checkBasicAuth,\n renderIndexHtml,\n resolveBasePath,\n serveStaticAsset,\n serveUiFile,\n WorkbenchCore,\n type WorkbenchOptions,\n} from \"@getworkbench/core\";\nimport type { Queue } from \"bullmq\";\nimport express, { type Router } from \"express\";\n\n/**\n * Mount the Workbench dashboard on an Express app.\n *\n * Returns an Express `Router` so it composes with `app.use(\"/path\", router)`.\n *\n * @example\n * ```ts\n * import express from \"express\";\n * import { Queue } from \"bullmq\";\n * import { workbench } from \"@getworkbench/express\";\n *\n * const app = express();\n * const emailQueue = new Queue(\"email\", {\n * connection: { url: process.env.REDIS_URL! },\n * });\n *\n * app.use(\n * \"/jobs\",\n * workbench({\n * queues: [emailQueue],\n * auth: {\n * username: process.env.WORKBENCH_USER!,\n * password: process.env.WORKBENCH_PASS!,\n * },\n * }),\n * );\n *\n * app.listen(3000);\n * ```\n */\nexport function workbench(options: WorkbenchOptions | Queue[]): Router {\n const core = new WorkbenchCore(options);\n const router = express.Router();\n\n if (core.requiresAuth()) {\n router.use((req, res, next) => {\n if (\n !checkBasicAuth(\n req.headers.authorization,\n core.options.auth!.username,\n core.options.auth!.password,\n )\n ) {\n res.set(BASIC_AUTH_CHALLENGE.headers);\n res.status(BASIC_AUTH_CHALLENGE.status).send(BASIC_AUTH_CHALLENGE.body);\n return;\n }\n next();\n });\n }\n\n router.use(\"/api\", (_req, res, next) => {\n res.set(\"Access-Control-Allow-Origin\", \"*\");\n res.set(\"Access-Control-Allow-Methods\", \"GET,HEAD,PUT,PATCH,POST,DELETE\");\n res.set(\"Access-Control-Allow-Headers\", \"*\");\n next();\n });\n\n router.use(\"/api\", express.json());\n\n for (const route of buildRouteTable(core)) {\n router[route.method](`/api${route.path}`, async (req, res) => {\n try {\n const result = await route.handler({\n params: req.params as Record<string, string>,\n query: req.query as Record<string, string | undefined>,\n body: req.body,\n });\n res.status(result.status).json(result.body);\n } catch (error) {\n res.status(500).json({\n error:\n error instanceof Error ? error.message : \"Internal server error\",\n });\n }\n });\n }\n\n router.get(\"/config\", (_req, res) => {\n res.json(core.getConfig());\n });\n\n router.get(\"/assets/:file\", (req, res) => {\n const asset = serveStaticAsset(req.params.file as string);\n if (asset.status === 404 || !asset.body) {\n res.status(404).type(\"text/plain\").send(\"Not found\");\n return;\n }\n res.status(200).type(asset.contentType).send(asset.body);\n });\n\n router.get(\"/app-icon.svg\", (_req, res) => {\n const asset = serveUiFile(\"app-icon.svg\");\n if (asset.status === 404 || !asset.body) {\n res.status(404).type(\"text/plain\").send(\"Not found\");\n return;\n }\n res.status(200).type(asset.contentType).send(asset.body);\n });\n\n router.use((req, res, next) => {\n if (req.method !== \"GET\") {\n next();\n return;\n }\n const pathname = (req.originalUrl ?? req.url).split(\"?\")[0] ?? \"/\";\n const basePath = resolveBasePath(core.options.basePath, pathname);\n const html = renderIndexHtml(basePath, core.options.title || \"Workbench\");\n res.status(200).type(\"text/html; charset=utf-8\").send(html.body);\n });\n\n return router;\n}\n\nexport type { WorkbenchOptions } from \"@getworkbench/core\";\n"],"mappings":";;;;;;;;;;;;AAYA,OAAO,aAA8B;AAgC9B,SAAS,UAAU,SAA6C;AACrE,QAAM,OAAO,IAAI,cAAc,OAAO;AACtC,QAAM,SAAS,QAAQ,OAAO;AAE9B,MAAI,KAAK,aAAa,GAAG;AACvB,WAAO,IAAI,CAAC,KAAK,KAAK,SAAS;AAC7B,UACE,CAAC;QACC,IAAI,QAAQ;QACZ,KAAK,QAAQ,KAAM;QACnB,KAAK,QAAQ,KAAM;MACrB,GACA;AACA,YAAI,IAAI,qBAAqB,OAAO;AACpC,YAAI,OAAO,qBAAqB,MAAM,EAAE,KAAK,qBAAqB,IAAI;AACtE;MACF;AACA,WAAK;IACP,CAAC;EACH;AAEA,SAAO,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS;AACtC,QAAI,IAAI,+BAA+B,GAAG;AAC1C,QAAI,IAAI,gCAAgC,gCAAgC;AACxE,QAAI,IAAI,gCAAgC,GAAG;AAC3C,SAAK;EACP,CAAC;AAED,SAAO,IAAI,QAAQ,QAAQ,KAAK,CAAC;AAEjC,aAAW,SAAS,gBAAgB,IAAI,GAAG;AACzC,WAAO,MAAM,MAAM,EAAE,OAAO,MAAM,IAAI,IAAI,OAAO,KAAK,QAAQ;AAC5D,UAAI;AACF,cAAM,SAAS,MAAM,MAAM,QAAQ;UACjC,QAAQ,IAAI;UACZ,OAAO,IAAI;UACX,MAAM,IAAI;QACZ,CAAC;AACD,YAAI,OAAO,OAAO,MAAM,EAAE,KAAK,OAAO,IAAI;MAC5C,SAAS,OAAO;AACd,YAAI,OAAO,GAAG,EAAE,KAAK;UACnB,OACE,iBAAiB,QAAQ,MAAM,UAAU;QAC7C,CAAC;MACH;IACF,CAAC;EACH;AAEA,SAAO,IAAI,WAAW,CAAC,MAAM,QAAQ;AACnC,QAAI,KAAK,KAAK,UAAU,CAAC;EAC3B,CAAC;AAED,SAAO,IAAI,iBAAiB,CAAC,KAAK,QAAQ;AACxC,UAAM,QAAQ,iBAAiB,IAAI,OAAO,IAAc;AACxD,QAAI,MAAM,WAAW,OAAO,CAAC,MAAM,MAAM;AACvC,UAAI,OAAO,GAAG,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW;AACnD;IACF;AACA,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,WAAW,EAAE,KAAK,MAAM,IAAI;EACzD,CAAC;AAED,SAAO,IAAI,iBAAiB,CAAC,MAAM,QAAQ;AACzC,UAAM,QAAQ,YAAY,cAAc;AACxC,QAAI,MAAM,WAAW,OAAO,CAAC,MAAM,MAAM;AACvC,UAAI,OAAO,GAAG,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW;AACnD;IACF;AACA,QAAI,OAAO,GAAG,EAAE,KAAK,MAAM,WAAW,EAAE,KAAK,MAAM,IAAI;EACzD,CAAC;AAED,SAAO,IAAI,CAAC,KAAK,KAAK,SAAS;AAC7B,QAAI,IAAI,WAAW,OAAO;AACxB,WAAK;AACL;IACF;AACA,UAAM,YAAY,IAAI,eAAe,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK;AAC/D,UAAM,WAAW,gBAAgB,KAAK,QAAQ,UAAU,QAAQ;AAChE,UAAM,OAAO,gBAAgB,UAAU,KAAK,QAAQ,SAAS,WAAW;AACxE,QAAI,OAAO,GAAG,EAAE,KAAK,0BAA0B,EAAE,KAAK,KAAK,IAAI;EACjE,CAAC;AAED,SAAO;AACT;","names":[]}