@azlib/scheduler 1.0.5 → 1.2.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/Dockerfile +14 -0
- package/README.md +53 -0
- package/compose.yaml +28 -0
- package/dist/cli-config-6u8AzSdw.mjs +542 -0
- package/dist/cli-config-6u8AzSdw.mjs.map +1 -0
- package/dist/cli-config-CFBl-YOh.cjs +628 -0
- package/dist/dashboard/cli.cjs +30 -0
- package/dist/dashboard/cli.d.cts +6 -0
- package/dist/dashboard/cli.d.cts.map +1 -0
- package/dist/dashboard/cli.d.mts +6 -0
- package/dist/dashboard/cli.d.mts.map +1 -0
- package/dist/dashboard/cli.mjs +31 -0
- package/dist/dashboard/cli.mjs.map +1 -0
- package/dist/dashboard/public/assets/index-CbmpG6_w.css +2 -0
- package/dist/dashboard/public/assets/index-DczjtqRn.js +12 -0
- package/dist/dashboard/public/index.html +13 -0
- package/dist/dashboard-types-BHjRLcxJ.d.cts +127 -0
- package/dist/dashboard-types-BHjRLcxJ.d.cts.map +1 -0
- package/dist/dashboard-types-BHjRLcxJ.d.mts +127 -0
- package/dist/dashboard-types-BHjRLcxJ.d.mts.map +1 -0
- package/dist/dashboard.cjs +15 -0
- package/dist/dashboard.d.cts +31 -0
- package/dist/dashboard.d.cts.map +1 -0
- package/dist/dashboard.d.mts +31 -0
- package/dist/dashboard.d.mts.map +1 -0
- package/dist/dashboard.mjs +4 -0
- package/dist/http-server-Cme4XKyH.d.mts +34 -0
- package/dist/http-server-Cme4XKyH.d.mts.map +1 -0
- package/dist/http-server-u1BnbkAK.d.cts +34 -0
- package/dist/http-server-u1BnbkAK.d.cts.map +1 -0
- package/dist/index.cjs +49 -442
- package/dist/index.d.cts +3 -158
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -158
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +44 -439
- package/dist/index.mjs.map +1 -1
- package/dist/queue-dashboard-service-BFXcnfUr.cjs +55 -0
- package/dist/queue-dashboard-service-C0QapgOg.mjs +52 -0
- package/dist/queue-dashboard-service-C0QapgOg.mjs.map +1 -0
- package/dist/store-dashboard-C44d7sdh.d.cts +59 -0
- package/dist/store-dashboard-C44d7sdh.d.cts.map +1 -0
- package/dist/store-dashboard-Cyf_JILa.mjs +515 -0
- package/dist/store-dashboard-Cyf_JILa.mjs.map +1 -0
- package/dist/store-dashboard-D-yIxXxL.d.mts +59 -0
- package/dist/store-dashboard-D-yIxXxL.d.mts.map +1 -0
- package/dist/store-dashboard-Sg4BBWiJ.cjs +554 -0
- package/package.json +54 -6
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
const require_store_dashboard = require("./store-dashboard-Sg4BBWiJ.cjs");
|
|
24
|
+
let _azlib_std = require("@azlib/std");
|
|
25
|
+
let node_http = require("node:http");
|
|
26
|
+
node_http = __toESM(node_http, 1);
|
|
27
|
+
let node_fs = require("node:fs");
|
|
28
|
+
let node_path = require("node:path");
|
|
29
|
+
let node_url = require("node:url");
|
|
30
|
+
let node_async_hooks = require("node:async_hooks");
|
|
31
|
+
let _azlib_persistence = require("@azlib/persistence");
|
|
32
|
+
//#region src/dashboard/http-auth.ts
|
|
33
|
+
var DashboardHttpError = class extends Error {
|
|
34
|
+
status;
|
|
35
|
+
constructor(status, message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "DashboardHttpError";
|
|
38
|
+
this.status = status;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function isLoopbackHost(host) {
|
|
42
|
+
const normalized = host.trim().toLowerCase();
|
|
43
|
+
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1" || normalized === "[::1]";
|
|
44
|
+
}
|
|
45
|
+
function authorizeDashboardRequest(input) {
|
|
46
|
+
if (!input.token) return;
|
|
47
|
+
if ((Array.isArray(input.authorization) ? input.authorization[0] : input.authorization) !== `Bearer ${input.token}`) throw new DashboardHttpError(401, "Unauthorized");
|
|
48
|
+
}
|
|
49
|
+
function assertDashboardBindSafety(input) {
|
|
50
|
+
if (!isLoopbackHost(input.host) && !input.token) throw new Error("DASHBOARD_TOKEN is required when HOST is not a loopback address");
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/dashboard/http-parse.ts
|
|
54
|
+
const DEFAULT_TIMEZONE = "UTC";
|
|
55
|
+
function requiredText(value, field) {
|
|
56
|
+
const text = (0, _azlib_std.asString)(value)?.trim();
|
|
57
|
+
if (!text) throw new DashboardHttpError(400, `${field} is required.`);
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
function parseConfig(value) {
|
|
61
|
+
if (value === void 0 || value === null || value === "") return {};
|
|
62
|
+
if (typeof value === "string") try {
|
|
63
|
+
return JSON.parse(value);
|
|
64
|
+
} catch {
|
|
65
|
+
throw new DashboardHttpError(400, "config must be valid JSON.");
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function parseScheduleFromRecord(record) {
|
|
70
|
+
const nested = (0, _azlib_std.asRecord)(record.schedule);
|
|
71
|
+
const scheduleType = (0, _azlib_std.asString)(record.scheduleType)?.trim() || (0, _azlib_std.asString)(nested?.scheduleType)?.trim() || "cron";
|
|
72
|
+
if (scheduleType !== "cron" && scheduleType !== "once") throw new DashboardHttpError(400, "scheduleType must be cron or once.");
|
|
73
|
+
const expression = requiredText(record.expression ?? nested?.expression, "expression");
|
|
74
|
+
const timezone = (0, _azlib_std.asString)(record.timezone)?.trim() || (0, _azlib_std.asString)(nested?.timezone)?.trim() || DEFAULT_TIMEZONE;
|
|
75
|
+
require_store_dashboard.parseSchedule(scheduleType, expression, timezone);
|
|
76
|
+
return {
|
|
77
|
+
scheduleType,
|
|
78
|
+
expression,
|
|
79
|
+
timezone
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseCreateJobBody(body) {
|
|
83
|
+
const record = (0, _azlib_std.asRecord)(body);
|
|
84
|
+
if (!record) throw new DashboardHttpError(400, "Request body must be a JSON object.");
|
|
85
|
+
return {
|
|
86
|
+
name: requiredText(record.name, "name"),
|
|
87
|
+
handlerKey: requiredText(record.handlerKey, "handlerKey"),
|
|
88
|
+
schedule: parseScheduleFromRecord(record),
|
|
89
|
+
config: parseConfig(record.config),
|
|
90
|
+
enabled: (0, _azlib_std.asBoolean)(record.enabled) ?? true
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function parseUpdateJobBody(body) {
|
|
94
|
+
const record = (0, _azlib_std.asRecord)(body);
|
|
95
|
+
if (!record) throw new DashboardHttpError(400, "Request body must be a JSON object.");
|
|
96
|
+
const patch = {};
|
|
97
|
+
if (record.name !== void 0) patch.name = requiredText(record.name, "name");
|
|
98
|
+
if (record.handlerKey !== void 0) patch.handlerKey = requiredText(record.handlerKey, "handlerKey");
|
|
99
|
+
if (record.expression !== void 0 || record.timezone !== void 0 || record.scheduleType !== void 0 || record.schedule !== void 0) patch.schedule = parseScheduleFromRecord(record);
|
|
100
|
+
if (record.config !== void 0) patch.config = parseConfig(record.config);
|
|
101
|
+
if (record.enabled !== void 0) patch.enabled = (0, _azlib_std.asBoolean)(record.enabled) ?? true;
|
|
102
|
+
return patch;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/dashboard/http-path.ts
|
|
106
|
+
function normalizeDashboardBasePath(basePath) {
|
|
107
|
+
const trimmed = basePath?.trim();
|
|
108
|
+
if (!trimmed || trimmed === "/") return "";
|
|
109
|
+
return (trimmed.startsWith("/") ? trimmed : `/${trimmed}`).replace(/\/+$/, "");
|
|
110
|
+
}
|
|
111
|
+
function stripDashboardBasePath(pathname, basePath) {
|
|
112
|
+
const base = normalizeDashboardBasePath(basePath);
|
|
113
|
+
const path = pathname || "/";
|
|
114
|
+
if (!base) return path.startsWith("/") ? path : `/${path}`;
|
|
115
|
+
if (path === base || path === `${base}/`) return "/";
|
|
116
|
+
if (path.startsWith(`${base}/`)) {
|
|
117
|
+
const rest = path.slice(base.length);
|
|
118
|
+
return rest.startsWith("/") ? rest : `/${rest}`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function resolveDashboardApiBase(pathname) {
|
|
123
|
+
const trimmed = (pathname.split("?")[0] || "/").replace(/\/index\.html$/i, "/").replace(/\/[^/]+\.[a-z0-9]+$/i, "/").replace(/\/+$/, "");
|
|
124
|
+
return trimmed === "" ? "/api" : `${trimmed}/api`;
|
|
125
|
+
}
|
|
126
|
+
function requestPathname(url, originalUrl) {
|
|
127
|
+
const raw = originalUrl || url || "/";
|
|
128
|
+
try {
|
|
129
|
+
return new URL(raw, "http://127.0.0.1").pathname;
|
|
130
|
+
} catch {
|
|
131
|
+
return raw.split("?")[0] || "/";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/dashboard/public-dir.ts
|
|
136
|
+
function resolveDashboardPublicDir(fromFileUrl) {
|
|
137
|
+
const dir = (0, node_path.dirname)((0, node_url.fileURLToPath)(fromFileUrl));
|
|
138
|
+
const nested = (0, node_path.join)(dir, "public");
|
|
139
|
+
const sibling = (0, node_path.join)(dir, "dashboard", "public");
|
|
140
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(nested, "index.html"))) return nested;
|
|
141
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(sibling, "index.html"))) return sibling;
|
|
142
|
+
return nested;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/dashboard/http-handler.ts
|
|
146
|
+
const MIME_TYPES = {
|
|
147
|
+
".css": "text/css; charset=utf-8",
|
|
148
|
+
".html": "text/html; charset=utf-8",
|
|
149
|
+
".js": "text/javascript; charset=utf-8",
|
|
150
|
+
".json": "application/json; charset=utf-8",
|
|
151
|
+
".map": "application/json; charset=utf-8",
|
|
152
|
+
".svg": "image/svg+xml",
|
|
153
|
+
".woff2": "font/woff2"
|
|
154
|
+
};
|
|
155
|
+
function sendJson(res, status, body) {
|
|
156
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
157
|
+
res.end(JSON.stringify(body));
|
|
158
|
+
}
|
|
159
|
+
function sendDashboardError(res, error) {
|
|
160
|
+
if (error instanceof DashboardHttpError) {
|
|
161
|
+
sendJson(res, error.status, { error: error.message });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
165
|
+
sendJson(res, message.includes("not found") ? 404 : message.includes("Only failed") ? 400 : message.includes("Unknown handlerKey") ? 400 : 500, { error: message });
|
|
166
|
+
}
|
|
167
|
+
function hasParsedBody(req) {
|
|
168
|
+
return Object.prototype.hasOwnProperty.call(req, "body");
|
|
169
|
+
}
|
|
170
|
+
async function readJsonBody(req) {
|
|
171
|
+
if (req.readableEnded && hasParsedBody(req)) return req.body ?? {};
|
|
172
|
+
const chunks = [];
|
|
173
|
+
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
174
|
+
if (chunks.length === 0) {
|
|
175
|
+
if (hasParsedBody(req)) return req.body ?? {};
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
179
|
+
if (!raw.trim()) return {};
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(raw);
|
|
182
|
+
} catch {
|
|
183
|
+
throw new DashboardHttpError(400, "Request body must be valid JSON.");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function matchPath(pathname, pattern) {
|
|
187
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
188
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
189
|
+
if (pathParts.length !== patternParts.length) return null;
|
|
190
|
+
const params = {};
|
|
191
|
+
for (let i = 0; i < patternParts.length; i += 1) {
|
|
192
|
+
const expected = patternParts[i] ?? "";
|
|
193
|
+
const actual = pathParts[i] ?? "";
|
|
194
|
+
if (expected.startsWith(":")) {
|
|
195
|
+
params[expected.slice(1)] = decodeURIComponent(actual);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (expected !== actual) return null;
|
|
199
|
+
}
|
|
200
|
+
return params;
|
|
201
|
+
}
|
|
202
|
+
function servePublicFile(publicDir, urlPath, res) {
|
|
203
|
+
if (!(0, node_fs.existsSync)(publicDir)) return false;
|
|
204
|
+
const pathname = decodeURIComponent(urlPath.split("?")[0] ?? "/");
|
|
205
|
+
const abs = (0, node_path.resolve)(publicDir, `.${pathname === "/" ? "/index.html" : pathname}`);
|
|
206
|
+
const rel = (0, node_path.relative)((0, node_path.resolve)(publicDir), abs);
|
|
207
|
+
if (rel.startsWith("..") || rel.includes(`..${node_path.sep}`)) return false;
|
|
208
|
+
const filePath = (0, node_fs.existsSync)(abs) && (0, node_fs.statSync)(abs).isFile() ? abs : (0, node_path.join)(publicDir, "index.html");
|
|
209
|
+
if (!(0, node_fs.existsSync)(filePath) || !(0, node_fs.statSync)(filePath).isFile()) return false;
|
|
210
|
+
const type = MIME_TYPES[(0, node_path.extname)(filePath)] ?? "application/octet-stream";
|
|
211
|
+
res.writeHead(200, { "Content-Type": type });
|
|
212
|
+
(0, node_fs.createReadStream)(filePath).pipe(res);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
async function authorizeApi(options, method, pathname, req) {
|
|
216
|
+
if (options.authorize) {
|
|
217
|
+
await options.authorize({
|
|
218
|
+
method,
|
|
219
|
+
pathname,
|
|
220
|
+
authorization: req.headers.authorization
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
authorizeDashboardRequest({
|
|
225
|
+
token: options.token,
|
|
226
|
+
authorization: req.headers.authorization
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async function handleApi(dashboard, method, pathname, req, res) {
|
|
230
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
231
|
+
sendJson(res, 200, await dashboard.getHealth());
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (method === "GET" && pathname === "/api/handlers") {
|
|
235
|
+
sendJson(res, 200, { handlers: dashboard.listHandlerKeys() });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (method === "GET" && pathname === "/api/jobs") {
|
|
239
|
+
sendJson(res, 200, { items: await dashboard.listItems() });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (method === "POST" && pathname === "/api/jobs") {
|
|
243
|
+
sendJson(res, 201, await dashboard.createJob(parseCreateJobBody(await readJsonBody(req))));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const jobExec = matchPath(pathname, "/api/jobs/:jobId/executions");
|
|
247
|
+
if (jobExec && method === "GET") {
|
|
248
|
+
sendJson(res, 200, { executions: await dashboard.listExecutions(jobExec.jobId ?? "") });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const jobAction = matchPath(pathname, "/api/jobs/:jobId/:action");
|
|
252
|
+
if (jobAction && method === "POST") {
|
|
253
|
+
const jobId = jobAction.jobId ?? "";
|
|
254
|
+
if (jobAction.action === "pause") {
|
|
255
|
+
await dashboard.pauseJob(jobId);
|
|
256
|
+
res.writeHead(204);
|
|
257
|
+
res.end();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (jobAction.action === "resume") {
|
|
261
|
+
await dashboard.resumeJob(jobId);
|
|
262
|
+
res.writeHead(204);
|
|
263
|
+
res.end();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (jobAction.action === "run") {
|
|
267
|
+
await dashboard.runJob(jobId);
|
|
268
|
+
res.writeHead(204);
|
|
269
|
+
res.end();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const retry = matchPath(pathname, "/api/executions/:executionId/retry");
|
|
274
|
+
if (retry && method === "POST") {
|
|
275
|
+
await dashboard.retryExecution(retry.executionId ?? "");
|
|
276
|
+
res.writeHead(204);
|
|
277
|
+
res.end();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const job = matchPath(pathname, "/api/jobs/:jobId");
|
|
281
|
+
if (job) {
|
|
282
|
+
const jobId = job.jobId ?? "";
|
|
283
|
+
if (method === "GET") {
|
|
284
|
+
const item = await dashboard.getJob(jobId);
|
|
285
|
+
if (!item) throw new DashboardHttpError(404, `Job not found: ${jobId}`);
|
|
286
|
+
sendJson(res, 200, item);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (method === "PATCH") {
|
|
290
|
+
await dashboard.updateJob(jobId, parseUpdateJobBody(await readJsonBody(req)));
|
|
291
|
+
res.writeHead(204);
|
|
292
|
+
res.end();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (method === "DELETE") {
|
|
296
|
+
await dashboard.deleteJob(jobId);
|
|
297
|
+
res.writeHead(204);
|
|
298
|
+
res.end();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
sendJson(res, 404, { error: "Not Found" });
|
|
303
|
+
}
|
|
304
|
+
async function handleSchedulerDashboardRequest(req, res, options) {
|
|
305
|
+
const method = req.method ?? "GET";
|
|
306
|
+
const originalUrl = "originalUrl" in req && typeof req.originalUrl === "string" ? req.originalUrl : void 0;
|
|
307
|
+
const pathname = requestPathname(req.url, originalUrl);
|
|
308
|
+
const basePath = normalizeDashboardBasePath(options.basePath);
|
|
309
|
+
const relative = stripDashboardBasePath(pathname, basePath);
|
|
310
|
+
if (relative === null) return false;
|
|
311
|
+
if (method === "GET" && basePath && pathname === basePath) {
|
|
312
|
+
res.writeHead(302, { Location: `${basePath}/` });
|
|
313
|
+
res.end();
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
const publicDir = options.publicDir ?? resolveDashboardPublicDir(require("url").pathToFileURL(__filename).href);
|
|
317
|
+
try {
|
|
318
|
+
if (method === "GET" && relative === "/health") {
|
|
319
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
320
|
+
res.end("ok");
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (relative === "/api" || relative.startsWith("/api/")) {
|
|
324
|
+
await authorizeApi(options, method, relative, req);
|
|
325
|
+
await handleApi(options.dashboard, method, relative, req, res);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
if (method === "GET" && servePublicFile(publicDir, relative, res)) return true;
|
|
329
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
330
|
+
res.end("Not Found");
|
|
331
|
+
return true;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
sendDashboardError(res, error);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function createSchedulerDashboardNodeHandler(options) {
|
|
338
|
+
return (req, res, next) => {
|
|
339
|
+
handleSchedulerDashboardRequest(req, res, options).then((handled) => {
|
|
340
|
+
if (!handled) next?.();
|
|
341
|
+
}).catch((error) => {
|
|
342
|
+
if (next) {
|
|
343
|
+
next(error);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (!res.headersSent) sendDashboardError(res, error);
|
|
347
|
+
});
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/dashboard/http-server.ts
|
|
352
|
+
function createSchedulerDashboardServer(options) {
|
|
353
|
+
const port = options.port ?? 9100;
|
|
354
|
+
const host = options.host ?? "127.0.0.1";
|
|
355
|
+
const handler = createSchedulerDashboardNodeHandler({
|
|
356
|
+
...options,
|
|
357
|
+
publicDir: options.publicDir ?? resolveDashboardPublicDir(require("url").pathToFileURL(__filename).href)
|
|
358
|
+
});
|
|
359
|
+
const server = node_http.createServer((req, res) => {
|
|
360
|
+
handler(req, res, () => {
|
|
361
|
+
if (!res.headersSent) {
|
|
362
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
363
|
+
res.end("Not Found");
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
get port() {
|
|
369
|
+
return port;
|
|
370
|
+
},
|
|
371
|
+
get host() {
|
|
372
|
+
return host;
|
|
373
|
+
},
|
|
374
|
+
listen() {
|
|
375
|
+
return new Promise((resolveListen, reject) => {
|
|
376
|
+
server.once("error", reject);
|
|
377
|
+
server.listen(port, host, () => {
|
|
378
|
+
server.off("error", reject);
|
|
379
|
+
resolveListen();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
},
|
|
383
|
+
close() {
|
|
384
|
+
return new Promise((resolveClose, reject) => {
|
|
385
|
+
server.close((err) => err ? reject(err) : resolveClose());
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/dashboard/sql-drivers.ts
|
|
392
|
+
function rewriteQuestionParams(sql, style) {
|
|
393
|
+
if (style === "?") return sql;
|
|
394
|
+
let index = 0;
|
|
395
|
+
return sql.replaceAll("?", () => {
|
|
396
|
+
index += 1;
|
|
397
|
+
return style === "$n" ? `$${index}` : `@p${index}`;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function inferDashboardDialect(databaseUrl) {
|
|
401
|
+
const scheme = databaseUrl.split(":")[0]?.toLowerCase() ?? "";
|
|
402
|
+
if (scheme === "postgres" || scheme === "postgresql") return "postgres";
|
|
403
|
+
if (scheme === "mysql" || scheme === "mariadb") return "mysql";
|
|
404
|
+
if (scheme === "mssql" || scheme === "sqlserver") return "mssql";
|
|
405
|
+
if (scheme === "sqlite" || scheme === "file") return "sqlite";
|
|
406
|
+
return "postgres";
|
|
407
|
+
}
|
|
408
|
+
function dialectAdapterFor(dialect) {
|
|
409
|
+
if (dialect === "mysql") return (0, _azlib_persistence.createMySqlDialectAdapter)();
|
|
410
|
+
if (dialect === "sqlite") return (0, _azlib_persistence.createSqliteDialectAdapter)();
|
|
411
|
+
if (dialect === "mssql") return (0, _azlib_persistence.createSqlServerDialectAdapter)();
|
|
412
|
+
return (0, _azlib_persistence.createPostgresDialectAdapter)();
|
|
413
|
+
}
|
|
414
|
+
function sqlitePathFromUrl(databaseUrl) {
|
|
415
|
+
if (databaseUrl.startsWith("file:")) return databaseUrl.slice(5);
|
|
416
|
+
if (databaseUrl.startsWith("sqlite:")) return databaseUrl.slice(7);
|
|
417
|
+
return databaseUrl;
|
|
418
|
+
}
|
|
419
|
+
function bindMssqlParams(request, params) {
|
|
420
|
+
(params ?? []).forEach((value, index) => {
|
|
421
|
+
request.input(`p${index + 1}`, value);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function createPgDriver(pool) {
|
|
425
|
+
const txStore = new node_async_hooks.AsyncLocalStorage();
|
|
426
|
+
const active = () => txStore.getStore() ?? pool;
|
|
427
|
+
const runQuery = async (sql, params) => active().query(rewriteQuestionParams(sql, "$n"), [...params ?? []]);
|
|
428
|
+
return {
|
|
429
|
+
query: async (sql, params) => {
|
|
430
|
+
return (await runQuery(sql, params)).rows;
|
|
431
|
+
},
|
|
432
|
+
execute: async (sql, params) => {
|
|
433
|
+
await runQuery(sql, params);
|
|
434
|
+
},
|
|
435
|
+
transaction: async (run) => {
|
|
436
|
+
if (txStore.getStore()) return run();
|
|
437
|
+
const conn = await pool.connect();
|
|
438
|
+
try {
|
|
439
|
+
await conn.query("BEGIN");
|
|
440
|
+
const result = await txStore.run(conn, run);
|
|
441
|
+
await conn.query("COMMIT");
|
|
442
|
+
return result;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
await conn.query("ROLLBACK");
|
|
445
|
+
throw error;
|
|
446
|
+
} finally {
|
|
447
|
+
conn.release();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function createMysqlDriver(pool) {
|
|
453
|
+
const txStore = new node_async_hooks.AsyncLocalStorage();
|
|
454
|
+
const active = () => txStore.getStore() ?? pool;
|
|
455
|
+
return {
|
|
456
|
+
query: async (sql, params) => {
|
|
457
|
+
const [rows] = await active().execute(sql, [...params ?? []]);
|
|
458
|
+
return rows;
|
|
459
|
+
},
|
|
460
|
+
execute: async (sql, params) => {
|
|
461
|
+
await active().execute(sql, [...params ?? []]);
|
|
462
|
+
},
|
|
463
|
+
transaction: async (run) => {
|
|
464
|
+
if (txStore.getStore()) return run();
|
|
465
|
+
const conn = await pool.getConnection();
|
|
466
|
+
try {
|
|
467
|
+
await conn.beginTransaction();
|
|
468
|
+
const result = await txStore.run(conn, run);
|
|
469
|
+
await conn.commit();
|
|
470
|
+
return result;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
await conn.rollback();
|
|
473
|
+
throw error;
|
|
474
|
+
} finally {
|
|
475
|
+
conn.release();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function createDashboardSqlDriver(databaseUrl, dialect) {
|
|
481
|
+
if (dialect === "postgres") {
|
|
482
|
+
const pg = await import("pg");
|
|
483
|
+
const Pool = pg.Pool ?? pg.default?.Pool;
|
|
484
|
+
if (!Pool) throw new Error("The pg package is required for postgres dashboards.");
|
|
485
|
+
return createPgDriver(new Pool({ connectionString: databaseUrl }));
|
|
486
|
+
}
|
|
487
|
+
if (dialect === "mysql") {
|
|
488
|
+
const mysql = await import("mysql2/promise");
|
|
489
|
+
const createPool = mysql.createPool ?? mysql.default?.createPool;
|
|
490
|
+
if (!createPool) throw new Error("The mysql2 package is required for mysql dashboards.");
|
|
491
|
+
return createMysqlDriver(createPool(databaseUrl));
|
|
492
|
+
}
|
|
493
|
+
if (dialect === "sqlite") {
|
|
494
|
+
const Database = (await import("better-sqlite3")).default;
|
|
495
|
+
if (!Database) throw new Error("The better-sqlite3 package is required for sqlite dashboards.");
|
|
496
|
+
const db = new Database(sqlitePathFromUrl(databaseUrl));
|
|
497
|
+
return {
|
|
498
|
+
query: async (sql, params) => db.prepare(sql).all(...params ?? []),
|
|
499
|
+
execute: async (sql, params) => {
|
|
500
|
+
db.prepare(sql).run(...params ?? []);
|
|
501
|
+
},
|
|
502
|
+
transaction: async (run) => run()
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const mssql = await import("mssql");
|
|
506
|
+
const connect = mssql.connect ?? mssql.default?.connect;
|
|
507
|
+
if (!connect) throw new Error("The mssql package is required for mssql dashboards.");
|
|
508
|
+
const pool = await connect(databaseUrl);
|
|
509
|
+
return {
|
|
510
|
+
query: async (sql, params) => {
|
|
511
|
+
const request = pool.request();
|
|
512
|
+
bindMssqlParams(request, params);
|
|
513
|
+
return (await request.query(rewriteQuestionParams(sql, "@pn"))).recordset;
|
|
514
|
+
},
|
|
515
|
+
execute: async (sql, params) => {
|
|
516
|
+
const request = pool.request();
|
|
517
|
+
bindMssqlParams(request, params);
|
|
518
|
+
await request.query(rewriteQuestionParams(sql, "@pn"));
|
|
519
|
+
},
|
|
520
|
+
transaction: async (run) => run()
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function createDashboardPersistenceConfig(input) {
|
|
524
|
+
return {
|
|
525
|
+
client: (0, _azlib_persistence.createSqlClientAdapter)(input.driver),
|
|
526
|
+
dialect: dialectAdapterFor(input.dialect),
|
|
527
|
+
namespace: input.namespace
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/dashboard/cli-config.ts
|
|
532
|
+
function parsePort(value, fallback) {
|
|
533
|
+
if (!value) return fallback;
|
|
534
|
+
const port = Number(value);
|
|
535
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error(`Invalid PORT: ${value}`);
|
|
536
|
+
return port;
|
|
537
|
+
}
|
|
538
|
+
function parseDialect(value, databaseUrl) {
|
|
539
|
+
const explicit = value?.trim().toLowerCase();
|
|
540
|
+
if (!explicit) return inferDashboardDialect(databaseUrl);
|
|
541
|
+
if (explicit === "postgres" || explicit === "mysql" || explicit === "sqlite" || explicit === "mssql") return explicit;
|
|
542
|
+
throw new Error(`Invalid SCHEDULER_DIALECT: ${value}. Use postgres, mysql, sqlite, or mssql.`);
|
|
543
|
+
}
|
|
544
|
+
function parseDashboardCliEnv(env) {
|
|
545
|
+
const databaseUrl = (0, _azlib_std.asString)(env.DATABASE_URL)?.trim();
|
|
546
|
+
if (!databaseUrl) throw new Error("DATABASE_URL is required");
|
|
547
|
+
const host = (0, _azlib_std.asString)(env.HOST)?.trim() || "127.0.0.1";
|
|
548
|
+
const token = (0, _azlib_std.asString)(env.DASHBOARD_TOKEN)?.trim() || void 0;
|
|
549
|
+
assertDashboardBindSafety({
|
|
550
|
+
host,
|
|
551
|
+
token
|
|
552
|
+
});
|
|
553
|
+
return {
|
|
554
|
+
databaseUrl,
|
|
555
|
+
dialect: parseDialect(env.SCHEDULER_DIALECT, databaseUrl),
|
|
556
|
+
namespace: (0, _azlib_std.asString)(env.SCHEDULER_NAMESPACE)?.trim() || "azlib",
|
|
557
|
+
port: parsePort(env.PORT, 9100),
|
|
558
|
+
host,
|
|
559
|
+
token
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
//#endregion
|
|
563
|
+
Object.defineProperty(exports, "DashboardHttpError", {
|
|
564
|
+
enumerable: true,
|
|
565
|
+
get: function() {
|
|
566
|
+
return DashboardHttpError;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
Object.defineProperty(exports, "createDashboardPersistenceConfig", {
|
|
570
|
+
enumerable: true,
|
|
571
|
+
get: function() {
|
|
572
|
+
return createDashboardPersistenceConfig;
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
Object.defineProperty(exports, "createDashboardSqlDriver", {
|
|
576
|
+
enumerable: true,
|
|
577
|
+
get: function() {
|
|
578
|
+
return createDashboardSqlDriver;
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
Object.defineProperty(exports, "createSchedulerDashboardNodeHandler", {
|
|
582
|
+
enumerable: true,
|
|
583
|
+
get: function() {
|
|
584
|
+
return createSchedulerDashboardNodeHandler;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
Object.defineProperty(exports, "createSchedulerDashboardServer", {
|
|
588
|
+
enumerable: true,
|
|
589
|
+
get: function() {
|
|
590
|
+
return createSchedulerDashboardServer;
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
Object.defineProperty(exports, "handleSchedulerDashboardRequest", {
|
|
594
|
+
enumerable: true,
|
|
595
|
+
get: function() {
|
|
596
|
+
return handleSchedulerDashboardRequest;
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
Object.defineProperty(exports, "normalizeDashboardBasePath", {
|
|
600
|
+
enumerable: true,
|
|
601
|
+
get: function() {
|
|
602
|
+
return normalizeDashboardBasePath;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
Object.defineProperty(exports, "parseDashboardCliEnv", {
|
|
606
|
+
enumerable: true,
|
|
607
|
+
get: function() {
|
|
608
|
+
return parseDashboardCliEnv;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
Object.defineProperty(exports, "resolveDashboardApiBase", {
|
|
612
|
+
enumerable: true,
|
|
613
|
+
get: function() {
|
|
614
|
+
return resolveDashboardApiBase;
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
Object.defineProperty(exports, "resolveDashboardPublicDir", {
|
|
618
|
+
enumerable: true,
|
|
619
|
+
get: function() {
|
|
620
|
+
return resolveDashboardPublicDir;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
Object.defineProperty(exports, "stripDashboardBasePath", {
|
|
624
|
+
enumerable: true,
|
|
625
|
+
get: function() {
|
|
626
|
+
return stripDashboardBasePath;
|
|
627
|
+
}
|
|
628
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const require_cli_config = require("../cli-config-CFBl-YOh.cjs");
|
|
4
|
+
const require_store_dashboard = require("../store-dashboard-Sg4BBWiJ.cjs");
|
|
5
|
+
let node_url = require("node:url");
|
|
6
|
+
//#region src/dashboard/cli.ts
|
|
7
|
+
async function startSchedulerDashboardCli(env = process.env) {
|
|
8
|
+
const config = require_cli_config.parseDashboardCliEnv(env);
|
|
9
|
+
const server = require_cli_config.createSchedulerDashboardServer({
|
|
10
|
+
dashboard: require_store_dashboard.createSchedulerDashboardFromPersistence(require_cli_config.createDashboardPersistenceConfig({
|
|
11
|
+
driver: await require_cli_config.createDashboardSqlDriver(config.databaseUrl, config.dialect),
|
|
12
|
+
dialect: config.dialect,
|
|
13
|
+
namespace: config.namespace
|
|
14
|
+
})),
|
|
15
|
+
port: config.port,
|
|
16
|
+
host: config.host,
|
|
17
|
+
token: config.token,
|
|
18
|
+
publicDir: require_cli_config.resolveDashboardPublicDir(require("url").pathToFileURL(__filename).href)
|
|
19
|
+
});
|
|
20
|
+
await server.listen();
|
|
21
|
+
process.stderr.write(`@azlib/scheduler dashboard listening on http://${config.host}:${config.port}\n`);
|
|
22
|
+
return server;
|
|
23
|
+
}
|
|
24
|
+
if (Boolean(process.argv[1]) && require("url").pathToFileURL(__filename).href === (0, node_url.pathToFileURL)(process.argv[1]).href) startSchedulerDashboardCli().catch((error) => {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
process.stderr.write(`${message}\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
});
|
|
29
|
+
//#endregion
|
|
30
|
+
exports.startSchedulerDashboardCli = startSchedulerDashboardCli;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { t as SchedulerDashboardServer } from "../http-server-u1BnbkAK.cjs";
|
|
2
|
+
//#region src/dashboard/cli.d.ts
|
|
3
|
+
declare function startSchedulerDashboardCli(env?: Record<string, string | undefined>): Promise<SchedulerDashboardServer>;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { startSchedulerDashboardCli };
|
|
6
|
+
//# sourceMappingURL=cli.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.cts","names":[],"sources":["../../src/dashboard/cli.ts"],"mappings":";;iBAasB,2BACpB,MAAK,qCAAgD,QAAhD"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { t as SchedulerDashboardServer } from "../http-server-Cme4XKyH.mjs";
|
|
2
|
+
//#region src/dashboard/cli.d.ts
|
|
3
|
+
declare function startSchedulerDashboardCli(env?: Record<string, string | undefined>): Promise<SchedulerDashboardServer>;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { startSchedulerDashboardCli };
|
|
6
|
+
//# sourceMappingURL=cli.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.mts","names":[],"sources":["../../src/dashboard/cli.ts"],"mappings":";;iBAasB,2BACpB,MAAK,qCAAgD,QAAhD"}
|