@infuro/cms-core 1.0.27 → 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/admin.cjs +1768 -1193
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +10 -1
- package/dist/admin.d.ts +10 -1
- package/dist/admin.js +1730 -1156
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +562 -13
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +566 -4
- package/dist/api.js.map +1 -1
- package/dist/cli.cjs +2 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/{index-CI6J9dxr.d.cts → index-BPQSXgXF.d.cts} +20 -1
- package/dist/{index-CMJZ5Fpr.d.ts → index-D9SdaDJ0.d.ts} +20 -1
- package/dist/index.cjs +2241 -1416
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -16
- package/dist/index.d.ts +106 -16
- package/dist/index.js +2210 -1383
- package/dist/index.js.map +1 -1
- package/dist/migrations/1775700000000-JobSchedules.ts +70 -0
- package/package.json +5 -3
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
|
|
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
|
|
9762
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|