@infuro/cms-core 1.0.28 → 1.0.29

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/api.cjs CHANGED
@@ -213,6 +213,81 @@ var init_email_queue = __esm({
213
213
  }
214
214
  });
215
215
 
216
+ // src/plugins/pg-boss/pg-boss-service.ts
217
+ var pg_boss_service_exports = {};
218
+ __export(pg_boss_service_exports, {
219
+ JOB_RUNNER_QUEUE: () => JOB_RUNNER_QUEUE,
220
+ PgBossService: () => PgBossService
221
+ });
222
+ var import_pg_boss, JOB_RUNNER_QUEUE, PgBossService;
223
+ var init_pg_boss_service = __esm({
224
+ "src/plugins/pg-boss/pg-boss-service.ts"() {
225
+ "use strict";
226
+ import_pg_boss = __toESM(require("pg-boss"), 1);
227
+ JOB_RUNNER_QUEUE = "job-runner";
228
+ PgBossService = class {
229
+ boss;
230
+ started = false;
231
+ workRegistered = /* @__PURE__ */ new Set();
232
+ constructor(connectionString, schema) {
233
+ this.boss = new import_pg_boss.default({
234
+ connectionString,
235
+ ...schema?.trim() ? { schema: schema.trim() } : {},
236
+ schedule: true
237
+ });
238
+ this.boss.on("error", (err) => {
239
+ console.error("[pg-boss]", err);
240
+ });
241
+ }
242
+ async start() {
243
+ if (this.started) return;
244
+ await this.boss.start();
245
+ await this.ensureQueue(JOB_RUNNER_QUEUE);
246
+ this.started = true;
247
+ }
248
+ async stop() {
249
+ if (!this.started) return;
250
+ await this.boss.stop({ graceful: true, timeout: 1e4 });
251
+ this.started = false;
252
+ this.workRegistered.clear();
253
+ }
254
+ get raw() {
255
+ return this.boss;
256
+ }
257
+ async ensureQueue(name) {
258
+ const existing = await this.boss.getQueue(name);
259
+ if (!existing) {
260
+ await this.boss.createQueue(name);
261
+ }
262
+ }
263
+ async send(queue, data) {
264
+ await this.ensureQueue(queue);
265
+ return this.boss.send(queue, data, { retryLimit: 2 });
266
+ }
267
+ async schedule(name, cron, data, options) {
268
+ await this.ensureQueue(name);
269
+ await this.boss.schedule(name, cron, data, options);
270
+ }
271
+ async unschedule(name) {
272
+ try {
273
+ await this.boss.unschedule(name);
274
+ } catch {
275
+ }
276
+ }
277
+ async registerWork(queue, handler) {
278
+ if (this.workRegistered.has(queue)) return;
279
+ await this.ensureQueue(queue);
280
+ await this.boss.work(queue, async (jobs) => {
281
+ for (const job of jobs) {
282
+ await handler(job.data);
283
+ }
284
+ });
285
+ this.workRegistered.add(queue);
286
+ }
287
+ };
288
+ }
289
+ });
290
+
216
291
  // src/plugins/erp/erp-response-map.ts
217
292
  function pickString(o, keys) {
218
293
  for (const k of keys) {
@@ -508,6 +583,7 @@ __export(api_exports, {
508
583
  createForgotPasswordHandler: () => createForgotPasswordHandler,
509
584
  createFormBySlugHandler: () => createFormBySlugHandler,
510
585
  createInviteAcceptHandler: () => createInviteAcceptHandler,
586
+ createJobScheduleHandlers: () => createJobScheduleHandlers,
511
587
  createLlmAgentKnowledgeHandlers: () => createLlmAgentKnowledgeHandlers,
512
588
  createMediaZipExtractHandler: () => createMediaZipExtractHandler,
513
589
  createSetPasswordHandler: () => createSetPasswordHandler,
@@ -7355,6 +7431,145 @@ RssFeed = __decorateClass([
7355
7431
  (0, import_typeorm47.Entity)("rss_feeds")
7356
7432
  ], RssFeed);
7357
7433
 
7434
+ // src/entities/job-schedule.entity.ts
7435
+ var import_typeorm49 = require("typeorm");
7436
+
7437
+ // src/entities/job-schedule-run.entity.ts
7438
+ var import_typeorm48 = require("typeorm");
7439
+ var JobScheduleRun = class {
7440
+ id;
7441
+ scheduleId;
7442
+ schedule;
7443
+ startedAt;
7444
+ finishedAt;
7445
+ status;
7446
+ error;
7447
+ result;
7448
+ triggeredBy;
7449
+ createdAt;
7450
+ };
7451
+ __decorateClass([
7452
+ (0, import_typeorm48.PrimaryGeneratedColumn)("uuid")
7453
+ ], JobScheduleRun.prototype, "id", 2);
7454
+ __decorateClass([
7455
+ (0, import_typeorm48.Column)({ type: "uuid" })
7456
+ ], JobScheduleRun.prototype, "scheduleId", 2);
7457
+ __decorateClass([
7458
+ (0, import_typeorm48.ManyToOne)(() => JobSchedule, (s) => s.runs, { onDelete: "CASCADE" }),
7459
+ (0, import_typeorm48.JoinColumn)({ name: "scheduleId" })
7460
+ ], JobScheduleRun.prototype, "schedule", 2);
7461
+ __decorateClass([
7462
+ (0, import_typeorm48.Column)({ type: "timestamp" })
7463
+ ], JobScheduleRun.prototype, "startedAt", 2);
7464
+ __decorateClass([
7465
+ (0, import_typeorm48.Column)({ type: "timestamp", nullable: true })
7466
+ ], JobScheduleRun.prototype, "finishedAt", 2);
7467
+ __decorateClass([
7468
+ (0, import_typeorm48.Column)({ type: "varchar", length: 32 })
7469
+ ], JobScheduleRun.prototype, "status", 2);
7470
+ __decorateClass([
7471
+ (0, import_typeorm48.Column)({ type: "text", nullable: true })
7472
+ ], JobScheduleRun.prototype, "error", 2);
7473
+ __decorateClass([
7474
+ (0, import_typeorm48.Column)({ type: "jsonb", nullable: true })
7475
+ ], JobScheduleRun.prototype, "result", 2);
7476
+ __decorateClass([
7477
+ (0, import_typeorm48.Column)({ type: "varchar", length: 16, default: "schedule" })
7478
+ ], JobScheduleRun.prototype, "triggeredBy", 2);
7479
+ __decorateClass([
7480
+ (0, import_typeorm48.CreateDateColumn)()
7481
+ ], JobScheduleRun.prototype, "createdAt", 2);
7482
+ JobScheduleRun = __decorateClass([
7483
+ (0, import_typeorm48.Entity)("job_schedule_runs")
7484
+ ], JobScheduleRun);
7485
+
7486
+ // src/entities/job-schedule.entity.ts
7487
+ var JobSchedule = class {
7488
+ id;
7489
+ name;
7490
+ jobType;
7491
+ enabled;
7492
+ scheduleMode;
7493
+ intervalMinutes;
7494
+ runAtTime;
7495
+ runOnDays;
7496
+ timezone;
7497
+ cronExpression;
7498
+ payload;
7499
+ authorId;
7500
+ pgBossScheduleName;
7501
+ lastRunAt;
7502
+ lastRunStatus;
7503
+ lastRunError;
7504
+ nextRunAt;
7505
+ runs;
7506
+ createdAt;
7507
+ updatedAt;
7508
+ };
7509
+ __decorateClass([
7510
+ (0, import_typeorm49.PrimaryGeneratedColumn)("uuid")
7511
+ ], JobSchedule.prototype, "id", 2);
7512
+ __decorateClass([
7513
+ (0, import_typeorm49.Column)({ type: "text" })
7514
+ ], JobSchedule.prototype, "name", 2);
7515
+ __decorateClass([
7516
+ (0, import_typeorm49.Column)({ type: "varchar", length: 64, default: "blog_generate" })
7517
+ ], JobSchedule.prototype, "jobType", 2);
7518
+ __decorateClass([
7519
+ (0, import_typeorm49.Column)({ type: "boolean", default: false })
7520
+ ], JobSchedule.prototype, "enabled", 2);
7521
+ __decorateClass([
7522
+ (0, import_typeorm49.Column)({ type: "varchar", length: 32, default: "daily" })
7523
+ ], JobSchedule.prototype, "scheduleMode", 2);
7524
+ __decorateClass([
7525
+ (0, import_typeorm49.Column)({ type: "int", nullable: true })
7526
+ ], JobSchedule.prototype, "intervalMinutes", 2);
7527
+ __decorateClass([
7528
+ (0, import_typeorm49.Column)({ type: "varchar", length: 8, nullable: true })
7529
+ ], JobSchedule.prototype, "runAtTime", 2);
7530
+ __decorateClass([
7531
+ (0, import_typeorm49.Column)({ type: "jsonb", nullable: true })
7532
+ ], JobSchedule.prototype, "runOnDays", 2);
7533
+ __decorateClass([
7534
+ (0, import_typeorm49.Column)({ type: "varchar", length: 64, default: "UTC" })
7535
+ ], JobSchedule.prototype, "timezone", 2);
7536
+ __decorateClass([
7537
+ (0, import_typeorm49.Column)({ type: "text", nullable: true })
7538
+ ], JobSchedule.prototype, "cronExpression", 2);
7539
+ __decorateClass([
7540
+ (0, import_typeorm49.Column)({ type: "jsonb", default: {} })
7541
+ ], JobSchedule.prototype, "payload", 2);
7542
+ __decorateClass([
7543
+ (0, import_typeorm49.Column)({ type: "int", nullable: true })
7544
+ ], JobSchedule.prototype, "authorId", 2);
7545
+ __decorateClass([
7546
+ (0, import_typeorm49.Column)({ type: "varchar", length: 128, unique: true })
7547
+ ], JobSchedule.prototype, "pgBossScheduleName", 2);
7548
+ __decorateClass([
7549
+ (0, import_typeorm49.Column)({ type: "timestamp", nullable: true })
7550
+ ], JobSchedule.prototype, "lastRunAt", 2);
7551
+ __decorateClass([
7552
+ (0, import_typeorm49.Column)({ type: "varchar", length: 32, nullable: true })
7553
+ ], JobSchedule.prototype, "lastRunStatus", 2);
7554
+ __decorateClass([
7555
+ (0, import_typeorm49.Column)({ type: "text", nullable: true })
7556
+ ], JobSchedule.prototype, "lastRunError", 2);
7557
+ __decorateClass([
7558
+ (0, import_typeorm49.Column)({ type: "timestamp", nullable: true })
7559
+ ], JobSchedule.prototype, "nextRunAt", 2);
7560
+ __decorateClass([
7561
+ (0, import_typeorm49.OneToMany)(() => JobScheduleRun, (r) => r.schedule)
7562
+ ], JobSchedule.prototype, "runs", 2);
7563
+ __decorateClass([
7564
+ (0, import_typeorm49.CreateDateColumn)()
7565
+ ], JobSchedule.prototype, "createdAt", 2);
7566
+ __decorateClass([
7567
+ (0, import_typeorm49.UpdateDateColumn)()
7568
+ ], JobSchedule.prototype, "updatedAt", 2);
7569
+ JobSchedule = __decorateClass([
7570
+ (0, import_typeorm49.Entity)("job_schedules")
7571
+ ], JobSchedule);
7572
+
7358
7573
  // src/entities/index.ts
7359
7574
  var CMS_ENTITY_MAP = {
7360
7575
  users: User,
@@ -7398,7 +7613,9 @@ var CMS_ENTITY_MAP = {
7398
7613
  llm_agents: LlmAgent,
7399
7614
  llm_agent_knowledge_documents: LlmAgentKnowledgeDocument,
7400
7615
  rss_feeds: RssFeed,
7401
- rss_articles: RssArticle
7616
+ rss_articles: RssArticle,
7617
+ job_schedules: JobSchedule,
7618
+ job_schedule_runs: JobScheduleRun
7402
7619
  };
7403
7620
 
7404
7621
  // src/plugins/blog-generator/blog-generator-service.ts
@@ -8601,6 +8818,302 @@ ${excerpt}`).trim().slice(0, 2200);
8601
8818
  };
8602
8819
  }
8603
8820
 
8821
+ // src/api/job-schedule-handlers.ts
8822
+ var import_crypto = require("crypto");
8823
+
8824
+ // src/plugins/jobs/schedule-cron.ts
8825
+ function pgBossScheduleNameForId(scheduleId) {
8826
+ return `schedule:${scheduleId}`;
8827
+ }
8828
+ function buildCronFromSchedule(schedule) {
8829
+ if (schedule.scheduleMode === "cron" && schedule.cronExpression?.trim()) {
8830
+ return schedule.cronExpression.trim();
8831
+ }
8832
+ const time = parseRunAtTime(schedule.runAtTime);
8833
+ const minute = time?.minute ?? 0;
8834
+ const hour = time?.hour ?? 9;
8835
+ if (schedule.scheduleMode === "interval") {
8836
+ const mins = schedule.intervalMinutes ?? 60;
8837
+ if (mins >= 60 && mins % 60 === 0) {
8838
+ const h = Math.max(1, Math.min(23, Math.floor(mins / 60)));
8839
+ if (h === 1) return `${minute} * * * *`;
8840
+ return `${minute} */${h} * * *`;
8841
+ }
8842
+ if (mins <= 59) {
8843
+ return `*/${Math.max(1, mins)} * * * *`;
8844
+ }
8845
+ return `${minute} * * * *`;
8846
+ }
8847
+ if (schedule.scheduleMode === "weekly") {
8848
+ const days = Array.isArray(schedule.runOnDays) && schedule.runOnDays.length > 0 ? schedule.runOnDays.filter((d) => d >= 0 && d <= 6).join(",") : "1";
8849
+ return `${minute} ${hour} * * ${days}`;
8850
+ }
8851
+ return `${minute} ${hour} * * *`;
8852
+ }
8853
+ function parseRunAtTime(runAtTime) {
8854
+ if (!runAtTime?.trim()) return null;
8855
+ const m = runAtTime.trim().match(/^(\d{1,2}):(\d{2})$/);
8856
+ if (!m) return null;
8857
+ const hour = parseInt(m[1], 10);
8858
+ const minute = parseInt(m[2], 10);
8859
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null;
8860
+ return { hour, minute };
8861
+ }
8862
+ function validateScheduleInput(schedule) {
8863
+ if (schedule.scheduleMode === "cron") {
8864
+ if (!schedule.cronExpression?.trim()) return "cronExpression is required for cron mode";
8865
+ return null;
8866
+ }
8867
+ if (schedule.scheduleMode === "interval") {
8868
+ const mins = schedule.intervalMinutes;
8869
+ if (mins == null || !Number.isFinite(mins) || mins < 1 || mins > 10080) {
8870
+ return "intervalMinutes must be between 1 and 10080";
8871
+ }
8872
+ return null;
8873
+ }
8874
+ if (schedule.runAtTime?.trim()) {
8875
+ if (!parseRunAtTime(schedule.runAtTime)) return "runAtTime must be HH:mm (24h)";
8876
+ }
8877
+ return null;
8878
+ }
8879
+
8880
+ // src/plugins/jobs/schedule-sync.ts
8881
+ async function syncJobScheduleToPgBoss(cms, schedule, onRegisterQueue) {
8882
+ const boss = cms.getPlugin("pg_boss");
8883
+ if (!boss) return;
8884
+ const name = schedule.pgBossScheduleName;
8885
+ await boss.unschedule(name);
8886
+ if (!schedule.enabled) return;
8887
+ const cron = buildCronFromSchedule(schedule);
8888
+ const data = {
8889
+ scheduleId: schedule.id,
8890
+ triggeredBy: "schedule"
8891
+ };
8892
+ await boss.schedule(name, cron, data, { tz: schedule.timezone || "UTC" });
8893
+ if (onRegisterQueue) {
8894
+ await onRegisterQueue(name);
8895
+ }
8896
+ }
8897
+ async function queueJobScheduleNow(cms, scheduleId) {
8898
+ const boss = cms.getPlugin("pg_boss");
8899
+ if (!boss) {
8900
+ throw new Error("pg-boss is not configured (DATABASE_URL required)");
8901
+ }
8902
+ const { JOB_RUNNER_QUEUE: JOB_RUNNER_QUEUE2 } = await Promise.resolve().then(() => (init_pg_boss_service(), pg_boss_service_exports));
8903
+ await boss.send(JOB_RUNNER_QUEUE2, {
8904
+ scheduleId,
8905
+ triggeredBy: "manual"
8906
+ });
8907
+ }
8908
+
8909
+ // src/plugins/jobs/job-runner.ts
8910
+ init_pg_boss_service();
8911
+
8912
+ // src/plugins/jobs/blog-generate-job.ts
8913
+ var import_typeorm50 = require("typeorm");
8914
+
8915
+ // src/plugins/jobs/job-runner.ts
8916
+ var executeJobRef = null;
8917
+ async function ensureScheduleQueueWorker(cms, queueName) {
8918
+ const boss = cms.getPlugin("pg_boss");
8919
+ if (!boss || !executeJobRef) return;
8920
+ await boss.registerWork(queueName, executeJobRef);
8921
+ }
8922
+
8923
+ // src/api/job-schedule-handlers.ts
8924
+ function serializeSchedule(s) {
8925
+ return {
8926
+ id: s.id,
8927
+ name: s.name,
8928
+ jobType: s.jobType,
8929
+ enabled: s.enabled,
8930
+ scheduleMode: s.scheduleMode,
8931
+ intervalMinutes: s.intervalMinutes,
8932
+ runAtTime: s.runAtTime,
8933
+ runOnDays: s.runOnDays,
8934
+ timezone: s.timezone,
8935
+ cronExpression: s.cronExpression,
8936
+ cronResolved: buildCronFromSchedule(s),
8937
+ payload: s.payload ?? {},
8938
+ authorId: s.authorId,
8939
+ pgBossScheduleName: s.pgBossScheduleName,
8940
+ lastRunAt: s.lastRunAt?.toISOString() ?? null,
8941
+ lastRunStatus: s.lastRunStatus,
8942
+ lastRunError: s.lastRunError,
8943
+ nextRunAt: s.nextRunAt?.toISOString() ?? null,
8944
+ createdAt: s.createdAt.toISOString(),
8945
+ updatedAt: s.updatedAt.toISOString()
8946
+ };
8947
+ }
8948
+ function serializeRun(r) {
8949
+ return {
8950
+ id: r.id,
8951
+ scheduleId: r.scheduleId,
8952
+ startedAt: r.startedAt.toISOString(),
8953
+ finishedAt: r.finishedAt?.toISOString() ?? null,
8954
+ status: r.status,
8955
+ error: r.error,
8956
+ result: r.result,
8957
+ triggeredBy: r.triggeredBy,
8958
+ createdAt: r.createdAt.toISOString()
8959
+ };
8960
+ }
8961
+ function parseScheduleBody(body, existing) {
8962
+ const scheduleMode = typeof body.scheduleMode === "string" ? body.scheduleMode : existing?.scheduleMode ?? "daily";
8963
+ const patch = {
8964
+ name: typeof body.name === "string" ? body.name.trim().slice(0, 500) : existing?.name,
8965
+ jobType: body.jobType === "blog_generate" ? "blog_generate" : existing?.jobType ?? "blog_generate",
8966
+ enabled: typeof body.enabled === "boolean" ? body.enabled : existing?.enabled ?? false,
8967
+ scheduleMode,
8968
+ intervalMinutes: typeof body.intervalMinutes === "number" ? body.intervalMinutes : body.intervalMinutes != null ? Number(body.intervalMinutes) : existing?.intervalMinutes ?? null,
8969
+ runAtTime: typeof body.runAtTime === "string" ? body.runAtTime.trim() || null : body.runAtTime === null ? null : existing?.runAtTime ?? null,
8970
+ runOnDays: Array.isArray(body.runOnDays) ? body.runOnDays.map((d) => Number(d)).filter((d) => Number.isFinite(d)) : existing?.runOnDays ?? null,
8971
+ timezone: typeof body.timezone === "string" && body.timezone.trim() ? body.timezone.trim().slice(0, 64) : existing?.timezone ?? "UTC",
8972
+ cronExpression: typeof body.cronExpression === "string" ? body.cronExpression.trim() || null : existing?.cronExpression ?? null,
8973
+ payload: body.payload != null && typeof body.payload === "object" && !Array.isArray(body.payload) ? body.payload : existing?.payload ?? {},
8974
+ authorId: typeof body.authorId === "number" ? body.authorId : body.authorId != null ? Number(body.authorId) : existing?.authorId ?? null
8975
+ };
8976
+ return patch;
8977
+ }
8978
+ function createJobScheduleHandlers(apiConfig) {
8979
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission, getCms } = apiConfig;
8980
+ const ScheduleEntity = entityMap.job_schedules ?? JobSchedule;
8981
+ const RunEntity = entityMap.job_schedule_runs ?? JobScheduleRun;
8982
+ async function perm(req, action) {
8983
+ if (!requireEntityPermission) return null;
8984
+ return requireEntityPermission(req, "blogs", action);
8985
+ }
8986
+ async function syncAfterSave(cms, schedule) {
8987
+ await syncJobScheduleToPgBoss(cms, schedule, async (queueName) => {
8988
+ await ensureScheduleQueueWorker(cms, queueName);
8989
+ });
8990
+ }
8991
+ return {
8992
+ async list(req) {
8993
+ const a = await requireAuth(req);
8994
+ if (a) return a;
8995
+ const pe = await perm(req, "read");
8996
+ if (pe) return pe;
8997
+ const list = await dataSource.getRepository(ScheduleEntity).find({
8998
+ order: { updatedAt: "DESC" }
8999
+ });
9000
+ return json({ schedules: list.map(serializeSchedule) });
9001
+ },
9002
+ async getById(req, id) {
9003
+ const a = await requireAuth(req);
9004
+ if (a) return a;
9005
+ const pe = await perm(req, "read");
9006
+ if (pe) return pe;
9007
+ const row = await dataSource.getRepository(ScheduleEntity).findOne({ where: { id } });
9008
+ if (!row) return json({ error: "Schedule not found" }, { status: 404 });
9009
+ const runs = await dataSource.getRepository(RunEntity).find({
9010
+ where: { scheduleId: id },
9011
+ order: { startedAt: "DESC" },
9012
+ take: 20
9013
+ });
9014
+ return json({ schedule: serializeSchedule(row), runs: runs.map(serializeRun) });
9015
+ },
9016
+ async create(req) {
9017
+ const a = await requireAuth(req);
9018
+ if (a) return a;
9019
+ const pe = await perm(req, "create");
9020
+ if (pe) return pe;
9021
+ if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
9022
+ let body;
9023
+ try {
9024
+ body = await req.json();
9025
+ } catch {
9026
+ return json({ error: "Invalid JSON body" }, { status: 400 });
9027
+ }
9028
+ const patch = parseScheduleBody(body);
9029
+ if (!patch.name) return json({ error: "name is required" }, { status: 400 });
9030
+ const draft = Object.assign(new JobSchedule(), patch);
9031
+ const validationError = validateScheduleInput(draft);
9032
+ if (validationError) return json({ error: validationError }, { status: 400 });
9033
+ const repo = dataSource.getRepository(ScheduleEntity);
9034
+ let row = repo.create({
9035
+ ...patch,
9036
+ pgBossScheduleName: `pending-${(0, import_crypto.randomUUID)()}`
9037
+ });
9038
+ row = await repo.save(row);
9039
+ row.pgBossScheduleName = pgBossScheduleNameForId(row.id);
9040
+ row = await repo.save(row);
9041
+ const cms = await getCms();
9042
+ await syncAfterSave(cms, row);
9043
+ return json({ schedule: serializeSchedule(row) }, { status: 201 });
9044
+ },
9045
+ async update(req, id) {
9046
+ const a = await requireAuth(req);
9047
+ if (a) return a;
9048
+ const pe = await perm(req, "update");
9049
+ if (pe) return pe;
9050
+ if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
9051
+ const repo = dataSource.getRepository(ScheduleEntity);
9052
+ const existing = await repo.findOne({ where: { id } });
9053
+ if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
9054
+ let body;
9055
+ try {
9056
+ body = await req.json();
9057
+ } catch {
9058
+ return json({ error: "Invalid JSON body" }, { status: 400 });
9059
+ }
9060
+ const patch = parseScheduleBody(body, existing);
9061
+ Object.assign(existing, patch);
9062
+ const validationError = validateScheduleInput(existing);
9063
+ if (validationError) return json({ error: validationError }, { status: 400 });
9064
+ const row = await repo.save(existing);
9065
+ const cms = await getCms();
9066
+ await syncAfterSave(cms, row);
9067
+ return json({ schedule: serializeSchedule(row) });
9068
+ },
9069
+ async remove(req, id) {
9070
+ const a = await requireAuth(req);
9071
+ if (a) return a;
9072
+ const pe = await perm(req, "delete");
9073
+ if (pe) return pe;
9074
+ if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
9075
+ const repo = dataSource.getRepository(ScheduleEntity);
9076
+ const existing = await repo.findOne({ where: { id } });
9077
+ if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
9078
+ const cms = await getCms();
9079
+ existing.enabled = false;
9080
+ await syncJobScheduleToPgBoss(cms, existing);
9081
+ await repo.delete({ id });
9082
+ return json({ ok: true });
9083
+ },
9084
+ async runNow(req, id) {
9085
+ const a = await requireAuth(req);
9086
+ if (a) return a;
9087
+ const pe = await perm(req, "create");
9088
+ if (pe) return pe;
9089
+ if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
9090
+ const repo = dataSource.getRepository(ScheduleEntity);
9091
+ const existing = await repo.findOne({ where: { id } });
9092
+ if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
9093
+ try {
9094
+ const cms = await getCms();
9095
+ await queueJobScheduleNow(cms, id);
9096
+ return json({ ok: true, message: "Job queued" });
9097
+ } catch (e) {
9098
+ const message = e instanceof Error ? e.message : "Failed to queue job";
9099
+ return json({ error: message }, { status: 503 });
9100
+ }
9101
+ },
9102
+ async listRuns(req, id) {
9103
+ const a = await requireAuth(req);
9104
+ if (a) return a;
9105
+ const pe = await perm(req, "read");
9106
+ if (pe) return pe;
9107
+ const runs = await dataSource.getRepository(RunEntity).find({
9108
+ where: { scheduleId: id },
9109
+ order: { startedAt: "DESC" },
9110
+ take: 50
9111
+ });
9112
+ return json({ runs: runs.map(serializeRun) });
9113
+ }
9114
+ };
9115
+ }
9116
+
8604
9117
  // src/api/cms-api-handler.ts
8605
9118
  var KNOWLEDGE_SUFFIX = "knowledge";
8606
9119
  var CMS_API_LOG = "[cms-api]";
@@ -8661,7 +9174,9 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
8661
9174
  "message_templates",
8662
9175
  "llm_agent_knowledge_documents",
8663
9176
  "rss_feeds",
8664
- "rss_articles"
9177
+ "rss_articles",
9178
+ "job_schedules",
9179
+ "job_schedule_runs"
8665
9180
  ]);
8666
9181
  function createCmsApiHandler(config) {
8667
9182
  const {
@@ -8824,6 +9339,15 @@ function createCmsApiHandler(config) {
8824
9339
  ...llmAgentKnowledgeMerged,
8825
9340
  requireEntityPermission: requireEntityPermissionEffective
8826
9341
  }) : null;
9342
+ const jobScheduleHandlers = getCms ? createJobScheduleHandlers({
9343
+ dataSource,
9344
+ entityMap,
9345
+ json: config.json,
9346
+ requireAuth: config.requireAuth,
9347
+ requireEntityPermission: requireEntityPermissionEffective,
9348
+ getCms,
9349
+ config: typeof process !== "undefined" ? process.env : {}
9350
+ }) : null;
8827
9351
  function resolveResource(segment) {
8828
9352
  const model = pathToModel(segment);
8829
9353
  return crudResources.includes(model) ? model : segment;
@@ -8854,6 +9378,30 @@ function createCmsApiHandler(config) {
8854
9378
  if (g) return g;
8855
9379
  return ecommerceAnalyticsGet(req);
8856
9380
  }
9381
+ if (path2[0] === "job-schedules" && jobScheduleHandlers) {
9382
+ if (path2.length === 2 && m === "GET") {
9383
+ return jobScheduleHandlers.getById(req, path2[1]);
9384
+ }
9385
+ if (path2.length === 2 && m === "PATCH") {
9386
+ return jobScheduleHandlers.update(req, path2[1]);
9387
+ }
9388
+ if (path2.length === 2 && m === "DELETE") {
9389
+ return jobScheduleHandlers.remove(req, path2[1]);
9390
+ }
9391
+ if (path2.length === 3 && path2[2] === "run-now" && m === "POST") {
9392
+ return jobScheduleHandlers.runNow(req, path2[1]);
9393
+ }
9394
+ if (path2.length === 3 && path2[2] === "runs" && m === "GET") {
9395
+ return jobScheduleHandlers.listRuns(req, path2[1]);
9396
+ }
9397
+ if (path2.length === 1 && m === "GET") {
9398
+ return jobScheduleHandlers.list(req);
9399
+ }
9400
+ if (path2.length === 1 && m === "POST") {
9401
+ return jobScheduleHandlers.create(req);
9402
+ }
9403
+ return config.json({ error: "Not found" }, { status: 404 });
9404
+ }
8857
9405
  if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 3 && m === "DELETE" && getCms) {
8858
9406
  const id = path2[2];
8859
9407
  const uuidLooksValid = typeof id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);
@@ -9499,7 +10047,7 @@ function createCmsApiHandler(config) {
9499
10047
  }
9500
10048
 
9501
10049
  // src/api/storefront-handlers.ts
9502
- var import_typeorm49 = require("typeorm");
10050
+ var import_typeorm52 = require("typeorm");
9503
10051
 
9504
10052
  // src/lib/is-valid-signup-email.ts
9505
10053
  var MAX_EMAIL = 254;
@@ -9758,8 +10306,8 @@ async function queueSms(cms, payload) {
9758
10306
  }
9759
10307
 
9760
10308
  // src/lib/otp-challenge.ts
9761
- var import_crypto = require("crypto");
9762
- var import_typeorm48 = require("typeorm");
10309
+ var import_crypto2 = require("crypto");
10310
+ var import_typeorm51 = require("typeorm");
9763
10311
  var OTP_TTL_MS = 10 * 60 * 1e3;
9764
10312
  var MAX_SENDS_PER_HOUR = 5;
9765
10313
  var MAX_VERIFY_ATTEMPTS = 8;
@@ -9767,19 +10315,19 @@ function getPepper(explicit) {
9767
10315
  return (explicit || process.env.OTP_PEPPER || process.env.NEXTAUTH_SECRET || "dev-otp-pepper").trim();
9768
10316
  }
9769
10317
  function hashOtpCode(code, purpose, identifier, pepper) {
9770
- return (0, import_crypto.createHmac)("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
10318
+ return (0, import_crypto2.createHmac)("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
9771
10319
  }
9772
10320
  function verifyOtpCodeHash(code, storedHash, purpose, identifier, pepper) {
9773
10321
  const h = hashOtpCode(code, purpose, identifier, pepper);
9774
10322
  try {
9775
- return (0, import_crypto.timingSafeEqual)(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
10323
+ return (0, import_crypto2.timingSafeEqual)(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
9776
10324
  } catch {
9777
10325
  return false;
9778
10326
  }
9779
10327
  }
9780
10328
  function generateNumericOtp(length = 6) {
9781
10329
  const max = 10 ** length;
9782
- return (0, import_crypto.randomInt)(0, max).toString().padStart(length, "0");
10330
+ return (0, import_crypto2.randomInt)(0, max).toString().padStart(length, "0");
9783
10331
  }
9784
10332
  function normalizePhoneE164(raw, defaultCountryCode) {
9785
10333
  const t = raw.trim();
@@ -9793,7 +10341,7 @@ function normalizePhoneE164(raw, defaultCountryCode) {
9793
10341
  async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
9794
10342
  const repo = dataSource.getRepository(entityMap.otp_challenges);
9795
10343
  return repo.count({
9796
- where: { purpose, identifier, createdAt: (0, import_typeorm48.MoreThan)(since) }
10344
+ where: { purpose, identifier, createdAt: (0, import_typeorm51.MoreThan)(since) }
9797
10345
  });
9798
10346
  }
9799
10347
  async function createOtpChallenge(dataSource, entityMap, input) {
@@ -9807,7 +10355,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
9807
10355
  await repo.delete({
9808
10356
  purpose,
9809
10357
  identifier,
9810
- consumedAt: (0, import_typeorm48.IsNull)()
10358
+ consumedAt: (0, import_typeorm51.IsNull)()
9811
10359
  });
9812
10360
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
9813
10361
  const codeHash = hashOtpCode(code, purpose, identifier, pepper);
@@ -9828,7 +10376,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
9828
10376
  const { purpose, identifier, code, pepper } = input;
9829
10377
  const repo = dataSource.getRepository(entityMap.otp_challenges);
9830
10378
  const row = await repo.findOne({
9831
- where: { purpose, identifier, consumedAt: (0, import_typeorm48.IsNull)() },
10379
+ where: { purpose, identifier, consumedAt: (0, import_typeorm51.IsNull)() },
9832
10380
  order: { id: "DESC" }
9833
10381
  });
9834
10382
  if (!row) {
@@ -10062,7 +10610,7 @@ function createStorefrontApiHandler(config) {
10062
10610
  const u = await userRepo().findOne({ where: { id: userId } });
10063
10611
  if (!u) return null;
10064
10612
  const unclaimed = await contactRepo().findOne({
10065
- where: { email: u.email, userId: (0, import_typeorm49.IsNull)(), deleted: false }
10613
+ where: { email: u.email, userId: (0, import_typeorm52.IsNull)(), deleted: false }
10066
10614
  });
10067
10615
  if (unclaimed) {
10068
10616
  await contactRepo().update(unclaimed.id, { userId });
@@ -11113,7 +11661,7 @@ function createStorefrontApiHandler(config) {
11113
11661
  const previewByOrder = {};
11114
11662
  if (orderIds.length) {
11115
11663
  const oItems = await orderItemRepo().find({
11116
- where: { orderId: (0, import_typeorm49.In)(orderIds) },
11664
+ where: { orderId: (0, import_typeorm52.In)(orderIds) },
11117
11665
  relations: ["product"],
11118
11666
  order: { id: "ASC" }
11119
11667
  });
@@ -11250,6 +11798,7 @@ function createStorefrontApiHandler(config) {
11250
11798
  createForgotPasswordHandler,
11251
11799
  createFormBySlugHandler,
11252
11800
  createInviteAcceptHandler,
11801
+ createJobScheduleHandlers,
11253
11802
  createLlmAgentKnowledgeHandlers,
11254
11803
  createMediaZipExtractHandler,
11255
11804
  createSetPasswordHandler,