@open-mercato/scheduler 0.7.1-develop.7113.1.9d83dca10c → 0.7.1-develop.7121.1.734c263bda

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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/scheduler/__integration__/TC-SCHED-009.spec.js +61 -0
  3. package/dist/modules/scheduler/__integration__/TC-SCHED-009.spec.js.map +7 -0
  4. package/dist/modules/scheduler/api/trigger/route.js +37 -37
  5. package/dist/modules/scheduler/api/trigger/route.js.map +2 -2
  6. package/dist/modules/scheduler/commands/jobs.js +3 -0
  7. package/dist/modules/scheduler/commands/jobs.js.map +2 -2
  8. package/dist/modules/scheduler/commands/trigger.js +146 -0
  9. package/dist/modules/scheduler/commands/trigger.js.map +7 -0
  10. package/dist/modules/scheduler/i18n/de.json +1 -0
  11. package/dist/modules/scheduler/i18n/en.json +1 -0
  12. package/dist/modules/scheduler/i18n/es.json +1 -0
  13. package/dist/modules/scheduler/i18n/ko.json +1 -0
  14. package/dist/modules/scheduler/i18n/pl.json +1 -0
  15. package/dist/modules/scheduler/lib/commandContext.js +15 -5
  16. package/dist/modules/scheduler/lib/commandContext.js.map +2 -2
  17. package/dist/modules/scheduler/lib/scheduler-safe-commands.js +11 -4
  18. package/dist/modules/scheduler/lib/scheduler-safe-commands.js.map +2 -2
  19. package/dist/modules/scheduler/services/localSchedulerService.js +2 -2
  20. package/dist/modules/scheduler/services/localSchedulerService.js.map +2 -2
  21. package/dist/modules/scheduler/workers/execute-schedule.worker.js +38 -11
  22. package/dist/modules/scheduler/workers/execute-schedule.worker.js.map +2 -2
  23. package/package.json +5 -5
  24. package/src/modules/scheduler/__integration__/TC-SCHED-009.spec.ts +98 -0
  25. package/src/modules/scheduler/api/trigger/__tests__/route.test.ts +154 -0
  26. package/src/modules/scheduler/api/trigger/route.ts +63 -47
  27. package/src/modules/scheduler/commands/__tests__/trigger.test.ts +411 -0
  28. package/src/modules/scheduler/commands/jobs.ts +1 -1
  29. package/src/modules/scheduler/commands/trigger.ts +258 -0
  30. package/src/modules/scheduler/i18n/de.json +1 -0
  31. package/src/modules/scheduler/i18n/en.json +1 -0
  32. package/src/modules/scheduler/i18n/es.json +1 -0
  33. package/src/modules/scheduler/i18n/ko.json +1 -0
  34. package/src/modules/scheduler/i18n/pl.json +1 -0
  35. package/src/modules/scheduler/lib/__tests__/commandContext.test.ts +83 -1
  36. package/src/modules/scheduler/lib/commandContext.ts +39 -3
  37. package/src/modules/scheduler/lib/scheduler-safe-commands.ts +25 -4
  38. package/src/modules/scheduler/services/__tests__/localSchedulerService.test.ts +1 -1
  39. package/src/modules/scheduler/services/localSchedulerService.ts +2 -2
  40. package/src/modules/scheduler/workers/__tests__/execute-schedule.worker.test.ts +132 -1
  41. package/src/modules/scheduler/workers/execute-schedule.worker.ts +58 -13
@@ -1,2 +1,2 @@
1
- [build:scheduler] found 60 entry points
1
+ [build:scheduler] found 62 entry points
2
2
  [build:scheduler] built successfully
@@ -0,0 +1,61 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
3
+ import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
4
+ import { ACTIONS_PATH as AUDIT_ACTIONS_PATH } from "@open-mercato/core/modules/audit_logs/__integration__/helpers/auditLogsApi";
5
+ import {
6
+ SCHEDULER_TRIGGER_PATH,
7
+ createScheduleJob,
8
+ deleteScheduleJob,
9
+ uniqueScheduleName
10
+ } from "./helpers/scheduler.js";
11
+ const TRIGGER_COMMAND_ID = "scheduler.jobs.trigger";
12
+ async function findTriggerLogEntry(request, token, resourceId) {
13
+ const query = new URLSearchParams({ resourceKind: "scheduler.job", resourceId });
14
+ const response = await apiRequest(request, "GET", `${AUDIT_ACTIONS_PATH}?${query.toString()}`, {
15
+ token
16
+ });
17
+ expect(response.status()).toBe(200);
18
+ const body = await readJsonSafe(response);
19
+ return (body?.items ?? []).find((item) => item.commandId === TRIGGER_COMMAND_ID) ?? null;
20
+ }
21
+ test.describe("TC-SCHED-009: manual schedule triggers are written to the action log", () => {
22
+ test("records a trigger attempt against the calling user", async ({ request }) => {
23
+ const token = await getAuthToken(request, "admin");
24
+ let scheduleId = null;
25
+ try {
26
+ scheduleId = await createScheduleJob(request, token, {
27
+ name: uniqueScheduleName("Trigger-Audit"),
28
+ scheduleType: "interval",
29
+ scheduleValue: "1h"
30
+ });
31
+ const response = await apiRequest(request, "POST", SCHEDULER_TRIGGER_PATH, {
32
+ token,
33
+ data: { id: scheduleId }
34
+ });
35
+ expect([200, 400]).toContain(response.status());
36
+ const entry = await findTriggerLogEntry(request, token, scheduleId);
37
+ expect(entry, "a manual trigger writes an action-log entry").not.toBeNull();
38
+ expect(entry?.resourceKind).toBe("scheduler.job");
39
+ expect(entry?.resourceId).toBe(scheduleId);
40
+ expect(entry?.actorUserId, "the entry names the caller").toBeTruthy();
41
+ expect(entry?.actionLabel ?? "").toMatch(/trigger/i);
42
+ expect(entry?.undoToken).toBeNull();
43
+ } finally {
44
+ await deleteScheduleJob(request, token, scheduleId);
45
+ }
46
+ });
47
+ test("records a refused trigger for a schedule id that does not exist", async ({ request }) => {
48
+ const token = await getAuthToken(request, "admin");
49
+ const missingScheduleId = "00000000-0000-4000-8000-0000000009de";
50
+ const response = await apiRequest(request, "POST", SCHEDULER_TRIGGER_PATH, {
51
+ token,
52
+ data: { id: missingScheduleId }
53
+ });
54
+ expect(response.status()).toBe(404);
55
+ const entry = await findTriggerLogEntry(request, token, missingScheduleId);
56
+ expect(entry, "a refused trigger still writes an action-log entry").not.toBeNull();
57
+ expect(entry?.resourceId).toBe(missingScheduleId);
58
+ expect(entry?.actorUserId, "the refusal names the caller").toBeTruthy();
59
+ });
60
+ });
61
+ //# sourceMappingURL=TC-SCHED-009.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/scheduler/__integration__/TC-SCHED-009.spec.ts"],
4
+ "sourcesContent": ["import { expect, test, type APIRequestContext } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport { ACTIONS_PATH as AUDIT_ACTIONS_PATH } from '@open-mercato/core/modules/audit_logs/__integration__/helpers/auditLogsApi'\nimport {\n SCHEDULER_TRIGGER_PATH,\n createScheduleJob,\n deleteScheduleJob,\n uniqueScheduleName,\n} from './helpers/scheduler'\n\nconst TRIGGER_COMMAND_ID = 'scheduler.jobs.trigger'\n\ntype ActionLogItem = {\n commandId: string\n actionLabel: string | null\n actorUserId: string | null\n resourceKind: string | null\n resourceId: string | null\n undoToken: string | null\n}\n\nasync function findTriggerLogEntry(\n request: APIRequestContext,\n token: string,\n resourceId: string,\n): Promise<ActionLogItem | null> {\n const query = new URLSearchParams({ resourceKind: 'scheduler.job', resourceId })\n const response = await apiRequest(request, 'GET', `${AUDIT_ACTIONS_PATH}?${query.toString()}`, {\n token,\n })\n expect(response.status()).toBe(200)\n const body = await readJsonSafe<{ items?: ActionLogItem[] }>(response)\n return (body?.items ?? []).find((item) => item.commandId === TRIGGER_COMMAND_ID) ?? null\n}\n\n/**\n * TC-SCHED-009: every authenticated manual trigger of a schedule is recorded in\n * the action log against the calling user \u2014 including refusals.\n *\n * Manual triggering is an authorised-looking way to make an automated job run at\n * a moment of the actor's choosing, so the attempt itself is the auditable event.\n * Both assertions below are therefore independent of QUEUE_STRATEGY: under the\n * async strategy the trigger is enqueued, under the local strategy it is refused\n * with \"async required\", and either way the attempt must leave an entry.\n */\ntest.describe('TC-SCHED-009: manual schedule triggers are written to the action log', () => {\n test('records a trigger attempt against the calling user', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let scheduleId: string | null = null\n\n try {\n scheduleId = await createScheduleJob(request, token, {\n name: uniqueScheduleName('Trigger-Audit'),\n scheduleType: 'interval',\n scheduleValue: '1h',\n })\n\n const response = await apiRequest(request, 'POST', SCHEDULER_TRIGGER_PATH, {\n token,\n data: { id: scheduleId },\n })\n // 200 under the async strategy, 400 \"async required\" under local \u2014 the\n // attempt is audited either way.\n expect([200, 400]).toContain(response.status())\n\n const entry = await findTriggerLogEntry(request, token, scheduleId)\n expect(entry, 'a manual trigger writes an action-log entry').not.toBeNull()\n expect(entry?.resourceKind).toBe('scheduler.job')\n expect(entry?.resourceId).toBe(scheduleId)\n expect(entry?.actorUserId, 'the entry names the caller').toBeTruthy()\n expect(entry?.actionLabel ?? '').toMatch(/trigger/i)\n // The enqueued run is reversed through the entries the run itself writes,\n // so the trigger exposes no undo affordance (see TC-UNDO-001 \u00A74).\n expect(entry?.undoToken).toBeNull()\n } finally {\n await deleteScheduleJob(request, token, scheduleId)\n }\n })\n\n test('records a refused trigger for a schedule id that does not exist', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n // A well-formed UUID that no schedule uses: the request passes validation and\n // is refused on lookup, which is exactly the probing attempt worth recording.\n const missingScheduleId = '00000000-0000-4000-8000-0000000009de'\n\n const response = await apiRequest(request, 'POST', SCHEDULER_TRIGGER_PATH, {\n token,\n data: { id: missingScheduleId },\n })\n expect(response.status()).toBe(404)\n\n const entry = await findTriggerLogEntry(request, token, missingScheduleId)\n expect(entry, 'a refused trigger still writes an action-log entry').not.toBeNull()\n expect(entry?.resourceId).toBe(missingScheduleId)\n expect(entry?.actorUserId, 'the refusal names the caller').toBeTruthy()\n })\n})\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAoC;AACrD,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,0BAA0B;AACnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,qBAAqB;AAW3B,eAAe,oBACb,SACA,OACA,YAC+B;AAC/B,QAAM,QAAQ,IAAI,gBAAgB,EAAE,cAAc,iBAAiB,WAAW,CAAC;AAC/E,QAAM,WAAW,MAAM,WAAW,SAAS,OAAO,GAAG,kBAAkB,IAAI,MAAM,SAAS,CAAC,IAAI;AAAA,IAC7F;AAAA,EACF,CAAC;AACD,SAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAClC,QAAM,OAAO,MAAM,aAA0C,QAAQ;AACrE,UAAQ,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,cAAc,kBAAkB,KAAK;AACtF;AAYA,KAAK,SAAS,wEAAwE,MAAM;AAC1F,OAAK,sDAAsD,OAAO,EAAE,QAAQ,MAAM;AAChF,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,QAAI,aAA4B;AAEhC,QAAI;AACF,mBAAa,MAAM,kBAAkB,SAAS,OAAO;AAAA,QACnD,MAAM,mBAAmB,eAAe;AAAA,QACxC,cAAc;AAAA,QACd,eAAe;AAAA,MACjB,CAAC;AAED,YAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,wBAAwB;AAAA,QACzE;AAAA,QACA,MAAM,EAAE,IAAI,WAAW;AAAA,MACzB,CAAC;AAGD,aAAO,CAAC,KAAK,GAAG,CAAC,EAAE,UAAU,SAAS,OAAO,CAAC;AAE9C,YAAM,QAAQ,MAAM,oBAAoB,SAAS,OAAO,UAAU;AAClE,aAAO,OAAO,6CAA6C,EAAE,IAAI,SAAS;AAC1E,aAAO,OAAO,YAAY,EAAE,KAAK,eAAe;AAChD,aAAO,OAAO,UAAU,EAAE,KAAK,UAAU;AACzC,aAAO,OAAO,aAAa,4BAA4B,EAAE,WAAW;AACpE,aAAO,OAAO,eAAe,EAAE,EAAE,QAAQ,UAAU;AAGnD,aAAO,OAAO,SAAS,EAAE,SAAS;AAAA,IACpC,UAAE;AACA,YAAM,kBAAkB,SAAS,OAAO,UAAU;AAAA,IACpD;AAAA,EACF,CAAC;AAED,OAAK,mEAAmE,OAAO,EAAE,QAAQ,MAAM;AAC7F,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AAGjD,UAAM,oBAAoB;AAE1B,UAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,wBAAwB;AAAA,MACzE;AAAA,MACA,MAAM,EAAE,IAAI,kBAAkB;AAAA,IAChC,CAAC;AACD,WAAO,SAAS,OAAO,CAAC,EAAE,KAAK,GAAG;AAElC,UAAM,QAAQ,MAAM,oBAAoB,SAAS,OAAO,iBAAiB;AACzE,WAAO,OAAO,oDAAoD,EAAE,IAAI,SAAS;AACjF,WAAO,OAAO,UAAU,EAAE,KAAK,iBAAiB;AAChD,WAAO,OAAO,aAAa,8BAA8B,EAAE,WAAW;AAAA,EACxE,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -2,10 +2,7 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
- import { createQueue } from "@open-mercato/queue";
6
- import { getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
7
- import { ScheduledJob } from "../../data/entities.js";
8
- import { resolveScheduleAccess } from "../../lib/scheduleAccess.js";
5
+ import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
9
6
  import { scheduleTriggerSchema } from "../../data/validators.js";
10
7
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
11
8
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -21,23 +18,29 @@ async function POST(req) {
21
18
  return NextResponse.json({ error: translate("scheduler.error.unauthorized", "Unauthorized") }, { status: 401 });
22
19
  }
23
20
  const container = await createRequestContainer();
24
- const em = container.resolve("em");
25
21
  try {
26
22
  const body = await req.json();
27
23
  const input = scheduleTriggerSchema.parse(body);
28
- const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null });
29
- if (!schedule) {
30
- return NextResponse.json({ error: translate("scheduler.error.not_found", "Schedule not found") }, { status: 404 });
31
- }
32
- const access = resolveScheduleAccess(schedule, auth);
33
- if (access === "not_found") {
24
+ const ctx = {
25
+ container,
26
+ auth,
27
+ organizationScope: null,
28
+ selectedOrganizationId: auth.orgId ?? null,
29
+ organizationIds: auth.orgId ? [auth.orgId] : null,
30
+ request: req
31
+ };
32
+ const commandBus = container.resolve("commandBus");
33
+ const { result } = await commandBus.execute(
34
+ "scheduler.jobs.trigger",
35
+ { input, ctx }
36
+ );
37
+ if (result.outcome === "not_found") {
34
38
  return NextResponse.json({ error: translate("scheduler.error.not_found", "Schedule not found") }, { status: 404 });
35
39
  }
36
- if (access === "forbidden") {
40
+ if (result.outcome === "forbidden") {
37
41
  return NextResponse.json({ error: translate("scheduler.error.access_denied", "Access denied") }, { status: 403 });
38
42
  }
39
- const queueStrategy = process.env.QUEUE_STRATEGY || "local";
40
- if (queueStrategy !== "async") {
43
+ if (result.outcome === "strategy_unsupported") {
41
44
  return NextResponse.json(
42
45
  {
43
46
  error: translate("scheduler.error.trigger_async_required", "Manual trigger requires QUEUE_STRATEGY=async"),
@@ -46,32 +49,24 @@ async function POST(req) {
46
49
  { status: 400 }
47
50
  );
48
51
  }
49
- const executionQueue = createQueue("scheduler-execution", queueStrategy, {
50
- connection: { url: getRedisUrlOrThrow("QUEUE") }
51
- });
52
- const payload = {
53
- scheduleId: schedule.id,
54
- tenantId: schedule.tenantId,
55
- organizationId: schedule.organizationId,
56
- scopeType: schedule.scopeType,
57
- triggerType: "manual",
58
- triggeredByUserId: auth.sub
59
- };
60
- const jobId = await executionQueue.enqueue(payload);
61
- await executionQueue.close();
62
- logger.info("Manually triggered schedule", {
63
- scheduleId: schedule.id,
64
- scheduleName: schedule.name,
65
- jobId,
66
- triggeredBy: auth.sub
67
- });
52
+ if (result.outcome === "failed") {
53
+ return NextResponse.json(
54
+ { error: result.error ?? translate("scheduler.error.trigger_failed", "Failed to trigger schedule") },
55
+ { status: 400 }
56
+ );
57
+ }
68
58
  return NextResponse.json({
69
59
  ok: true,
70
- jobId,
60
+ jobId: result.queueJobId,
71
61
  // BullMQ job ID
72
62
  message: translate("scheduler.success.triggered", "Schedule queued for execution")
73
63
  });
74
64
  } catch (error) {
65
+ const interceptorRejection = getCommandInterceptorHttpRejection(error);
66
+ if (interceptorRejection) {
67
+ logger.warn("Manual trigger blocked by interceptor", { err: error });
68
+ return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
69
+ }
75
70
  logger.error("Manual trigger failed", { err: error });
76
71
  return NextResponse.json(
77
72
  { error: error instanceof Error ? error.message : translate("scheduler.error.trigger_failed", "Failed to trigger schedule") },
@@ -91,12 +86,12 @@ const errorResponseSchema = z.object({
91
86
  const openApi = {
92
87
  tag: "Scheduler",
93
88
  summary: "Manually trigger a schedule",
94
- description: "Execute a schedule immediately by enqueueing it in the scheduler-execution queue. Requires QUEUE_STRATEGY=async.",
89
+ description: "Execute a schedule immediately by enqueueing it in the scheduler-execution queue. Requires QUEUE_STRATEGY=async. Every authenticated attempt, including refusals, is recorded in the action log against the calling user; the one exception is an attempt a before command interceptor blocks, which is rejected before the command runs and therefore writes no row.",
95
90
  methods: {
96
91
  POST: {
97
92
  operationId: "triggerScheduledJob",
98
93
  summary: "Manually trigger a schedule",
99
- description: "Executes a scheduled job immediately, bypassing the scheduled time. Only works with async queue strategy.",
94
+ description: "Executes a scheduled job immediately, bypassing the scheduled time. Only works with async queue strategy. The attempt is audited against the calling user whether it succeeds, is refused, or fails, except when a before command interceptor blocks it before the command runs.",
100
95
  requestBody: {
101
96
  schema: scheduleTriggerSchema,
102
97
  contentType: "application/json"
@@ -112,7 +107,12 @@ const openApi = {
112
107
  { status: 400, description: "Invalid request or local strategy not supported", schema: errorResponseSchema },
113
108
  { status: 401, description: "Unauthorized", schema: errorResponseSchema },
114
109
  { status: 403, description: "Access denied", schema: errorResponseSchema },
115
- { status: 404, description: "Schedule not found", schema: errorResponseSchema }
110
+ { status: 404, description: "Schedule not found", schema: errorResponseSchema },
111
+ {
112
+ status: 409,
113
+ description: "Trigger deliberately blocked by a before command interceptor. The interceptor chooses the status (any 4xx/5xx) and may replace the body.",
114
+ schema: errorResponseSchema
115
+ }
116
116
  ]
117
117
  }
118
118
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/scheduler/api/trigger/route.ts"],
4
- "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createQueue } from '@open-mercato/queue'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { ScheduledJob } from '../../data/entities.js'\nimport { resolveScheduleAccess } from '../../lib/scheduleAccess.js'\nimport { scheduleTriggerSchema } from '../../data/validators.js'\nimport type { ExecuteSchedulePayload } from '../../workers/execute-schedule.worker.js'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler').child({ component: 'trigger' })\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.trigger'],\n}\n\n/**\n * POST /api/scheduler/trigger\n * Manually trigger a schedule\n * \n * This enqueues the schedule execution job in BullMQ.\n * Execution history is tracked in BullMQ job state.\n */\nexport async function POST(req: NextRequest) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('scheduler.error.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n try {\n const body = await req.json()\n const input = scheduleTriggerSchema.parse(body)\n\n // Load by id alone, then apply isolation to the loaded row. Folding the actor's\n // tenant/org into the lookup would make every system-scoped schedule unmatchable.\n const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null })\n\n if (!schedule) {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n\n const access = resolveScheduleAccess(schedule, auth)\n if (access === 'not_found') {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n if (access === 'forbidden') {\n return NextResponse.json({ error: translate('scheduler.error.access_denied', 'Access denied') }, { status: 403 })\n }\n\n // Check if using async queue strategy\n const queueStrategy = (process.env.QUEUE_STRATEGY || 'local') as 'local' | 'async'\n \n if (queueStrategy !== 'async') {\n return NextResponse.json(\n { \n error: translate('scheduler.error.trigger_async_required', 'Manual trigger requires QUEUE_STRATEGY=async'),\n message: translate('scheduler.error.trigger_async_hint', 'Execution history and manual triggers are only available with BullMQ (async strategy)')\n },\n { status: 400 }\n )\n }\n\n // Enqueue execution job to scheduler-execution queue\n const executionQueue = createQueue<ExecuteSchedulePayload>('scheduler-execution', queueStrategy, {\n connection: { url: getRedisUrlOrThrow('QUEUE') },\n })\n\n const payload: ExecuteSchedulePayload = {\n scheduleId: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scopeType: schedule.scopeType,\n triggerType: 'manual',\n triggeredByUserId: auth.sub,\n }\n\n const jobId = await executionQueue.enqueue(payload)\n await executionQueue.close()\n\n logger.info('Manually triggered schedule', {\n scheduleId: schedule.id,\n scheduleName: schedule.name,\n jobId,\n triggeredBy: auth.sub,\n })\n\n return NextResponse.json({\n ok: true,\n jobId, // BullMQ job ID\n message: translate('scheduler.success.triggered', 'Schedule queued for execution'),\n })\n\n } catch (error) {\n logger.error('Manual trigger failed', { err: error })\n return NextResponse.json(\n { error: error instanceof Error ? error.message : translate('scheduler.error.trigger_failed', 'Failed to trigger schedule') },\n { status: 400 }\n )\n }\n}\n\n// Response schemas\nconst triggerResponseSchema = z.object({\n ok: z.boolean(),\n jobId: z.string(),\n message: z.string(),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n})\n\n// OpenAPI specification\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'Manually trigger a schedule',\n description: 'Execute a schedule immediately by enqueueing it in the scheduler-execution queue. Requires QUEUE_STRATEGY=async.',\n methods: {\n POST: {\n operationId: 'triggerScheduledJob',\n summary: 'Manually trigger a schedule',\n description: 'Executes a scheduled job immediately, bypassing the scheduled time. Only works with async queue strategy.',\n requestBody: {\n schema: scheduleTriggerSchema,\n contentType: 'application/json',\n },\n responses: [\n {\n status: 200,\n description: 'Schedule triggered successfully',\n schema: triggerResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request or local strategy not supported', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Schedule not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { scheduleTriggerSchema } from '../../data/validators.js'\nimport type { TriggerScheduleResult } from '../../commands/trigger.js'\nimport type { ScheduleTriggerInput } from '../../data/validators.js'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler').child({ component: 'trigger' })\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.trigger'],\n}\n\n/**\n * POST /api/scheduler/trigger\n * Manually trigger a schedule\n *\n * This enqueues the schedule execution job in BullMQ.\n * Execution history is tracked in BullMQ job state.\n *\n * The handler is a thin mapper: `scheduler.jobs.trigger` owns loading the\n * schedule, deciding access, checking the queue strategy and enqueueing, so that\n * every authenticated attempt \u2014 refusals included \u2014 leaves an action-log entry\n * attributed to the caller. Expected refusals come back as an `outcome`, never as\n * an exception, because a throwing handler would skip the audit write entirely.\n */\nexport async function POST(req: NextRequest) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n // No actor means nothing to attribute, so this is the one path that\n // legitimately leaves no action-log entry.\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('scheduler.error.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n\n try {\n const body = await req.json()\n // Parsed before the command runs, so a malformed body still answers 400 with\n // the validation message and the command never sees unvalidated input.\n const input = scheduleTriggerSchema.parse(body)\n\n // Null organization scope is the supported non-CRUD shape (see\n // `ensureOrganizationScope`), and the scheduler's own worker and\n // `localSchedulerService` build their contexts the same way. The trigger\n // command performs its isolation through `resolveScheduleAccess` on the loaded\n // row, so the only consumer of these fields is the action-log row's scope.\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n\n const commandBus = container.resolve<CommandBus>('commandBus')\n const { result } = await commandBus.execute<ScheduleTriggerInput, TriggerScheduleResult>(\n 'scheduler.jobs.trigger',\n { input, ctx },\n )\n\n if (result.outcome === 'not_found') {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n\n if (result.outcome === 'forbidden') {\n return NextResponse.json({ error: translate('scheduler.error.access_denied', 'Access denied') }, { status: 403 })\n }\n\n if (result.outcome === 'strategy_unsupported') {\n return NextResponse.json(\n {\n error: translate('scheduler.error.trigger_async_required', 'Manual trigger requires QUEUE_STRATEGY=async'),\n message: translate('scheduler.error.trigger_async_hint', 'Execution history and manual triggers are only available with BullMQ (async strategy)')\n },\n { status: 400 }\n )\n }\n\n if (result.outcome === 'failed') {\n return NextResponse.json(\n { error: result.error ?? translate('scheduler.error.trigger_failed', 'Failed to trigger schedule') },\n { status: 400 }\n )\n }\n\n return NextResponse.json({\n ok: true,\n jobId: result.queueJobId, // BullMQ job ID\n message: translate('scheduler.success.triggered', 'Schedule queued for execution'),\n })\n\n } catch (error) {\n // A command interceptor that blocked with an explicit status is a deliberate\n // business rejection, not a malformed request \u2014 surface its status and body\n // instead of flattening it to the generic 400 below.\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n logger.warn('Manual trigger blocked by interceptor', { err: error })\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n // The action log is written after the enqueue, and the bus does not guard that\n // write, so a failing audit store surfaces here with the job already queued.\n // Record it loudly: the run stays traceable even when its entry was lost.\n logger.error('Manual trigger failed', { err: error })\n return NextResponse.json(\n { error: error instanceof Error ? error.message : translate('scheduler.error.trigger_failed', 'Failed to trigger schedule') },\n { status: 400 }\n )\n }\n}\n\n// Response schemas\nconst triggerResponseSchema = z.object({\n ok: z.boolean(),\n jobId: z.string(),\n message: z.string(),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n})\n\n// OpenAPI specification\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'Manually trigger a schedule',\n description: 'Execute a schedule immediately by enqueueing it in the scheduler-execution queue. Requires QUEUE_STRATEGY=async. Every authenticated attempt, including refusals, is recorded in the action log against the calling user; the one exception is an attempt a before command interceptor blocks, which is rejected before the command runs and therefore writes no row.',\n methods: {\n POST: {\n operationId: 'triggerScheduledJob',\n summary: 'Manually trigger a schedule',\n description: 'Executes a scheduled job immediately, bypassing the scheduled time. Only works with async queue strategy. The attempt is audited against the calling user whether it succeeds, is refused, or fails, except when a before command interceptor blocks it before the command runs.',\n requestBody: {\n schema: scheduleTriggerSchema,\n contentType: 'application/json',\n },\n responses: [\n {\n status: 200,\n description: 'Schedule triggered successfully',\n schema: triggerResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request or local strategy not supported', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Schedule not found', schema: errorResponseSchema },\n {\n status: 409,\n description:\n 'Trigger deliberately blocked by a before command interceptor. The interceptor chooses the status (any 4xx/5xx) and may replace the body.',\n schema: errorResponseSchema,\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -426,4 +426,7 @@ const deleteScheduleCommand = {
426
426
  registerCommand(createScheduleCommand);
427
427
  registerCommand(updateScheduleCommand);
428
428
  registerCommand(deleteScheduleCommand);
429
+ export {
430
+ resolveCommandActorUserId
431
+ };
429
432
  //# sourceMappingURL=jobs.js.map
@@ -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'\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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",
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\nexport function 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
  }
@@ -0,0 +1,146 @@
1
+ import { registerCommand } from "@open-mercato/shared/lib/commands";
2
+ import { createQueue } from "@open-mercato/queue";
3
+ import { getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
4
+ import { createLogger } from "@open-mercato/shared/lib/logger";
5
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
6
+ import { ScheduledJob } from "../data/entities.js";
7
+ import { resolveScheduleAccess } from "../lib/scheduleAccess.js";
8
+ import {
9
+ assertSchedulerSafeCommandAuthorized,
10
+ SchedulerCommandAuthorizationError
11
+ } from "../lib/scheduler-safe-commands.js";
12
+ import { resolveScheduledCommandActorUserId } from "../lib/commandContext.js";
13
+ import { resolveCommandActorUserId } from "./jobs.js";
14
+ const logger = createLogger("scheduler").child({ component: "trigger" });
15
+ function refusedTrigger(scheduleId, outcome) {
16
+ return {
17
+ scheduleId,
18
+ scheduleName: null,
19
+ targetType: null,
20
+ target: null,
21
+ outcome,
22
+ queueJobId: null,
23
+ error: null
24
+ };
25
+ }
26
+ const triggerScheduleCommand = {
27
+ id: "scheduler.jobs.trigger",
28
+ isUndoable: false,
29
+ async execute(input, ctx) {
30
+ const em = ctx.container.resolve("em");
31
+ const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null });
32
+ if (!schedule) return refusedTrigger(input.id, "not_found");
33
+ const access = resolveScheduleAccess(schedule, ctx.auth);
34
+ if (access === "not_found") return refusedTrigger(input.id, "not_found");
35
+ if (access === "forbidden") return refusedTrigger(input.id, "forbidden");
36
+ const details = {
37
+ scheduleId: schedule.id,
38
+ scheduleName: schedule.name ?? null,
39
+ targetType: schedule.targetType ?? null,
40
+ target: (schedule.targetType === "command" ? schedule.targetCommand : schedule.targetQueue) ?? null
41
+ };
42
+ const queueStrategy = process.env.QUEUE_STRATEGY || "local";
43
+ if (queueStrategy !== "async") {
44
+ return { ...details, outcome: "strategy_unsupported", queueJobId: null, error: null };
45
+ }
46
+ const triggeredByUserId = resolveCommandActorUserId(ctx);
47
+ if (schedule.targetType === "command") {
48
+ const actorUserId = resolveScheduledCommandActorUserId(schedule, { triggeredByUserId });
49
+ try {
50
+ await assertSchedulerSafeCommandAuthorized({
51
+ commandId: schedule.targetCommand,
52
+ actorUserId,
53
+ tenantId: schedule.tenantId,
54
+ organizationId: schedule.organizationId,
55
+ rbacService: ctx.container.resolve("rbacService")
56
+ });
57
+ } catch (error) {
58
+ const refused = error instanceof SchedulerCommandAuthorizationError;
59
+ const message = error instanceof Error ? error.message : null;
60
+ if (refused) {
61
+ logger.info("Manual trigger refused by scheduled-command authorization", {
62
+ scheduleId: schedule.id,
63
+ commandId: schedule.targetCommand,
64
+ reason: message
65
+ });
66
+ } else {
67
+ logger.error("Scheduled-command authorization check failed", {
68
+ err: error,
69
+ scheduleId: schedule.id,
70
+ commandId: schedule.targetCommand
71
+ });
72
+ }
73
+ return {
74
+ ...details,
75
+ outcome: refused ? "forbidden" : "failed",
76
+ queueJobId: null,
77
+ error: message
78
+ };
79
+ }
80
+ }
81
+ const payload = {
82
+ scheduleId: schedule.id,
83
+ tenantId: schedule.tenantId,
84
+ organizationId: schedule.organizationId,
85
+ scopeType: schedule.scopeType,
86
+ triggerType: "manual",
87
+ triggeredByUserId
88
+ };
89
+ let executionQueue = null;
90
+ try {
91
+ executionQueue = createQueue("scheduler-execution", queueStrategy, {
92
+ connection: { url: getRedisUrlOrThrow("QUEUE") }
93
+ });
94
+ const queueJobId = await executionQueue.enqueue(payload);
95
+ logger.info("Manually triggered schedule", {
96
+ scheduleId: schedule.id,
97
+ scheduleName: schedule.name,
98
+ jobId: queueJobId,
99
+ triggeredBy: ctx.auth?.sub ?? null
100
+ });
101
+ return { ...details, outcome: "enqueued", queueJobId, error: null };
102
+ } catch (error) {
103
+ logger.error("Manual trigger failed", { err: error, scheduleId: schedule.id });
104
+ return {
105
+ ...details,
106
+ outcome: "failed",
107
+ queueJobId: null,
108
+ error: error instanceof Error ? error.message : null
109
+ };
110
+ } finally {
111
+ try {
112
+ await executionQueue?.close();
113
+ } catch (closeError) {
114
+ logger.warn("Failed to close scheduler execution queue", { err: closeError });
115
+ }
116
+ }
117
+ },
118
+ async buildLog({ result, ctx }) {
119
+ const { translate } = await resolveTranslations();
120
+ return {
121
+ // Keep the label free of create/update/delete/assign verbs: the action-log
122
+ // projection derives `actionType` by string-matching it.
123
+ actionLabel: translate("scheduler.audit.trigger", "Trigger schedule"),
124
+ resourceKind: "scheduler.job",
125
+ // The requested id, so that probing for ids that do not exist is traceable.
126
+ resourceId: result.scheduleId,
127
+ // The actor's scope on every outcome, unlike the sibling create/update/delete
128
+ // handlers which log the schedule's. A refusal must never carry the target's
129
+ // tenant: that would leak cross-tenant existence into the caller's trail and,
130
+ // because the action-log reader filters organization_id by strict equality,
131
+ // file the row where the caller's own auditors could never read it.
132
+ tenantId: ctx.auth?.tenantId ?? null,
133
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
134
+ payload: {
135
+ scheduleName: result.scheduleName,
136
+ targetType: result.targetType,
137
+ target: result.target,
138
+ outcome: result.outcome,
139
+ queueJobId: result.queueJobId,
140
+ error: result.error
141
+ }
142
+ };
143
+ }
144
+ };
145
+ registerCommand(triggerScheduleCommand);
146
+ //# sourceMappingURL=trigger.js.map