@open-mercato/scheduler 0.6.8-develop.7100.1.fbf66fca35 → 0.7.0
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 +4 -9
- 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-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 +44 -107
- package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/helpers/scheduler.js +3 -13
- package/dist/modules/scheduler/__integration__/helpers/scheduler.js.map +2 -2
- package/dist/modules/scheduler/api/jobs/[id]/executions/route.js +13 -7
- package/dist/modules/scheduler/api/jobs/[id]/executions/route.js.map +2 -2
- package/dist/modules/scheduler/api/targets/route.js +12 -2
- package/dist/modules/scheduler/api/targets/route.js.map +2 -2
- package/dist/modules/scheduler/api/trigger/route.js +13 -7
- package/dist/modules/scheduler/api/trigger/route.js.map +2 -2
- package/dist/modules/scheduler/backend/config/scheduled-jobs/page.js +1 -3
- package/dist/modules/scheduler/backend/config/scheduled-jobs/page.js.map +2 -2
- package/dist/modules/scheduler/cli.js +1 -32
- package/dist/modules/scheduler/cli.js.map +2 -2
- package/dist/modules/scheduler/commands/jobs.js +2 -84
- package/dist/modules/scheduler/commands/jobs.js.map +2 -2
- package/dist/modules/scheduler/data/validators.js +3 -27
- package/dist/modules/scheduler/data/validators.js.map +2 -2
- package/dist/modules/scheduler/i18n/de.json +0 -2
- package/dist/modules/scheduler/i18n/en.json +0 -2
- package/dist/modules/scheduler/i18n/es.json +0 -2
- package/dist/modules/scheduler/i18n/ko.json +0 -2
- package/dist/modules/scheduler/i18n/pl.json +0 -2
- package/dist/modules/scheduler/services/localSchedulerService.js +6 -31
- package/dist/modules/scheduler/services/localSchedulerService.js.map +2 -2
- package/dist/modules/scheduler/workers/execute-schedule.worker.js +6 -64
- package/dist/modules/scheduler/workers/execute-schedule.worker.js.map +2 -2
- package/package.json +6 -7
- package/src/modules/scheduler/__integration__/TC-SCHED-001.spec.ts +2 -2
- package/src/modules/scheduler/__integration__/TC-SCHED-004.spec.ts +8 -14
- package/src/modules/scheduler/__integration__/TC-SCHED-007.spec.ts +3 -3
- 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 +66 -154
- package/src/modules/scheduler/__integration__/helpers/scheduler.ts +5 -24
- package/src/modules/scheduler/api/jobs/[id]/executions/route.ts +19 -9
- package/src/modules/scheduler/api/targets/__tests__/route.test.ts +4 -3
- package/src/modules/scheduler/api/targets/route.ts +17 -6
- package/src/modules/scheduler/api/trigger/route.ts +19 -9
- package/src/modules/scheduler/backend/config/scheduled-jobs/page.tsx +1 -4
- package/src/modules/scheduler/cli.ts +1 -44
- package/src/modules/scheduler/commands/jobs.ts +3 -137
- package/src/modules/scheduler/data/__tests__/validators.test.ts +4 -26
- package/src/modules/scheduler/data/validators.ts +2 -37
- package/src/modules/scheduler/i18n/de.json +0 -2
- package/src/modules/scheduler/i18n/en.json +0 -2
- package/src/modules/scheduler/i18n/es.json +0 -2
- package/src/modules/scheduler/i18n/ko.json +0 -2
- package/src/modules/scheduler/i18n/pl.json +0 -2
- package/src/modules/scheduler/services/__tests__/localSchedulerService.test.ts +0 -179
- package/src/modules/scheduler/services/localSchedulerService.ts +6 -39
- package/src/modules/scheduler/workers/__tests__/execute-schedule.worker.test.ts +3 -25
- package/src/modules/scheduler/workers/execute-schedule.worker.ts +7 -76
- package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js +0 -72
- package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js.map +0 -7
- package/dist/modules/scheduler/__integration__/TC-SCHED-008.spec.js +0 -66
- package/dist/modules/scheduler/__integration__/TC-SCHED-008.spec.js.map +0 -7
- package/dist/modules/scheduler/lib/safeQueueTargets.js +0 -157
- package/dist/modules/scheduler/lib/safeQueueTargets.js.map +0 -7
- package/dist/modules/scheduler/lib/scheduleAccess.js +0 -22
- package/dist/modules/scheduler/lib/scheduleAccess.js.map +0 -7
- package/dist/modules/scheduler/workers/test-echo-queue.worker.js +0 -22
- package/dist/modules/scheduler/workers/test-echo-queue.worker.js.map +0 -7
- package/src/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.ts +0 -90
- package/src/modules/scheduler/__integration__/TC-SCHED-008.spec.ts +0 -94
- package/src/modules/scheduler/commands/__tests__/jobs.target-guard.test.ts +0 -177
- package/src/modules/scheduler/data/__tests__/safe-targets.test.ts +0 -110
- package/src/modules/scheduler/lib/__tests__/safeQueueTargets.test.ts +0 -311
- package/src/modules/scheduler/lib/__tests__/scheduleAccess.test.ts +0 -164
- package/src/modules/scheduler/lib/safeQueueTargets.ts +0 -324
- package/src/modules/scheduler/lib/scheduleAccess.ts +0 -104
- package/src/modules/scheduler/workers/test-echo-queue.worker.ts +0 -25
|
@@ -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'\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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,16 +4,12 @@ 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";
|
|
8
7
|
function validateCommandExists(commandId) {
|
|
9
8
|
return commandRegistry.has(commandId);
|
|
10
9
|
}
|
|
11
10
|
function validateCommandIsSchedulable(commandId) {
|
|
12
11
|
return isSchedulerSafeCommandId(commandId);
|
|
13
12
|
}
|
|
14
|
-
function validateSchedulerSafeQueue(queue) {
|
|
15
|
-
return isSchedulerSafeQueue(queue);
|
|
16
|
-
}
|
|
17
13
|
const scheduleBaseSchema = z.object({
|
|
18
14
|
name: z.string().min(1, "Name is required").max(200),
|
|
19
15
|
description: z.string().max(500).optional().nullable(),
|
|
@@ -28,7 +24,9 @@ const scheduleBaseSchema = z.object({
|
|
|
28
24
|
targetCommand: z.string().optional().nullable(),
|
|
29
25
|
targetPayload: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
30
26
|
requireFeature: z.string().optional().nullable(),
|
|
31
|
-
isEnabled: z.boolean().default(true)
|
|
27
|
+
isEnabled: z.boolean().default(true),
|
|
28
|
+
sourceType: z.enum(["user", "module"]).default("user"),
|
|
29
|
+
sourceModule: z.string().optional().nullable()
|
|
32
30
|
});
|
|
33
31
|
const scheduleCreateSchema = scheduleBaseSchema.refine(
|
|
34
32
|
(data) => {
|
|
@@ -97,28 +95,6 @@ const scheduleCreateSchema = scheduleBaseSchema.refine(
|
|
|
97
95
|
message: "Command is not schedulable. Only scheduler-safe commands can be used as schedule targets.",
|
|
98
96
|
path: ["targetCommand"]
|
|
99
97
|
}
|
|
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
|
-
}
|
|
122
98
|
);
|
|
123
99
|
const scheduleUpdateSchema = z.object({
|
|
124
100
|
id: z.uuid(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/data/validators.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport { validateCron } from '../lib/cronParser'\nimport { validateInterval } from '../lib/intervalParser'\nimport { commandRegistry } from '@open-mercato/shared/lib/commands/registry'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { isSchedulerSafeCommandId } from '../lib/scheduler-safe-commands'\nimport { isSchedulerSafeQueue, validateSchedulerTargetPayload } from '../lib/safeQueueTargets'\n\n/**\n * Validate that a command exists in the command registry\n */\nfunction validateCommandExists(commandId: string): boolean {\n return commandRegistry.has(commandId)\n}\n\nfunction validateCommandIsSchedulable(commandId: string): boolean {\n return isSchedulerSafeCommandId(commandId)\n}\n\n/**\n * Validate that a queue was declared scheduler-safe by its owning worker.\n * Internal and system-only workers (webhook processors, indexers, \u2026) stay\n * undiscoverable as schedule targets \u2014 see issue #5213.\n */\nfunction validateSchedulerSafeQueue(queue: string | null | undefined): boolean {\n return isSchedulerSafeQueue(queue)\n}\n\n/**\n * Base schedule fields\n */\nconst scheduleBaseSchema = z.object({\n name: z.string().min(1, 'Name is required').max(200),\n description: z.string().max(500).optional().nullable(),\n \n scopeType: z.enum(['system', 'organization', 'tenant']),\n organizationId: z.uuid().optional().nullable(),\n tenantId: z.uuid().optional().nullable(),\n \n scheduleType: z.enum(['cron', 'interval']),\n scheduleValue: z.string().min(1, 'Schedule value is required'),\n timezone: z.string().default('UTC'),\n \n targetType: z.enum(['queue', 'command']),\n targetQueue: z.string().optional().nullable(),\n targetCommand: z.string().optional().nullable(),\n targetPayload: z.record(z.string(), z.unknown()).optional().nullable(),\n \n requireFeature: z.string().optional().nullable(),\n \n isEnabled: z.boolean().default(true),\n})\n\n/**\n * Create schedule schema\n */\nexport const scheduleCreateSchema = scheduleBaseSchema\n .refine(\n (data) => {\n if (data.scopeType === 'system') {\n return !data.organizationId && !data.tenantId\n }\n if (data.scopeType === 'organization') {\n return !!data.organizationId && !!data.tenantId\n }\n if (data.scopeType === 'tenant') {\n return !data.organizationId && !!data.tenantId\n }\n return false\n },\n {\n message: 'Invalid scope configuration',\n path: ['scopeType'],\n }\n )\n .refine(\n (data) => {\n if (data.targetType === 'queue') {\n return !!data.targetQueue\n }\n if (data.targetType === 'command') {\n return !!data.targetCommand\n }\n return false\n },\n {\n message: 'Target queue or command is required based on target type',\n path: ['targetType'],\n }\n )\n .refine(\n (data) => {\n if (data.scheduleType === 'cron') {\n return validateCron(data.scheduleValue)\n }\n if (data.scheduleType === 'interval') {\n return validateInterval(data.scheduleValue)\n }\n return false\n },\n {\n message: 'Invalid schedule value. For cron: use valid cron expression (e.g., \"0 0 * * *\"). For interval: use <number><unit> format (e.g., \"15m\", \"2h\", \"1d\")',\n path: ['scheduleValue'],\n }\n )\n .refine(\n (data) => {\n // Validate that command exists if targetType is 'command'\n if (data.targetType === 'command' && data.targetCommand) {\n return validateCommandExists(data.targetCommand)\n }\n return true\n },\n {\n message: 'Command does not exist. Please ensure the command is registered before creating a schedule.',\n path: ['targetCommand'],\n }\n )\n .refine(\n (data) => {\n if (data.targetType === 'command' && data.targetCommand) {\n return validateCommandIsSchedulable(data.targetCommand)\n }\n return true\n },\n {\n message: 'Command is not schedulable. Only scheduler-safe commands can be used as schedule targets.',\n path: ['targetCommand'],\n }\n )\n .refine(\n (data) => {\n if (data.targetType === 'queue' && data.targetQueue) {\n return validateSchedulerSafeQueue(data.targetQueue)\n }\n return true\n },\n {\n message: 'Target queue is not an approved scheduler target. Only workers that opted into scheduling can be selected.',\n path: ['targetQueue'],\n }\n )\n .refine(\n (data) => {\n if (data.targetType === 'queue' && data.targetQueue) {\n return validateSchedulerTargetPayload(data.targetQueue, data.targetPayload) === null\n }\n return true\n },\n {\n message: 'Invalid target payload for the selected scheduler queue.',\n path: ['targetPayload'],\n }\n )\n\n/**\n * Update schedule schema (all fields optional except id)\n */\nexport const scheduleUpdateSchema = z.object({\n id: z.uuid(),\n name: z.string().min(1).max(200).optional(),\n description: z.string().max(500).optional().nullable(),\n \n scheduleType: z.enum(['cron', 'interval']).optional(),\n scheduleValue: z.string().min(1).optional(),\n timezone: z.string().optional(),\n \n targetType: z.enum(['queue', 'command']).optional(),\n targetQueue: z.string().optional().nullable(),\n targetCommand: z.string().optional().nullable(),\n targetPayload: z.record(z.string(), z.unknown()).optional().nullable(),\n requireFeature: z.string().optional().nullable(),\n \n isEnabled: z.boolean().optional(),\n})\n .refine(\n (data) => {\n // If scheduleValue is provided, scheduleType must also be provided\n // This ensures we can validate the value against its type\n if (data.scheduleValue !== undefined && data.scheduleType === undefined) {\n return false\n }\n return true\n },\n {\n message: 'scheduleType is required when updating scheduleValue',\n path: ['scheduleType'],\n }\n )\n .refine(\n (data) => {\n // If scheduleValue is provided, validate it based on scheduleType\n if (data.scheduleValue && data.scheduleType) {\n if (data.scheduleType === 'cron') {\n return validateCron(data.scheduleValue)\n }\n if (data.scheduleType === 'interval') {\n return validateInterval(data.scheduleValue)\n }\n }\n return true\n },\n {\n message: 'Invalid schedule value. For cron: use valid cron expression (e.g., \"0 0 * * *\"). For interval: use <number><unit> format (e.g., \"15m\", \"2h\", \"1d\")',\n path: ['scheduleValue'],\n }\n )\n .refine(\n (data) => {\n // If targetType is provided, ensure appropriate target is set\n if (data.targetType === 'queue') {\n return data.targetQueue !== undefined\n }\n if (data.targetType === 'command') {\n return data.targetCommand !== undefined\n }\n return true\n },\n {\n message: 'When changing target type, you must provide the corresponding targetQueue or targetCommand',\n path: ['targetType'],\n }\n )\n .refine(\n (data) => {\n // Validate that command exists if targetCommand is provided\n if (data.targetCommand) {\n return validateCommandExists(data.targetCommand)\n }\n return true\n },\n {\n message: 'Command does not exist. Please ensure the command is registered before updating.',\n path: ['targetCommand'],\n }\n )\n .refine(\n (data) => {\n if (data.targetCommand) {\n return validateCommandIsSchedulable(data.targetCommand)\n }\n return true\n },\n {\n message: 'Command is not schedulable. Only scheduler-safe commands can be used as schedule targets.',\n path: ['targetCommand'],\n }\n )\n // Queue-target safety and payload-shape rules for updates are enforced in the\n // scheduler.jobs.update command against the stored row, so unchanged targets\n // (always resent by the edit form) and legacy remediation stays possible (#5213).\n\n/**\n * Delete schedule schema\n */\nexport const scheduleDeleteSchema = z.object({\n id: z.uuid(),\n})\n\n/**\n * List schedules query schema\n */\nexport const scheduleListQuerySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n id: z.string().uuid().optional(),\n search: z.string().optional(),\n scopeType: z.enum(['system', 'organization', 'tenant']).optional(),\n isEnabled: z.string().optional().transform((val) => {\n if (val === undefined) return undefined\n return parseBooleanToken(val) ?? undefined\n }),\n sourceType: z.enum(['user', 'module']).optional(),\n sourceModule: z.string().optional(),\n sort: z.string().optional(),\n order: z.enum(['asc', 'desc']).optional(),\n})\n\n/**\n * Trigger schedule schema (manual execution)\n */\nexport const scheduleTriggerSchema = z.object({\n id: z.uuid(),\n})\n\n/**\n * Get schedule runs query schema\n */\nexport const scheduleRunsQuerySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n scheduledJobId: z.uuid().optional(),\n status: z.enum(['running', 'completed', 'failed', 'skipped']).optional(),\n triggerType: z.enum(['scheduled', 'manual']).optional(),\n fromDate: z.string().datetime().optional(),\n toDate: z.string().datetime().optional(),\n sort: z.string().optional(),\n order: z.enum(['asc', 'desc']).optional(),\n})\n\n/**\n * Type exports\n */\nexport type ScheduleCreateInput = z.infer<typeof scheduleCreateSchema>\nexport type ScheduleUpdateInput = z.infer<typeof scheduleUpdateSchema>\nexport type ScheduleDeleteInput = z.infer<typeof scheduleDeleteSchema>\nexport type ScheduleListQuery = z.infer<typeof scheduleListQuerySchema>\nexport type ScheduleTriggerInput = z.infer<typeof scheduleTriggerSchema>\nexport type ScheduleRunsQuery = z.infer<typeof scheduleRunsQuerySchema>\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,gCAAgC;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { validateCron } from '../lib/cronParser'\nimport { validateInterval } from '../lib/intervalParser'\nimport { commandRegistry } from '@open-mercato/shared/lib/commands/registry'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { isSchedulerSafeCommandId } from '../lib/scheduler-safe-commands'\n\n/**\n * Validate that a command exists in the command registry\n */\nfunction validateCommandExists(commandId: string): boolean {\n return commandRegistry.has(commandId)\n}\n\nfunction validateCommandIsSchedulable(commandId: string): boolean {\n return isSchedulerSafeCommandId(commandId)\n}\n\n/**\n * Base schedule fields\n */\nconst scheduleBaseSchema = z.object({\n name: z.string().min(1, 'Name is required').max(200),\n description: z.string().max(500).optional().nullable(),\n \n scopeType: z.enum(['system', 'organization', 'tenant']),\n organizationId: z.uuid().optional().nullable(),\n tenantId: z.uuid().optional().nullable(),\n \n scheduleType: z.enum(['cron', 'interval']),\n scheduleValue: z.string().min(1, 'Schedule value is required'),\n timezone: z.string().default('UTC'),\n \n targetType: z.enum(['queue', 'command']),\n targetQueue: z.string().optional().nullable(),\n targetCommand: z.string().optional().nullable(),\n targetPayload: z.record(z.string(), z.unknown()).optional().nullable(),\n \n requireFeature: z.string().optional().nullable(),\n \n isEnabled: z.boolean().default(true),\n sourceType: z.enum(['user', 'module']).default('user'),\n sourceModule: z.string().optional().nullable(),\n})\n\n/**\n * Create schedule schema\n */\nexport const scheduleCreateSchema = scheduleBaseSchema\n .refine(\n (data) => {\n if (data.scopeType === 'system') {\n return !data.organizationId && !data.tenantId\n }\n if (data.scopeType === 'organization') {\n return !!data.organizationId && !!data.tenantId\n }\n if (data.scopeType === 'tenant') {\n return !data.organizationId && !!data.tenantId\n }\n return false\n },\n {\n message: 'Invalid scope configuration',\n path: ['scopeType'],\n }\n )\n .refine(\n (data) => {\n if (data.targetType === 'queue') {\n return !!data.targetQueue\n }\n if (data.targetType === 'command') {\n return !!data.targetCommand\n }\n return false\n },\n {\n message: 'Target queue or command is required based on target type',\n path: ['targetType'],\n }\n )\n .refine(\n (data) => {\n if (data.scheduleType === 'cron') {\n return validateCron(data.scheduleValue)\n }\n if (data.scheduleType === 'interval') {\n return validateInterval(data.scheduleValue)\n }\n return false\n },\n {\n message: 'Invalid schedule value. For cron: use valid cron expression (e.g., \"0 0 * * *\"). For interval: use <number><unit> format (e.g., \"15m\", \"2h\", \"1d\")',\n path: ['scheduleValue'],\n }\n )\n .refine(\n (data) => {\n // Validate that command exists if targetType is 'command'\n if (data.targetType === 'command' && data.targetCommand) {\n return validateCommandExists(data.targetCommand)\n }\n return true\n },\n {\n message: 'Command does not exist. Please ensure the command is registered before creating a schedule.',\n path: ['targetCommand'],\n }\n )\n .refine(\n (data) => {\n if (data.targetType === 'command' && data.targetCommand) {\n return validateCommandIsSchedulable(data.targetCommand)\n }\n return true\n },\n {\n message: 'Command is not schedulable. Only scheduler-safe commands can be used as schedule targets.',\n path: ['targetCommand'],\n }\n )\n\n/**\n * Update schedule schema (all fields optional except id)\n */\nexport const scheduleUpdateSchema = z.object({\n id: z.uuid(),\n name: z.string().min(1).max(200).optional(),\n description: z.string().max(500).optional().nullable(),\n \n scheduleType: z.enum(['cron', 'interval']).optional(),\n scheduleValue: z.string().min(1).optional(),\n timezone: z.string().optional(),\n \n targetType: z.enum(['queue', 'command']).optional(),\n targetQueue: z.string().optional().nullable(),\n targetCommand: z.string().optional().nullable(),\n targetPayload: z.record(z.string(), z.unknown()).optional().nullable(),\n requireFeature: z.string().optional().nullable(),\n \n isEnabled: z.boolean().optional(),\n})\n .refine(\n (data) => {\n // If scheduleValue is provided, scheduleType must also be provided\n // This ensures we can validate the value against its type\n if (data.scheduleValue !== undefined && data.scheduleType === undefined) {\n return false\n }\n return true\n },\n {\n message: 'scheduleType is required when updating scheduleValue',\n path: ['scheduleType'],\n }\n )\n .refine(\n (data) => {\n // If scheduleValue is provided, validate it based on scheduleType\n if (data.scheduleValue && data.scheduleType) {\n if (data.scheduleType === 'cron') {\n return validateCron(data.scheduleValue)\n }\n if (data.scheduleType === 'interval') {\n return validateInterval(data.scheduleValue)\n }\n }\n return true\n },\n {\n message: 'Invalid schedule value. For cron: use valid cron expression (e.g., \"0 0 * * *\"). For interval: use <number><unit> format (e.g., \"15m\", \"2h\", \"1d\")',\n path: ['scheduleValue'],\n }\n )\n .refine(\n (data) => {\n // If targetType is provided, ensure appropriate target is set\n if (data.targetType === 'queue') {\n return data.targetQueue !== undefined\n }\n if (data.targetType === 'command') {\n return data.targetCommand !== undefined\n }\n return true\n },\n {\n message: 'When changing target type, you must provide the corresponding targetQueue or targetCommand',\n path: ['targetType'],\n }\n )\n .refine(\n (data) => {\n // Validate that command exists if targetCommand is provided\n if (data.targetCommand) {\n return validateCommandExists(data.targetCommand)\n }\n return true\n },\n {\n message: 'Command does not exist. Please ensure the command is registered before updating.',\n path: ['targetCommand'],\n }\n )\n .refine(\n (data) => {\n if (data.targetCommand) {\n return validateCommandIsSchedulable(data.targetCommand)\n }\n return true\n },\n {\n message: 'Command is not schedulable. Only scheduler-safe commands can be used as schedule targets.',\n path: ['targetCommand'],\n }\n )\n\n/**\n * Delete schedule schema\n */\nexport const scheduleDeleteSchema = z.object({\n id: z.uuid(),\n})\n\n/**\n * List schedules query schema\n */\nexport const scheduleListQuerySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n id: z.string().uuid().optional(),\n search: z.string().optional(),\n scopeType: z.enum(['system', 'organization', 'tenant']).optional(),\n isEnabled: z.string().optional().transform((val) => {\n if (val === undefined) return undefined\n return parseBooleanToken(val) ?? undefined\n }),\n sourceType: z.enum(['user', 'module']).optional(),\n sourceModule: z.string().optional(),\n sort: z.string().optional(),\n order: z.enum(['asc', 'desc']).optional(),\n})\n\n/**\n * Trigger schedule schema (manual execution)\n */\nexport const scheduleTriggerSchema = z.object({\n id: z.uuid(),\n})\n\n/**\n * Get schedule runs query schema\n */\nexport const scheduleRunsQuerySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n scheduledJobId: z.uuid().optional(),\n status: z.enum(['running', 'completed', 'failed', 'skipped']).optional(),\n triggerType: z.enum(['scheduled', 'manual']).optional(),\n fromDate: z.string().datetime().optional(),\n toDate: z.string().datetime().optional(),\n sort: z.string().optional(),\n order: z.enum(['asc', 'desc']).optional(),\n})\n\n/**\n * Type exports\n */\nexport type ScheduleCreateInput = z.infer<typeof scheduleCreateSchema>\nexport type ScheduleUpdateInput = z.infer<typeof scheduleUpdateSchema>\nexport type ScheduleDeleteInput = z.infer<typeof scheduleDeleteSchema>\nexport type ScheduleListQuery = z.infer<typeof scheduleListQuerySchema>\nexport type ScheduleTriggerInput = z.infer<typeof scheduleTriggerSchema>\nexport type ScheduleRunsQuery = z.infer<typeof scheduleRunsQuerySchema>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -48,8 +48,6 @@
|
|
|
48
48
|
"scheduler.error.unauthorized": "Nicht autorisiert",
|
|
49
49
|
"scheduler.error.update_failed": "Fehler beim Aktualisieren des Zeitplans",
|
|
50
50
|
"scheduler.errors.id_required": "Zeitplankennung ist erforderlich.",
|
|
51
|
-
"scheduler.errors.module_target_immutable": "Von Modulen erstellte Zeitpläne besitzen ihr Ziel; nur operative Felder können hier geändert werden.",
|
|
52
|
-
"scheduler.errors.queue_not_approved": "Ziel-Queue ist kein genehmigtes Scheduler-Ziel. Nur Worker, die sich angemeldet haben, können ausgewählt werden.",
|
|
53
51
|
"scheduler.execution.actions": "Aktionen",
|
|
54
52
|
"scheduler.execution.completed_message": "Diese Ausführung wurde erfolgreich ohne Fehler abgeschlossen.",
|
|
55
53
|
"scheduler.execution.details_title": "Ausführungsdetails",
|
|
@@ -48,8 +48,6 @@
|
|
|
48
48
|
"scheduler.error.unauthorized": "Unauthorized",
|
|
49
49
|
"scheduler.error.update_failed": "Failed to update schedule",
|
|
50
50
|
"scheduler.errors.id_required": "Schedule identifier is required.",
|
|
51
|
-
"scheduler.errors.module_target_immutable": "Module-authored schedules own their target; only operational fields can be updated here.",
|
|
52
|
-
"scheduler.errors.queue_not_approved": "Target queue is not an approved scheduler target. Only workers that opted into scheduling can be selected.",
|
|
53
51
|
"scheduler.execution.actions": "Actions",
|
|
54
52
|
"scheduler.execution.completed_message": "This execution completed successfully without errors.",
|
|
55
53
|
"scheduler.execution.details_title": "Execution Details",
|
|
@@ -48,8 +48,6 @@
|
|
|
48
48
|
"scheduler.error.unauthorized": "No autorizado",
|
|
49
49
|
"scheduler.error.update_failed": "Error al actualizar la programación",
|
|
50
50
|
"scheduler.errors.id_required": "El identificador de programación es obligatorio.",
|
|
51
|
-
"scheduler.errors.module_target_immutable": "Los programados creados por módulos poseen su destino; solo los campos operativos pueden modificarse aquí.",
|
|
52
|
-
"scheduler.errors.queue_not_approved": "La cola de destino no es un destino aprobado del planificador. Solo se pueden seleccionar workers que hayan optado.",
|
|
53
51
|
"scheduler.execution.actions": "Acciones",
|
|
54
52
|
"scheduler.execution.completed_message": "Esta ejecución se completó con éxito sin errores.",
|
|
55
53
|
"scheduler.execution.details_title": "Detalles de ejecución",
|
|
@@ -48,8 +48,6 @@
|
|
|
48
48
|
"scheduler.error.unauthorized": "권한이 없습니다",
|
|
49
49
|
"scheduler.error.update_failed": "일정 수정에 실패했습니다",
|
|
50
50
|
"scheduler.errors.id_required": "일정 식별자는 필수입니다.",
|
|
51
|
-
"scheduler.errors.module_target_immutable": "모듈이 생성한 일정은 대상을 소유하므로 여기서는 운영 필드만 수정할 수 있습니다.",
|
|
52
|
-
"scheduler.errors.queue_not_approved": "대상 큐는 승인된 스케줄러 대상이 아닙니다. 옵트인한 워커만 선택할 수 있습니다.",
|
|
53
51
|
"scheduler.execution.actions": "작업",
|
|
54
52
|
"scheduler.execution.completed_message": "이 실행은 오류 없이 성공적으로 완료되었습니다.",
|
|
55
53
|
"scheduler.execution.details_title": "실행 상세 정보",
|
|
@@ -48,8 +48,6 @@
|
|
|
48
48
|
"scheduler.error.unauthorized": "Brak autoryzacji",
|
|
49
49
|
"scheduler.error.update_failed": "Nie udało się zaktualizować harmonogramu",
|
|
50
50
|
"scheduler.errors.id_required": "Identyfikator harmonogramu jest wymagany.",
|
|
51
|
-
"scheduler.errors.module_target_immutable": "Harmonogramy utworzone przez moduły są właścicielami swojego celu; tutaj można zmieniać wyłącznie pola operacyjne.",
|
|
52
|
-
"scheduler.errors.queue_not_approved": "Kolejka docelowa nie jest zatwierdzonym celem harmonogramu. Można wybrać tylko workery, które się zadeklarowały.",
|
|
53
51
|
"scheduler.execution.actions": "Akcje",
|
|
54
52
|
"scheduler.execution.completed_message": "To wykonanie zakończyło się pomyślnie bez błędów.",
|
|
55
53
|
"scheduler.execution.details_title": "Szczegóły wykonania",
|
|
@@ -8,12 +8,6 @@ import { assertSchedulerSafeCommandAuthorized } from "../lib/scheduler-safe-comm
|
|
|
8
8
|
import { buildScheduledCommandContext } from "../lib/commandContext.js";
|
|
9
9
|
import { buildQueueTargetPayload, buildSchedulerIdempotencyKey } from "../lib/queueTargetPayload.js";
|
|
10
10
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
11
|
-
import {
|
|
12
|
-
canDispatchScheduleQueueTarget,
|
|
13
|
-
getSchedulerQueueRequiredFeatures,
|
|
14
|
-
sanitizeSchedulerTargetPayload,
|
|
15
|
-
validateSchedulerTargetPayload
|
|
16
|
-
} from "../lib/safeQueueTargets.js";
|
|
17
11
|
const logger = createLogger("scheduler").child({ component: "local" });
|
|
18
12
|
class LocalSchedulerService {
|
|
19
13
|
constructor(em, queueFactory, rbacService, config = { pollIntervalMs: 3e4 }) {
|
|
@@ -177,32 +171,13 @@ class LocalSchedulerService {
|
|
|
177
171
|
if (!schedule.targetQueue) {
|
|
178
172
|
throw new Error("Target queue is required for queue target type");
|
|
179
173
|
}
|
|
180
|
-
if (!canDispatchScheduleQueueTarget(schedule)) {
|
|
181
|
-
throw new Error(`Queue is not an approved scheduler target: ${schedule.targetQueue}`);
|
|
182
|
-
}
|
|
183
|
-
const payloadIssue = validateSchedulerTargetPayload(schedule.targetQueue, schedule.targetPayload);
|
|
184
|
-
if (payloadIssue) {
|
|
185
|
-
throw new Error(`Invalid payload for scheduler queue ${schedule.targetQueue}: ${payloadIssue}`);
|
|
186
|
-
}
|
|
187
|
-
if (schedule.scopeType !== "system") {
|
|
188
|
-
for (const feature of getSchedulerQueueRequiredFeatures(schedule.targetQueue)) {
|
|
189
|
-
const hasFeature = await this.checkFeature({ ...schedule, requireFeature: feature });
|
|
190
|
-
if (!hasFeature) {
|
|
191
|
-
throw new Error(`Tenant lacks feature required by scheduler queue ${schedule.targetQueue}: ${feature}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
174
|
const queue = this.queueFactory(schedule.targetQueue);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
idempotencyKey: buildSchedulerIdempotencyKey(schedule.id, Date.now())
|
|
203
|
-
}),
|
|
204
|
-
_jobOrigin: "scheduler"
|
|
205
|
-
});
|
|
175
|
+
await queue.enqueue(buildQueueTargetPayload({
|
|
176
|
+
targetPayload: schedule.targetPayload,
|
|
177
|
+
tenantId: schedule.tenantId,
|
|
178
|
+
organizationId: schedule.organizationId,
|
|
179
|
+
idempotencyKey: buildSchedulerIdempotencyKey(schedule.id, Date.now())
|
|
180
|
+
}));
|
|
206
181
|
logger.info("Enqueued job to target queue", { scheduleId: schedule.id, targetQueue: schedule.targetQueue });
|
|
207
182
|
}
|
|
208
183
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/services/localSchedulerService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { Queue } from '@open-mercato/queue'\nimport { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { ScheduledJob } from '../data/entities.js'\nimport { LocalLockStrategy } from '../lib/localLockStrategy'\nimport { recalculateNextRun } from '../lib/nextRunCalculator'\nimport { emitSchedulerEvent } from '../events.js'\nimport { getGlobalEventBus } from '@open-mercato/shared/modules/events'\nimport { assertSchedulerSafeCommandAuthorized } from '../lib/scheduler-safe-commands.js'\nimport { buildScheduledCommandContext } from '../lib/commandContext.js'\nimport { buildQueueTargetPayload, buildSchedulerIdempotencyKey } from '../lib/queueTargetPayload.js'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n canDispatchScheduleQueueTarget,\n getSchedulerQueueRequiredFeatures,\n sanitizeSchedulerTargetPayload,\n validateSchedulerTargetPayload,\n} from '../lib/safeQueueTargets'\n\nconst logger = createLogger('scheduler').child({ component: 'local' })\n\nexport interface RbacServiceLike {\n tenantHasFeature(tenantId: string | null | undefined, feature: string, opts?: { organizationId?: string | null }): Promise<boolean>\n userHasAllFeatures(userId: string, required: readonly string[], scope: { tenantId: string | null; organizationId: string | null }): Promise<boolean>\n}\n\nexport interface LocalSchedulerConfig {\n pollIntervalMs: number\n}\n\n/**\n * Local scheduler service for development\n * \n * This service polls the database for due schedules and executes them locally\n * without requiring Redis. Perfect for local development.\n * \n * Features:\n * - PostgreSQL polling (configurable interval, default 30s)\n * - In-process duplicate prevention across overlapping poll ticks\n * - Supports both queue and command targets\n * - Emits the same events as async strategy\n * - Updates nextRunAt after execution\n *\n * Limitations:\n * - Polling delay (up to configured interval)\n * - Single instance only (no distributed locking across instances). Duplicate\n * prevention is in-process only: the advisory lock serialises the claim, not\n * the execution, and nextRunAt is advanced only after the target has run, so\n * two processes polling the same database will both execute a due schedule.\n * Run exactly one process with QUEUE_STRATEGY=local, or use the async\n * strategy, which locks across instances.\n * - Higher database load than async strategy\n * \n * Set QUEUE_STRATEGY=local to use this service.\n */\nexport class LocalSchedulerService {\n private isRunning = false\n private pollTimer?: NodeJS.Timeout\n private lockStrategy: LocalLockStrategy\n\n constructor(\n private em: () => EntityManager,\n private queueFactory: (name: string) => Queue,\n private rbacService: RbacServiceLike,\n private config: LocalSchedulerConfig = { pollIntervalMs: 30000 },\n ) {\n this.lockStrategy = new LocalLockStrategy(em)\n }\n\n /**\n * Start the local scheduler polling engine\n */\n async start(): Promise<void> {\n if (this.isRunning) {\n logger.warn('Polling engine already running')\n return\n }\n\n this.isRunning = true\n logger.info('Starting polling engine', { pollIntervalMs: this.config.pollIntervalMs })\n logger.warn(\n 'Local scheduler strategy provides no cross-process protection: duplicate prevention is in-process only, so a second process polling the same database will execute the same due schedule. Run exactly one process with QUEUE_STRATEGY=local, or use the async strategy for distributed locking.',\n )\n\n // Run initial poll immediately\n await this.poll()\n\n // Schedule recurring polls\n this.pollTimer = setInterval(() => {\n this.poll().catch((error) => {\n logger.error('Poll error', { err: error })\n })\n }, this.config.pollIntervalMs)\n\n logger.info('Polling engine started')\n }\n\n /**\n * Stop the local scheduler\n */\n async stop(): Promise<void> {\n logger.info('Stopping polling engine')\n this.isRunning = false\n \n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = undefined\n }\n\n logger.info('Polling engine stopped')\n }\n\n /**\n * Poll for due schedules and execute them\n */\n private async poll(): Promise<void> {\n if (!this.isRunning) {\n return\n }\n\n const em = this.em().fork()\n\n try {\n // Find enabled schedules that are due (limited to avoid spikes after outages)\n const dueSchedules = await em.find(ScheduledJob, {\n isEnabled: true,\n deletedAt: null,\n nextRunAt: { $lte: new Date() },\n }, {\n limit: 100,\n orderBy: { nextRunAt: 'ASC' },\n })\n\n if (dueSchedules.length === 0) {\n logger.debug('No due schedules')\n return\n }\n\n logger.info('Found due schedules', { count: dueSchedules.length })\n\n // Execute each schedule\n for (const schedule of dueSchedules) {\n await this.executeSchedule(schedule)\n }\n } catch (error: unknown) {\n logger.error('Poll failed', { err: error })\n }\n }\n\n /**\n * Execute a single schedule\n */\n private async executeSchedule(schedule: ScheduledJob): Promise<void> {\n const lockKey = `schedule:${schedule.id}`\n const scheduleLogger = logger.child({ scheduleId: schedule.id, scheduleName: schedule.name })\n\n const { acquired } = await this.lockStrategy.runWithLock(lockKey, async () => {\n scheduleLogger.info('Executing schedule')\n\n // Emit started event\n await emitSchedulerEvent('scheduler.job.started', {\n id: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scheduleName: schedule.name,\n scopeType: schedule.scopeType,\n triggerType: 'scheduled',\n startedAt: new Date(),\n })\n\n try {\n // Check feature flag if required\n if (schedule.requireFeature) {\n const hasFeature = await this.checkFeature(schedule)\n \n if (!hasFeature) {\n scheduleLogger.info('Schedule skipped: missing required feature', { requireFeature: schedule.requireFeature })\n \n await emitSchedulerEvent('scheduler.job.skipped', {\n id: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scheduleName: schedule.name,\n scopeType: schedule.scopeType,\n reason: `Missing required feature: ${schedule.requireFeature}`,\n skippedAt: new Date(),\n })\n\n // Still update next run time\n await this.updateNextRun(schedule)\n return\n }\n }\n\n // Execute based on target type\n if (schedule.targetType === 'queue') {\n await this.executeQueueTarget(schedule)\n } else if (schedule.targetType === 'command') {\n await this.executeCommandTarget(schedule)\n } else {\n throw new Error(`Unknown target type: ${schedule.targetType}`)\n }\n\n // Update last run and next run times\n const em = this.em().fork()\n const freshSchedule = await em.findOne(ScheduledJob, { id: schedule.id })\n \n if (freshSchedule) {\n freshSchedule.lastRunAt = new Date()\n \n // Calculate next run time\n const nextRun = recalculateNextRun(\n freshSchedule.scheduleType,\n freshSchedule.scheduleValue,\n freshSchedule.timezone\n )\n \n if (nextRun) {\n freshSchedule.nextRunAt = nextRun\n }\n \n await em.flush()\n }\n\n scheduleLogger.info('Schedule completed')\n\n // Emit completed event\n await emitSchedulerEvent('scheduler.job.completed', {\n id: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scheduleName: schedule.name,\n scopeType: schedule.scopeType,\n completedAt: new Date(),\n })\n } catch (error: unknown) {\n scheduleLogger.error('Schedule failed', { err: error })\n\n // Emit failed event\n await emitSchedulerEvent('scheduler.job.failed', {\n id: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scheduleName: schedule.name,\n scopeType: schedule.scopeType,\n error: error instanceof Error ? error.message : String(error),\n failedAt: new Date(),\n })\n\n // Still update next run time even on failure\n await this.updateNextRun(schedule)\n }\n })\n\n if (!acquired) {\n scheduleLogger.debug('Schedule already locked, skipping')\n return\n }\n }\n\n /**\n * Execute a queue-based target\n */\n private async executeQueueTarget(schedule: ScheduledJob): Promise<void> {\n if (!schedule.targetQueue) {\n throw new Error('Target queue is required for queue target type')\n }\n\n // Dispatch-time reauthorization (#5213): provenance is verified against the\n // live registry (module rows must own their queue), API rows must target\n // opted-in queues.\n if (!canDispatchScheduleQueueTarget(schedule)) {\n throw new Error(`Queue is not an approved scheduler target: ${schedule.targetQueue}`)\n }\n\n const payloadIssue = validateSchedulerTargetPayload(schedule.targetQueue, schedule.targetPayload)\n if (payloadIssue) {\n throw new Error(`Invalid payload for scheduler queue ${schedule.targetQueue}: ${payloadIssue}`)\n }\n\n if (schedule.scopeType !== 'system') {\n for (const feature of getSchedulerQueueRequiredFeatures(schedule.targetQueue)) {\n const hasFeature = await this.checkFeature({ ...schedule, requireFeature: feature } as ScheduledJob)\n if (!hasFeature) {\n throw new Error(`Tenant lacks feature required by scheduler queue ${schedule.targetQueue}: ${feature}`)\n }\n }\n }\n\n const queue = this.queueFactory(schedule.targetQueue)\n\n // Deliver the same flat contract as the asynchronous execute-schedule\n // worker: targetPayload fields on the root, scheduler-owned scope and\n // idempotency fields applied last. Local mode runs each firing exactly\n // once, so the firing timestamp is a valid logical execution key.\n // Author-supplied scope/envelope keys never survive dispatch (#5213).\n const sanitizedPayload = sanitizeSchedulerTargetPayload(schedule.targetPayload, schedule)\n\n await queue.enqueue({\n ...buildQueueTargetPayload({\n targetPayload: sanitizedPayload,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n idempotencyKey: buildSchedulerIdempotencyKey(schedule.id, Date.now()),\n }),\n _jobOrigin: 'scheduler' as const,\n })\n\n logger.info('Enqueued job to target queue', { scheduleId: schedule.id, targetQueue: schedule.targetQueue })\n }\n\n /**\n * Execute a command-based target\n */\n private async executeCommandTarget(schedule: ScheduledJob): Promise<void> {\n if (!schedule.targetCommand) {\n throw new Error('Target command is required for command target type')\n }\n\n const commandBus = new CommandBus()\n const actorUserId = typeof schedule.createdByUserId === 'string' ? schedule.createdByUserId.trim() : ''\n await assertSchedulerSafeCommandAuthorized({\n commandId: schedule.targetCommand,\n actorUserId,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n rbacService: this.rbacService,\n })\n \n const commandInput = {\n ...((schedule.targetPayload as Record<string, unknown>) || {}),\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n }\n \n // Build the schedule-scoped command context after the allowlist/RBAC gate.\n const commandCtx = buildScheduledCommandContext(\n schedule,\n {\n resolve: (name: string) => {\n // Simple resolver that forwards to our dependencies\n if (name === 'em') return this.em()\n if (name === 'eventBus') return getGlobalEventBus()\n if (name === 'rbacService') return this.rbacService\n throw new Error(`Service not available in scheduler context: ${name}`)\n },\n } as AppContainer,\n )\n\n await commandBus.execute(schedule.targetCommand, {\n input: commandInput,\n ctx: commandCtx,\n })\n\n logger.info('Executed command', { scheduleId: schedule.id, targetCommand: schedule.targetCommand })\n }\n\n /**\n * Check if user/tenant has required feature\n */\n private async checkFeature(schedule: ScheduledJob): Promise<boolean> {\n if (!schedule.requireFeature) {\n return true\n }\n\n try {\n // For system-scoped schedules, no RBAC check needed\n if (schedule.scopeType === 'system') {\n return true\n }\n\n // For tenant/org scoped schedules, check if ANY admin user has the feature\n // This is a simplified check - in reality, scheduled jobs run without a user context\n // so we're just checking if the feature is enabled for the tenant\n const hasFeature = await this.rbacService.tenantHasFeature(\n schedule.tenantId,\n schedule.requireFeature,\n {\n organizationId: schedule.organizationId,\n }\n )\n\n return hasFeature\n } catch (error: unknown) {\n logger.error('Feature check failed', { scheduleId: schedule.id, err: error })\n return false\n }\n }\n\n /**\n * Update next run time for a schedule\n */\n private async updateNextRun(schedule: ScheduledJob): Promise<void> {\n const em = this.em().fork()\n const freshSchedule = await em.findOne(ScheduledJob, { id: schedule.id })\n \n if (freshSchedule) {\n const nextRun = recalculateNextRun(\n freshSchedule.scheduleType,\n freshSchedule.scheduleValue,\n freshSchedule.timezone\n )\n \n if (nextRun) {\n freshSchedule.nextRunAt = nextRun\n await em.flush()\n }\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,kBAAkB;AAE3B,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,4CAA4C;AACrD,SAAS,oCAAoC;AAC7C,SAAS,yBAAyB,oCAAoC;AACtE,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { Queue } from '@open-mercato/queue'\nimport { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { ScheduledJob } from '../data/entities.js'\nimport { LocalLockStrategy } from '../lib/localLockStrategy'\nimport { recalculateNextRun } from '../lib/nextRunCalculator'\nimport { emitSchedulerEvent } from '../events.js'\nimport { getGlobalEventBus } from '@open-mercato/shared/modules/events'\nimport { assertSchedulerSafeCommandAuthorized } from '../lib/scheduler-safe-commands.js'\nimport { buildScheduledCommandContext } from '../lib/commandContext.js'\nimport { buildQueueTargetPayload, buildSchedulerIdempotencyKey } from '../lib/queueTargetPayload.js'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler').child({ component: 'local' })\n\nexport interface RbacServiceLike {\n tenantHasFeature(tenantId: string | null | undefined, feature: string, opts?: { organizationId?: string | null }): Promise<boolean>\n userHasAllFeatures(userId: string, required: readonly string[], scope: { tenantId: string | null; organizationId: string | null }): Promise<boolean>\n}\n\nexport interface LocalSchedulerConfig {\n pollIntervalMs: number\n}\n\n/**\n * Local scheduler service for development\n * \n * This service polls the database for due schedules and executes them locally\n * without requiring Redis. Perfect for local development.\n * \n * Features:\n * - PostgreSQL polling (configurable interval, default 30s)\n * - In-process duplicate prevention across overlapping poll ticks\n * - Supports both queue and command targets\n * - Emits the same events as async strategy\n * - Updates nextRunAt after execution\n *\n * Limitations:\n * - Polling delay (up to configured interval)\n * - Single instance only (no distributed locking across instances). Duplicate\n * prevention is in-process only: the advisory lock serialises the claim, not\n * the execution, and nextRunAt is advanced only after the target has run, so\n * two processes polling the same database will both execute a due schedule.\n * Run exactly one process with QUEUE_STRATEGY=local, or use the async\n * strategy, which locks across instances.\n * - Higher database load than async strategy\n * \n * Set QUEUE_STRATEGY=local to use this service.\n */\nexport class LocalSchedulerService {\n private isRunning = false\n private pollTimer?: NodeJS.Timeout\n private lockStrategy: LocalLockStrategy\n\n constructor(\n private em: () => EntityManager,\n private queueFactory: (name: string) => Queue,\n private rbacService: RbacServiceLike,\n private config: LocalSchedulerConfig = { pollIntervalMs: 30000 },\n ) {\n this.lockStrategy = new LocalLockStrategy(em)\n }\n\n /**\n * Start the local scheduler polling engine\n */\n async start(): Promise<void> {\n if (this.isRunning) {\n logger.warn('Polling engine already running')\n return\n }\n\n this.isRunning = true\n logger.info('Starting polling engine', { pollIntervalMs: this.config.pollIntervalMs })\n logger.warn(\n 'Local scheduler strategy provides no cross-process protection: duplicate prevention is in-process only, so a second process polling the same database will execute the same due schedule. Run exactly one process with QUEUE_STRATEGY=local, or use the async strategy for distributed locking.',\n )\n\n // Run initial poll immediately\n await this.poll()\n\n // Schedule recurring polls\n this.pollTimer = setInterval(() => {\n this.poll().catch((error) => {\n logger.error('Poll error', { err: error })\n })\n }, this.config.pollIntervalMs)\n\n logger.info('Polling engine started')\n }\n\n /**\n * Stop the local scheduler\n */\n async stop(): Promise<void> {\n logger.info('Stopping polling engine')\n this.isRunning = false\n \n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = undefined\n }\n\n logger.info('Polling engine stopped')\n }\n\n /**\n * Poll for due schedules and execute them\n */\n private async poll(): Promise<void> {\n if (!this.isRunning) {\n return\n }\n\n const em = this.em().fork()\n\n try {\n // Find enabled schedules that are due (limited to avoid spikes after outages)\n const dueSchedules = await em.find(ScheduledJob, {\n isEnabled: true,\n deletedAt: null,\n nextRunAt: { $lte: new Date() },\n }, {\n limit: 100,\n orderBy: { nextRunAt: 'ASC' },\n })\n\n if (dueSchedules.length === 0) {\n logger.debug('No due schedules')\n return\n }\n\n logger.info('Found due schedules', { count: dueSchedules.length })\n\n // Execute each schedule\n for (const schedule of dueSchedules) {\n await this.executeSchedule(schedule)\n }\n } catch (error: unknown) {\n logger.error('Poll failed', { err: error })\n }\n }\n\n /**\n * Execute a single schedule\n */\n private async executeSchedule(schedule: ScheduledJob): Promise<void> {\n const lockKey = `schedule:${schedule.id}`\n const scheduleLogger = logger.child({ scheduleId: schedule.id, scheduleName: schedule.name })\n\n const { acquired } = await this.lockStrategy.runWithLock(lockKey, async () => {\n scheduleLogger.info('Executing schedule')\n\n // Emit started event\n await emitSchedulerEvent('scheduler.job.started', {\n id: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scheduleName: schedule.name,\n scopeType: schedule.scopeType,\n triggerType: 'scheduled',\n startedAt: new Date(),\n })\n\n try {\n // Check feature flag if required\n if (schedule.requireFeature) {\n const hasFeature = await this.checkFeature(schedule)\n \n if (!hasFeature) {\n scheduleLogger.info('Schedule skipped: missing required feature', { requireFeature: schedule.requireFeature })\n \n await emitSchedulerEvent('scheduler.job.skipped', {\n id: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scheduleName: schedule.name,\n scopeType: schedule.scopeType,\n reason: `Missing required feature: ${schedule.requireFeature}`,\n skippedAt: new Date(),\n })\n\n // Still update next run time\n await this.updateNextRun(schedule)\n return\n }\n }\n\n // Execute based on target type\n if (schedule.targetType === 'queue') {\n await this.executeQueueTarget(schedule)\n } else if (schedule.targetType === 'command') {\n await this.executeCommandTarget(schedule)\n } else {\n throw new Error(`Unknown target type: ${schedule.targetType}`)\n }\n\n // Update last run and next run times\n const em = this.em().fork()\n const freshSchedule = await em.findOne(ScheduledJob, { id: schedule.id })\n \n if (freshSchedule) {\n freshSchedule.lastRunAt = new Date()\n \n // Calculate next run time\n const nextRun = recalculateNextRun(\n freshSchedule.scheduleType,\n freshSchedule.scheduleValue,\n freshSchedule.timezone\n )\n \n if (nextRun) {\n freshSchedule.nextRunAt = nextRun\n }\n \n await em.flush()\n }\n\n scheduleLogger.info('Schedule completed')\n\n // Emit completed event\n await emitSchedulerEvent('scheduler.job.completed', {\n id: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scheduleName: schedule.name,\n scopeType: schedule.scopeType,\n completedAt: new Date(),\n })\n } catch (error: unknown) {\n scheduleLogger.error('Schedule failed', { err: error })\n\n // Emit failed event\n await emitSchedulerEvent('scheduler.job.failed', {\n id: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scheduleName: schedule.name,\n scopeType: schedule.scopeType,\n error: error instanceof Error ? error.message : String(error),\n failedAt: new Date(),\n })\n\n // Still update next run time even on failure\n await this.updateNextRun(schedule)\n }\n })\n\n if (!acquired) {\n scheduleLogger.debug('Schedule already locked, skipping')\n return\n }\n }\n\n /**\n * Execute a queue-based target\n */\n private async executeQueueTarget(schedule: ScheduledJob): Promise<void> {\n if (!schedule.targetQueue) {\n throw new Error('Target queue is required for queue target type')\n }\n\n const queue = this.queueFactory(schedule.targetQueue)\n\n // Deliver the same flat contract as the asynchronous execute-schedule\n // worker: targetPayload fields on the root, scheduler-owned scope and\n // idempotency fields applied last. Local mode runs each firing exactly\n // once, so the firing timestamp is a valid logical execution key.\n await queue.enqueue(buildQueueTargetPayload({\n targetPayload: schedule.targetPayload,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n idempotencyKey: buildSchedulerIdempotencyKey(schedule.id, Date.now()),\n }))\n\n logger.info('Enqueued job to target queue', { scheduleId: schedule.id, targetQueue: schedule.targetQueue })\n }\n\n /**\n * Execute a command-based target\n */\n private async executeCommandTarget(schedule: ScheduledJob): Promise<void> {\n if (!schedule.targetCommand) {\n throw new Error('Target command is required for command target type')\n }\n\n const commandBus = new CommandBus()\n const actorUserId = typeof schedule.createdByUserId === 'string' ? schedule.createdByUserId.trim() : ''\n await assertSchedulerSafeCommandAuthorized({\n commandId: schedule.targetCommand,\n actorUserId,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n rbacService: this.rbacService,\n })\n \n const commandInput = {\n ...((schedule.targetPayload as Record<string, unknown>) || {}),\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n }\n \n // Build the schedule-scoped command context after the allowlist/RBAC gate.\n const commandCtx = buildScheduledCommandContext(\n schedule,\n {\n resolve: (name: string) => {\n // Simple resolver that forwards to our dependencies\n if (name === 'em') return this.em()\n if (name === 'eventBus') return getGlobalEventBus()\n if (name === 'rbacService') return this.rbacService\n throw new Error(`Service not available in scheduler context: ${name}`)\n },\n } as AppContainer,\n )\n\n await commandBus.execute(schedule.targetCommand, {\n input: commandInput,\n ctx: commandCtx,\n })\n\n logger.info('Executed command', { scheduleId: schedule.id, targetCommand: schedule.targetCommand })\n }\n\n /**\n * Check if user/tenant has required feature\n */\n private async checkFeature(schedule: ScheduledJob): Promise<boolean> {\n if (!schedule.requireFeature) {\n return true\n }\n\n try {\n // For system-scoped schedules, no RBAC check needed\n if (schedule.scopeType === 'system') {\n return true\n }\n\n // For tenant/org scoped schedules, check if ANY admin user has the feature\n // This is a simplified check - in reality, scheduled jobs run without a user context\n // so we're just checking if the feature is enabled for the tenant\n const hasFeature = await this.rbacService.tenantHasFeature(\n schedule.tenantId,\n schedule.requireFeature,\n {\n organizationId: schedule.organizationId,\n }\n )\n\n return hasFeature\n } catch (error: unknown) {\n logger.error('Feature check failed', { scheduleId: schedule.id, err: error })\n return false\n }\n }\n\n /**\n * Update next run time for a schedule\n */\n private async updateNextRun(schedule: ScheduledJob): Promise<void> {\n const em = this.em().fork()\n const freshSchedule = await em.findOne(ScheduledJob, { id: schedule.id })\n \n if (freshSchedule) {\n const nextRun = recalculateNextRun(\n freshSchedule.scheduleType,\n freshSchedule.scheduleValue,\n freshSchedule.timezone\n )\n \n if (nextRun) {\n freshSchedule.nextRunAt = nextRun\n await em.flush()\n }\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,12 +7,6 @@ import { assertSchedulerSafeCommandAuthorized } from "../lib/scheduler-safe-comm
|
|
|
7
7
|
import { buildScheduledCommandContext } from "../lib/commandContext.js";
|
|
8
8
|
import { buildQueueTargetPayload, buildSchedulerIdempotencyKey } from "../lib/queueTargetPayload.js";
|
|
9
9
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
10
|
-
import {
|
|
11
|
-
canDispatchScheduleQueueTarget,
|
|
12
|
-
getSchedulerQueueRequiredFeatures,
|
|
13
|
-
sanitizeSchedulerTargetPayload,
|
|
14
|
-
validateSchedulerTargetPayload
|
|
15
|
-
} from "../lib/safeQueueTargets.js";
|
|
16
10
|
const logger = createLogger("scheduler").child({ component: "worker" });
|
|
17
11
|
const metadata = {
|
|
18
12
|
queue: "scheduler-execution",
|
|
@@ -99,54 +93,6 @@ async function executeScheduleWorker(job, ctx) {
|
|
|
99
93
|
}
|
|
100
94
|
}
|
|
101
95
|
if (schedule.targetType === "queue" && schedule.targetQueue) {
|
|
102
|
-
if (!canDispatchScheduleQueueTarget(schedule)) {
|
|
103
|
-
logger.error("Refusing non-safe queue target for schedule", {
|
|
104
|
-
scheduleId,
|
|
105
|
-
targetQueue: schedule.targetQueue,
|
|
106
|
-
sourceType: schedule.sourceType,
|
|
107
|
-
sourceModule: schedule.sourceType === "module" ? schedule.sourceModule : void 0
|
|
108
|
-
});
|
|
109
|
-
await emitSchedulerEvent("scheduler.job.skipped", {
|
|
110
|
-
id: schedule.id,
|
|
111
|
-
tenantId: schedule.tenantId,
|
|
112
|
-
organizationId: schedule.organizationId,
|
|
113
|
-
reason: `Queue is not an approved scheduler target: ${schedule.targetQueue}`
|
|
114
|
-
});
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const payloadIssue = validateSchedulerTargetPayload(schedule.targetQueue, schedule.targetPayload);
|
|
118
|
-
if (payloadIssue) {
|
|
119
|
-
logger.error("Refusing schedule with invalid payload for queue target", {
|
|
120
|
-
scheduleId,
|
|
121
|
-
targetQueue: schedule.targetQueue,
|
|
122
|
-
issue: payloadIssue
|
|
123
|
-
});
|
|
124
|
-
await emitSchedulerEvent("scheduler.job.skipped", {
|
|
125
|
-
id: schedule.id,
|
|
126
|
-
tenantId: schedule.tenantId,
|
|
127
|
-
organizationId: schedule.organizationId,
|
|
128
|
-
reason: `Invalid payload for scheduler queue ${schedule.targetQueue}`
|
|
129
|
-
});
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const requiredQueueFeatures = getSchedulerQueueRequiredFeatures(schedule.targetQueue);
|
|
133
|
-
if (schedule.scopeType !== "system" && requiredQueueFeatures.length > 0) {
|
|
134
|
-
for (const feature of requiredQueueFeatures) {
|
|
135
|
-
if (await rbacService.tenantHasFeature(schedule.tenantId, feature, { organizationId: schedule.organizationId })) continue;
|
|
136
|
-
logger.error("Refusing schedule whose tenant lacks a required feature of the queue target", {
|
|
137
|
-
scheduleId,
|
|
138
|
-
targetQueue: schedule.targetQueue,
|
|
139
|
-
feature
|
|
140
|
-
});
|
|
141
|
-
await emitSchedulerEvent("scheduler.job.skipped", {
|
|
142
|
-
id: schedule.id,
|
|
143
|
-
tenantId: schedule.tenantId,
|
|
144
|
-
organizationId: schedule.organizationId,
|
|
145
|
-
reason: `Tenant lacks feature required by scheduler queue ${schedule.targetQueue}: ${feature}`
|
|
146
|
-
});
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
96
|
const queueStrategy = process.env.QUEUE_STRATEGY || "local";
|
|
151
97
|
const targetQueue = createQueue(schedule.targetQueue, queueStrategy, {
|
|
152
98
|
connection: { url: getRedisUrlOrThrow("QUEUE") }
|
|
@@ -154,16 +100,12 @@ async function executeScheduleWorker(job, ctx) {
|
|
|
154
100
|
let targetJobId;
|
|
155
101
|
try {
|
|
156
102
|
const idempotencyKey = buildSchedulerIdempotencyKey(schedule.id, ctx.jobId ?? Date.now());
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
idempotencyKey
|
|
164
|
-
}),
|
|
165
|
-
_jobOrigin: "scheduler"
|
|
166
|
-
});
|
|
103
|
+
targetJobId = await targetQueue.enqueue(buildQueueTargetPayload({
|
|
104
|
+
targetPayload: schedule.targetPayload,
|
|
105
|
+
tenantId: schedule.tenantId,
|
|
106
|
+
organizationId: schedule.organizationId,
|
|
107
|
+
idempotencyKey
|
|
108
|
+
}));
|
|
167
109
|
} finally {
|
|
168
110
|
await targetQueue.close();
|
|
169
111
|
}
|