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