@cronvello/sdk 0.1.0 → 0.1.2
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/CHANGELOG.md +44 -0
- package/README.md +162 -11
- package/dist/cli.cjs +877 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +874 -0
- package/dist/cli.js.map +1 -0
- package/dist/{dispatch-handler-Bnda1Ekq.d.cts → dispatch-handler-BNy-H5Nr.d.cts} +41 -2
- package/dist/{dispatch-handler-Bnda1Ekq.d.ts → dispatch-handler-BNy-H5Nr.d.ts} +41 -2
- package/dist/express.d.cts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/index.cjs +385 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -3
- package/dist/index.d.ts +108 -3
- package/dist/index.js +372 -41
- package/dist/index.js.map +1 -1
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/package.json +39 -9
package/dist/index.cjs
CHANGED
|
@@ -119,7 +119,7 @@ var Transport = class {
|
|
|
119
119
|
this.onRequest?.({ method: opts.method, path: opts.path, status: res.status, attempt, durationMs });
|
|
120
120
|
const text = await res.text();
|
|
121
121
|
const parsed = text ? safeJson(text) : null;
|
|
122
|
-
if (res.ok) return parsed;
|
|
122
|
+
if (res.ok) return unwrapEnvelope(parsed);
|
|
123
123
|
const retryAfter = parseRetryAfter(res, parsed);
|
|
124
124
|
if (RETRYABLE_STATUS.has(res.status) && attempt < this.maxRetries) {
|
|
125
125
|
await sleep(this.backoff(attempt, retryAfter));
|
|
@@ -184,6 +184,12 @@ function safeJson(text) {
|
|
|
184
184
|
return text;
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
+
function unwrapEnvelope(body) {
|
|
188
|
+
if (body !== null && typeof body === "object" && "success" in body && typeof body.success === "boolean" && "data" in body) {
|
|
189
|
+
return body.data;
|
|
190
|
+
}
|
|
191
|
+
return body;
|
|
192
|
+
}
|
|
187
193
|
function extractMessage(body) {
|
|
188
194
|
if (body && typeof body === "object" && "message" in body) {
|
|
189
195
|
const m = body.message;
|
|
@@ -366,6 +372,109 @@ function enc(segment) {
|
|
|
366
372
|
return encodeURIComponent(segment);
|
|
367
373
|
}
|
|
368
374
|
|
|
375
|
+
// src/internal/cron.ts
|
|
376
|
+
var MACROS = /* @__PURE__ */ new Set([
|
|
377
|
+
"@yearly",
|
|
378
|
+
"@annually",
|
|
379
|
+
"@monthly",
|
|
380
|
+
"@weekly",
|
|
381
|
+
"@daily",
|
|
382
|
+
"@midnight",
|
|
383
|
+
"@hourly",
|
|
384
|
+
"@reboot"
|
|
385
|
+
]);
|
|
386
|
+
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
387
|
+
var DOWS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
388
|
+
var FIVE = [
|
|
389
|
+
{ min: 0, max: 59, label: "minute" },
|
|
390
|
+
{ min: 0, max: 23, label: "hour" },
|
|
391
|
+
{ min: 1, max: 31, label: "day-of-month" },
|
|
392
|
+
{ min: 1, max: 12, names: MONTHS, label: "month" },
|
|
393
|
+
{ min: 0, max: 7, names: DOWS, label: "day-of-week" }
|
|
394
|
+
// 0 and 7 are both Sunday
|
|
395
|
+
];
|
|
396
|
+
var SECONDS = { min: 0, max: 59, label: "second" };
|
|
397
|
+
function validateCron(expr) {
|
|
398
|
+
if (typeof expr !== "string" || !expr.trim()) {
|
|
399
|
+
return { valid: false, error: "schedule is empty" };
|
|
400
|
+
}
|
|
401
|
+
const trimmed = expr.trim();
|
|
402
|
+
if (trimmed.startsWith("@")) {
|
|
403
|
+
return MACROS.has(trimmed.toLowerCase()) ? { valid: true } : { valid: false, error: `unknown cron macro "${trimmed}" (try @daily, @hourly, \u2026)` };
|
|
404
|
+
}
|
|
405
|
+
const parts = trimmed.split(/\s+/);
|
|
406
|
+
if (parts.length !== 5 && parts.length !== 6) {
|
|
407
|
+
return {
|
|
408
|
+
valid: false,
|
|
409
|
+
error: `expected 5 fields (min hour dom month dow) or 6 with seconds, got ${parts.length}: "${trimmed}"`
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const specs = parts.length === 6 ? [SECONDS, ...FIVE] : FIVE;
|
|
413
|
+
for (let i = 0; i < parts.length; i++) {
|
|
414
|
+
const err = validateField(parts[i], specs[i]);
|
|
415
|
+
if (err) return { valid: false, error: `invalid ${specs[i].label} "${parts[i]}": ${err}` };
|
|
416
|
+
}
|
|
417
|
+
return { valid: true };
|
|
418
|
+
}
|
|
419
|
+
function validateField(field2, spec) {
|
|
420
|
+
for (const term of field2.split(",")) {
|
|
421
|
+
const err = validateTerm(term, spec);
|
|
422
|
+
if (err) return err;
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
function validateTerm(term, spec) {
|
|
427
|
+
if (term === "") return "empty list item";
|
|
428
|
+
let step;
|
|
429
|
+
let base = term;
|
|
430
|
+
const slash = term.indexOf("/");
|
|
431
|
+
if (slash >= 0) {
|
|
432
|
+
base = term.slice(0, slash);
|
|
433
|
+
step = term.slice(slash + 1);
|
|
434
|
+
if (!/^\d+$/.test(step) || Number(step) === 0) return `step must be a positive integer`;
|
|
435
|
+
}
|
|
436
|
+
if (base === "*") return null;
|
|
437
|
+
const dash = base.indexOf("-");
|
|
438
|
+
if (dash > 0) {
|
|
439
|
+
const lo = resolveValue(base.slice(0, dash), spec);
|
|
440
|
+
const hi = resolveValue(base.slice(dash + 1), spec);
|
|
441
|
+
if (lo === null) return `"${base.slice(0, dash)}" is out of range ${spec.min}-${spec.max}`;
|
|
442
|
+
if (hi === null) return `"${base.slice(dash + 1)}" is out of range ${spec.min}-${spec.max}`;
|
|
443
|
+
if (lo > hi) return `range start ${lo} is greater than end ${hi}`;
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
if (step !== void 0 && base !== "*") {
|
|
447
|
+
return resolveValue(base, spec) === null ? `"${base}" is out of range ${spec.min}-${spec.max}` : null;
|
|
448
|
+
}
|
|
449
|
+
return resolveValue(base, spec) === null ? `"${base}" is out of range ${spec.min}-${spec.max}` : null;
|
|
450
|
+
}
|
|
451
|
+
function resolveValue(token, spec) {
|
|
452
|
+
if (/^\d+$/.test(token)) {
|
|
453
|
+
const n = Number(token);
|
|
454
|
+
return n >= spec.min && n <= spec.max ? n : null;
|
|
455
|
+
}
|
|
456
|
+
if (spec.names) {
|
|
457
|
+
const idx = spec.names.indexOf(token.toLowerCase());
|
|
458
|
+
if (idx >= 0) return idx + (spec.label === "month" ? 1 : 0);
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
function assertValidCron(expr, context = "") {
|
|
463
|
+
const res = validateCron(expr);
|
|
464
|
+
if (!res.valid) {
|
|
465
|
+
throw new Error(`${context ? `${context}: ` : ""}invalid cron schedule \u2014 ${res.error}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function isValidTimeZone(tz) {
|
|
469
|
+
if (!tz || typeof tz !== "string") return false;
|
|
470
|
+
try {
|
|
471
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
472
|
+
return true;
|
|
473
|
+
} catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
369
478
|
// src/registry/verify.ts
|
|
370
479
|
var encoder = new TextEncoder();
|
|
371
480
|
function parseBearer(headerValue2) {
|
|
@@ -397,17 +506,54 @@ async function signHmacSha256(payload, secret) {
|
|
|
397
506
|
}
|
|
398
507
|
|
|
399
508
|
// src/registry/dispatch.ts
|
|
509
|
+
var NOOP_LOGGER = {};
|
|
400
510
|
function createDispatcher(state) {
|
|
401
|
-
const log = state.logger;
|
|
511
|
+
const log = state.logger ?? NOOP_LOGGER;
|
|
512
|
+
const hooks = state.hooks;
|
|
513
|
+
const maxBodyBytes = state.maxBodyBytes ?? 1048576;
|
|
514
|
+
async function invokeHandler(job, ctx) {
|
|
515
|
+
await safeHook(() => hooks?.onJobStart?.({ key: job.key, source: ctx.source, isAsync: ctx.isAsync }), log);
|
|
516
|
+
const startedAt = Date.now();
|
|
517
|
+
try {
|
|
518
|
+
const result = await job.config.handler(ctx);
|
|
519
|
+
const durationMs = Date.now() - startedAt;
|
|
520
|
+
log.debug?.(`[cronvello] job '${job.key}' completed in ${durationMs}ms`);
|
|
521
|
+
await safeHook(() => hooks?.onJobSuccess?.({ key: job.key, source: ctx.source, durationMs, result }), log);
|
|
522
|
+
return { durationMs, result };
|
|
523
|
+
} catch (err) {
|
|
524
|
+
const durationMs = Date.now() - startedAt;
|
|
525
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
526
|
+
log.error?.(`[cronvello] job '${job.key}' failed: ${error.message}`, { error: error.message });
|
|
527
|
+
await safeHook(() => hooks?.onJobError?.({ key: job.key, source: ctx.source, durationMs, error }), log);
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function buildContext(key, job, req, body, isAsync, source) {
|
|
532
|
+
return {
|
|
533
|
+
key,
|
|
534
|
+
schedule: typeof body["schedule"] === "string" ? body["schedule"] : job.config.schedule ?? "",
|
|
535
|
+
payload: job.config.payload ?? {},
|
|
536
|
+
body,
|
|
537
|
+
headers: req?.headers ?? {},
|
|
538
|
+
isAsync,
|
|
539
|
+
source,
|
|
540
|
+
logger: log,
|
|
541
|
+
signal: req?.signal
|
|
542
|
+
};
|
|
543
|
+
}
|
|
402
544
|
async function handle(req) {
|
|
403
545
|
if (req.method.toUpperCase() !== "POST") {
|
|
404
546
|
return resp(405, { ok: false, error: "Method not allowed" });
|
|
405
547
|
}
|
|
406
548
|
const token = parseBearer(req.authorization);
|
|
407
549
|
if (!token || !timingSafeEqual(token, state.dispatchSecret)) {
|
|
408
|
-
log
|
|
550
|
+
log.warn?.("[cronvello] dispatch rejected: bad or missing bearer token");
|
|
409
551
|
return resp(401, { ok: false, error: "Unauthorized" });
|
|
410
552
|
}
|
|
553
|
+
if (byteLength(req.rawBody) > maxBodyBytes) {
|
|
554
|
+
log.warn?.(`[cronvello] dispatch rejected: body exceeds ${maxBodyBytes} bytes`);
|
|
555
|
+
return resp(413, { ok: false, error: "Payload too large" });
|
|
556
|
+
}
|
|
411
557
|
let body;
|
|
412
558
|
try {
|
|
413
559
|
const parsed = req.rawBody ? JSON.parse(req.rawBody) : {};
|
|
@@ -424,54 +570,44 @@ function createDispatcher(state) {
|
|
|
424
570
|
}
|
|
425
571
|
const job = state.jobs.get(key);
|
|
426
572
|
if (!job) {
|
|
427
|
-
log
|
|
573
|
+
log.warn?.(`[cronvello] dispatch for unknown job '${key}'`);
|
|
428
574
|
return resp(404, { ok: false, error: `Unknown job: ${key}` });
|
|
429
575
|
}
|
|
430
576
|
const callback = readCallback(body["_callback"]);
|
|
431
577
|
const isAsync = callback !== null;
|
|
432
|
-
const ctx =
|
|
433
|
-
key,
|
|
434
|
-
schedule: typeof body["schedule"] === "string" ? body["schedule"] : "",
|
|
435
|
-
payload: job.config.payload ?? {},
|
|
436
|
-
body,
|
|
437
|
-
headers: req.headers,
|
|
438
|
-
isAsync,
|
|
439
|
-
signal: req.signal
|
|
440
|
-
};
|
|
578
|
+
const ctx = buildContext(key, job, req, body, isAsync, "dispatch");
|
|
441
579
|
if (callback) {
|
|
442
|
-
const work = runAndReportCallback(job, ctx, callback, state.dispatchSecret, log);
|
|
580
|
+
const work = runAndReportCallback(job, ctx, callback, state.dispatchSecret, log, invokeHandler);
|
|
443
581
|
if (req.waitUntil) req.waitUntil(work);
|
|
444
582
|
return resp(202, { ok: true, job: key, accepted: true });
|
|
445
583
|
}
|
|
446
|
-
const startedAt = Date.now();
|
|
447
584
|
try {
|
|
448
|
-
const result = await job
|
|
449
|
-
log?.debug?.(`[cronvello] job '${key}' completed in ${Date.now() - startedAt}ms`);
|
|
585
|
+
const { result } = await invokeHandler(job, ctx);
|
|
450
586
|
return resp(200, { ok: true, job: key, result: result ?? null });
|
|
451
587
|
} catch (err) {
|
|
452
|
-
|
|
453
|
-
log?.error?.(`[cronvello] job '${key}' failed: ${message}`, { error: message });
|
|
454
|
-
return resp(500, { ok: false, job: key, error: message });
|
|
588
|
+
return resp(500, { ok: false, job: key, error: err instanceof Error ? err.message : String(err) });
|
|
455
589
|
}
|
|
456
590
|
}
|
|
457
|
-
|
|
591
|
+
async function runLocal(key, payload) {
|
|
592
|
+
const job = state.jobs.get(key);
|
|
593
|
+
if (!job) throw new Error(`Unknown job '${key}'. Known: ${[...state.jobs.keys()].join(", ") || "(none)"}`);
|
|
594
|
+
const body = { job: key, schedule: job.config.schedule, ...payload ?? {} };
|
|
595
|
+
const ctx = buildContext(key, job, null, body, false, "local");
|
|
596
|
+
const { result } = await invokeHandler(job, ctx);
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
return { handle, runLocal };
|
|
458
600
|
}
|
|
459
|
-
async function runAndReportCallback(job, ctx, callback, secret, log) {
|
|
460
|
-
const startedAt = Date.now();
|
|
601
|
+
async function runAndReportCallback(job, ctx, callback, secret, log, invokeHandler) {
|
|
461
602
|
let payload;
|
|
462
603
|
try {
|
|
463
|
-
const result = await job
|
|
464
|
-
payload = {
|
|
465
|
-
runId: callback.runId,
|
|
466
|
-
success: true,
|
|
467
|
-
durationMs: Date.now() - startedAt,
|
|
468
|
-
result: result ?? null
|
|
469
|
-
};
|
|
604
|
+
const { durationMs, result } = await invokeHandler(job, ctx);
|
|
605
|
+
payload = { runId: callback.runId, success: true, durationMs, result: result ?? null };
|
|
470
606
|
} catch (err) {
|
|
471
607
|
payload = {
|
|
472
608
|
runId: callback.runId,
|
|
473
609
|
success: false,
|
|
474
|
-
durationMs:
|
|
610
|
+
durationMs: 0,
|
|
475
611
|
error: err instanceof Error ? err.message : String(err)
|
|
476
612
|
};
|
|
477
613
|
}
|
|
@@ -485,14 +621,26 @@ async function runAndReportCallback(job, ctx, callback, secret, log) {
|
|
|
485
621
|
body: bodyStr
|
|
486
622
|
});
|
|
487
623
|
if (!res.ok) {
|
|
488
|
-
log
|
|
624
|
+
log.warn?.(`[cronvello] callback POST for job '${job.key}' returned ${res.status}`);
|
|
489
625
|
}
|
|
490
626
|
} catch (err) {
|
|
491
|
-
log
|
|
627
|
+
log.error?.(`[cronvello] failed to deliver callback for job '${job.key}'`, {
|
|
492
628
|
error: err instanceof Error ? err.message : String(err)
|
|
493
629
|
});
|
|
494
630
|
}
|
|
495
631
|
}
|
|
632
|
+
async function safeHook(fn, log) {
|
|
633
|
+
try {
|
|
634
|
+
await fn();
|
|
635
|
+
} catch (err) {
|
|
636
|
+
log.warn?.(`[cronvello] a lifecycle hook threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function byteLength(s) {
|
|
640
|
+
if (!s) return 0;
|
|
641
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
|
|
642
|
+
return s.length;
|
|
643
|
+
}
|
|
496
644
|
function readCallback(value) {
|
|
497
645
|
if (!value || typeof value !== "object") return null;
|
|
498
646
|
const v = value;
|
|
@@ -602,9 +750,9 @@ function buildDesiredTask(args) {
|
|
|
602
750
|
function diffTask(existing, desired, opts) {
|
|
603
751
|
const patch = {};
|
|
604
752
|
const changed = [];
|
|
605
|
-
const set = (
|
|
606
|
-
patch[
|
|
607
|
-
changed.push(
|
|
753
|
+
const set = (field2, value) => {
|
|
754
|
+
patch[field2] = value;
|
|
755
|
+
changed.push(field2);
|
|
608
756
|
};
|
|
609
757
|
if (existing.schedule !== desired.schedule) set("schedule", desired.schedule);
|
|
610
758
|
if (desired.timeZone !== void 0 && existing.timeZone !== desired.timeZone) set("timeZone", desired.timeZone);
|
|
@@ -736,10 +884,14 @@ var DEFAULT_DISPATCH_PATH = "/cronvello/dispatch";
|
|
|
736
884
|
var DEFAULT_TIME_ZONE = "Europe/Berlin";
|
|
737
885
|
function defineCronvello(config) {
|
|
738
886
|
validateConfig(config);
|
|
739
|
-
const
|
|
887
|
+
const validate = config.validateSchedules ?? true;
|
|
888
|
+
const defaultTimeZone = config.timeZone ?? DEFAULT_TIME_ZONE;
|
|
889
|
+
if (validate && !isValidTimeZone(defaultTimeZone)) {
|
|
890
|
+
throw new CronvelloConfigError(`Invalid \`timeZone\` "${defaultTimeZone}" \u2014 expected an IANA name like "Europe/Berlin" or "UTC".`);
|
|
891
|
+
}
|
|
892
|
+
const jobs = normalizeJobs(config.jobs, validate);
|
|
740
893
|
const dispatchPath = normalizePath(config.dispatchPath ?? DEFAULT_DISPATCH_PATH);
|
|
741
894
|
const dispatchUrl = joinUrl(config.appUrl, dispatchPath);
|
|
742
|
-
const defaultTimeZone = config.timeZone ?? DEFAULT_TIME_ZONE;
|
|
743
895
|
const client = new CronvelloClient({
|
|
744
896
|
apiKey: config.apiKey,
|
|
745
897
|
baseUrl: config.baseUrl ?? CRONVELLO_DEFAULT_BASE_URL,
|
|
@@ -750,7 +902,9 @@ function defineCronvello(config) {
|
|
|
750
902
|
const dispatcher = createDispatcher({
|
|
751
903
|
jobs,
|
|
752
904
|
dispatchSecret: config.dispatchSecret,
|
|
753
|
-
logger: config.logger
|
|
905
|
+
logger: config.logger,
|
|
906
|
+
...config.hooks ? { hooks: config.hooks } : {},
|
|
907
|
+
...config.maxBodyBytes !== void 0 ? { maxBodyBytes: config.maxBodyBytes } : {}
|
|
754
908
|
});
|
|
755
909
|
const app = {
|
|
756
910
|
client,
|
|
@@ -814,6 +968,7 @@ function defineCronvello(config) {
|
|
|
814
968
|
}
|
|
815
969
|
return client.tasks.runNow(task.id);
|
|
816
970
|
},
|
|
971
|
+
trigger: (key, payload) => dispatcher.runLocal(key, payload),
|
|
817
972
|
handle: dispatcher.handle,
|
|
818
973
|
expressHandler: () => expressHandler(app),
|
|
819
974
|
nextHandler: () => nextHandler(app),
|
|
@@ -821,6 +976,30 @@ function defineCronvello(config) {
|
|
|
821
976
|
};
|
|
822
977
|
return app;
|
|
823
978
|
}
|
|
979
|
+
((defineCronvello2) => {
|
|
980
|
+
function fromEnv(config, env = typeof process !== "undefined" ? process.env : {}) {
|
|
981
|
+
const apiKey = config.apiKey ?? env["CRONVELLO_API_KEY"];
|
|
982
|
+
const dispatchSecret = config.dispatchSecret ?? env["CRONVELLO_DISPATCH_SECRET"];
|
|
983
|
+
const appUrl = config.appUrl ?? env["CRONVELLO_APP_URL"] ?? env["PUBLIC_URL"];
|
|
984
|
+
const baseUrl = config.baseUrl ?? env["CRONVELLO_API_URL"];
|
|
985
|
+
const missing = [
|
|
986
|
+
!apiKey && "CRONVELLO_API_KEY",
|
|
987
|
+
!dispatchSecret && "CRONVELLO_DISPATCH_SECRET",
|
|
988
|
+
!appUrl && "CRONVELLO_APP_URL (or PUBLIC_URL)"
|
|
989
|
+
].filter(Boolean);
|
|
990
|
+
if (missing.length) {
|
|
991
|
+
throw new CronvelloConfigError(`defineCronvello.fromEnv() is missing required env: ${missing.join(", ")}.`);
|
|
992
|
+
}
|
|
993
|
+
return defineCronvello2({
|
|
994
|
+
...config,
|
|
995
|
+
apiKey,
|
|
996
|
+
dispatchSecret,
|
|
997
|
+
appUrl,
|
|
998
|
+
...baseUrl ? { baseUrl } : {}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
defineCronvello2.fromEnv = fromEnv;
|
|
1002
|
+
})(defineCronvello || (defineCronvello = {}));
|
|
824
1003
|
function validateConfig(config) {
|
|
825
1004
|
if (!config) throw new CronvelloConfigError("defineCronvello requires a config object.");
|
|
826
1005
|
if (!config.appName || !config.appName.trim()) throw new CronvelloConfigError("`appName` is required.");
|
|
@@ -833,7 +1012,7 @@ function validateConfig(config) {
|
|
|
833
1012
|
}
|
|
834
1013
|
if (!config.jobs) throw new CronvelloConfigError("`jobs` is required.");
|
|
835
1014
|
}
|
|
836
|
-
function normalizeJobs(input) {
|
|
1015
|
+
function normalizeJobs(input, validate) {
|
|
837
1016
|
const map = /* @__PURE__ */ new Map();
|
|
838
1017
|
const entries = Array.isArray(input) ? input.map(({ key, ...rest }) => ({ key, config: rest })) : Object.entries(input).map(([key, config]) => ({ key, config }));
|
|
839
1018
|
for (const { key, config } of entries) {
|
|
@@ -847,6 +1026,15 @@ function normalizeJobs(input) {
|
|
|
847
1026
|
if (!config.schedule || !config.schedule.trim()) {
|
|
848
1027
|
throw new CronvelloConfigError(`Job '${trimmed}' is missing a schedule.`);
|
|
849
1028
|
}
|
|
1029
|
+
if (validate) {
|
|
1030
|
+
const cronCheck = validateCron(config.schedule);
|
|
1031
|
+
if (!cronCheck.valid) {
|
|
1032
|
+
throw new CronvelloConfigError(`Job '${trimmed}' has an invalid schedule "${config.schedule}" \u2014 ${cronCheck.error}.`);
|
|
1033
|
+
}
|
|
1034
|
+
if (config.timeZone !== void 0 && !isValidTimeZone(config.timeZone)) {
|
|
1035
|
+
throw new CronvelloConfigError(`Job '${trimmed}' has an invalid timeZone "${config.timeZone}" \u2014 expected an IANA name.`);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
850
1038
|
map.set(trimmed, { key: trimmed, config });
|
|
851
1039
|
}
|
|
852
1040
|
if (map.size === 0) throw new CronvelloConfigError("At least one job is required.");
|
|
@@ -889,6 +1077,149 @@ function buildRegistryRequest(config, dispatchUrl, defaultTimeZone, jobs, opts)
|
|
|
889
1077
|
};
|
|
890
1078
|
}
|
|
891
1079
|
|
|
1080
|
+
// src/registry/format.ts
|
|
1081
|
+
var ESC = String.fromCharCode(27);
|
|
1082
|
+
var ANSI = {
|
|
1083
|
+
reset: `${ESC}[0m`,
|
|
1084
|
+
dim: `${ESC}[2m`,
|
|
1085
|
+
bold: `${ESC}[1m`,
|
|
1086
|
+
green: `${ESC}[32m`,
|
|
1087
|
+
yellow: `${ESC}[33m`,
|
|
1088
|
+
red: `${ESC}[31m`,
|
|
1089
|
+
cyan: `${ESC}[36m`,
|
|
1090
|
+
gray: `${ESC}[90m`
|
|
1091
|
+
};
|
|
1092
|
+
var GLYPH = {
|
|
1093
|
+
created: { sign: "+", color: "green" },
|
|
1094
|
+
updated: { sign: "~", color: "yellow" },
|
|
1095
|
+
unchanged: { sign: "=", color: "gray" },
|
|
1096
|
+
deleted: { sign: "-", color: "red" },
|
|
1097
|
+
skipped: { sign: "\xB7", color: "cyan" }
|
|
1098
|
+
};
|
|
1099
|
+
function formatSyncResult(result, options = {}) {
|
|
1100
|
+
const paint = (s, c) => options.color ? `${ANSI[c]}${s}${ANSI.reset}` : s;
|
|
1101
|
+
const verb = options.dryRun ? "would sync" : "synced";
|
|
1102
|
+
const lines = [];
|
|
1103
|
+
lines.push(
|
|
1104
|
+
`${paint("Cronvello", "cyan")} ${verb} ${paint(`"${result.jobName}"`, "bold")} ${paint(`(${result.jobId})`, "gray")}`
|
|
1105
|
+
);
|
|
1106
|
+
const order = ["created", "updated", "deleted", "skipped", "unchanged"];
|
|
1107
|
+
const sorted = [...result.changes].sort((a, b) => order.indexOf(a.action) - order.indexOf(b.action));
|
|
1108
|
+
for (const c of sorted) {
|
|
1109
|
+
const g = GLYPH[c.action];
|
|
1110
|
+
const detail = c.changedFields && c.changedFields.length ? paint(` (${c.changedFields.join(", ")})`, "gray") : c.reason ? paint(` (${c.reason})`, "gray") : "";
|
|
1111
|
+
lines.push(` ${paint(g.sign, g.color)} ${paint(c.action.padEnd(9), g.color)} ${c.key}${detail}`);
|
|
1112
|
+
}
|
|
1113
|
+
const tally = [
|
|
1114
|
+
result.created && `${result.created} created`,
|
|
1115
|
+
result.updated && `${result.updated} updated`,
|
|
1116
|
+
result.unchanged && `${result.unchanged} unchanged`,
|
|
1117
|
+
result.deleted && `${result.deleted} deleted`,
|
|
1118
|
+
result.skipped && `${result.skipped} skipped`
|
|
1119
|
+
].filter(Boolean);
|
|
1120
|
+
const summary = tally.length ? tally.join(", ") : "no changes";
|
|
1121
|
+
const containerNote = result.jobCreated ? paint(" \xB7 container created", "gray") : "";
|
|
1122
|
+
lines.push(` ${paint(summary, "bold")}${containerNote}`);
|
|
1123
|
+
return lines.join("\n");
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/schedule/index.ts
|
|
1127
|
+
var DOW = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
|
|
1128
|
+
function cron(expression) {
|
|
1129
|
+
assertValidCron(expression, "cron()");
|
|
1130
|
+
return expression;
|
|
1131
|
+
}
|
|
1132
|
+
function every(interval) {
|
|
1133
|
+
const m = /^(\d+)\s*(s|sec|secs|m|min|mins|h|hr|hrs|hour|hours|d|day|days)$/i.exec(interval.trim());
|
|
1134
|
+
if (!m) throw new Error(`every(): cannot parse interval "${interval}" \u2014 use forms like "30s", "15m", "2h", "1d"`);
|
|
1135
|
+
const n = Number(m[1]);
|
|
1136
|
+
const unit = m[2].toLowerCase()[0];
|
|
1137
|
+
if (n < 1) throw new Error(`every(): interval must be at least 1, got ${n}`);
|
|
1138
|
+
let expr;
|
|
1139
|
+
if (unit === "s") {
|
|
1140
|
+
if (n > 59) throw new Error(`every(): seconds must be 1\u201359, got ${n}`);
|
|
1141
|
+
expr = `*/${n} * * * * *`;
|
|
1142
|
+
} else if (unit === "m") {
|
|
1143
|
+
if (n > 59) throw new Error(`every(): minutes must be 1\u201359 (use "1h" for 60), got ${n}`);
|
|
1144
|
+
expr = `*/${n} * * * *`;
|
|
1145
|
+
} else if (unit === "h") {
|
|
1146
|
+
if (n > 23) throw new Error(`every(): hours must be 1\u201323 (use "1d" for 24), got ${n}`);
|
|
1147
|
+
expr = `0 */${n} * * *`;
|
|
1148
|
+
} else {
|
|
1149
|
+
if (n > 31) throw new Error(`every(): days must be 1\u201331, got ${n}`);
|
|
1150
|
+
expr = n === 1 ? `0 0 * * *` : `0 0 */${n} * *`;
|
|
1151
|
+
}
|
|
1152
|
+
assertValidCron(expr, `every("${interval}")`);
|
|
1153
|
+
return expr;
|
|
1154
|
+
}
|
|
1155
|
+
function everyMinutes(n) {
|
|
1156
|
+
return every(`${n}m`);
|
|
1157
|
+
}
|
|
1158
|
+
function everyHours(n) {
|
|
1159
|
+
return every(`${n}h`);
|
|
1160
|
+
}
|
|
1161
|
+
function hourly(minute = 0) {
|
|
1162
|
+
const mm = field(minute, 0, 59, "minute");
|
|
1163
|
+
return cron(`${mm} * * * *`);
|
|
1164
|
+
}
|
|
1165
|
+
function daily(time = "00:00") {
|
|
1166
|
+
const { hh, mm } = parseTime(time);
|
|
1167
|
+
return cron(`${mm} ${hh} * * *`);
|
|
1168
|
+
}
|
|
1169
|
+
function weekly(day, time = "00:00") {
|
|
1170
|
+
const { hh, mm } = parseTime(time);
|
|
1171
|
+
return cron(`${mm} ${hh} * * ${weekday(day)}`);
|
|
1172
|
+
}
|
|
1173
|
+
function monthly(dayOfMonth, time = "00:00") {
|
|
1174
|
+
const { hh, mm } = parseTime(time);
|
|
1175
|
+
const dom = field(dayOfMonth, 1, 31, "day-of-month");
|
|
1176
|
+
return cron(`${mm} ${hh} ${dom} * *`);
|
|
1177
|
+
}
|
|
1178
|
+
function weekdays(time = "00:00") {
|
|
1179
|
+
const { hh, mm } = parseTime(time);
|
|
1180
|
+
return cron(`${mm} ${hh} * * 1-5`);
|
|
1181
|
+
}
|
|
1182
|
+
function weekends(time = "00:00") {
|
|
1183
|
+
const { hh, mm } = parseTime(time);
|
|
1184
|
+
return cron(`${mm} ${hh} * * 0,6`);
|
|
1185
|
+
}
|
|
1186
|
+
var schedule = {
|
|
1187
|
+
cron,
|
|
1188
|
+
every,
|
|
1189
|
+
everyMinutes,
|
|
1190
|
+
everyHours,
|
|
1191
|
+
hourly,
|
|
1192
|
+
daily,
|
|
1193
|
+
weekly,
|
|
1194
|
+
monthly,
|
|
1195
|
+
weekdays,
|
|
1196
|
+
weekends
|
|
1197
|
+
};
|
|
1198
|
+
function parseTime(time) {
|
|
1199
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
|
|
1200
|
+
if (!m) throw new Error(`Expected a time like "08:00" or "23:30", got "${time}"`);
|
|
1201
|
+
const hh = Number(m[1]);
|
|
1202
|
+
const mm = Number(m[2]);
|
|
1203
|
+
if (hh > 23) throw new Error(`Hour must be 0\u201323, got ${hh}`);
|
|
1204
|
+
if (mm > 59) throw new Error(`Minute must be 0\u201359, got ${mm}`);
|
|
1205
|
+
return { hh, mm };
|
|
1206
|
+
}
|
|
1207
|
+
function weekday(day) {
|
|
1208
|
+
if (typeof day === "number") {
|
|
1209
|
+
if (day < 0 || day > 6) throw new Error(`Weekday number must be 0 (Sun) \u2013 6 (Sat), got ${day}`);
|
|
1210
|
+
return day;
|
|
1211
|
+
}
|
|
1212
|
+
const n = DOW[day.toLowerCase()];
|
|
1213
|
+
if (n === void 0) throw new Error(`Unknown weekday "${day}" \u2014 use sun\u2026sat or 0\u20266`);
|
|
1214
|
+
return n;
|
|
1215
|
+
}
|
|
1216
|
+
function field(value, min, max, label) {
|
|
1217
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
1218
|
+
throw new Error(`${label} must be an integer ${min}\u2013${max}, got ${value}`);
|
|
1219
|
+
}
|
|
1220
|
+
return value;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
892
1223
|
// src/index.ts
|
|
893
1224
|
function generateDispatchSecret(bytes = 32) {
|
|
894
1225
|
const buf = new Uint8Array(bytes);
|
|
@@ -902,7 +1233,21 @@ exports.CronvelloClient = CronvelloClient;
|
|
|
902
1233
|
exports.CronvelloConfigError = CronvelloConfigError;
|
|
903
1234
|
exports.CronvelloError = CronvelloError;
|
|
904
1235
|
exports.CronvelloNetworkError = CronvelloNetworkError;
|
|
1236
|
+
exports.cron = cron;
|
|
1237
|
+
exports.daily = daily;
|
|
905
1238
|
exports.defineCronvello = defineCronvello;
|
|
1239
|
+
exports.every = every;
|
|
1240
|
+
exports.everyHours = everyHours;
|
|
1241
|
+
exports.everyMinutes = everyMinutes;
|
|
1242
|
+
exports.formatSyncResult = formatSyncResult;
|
|
906
1243
|
exports.generateDispatchSecret = generateDispatchSecret;
|
|
1244
|
+
exports.hourly = hourly;
|
|
1245
|
+
exports.isValidTimeZone = isValidTimeZone;
|
|
1246
|
+
exports.monthly = monthly;
|
|
1247
|
+
exports.schedule = schedule;
|
|
1248
|
+
exports.validateCron = validateCron;
|
|
1249
|
+
exports.weekdays = weekdays;
|
|
1250
|
+
exports.weekends = weekends;
|
|
1251
|
+
exports.weekly = weekly;
|
|
907
1252
|
//# sourceMappingURL=index.cjs.map
|
|
908
1253
|
//# sourceMappingURL=index.cjs.map
|