@open-mercato/scheduler 0.6.8-develop.7066.1.6bfa9ff69f → 0.6.8-develop.7068.1.c55b52e1a1
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js +9 -4
- package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js +3 -3
- package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js +72 -0
- package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js.map +7 -0
- package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js +3 -3
- package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js.map +1 -1
- package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js +107 -44
- package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/helpers/scheduler.js +13 -3
- package/dist/modules/scheduler/__integration__/helpers/scheduler.js.map +2 -2
- package/dist/modules/scheduler/api/targets/route.js +2 -12
- package/dist/modules/scheduler/api/targets/route.js.map +2 -2
- package/dist/modules/scheduler/cli.js +32 -1
- package/dist/modules/scheduler/cli.js.map +2 -2
- package/dist/modules/scheduler/commands/jobs.js +84 -2
- package/dist/modules/scheduler/commands/jobs.js.map +2 -2
- package/dist/modules/scheduler/data/validators.js +27 -3
- package/dist/modules/scheduler/data/validators.js.map +2 -2
- package/dist/modules/scheduler/i18n/de.json +2 -0
- package/dist/modules/scheduler/i18n/en.json +2 -0
- package/dist/modules/scheduler/i18n/es.json +2 -0
- package/dist/modules/scheduler/i18n/ko.json +2 -0
- package/dist/modules/scheduler/i18n/pl.json +2 -0
- package/dist/modules/scheduler/lib/safeQueueTargets.js +157 -0
- package/dist/modules/scheduler/lib/safeQueueTargets.js.map +7 -0
- package/dist/modules/scheduler/services/localSchedulerService.js +31 -6
- package/dist/modules/scheduler/services/localSchedulerService.js.map +2 -2
- package/dist/modules/scheduler/workers/execute-schedule.worker.js +64 -6
- package/dist/modules/scheduler/workers/execute-schedule.worker.js.map +2 -2
- package/dist/modules/scheduler/workers/test-echo-queue.worker.js +22 -0
- package/dist/modules/scheduler/workers/test-echo-queue.worker.js.map +7 -0
- package/package.json +5 -5
- package/src/modules/scheduler/__integration__/TC-SCHED-001.spec.ts +2 -2
- package/src/modules/scheduler/__integration__/TC-SCHED-004.spec.ts +14 -8
- package/src/modules/scheduler/__integration__/TC-SCHED-007.spec.ts +3 -3
- package/src/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.ts +90 -0
- package/src/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.ts +3 -3
- package/src/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.ts +154 -66
- package/src/modules/scheduler/__integration__/helpers/scheduler.ts +24 -5
- package/src/modules/scheduler/api/targets/__tests__/route.test.ts +3 -4
- package/src/modules/scheduler/api/targets/route.ts +6 -17
- package/src/modules/scheduler/cli.ts +44 -1
- package/src/modules/scheduler/commands/__tests__/jobs.target-guard.test.ts +177 -0
- package/src/modules/scheduler/commands/jobs.ts +137 -3
- package/src/modules/scheduler/data/__tests__/safe-targets.test.ts +110 -0
- package/src/modules/scheduler/data/__tests__/validators.test.ts +26 -4
- package/src/modules/scheduler/data/validators.ts +37 -2
- package/src/modules/scheduler/i18n/de.json +2 -0
- package/src/modules/scheduler/i18n/en.json +2 -0
- package/src/modules/scheduler/i18n/es.json +2 -0
- package/src/modules/scheduler/i18n/ko.json +2 -0
- package/src/modules/scheduler/i18n/pl.json +2 -0
- package/src/modules/scheduler/lib/__tests__/safeQueueTargets.test.ts +311 -0
- package/src/modules/scheduler/lib/safeQueueTargets.ts +324 -0
- package/src/modules/scheduler/services/__tests__/localSchedulerService.test.ts +179 -0
- package/src/modules/scheduler/services/localSchedulerService.ts +39 -6
- package/src/modules/scheduler/workers/__tests__/execute-schedule.worker.test.ts +25 -3
- package/src/modules/scheduler/workers/execute-schedule.worker.ts +76 -7
- package/src/modules/scheduler/workers/test-echo-queue.worker.ts +25 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/scheduler/__integration__/helpers/scheduler.ts"],
|
|
4
|
-
"sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\nexport const SCHEDULER_JOBS_PATH = '/api/scheduler/jobs'\nexport const SCHEDULER_TARGETS_PATH = '/api/scheduler/targets'\nexport const SCHEDULER_TRIGGER_PATH = '/api/scheduler/trigger'\n\n/** Queue registered by the scheduler module's execute-schedule worker. */\nexport const SCHEDULER_EXECUTION_QUEUE = 'scheduler-execution'\n\nexport type SchedulerJob = {\n id: string\n name: string\n description: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId: string | null\n tenantId: string | null\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue: string | null\n targetCommand: string | null\n isEnabled: boolean\n createdAt: string | null\n updatedAt: string | null\n}\n\nexport type CreateScheduleOverrides = {\n name?: string\n description?: string | null\n scopeType?: 'system' | 'organization' | 'tenant'\n scheduleType?: 'cron' | 'interval'\n scheduleValue?: string\n timezone?: string\n targetType?: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: Record<string, unknown>\n isEnabled?: boolean\n sourceType?: 'user' | 'module'\n}\n\nlet sequence = 0\n\n/** Stable-ish unique label so concurrent fixtures never collide on name. */\nexport function uniqueScheduleName(prefix: string): string {\n sequence += 1\n return `${prefix} ${Date.now()}-${sequence}`\n}\n\n/**\n * Create an organization-scoped
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAsC;AAC/C,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAEtB,MAAM,sBAAsB;AAC5B,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAG/B,MAAM,4BAA4B;
|
|
4
|
+
"sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\nexport const SCHEDULER_JOBS_PATH = '/api/scheduler/jobs'\nexport const SCHEDULER_TARGETS_PATH = '/api/scheduler/targets'\nexport const SCHEDULER_TRIGGER_PATH = '/api/scheduler/trigger'\n\n/** Queue registered by the scheduler module's execute-schedule worker (internal, not schedulable). */\nexport const SCHEDULER_EXECUTION_QUEUE = 'scheduler-execution'\n\n/** Deliberately side-effect-free queue whose worker opted into scheduling (#5213). */\nexport const SCHEDULER_TEST_QUEUE = 'scheduler-test'\n\n/**\n * Internal queues that must never be offered as scheduler targets (#5213).\n * Workers must opt in via `schedulerSafe: true` to become schedulable.\n */\nexport const SCHEDULER_FORBIDDEN_INTERNAL_QUEUES = [\n SCHEDULER_EXECUTION_QUEUE,\n 'stripe-webhook',\n 'payment-gateways-webhook',\n] as const\n\n/** Always-registered command used by fixtures now that queue targets are restricted. */\nexport const SCHEDULER_TEST_COMMAND = 'scheduler.test.echo'\n\nexport type SchedulerJob = {\n id: string\n name: string\n description: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId: string | null\n tenantId: string | null\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue: string | null\n targetCommand: string | null\n isEnabled: boolean\n createdAt: string | null\n updatedAt: string | null\n}\n\nexport type CreateScheduleOverrides = {\n name?: string\n description?: string | null\n scopeType?: 'system' | 'organization' | 'tenant'\n scheduleType?: 'cron' | 'interval'\n scheduleValue?: string\n timezone?: string\n targetType?: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: Record<string, unknown>\n isEnabled?: boolean\n sourceType?: 'user' | 'module'\n}\n\nlet sequence = 0\n\n/** Stable-ish unique label so concurrent fixtures never collide on name. */\nexport function uniqueScheduleName(prefix: string): string {\n sequence += 1\n return `${prefix} ${Date.now()}-${sequence}`\n}\n\n/**\n * Create an organization-scoped schedule via the API and return its id.\n * Org/tenant are derived server-side from the caller's auth context, so the caller\n * only needs an admin (or otherwise scheduler.jobs.manage) token.\n *\n * Defaults to a command target: user-authored queue targets are restricted to\n * scheduler-safe queues (#5213), so fixtures must not assume a queue is allowed.\n */\nexport async function createScheduleJob(\n request: APIRequestContext,\n token: string,\n overrides: CreateScheduleOverrides = {},\n): Promise<string> {\n const targetType = overrides.targetType ?? 'command'\n const data: Record<string, unknown> = {\n name: overrides.name ?? uniqueScheduleName('Integration Scheduler'),\n description: overrides.description ?? 'Created by scheduler integration tests',\n scopeType: overrides.scopeType ?? 'organization',\n scheduleType: overrides.scheduleType ?? 'interval',\n scheduleValue: overrides.scheduleValue ?? '15m',\n timezone: overrides.timezone ?? 'UTC',\n targetType,\n targetPayload: overrides.targetPayload ?? { source: 'integration-test' },\n isEnabled: overrides.isEnabled ?? true,\n sourceType: overrides.sourceType ?? 'user',\n }\n if (targetType === 'command') {\n data.targetCommand = overrides.targetCommand ?? SCHEDULER_TEST_COMMAND\n } else {\n data.targetQueue = overrides.targetQueue ?? SCHEDULER_TEST_QUEUE\n }\n\n const response = await apiRequest(request, 'POST', SCHEDULER_JOBS_PATH, { token, data })\n const body = await readJsonSafe<{ id?: string }>(response)\n expect(response.status(), 'POST /api/scheduler/jobs should return 201').toBe(201)\n const id = body?.id\n expect(typeof id === 'string' && /^[0-9a-f-]{36}$/i.test(id), 'create response should include a UUID id').toBe(true)\n return String(id)\n}\n\n/** Fetch a single schedule by id from the list endpoint; null when absent (e.g. soft-deleted). */\nexport async function getScheduleJobById(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<SchedulerJob | null> {\n const response = await apiRequest(request, 'GET', `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}`, { token })\n expect(response.status(), 'GET /api/scheduler/jobs?id should return 200').toBe(200)\n const body = await readJsonSafe<{ items?: SchedulerJob[] }>(response)\n return (body?.items ?? []).find((item) => item.id === id) ?? null\n}\n\n/** Best-effort cleanup; swallows errors so teardown never masks the real assertion. */\nexport async function deleteScheduleJob(\n request: APIRequestContext,\n token: string,\n id: string | null,\n): Promise<void> {\n if (!id) return\n await apiRequest(request, 'DELETE', SCHEDULER_JOBS_PATH, { token, data: { id } }).catch(() => null)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,cAAsC;AAC/C,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAEtB,MAAM,sBAAsB;AAC5B,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAG/B,MAAM,4BAA4B;AAGlC,MAAM,uBAAuB;AAM7B,MAAM,sCAAsC;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,yBAAyB;AAmCtC,IAAI,WAAW;AAGR,SAAS,mBAAmB,QAAwB;AACzD,cAAY;AACZ,SAAO,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,QAAQ;AAC5C;AAUA,eAAsB,kBACpB,SACA,OACA,YAAqC,CAAC,GACrB;AACjB,QAAM,aAAa,UAAU,cAAc;AAC3C,QAAM,OAAgC;AAAA,IACpC,MAAM,UAAU,QAAQ,mBAAmB,uBAAuB;AAAA,IAClE,aAAa,UAAU,eAAe;AAAA,IACtC,WAAW,UAAU,aAAa;AAAA,IAClC,cAAc,UAAU,gBAAgB;AAAA,IACxC,eAAe,UAAU,iBAAiB;AAAA,IAC1C,UAAU,UAAU,YAAY;AAAA,IAChC;AAAA,IACA,eAAe,UAAU,iBAAiB,EAAE,QAAQ,mBAAmB;AAAA,IACvE,WAAW,UAAU,aAAa;AAAA,IAClC,YAAY,UAAU,cAAc;AAAA,EACtC;AACA,MAAI,eAAe,WAAW;AAC5B,SAAK,gBAAgB,UAAU,iBAAiB;AAAA,EAClD,OAAO;AACL,SAAK,cAAc,UAAU,eAAe;AAAA,EAC9C;AAEA,QAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,qBAAqB,EAAE,OAAO,KAAK,CAAC;AACvF,QAAM,OAAO,MAAM,aAA8B,QAAQ;AACzD,SAAO,SAAS,OAAO,GAAG,4CAA4C,EAAE,KAAK,GAAG;AAChF,QAAM,KAAK,MAAM;AACjB,SAAO,OAAO,OAAO,YAAY,mBAAmB,KAAK,EAAE,GAAG,0CAA0C,EAAE,KAAK,IAAI;AACnH,SAAO,OAAO,EAAE;AAClB;AAGA,eAAsB,mBACpB,SACA,OACA,IAC8B;AAC9B,QAAM,WAAW,MAAM,WAAW,SAAS,OAAO,GAAG,mBAAmB,OAAO,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC;AAClH,SAAO,SAAS,OAAO,GAAG,8CAA8C,EAAE,KAAK,GAAG;AAClF,QAAM,OAAO,MAAM,aAAyC,QAAQ;AACpE,UAAQ,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE,KAAK;AAC/D;AAGA,eAAsB,kBACpB,SACA,OACA,IACe;AACf,MAAI,CAAC,GAAI;AACT,QAAM,WAAW,SAAS,UAAU,qBAAqB,EAAE,OAAO,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,MAAM,IAAI;AACpG;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
-
import { getModules } from "@open-mercato/shared/lib/modules/registry";
|
|
5
4
|
import { commandRegistry } from "@open-mercato/shared/lib/commands/registry";
|
|
6
5
|
import { listSchedulerSafeCommands } from "../../lib/scheduler-safe-commands.js";
|
|
6
|
+
import { listSchedulerSafeQueueTargets } from "../../lib/safeQueueTargets.js";
|
|
7
7
|
const metadata = {
|
|
8
8
|
requireAuth: true,
|
|
9
9
|
requireFeatures: ["scheduler.jobs.view"]
|
|
@@ -13,16 +13,7 @@ async function GET(req) {
|
|
|
13
13
|
if (!auth) {
|
|
14
14
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
15
15
|
}
|
|
16
|
-
const
|
|
17
|
-
const queueSet = /* @__PURE__ */ new Set();
|
|
18
|
-
for (const mod of modules) {
|
|
19
|
-
if (mod.workers) {
|
|
20
|
-
for (const worker of mod.workers) {
|
|
21
|
-
queueSet.add(worker.queue);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const queues = Array.from(queueSet).sort((a, b) => a.localeCompare(b)).map((queue) => ({ value: queue, label: queue }));
|
|
16
|
+
const queues = listSchedulerSafeQueueTargets().map((target) => ({ value: target.queue, label: target.queue }));
|
|
26
17
|
const commands = listSchedulerSafeCommands().filter((command) => commandRegistry.has(command.commandId)).map((command) => ({ value: command.commandId, label: command.commandId }));
|
|
27
18
|
return NextResponse.json({ queues, commands });
|
|
28
19
|
}
|
|
@@ -45,7 +36,6 @@ const openApi = {
|
|
|
45
36
|
GET: {
|
|
46
37
|
operationId: "listScheduleTargets",
|
|
47
38
|
summary: "List available queues and commands",
|
|
48
|
-
description: "Returns all registered queue names (from module workers) and explicitly scheduler-safe command IDs that can be used as schedule targets.",
|
|
49
39
|
responses: [
|
|
50
40
|
{
|
|
51
41
|
status: 200,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/scheduler/api/targets/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { commandRegistry } from '@open-mercato/shared/lib/commands/registry'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { listSchedulerSafeCommands } from '../../lib/scheduler-safe-commands'\nimport { listSchedulerSafeQueueTargets } from '../../lib/safeQueueTargets'\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.view'],\n}\n\n/**\n * GET /api/scheduler/targets\n * Returns scheduler-safe queue names and command IDs for schedule target selection.\n * Only queues whose workers opted into scheduling (`schedulerSafe: true`) are\n * advertised; internal and system-only workers stay undiscoverable (#5213).\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const queues = listSchedulerSafeQueueTargets()\n .map((target) => ({ value: target.queue, label: target.queue }))\n\n const commands = listSchedulerSafeCommands()\n .filter((command) => commandRegistry.has(command.commandId))\n .map((command) => ({ value: command.commandId, label: command.commandId }))\n\n return NextResponse.json({ queues, commands })\n}\n\n// Response schemas\nconst targetOptionSchema = z.object({\n value: z.string(),\n label: z.string(),\n})\n\nconst targetsResponseSchema = z.object({\n queues: z.array(targetOptionSchema),\n commands: z.array(targetOptionSchema),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'List available schedule targets',\n description: 'Returns available queue names and scheduler-safe command IDs for schedule target selection.',\n methods: {\n GET: {\n operationId: 'listScheduleTargets',\n summary: 'List available queues and commands',\n responses: [\n {\n status: 200,\n description: 'Available targets',\n schema: targetsResponseSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAEhC,SAAS,iCAAiC;AAC1C,SAAS,qCAAqC;AAEvC,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,qBAAqB;AACzC;AAQA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,SAAS,8BAA8B,EAC1C,IAAI,CAAC,YAAY,EAAE,OAAO,OAAO,OAAO,OAAO,OAAO,MAAM,EAAE;AAEjE,QAAM,WAAW,0BAA0B,EACxC,OAAO,CAAC,YAAY,gBAAgB,IAAI,QAAQ,SAAS,CAAC,EAC1D,IAAI,CAAC,aAAa,EAAE,OAAO,QAAQ,WAAW,OAAO,QAAQ,UAAU,EAAE;AAE5E,SAAO,aAAa,KAAK,EAAE,QAAQ,SAAS,CAAC;AAC/C;AAGA,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,OAAO,EAAE,OAAO;AAAA,EAChB,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,QAAQ,EAAE,MAAM,kBAAkB;AAAA,EAClC,UAAU,EAAE,MAAM,kBAAkB;AACtC,CAAC;AAED,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
2
|
import { ScheduledJob } from "./data/entities.js";
|
|
3
3
|
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
4
|
+
import { auditSchedulerModuleQueueRows } from "./lib/safeQueueTargets.js";
|
|
4
5
|
const writeLine = (text = "") => {
|
|
5
6
|
process.stdout.write(`${text}
|
|
6
7
|
`);
|
|
@@ -182,7 +183,37 @@ const startCommand = {
|
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
};
|
|
185
|
-
|
|
186
|
+
const auditQueueTargetsCommand = {
|
|
187
|
+
command: "audit-queue-targets",
|
|
188
|
+
async run(rest) {
|
|
189
|
+
const args = parseArgs(rest);
|
|
190
|
+
const { resolve } = await createRequestContainer();
|
|
191
|
+
const em = resolve("em");
|
|
192
|
+
const where = { deletedAt: null, sourceType: "module" };
|
|
193
|
+
if (args.tenant || args.tenantId) where.tenantId = args.tenant || args.tenantId;
|
|
194
|
+
const rows = await em.find(ScheduledJob, where, {
|
|
195
|
+
orderBy: { name: "ASC" }
|
|
196
|
+
});
|
|
197
|
+
const audits = auditSchedulerModuleQueueRows(rows);
|
|
198
|
+
writeLine(`Module-authored queue-target schedules: ${audits.length}`);
|
|
199
|
+
let allowedCount = 0;
|
|
200
|
+
for (const audit of audits) {
|
|
201
|
+
const verdict = audit.dispatchAllowed ? "ALLOWED" : "blocked";
|
|
202
|
+
if (audit.dispatchAllowed) allowedCount += 1;
|
|
203
|
+
writeLine(
|
|
204
|
+
`[${verdict}] ${audit.scheduleId}${audit.name ? ` \u2014 ${audit.name}` : ""} module=${audit.sourceModule ?? "(none)"} queue=${audit.targetQueue ?? "(none)"} enabled=${audit.isEnabled ? "yes" : "no"}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (allowedCount > 0) {
|
|
208
|
+
writeLine("");
|
|
209
|
+
writeLine(`Review the ${allowedCount} ALLOWED row(s) above against your known module integrations.`);
|
|
210
|
+
writeLine("Pre-upgrade rows created with a non-user-bound API key cannot be distinguished");
|
|
211
|
+
writeLine("from genuine registrations at rest; disable anything unrecognized:");
|
|
212
|
+
writeLine(" yarn mercato scheduler list --tenant <tenantId>");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var cli_default = [listCommand, statusCommand, runCommand, startCommand, auditQueueTargetsCommand];
|
|
186
217
|
export {
|
|
187
218
|
cli_default as default
|
|
188
219
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/scheduler/cli.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { ScheduledJob } from './data/entities.js'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst writeLine = (text = '') => {\n process.stdout.write(`${text}\\n`)\n}\n\nconst writeErrorLine = (text: string) => {\n process.stderr.write(`${text}\\n`)\n}\n\nfunction parseArgs(rest: string[]): Record<string, string> {\n const args: Record<string, string> = {}\n for (let i = 0; i < rest.length; i += 2) {\n const key = rest[i]?.replace(/^--/, '')\n const value = rest[i + 1]\n if (key) args[key] = value ?? ''\n }\n return args\n}\n\nconst listCommand: ModuleCli = {\n command: 'list',\n async run(rest) {\n const args = parseArgs(rest)\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n\n const where: Record<string, unknown> = { deletedAt: null }\n\n // Filter by tenant if provided\n if (args.tenant || args.tenantId) {\n where.tenantId = args.tenant || args.tenantId\n }\n\n // Filter by scope type if provided\n if (args.scope || args.scopeType) {\n where.scopeType = args.scope || args.scopeType\n }\n\n // Filter by enabled status if provided\n if (args.enabled) {\n const parsed = parseBooleanToken(args.enabled)\n if (parsed !== null) {\n where.isEnabled = parsed\n }\n }\n\n const jobs = await em.find(ScheduledJob, where, {\n orderBy: { isEnabled: 'DESC', name: 'ASC' },\n })\n\n if (jobs.length === 0) {\n writeLine('No scheduled jobs found.')\n return\n }\n\n writeLine(`\\nFound ${jobs.length} scheduled job(s):\\n`)\n writeLine('ID'.padEnd(38) + 'Name'.padEnd(35) + 'Type'.padEnd(12) + 'Schedule'.padEnd(20) + 'Status'.padEnd(10) + 'Next Run')\n writeLine('-'.repeat(140))\n\n for (const job of jobs) {\n const id = String(job.id).padEnd(38)\n const name = (job.name || '').substring(0, 33).padEnd(35)\n const type = job.scheduleType.padEnd(12)\n const schedule = (job.scheduleValue || '').substring(0, 18).padEnd(20)\n const status = (job.isEnabled ? '\u2713 Enabled' : '\u2717 Disabled').padEnd(10)\n const nextRun = job.nextRunAt ? new Date(job.nextRunAt).toISOString() : 'N/A'\n writeLine(`${id}${name}${type}${schedule}${status}${nextRun}`)\n }\n\n writeLine('')\n },\n}\n\nconst statusCommand: ModuleCli = {\n command: 'status',\n async run() {\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n\n const totalCount = await em.count(ScheduledJob, { deletedAt: null })\n const enabledCount = await em.count(ScheduledJob, { isEnabled: true, deletedAt: null })\n const dueCount = await em.count(ScheduledJob, {\n isEnabled: true,\n deletedAt: null,\n nextRunAt: { $lte: new Date() },\n })\n\n const queueStrategy = process.env.QUEUE_STRATEGY || 'local'\n\n writeLine('\\n\uD83D\uDCCA Scheduler Status\\n')\n writeLine(`Strategy: ${queueStrategy === 'async' ? 'BullMQ (async)' : 'Local (polling)'}`)\n \n if (queueStrategy === 'local') {\n const pollInterval = parseInt(process.env.SCHEDULER_POLL_INTERVAL_MS || '30000', 10)\n writeLine(`Poll Interval: ${Math.round(pollInterval / 1000)}s`)\n }\n \n writeLine('')\n writeLine('Schedules:')\n writeLine(` Total: ${totalCount}`)\n writeLine(` Enabled: ${enabledCount}`)\n writeLine(` Due Now: ${dueCount}`)\n writeLine('')\n },\n}\n\nconst runCommand: ModuleCli = {\n command: 'run',\n async run(rest) {\n const scheduleId = rest[0]\n if (!scheduleId) {\n writeErrorLine('Usage: mercato scheduler run <schedule-id>')\n return\n }\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const queueService = resolve('queueService') as { getQueue(name: string): { add(name: string, data: unknown): Promise<unknown> } }\n\n const job = await em.findOne(ScheduledJob, { id: scheduleId, deletedAt: null })\n if (!job) {\n writeErrorLine(`Schedule not found: ${scheduleId}`)\n return\n }\n\n writeLine(`\\n\uD83D\uDE80 Manually triggering schedule: ${job.name}\\n`)\n writeLine(` ID: ${job.id}`)\n writeLine(` Type: ${job.scheduleType}`)\n writeLine(` Schedule: ${job.scheduleValue}`)\n writeLine(` Target: ${job.targetType === 'queue' ? job.targetQueue : job.targetCommand}`)\n writeLine('')\n\n try {\n // Manually enqueue the job (triggering the scheduler-execution worker)\n const schedulerQueue = queueService.getQueue('scheduler-execution')\n await schedulerQueue.add('execute-schedule', { scheduleId: job.id })\n\n writeLine('\u2713 Job successfully triggered via scheduler-execution queue')\n writeLine(` The worker will pick it up and enqueue to: ${job.targetType === 'queue' ? job.targetQueue : job.targetCommand}`)\n writeLine('\u2713 Manual trigger completed\\n')\n } catch (error: unknown) {\n writeErrorLine(`\u2717 Failed to trigger job: ${error instanceof Error ? error.message : String(error)}`)\n process.exit(1)\n }\n },\n}\n\nconst startCommand: ModuleCli = {\n command: 'start',\n async run() {\n const { resolve } = await createRequestContainer()\n const queueStrategy = process.env.QUEUE_STRATEGY || 'local'\n\n writeLine(`\uD83D\uDE80 Starting scheduler (strategy: ${queueStrategy})...\\n`)\n\n if (queueStrategy === 'async') {\n // BullMQ strategy: Sync schedules with BullMQ repeatable jobs\n try {\n const bullmqService = resolve('bullmqSchedulerService') as { syncAll(): Promise<void> } | undefined\n \n if (!bullmqService) {\n writeErrorLine('\u274C BullMQSchedulerService not available.')\n writeErrorLine(' Set QUEUE_STRATEGY=async and configure REDIS_URL.')\n process.exit(1)\n }\n\n // Sync all enabled schedules with BullMQ\n await bullmqService.syncAll()\n\n writeLine('\u2713 Scheduler sync completed')\n writeLine('')\n writeLine('BullMQ is managing all schedules.')\n writeLine('Start workers to process jobs:')\n writeLine(' yarn mercato worker:start')\n writeLine('')\n } catch (error: unknown) {\n writeErrorLine(`\u274C Failed to sync schedules: ${error instanceof Error ? error.message : String(error)}`)\n process.exit(1)\n }\n } else {\n // Local strategy: Start polling engine\n try {\n const localService = resolve('localSchedulerService') as { start(): Promise<void>; stop(): Promise<void> } | undefined\n \n if (!localService) {\n writeErrorLine('\u274C LocalSchedulerService not available.')\n writeErrorLine(' This should not happen in local mode.')\n process.exit(1)\n }\n\n const pollInterval = parseInt(process.env.SCHEDULER_POLL_INTERVAL_MS || '30000', 10)\n\n // Start the local polling engine\n await localService.start()\n\n writeLine(`\u2713 Local scheduler started (polling every ${Math.round(pollInterval / 1000)} seconds)`)\n writeLine('Press Ctrl+C to stop.')\n writeLine('')\n writeLine('\uD83D\uDCA1 Tip: For production, use QUEUE_STRATEGY=async with Redis.')\n writeLine('')\n\n // Keep the process alive and handle graceful shutdown\n const gracefulShutdown = async () => {\n writeLine('\\n\uD83D\uDCDB Shutting down...')\n await localService.stop()\n writeLine('\u2713 Stopped')\n process.exit(0)\n }\n\n process.on('SIGINT', gracefulShutdown)\n process.on('SIGTERM', gracefulShutdown)\n\n // Keep process alive\n await new Promise(() => {}) // Never resolves\n } catch (error: unknown) {\n writeErrorLine(`\u274C Failed to start local scheduler: ${error instanceof Error ? error.message : String(error)}`)\n process.exit(1)\n }\n }\n },\n}\n\nexport default [listCommand, statusCommand, runCommand, startCommand]\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,8BAA8B;AAEvC,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { ScheduledJob } from './data/entities.js'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { auditSchedulerModuleQueueRows } from './lib/safeQueueTargets'\n\nconst writeLine = (text = '') => {\n process.stdout.write(`${text}\\n`)\n}\n\nconst writeErrorLine = (text: string) => {\n process.stderr.write(`${text}\\n`)\n}\n\nfunction parseArgs(rest: string[]): Record<string, string> {\n const args: Record<string, string> = {}\n for (let i = 0; i < rest.length; i += 2) {\n const key = rest[i]?.replace(/^--/, '')\n const value = rest[i + 1]\n if (key) args[key] = value ?? ''\n }\n return args\n}\n\nconst listCommand: ModuleCli = {\n command: 'list',\n async run(rest) {\n const args = parseArgs(rest)\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n\n const where: Record<string, unknown> = { deletedAt: null }\n\n // Filter by tenant if provided\n if (args.tenant || args.tenantId) {\n where.tenantId = args.tenant || args.tenantId\n }\n\n // Filter by scope type if provided\n if (args.scope || args.scopeType) {\n where.scopeType = args.scope || args.scopeType\n }\n\n // Filter by enabled status if provided\n if (args.enabled) {\n const parsed = parseBooleanToken(args.enabled)\n if (parsed !== null) {\n where.isEnabled = parsed\n }\n }\n\n const jobs = await em.find(ScheduledJob, where, {\n orderBy: { isEnabled: 'DESC', name: 'ASC' },\n })\n\n if (jobs.length === 0) {\n writeLine('No scheduled jobs found.')\n return\n }\n\n writeLine(`\\nFound ${jobs.length} scheduled job(s):\\n`)\n writeLine('ID'.padEnd(38) + 'Name'.padEnd(35) + 'Type'.padEnd(12) + 'Schedule'.padEnd(20) + 'Status'.padEnd(10) + 'Next Run')\n writeLine('-'.repeat(140))\n\n for (const job of jobs) {\n const id = String(job.id).padEnd(38)\n const name = (job.name || '').substring(0, 33).padEnd(35)\n const type = job.scheduleType.padEnd(12)\n const schedule = (job.scheduleValue || '').substring(0, 18).padEnd(20)\n const status = (job.isEnabled ? '\u2713 Enabled' : '\u2717 Disabled').padEnd(10)\n const nextRun = job.nextRunAt ? new Date(job.nextRunAt).toISOString() : 'N/A'\n writeLine(`${id}${name}${type}${schedule}${status}${nextRun}`)\n }\n\n writeLine('')\n },\n}\n\nconst statusCommand: ModuleCli = {\n command: 'status',\n async run() {\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n\n const totalCount = await em.count(ScheduledJob, { deletedAt: null })\n const enabledCount = await em.count(ScheduledJob, { isEnabled: true, deletedAt: null })\n const dueCount = await em.count(ScheduledJob, {\n isEnabled: true,\n deletedAt: null,\n nextRunAt: { $lte: new Date() },\n })\n\n const queueStrategy = process.env.QUEUE_STRATEGY || 'local'\n\n writeLine('\\n\uD83D\uDCCA Scheduler Status\\n')\n writeLine(`Strategy: ${queueStrategy === 'async' ? 'BullMQ (async)' : 'Local (polling)'}`)\n \n if (queueStrategy === 'local') {\n const pollInterval = parseInt(process.env.SCHEDULER_POLL_INTERVAL_MS || '30000', 10)\n writeLine(`Poll Interval: ${Math.round(pollInterval / 1000)}s`)\n }\n \n writeLine('')\n writeLine('Schedules:')\n writeLine(` Total: ${totalCount}`)\n writeLine(` Enabled: ${enabledCount}`)\n writeLine(` Due Now: ${dueCount}`)\n writeLine('')\n },\n}\n\nconst runCommand: ModuleCli = {\n command: 'run',\n async run(rest) {\n const scheduleId = rest[0]\n if (!scheduleId) {\n writeErrorLine('Usage: mercato scheduler run <schedule-id>')\n return\n }\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const queueService = resolve('queueService') as { getQueue(name: string): { add(name: string, data: unknown): Promise<unknown> } }\n\n const job = await em.findOne(ScheduledJob, { id: scheduleId, deletedAt: null })\n if (!job) {\n writeErrorLine(`Schedule not found: ${scheduleId}`)\n return\n }\n\n writeLine(`\\n\uD83D\uDE80 Manually triggering schedule: ${job.name}\\n`)\n writeLine(` ID: ${job.id}`)\n writeLine(` Type: ${job.scheduleType}`)\n writeLine(` Schedule: ${job.scheduleValue}`)\n writeLine(` Target: ${job.targetType === 'queue' ? job.targetQueue : job.targetCommand}`)\n writeLine('')\n\n try {\n // Manually enqueue the job (triggering the scheduler-execution worker)\n const schedulerQueue = queueService.getQueue('scheduler-execution')\n await schedulerQueue.add('execute-schedule', { scheduleId: job.id })\n\n writeLine('\u2713 Job successfully triggered via scheduler-execution queue')\n writeLine(` The worker will pick it up and enqueue to: ${job.targetType === 'queue' ? job.targetQueue : job.targetCommand}`)\n writeLine('\u2713 Manual trigger completed\\n')\n } catch (error: unknown) {\n writeErrorLine(`\u2717 Failed to trigger job: ${error instanceof Error ? error.message : String(error)}`)\n process.exit(1)\n }\n },\n}\n\nconst startCommand: ModuleCli = {\n command: 'start',\n async run() {\n const { resolve } = await createRequestContainer()\n const queueStrategy = process.env.QUEUE_STRATEGY || 'local'\n\n writeLine(`\uD83D\uDE80 Starting scheduler (strategy: ${queueStrategy})...\\n`)\n\n if (queueStrategy === 'async') {\n // BullMQ strategy: Sync schedules with BullMQ repeatable jobs\n try {\n const bullmqService = resolve('bullmqSchedulerService') as { syncAll(): Promise<void> } | undefined\n \n if (!bullmqService) {\n writeErrorLine('\u274C BullMQSchedulerService not available.')\n writeErrorLine(' Set QUEUE_STRATEGY=async and configure REDIS_URL.')\n process.exit(1)\n }\n\n // Sync all enabled schedules with BullMQ\n await bullmqService.syncAll()\n\n writeLine('\u2713 Scheduler sync completed')\n writeLine('')\n writeLine('BullMQ is managing all schedules.')\n writeLine('Start workers to process jobs:')\n writeLine(' yarn mercato worker:start')\n writeLine('')\n } catch (error: unknown) {\n writeErrorLine(`\u274C Failed to sync schedules: ${error instanceof Error ? error.message : String(error)}`)\n process.exit(1)\n }\n } else {\n // Local strategy: Start polling engine\n try {\n const localService = resolve('localSchedulerService') as { start(): Promise<void>; stop(): Promise<void> } | undefined\n \n if (!localService) {\n writeErrorLine('\u274C LocalSchedulerService not available.')\n writeErrorLine(' This should not happen in local mode.')\n process.exit(1)\n }\n\n const pollInterval = parseInt(process.env.SCHEDULER_POLL_INTERVAL_MS || '30000', 10)\n\n // Start the local polling engine\n await localService.start()\n\n writeLine(`\u2713 Local scheduler started (polling every ${Math.round(pollInterval / 1000)} seconds)`)\n writeLine('Press Ctrl+C to stop.')\n writeLine('')\n writeLine('\uD83D\uDCA1 Tip: For production, use QUEUE_STRATEGY=async with Redis.')\n writeLine('')\n\n // Keep the process alive and handle graceful shutdown\n const gracefulShutdown = async () => {\n writeLine('\\n\uD83D\uDCDB Shutting down...')\n await localService.stop()\n writeLine('\u2713 Stopped')\n process.exit(0)\n }\n\n process.on('SIGINT', gracefulShutdown)\n process.on('SIGTERM', gracefulShutdown)\n\n // Keep process alive\n await new Promise(() => {}) // Never resolves\n } catch (error: unknown) {\n writeErrorLine(`\u274C Failed to start local scheduler: ${error instanceof Error ? error.message : String(error)}`)\n process.exit(1)\n }\n }\n },\n}\n\n// Lists every persisted module-authored queue-target row with its current\n// dispatch verdict (#5213). Rows authored before the hardening by a\n// non-user-bound API key are indistinguishable from genuine registrations at\n// rest, so operators review the ALLOWED entries against their known integrations.\nconst auditQueueTargetsCommand: ModuleCli = {\n command: 'audit-queue-targets',\n async run(rest) {\n const args = parseArgs(rest)\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n\n const where: Record<string, unknown> = { deletedAt: null, sourceType: 'module' }\n if (args.tenant || args.tenantId) where.tenantId = args.tenant || args.tenantId\n\n const rows = await em.find(ScheduledJob, where, {\n orderBy: { name: 'ASC' },\n })\n\n const audits = auditSchedulerModuleQueueRows(rows)\n\n writeLine(`Module-authored queue-target schedules: ${audits.length}`)\n let allowedCount = 0\n for (const audit of audits) {\n const verdict = audit.dispatchAllowed ? 'ALLOWED' : 'blocked'\n if (audit.dispatchAllowed) allowedCount += 1\n writeLine(\n `[${verdict}] ${audit.scheduleId}${audit.name ? ` \u2014 ${audit.name}` : ''}` +\n ` module=${audit.sourceModule ?? '(none)'} queue=${audit.targetQueue ?? '(none)'}` +\n ` enabled=${audit.isEnabled ? 'yes' : 'no'}`,\n )\n }\n\n if (allowedCount > 0) {\n writeLine('')\n writeLine(`Review the ${allowedCount} ALLOWED row(s) above against your known module integrations.`)\n writeLine('Pre-upgrade rows created with a non-user-bound API key cannot be distinguished')\n writeLine('from genuine registrations at rest; disable anything unrecognized:')\n writeLine(' yarn mercato scheduler list --tenant <tenantId>')\n }\n },\n}\n\nexport default [listCommand, statusCommand, runCommand, startCommand, auditQueueTargetsCommand]\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,6 +7,11 @@ import { ScheduledJob } from "../data/entities.js";
|
|
|
7
7
|
import { calculateNextRunForWrite } from "../lib/nextRunCalculator.js";
|
|
8
8
|
import { enforceTenantActiveScheduleLimit } from "../lib/activeScheduleLimits.js";
|
|
9
9
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
10
|
+
import {
|
|
11
|
+
assertSchedulerQueueTargetAuthorized,
|
|
12
|
+
isSchedulerSafeQueue,
|
|
13
|
+
validateSchedulerTargetPayload
|
|
14
|
+
} from "../lib/safeQueueTargets.js";
|
|
10
15
|
async function loadScheduleSnapshot(em, scheduleId) {
|
|
11
16
|
const schedule = await em.findOne(ScheduledJob, { id: scheduleId });
|
|
12
17
|
if (!schedule) return null;
|
|
@@ -36,6 +41,48 @@ function toDate(value) {
|
|
|
36
41
|
if (!value) return null;
|
|
37
42
|
return value instanceof Date ? value : new Date(value);
|
|
38
43
|
}
|
|
44
|
+
function canonicalJson(value) {
|
|
45
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value ?? null);
|
|
46
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
47
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b));
|
|
48
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
49
|
+
}
|
|
50
|
+
function normalizeTargetPayloadValue(value) {
|
|
51
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) return null;
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function targetPayloadChanged(inputValue, storedValue) {
|
|
55
|
+
return targetValueChanged(
|
|
56
|
+
normalizeTargetPayloadValue(inputValue),
|
|
57
|
+
normalizeTargetPayloadValue(storedValue)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
function targetValueChanged(inputValue, storedValue) {
|
|
61
|
+
if (inputValue === void 0) return false;
|
|
62
|
+
return canonicalJson(inputValue ?? null) !== canonicalJson(storedValue ?? null);
|
|
63
|
+
}
|
|
64
|
+
async function assertQueueTargetAuthorizedForActor(ctx, queue) {
|
|
65
|
+
const rbacService = (() => {
|
|
66
|
+
try {
|
|
67
|
+
return ctx.container?.resolve("rbacService") ?? null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
const reason = await assertSchedulerQueueTargetAuthorized({
|
|
73
|
+
queue,
|
|
74
|
+
actorUserId: resolveCommandActorUserId(ctx),
|
|
75
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
76
|
+
organizationId: ctx.auth?.orgId ?? null,
|
|
77
|
+
isSuperAdmin: ctx.auth?.isSuperAdmin === true,
|
|
78
|
+
rbacService
|
|
79
|
+
});
|
|
80
|
+
if (reason) throw new CrudHttpError(403, { error: reason });
|
|
81
|
+
}
|
|
82
|
+
async function assertQueueTargetPayloadAllowed(queue, targetPayload) {
|
|
83
|
+
const reason = validateSchedulerTargetPayload(queue, targetPayload);
|
|
84
|
+
if (reason) throw new CrudHttpError(422, { error: `Invalid target payload for queue ${queue}: ${reason}` });
|
|
85
|
+
}
|
|
39
86
|
function resolveCommandActorUserId(ctx) {
|
|
40
87
|
const auth = ctx.auth;
|
|
41
88
|
if (!auth) return null;
|
|
@@ -120,6 +167,10 @@ const createScheduleCommand = {
|
|
|
120
167
|
if (!nextRunAt) {
|
|
121
168
|
throw new Error("Failed to calculate next run time");
|
|
122
169
|
}
|
|
170
|
+
if (input.targetType === "queue" && input.targetQueue) {
|
|
171
|
+
await assertQueueTargetAuthorizedForActor(ctx, input.targetQueue);
|
|
172
|
+
await assertQueueTargetPayloadAllowed(input.targetQueue, input.targetPayload);
|
|
173
|
+
}
|
|
123
174
|
const schedule = em.create(ScheduledJob, {
|
|
124
175
|
name: input.name,
|
|
125
176
|
description: input.description ?? null,
|
|
@@ -135,8 +186,8 @@ const createScheduleCommand = {
|
|
|
135
186
|
targetPayload: input.targetPayload ?? null,
|
|
136
187
|
requireFeature: input.requireFeature ?? null,
|
|
137
188
|
isEnabled: input.isEnabled ?? true,
|
|
138
|
-
sourceType:
|
|
139
|
-
sourceModule:
|
|
189
|
+
sourceType: "user",
|
|
190
|
+
sourceModule: null,
|
|
140
191
|
nextRunAt,
|
|
141
192
|
createdByUserId: resolveCommandActorUserId(ctx),
|
|
142
193
|
createdAt: /* @__PURE__ */ new Date(),
|
|
@@ -199,9 +250,40 @@ const updateScheduleCommand = {
|
|
|
199
250
|
ensureTenantScope(ctx, schedule.tenantId);
|
|
200
251
|
if (schedule.organizationId) ensureOrganizationScope(ctx, schedule.organizationId);
|
|
201
252
|
ensureCanManageSystemScopedJob(ctx, schedule);
|
|
253
|
+
const touchesModuleOwnedTarget = schedule.sourceType === "module" && (targetValueChanged(input.targetType, schedule.targetType) || targetValueChanged(input.targetQueue, schedule.targetQueue) || targetValueChanged(input.targetCommand, schedule.targetCommand) || targetPayloadChanged(input.targetPayload, schedule.targetPayload));
|
|
254
|
+
if (touchesModuleOwnedTarget) {
|
|
255
|
+
const { translate } = await resolveTranslations();
|
|
256
|
+
throw new CrudHttpError(403, {
|
|
257
|
+
error: translate(
|
|
258
|
+
"scheduler.errors.module_target_immutable",
|
|
259
|
+
"Module-authored schedules own their target; only operational fields can be updated here."
|
|
260
|
+
)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
202
263
|
if (input.isEnabled === true && schedule.isEnabled !== true) {
|
|
203
264
|
await enforceTenantActiveScheduleLimit(em, schedule.tenantId);
|
|
204
265
|
}
|
|
266
|
+
const effectiveTargetType = input.targetType ?? schedule.targetType;
|
|
267
|
+
const effectiveQueue = input.targetQueue ?? schedule.targetQueue;
|
|
268
|
+
const queueChanged = effectiveTargetType === "queue" && (targetValueChanged(input.targetQueue, schedule.targetQueue) || targetValueChanged(input.targetType, schedule.targetType) || targetPayloadChanged(input.targetPayload, schedule.targetPayload));
|
|
269
|
+
if (effectiveTargetType === "queue" && effectiveQueue) {
|
|
270
|
+
const retargetedToUnapprovedQueue = input.targetQueue !== void 0 && input.targetQueue !== schedule.targetQueue && !isSchedulerSafeQueue(input.targetQueue);
|
|
271
|
+
const keepsUnapprovedQueueActive = effectiveQueue != null && !isSchedulerSafeQueue(effectiveQueue) && input.isEnabled !== false && schedule.isEnabled !== false;
|
|
272
|
+
if ((retargetedToUnapprovedQueue || keepsUnapprovedQueueActive) && schedule.sourceType !== "module") {
|
|
273
|
+
const { translate } = await resolveTranslations();
|
|
274
|
+
throw new CrudHttpError(422, {
|
|
275
|
+
error: translate(
|
|
276
|
+
"scheduler.errors.queue_not_approved",
|
|
277
|
+
"Target queue is not an approved scheduler target. Only workers that opted into scheduling can be selected."
|
|
278
|
+
)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (queueChanged) {
|
|
282
|
+
await assertQueueTargetAuthorizedForActor(ctx, effectiveQueue);
|
|
283
|
+
const effectivePayload = input.targetPayload !== void 0 ? input.targetPayload : schedule.targetPayload;
|
|
284
|
+
await assertQueueTargetPayloadAllowed(effectiveQueue, effectivePayload);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
205
287
|
const scheduleChanged = input.scheduleType !== void 0 || input.scheduleValue !== void 0 || input.timezone !== void 0;
|
|
206
288
|
const nextRunAt = scheduleChanged ? calculateNextRunForWrite(
|
|
207
289
|
input.scheduleType ?? schedule.scheduleType,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/commands/jobs.ts"],
|
|
4
|
-
"sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { ensureOrganizationScope } from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { ScheduledJob } from '../data/entities.js'\nimport { calculateNextRunForWrite } from '../lib/nextRunCalculator.js'\nimport { enforceTenantActiveScheduleLimit } from '../lib/activeScheduleLimits.js'\nimport type {\n ScheduleCreateInput,\n ScheduleUpdateInput,\n} from '../data/validators.js'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { BullMQSchedulerService } from '../services/bullmqSchedulerService.js'\n\n/**\n * Snapshot of a schedule for undo/redo\n */\ntype ScheduleSnapshot = {\n id: string\n name: string\n description: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId: string | null\n tenantId: string | null\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue: string | null\n targetCommand: string | null\n targetPayload: Record<string, unknown> | null\n requireFeature: string | null\n isEnabled: boolean\n sourceType: 'user' | 'module'\n sourceModule: string | null\n nextRunAt: Date | null\n lastRunAt: Date | null\n}\n\n/**\n * Load a schedule snapshot\n */\nasync function loadScheduleSnapshot(\n em: EntityManager,\n scheduleId: string\n): Promise<ScheduleSnapshot | null> {\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n if (!schedule) return null\n\n return {\n id: schedule.id,\n name: schedule.name,\n description: schedule.description ?? null,\n scopeType: schedule.scopeType,\n organizationId: schedule.organizationId ?? null,\n tenantId: schedule.tenantId ?? null,\n scheduleType: schedule.scheduleType,\n scheduleValue: schedule.scheduleValue,\n timezone: schedule.timezone,\n targetType: schedule.targetType,\n targetQueue: schedule.targetQueue ?? null,\n targetCommand: schedule.targetCommand ?? null,\n targetPayload: schedule.targetPayload ?? null,\n requireFeature: schedule.requireFeature ?? null,\n isEnabled: schedule.isEnabled,\n sourceType: schedule.sourceType,\n sourceModule: schedule.sourceModule ?? null,\n nextRunAt: schedule.nextRunAt ?? null,\n lastRunAt: schedule.lastRunAt ?? null,\n }\n}\n\n/**\n * Snapshots are persisted as JSON (in the action log's command payload), so Date\n * fields come back as ISO strings on undo. Coerce them to Date before writing\n * them onto the entity, otherwise MikroORM throws while flushing a Date column.\n */\nfunction toDate(value: Date | string | null | undefined): Date | null {\n if (!value) return null\n return value instanceof Date ? value : new Date(value)\n}\n\nfunction resolveCommandActorUserId(ctx: CommandRuntimeContext): string | null {\n const auth = ctx.auth\n if (!auth) return null\n if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId.trim()\n if (auth.isApiKey) return null\n return typeof auth.sub === 'string' && auth.sub.trim().length > 0 ? auth.sub.trim() : null\n}\n\n/**\n * Build a full create seed (including the original id and Date-coerced timestamps)\n * from a snapshot. Shared by `materializeScheduleFromSnapshot` (delete-undo) and the\n * create command's id-preserving `redo`.\n */\nfunction scheduleSeedFromSnapshot(snapshot: ScheduleSnapshot): Record<string, unknown> {\n const now = new Date()\n return {\n id: snapshot.id,\n name: snapshot.name,\n description: snapshot.description,\n scopeType: snapshot.scopeType,\n organizationId: snapshot.organizationId,\n tenantId: snapshot.tenantId,\n scheduleType: snapshot.scheduleType,\n scheduleValue: snapshot.scheduleValue,\n timezone: snapshot.timezone,\n targetType: snapshot.targetType,\n targetQueue: snapshot.targetQueue,\n targetCommand: snapshot.targetCommand,\n targetPayload: snapshot.targetPayload,\n requireFeature: snapshot.requireFeature,\n isEnabled: snapshot.isEnabled,\n sourceType: snapshot.sourceType,\n sourceModule: snapshot.sourceModule,\n nextRunAt: toDate(snapshot.nextRunAt),\n lastRunAt: toDate(snapshot.lastRunAt),\n deletedAt: null,\n createdAt: now,\n updatedAt: now,\n }\n}\n\n/**\n * Re-create a ScheduledJob entity from a snapshot, preserving its original id.\n * Used by delete-undo when the row was hard-removed rather than soft-deleted.\n */\nfunction materializeScheduleFromSnapshot(em: EntityManager, snapshot: ScheduleSnapshot): ScheduledJob {\n return em.create(ScheduledJob, scheduleSeedFromSnapshot(snapshot) as never)\n}\n\n/**\n * Trigger BullMQ sync after undo operations.\n * Best-effort: if BullMQ service is unavailable (e.g. local strategy), this is a no-op.\n */\nasync function syncBullMQAfterUndo(ctx: CommandRuntimeContext, schedule: ScheduledJob | null): Promise<void> {\n try {\n if (!ctx.container?.resolve) return\n const bullmqService = ctx.container.resolve<BullMQSchedulerService>('bullmqSchedulerService')\n if (!bullmqService) return\n\n if (schedule && schedule.isEnabled && !schedule.deletedAt) {\n await bullmqService.register(schedule, { skipNextRunUpdate: true })\n } else if (schedule) {\n await bullmqService.unregister(schedule.id)\n }\n } catch {\n // Best-effort: BullMQ service may not be registered (local strategy)\n }\n}\n\n/**\n * Ensure tenant/org scope for security\n */\nfunction ensureTenantScope(ctx: CommandRuntimeContext, tenantId: string | null | undefined) {\n if (tenantId && ctx.auth?.tenantId && ctx.auth.tenantId !== tenantId) {\n throw new Error('Tenant mismatch')\n }\n}\n\n// Super-admin status is the immutable `isSuperAdmin` flag derived from\n// RoleAcl/UserAcl at session resolution. Never compare role names to a string\n// like 'superadmin' \u2014 role names are tenant-mutable and trivially spoofable.\nfunction isSuperAdminActor(ctx: CommandRuntimeContext): boolean {\n return ctx.auth?.isSuperAdmin === true\n}\n\n// `ensureTenantScope` is a no-op for system-scoped jobs (tenantId === null), so a\n// `scheduler.jobs.manage` holder could otherwise update/delete a system schedule\n// belonging to the whole deployment. System-scoped jobs MUST be super-admin only,\n// mirroring the create route's system-scope gate.\nfunction ensureCanManageSystemScopedJob(\n ctx: CommandRuntimeContext,\n job: { scopeType?: string | null; tenantId?: string | null },\n): void {\n const isSystemScoped = job.scopeType === 'system' || job.tenantId == null\n if (!isSystemScoped) return\n if (isSuperAdminActor(ctx)) return\n throw new CrudHttpError(403, {\n error: 'System-scoped scheduled jobs can only be managed by a super administrator.',\n })\n}\n\n/**\n * CREATE SCHEDULE COMMAND\n */\nconst createScheduleCommand: CommandHandler<ScheduleCreateInput, { id: string }> = {\n id: 'scheduler.jobs.create',\n\n async execute(input, ctx) {\n ensureTenantScope(ctx, input.tenantId)\n if (input.organizationId) ensureOrganizationScope(ctx, input.organizationId)\n ensureCanManageSystemScopedJob(ctx, { scopeType: input.scopeType, tenantId: input.tenantId })\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n if (input.isEnabled ?? true) {\n await enforceTenantActiveScheduleLimit(em, input.tenantId)\n }\n\n // Calculate next run time\n const nextRunAt = calculateNextRunForWrite(\n input.scheduleType,\n input.scheduleValue,\n input.timezone || 'UTC'\n )\n\n if (!nextRunAt) {\n throw new Error('Failed to calculate next run time')\n }\n\n // Create schedule\n const schedule = em.create(ScheduledJob, {\n name: input.name,\n description: input.description ?? null,\n scopeType: input.scopeType,\n organizationId: input.organizationId ?? null,\n tenantId: input.tenantId ?? null,\n scheduleType: input.scheduleType,\n scheduleValue: input.scheduleValue,\n timezone: input.timezone ?? 'UTC',\n targetType: input.targetType,\n targetQueue: input.targetQueue ?? null,\n targetCommand: input.targetCommand ?? null,\n targetPayload: input.targetPayload ?? null,\n requireFeature: input.requireFeature ?? null,\n isEnabled: input.isEnabled ?? true,\n sourceType: input.sourceType ?? 'user',\n sourceModule: input.sourceModule ?? null,\n nextRunAt,\n createdByUserId: resolveCommandActorUserId(ctx),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n\n em.persist(schedule)\n await em.flush()\n\n return { id: schedule.id }\n },\n\n async captureAfter(_input, result, ctx) {\n const em = ctx.container.resolve<EntityManager>('em')\n return await loadScheduleSnapshot(em, result.id)\n },\n\n async buildLog({ result, ctx, snapshots }) {\n const { translate } = await resolveTranslations()\n const after = snapshots.after as ScheduleSnapshot | undefined\n\n return {\n actionLabel: translate('scheduler.audit.create', 'Create schedule'),\n resourceKind: 'scheduler.job',\n resourceId: result.id,\n tenantId: after?.tenantId || null,\n organizationId: after?.organizationId || null,\n snapshotAfter: after,\n payload: { undo: { after } },\n }\n },\n\n async undo({ logEntry, ctx }) {\n const after = extractUndoPayload<UndoPayload<ScheduleSnapshot>>(logEntry)?.after\n if (!after) return\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n const schedule = await em.findOne(ScheduledJob, { id: after.id })\n\n if (schedule) {\n await em.remove(schedule).flush()\n await syncBullMQAfterUndo(ctx, schedule)\n }\n },\n\n redo: makeCreateRedo<ScheduledJob, ScheduleSnapshot, ScheduleCreateInput, { id: string }>({\n entityClass: ScheduledJob,\n getSnapshotId: (snapshot) => snapshot.id,\n seedFromSnapshot: scheduleSeedFromSnapshot,\n buildResult: (entity) => ({ id: entity.id }),\n afterRestore: async ({ ctx, entity }) => {\n await syncBullMQAfterUndo(ctx, entity)\n },\n }),\n}\n\n/**\n * UPDATE SCHEDULE COMMAND\n */\nconst updateScheduleCommand: CommandHandler<ScheduleUpdateInput, { ok: boolean }> = {\n id: 'scheduler.jobs.update',\n\n async prepare(input, ctx) {\n const em = ctx.container.resolve<EntityManager>('em')\n const before = await loadScheduleSnapshot(em, input.id)\n return { before }\n },\n\n async execute(input, ctx) {\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null })\n if (!schedule) {\n throw new Error('Schedule not found')\n }\n\n ensureTenantScope(ctx, schedule.tenantId)\n if (schedule.organizationId) ensureOrganizationScope(ctx, schedule.organizationId)\n ensureCanManageSystemScopedJob(ctx, schedule)\n\n if (input.isEnabled === true && schedule.isEnabled !== true) {\n await enforceTenantActiveScheduleLimit(em, schedule.tenantId)\n }\n\n const scheduleChanged = input.scheduleType !== undefined || input.scheduleValue !== undefined || input.timezone !== undefined\n const nextRunAt = scheduleChanged\n ? calculateNextRunForWrite(\n input.scheduleType ?? schedule.scheduleType,\n input.scheduleValue ?? schedule.scheduleValue,\n input.timezone ?? schedule.timezone,\n )\n : null\n\n if (scheduleChanged && !nextRunAt) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(422, {\n error: translate('scheduler.error.invalid_schedule_value', 'Invalid schedule value.'),\n })\n }\n\n // Update fields\n if (input.name !== undefined) schedule.name = input.name\n if (input.description !== undefined) schedule.description = input.description ?? null\n if (input.scheduleType !== undefined) schedule.scheduleType = input.scheduleType\n if (input.scheduleValue !== undefined) schedule.scheduleValue = input.scheduleValue\n if (input.timezone !== undefined) schedule.timezone = input.timezone\n if (input.targetPayload !== undefined) schedule.targetPayload = input.targetPayload ?? null\n if (input.requireFeature !== undefined) schedule.requireFeature = input.requireFeature || null\n if (input.isEnabled !== undefined) schedule.isEnabled = input.isEnabled\n \n // Handle target type changes - clear stale values when switching between queue and command\n if (input.targetType !== undefined) {\n schedule.targetType = input.targetType\n \n if (input.targetType === 'queue') {\n // Switching to queue: set new queue and clear command\n if (input.targetQueue !== undefined) schedule.targetQueue = input.targetQueue\n schedule.targetCommand = null\n } else if (input.targetType === 'command') {\n // Switching to command: set new command and clear queue\n if (input.targetCommand !== undefined) schedule.targetCommand = input.targetCommand\n schedule.targetQueue = null\n }\n } else {\n // targetType not changing, but allow updating individual target fields\n if (input.targetQueue !== undefined) schedule.targetQueue = input.targetQueue\n if (input.targetCommand !== undefined) schedule.targetCommand = input.targetCommand\n }\n\n if (nextRunAt) {\n schedule.nextRunAt = nextRunAt\n }\n\n schedule.updatedAt = new Date()\n schedule.updatedByUserId = resolveCommandActorUserId(ctx)\n\n await em.flush()\n\n return { ok: true }\n },\n\n async captureAfter(input, _result, ctx) {\n const em = ctx.container.resolve<EntityManager>('em')\n return await loadScheduleSnapshot(em, input.id)\n },\n\n async buildLog({ input, ctx, snapshots }) {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as ScheduleSnapshot | undefined\n const after = snapshots.after as ScheduleSnapshot | undefined\n\n return {\n actionLabel: translate('scheduler.audit.update', 'Update schedule'),\n resourceKind: 'scheduler.job',\n resourceId: input.id,\n tenantId: after?.tenantId || null,\n organizationId: after?.organizationId || null,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: { undo: { before, after } },\n }\n },\n\n async undo({ logEntry, ctx }) {\n const before = extractUndoPayload<UndoPayload<ScheduleSnapshot>>(logEntry)?.before\n if (!before) return\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n const schedule = await em.findOne(ScheduledJob, { id: before.id })\n\n if (schedule) {\n // Restore all fields\n schedule.name = before.name\n schedule.description = before.description\n schedule.scopeType = before.scopeType\n schedule.organizationId = before.organizationId\n schedule.tenantId = before.tenantId\n schedule.scheduleType = before.scheduleType\n schedule.scheduleValue = before.scheduleValue\n schedule.timezone = before.timezone\n schedule.targetType = before.targetType\n schedule.targetQueue = before.targetQueue\n schedule.targetCommand = before.targetCommand\n schedule.targetPayload = before.targetPayload\n schedule.requireFeature = before.requireFeature\n schedule.isEnabled = before.isEnabled\n schedule.sourceType = before.sourceType\n schedule.sourceModule = before.sourceModule\n schedule.nextRunAt = toDate(before.nextRunAt)\n schedule.lastRunAt = toDate(before.lastRunAt)\n schedule.updatedAt = new Date()\n\n await em.flush()\n await syncBullMQAfterUndo(ctx, schedule)\n }\n },\n}\n\n/**\n * DELETE SCHEDULE COMMAND\n */\nconst deleteScheduleCommand: CommandHandler<{ id: string }, { ok: boolean }> = {\n id: 'scheduler.jobs.delete',\n\n async prepare(input, ctx) {\n const em = ctx.container.resolve<EntityManager>('em')\n const before = await loadScheduleSnapshot(em, input.id)\n return { before }\n },\n\n async execute(input, ctx) {\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null })\n if (!schedule) {\n throw new Error('Schedule not found')\n }\n\n ensureTenantScope(ctx, schedule.tenantId)\n if (schedule.organizationId) ensureOrganizationScope(ctx, schedule.organizationId)\n ensureCanManageSystemScopedJob(ctx, schedule)\n\n // Soft delete\n schedule.deletedAt = new Date()\n schedule.updatedAt = new Date()\n schedule.updatedByUserId = resolveCommandActorUserId(ctx)\n\n await em.flush()\n\n return { ok: true }\n },\n\n async buildLog({ input, ctx, snapshots }) {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as ScheduleSnapshot | undefined\n\n return {\n actionLabel: translate('scheduler.audit.delete', 'Delete schedule'),\n resourceKind: 'scheduler.job',\n resourceId: input.id,\n tenantId: before?.tenantId || null,\n organizationId: before?.organizationId || null,\n snapshotBefore: before,\n payload: { undo: { before } },\n }\n },\n\n async undo({ logEntry, ctx }) {\n const before = extractUndoPayload<UndoPayload<ScheduleSnapshot>>(logEntry)?.before\n if (!before) return\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n const existing = await em.findOne(ScheduledJob, { id: before.id })\n\n // Soft-deleted rows are restored by clearing `deletedAt`. A hard-removed row\n // (no surviving record) is re-materialized from the snapshot so undo is\n // robust to either deletion strategy \u2014 mirroring the sales reference undo.\n const schedule = existing ?? materializeScheduleFromSnapshot(em, before)\n if (existing) {\n schedule.deletedAt = null\n schedule.updatedAt = new Date()\n }\n await em.flush()\n await syncBullMQAfterUndo(ctx, schedule)\n },\n}\n\n// Register all commands\nregisterCommand(createScheduleCommand)\nregisterCommand(updateScheduleCommand)\nregisterCommand(deleteScheduleCommand)\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,uBAAuB;AAEhC,SAAS,0BAA4C;AACrD,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AACxC,SAAS,qBAAqB;AAE9B,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AACzC,SAAS,wCAAwC;AAKjD,SAAS,2BAA2B;AA+BpC,eAAe,qBACb,IACA,YACkC;AAClC,QAAM,WAAW,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,WAAW,CAAC;AAClE,MAAI,CAAC,SAAU,QAAO;AAEpB,SAAO;AAAA,IACP,IAAI,SAAS;AAAA,IACb,MAAM,SAAS;AAAA,IACf,aAAa,SAAS,eAAe;AAAA,IACrC,WAAW,SAAS;AAAA,IACpB,gBAAgB,SAAS,kBAAkB;AAAA,IAC3C,UAAU,SAAS,YAAY;AAAA,IAC/B,cAAc,SAAS;AAAA,IACvB,eAAe,SAAS;AAAA,IACxB,UAAU,SAAS;AAAA,IACnB,YAAY,SAAS;AAAA,IACrB,aAAa,SAAS,eAAe;AAAA,IACrC,eAAe,SAAS,iBAAiB;AAAA,IACzC,eAAe,SAAS,iBAAiB;AAAA,IACzC,gBAAgB,SAAS,kBAAkB;AAAA,IAC3C,WAAW,SAAS;AAAA,IACpB,YAAY,SAAS;AAAA,IACrB,cAAc,SAAS,gBAAgB;AAAA,IACvC,WAAW,SAAS,aAAa;AAAA,IACjC,WAAW,SAAS,aAAa;AAAA,EACnC;AACF;AAOA,SAAS,OAAO,OAAsD;AACpE,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AACvD;AAEA,SAAS,0BAA0B,KAA2C;AAC5E,QAAM,OAAO,IAAI;AACjB,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,OAAO,KAAK,WAAW,YAAY,KAAK,OAAO,KAAK,EAAE,SAAS,EAAG,QAAO,KAAK,OAAO,KAAK;AAC9F,MAAI,KAAK,SAAU,QAAO;AAC1B,SAAO,OAAO,KAAK,QAAQ,YAAY,KAAK,IAAI,KAAK,EAAE,SAAS,IAAI,KAAK,IAAI,KAAK,IAAI;AACxF;AAOA,SAAS,yBAAyB,UAAqD;AACrF,QAAM,MAAM,oBAAI,KAAK;AACrB,SAAO;AAAA,IACL,IAAI,SAAS;AAAA,IACb,MAAM,SAAS;AAAA,IACf,aAAa,SAAS;AAAA,IACtB,WAAW,SAAS;AAAA,IACpB,gBAAgB,SAAS;AAAA,IACzB,UAAU,SAAS;AAAA,IACnB,cAAc,SAAS;AAAA,IACvB,eAAe,SAAS;AAAA,IACxB,UAAU,SAAS;AAAA,IACnB,YAAY,SAAS;AAAA,IACrB,aAAa,SAAS;AAAA,IACtB,eAAe,SAAS;AAAA,IACxB,eAAe,SAAS;AAAA,IACxB,gBAAgB,SAAS;AAAA,IACzB,WAAW,SAAS;AAAA,IACpB,YAAY,SAAS;AAAA,IACrB,cAAc,SAAS;AAAA,IACvB,WAAW,OAAO,SAAS,SAAS;AAAA,IACpC,WAAW,OAAO,SAAS,SAAS;AAAA,IACpC,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACF;AAMA,SAAS,gCAAgC,IAAmB,UAA0C;AACpG,SAAO,GAAG,OAAO,cAAc,yBAAyB,QAAQ,CAAU;AAC5E;AAMA,eAAe,oBAAoB,KAA4B,UAA8C;AAC3G,MAAI;AACF,QAAI,CAAC,IAAI,WAAW,QAAS;AAC7B,UAAM,gBAAgB,IAAI,UAAU,QAAgC,wBAAwB;AAC5F,QAAI,CAAC,cAAe;AAEpB,QAAI,YAAY,SAAS,aAAa,CAAC,SAAS,WAAW;AACzD,YAAM,cAAc,SAAS,UAAU,EAAE,mBAAmB,KAAK,CAAC;AAAA,IACpE,WAAW,UAAU;AACnB,YAAM,cAAc,WAAW,SAAS,EAAE;AAAA,IAC5C;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAKA,SAAS,kBAAkB,KAA4B,UAAqC;AAC1F,MAAI,YAAY,IAAI,MAAM,YAAY,IAAI,KAAK,aAAa,UAAU;AACpE,UAAM,IAAI,MAAM,iBAAiB;AAAA,EACnC;AACF;AAKA,SAAS,kBAAkB,KAAqC;AAC9D,SAAO,IAAI,MAAM,iBAAiB;AACpC;AAMA,SAAS,+BACP,KACA,KACM;AACN,QAAM,iBAAiB,IAAI,cAAc,YAAY,IAAI,YAAY;AACrE,MAAI,CAAC,eAAgB;AACrB,MAAI,kBAAkB,GAAG,EAAG;AAC5B,QAAM,IAAI,cAAc,KAAK;AAAA,IAC3B,OAAO;AAAA,EACT,CAAC;AACH;AAKA,MAAM,wBAA6E;AAAA,EACjF,IAAI;AAAA,EAEJ,MAAM,QAAQ,OAAO,KAAK;AACxB,sBAAkB,KAAK,MAAM,QAAQ;AACrC,QAAI,MAAM,eAAgB,yBAAwB,KAAK,MAAM,cAAc;AAC3E,mCAA+B,KAAK,EAAE,WAAW,MAAM,WAAW,UAAU,MAAM,SAAS,CAAC;AAE5F,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI,EAAE,KAAK;AAE3D,QAAI,MAAM,aAAa,MAAM;AAC3B,YAAM,iCAAiC,IAAI,MAAM,QAAQ;AAAA,IAC3D;AAGA,UAAM,YAAY;AAAA,MAChB,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM,YAAY;AAAA,IACpB;AAEA,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AAGA,UAAM,WAAW,GAAG,OAAO,cAAc;AAAA,MACvC,MAAM,MAAM;AAAA,MACZ,aAAa,MAAM,eAAe;AAAA,MAClC,WAAW,MAAM;AAAA,MACjB,gBAAgB,MAAM,kBAAkB;AAAA,MACxC,UAAU,MAAM,YAAY;AAAA,MAC5B,cAAc,MAAM;AAAA,MACpB,eAAe,MAAM;AAAA,MACrB,UAAU,MAAM,YAAY;AAAA,MAC5B,YAAY,MAAM;AAAA,MAClB,aAAa,MAAM,eAAe;AAAA,MAClC,eAAe,MAAM,iBAAiB;AAAA,MACtC,eAAe,MAAM,iBAAiB;AAAA,MACtC,gBAAgB,MAAM,kBAAkB;AAAA,MACxC,WAAW,MAAM,aAAa;AAAA,MAC9B,YAAY,MAAM,cAAc;AAAA,MAChC,cAAc,MAAM,gBAAgB;AAAA,MACpC;AAAA,MACA,iBAAiB,0BAA0B,GAAG;AAAA,MAC9C,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AAED,OAAG,QAAQ,QAAQ;AACnB,UAAM,GAAG,MAAM;AAEf,WAAO,EAAE,IAAI,SAAS,GAAG;AAAA,EAC3B;AAAA,EAEA,MAAM,aAAa,QAAQ,QAAQ,KAAK;AACtC,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI;AACpD,WAAO,MAAM,qBAAqB,IAAI,OAAO,EAAE;AAAA,EACjD;AAAA,EAEA,MAAM,SAAS,EAAE,QAAQ,KAAK,UAAU,GAAG;AACzC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,QAAQ,UAAU;AAExB,WAAO;AAAA,MACL,aAAa,UAAU,0BAA0B,iBAAiB;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC,eAAe;AAAA,MACf,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,EAAE,UAAU,IAAI,GAAG;AAC5B,UAAM,QAAQ,mBAAkD,QAAQ,GAAG;AAC3E,QAAI,CAAC,MAAO;AAEZ,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI,EAAE,KAAK;AAC3D,UAAM,WAAW,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,MAAM,GAAG,CAAC;AAEhE,QAAI,UAAU;AACZ,YAAM,GAAG,OAAO,QAAQ,EAAE,MAAM;AAChC,YAAM,oBAAoB,KAAK,QAAQ;AAAA,IACzC;AAAA,EACF;AAAA,EAEA,MAAM,eAAoF;AAAA,IACxF,aAAa;AAAA,IACb,eAAe,CAAC,aAAa,SAAS;AAAA,IACtC,kBAAkB;AAAA,IAClB,aAAa,CAAC,YAAY,EAAE,IAAI,OAAO,GAAG;AAAA,IAC1C,cAAc,OAAO,EAAE,KAAK,OAAO,MAAM;AACvC,YAAM,oBAAoB,KAAK,MAAM;AAAA,IACvC;AAAA,EACF,CAAC;AACH;AAKA,MAAM,wBAA8E;AAAA,EAClF,IAAI;AAAA,EAEJ,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI;AACpD,UAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM,EAAE;AACtD,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI,EAAE,KAAK;AAE3D,UAAM,WAAW,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,MAAM,IAAI,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,oBAAoB;AAAA,IACtC;AAEA,sBAAkB,KAAK,SAAS,QAAQ;AACxC,QAAI,SAAS,eAAgB,yBAAwB,KAAK,SAAS,cAAc;AACjF,mCAA+B,KAAK,QAAQ;AAE5C,QAAI,MAAM,cAAc,QAAQ,SAAS,cAAc,MAAM;AAC3D,YAAM,iCAAiC,IAAI,SAAS,QAAQ;AAAA,IAC9D;AAEA,UAAM,kBAAkB,MAAM,iBAAiB,UAAa,MAAM,kBAAkB,UAAa,MAAM,aAAa;AACpH,UAAM,YAAY,kBACd;AAAA,MACA,MAAM,gBAAgB,SAAS;AAAA,MAC/B,MAAM,iBAAiB,SAAS;AAAA,MAChC,MAAM,YAAY,SAAS;AAAA,IAC7B,IACE;AAEJ,QAAI,mBAAmB,CAAC,WAAW;AACjC,YAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,YAAM,IAAI,cAAc,KAAK;AAAA,QAC3B,OAAO,UAAU,0CAA0C,yBAAyB;AAAA,MACtF,CAAC;AAAA,IACH;AAGA,QAAI,MAAM,SAAS,OAAW,UAAS,OAAO,MAAM;AACpD,QAAI,MAAM,gBAAgB,OAAW,UAAS,cAAc,MAAM,eAAe;AACjF,QAAI,MAAM,iBAAiB,OAAW,UAAS,eAAe,MAAM;AACpE,QAAI,MAAM,kBAAkB,OAAW,UAAS,gBAAgB,MAAM;AACtE,QAAI,MAAM,aAAa,OAAW,UAAS,WAAW,MAAM;AAC5D,QAAI,MAAM,kBAAkB,OAAW,UAAS,gBAAgB,MAAM,iBAAiB;AACvF,QAAI,MAAM,mBAAmB,OAAW,UAAS,iBAAiB,MAAM,kBAAkB;AAC1F,QAAI,MAAM,cAAc,OAAW,UAAS,YAAY,MAAM;AAG9D,QAAI,MAAM,eAAe,QAAW;AAClC,eAAS,aAAa,MAAM;AAE5B,UAAI,MAAM,eAAe,SAAS;AAEhC,YAAI,MAAM,gBAAgB,OAAW,UAAS,cAAc,MAAM;AAClE,iBAAS,gBAAgB;AAAA,MAC3B,WAAW,MAAM,eAAe,WAAW;AAEzC,YAAI,MAAM,kBAAkB,OAAW,UAAS,gBAAgB,MAAM;AACtE,iBAAS,cAAc;AAAA,MACzB;AAAA,IACF,OAAO;AAEL,UAAI,MAAM,gBAAgB,OAAW,UAAS,cAAc,MAAM;AAClE,UAAI,MAAM,kBAAkB,OAAW,UAAS,gBAAgB,MAAM;AAAA,IACxE;AAEA,QAAI,WAAW;AACb,eAAS,YAAY;AAAA,IACvB;AAEA,aAAS,YAAY,oBAAI,KAAK;AAC9B,aAAS,kBAAkB,0BAA0B,GAAG;AAExD,UAAM,GAAG,MAAM;AAEf,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AAAA,EAEA,MAAM,aAAa,OAAO,SAAS,KAAK;AACtC,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI;AACpD,WAAO,MAAM,qBAAqB,IAAI,MAAM,EAAE;AAAA,EAChD;AAAA,EAEA,MAAM,SAAS,EAAE,OAAO,KAAK,UAAU,GAAG;AACxC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,SAAS,UAAU;AACzB,UAAM,QAAQ,UAAU;AAExB,WAAO;AAAA,MACL,aAAa,UAAU,0BAA0B,iBAAiB;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,MAAM;AAAA,MAClB,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,SAAS,EAAE,MAAM,EAAE,QAAQ,MAAM,EAAE;AAAA,IACrC;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,EAAE,UAAU,IAAI,GAAG;AAC5B,UAAM,SAAS,mBAAkD,QAAQ,GAAG;AAC5E,QAAI,CAAC,OAAQ;AAEb,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI,EAAE,KAAK;AAC3D,UAAM,WAAW,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,OAAO,GAAG,CAAC;AAEjE,QAAI,UAAU;AAEZ,eAAS,OAAO,OAAO;AACvB,eAAS,cAAc,OAAO;AAC9B,eAAS,YAAY,OAAO;AAC5B,eAAS,iBAAiB,OAAO;AACjC,eAAS,WAAW,OAAO;AAC3B,eAAS,eAAe,OAAO;AAC/B,eAAS,gBAAgB,OAAO;AAChC,eAAS,WAAW,OAAO;AAC3B,eAAS,aAAa,OAAO;AAC7B,eAAS,cAAc,OAAO;AAC9B,eAAS,gBAAgB,OAAO;AAChC,eAAS,gBAAgB,OAAO;AAChC,eAAS,iBAAiB,OAAO;AACjC,eAAS,YAAY,OAAO;AAC5B,eAAS,aAAa,OAAO;AAC7B,eAAS,eAAe,OAAO;AAC/B,eAAS,YAAY,OAAO,OAAO,SAAS;AAC5C,eAAS,YAAY,OAAO,OAAO,SAAS;AAC5C,eAAS,YAAY,oBAAI,KAAK;AAE9B,YAAM,GAAG,MAAM;AACf,YAAM,oBAAoB,KAAK,QAAQ;AAAA,IACzC;AAAA,EACF;AACF;AAKA,MAAM,wBAAyE;AAAA,EAC7E,IAAI;AAAA,EAEJ,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI;AACpD,UAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM,EAAE;AACtD,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI,EAAE,KAAK;AAE3D,UAAM,WAAW,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,MAAM,IAAI,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,oBAAoB;AAAA,IACtC;AAEA,sBAAkB,KAAK,SAAS,QAAQ;AACxC,QAAI,SAAS,eAAgB,yBAAwB,KAAK,SAAS,cAAc;AACjF,mCAA+B,KAAK,QAAQ;AAG5C,aAAS,YAAY,oBAAI,KAAK;AAC9B,aAAS,YAAY,oBAAI,KAAK;AAC9B,aAAS,kBAAkB,0BAA0B,GAAG;AAExD,UAAM,GAAG,MAAM;AAEf,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AAAA,EAEA,MAAM,SAAS,EAAE,OAAO,KAAK,UAAU,GAAG;AACxC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,SAAS,UAAU;AAEzB,WAAO;AAAA,MACL,aAAa,UAAU,0BAA0B,iBAAiB;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,MAAM;AAAA,MAClB,UAAU,QAAQ,YAAY;AAAA,MAC9B,gBAAgB,QAAQ,kBAAkB;AAAA,MAC1C,gBAAgB;AAAA,MAChB,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,EAAE,UAAU,IAAI,GAAG;AAC5B,UAAM,SAAS,mBAAkD,QAAQ,GAAG;AAC5E,QAAI,CAAC,OAAQ;AAEb,UAAM,KAAK,IAAI,UAAU,QAAuB,IAAI,EAAE,KAAK;AAC3D,UAAM,WAAW,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,OAAO,GAAG,CAAC;AAKjE,UAAM,WAAW,YAAY,gCAAgC,IAAI,MAAM;AACvE,QAAI,UAAU;AACZ,eAAS,YAAY;AACrB,eAAS,YAAY,oBAAI,KAAK;AAAA,IAChC;AACA,UAAM,GAAG,MAAM;AACf,UAAM,oBAAoB,KAAK,QAAQ;AAAA,EACzC;AACF;AAGA,gBAAgB,qBAAqB;AACrC,gBAAgB,qBAAqB;AACrC,gBAAgB,qBAAqB;",
|
|
4
|
+
"sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { ensureOrganizationScope } from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { ScheduledJob } from '../data/entities.js'\nimport { calculateNextRunForWrite } from '../lib/nextRunCalculator.js'\nimport { enforceTenantActiveScheduleLimit } from '../lib/activeScheduleLimits.js'\nimport type {\n ScheduleCreateInput,\n ScheduleUpdateInput,\n} from '../data/validators.js'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { BullMQSchedulerService } from '../services/bullmqSchedulerService.js'\nimport {\n assertSchedulerQueueTargetAuthorized,\n isSchedulerSafeQueue,\n validateSchedulerTargetPayload,\n} from '../lib/safeQueueTargets.js'\nimport type { SchedulerTargetRbacService } from '../lib/safeQueueTargets.js'\n/**\n * Snapshot of a schedule for undo/redo\n */\ntype ScheduleSnapshot = {\n id: string\n name: string\n description: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId: string | null\n tenantId: string | null\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue: string | null\n targetCommand: string | null\n targetPayload: Record<string, unknown> | null\n requireFeature: string | null\n isEnabled: boolean\n sourceType: 'user' | 'module'\n sourceModule: string | null\n nextRunAt: Date | null\n lastRunAt: Date | null\n}\n\n/**\n * Load a schedule snapshot\n */\nasync function loadScheduleSnapshot(\n em: EntityManager,\n scheduleId: string\n): Promise<ScheduleSnapshot | null> {\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n if (!schedule) return null\n\n return {\n id: schedule.id,\n name: schedule.name,\n description: schedule.description ?? null,\n scopeType: schedule.scopeType,\n organizationId: schedule.organizationId ?? null,\n tenantId: schedule.tenantId ?? null,\n scheduleType: schedule.scheduleType,\n scheduleValue: schedule.scheduleValue,\n timezone: schedule.timezone,\n targetType: schedule.targetType,\n targetQueue: schedule.targetQueue ?? null,\n targetCommand: schedule.targetCommand ?? null,\n targetPayload: schedule.targetPayload ?? null,\n requireFeature: schedule.requireFeature ?? null,\n isEnabled: schedule.isEnabled,\n sourceType: schedule.sourceType,\n sourceModule: schedule.sourceModule ?? null,\n nextRunAt: schedule.nextRunAt ?? null,\n lastRunAt: schedule.lastRunAt ?? null,\n }\n}\n\n/**\n * Snapshots are persisted as JSON (in the action log's command payload), so Date\n * fields come back as ISO strings on undo. Coerce them to Date before writing\n * them onto the entity, otherwise MikroORM throws while flushing a Date column.\n */\nfunction toDate(value: Date | string | null | undefined): Date | null {\n if (!value) return null\n return value instanceof Date ? value : new Date(value)\n}\n\nfunction canonicalJson(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null)\n if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => a.localeCompare(b))\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(',')}}`\n}\n\nfunction normalizeTargetPayloadValue(value: unknown): unknown {\n // `schedulerService.register()` persists `{}` as-is while the edit form sends\n // `null` for an empty payload \u2014 treat the three empty shapes as equal (#5213).\n if (value !== null && typeof value === 'object' && !Array.isArray(value)\n && Object.keys(value as Record<string, unknown>).length === 0) return null\n return value\n}\n\nfunction targetPayloadChanged(inputValue: unknown, storedValue: unknown): boolean {\n return targetValueChanged(\n normalizeTargetPayloadValue(inputValue),\n normalizeTargetPayloadValue(storedValue),\n )\n}\n\nfunction targetValueChanged(inputValue: unknown, storedValue: unknown): boolean {\n if (inputValue === undefined) return false\n return canonicalJson(inputValue ?? null) !== canonicalJson(storedValue ?? null)\n}\n\ntype QueueTargetAuthorizationContext = CommandRuntimeContext\n\nasync function assertQueueTargetAuthorizedForActor(\n ctx: QueueTargetAuthorizationContext,\n queue: string,\n): Promise<void> {\n const rbacService = (() => {\n try {\n return ctx.container?.resolve<SchedulerTargetRbacService>('rbacService') ?? null\n } catch {\n return null\n }\n })()\n const reason = await assertSchedulerQueueTargetAuthorized({\n queue,\n actorUserId: resolveCommandActorUserId(ctx),\n tenantId: ctx.auth?.tenantId ?? null,\n organizationId: ctx.auth?.orgId ?? null,\n isSuperAdmin: ctx.auth?.isSuperAdmin === true,\n rbacService,\n })\n if (reason) throw new CrudHttpError(403, { error: reason })\n}\n\nasync function assertQueueTargetPayloadAllowed(queue: string, targetPayload: Record<string, unknown> | null | undefined): Promise<void> {\n const reason = validateSchedulerTargetPayload(queue, targetPayload)\n if (reason) throw new CrudHttpError(422, { error: `Invalid target payload for queue ${queue}: ${reason}` })\n}\n\nfunction resolveCommandActorUserId(ctx: CommandRuntimeContext): string | null {\n const auth = ctx.auth\n if (!auth) return null\n if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId.trim()\n if (auth.isApiKey) return null\n return typeof auth.sub === 'string' && auth.sub.trim().length > 0 ? auth.sub.trim() : null\n}\n\n/**\n * Build a full create seed (including the original id and Date-coerced timestamps)\n * from a snapshot. Shared by `materializeScheduleFromSnapshot` (delete-undo) and the\n * create command's id-preserving `redo`.\n */\nfunction scheduleSeedFromSnapshot(snapshot: ScheduleSnapshot): Record<string, unknown> {\n const now = new Date()\n return {\n id: snapshot.id,\n name: snapshot.name,\n description: snapshot.description,\n scopeType: snapshot.scopeType,\n organizationId: snapshot.organizationId,\n tenantId: snapshot.tenantId,\n scheduleType: snapshot.scheduleType,\n scheduleValue: snapshot.scheduleValue,\n timezone: snapshot.timezone,\n targetType: snapshot.targetType,\n targetQueue: snapshot.targetQueue,\n targetCommand: snapshot.targetCommand,\n targetPayload: snapshot.targetPayload,\n requireFeature: snapshot.requireFeature,\n isEnabled: snapshot.isEnabled,\n sourceType: snapshot.sourceType,\n sourceModule: snapshot.sourceModule,\n nextRunAt: toDate(snapshot.nextRunAt),\n lastRunAt: toDate(snapshot.lastRunAt),\n deletedAt: null,\n createdAt: now,\n updatedAt: now,\n }\n}\n\n/**\n * Re-create a ScheduledJob entity from a snapshot, preserving its original id.\n * Used by delete-undo when the row was hard-removed rather than soft-deleted.\n */\nfunction materializeScheduleFromSnapshot(em: EntityManager, snapshot: ScheduleSnapshot): ScheduledJob {\n return em.create(ScheduledJob, scheduleSeedFromSnapshot(snapshot) as never)\n}\n\n/**\n * Trigger BullMQ sync after undo operations.\n * Best-effort: if BullMQ service is unavailable (e.g. local strategy), this is a no-op.\n */\nasync function syncBullMQAfterUndo(ctx: CommandRuntimeContext, schedule: ScheduledJob | null): Promise<void> {\n try {\n if (!ctx.container?.resolve) return\n const bullmqService = ctx.container.resolve<BullMQSchedulerService>('bullmqSchedulerService')\n if (!bullmqService) return\n\n if (schedule && schedule.isEnabled && !schedule.deletedAt) {\n await bullmqService.register(schedule, { skipNextRunUpdate: true })\n } else if (schedule) {\n await bullmqService.unregister(schedule.id)\n }\n } catch {\n // Best-effort: BullMQ service may not be registered (local strategy)\n }\n}\n\n/**\n * Ensure tenant/org scope for security\n */\nfunction ensureTenantScope(ctx: CommandRuntimeContext, tenantId: string | null | undefined) {\n if (tenantId && ctx.auth?.tenantId && ctx.auth.tenantId !== tenantId) {\n throw new Error('Tenant mismatch')\n }\n}\n\n// Super-admin status is the immutable `isSuperAdmin` flag derived from\n// RoleAcl/UserAcl at session resolution. Never compare role names to a string\n// like 'superadmin' \u2014 role names are tenant-mutable and trivially spoofable.\nfunction isSuperAdminActor(ctx: CommandRuntimeContext): boolean {\n return ctx.auth?.isSuperAdmin === true\n}\n\n// `ensureTenantScope` is a no-op for system-scoped jobs (tenantId === null), so a\n// `scheduler.jobs.manage` holder could otherwise update/delete a system schedule\n// belonging to the whole deployment. System-scoped jobs MUST be super-admin only,\n// mirroring the create route's system-scope gate.\nfunction ensureCanManageSystemScopedJob(\n ctx: CommandRuntimeContext,\n job: { scopeType?: string | null; tenantId?: string | null },\n): void {\n const isSystemScoped = job.scopeType === 'system' || job.tenantId == null\n if (!isSystemScoped) return\n if (isSuperAdminActor(ctx)) return\n throw new CrudHttpError(403, {\n error: 'System-scoped scheduled jobs can only be managed by a super administrator.',\n })\n}\n\n/**\n * CREATE SCHEDULE COMMAND\n */\nconst createScheduleCommand: CommandHandler<ScheduleCreateInput, { id: string }> = {\n id: 'scheduler.jobs.create',\n\n async execute(input, ctx) {\n ensureTenantScope(ctx, input.tenantId)\n if (input.organizationId) ensureOrganizationScope(ctx, input.organizationId)\n ensureCanManageSystemScopedJob(ctx, { scopeType: input.scopeType, tenantId: input.tenantId })\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n if (input.isEnabled ?? true) {\n await enforceTenantActiveScheduleLimit(em, input.tenantId)\n }\n\n // Calculate next run time\n const nextRunAt = calculateNextRunForWrite(\n input.scheduleType,\n input.scheduleValue,\n input.timezone || 'UTC'\n )\n\n if (!nextRunAt) {\n throw new Error('Failed to calculate next run time')\n }\n\n if (input.targetType === 'queue' && input.targetQueue) {\n await assertQueueTargetAuthorizedForActor(ctx, input.targetQueue)\n await assertQueueTargetPayloadAllowed(input.targetQueue, input.targetPayload)\n }\n\n // Create schedule\n const schedule = em.create(ScheduledJob, {\n name: input.name,\n description: input.description ?? null,\n scopeType: input.scopeType,\n organizationId: input.organizationId ?? null,\n tenantId: input.tenantId ?? null,\n scheduleType: input.scheduleType,\n scheduleValue: input.scheduleValue,\n timezone: input.timezone ?? 'UTC',\n targetType: input.targetType,\n targetQueue: input.targetQueue ?? null,\n targetCommand: input.targetCommand ?? null,\n targetPayload: input.targetPayload ?? null,\n requireFeature: input.requireFeature ?? null,\n isEnabled: input.isEnabled ?? true,\n sourceType: 'user',\n sourceModule: null,\n nextRunAt,\n createdByUserId: resolveCommandActorUserId(ctx),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n\n em.persist(schedule)\n await em.flush()\n\n return { id: schedule.id }\n },\n\n async captureAfter(_input, result, ctx) {\n const em = ctx.container.resolve<EntityManager>('em')\n return await loadScheduleSnapshot(em, result.id)\n },\n\n async buildLog({ result, ctx, snapshots }) {\n const { translate } = await resolveTranslations()\n const after = snapshots.after as ScheduleSnapshot | undefined\n\n return {\n actionLabel: translate('scheduler.audit.create', 'Create schedule'),\n resourceKind: 'scheduler.job',\n resourceId: result.id,\n tenantId: after?.tenantId || null,\n organizationId: after?.organizationId || null,\n snapshotAfter: after,\n payload: { undo: { after } },\n }\n },\n\n async undo({ logEntry, ctx }) {\n const after = extractUndoPayload<UndoPayload<ScheduleSnapshot>>(logEntry)?.after\n if (!after) return\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n const schedule = await em.findOne(ScheduledJob, { id: after.id })\n\n if (schedule) {\n await em.remove(schedule).flush()\n await syncBullMQAfterUndo(ctx, schedule)\n }\n },\n\n redo: makeCreateRedo<ScheduledJob, ScheduleSnapshot, ScheduleCreateInput, { id: string }>({\n entityClass: ScheduledJob,\n getSnapshotId: (snapshot) => snapshot.id,\n seedFromSnapshot: scheduleSeedFromSnapshot,\n buildResult: (entity) => ({ id: entity.id }),\n afterRestore: async ({ ctx, entity }) => {\n await syncBullMQAfterUndo(ctx, entity)\n },\n }),\n}\n\n/**\n * UPDATE SCHEDULE COMMAND\n */\nconst updateScheduleCommand: CommandHandler<ScheduleUpdateInput, { ok: boolean }> = {\n id: 'scheduler.jobs.update',\n\n async prepare(input, ctx) {\n const em = ctx.container.resolve<EntityManager>('em')\n const before = await loadScheduleSnapshot(em, input.id)\n return { before }\n },\n\n async execute(input, ctx) {\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null })\n if (!schedule) {\n throw new Error('Schedule not found')\n }\n\n ensureTenantScope(ctx, schedule.tenantId)\n if (schedule.organizationId) ensureOrganizationScope(ctx, schedule.organizationId)\n ensureCanManageSystemScopedJob(ctx, schedule)\n\n // Module-authored schedules are trusted infrastructure wiring (#5213): their\n // target and payload are owned by the registering module. Submitting them\n // unchanged is a no-op (the edit form always resends every field) \u2014 only an\n // actual change to provenance-relevant values is rejected.\n const touchesModuleOwnedTarget =\n schedule.sourceType === 'module'\n && (\n targetValueChanged(input.targetType, schedule.targetType)\n || targetValueChanged(input.targetQueue, schedule.targetQueue)\n || targetValueChanged(input.targetCommand, schedule.targetCommand)\n || targetPayloadChanged(input.targetPayload, schedule.targetPayload)\n )\n if (touchesModuleOwnedTarget) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(403, {\n error: translate(\n 'scheduler.errors.module_target_immutable',\n 'Module-authored schedules own their target; only operational fields can be updated here.',\n ),\n })\n }\n\n if (input.isEnabled === true && schedule.isEnabled !== true) {\n await enforceTenantActiveScheduleLimit(em, schedule.tenantId)\n }\n\n const effectiveTargetType = input.targetType ?? schedule.targetType\n const effectiveQueue = input.targetQueue ?? schedule.targetQueue\n const queueChanged =\n effectiveTargetType === 'queue'\n && (\n targetValueChanged(input.targetQueue, schedule.targetQueue)\n || targetValueChanged(input.targetType, schedule.targetType)\n || targetPayloadChanged(input.targetPayload, schedule.targetPayload)\n )\n if (effectiveTargetType === 'queue' && effectiveQueue) {\n const retargetedToUnapprovedQueue =\n input.targetQueue !== undefined\n && input.targetQueue !== schedule.targetQueue\n && !isSchedulerSafeQueue(input.targetQueue)\n // A save that leaves a user-authored row ACTIVE on an unapproved queue\n // restores the invariant the update schema used to enforce; disabling the\n // row stays allowed so admins can remediate legacy targets from the UI.\n // (Dispatch-time re-verification remains the backstop either way.)\n const keepsUnapprovedQueueActive =\n effectiveQueue != null\n && !isSchedulerSafeQueue(effectiveQueue)\n && input.isEnabled !== false\n && schedule.isEnabled !== false\n if (\n (retargetedToUnapprovedQueue || keepsUnapprovedQueueActive)\n && schedule.sourceType !== 'module'\n ) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(422, {\n error: translate(\n 'scheduler.errors.queue_not_approved',\n 'Target queue is not an approved scheduler target. Only workers that opted into scheduling can be selected.',\n ),\n })\n }\n if (queueChanged) {\n await assertQueueTargetAuthorizedForActor(ctx, effectiveQueue)\n const effectivePayload = input.targetPayload !== undefined\n ? input.targetPayload\n : schedule.targetPayload\n await assertQueueTargetPayloadAllowed(effectiveQueue, effectivePayload)\n }\n }\n\n const scheduleChanged = input.scheduleType !== undefined || input.scheduleValue !== undefined || input.timezone !== undefined\n const nextRunAt = scheduleChanged\n ? calculateNextRunForWrite(\n input.scheduleType ?? schedule.scheduleType,\n input.scheduleValue ?? schedule.scheduleValue,\n input.timezone ?? schedule.timezone,\n )\n : null\n\n if (scheduleChanged && !nextRunAt) {\n const { translate } = await resolveTranslations()\n throw new CrudHttpError(422, {\n error: translate('scheduler.error.invalid_schedule_value', 'Invalid schedule value.'),\n })\n }\n\n // Update fields\n if (input.name !== undefined) schedule.name = input.name\n if (input.description !== undefined) schedule.description = input.description ?? null\n if (input.scheduleType !== undefined) schedule.scheduleType = input.scheduleType\n if (input.scheduleValue !== undefined) schedule.scheduleValue = input.scheduleValue\n if (input.timezone !== undefined) schedule.timezone = input.timezone\n if (input.targetPayload !== undefined) schedule.targetPayload = input.targetPayload ?? null\n if (input.requireFeature !== undefined) schedule.requireFeature = input.requireFeature || null\n if (input.isEnabled !== undefined) schedule.isEnabled = input.isEnabled\n \n // Handle target type changes - clear stale values when switching between queue and command\n if (input.targetType !== undefined) {\n schedule.targetType = input.targetType\n \n if (input.targetType === 'queue') {\n // Switching to queue: set new queue and clear command\n if (input.targetQueue !== undefined) schedule.targetQueue = input.targetQueue\n schedule.targetCommand = null\n } else if (input.targetType === 'command') {\n // Switching to command: set new command and clear queue\n if (input.targetCommand !== undefined) schedule.targetCommand = input.targetCommand\n schedule.targetQueue = null\n }\n } else {\n // targetType not changing, but allow updating individual target fields\n if (input.targetQueue !== undefined) schedule.targetQueue = input.targetQueue\n if (input.targetCommand !== undefined) schedule.targetCommand = input.targetCommand\n }\n\n if (nextRunAt) {\n schedule.nextRunAt = nextRunAt\n }\n\n schedule.updatedAt = new Date()\n schedule.updatedByUserId = resolveCommandActorUserId(ctx)\n\n await em.flush()\n\n return { ok: true }\n },\n\n async captureAfter(input, _result, ctx) {\n const em = ctx.container.resolve<EntityManager>('em')\n return await loadScheduleSnapshot(em, input.id)\n },\n\n async buildLog({ input, ctx, snapshots }) {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as ScheduleSnapshot | undefined\n const after = snapshots.after as ScheduleSnapshot | undefined\n\n return {\n actionLabel: translate('scheduler.audit.update', 'Update schedule'),\n resourceKind: 'scheduler.job',\n resourceId: input.id,\n tenantId: after?.tenantId || null,\n organizationId: after?.organizationId || null,\n snapshotBefore: before,\n snapshotAfter: after,\n payload: { undo: { before, after } },\n }\n },\n\n async undo({ logEntry, ctx }) {\n const before = extractUndoPayload<UndoPayload<ScheduleSnapshot>>(logEntry)?.before\n if (!before) return\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n const schedule = await em.findOne(ScheduledJob, { id: before.id })\n\n if (schedule) {\n // Restore all fields\n schedule.name = before.name\n schedule.description = before.description\n schedule.scopeType = before.scopeType\n schedule.organizationId = before.organizationId\n schedule.tenantId = before.tenantId\n schedule.scheduleType = before.scheduleType\n schedule.scheduleValue = before.scheduleValue\n schedule.timezone = before.timezone\n schedule.targetType = before.targetType\n schedule.targetQueue = before.targetQueue\n schedule.targetCommand = before.targetCommand\n schedule.targetPayload = before.targetPayload\n schedule.requireFeature = before.requireFeature\n schedule.isEnabled = before.isEnabled\n schedule.sourceType = before.sourceType\n schedule.sourceModule = before.sourceModule\n schedule.nextRunAt = toDate(before.nextRunAt)\n schedule.lastRunAt = toDate(before.lastRunAt)\n schedule.updatedAt = new Date()\n\n await em.flush()\n await syncBullMQAfterUndo(ctx, schedule)\n }\n },\n}\n\n/**\n * DELETE SCHEDULE COMMAND\n */\nconst deleteScheduleCommand: CommandHandler<{ id: string }, { ok: boolean }> = {\n id: 'scheduler.jobs.delete',\n\n async prepare(input, ctx) {\n const em = ctx.container.resolve<EntityManager>('em')\n const before = await loadScheduleSnapshot(em, input.id)\n return { before }\n },\n\n async execute(input, ctx) {\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null })\n if (!schedule) {\n throw new Error('Schedule not found')\n }\n\n ensureTenantScope(ctx, schedule.tenantId)\n if (schedule.organizationId) ensureOrganizationScope(ctx, schedule.organizationId)\n ensureCanManageSystemScopedJob(ctx, schedule)\n\n // Soft delete\n schedule.deletedAt = new Date()\n schedule.updatedAt = new Date()\n schedule.updatedByUserId = resolveCommandActorUserId(ctx)\n\n await em.flush()\n\n return { ok: true }\n },\n\n async buildLog({ input, ctx, snapshots }) {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as ScheduleSnapshot | undefined\n\n return {\n actionLabel: translate('scheduler.audit.delete', 'Delete schedule'),\n resourceKind: 'scheduler.job',\n resourceId: input.id,\n tenantId: before?.tenantId || null,\n organizationId: before?.organizationId || null,\n snapshotBefore: before,\n payload: { undo: { before } },\n }\n },\n\n async undo({ logEntry, ctx }) {\n const before = extractUndoPayload<UndoPayload<ScheduleSnapshot>>(logEntry)?.before\n if (!before) return\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n const existing = await em.findOne(ScheduledJob, { id: before.id })\n\n // Soft-deleted rows are restored by clearing `deletedAt`. A hard-removed row\n // (no surviving record) is re-materialized from the snapshot so undo is\n // robust to either deletion strategy \u2014 mirroring the sales reference undo.\n const schedule = existing ?? materializeScheduleFromSnapshot(em, before)\n if (existing) {\n schedule.deletedAt = null\n schedule.updatedAt = new Date()\n }\n await em.flush()\n await syncBullMQAfterUndo(ctx, schedule)\n },\n}\n\n// Register all commands\nregisterCommand(createScheduleCommand)\nregisterCommand(updateScheduleCommand)\nregisterCommand(deleteScheduleCommand)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,12 +4,16 @@ import { validateInterval } from "../lib/intervalParser.js";
|
|
|
4
4
|
import { commandRegistry } from "@open-mercato/shared/lib/commands/registry";
|
|
5
5
|
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
6
6
|
import { isSchedulerSafeCommandId } from "../lib/scheduler-safe-commands.js";
|
|
7
|
+
import { isSchedulerSafeQueue, validateSchedulerTargetPayload } from "../lib/safeQueueTargets.js";
|
|
7
8
|
function validateCommandExists(commandId) {
|
|
8
9
|
return commandRegistry.has(commandId);
|
|
9
10
|
}
|
|
10
11
|
function validateCommandIsSchedulable(commandId) {
|
|
11
12
|
return isSchedulerSafeCommandId(commandId);
|
|
12
13
|
}
|
|
14
|
+
function validateSchedulerSafeQueue(queue) {
|
|
15
|
+
return isSchedulerSafeQueue(queue);
|
|
16
|
+
}
|
|
13
17
|
const scheduleBaseSchema = z.object({
|
|
14
18
|
name: z.string().min(1, "Name is required").max(200),
|
|
15
19
|
description: z.string().max(500).optional().nullable(),
|
|
@@ -24,9 +28,7 @@ const scheduleBaseSchema = z.object({
|
|
|
24
28
|
targetCommand: z.string().optional().nullable(),
|
|
25
29
|
targetPayload: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
26
30
|
requireFeature: z.string().optional().nullable(),
|
|
27
|
-
isEnabled: z.boolean().default(true)
|
|
28
|
-
sourceType: z.enum(["user", "module"]).default("user"),
|
|
29
|
-
sourceModule: z.string().optional().nullable()
|
|
31
|
+
isEnabled: z.boolean().default(true)
|
|
30
32
|
});
|
|
31
33
|
const scheduleCreateSchema = scheduleBaseSchema.refine(
|
|
32
34
|
(data) => {
|
|
@@ -95,6 +97,28 @@ const scheduleCreateSchema = scheduleBaseSchema.refine(
|
|
|
95
97
|
message: "Command is not schedulable. Only scheduler-safe commands can be used as schedule targets.",
|
|
96
98
|
path: ["targetCommand"]
|
|
97
99
|
}
|
|
100
|
+
).refine(
|
|
101
|
+
(data) => {
|
|
102
|
+
if (data.targetType === "queue" && data.targetQueue) {
|
|
103
|
+
return validateSchedulerSafeQueue(data.targetQueue);
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
message: "Target queue is not an approved scheduler target. Only workers that opted into scheduling can be selected.",
|
|
109
|
+
path: ["targetQueue"]
|
|
110
|
+
}
|
|
111
|
+
).refine(
|
|
112
|
+
(data) => {
|
|
113
|
+
if (data.targetType === "queue" && data.targetQueue) {
|
|
114
|
+
return validateSchedulerTargetPayload(data.targetQueue, data.targetPayload) === null;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
message: "Invalid target payload for the selected scheduler queue.",
|
|
120
|
+
path: ["targetPayload"]
|
|
121
|
+
}
|
|
98
122
|
);
|
|
99
123
|
const scheduleUpdateSchema = z.object({
|
|
100
124
|
id: z.uuid(),
|