@openmdm/core 0.2.0 → 0.3.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/dist/index.d.ts +105 -3
- package/dist/index.js +1553 -41
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +259 -0
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +591 -1
- package/dist/types.js +21 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/audit.ts +317 -0
- package/src/authorization.ts +418 -0
- package/src/dashboard.ts +327 -0
- package/src/index.ts +222 -0
- package/src/plugin-storage.ts +128 -0
- package/src/queue.ts +161 -0
- package/src/schedule.ts +325 -0
- package/src/schema.ts +277 -0
- package/src/tenant.ts +237 -0
- package/src/types.ts +708 -0
package/src/queue.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenMDM Message Queue Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides persistent message queue management for the MDM system.
|
|
5
|
+
* Ensures reliable message delivery with retry and expiration handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
MessageQueueManager,
|
|
10
|
+
QueuedMessage,
|
|
11
|
+
EnqueueMessageInput,
|
|
12
|
+
QueueStats,
|
|
13
|
+
DatabaseAdapter,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default maximum attempts for message delivery
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default TTL in seconds (24 hours)
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_TTL_SECONDS = 86400;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a MessageQueueManager instance
|
|
28
|
+
*/
|
|
29
|
+
export function createMessageQueueManager(db: DatabaseAdapter): MessageQueueManager {
|
|
30
|
+
return {
|
|
31
|
+
async enqueue(message: EnqueueMessageInput): Promise<QueuedMessage> {
|
|
32
|
+
if (!db.enqueueMessage) {
|
|
33
|
+
throw new Error('Database adapter does not support message queue');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Set defaults
|
|
37
|
+
const enrichedMessage: EnqueueMessageInput = {
|
|
38
|
+
...message,
|
|
39
|
+
priority: message.priority ?? 'normal',
|
|
40
|
+
maxAttempts: message.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
41
|
+
ttlSeconds: message.ttlSeconds ?? DEFAULT_TTL_SECONDS,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return db.enqueueMessage(enrichedMessage);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async enqueueBatch(messages: EnqueueMessageInput[]): Promise<QueuedMessage[]> {
|
|
48
|
+
if (!db.enqueueMessage) {
|
|
49
|
+
throw new Error('Database adapter does not support message queue');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results: QueuedMessage[] = [];
|
|
53
|
+
|
|
54
|
+
for (const message of messages) {
|
|
55
|
+
const enrichedMessage: EnqueueMessageInput = {
|
|
56
|
+
...message,
|
|
57
|
+
priority: message.priority ?? 'normal',
|
|
58
|
+
maxAttempts: message.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
59
|
+
ttlSeconds: message.ttlSeconds ?? DEFAULT_TTL_SECONDS,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const queued = await db.enqueueMessage(enrichedMessage);
|
|
63
|
+
results.push(queued);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return results;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async dequeue(deviceId: string, limit: number = 10): Promise<QueuedMessage[]> {
|
|
70
|
+
if (!db.dequeueMessages) {
|
|
71
|
+
throw new Error('Database adapter does not support message queue');
|
|
72
|
+
}
|
|
73
|
+
return db.dequeueMessages(deviceId, limit);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async acknowledge(messageId: string): Promise<void> {
|
|
77
|
+
if (!db.acknowledgeMessage) {
|
|
78
|
+
throw new Error('Database adapter does not support message queue');
|
|
79
|
+
}
|
|
80
|
+
await db.acknowledgeMessage(messageId);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async fail(messageId: string, error: string): Promise<void> {
|
|
84
|
+
if (!db.failMessage) {
|
|
85
|
+
throw new Error('Database adapter does not support message queue');
|
|
86
|
+
}
|
|
87
|
+
await db.failMessage(messageId, error);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async retryFailed(maxAttempts: number = DEFAULT_MAX_ATTEMPTS): Promise<number> {
|
|
91
|
+
if (!db.retryFailedMessages) {
|
|
92
|
+
throw new Error('Database adapter does not support message queue');
|
|
93
|
+
}
|
|
94
|
+
return db.retryFailedMessages(maxAttempts);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async purgeExpired(): Promise<number> {
|
|
98
|
+
if (!db.purgeExpiredMessages) {
|
|
99
|
+
throw new Error('Database adapter does not support message queue');
|
|
100
|
+
}
|
|
101
|
+
return db.purgeExpiredMessages();
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async getStats(tenantId?: string): Promise<QueueStats> {
|
|
105
|
+
if (!db.getQueueStats) {
|
|
106
|
+
throw new Error('Database adapter does not support message queue');
|
|
107
|
+
}
|
|
108
|
+
return db.getQueueStats(tenantId);
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async peek(deviceId: string, limit: number = 10): Promise<QueuedMessage[]> {
|
|
112
|
+
if (!db.peekMessages) {
|
|
113
|
+
throw new Error('Database adapter does not support message queue');
|
|
114
|
+
}
|
|
115
|
+
return db.peekMessages(deviceId, limit);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Message priority weights for sorting
|
|
122
|
+
*/
|
|
123
|
+
export const PRIORITY_WEIGHTS = {
|
|
124
|
+
high: 3,
|
|
125
|
+
normal: 2,
|
|
126
|
+
low: 1,
|
|
127
|
+
} as const;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compare messages by priority (higher priority first)
|
|
131
|
+
*/
|
|
132
|
+
export function compareByPriority(a: QueuedMessage, b: QueuedMessage): number {
|
|
133
|
+
return PRIORITY_WEIGHTS[b.priority] - PRIORITY_WEIGHTS[a.priority];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if a message has expired
|
|
138
|
+
*/
|
|
139
|
+
export function isMessageExpired(message: QueuedMessage): boolean {
|
|
140
|
+
if (!message.expiresAt) return false;
|
|
141
|
+
return new Date(message.expiresAt) < new Date();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if a message can be retried
|
|
146
|
+
*/
|
|
147
|
+
export function canRetryMessage(message: QueuedMessage): boolean {
|
|
148
|
+
return message.status === 'failed' && message.attempts < message.maxAttempts;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Calculate exponential backoff delay for retries
|
|
153
|
+
*/
|
|
154
|
+
export function calculateBackoffDelay(
|
|
155
|
+
attempts: number,
|
|
156
|
+
baseDelayMs: number = 1000,
|
|
157
|
+
maxDelayMs: number = 300000 // 5 minutes
|
|
158
|
+
): number {
|
|
159
|
+
const delay = baseDelayMs * Math.pow(2, attempts - 1);
|
|
160
|
+
return Math.min(delay, maxDelayMs);
|
|
161
|
+
}
|
package/src/schedule.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenMDM Schedule Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides scheduled task management for the MDM system.
|
|
5
|
+
* Enables scheduling of recurring operations, maintenance windows, and one-time tasks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ScheduleManager,
|
|
10
|
+
ScheduledTask,
|
|
11
|
+
ScheduledTaskFilter,
|
|
12
|
+
ScheduledTaskListResult,
|
|
13
|
+
CreateScheduledTaskInput,
|
|
14
|
+
UpdateScheduledTaskInput,
|
|
15
|
+
TaskSchedule,
|
|
16
|
+
TaskExecution,
|
|
17
|
+
DatabaseAdapter,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse cron expression and calculate next run time
|
|
22
|
+
* Supports: minute hour dayOfMonth month dayOfWeek
|
|
23
|
+
*/
|
|
24
|
+
function parseCronNextRun(cron: string, from: Date = new Date()): Date | null {
|
|
25
|
+
try {
|
|
26
|
+
const parts = cron.trim().split(/\s+/);
|
|
27
|
+
if (parts.length !== 5) return null;
|
|
28
|
+
|
|
29
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
30
|
+
|
|
31
|
+
// Simple cron parsing - handles basic patterns
|
|
32
|
+
const now = new Date(from);
|
|
33
|
+
const next = new Date(now);
|
|
34
|
+
|
|
35
|
+
// Start from the next minute
|
|
36
|
+
next.setSeconds(0);
|
|
37
|
+
next.setMilliseconds(0);
|
|
38
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
39
|
+
|
|
40
|
+
// Try to find next valid time (up to 1 year)
|
|
41
|
+
const maxIterations = 365 * 24 * 60; // 1 year in minutes
|
|
42
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
43
|
+
const matches =
|
|
44
|
+
matchesCronField(minute, next.getMinutes()) &&
|
|
45
|
+
matchesCronField(hour, next.getHours()) &&
|
|
46
|
+
matchesCronField(dayOfMonth, next.getDate()) &&
|
|
47
|
+
matchesCronField(month, next.getMonth() + 1) &&
|
|
48
|
+
matchesCronField(dayOfWeek, next.getDay());
|
|
49
|
+
|
|
50
|
+
if (matches) {
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if a value matches a cron field pattern
|
|
65
|
+
*/
|
|
66
|
+
function matchesCronField(pattern: string, value: number): boolean {
|
|
67
|
+
if (pattern === '*') return true;
|
|
68
|
+
|
|
69
|
+
// Handle step values: */5, */15, etc.
|
|
70
|
+
if (pattern.startsWith('*/')) {
|
|
71
|
+
const step = parseInt(pattern.slice(2), 10);
|
|
72
|
+
return value % step === 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle ranges: 1-5
|
|
76
|
+
if (pattern.includes('-')) {
|
|
77
|
+
const [start, end] = pattern.split('-').map((n) => parseInt(n, 10));
|
|
78
|
+
return value >= start && value <= end;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle lists: 1,3,5
|
|
82
|
+
if (pattern.includes(',')) {
|
|
83
|
+
const values = pattern.split(',').map((n) => parseInt(n, 10));
|
|
84
|
+
return values.includes(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Simple number
|
|
88
|
+
return parseInt(pattern, 10) === value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if current time is within a maintenance window
|
|
93
|
+
*/
|
|
94
|
+
function isInMaintenanceWindow(
|
|
95
|
+
window: TaskSchedule['window'],
|
|
96
|
+
now: Date = new Date()
|
|
97
|
+
): boolean {
|
|
98
|
+
if (!window) return false;
|
|
99
|
+
|
|
100
|
+
const dayOfWeek = now.getDay();
|
|
101
|
+
if (!window.daysOfWeek.includes(dayOfWeek)) return false;
|
|
102
|
+
|
|
103
|
+
// Parse times
|
|
104
|
+
const [startHour, startMin] = window.startTime.split(':').map(Number);
|
|
105
|
+
const [endHour, endMin] = window.endTime.split(':').map(Number);
|
|
106
|
+
|
|
107
|
+
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
108
|
+
const startMinutes = startHour * 60 + startMin;
|
|
109
|
+
const endMinutes = endHour * 60 + endMin;
|
|
110
|
+
|
|
111
|
+
// Handle overnight windows
|
|
112
|
+
if (endMinutes < startMinutes) {
|
|
113
|
+
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Calculate next run time for a maintenance window
|
|
121
|
+
*/
|
|
122
|
+
function calculateNextWindowRun(
|
|
123
|
+
window: TaskSchedule['window'],
|
|
124
|
+
from: Date = new Date()
|
|
125
|
+
): Date | null {
|
|
126
|
+
if (!window) return null;
|
|
127
|
+
|
|
128
|
+
const [startHour, startMin] = window.startTime.split(':').map(Number);
|
|
129
|
+
|
|
130
|
+
// Try each day for the next 7 days
|
|
131
|
+
for (let dayOffset = 0; dayOffset <= 7; dayOffset++) {
|
|
132
|
+
const candidate = new Date(from);
|
|
133
|
+
candidate.setDate(candidate.getDate() + dayOffset);
|
|
134
|
+
candidate.setHours(startHour, startMin, 0, 0);
|
|
135
|
+
|
|
136
|
+
// Skip if in the past
|
|
137
|
+
if (candidate <= from) continue;
|
|
138
|
+
|
|
139
|
+
// Check if day matches
|
|
140
|
+
if (window.daysOfWeek.includes(candidate.getDay())) {
|
|
141
|
+
return candidate;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a ScheduleManager instance
|
|
150
|
+
*/
|
|
151
|
+
export function createScheduleManager(db: DatabaseAdapter): ScheduleManager {
|
|
152
|
+
/**
|
|
153
|
+
* Calculate the next run time for a schedule
|
|
154
|
+
*/
|
|
155
|
+
function calculateNextRun(schedule: TaskSchedule): Date | null {
|
|
156
|
+
const now = new Date();
|
|
157
|
+
|
|
158
|
+
switch (schedule.type) {
|
|
159
|
+
case 'once':
|
|
160
|
+
// For one-time tasks, return the scheduled time if it's in the future
|
|
161
|
+
if (schedule.executeAt && new Date(schedule.executeAt) > now) {
|
|
162
|
+
return new Date(schedule.executeAt);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
|
|
166
|
+
case 'recurring':
|
|
167
|
+
// Parse cron expression
|
|
168
|
+
if (schedule.cron) {
|
|
169
|
+
return parseCronNextRun(schedule.cron, now);
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
|
|
173
|
+
case 'window':
|
|
174
|
+
// Calculate next maintenance window start
|
|
175
|
+
if (schedule.window) {
|
|
176
|
+
return calculateNextWindowRun(schedule.window, now);
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
|
|
180
|
+
default:
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
async get(id: string): Promise<ScheduledTask | null> {
|
|
187
|
+
if (!db.findScheduledTask) {
|
|
188
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
189
|
+
}
|
|
190
|
+
return db.findScheduledTask(id);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async list(filter?: ScheduledTaskFilter): Promise<ScheduledTaskListResult> {
|
|
194
|
+
if (!db.listScheduledTasks) {
|
|
195
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
196
|
+
}
|
|
197
|
+
return db.listScheduledTasks(filter);
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
async create(data: CreateScheduledTaskInput): Promise<ScheduledTask> {
|
|
201
|
+
if (!db.createScheduledTask) {
|
|
202
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Calculate initial next run time
|
|
206
|
+
const nextRunAt = calculateNextRun(data.schedule);
|
|
207
|
+
|
|
208
|
+
// Create task with calculated next run
|
|
209
|
+
const task = await db.createScheduledTask({
|
|
210
|
+
...data,
|
|
211
|
+
// Note: nextRunAt is set by the database adapter based on schedule
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Update next run time if needed
|
|
215
|
+
if (nextRunAt && db.updateScheduledTask) {
|
|
216
|
+
return db.updateScheduledTask(task.id, {
|
|
217
|
+
...data,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return task;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
async update(id: string, data: UpdateScheduledTaskInput): Promise<ScheduledTask> {
|
|
225
|
+
if (!db.updateScheduledTask || !db.findScheduledTask) {
|
|
226
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const existing = await db.findScheduledTask(id);
|
|
230
|
+
if (!existing) {
|
|
231
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return db.updateScheduledTask(id, data);
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async delete(id: string): Promise<void> {
|
|
238
|
+
if (!db.deleteScheduledTask) {
|
|
239
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
240
|
+
}
|
|
241
|
+
await db.deleteScheduledTask(id);
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
async pause(id: string): Promise<ScheduledTask> {
|
|
245
|
+
if (!db.updateScheduledTask || !db.findScheduledTask) {
|
|
246
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const task = await db.findScheduledTask(id);
|
|
250
|
+
if (!task) {
|
|
251
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (task.status === 'paused') {
|
|
255
|
+
return task;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return db.updateScheduledTask(id, { status: 'paused' });
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async resume(id: string): Promise<ScheduledTask> {
|
|
262
|
+
if (!db.updateScheduledTask || !db.findScheduledTask) {
|
|
263
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const task = await db.findScheduledTask(id);
|
|
267
|
+
if (!task) {
|
|
268
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (task.status !== 'paused') {
|
|
272
|
+
return task;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Recalculate next run time
|
|
276
|
+
const nextRunAt = calculateNextRun(task.schedule);
|
|
277
|
+
|
|
278
|
+
return db.updateScheduledTask(id, { status: 'active' });
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async runNow(id: string): Promise<TaskExecution> {
|
|
282
|
+
if (
|
|
283
|
+
!db.findScheduledTask ||
|
|
284
|
+
!db.createTaskExecution ||
|
|
285
|
+
!db.updateScheduledTask
|
|
286
|
+
) {
|
|
287
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const task = await db.findScheduledTask(id);
|
|
291
|
+
if (!task) {
|
|
292
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Create execution record
|
|
296
|
+
const execution = await db.createTaskExecution({ taskId: id });
|
|
297
|
+
|
|
298
|
+
// Update task last run time
|
|
299
|
+
await db.updateScheduledTask(id, {});
|
|
300
|
+
|
|
301
|
+
return execution;
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
async getUpcoming(hours: number): Promise<ScheduledTask[]> {
|
|
305
|
+
if (!db.getUpcomingTasks) {
|
|
306
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
307
|
+
}
|
|
308
|
+
return db.getUpcomingTasks(hours);
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
async getExecutions(taskId: string, limit: number = 10): Promise<TaskExecution[]> {
|
|
312
|
+
if (!db.listTaskExecutions) {
|
|
313
|
+
throw new Error('Database adapter does not support task scheduling');
|
|
314
|
+
}
|
|
315
|
+
return db.listTaskExecutions(taskId, limit);
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
calculateNextRun,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Export utility functions
|
|
324
|
+
*/
|
|
325
|
+
export { parseCronNextRun, isInMaintenanceWindow, calculateNextWindowRun };
|