@open-mercato/scheduler 0.8.1-develop.7274.1.21a0083828 → 0.8.1-develop.7294.1.0ef99fc518
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/scheduler/lib/intervalParser.js +11 -28
- package/dist/modules/scheduler/lib/intervalParser.js.map +2 -2
- package/dist/modules/scheduler/services/schedulerService.js +11 -2
- package/dist/modules/scheduler/services/schedulerService.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/scheduler/lib/intervalParser.ts +16 -31
- package/src/modules/scheduler/services/schedulerService.ts +11 -2
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
MIN_SCHEDULE_INTERVAL_MS,
|
|
3
|
+
isValidScheduleInterval,
|
|
4
|
+
matchScheduleInterval,
|
|
5
|
+
parseScheduleInterval
|
|
6
|
+
} from "@open-mercato/shared/lib/schedule/interval";
|
|
2
7
|
function parseInterval(interval) {
|
|
3
|
-
|
|
4
|
-
const match = interval.match(regex);
|
|
5
|
-
if (!match) {
|
|
6
|
-
throw new Error(`Invalid interval format: ${interval}. Expected format: <number><unit> (e.g., 15m, 2h, 1d)`);
|
|
7
|
-
}
|
|
8
|
-
const value = parseInt(match[1], 10);
|
|
9
|
-
const unit = match[2];
|
|
10
|
-
const multipliers = {
|
|
11
|
-
s: 1e3,
|
|
12
|
-
// seconds
|
|
13
|
-
m: 60 * 1e3,
|
|
14
|
-
// minutes
|
|
15
|
-
h: 60 * 60 * 1e3,
|
|
16
|
-
// hours
|
|
17
|
-
d: 24 * 60 * 60 * 1e3
|
|
18
|
-
// days
|
|
19
|
-
};
|
|
20
|
-
return value * multipliers[unit];
|
|
8
|
+
return parseScheduleInterval(interval);
|
|
21
9
|
}
|
|
22
10
|
function resolveScheduleIntervalMs(interval) {
|
|
23
11
|
return Math.max(parseInterval(interval), MIN_SCHEDULE_INTERVAL_MS);
|
|
@@ -27,20 +15,15 @@ function calculateNextRunFromInterval(interval, fromDate = /* @__PURE__ */ new D
|
|
|
27
15
|
return new Date(fromDate.getTime() + ms);
|
|
28
16
|
}
|
|
29
17
|
function validateInterval(interval) {
|
|
30
|
-
|
|
31
|
-
return parseInterval(interval) >= MIN_SCHEDULE_INTERVAL_MS;
|
|
32
|
-
} catch {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
18
|
+
return isValidScheduleInterval(interval);
|
|
35
19
|
}
|
|
36
20
|
function intervalToHuman(interval) {
|
|
37
21
|
try {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
if (!match) {
|
|
22
|
+
const parts = matchScheduleInterval(interval);
|
|
23
|
+
if (!parts) {
|
|
41
24
|
return interval;
|
|
42
25
|
}
|
|
43
|
-
const unit =
|
|
26
|
+
const unit = parts.unit;
|
|
44
27
|
const ms = parseInterval(interval);
|
|
45
28
|
const seconds = ms / 1e3;
|
|
46
29
|
if (seconds === 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/lib/intervalParser.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Parse simple interval strings like '15m', '2h', '1d' into milliseconds\n */\nexport
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import {\n MIN_SCHEDULE_INTERVAL_MS,\n isValidScheduleInterval,\n matchScheduleInterval,\n parseScheduleInterval,\n} from '@open-mercato/shared/lib/schedule/interval'\n\n/**\n * Parse simple interval strings like '15m', '2h', '1d' into milliseconds\n */\nexport { MIN_SCHEDULE_INTERVAL_MS }\n\nexport function parseInterval(interval: string): number {\n return parseScheduleInterval(interval)\n}\n\nexport function resolveScheduleIntervalMs(interval: string): number {\n return Math.max(parseInterval(interval), MIN_SCHEDULE_INTERVAL_MS)\n}\n\n/**\n * Calculate next run time based on interval\n */\nexport function calculateNextRunFromInterval(\n interval: string,\n fromDate: Date = new Date()\n): Date {\n const ms = resolveScheduleIntervalMs(interval)\n return new Date(fromDate.getTime() + ms)\n}\n\n/**\n * Validate an interval string\n */\nexport function validateInterval(interval: string): boolean {\n return isValidScheduleInterval(interval)\n}\n\n/**\n * Convert interval to human-readable string\n * Preserves the original unit unless it can be cleanly converted to a larger unit\n * Special case: Zero always displays as \"0 seconds\"\n */\nexport function intervalToHuman(interval: string): string {\n try {\n const parts = matchScheduleInterval(interval)\n\n if (!parts) {\n return interval\n }\n\n const unit = parts.unit\n\n const ms = parseInterval(interval)\n const seconds = ms / 1000\n \n // Special case: zero always displays as \"0 seconds\"\n if (seconds === 0) {\n return '0 seconds'\n }\n \n const minutes = seconds / 60\n const hours = minutes / 60\n const days = hours / 24\n \n // Only convert to larger units based on the input unit\n // This preserves the original unit choice (e.g., 60s stays as \"60 seconds\")\n switch (unit) {\n case 's':\n // Seconds never convert up (preserve user's choice)\n return `${seconds} second${seconds !== 1 ? 's' : ''}`\n \n case 'm':\n // Minutes can convert to hours or days\n if (days >= 1 && days === Math.floor(days)) {\n return `${days} day${days !== 1 ? 's' : ''}`\n }\n if (hours >= 1 && hours === Math.floor(hours)) {\n return `${hours} hour${hours !== 1 ? 's' : ''}`\n }\n return `${minutes} minute${minutes !== 1 ? 's' : ''}`\n \n case 'h':\n // Hours can convert to days\n if (days >= 1 && days === Math.floor(days)) {\n return `${days} day${days !== 1 ? 's' : ''}`\n }\n return `${hours} hour${hours !== 1 ? 's' : ''}`\n \n case 'd':\n // Days stay as days\n return `${days} day${days !== 1 ? 's' : ''}`\n \n default:\n return interval\n }\n } catch {\n return interval\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAOA,SAAS,cAAc,UAA0B;AACtD,SAAO,sBAAsB,QAAQ;AACvC;AAEO,SAAS,0BAA0B,UAA0B;AAClE,SAAO,KAAK,IAAI,cAAc,QAAQ,GAAG,wBAAwB;AACnE;AAKO,SAAS,6BACd,UACA,WAAiB,oBAAI,KAAK,GACpB;AACN,QAAM,KAAK,0BAA0B,QAAQ;AAC7C,SAAO,IAAI,KAAK,SAAS,QAAQ,IAAI,EAAE;AACzC;AAKO,SAAS,iBAAiB,UAA2B;AAC1D,SAAO,wBAAwB,QAAQ;AACzC;AAOO,SAAS,gBAAgB,UAA0B;AACxD,MAAI;AACF,UAAM,QAAQ,sBAAsB,QAAQ;AAE5C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM;AAEnB,UAAM,KAAK,cAAc,QAAQ;AACjC,UAAM,UAAU,KAAK;AAGrB,QAAI,YAAY,GAAG;AACjB,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,UAAU;AAC1B,UAAM,QAAQ,UAAU;AACxB,UAAM,OAAO,QAAQ;AAIrB,YAAQ,MAAM;AAAA,MACZ,KAAK;AAEH,eAAO,GAAG,OAAO,UAAU,YAAY,IAAI,MAAM,EAAE;AAAA,MAErD,KAAK;AAEH,YAAI,QAAQ,KAAK,SAAS,KAAK,MAAM,IAAI,GAAG;AAC1C,iBAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,QAC5C;AACA,YAAI,SAAS,KAAK,UAAU,KAAK,MAAM,KAAK,GAAG;AAC7C,iBAAO,GAAG,KAAK,QAAQ,UAAU,IAAI,MAAM,EAAE;AAAA,QAC/C;AACA,eAAO,GAAG,OAAO,UAAU,YAAY,IAAI,MAAM,EAAE;AAAA,MAErD,KAAK;AAEH,YAAI,QAAQ,KAAK,SAAS,KAAK,MAAM,IAAI,GAAG;AAC1C,iBAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,QAC5C;AACA,eAAO,GAAG,KAAK,QAAQ,UAAU,IAAI,MAAM,EAAE;AAAA,MAE/C,KAAK;AAEH,eAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,MAE5C;AACE,eAAO;AAAA,IACX;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { ScheduledJob } from "../data/entities.js";
|
|
|
2
2
|
import { calculateNextRunForWrite } from "../lib/nextRunCalculator.js";
|
|
3
3
|
import { enforceTenantActiveScheduleLimit } from "../lib/activeScheduleLimits.js";
|
|
4
4
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
5
|
+
import { createInvalidScheduleValueError } from "@open-mercato/shared/lib/schedule/invalidScheduleValue";
|
|
5
6
|
const logger = createLogger("scheduler");
|
|
6
7
|
class SchedulerService {
|
|
7
8
|
constructor(em, bullmqService) {
|
|
@@ -21,7 +22,11 @@ class SchedulerService {
|
|
|
21
22
|
registration.timezone || "UTC"
|
|
22
23
|
);
|
|
23
24
|
if (!nextRunAt) {
|
|
24
|
-
throw
|
|
25
|
+
throw createInvalidScheduleValueError(
|
|
26
|
+
registration.scheduleType,
|
|
27
|
+
registration.scheduleValue,
|
|
28
|
+
`Failed to calculate next run time for schedule: ${registration.id}`
|
|
29
|
+
);
|
|
25
30
|
}
|
|
26
31
|
let schedule = await em.findOne(ScheduledJob, { id: registration.id });
|
|
27
32
|
const nextTenantId = registration.tenantId || null;
|
|
@@ -131,7 +136,11 @@ class SchedulerService {
|
|
|
131
136
|
changes.timezone ?? schedule.timezone
|
|
132
137
|
) : null;
|
|
133
138
|
if (scheduleChanged && !nextRunAt) {
|
|
134
|
-
throw
|
|
139
|
+
throw createInvalidScheduleValueError(
|
|
140
|
+
changes.scheduleType ?? schedule.scheduleType,
|
|
141
|
+
changes.scheduleValue ?? schedule.scheduleValue,
|
|
142
|
+
`Failed to calculate next run time for schedule: ${scheduleId}`
|
|
143
|
+
);
|
|
135
144
|
}
|
|
136
145
|
if (changes.name !== void 0) schedule.name = changes.name;
|
|
137
146
|
if (changes.description !== void 0) schedule.description = changes.description || null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/scheduler/services/schedulerService.ts"],
|
|
4
|
-
"sourcesContent": ["import 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 { BullMQSchedulerService } from './bullmqSchedulerService.js'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler')\n\nexport interface ScheduleRegistration {\n id: string\n name: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: unknown\n requireFeature?: string\n sourceType?: 'user' | 'module'\n sourceModule?: string\n isEnabled?: boolean\n description?: string\n}\n\nexport class SchedulerService {\n constructor(\n private em: () => EntityManager,\n private bullmqService?: BullMQSchedulerService,\n ) {}\n\n /**\n * Register a new schedule (upsert)\n */\n async register(registration: ScheduleRegistration): Promise<void> {\n const em = this.em().fork()\n \n // Validate scope consistency\n this.validateScope(registration)\n \n // Validate target consistency\n this.validateTarget(registration)\n \n // Calculate next run time\n const nextRunAt = calculateNextRunForWrite(\n registration.scheduleType,\n registration.scheduleValue,\n registration.timezone || 'UTC'\n )\n \n if (!nextRunAt) {\n throw new Error(`Failed to calculate next run time for schedule: ${registration.id}`)\n }\n \n // Check if schedule already exists\n let schedule = await em.findOne(ScheduledJob, { id: registration.id })\n const nextTenantId = registration.tenantId || null\n const nextIsEnabled = registration.isEnabled !== undefined ? registration.isEnabled : (schedule?.isEnabled ?? true)\n const tenantChanged = schedule ? (schedule.tenantId || null) !== nextTenantId : false\n if (nextIsEnabled && (schedule?.isEnabled !== true || tenantChanged)) {\n await enforceTenantActiveScheduleLimit(em, nextTenantId)\n }\n \n if (schedule) {\n // Update existing\n schedule.name = registration.name\n schedule.description = registration.description || null\n schedule.scopeType = registration.scopeType\n schedule.organizationId = registration.organizationId || null\n schedule.tenantId = registration.tenantId || null\n schedule.scheduleType = registration.scheduleType\n schedule.scheduleValue = registration.scheduleValue\n schedule.timezone = registration.timezone || 'UTC'\n schedule.targetType = registration.targetType\n schedule.targetQueue = registration.targetQueue || null\n schedule.targetCommand = registration.targetCommand || null\n schedule.targetPayload = registration.targetPayload as Record<string, unknown> || null\n schedule.requireFeature = registration.requireFeature || null\n schedule.sourceType = registration.sourceType || 'user'\n schedule.sourceModule = registration.sourceModule || null\n schedule.isEnabled = registration.isEnabled !== undefined ? registration.isEnabled : schedule.isEnabled\n schedule.nextRunAt = nextRunAt\n schedule.updatedAt = new Date()\n } else {\n // Create new\n schedule = em.create(ScheduledJob, {\n id: registration.id,\n name: registration.name,\n description: registration.description || null,\n scopeType: registration.scopeType,\n organizationId: registration.organizationId || null,\n tenantId: registration.tenantId || null,\n scheduleType: registration.scheduleType,\n scheduleValue: registration.scheduleValue,\n timezone: registration.timezone || 'UTC',\n targetType: registration.targetType,\n targetQueue: registration.targetQueue || null,\n targetCommand: registration.targetCommand || null,\n targetPayload: registration.targetPayload as Record<string, unknown> || null,\n requireFeature: registration.requireFeature || null,\n sourceType: registration.sourceType || 'module',\n sourceModule: registration.sourceModule || null,\n isEnabled: registration.isEnabled !== undefined ? registration.isEnabled : true,\n nextRunAt,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(schedule)\n }\n \n await em.flush()\n\n // Sync with BullMQ if available\n if (this.bullmqService && schedule) {\n try {\n if (schedule.isEnabled) {\n await this.bullmqService.register(schedule)\n } else {\n await this.bullmqService.unregister(schedule.id)\n }\n } catch (error: unknown) {\n logger.error('Failed to sync with BullMQ', { scheduleId: schedule.id, err: error })\n // Don't throw - DB is source of truth, BullMQ sync is best-effort\n }\n }\n }\n\n /**\n * Unregister a schedule\n */\n async unregister(scheduleId: string): Promise<void> {\n const em = this.em().fork()\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n \n if (schedule) {\n await em.remove(schedule).flush()\n \n // Unregister from BullMQ if available\n if (this.bullmqService) {\n try {\n await this.bullmqService.unregister(scheduleId)\n } catch (error: unknown) {\n logger.error('Failed to unregister from BullMQ', { scheduleId, err: error })\n }\n }\n }\n }\n\n /**\n * Check if a schedule exists\n */\n async exists(scheduleId: string): Promise<boolean> {\n const em = this.em()\n const count = await em.count(ScheduledJob, { id: scheduleId })\n return count > 0\n }\n\n /**\n * Update an existing schedule\n */\n async update(\n scheduleId: string,\n changes: Partial<Omit<ScheduleRegistration, 'id'>>\n ): Promise<void> {\n const em = this.em().fork()\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n \n if (!schedule) {\n throw new Error(`Schedule not found: ${scheduleId}`)\n }\n\n if (changes.isEnabled === true && schedule.isEnabled !== true) {\n await enforceTenantActiveScheduleLimit(em, schedule.tenantId)\n }\n\n const scheduleChanged = changes.scheduleType !== undefined || changes.scheduleValue !== undefined || changes.timezone !== undefined\n const nextRunAt = scheduleChanged\n ? calculateNextRunForWrite(\n changes.scheduleType ?? schedule.scheduleType,\n changes.scheduleValue ?? schedule.scheduleValue,\n changes.timezone ?? schedule.timezone,\n )\n : null\n if (scheduleChanged && !nextRunAt) {\n throw new Error(`Failed to calculate next run time for schedule: ${scheduleId}`)\n }\n \n // Apply changes\n if (changes.name !== undefined) schedule.name = changes.name\n if (changes.description !== undefined) schedule.description = changes.description || null\n if (changes.scheduleType !== undefined) schedule.scheduleType = changes.scheduleType\n if (changes.scheduleValue !== undefined) schedule.scheduleValue = changes.scheduleValue\n if (changes.timezone !== undefined) schedule.timezone = changes.timezone\n if (changes.targetPayload !== undefined) {\n schedule.targetPayload = changes.targetPayload as Record<string, unknown> || null\n }\n if (changes.requireFeature !== undefined) schedule.requireFeature = changes.requireFeature || null\n if (changes.isEnabled !== undefined) schedule.isEnabled = changes.isEnabled\n \n // Handle target type changes - clear stale values when switching between queue and command\n if (changes.targetType !== undefined) {\n schedule.targetType = changes.targetType\n \n if (changes.targetType === 'queue') {\n // Switching to queue: set new queue and clear command\n if (changes.targetQueue !== undefined) schedule.targetQueue = changes.targetQueue || null\n schedule.targetCommand = null\n } else if (changes.targetType === 'command') {\n // Switching to command: set new command and clear queue\n if (changes.targetCommand !== undefined) schedule.targetCommand = changes.targetCommand || null\n schedule.targetQueue = null\n }\n } else {\n // targetType not changing, but allow updating individual target fields\n if (changes.targetQueue !== undefined) schedule.targetQueue = changes.targetQueue || null\n if (changes.targetCommand !== undefined) schedule.targetCommand = changes.targetCommand || null\n }\n \n if (nextRunAt) {\n schedule.nextRunAt = nextRunAt\n }\n \n schedule.updatedAt = new Date()\n await em.flush()\n\n // Sync with BullMQ if available\n if (this.bullmqService) {\n try {\n if (schedule.isEnabled) {\n await this.bullmqService.register(schedule)\n } else {\n await this.bullmqService.unregister(scheduleId)\n }\n } catch (error: unknown) {\n logger.error('Failed to sync update with BullMQ', { scheduleId, err: error })\n }\n }\n }\n\n /**\n * Find schedules by module\n */\n async findByModule(moduleId: string, limit = 100): Promise<ScheduledJob[]> {\n const em = this.em()\n return em.find(ScheduledJob, { sourceModule: moduleId, deletedAt: null }, { limit })\n }\n\n /**\n * Enable a schedule\n */\n async enable(scheduleId: string): Promise<void> {\n await this.update(scheduleId, { isEnabled: true })\n }\n\n /**\n * Disable a schedule\n */\n async disable(scheduleId: string): Promise<void> {\n await this.update(scheduleId, { isEnabled: false })\n }\n\n private validateScope(registration: ScheduleRegistration): void {\n const { scopeType, organizationId, tenantId } = registration\n \n if (scopeType === 'system') {\n if (organizationId || tenantId) {\n throw new Error('System-scoped schedules cannot have organizationId or tenantId')\n }\n } else if (scopeType === 'organization') {\n if (!organizationId || !tenantId) {\n throw new Error('Organization-scoped schedules must have both organizationId and tenantId')\n }\n } else if (scopeType === 'tenant') {\n if (organizationId || !tenantId) {\n throw new Error('Tenant-scoped schedules must have tenantId and no organizationId')\n }\n }\n }\n\n private validateTarget(registration: ScheduleRegistration): void {\n const { targetType, targetQueue, targetCommand } = registration\n \n if (targetType === 'queue') {\n if (!targetQueue) {\n throw new Error('Queue target must have targetQueue')\n }\n } else if (targetType === 'command') {\n if (!targetCommand) {\n throw new Error('Command target must have targetCommand')\n }\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,gCAAgC;AACzC,SAAS,wCAAwC;AAEjD,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import 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 { BullMQSchedulerService } from './bullmqSchedulerService.js'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { createInvalidScheduleValueError } from '@open-mercato/shared/lib/schedule/invalidScheduleValue'\n\nconst logger = createLogger('scheduler')\n\nexport interface ScheduleRegistration {\n id: string\n name: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: unknown\n requireFeature?: string\n sourceType?: 'user' | 'module'\n sourceModule?: string\n isEnabled?: boolean\n description?: string\n}\n\nexport class SchedulerService {\n constructor(\n private em: () => EntityManager,\n private bullmqService?: BullMQSchedulerService,\n ) {}\n\n /**\n * Register a new schedule (upsert)\n */\n async register(registration: ScheduleRegistration): Promise<void> {\n const em = this.em().fork()\n \n // Validate scope consistency\n this.validateScope(registration)\n \n // Validate target consistency\n this.validateTarget(registration)\n \n // Calculate next run time\n const nextRunAt = calculateNextRunForWrite(\n registration.scheduleType,\n registration.scheduleValue,\n registration.timezone || 'UTC'\n )\n \n if (!nextRunAt) {\n throw createInvalidScheduleValueError(\n registration.scheduleType,\n registration.scheduleValue,\n `Failed to calculate next run time for schedule: ${registration.id}`,\n )\n }\n \n // Check if schedule already exists\n let schedule = await em.findOne(ScheduledJob, { id: registration.id })\n const nextTenantId = registration.tenantId || null\n const nextIsEnabled = registration.isEnabled !== undefined ? registration.isEnabled : (schedule?.isEnabled ?? true)\n const tenantChanged = schedule ? (schedule.tenantId || null) !== nextTenantId : false\n if (nextIsEnabled && (schedule?.isEnabled !== true || tenantChanged)) {\n await enforceTenantActiveScheduleLimit(em, nextTenantId)\n }\n \n if (schedule) {\n // Update existing\n schedule.name = registration.name\n schedule.description = registration.description || null\n schedule.scopeType = registration.scopeType\n schedule.organizationId = registration.organizationId || null\n schedule.tenantId = registration.tenantId || null\n schedule.scheduleType = registration.scheduleType\n schedule.scheduleValue = registration.scheduleValue\n schedule.timezone = registration.timezone || 'UTC'\n schedule.targetType = registration.targetType\n schedule.targetQueue = registration.targetQueue || null\n schedule.targetCommand = registration.targetCommand || null\n schedule.targetPayload = registration.targetPayload as Record<string, unknown> || null\n schedule.requireFeature = registration.requireFeature || null\n schedule.sourceType = registration.sourceType || 'user'\n schedule.sourceModule = registration.sourceModule || null\n schedule.isEnabled = registration.isEnabled !== undefined ? registration.isEnabled : schedule.isEnabled\n schedule.nextRunAt = nextRunAt\n schedule.updatedAt = new Date()\n } else {\n // Create new\n schedule = em.create(ScheduledJob, {\n id: registration.id,\n name: registration.name,\n description: registration.description || null,\n scopeType: registration.scopeType,\n organizationId: registration.organizationId || null,\n tenantId: registration.tenantId || null,\n scheduleType: registration.scheduleType,\n scheduleValue: registration.scheduleValue,\n timezone: registration.timezone || 'UTC',\n targetType: registration.targetType,\n targetQueue: registration.targetQueue || null,\n targetCommand: registration.targetCommand || null,\n targetPayload: registration.targetPayload as Record<string, unknown> || null,\n requireFeature: registration.requireFeature || null,\n sourceType: registration.sourceType || 'module',\n sourceModule: registration.sourceModule || null,\n isEnabled: registration.isEnabled !== undefined ? registration.isEnabled : true,\n nextRunAt,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(schedule)\n }\n \n await em.flush()\n\n // Sync with BullMQ if available\n if (this.bullmqService && schedule) {\n try {\n if (schedule.isEnabled) {\n await this.bullmqService.register(schedule)\n } else {\n await this.bullmqService.unregister(schedule.id)\n }\n } catch (error: unknown) {\n logger.error('Failed to sync with BullMQ', { scheduleId: schedule.id, err: error })\n // Don't throw - DB is source of truth, BullMQ sync is best-effort\n }\n }\n }\n\n /**\n * Unregister a schedule\n */\n async unregister(scheduleId: string): Promise<void> {\n const em = this.em().fork()\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n \n if (schedule) {\n await em.remove(schedule).flush()\n \n // Unregister from BullMQ if available\n if (this.bullmqService) {\n try {\n await this.bullmqService.unregister(scheduleId)\n } catch (error: unknown) {\n logger.error('Failed to unregister from BullMQ', { scheduleId, err: error })\n }\n }\n }\n }\n\n /**\n * Check if a schedule exists\n */\n async exists(scheduleId: string): Promise<boolean> {\n const em = this.em()\n const count = await em.count(ScheduledJob, { id: scheduleId })\n return count > 0\n }\n\n /**\n * Update an existing schedule\n */\n async update(\n scheduleId: string,\n changes: Partial<Omit<ScheduleRegistration, 'id'>>\n ): Promise<void> {\n const em = this.em().fork()\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n \n if (!schedule) {\n throw new Error(`Schedule not found: ${scheduleId}`)\n }\n\n if (changes.isEnabled === true && schedule.isEnabled !== true) {\n await enforceTenantActiveScheduleLimit(em, schedule.tenantId)\n }\n\n const scheduleChanged = changes.scheduleType !== undefined || changes.scheduleValue !== undefined || changes.timezone !== undefined\n const nextRunAt = scheduleChanged\n ? calculateNextRunForWrite(\n changes.scheduleType ?? schedule.scheduleType,\n changes.scheduleValue ?? schedule.scheduleValue,\n changes.timezone ?? schedule.timezone,\n )\n : null\n if (scheduleChanged && !nextRunAt) {\n throw createInvalidScheduleValueError(\n changes.scheduleType ?? schedule.scheduleType,\n changes.scheduleValue ?? schedule.scheduleValue,\n `Failed to calculate next run time for schedule: ${scheduleId}`,\n )\n }\n \n // Apply changes\n if (changes.name !== undefined) schedule.name = changes.name\n if (changes.description !== undefined) schedule.description = changes.description || null\n if (changes.scheduleType !== undefined) schedule.scheduleType = changes.scheduleType\n if (changes.scheduleValue !== undefined) schedule.scheduleValue = changes.scheduleValue\n if (changes.timezone !== undefined) schedule.timezone = changes.timezone\n if (changes.targetPayload !== undefined) {\n schedule.targetPayload = changes.targetPayload as Record<string, unknown> || null\n }\n if (changes.requireFeature !== undefined) schedule.requireFeature = changes.requireFeature || null\n if (changes.isEnabled !== undefined) schedule.isEnabled = changes.isEnabled\n \n // Handle target type changes - clear stale values when switching between queue and command\n if (changes.targetType !== undefined) {\n schedule.targetType = changes.targetType\n \n if (changes.targetType === 'queue') {\n // Switching to queue: set new queue and clear command\n if (changes.targetQueue !== undefined) schedule.targetQueue = changes.targetQueue || null\n schedule.targetCommand = null\n } else if (changes.targetType === 'command') {\n // Switching to command: set new command and clear queue\n if (changes.targetCommand !== undefined) schedule.targetCommand = changes.targetCommand || null\n schedule.targetQueue = null\n }\n } else {\n // targetType not changing, but allow updating individual target fields\n if (changes.targetQueue !== undefined) schedule.targetQueue = changes.targetQueue || null\n if (changes.targetCommand !== undefined) schedule.targetCommand = changes.targetCommand || null\n }\n \n if (nextRunAt) {\n schedule.nextRunAt = nextRunAt\n }\n \n schedule.updatedAt = new Date()\n await em.flush()\n\n // Sync with BullMQ if available\n if (this.bullmqService) {\n try {\n if (schedule.isEnabled) {\n await this.bullmqService.register(schedule)\n } else {\n await this.bullmqService.unregister(scheduleId)\n }\n } catch (error: unknown) {\n logger.error('Failed to sync update with BullMQ', { scheduleId, err: error })\n }\n }\n }\n\n /**\n * Find schedules by module\n */\n async findByModule(moduleId: string, limit = 100): Promise<ScheduledJob[]> {\n const em = this.em()\n return em.find(ScheduledJob, { sourceModule: moduleId, deletedAt: null }, { limit })\n }\n\n /**\n * Enable a schedule\n */\n async enable(scheduleId: string): Promise<void> {\n await this.update(scheduleId, { isEnabled: true })\n }\n\n /**\n * Disable a schedule\n */\n async disable(scheduleId: string): Promise<void> {\n await this.update(scheduleId, { isEnabled: false })\n }\n\n private validateScope(registration: ScheduleRegistration): void {\n const { scopeType, organizationId, tenantId } = registration\n \n if (scopeType === 'system') {\n if (organizationId || tenantId) {\n throw new Error('System-scoped schedules cannot have organizationId or tenantId')\n }\n } else if (scopeType === 'organization') {\n if (!organizationId || !tenantId) {\n throw new Error('Organization-scoped schedules must have both organizationId and tenantId')\n }\n } else if (scopeType === 'tenant') {\n if (organizationId || !tenantId) {\n throw new Error('Tenant-scoped schedules must have tenantId and no organizationId')\n }\n }\n }\n\n private validateTarget(registration: ScheduleRegistration): void {\n const { targetType, targetQueue, targetCommand } = registration\n \n if (targetType === 'queue') {\n if (!targetQueue) {\n throw new Error('Queue target must have targetQueue')\n }\n } else if (targetType === 'command') {\n if (!targetCommand) {\n throw new Error('Command target must have targetCommand')\n }\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/scheduler",
|
|
3
|
-
"version": "0.8.1-develop.
|
|
3
|
+
"version": "0.8.1-develop.7294.1.0ef99fc518",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Database-managed scheduled jobs with admin UI",
|
|
6
6
|
"type": "module",
|
|
@@ -59,13 +59,13 @@
|
|
|
59
59
|
"./*/*/*/*/*/*.json": "./src/*/*/*/*/*/*.json"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@open-mercato/events": "0.8.1-develop.
|
|
63
|
-
"@open-mercato/queue": "0.8.1-develop.
|
|
62
|
+
"@open-mercato/events": "0.8.1-develop.7294.1.0ef99fc518",
|
|
63
|
+
"@open-mercato/queue": "0.8.1-develop.7294.1.0ef99fc518",
|
|
64
64
|
"cron-parser": "^5.6.2"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@mikro-orm/core": "^7.0.14",
|
|
68
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
68
|
+
"@open-mercato/shared": "0.8.1-develop.7294.1.0ef99fc518",
|
|
69
69
|
"bullmq": "^5.0.0 || ^6.0.0",
|
|
70
70
|
"ioredis": "^5.0.0 || ^6.0.0",
|
|
71
71
|
"zod": ">=3.23.0"
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
82
|
+
"@open-mercato/shared": "0.8.1-develop.7294.1.0ef99fc518",
|
|
83
83
|
"@types/jest": "^30.0.0",
|
|
84
84
|
"@types/node": "^26.4.0",
|
|
85
85
|
"jest": "^30.4.2",
|
|
@@ -1,27 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MIN_SCHEDULE_INTERVAL_MS,
|
|
3
|
+
isValidScheduleInterval,
|
|
4
|
+
matchScheduleInterval,
|
|
5
|
+
parseScheduleInterval,
|
|
6
|
+
} from '@open-mercato/shared/lib/schedule/interval'
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Parse simple interval strings like '15m', '2h', '1d' into milliseconds
|
|
3
10
|
*/
|
|
4
|
-
export
|
|
11
|
+
export { MIN_SCHEDULE_INTERVAL_MS }
|
|
5
12
|
|
|
6
13
|
export function parseInterval(interval: string): number {
|
|
7
|
-
|
|
8
|
-
const match = interval.match(regex)
|
|
9
|
-
|
|
10
|
-
if (!match) {
|
|
11
|
-
throw new Error(`Invalid interval format: ${interval}. Expected format: <number><unit> (e.g., 15m, 2h, 1d)`)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const value = parseInt(match[1], 10)
|
|
15
|
-
const unit = match[2]
|
|
16
|
-
|
|
17
|
-
const multipliers: Record<string, number> = {
|
|
18
|
-
s: 1000, // seconds
|
|
19
|
-
m: 60 * 1000, // minutes
|
|
20
|
-
h: 60 * 60 * 1000, // hours
|
|
21
|
-
d: 24 * 60 * 60 * 1000, // days
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return value * multipliers[unit]
|
|
14
|
+
return parseScheduleInterval(interval)
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
export function resolveScheduleIntervalMs(interval: string): number {
|
|
@@ -43,11 +33,7 @@ export function calculateNextRunFromInterval(
|
|
|
43
33
|
* Validate an interval string
|
|
44
34
|
*/
|
|
45
35
|
export function validateInterval(interval: string): boolean {
|
|
46
|
-
|
|
47
|
-
return parseInterval(interval) >= MIN_SCHEDULE_INTERVAL_MS
|
|
48
|
-
} catch {
|
|
49
|
-
return false
|
|
50
|
-
}
|
|
36
|
+
return isValidScheduleInterval(interval)
|
|
51
37
|
}
|
|
52
38
|
|
|
53
39
|
/**
|
|
@@ -57,15 +43,14 @@ export function validateInterval(interval: string): boolean {
|
|
|
57
43
|
*/
|
|
58
44
|
export function intervalToHuman(interval: string): string {
|
|
59
45
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (!match) {
|
|
46
|
+
const parts = matchScheduleInterval(interval)
|
|
47
|
+
|
|
48
|
+
if (!parts) {
|
|
64
49
|
return interval
|
|
65
50
|
}
|
|
66
|
-
|
|
67
|
-
const unit =
|
|
68
|
-
|
|
51
|
+
|
|
52
|
+
const unit = parts.unit
|
|
53
|
+
|
|
69
54
|
const ms = parseInterval(interval)
|
|
70
55
|
const seconds = ms / 1000
|
|
71
56
|
|
|
@@ -4,6 +4,7 @@ import { calculateNextRunForWrite } from '../lib/nextRunCalculator.js'
|
|
|
4
4
|
import { enforceTenantActiveScheduleLimit } from '../lib/activeScheduleLimits.js'
|
|
5
5
|
import type { BullMQSchedulerService } from './bullmqSchedulerService.js'
|
|
6
6
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
7
|
+
import { createInvalidScheduleValueError } from '@open-mercato/shared/lib/schedule/invalidScheduleValue'
|
|
7
8
|
|
|
8
9
|
const logger = createLogger('scheduler')
|
|
9
10
|
|
|
@@ -53,7 +54,11 @@ export class SchedulerService {
|
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
if (!nextRunAt) {
|
|
56
|
-
throw
|
|
57
|
+
throw createInvalidScheduleValueError(
|
|
58
|
+
registration.scheduleType,
|
|
59
|
+
registration.scheduleValue,
|
|
60
|
+
`Failed to calculate next run time for schedule: ${registration.id}`,
|
|
61
|
+
)
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
// Check if schedule already exists
|
|
@@ -186,7 +191,11 @@ export class SchedulerService {
|
|
|
186
191
|
)
|
|
187
192
|
: null
|
|
188
193
|
if (scheduleChanged && !nextRunAt) {
|
|
189
|
-
throw
|
|
194
|
+
throw createInvalidScheduleValueError(
|
|
195
|
+
changes.scheduleType ?? schedule.scheduleType,
|
|
196
|
+
changes.scheduleValue ?? schedule.scheduleValue,
|
|
197
|
+
`Failed to calculate next run time for schedule: ${scheduleId}`,
|
|
198
|
+
)
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
// Apply changes
|