webspresso 0.0.99 → 0.0.100
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/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +246 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +90 -0
- package/.agents/PLUGINS.md +44 -7
- package/.agents/QUEUE.md +18 -10
- package/.agents/ROUTING.md +33 -0
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +1 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/index.js +63 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +343 -3
- package/core/auth/middleware.js +30 -5
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +4 -1
- package/core/auth/user-agent.js +161 -0
- package/core/orm/index.js +2 -1
- package/core/orm/model.js +51 -1
- package/core/orm/repository.js +283 -13
- package/core/orm/scopes.js +3 -2
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +252 -39
- package/core/queue/manager.js +20 -2
- package/core/queue/scheduler.js +115 -59
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +274 -14
- package/index.js +4 -0
- package/package.json +7 -5
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/audit-log/admin-component.js +54 -54
- package/plugins/audit-log/index.js +2 -2
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/cors.js +13 -2
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/file-manager/admin-component.js +49 -49
- package/plugins/file-manager/index.js +4 -4
- package/plugins/index.js +2 -0
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/bundle/index.js +2 -1
- package/src/file-router.js +173 -4
- package/src/helpers.js +7 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +112 -0
package/core/queue/cron.js
CHANGED
|
@@ -20,19 +20,24 @@ const CRON_ALIASES = {
|
|
|
20
20
|
'@annually': '0 0 1 1 *',
|
|
21
21
|
'@monthly': '0 0 1 * *',
|
|
22
22
|
'@weekly': '0 0 * * 0',
|
|
23
|
+
'@weekdays': '0 0 * * 1-5',
|
|
24
|
+
'@workdays': '0 0 * * 1-5',
|
|
25
|
+
'@weekends': '0 0 * * 6,0',
|
|
23
26
|
'@daily': '0 0 * * *',
|
|
24
27
|
'@midnight': '0 0 * * *',
|
|
25
28
|
'@hourly': '0 * * * *',
|
|
29
|
+
'@every_hour': '0 * * * *',
|
|
26
30
|
'@every_minute': '* * * * *',
|
|
27
31
|
'@minutely': '* * * * *',
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
|
-
* Resolves alias string (e.g. '@daily', '@every 5m') into a 5-part cron expression.
|
|
35
|
+
* Resolves alias string (e.g. '@daily', '@every 5m', '@every 2h', '@every 1d') into a 5-part cron expression.
|
|
32
36
|
* @param {string} str
|
|
33
37
|
* @returns {string}
|
|
34
38
|
*/
|
|
35
39
|
function resolveCronAlias(str) {
|
|
40
|
+
if (typeof str !== 'string') return str;
|
|
36
41
|
const trimmed = str.trim();
|
|
37
42
|
const lower = trimmed.toLowerCase();
|
|
38
43
|
|
|
@@ -40,8 +45,8 @@ function resolveCronAlias(str) {
|
|
|
40
45
|
return CRON_ALIASES[lower];
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
// Support '@every 5m', '@every 15m', '@every 2h'
|
|
44
|
-
const everyMatch = lower.match(/^@every\s+(\d+)\s*([
|
|
48
|
+
// Support '@every 5m', '@every 15m', '@every 2h', '@every 1d'
|
|
49
|
+
const everyMatch = lower.match(/^@every\s+(\d+)\s*([mhd])$/);
|
|
45
50
|
if (everyMatch) {
|
|
46
51
|
const amount = parseInt(everyMatch[1], 10);
|
|
47
52
|
const unit = everyMatch[2];
|
|
@@ -51,6 +56,9 @@ function resolveCronAlias(str) {
|
|
|
51
56
|
if (unit === 'h' && amount >= 1 && amount <= 23) {
|
|
52
57
|
return `0 */${amount} * * *`;
|
|
53
58
|
}
|
|
59
|
+
if (unit === 'd' && amount >= 1 && amount <= 30) {
|
|
60
|
+
return `0 0 */${amount} * *`;
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
return trimmed;
|
|
@@ -62,11 +70,23 @@ function resolveCronAlias(str) {
|
|
|
62
70
|
* @param {number} min
|
|
63
71
|
* @param {number} max
|
|
64
72
|
* @param {Record<string, number>} [nameMap]
|
|
73
|
+
* @param {boolean} [isDow=false]
|
|
65
74
|
* @returns {Set<number>}
|
|
66
75
|
*/
|
|
67
|
-
function parseField(field, min, max, nameMap = null) {
|
|
76
|
+
function parseField(field, min, max, nameMap = null, isDow = false) {
|
|
77
|
+
if (typeof field !== 'string' || !field.trim()) {
|
|
78
|
+
throw new Error(`Invalid empty cron segment in '${field}'`);
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
let str = field.trim().toLowerCase();
|
|
69
82
|
|
|
83
|
+
if (isDow) {
|
|
84
|
+
// When Sunday is at the end of a range (e.g. MON-SUN, FRI-SUN, 1-SUN), map SUN to 7
|
|
85
|
+
str = str.replace(/-\s*sun\b/g, '-7');
|
|
86
|
+
// When Sunday is at the start of a range (e.g. SUN-WED), map SUN to 0
|
|
87
|
+
str = str.replace(/\bsun\s*-/g, '0-');
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
if (nameMap) {
|
|
71
91
|
for (const [name, val] of Object.entries(nameMap)) {
|
|
72
92
|
str = str.replace(new RegExp(`\\b${name}\\b`, 'g'), String(val));
|
|
@@ -76,30 +96,37 @@ function parseField(field, min, max, nameMap = null) {
|
|
|
76
96
|
const values = new Set();
|
|
77
97
|
const parts = str.split(',');
|
|
78
98
|
|
|
79
|
-
for (
|
|
99
|
+
for (let rawPart of parts) {
|
|
100
|
+
const part = rawPart.trim();
|
|
80
101
|
if (!part) {
|
|
81
102
|
throw new Error(`Invalid empty cron segment in '${field}'`);
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
let step = 1;
|
|
85
106
|
let rangeStr = part;
|
|
107
|
+
let hasStep = false;
|
|
86
108
|
|
|
87
109
|
if (part.includes('/')) {
|
|
88
110
|
const sub = part.split('/');
|
|
89
111
|
if (sub.length !== 2) {
|
|
90
112
|
throw new Error(`Invalid step syntax in '${part}'`);
|
|
91
113
|
}
|
|
92
|
-
rangeStr = sub[0];
|
|
93
|
-
|
|
114
|
+
rangeStr = sub[0].trim();
|
|
115
|
+
const stepStr = sub[1].trim();
|
|
116
|
+
if (!/^\d+$/.test(stepStr)) {
|
|
117
|
+
throw new Error(`Invalid step value '${sub[1]}' in '${part}'`);
|
|
118
|
+
}
|
|
119
|
+
step = parseInt(stepStr, 10);
|
|
94
120
|
if (isNaN(step) || step <= 0) {
|
|
95
121
|
throw new Error(`Invalid step value '${sub[1]}' in '${part}'`);
|
|
96
122
|
}
|
|
123
|
+
hasStep = true;
|
|
97
124
|
}
|
|
98
125
|
|
|
99
126
|
let start;
|
|
100
127
|
let end;
|
|
101
128
|
|
|
102
|
-
if (rangeStr === '*' || rangeStr === '') {
|
|
129
|
+
if (rangeStr === '*' || rangeStr === '?' || rangeStr === '') {
|
|
103
130
|
start = min;
|
|
104
131
|
end = max;
|
|
105
132
|
} else if (rangeStr.includes('-')) {
|
|
@@ -107,42 +134,121 @@ function parseField(field, min, max, nameMap = null) {
|
|
|
107
134
|
if (sub.length !== 2) {
|
|
108
135
|
throw new Error(`Invalid range syntax in '${rangeStr}'`);
|
|
109
136
|
}
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
const startStr = sub[0].trim();
|
|
138
|
+
const endStr = sub[1].trim();
|
|
139
|
+
if (!/^\d+$/.test(startStr) || !/^\d+$/.test(endStr)) {
|
|
140
|
+
throw new Error(`Invalid range boundary '${rangeStr}' in '${part}'`);
|
|
141
|
+
}
|
|
142
|
+
start = parseInt(startStr, 10);
|
|
143
|
+
end = parseInt(endStr, 10);
|
|
112
144
|
if (isNaN(start) || isNaN(end) || start > end) {
|
|
113
145
|
throw new Error(`Invalid range boundary '${rangeStr}' in '${part}'`);
|
|
114
146
|
}
|
|
115
147
|
} else {
|
|
148
|
+
if (!/^\d+$/.test(rangeStr)) {
|
|
149
|
+
throw new Error(`Invalid numeric value '${rangeStr}' in '${part}'`);
|
|
150
|
+
}
|
|
116
151
|
start = parseInt(rangeStr, 10);
|
|
117
|
-
|
|
152
|
+
// In standard crontab, N/step means start at N through max (e.g. 10/15 -> 10,25,40,55)
|
|
153
|
+
end = hasStep ? max : start;
|
|
118
154
|
if (isNaN(start)) {
|
|
119
155
|
throw new Error(`Invalid numeric value '${rangeStr}' in '${part}'`);
|
|
120
156
|
}
|
|
121
157
|
}
|
|
122
158
|
|
|
123
|
-
// Day of week special case: 7 is Sunday (same as 0)
|
|
124
|
-
if (max === 6 && (start === 7 || end === 7)) {
|
|
125
|
-
if (start === 7) start = 0;
|
|
126
|
-
if (end === 7) end = 0;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
159
|
if (start < min || end > max) {
|
|
130
160
|
throw new Error(`Cron value out of range [${min}-${max}]: '${part}'`);
|
|
131
161
|
}
|
|
132
162
|
|
|
133
163
|
for (let i = start; i <= end; i += step) {
|
|
134
|
-
|
|
164
|
+
// In standard cron, day of week 7 is Sunday, same as 0
|
|
165
|
+
values.add(isDow && i === 7 ? 0 : i);
|
|
135
166
|
}
|
|
136
167
|
}
|
|
137
168
|
|
|
138
169
|
return values;
|
|
139
170
|
}
|
|
140
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Helper to extract date components according to UTC, Timezone, or Local time
|
|
174
|
+
* @param {Date} date
|
|
175
|
+
* @param {boolean} utc
|
|
176
|
+
* @param {string|null} timezone
|
|
177
|
+
* @returns {{ year: number, month: number, dom: number, hour: number, minute: number, second: number, dow: number }}
|
|
178
|
+
*/
|
|
179
|
+
function getDateParts(date, utc, timezone) {
|
|
180
|
+
if (timezone && timezone.toUpperCase() !== 'UTC') {
|
|
181
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
182
|
+
timeZone: timezone,
|
|
183
|
+
year: 'numeric',
|
|
184
|
+
month: 'numeric',
|
|
185
|
+
day: 'numeric',
|
|
186
|
+
hour: 'numeric',
|
|
187
|
+
minute: 'numeric',
|
|
188
|
+
second: 'numeric',
|
|
189
|
+
weekday: 'short',
|
|
190
|
+
hourCycle: 'h23',
|
|
191
|
+
});
|
|
192
|
+
const parts = dtf.formatToParts(date);
|
|
193
|
+
const map = {};
|
|
194
|
+
for (const p of parts) map[p.type] = p.value;
|
|
195
|
+
return {
|
|
196
|
+
year: parseInt(map.year, 10),
|
|
197
|
+
month: parseInt(map.month, 10),
|
|
198
|
+
dom: parseInt(map.day, 10),
|
|
199
|
+
hour: parseInt(map.hour, 10),
|
|
200
|
+
minute: parseInt(map.minute, 10),
|
|
201
|
+
second: parseInt(map.second, 10),
|
|
202
|
+
dow: DAY_NAMES[map.weekday.toLowerCase()] ?? 0,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (utc) {
|
|
207
|
+
return {
|
|
208
|
+
year: date.getUTCFullYear(),
|
|
209
|
+
month: date.getUTCMonth() + 1,
|
|
210
|
+
dom: date.getUTCDate(),
|
|
211
|
+
hour: date.getUTCHours(),
|
|
212
|
+
minute: date.getUTCMinutes(),
|
|
213
|
+
second: date.getUTCSeconds(),
|
|
214
|
+
dow: date.getUTCDay(),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
year: date.getFullYear(),
|
|
220
|
+
month: date.getMonth() + 1,
|
|
221
|
+
dom: date.getDate(),
|
|
222
|
+
hour: date.getHours(),
|
|
223
|
+
minute: date.getMinutes(),
|
|
224
|
+
second: date.getSeconds(),
|
|
225
|
+
dow: date.getDay(),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Finds the smallest element in set strictly greater than current
|
|
231
|
+
* @param {Set<number>} set
|
|
232
|
+
* @param {number} current
|
|
233
|
+
* @returns {number|null}
|
|
234
|
+
*/
|
|
235
|
+
function findNextInSet(set, current) {
|
|
236
|
+
let best = Infinity;
|
|
237
|
+
for (const v of set) {
|
|
238
|
+
if (v > current && v < best) {
|
|
239
|
+
best = v;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return best === Infinity ? null : best;
|
|
243
|
+
}
|
|
244
|
+
|
|
141
245
|
class CronExpression {
|
|
142
246
|
/**
|
|
143
247
|
* @param {string} expression - Standard 5-field cron or alias
|
|
144
248
|
* @param {Object} [options]
|
|
145
249
|
* @param {boolean} [options.utc=false] - Evaluate dates in UTC instead of local time
|
|
250
|
+
* @param {string} [options.timezone] - IANA Timezone (e.g. 'America/New_York', 'Europe/Istanbul')
|
|
251
|
+
* @param {string} [options.tz] - Alias for timezone
|
|
146
252
|
*/
|
|
147
253
|
constructor(expression, options = {}) {
|
|
148
254
|
if (typeof expression !== 'string' || !expression.trim()) {
|
|
@@ -152,6 +258,15 @@ class CronExpression {
|
|
|
152
258
|
this.rawExpression = expression;
|
|
153
259
|
this.resolvedExpression = resolveCronAlias(expression);
|
|
154
260
|
this.utc = !!options.utc;
|
|
261
|
+
this.timezone = options.timezone || options.tz || (this.utc ? 'UTC' : null);
|
|
262
|
+
|
|
263
|
+
if (this.timezone) {
|
|
264
|
+
try {
|
|
265
|
+
new Intl.DateTimeFormat('en-US', { timeZone: this.timezone });
|
|
266
|
+
} catch (_) {
|
|
267
|
+
throw new Error(`Invalid timeZone specified: '${this.timezone}'`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
155
270
|
|
|
156
271
|
const parts = this.resolvedExpression.trim().split(/\s+/);
|
|
157
272
|
if (parts.length !== 5) {
|
|
@@ -160,27 +275,27 @@ class CronExpression {
|
|
|
160
275
|
);
|
|
161
276
|
}
|
|
162
277
|
|
|
163
|
-
this.domAny = parts[2] === '*';
|
|
164
|
-
this.dowAny = parts[4] === '*';
|
|
278
|
+
this.domAny = parts[2] === '*' || parts[2] === '?';
|
|
279
|
+
this.dowAny = parts[4] === '*' || parts[4] === '?';
|
|
165
280
|
|
|
166
281
|
this.minutes = parseField(parts[0], 0, 59);
|
|
167
282
|
this.hours = parseField(parts[1], 0, 23);
|
|
168
283
|
this.doms = parseField(parts[2], 1, 31);
|
|
169
284
|
this.months = parseField(parts[3], 1, 12, MONTH_NAMES);
|
|
170
|
-
this.dows = parseField(parts[4], 0,
|
|
285
|
+
this.dows = parseField(parts[4], 0, 7, DAY_NAMES, true);
|
|
171
286
|
}
|
|
172
287
|
|
|
173
288
|
/**
|
|
174
289
|
* Check if a given Date matches the cron expression
|
|
175
|
-
* @param {Date} [date=new Date()]
|
|
290
|
+
* @param {Date|string|number} [date=new Date()]
|
|
176
291
|
* @returns {boolean}
|
|
177
292
|
*/
|
|
178
293
|
match(date = new Date()) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
const
|
|
294
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
295
|
+
if (isNaN(d.getTime())) return false;
|
|
296
|
+
|
|
297
|
+
const parts = getDateParts(d, this.utc, this.timezone);
|
|
298
|
+
const { minute, hour, month, dom, dow } = parts;
|
|
184
299
|
|
|
185
300
|
if (!this.minutes.has(minute)) return false;
|
|
186
301
|
if (!this.hours.has(hour)) return false;
|
|
@@ -199,12 +314,23 @@ class CronExpression {
|
|
|
199
314
|
* Calculate next matching Date after fromDate (default: now)
|
|
200
315
|
* Uses jumping skips to avoid long iteration loops.
|
|
201
316
|
*
|
|
202
|
-
* @param {Date} [fromDate=new Date()]
|
|
317
|
+
* @param {Date|string|number} [fromDate=new Date()]
|
|
203
318
|
* @returns {Date|null}
|
|
204
319
|
*/
|
|
205
320
|
getNextDate(fromDate = new Date()) {
|
|
206
|
-
const
|
|
321
|
+
const baseDate = fromDate instanceof Date ? fromDate : (fromDate ? new Date(fromDate) : new Date());
|
|
322
|
+
if (isNaN(baseDate.getTime())) {
|
|
323
|
+
throw new Error(`Invalid fromDate passed to getNextDate: '${fromDate}'`);
|
|
324
|
+
}
|
|
207
325
|
|
|
326
|
+
const d = new Date(baseDate.getTime());
|
|
327
|
+
|
|
328
|
+
// Timezone-aware calculation path
|
|
329
|
+
if (this.timezone && this.timezone.toUpperCase() !== 'UTC') {
|
|
330
|
+
return this._getNextDateTimezone(d);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Fast native path for UTC or Local time
|
|
208
334
|
if (this.utc) {
|
|
209
335
|
d.setUTCSeconds(0, 0);
|
|
210
336
|
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
@@ -253,20 +379,40 @@ class CronExpression {
|
|
|
253
379
|
|
|
254
380
|
const hour = this.utc ? d.getUTCHours() : d.getHours();
|
|
255
381
|
if (!this.hours.has(hour)) {
|
|
256
|
-
|
|
257
|
-
|
|
382
|
+
const nextHour = findNextInSet(this.hours, hour);
|
|
383
|
+
if (nextHour !== null) {
|
|
384
|
+
if (this.utc) {
|
|
385
|
+
d.setUTCHours(nextHour, 0, 0, 0);
|
|
386
|
+
} else {
|
|
387
|
+
d.setHours(nextHour, 0, 0, 0);
|
|
388
|
+
}
|
|
258
389
|
} else {
|
|
259
|
-
|
|
390
|
+
if (this.utc) {
|
|
391
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
392
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
393
|
+
} else {
|
|
394
|
+
d.setDate(d.getDate() + 1);
|
|
395
|
+
d.setHours(0, 0, 0, 0);
|
|
396
|
+
}
|
|
260
397
|
}
|
|
261
398
|
continue;
|
|
262
399
|
}
|
|
263
400
|
|
|
264
401
|
const minute = this.utc ? d.getUTCMinutes() : d.getMinutes();
|
|
265
402
|
if (!this.minutes.has(minute)) {
|
|
266
|
-
|
|
267
|
-
|
|
403
|
+
const nextMinute = findNextInSet(this.minutes, minute);
|
|
404
|
+
if (nextMinute !== null) {
|
|
405
|
+
if (this.utc) {
|
|
406
|
+
d.setUTCMinutes(nextMinute, 0, 0);
|
|
407
|
+
} else {
|
|
408
|
+
d.setMinutes(nextMinute, 0, 0);
|
|
409
|
+
}
|
|
268
410
|
} else {
|
|
269
|
-
|
|
411
|
+
if (this.utc) {
|
|
412
|
+
d.setUTCHours(d.getUTCHours() + 1, 0, 0, 0);
|
|
413
|
+
} else {
|
|
414
|
+
d.setHours(d.getHours() + 1, 0, 0, 0);
|
|
415
|
+
}
|
|
270
416
|
}
|
|
271
417
|
continue;
|
|
272
418
|
}
|
|
@@ -277,17 +423,82 @@ class CronExpression {
|
|
|
277
423
|
return null;
|
|
278
424
|
}
|
|
279
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Timezone-aware next date solver
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
_getNextDateTimezone(fromDate) {
|
|
431
|
+
// Advance 1 minute from fromDate in UTC
|
|
432
|
+
const d = new Date(fromDate.getTime());
|
|
433
|
+
d.setUTCSeconds(0, 0);
|
|
434
|
+
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
435
|
+
|
|
436
|
+
const initialParts = getDateParts(d, false, this.timezone);
|
|
437
|
+
const startYear = initialParts.year;
|
|
438
|
+
const maxYear = startYear + 5;
|
|
439
|
+
|
|
440
|
+
// Fast scan jumping through timezone parts
|
|
441
|
+
while (true) {
|
|
442
|
+
const parts = getDateParts(d, false, this.timezone);
|
|
443
|
+
if (parts.year > maxYear) return null;
|
|
444
|
+
|
|
445
|
+
if (!this.months.has(parts.month)) {
|
|
446
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
447
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const dayMatch = (this.domAny && this.dowAny)
|
|
452
|
+
? true
|
|
453
|
+
: (!this.domAny && this.dowAny)
|
|
454
|
+
? this.doms.has(parts.dom)
|
|
455
|
+
: (this.domAny && !this.dowAny)
|
|
456
|
+
? this.dows.has(parts.dow)
|
|
457
|
+
: (this.doms.has(parts.dom) || this.dows.has(parts.dow));
|
|
458
|
+
|
|
459
|
+
if (!dayMatch) {
|
|
460
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
461
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!this.hours.has(parts.hour)) {
|
|
466
|
+
const nextH = findNextInSet(this.hours, parts.hour);
|
|
467
|
+
if (nextH !== null) {
|
|
468
|
+
d.setUTCHours(d.getUTCHours() + (nextH - parts.hour), 0, 0, 0);
|
|
469
|
+
} else {
|
|
470
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
471
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
472
|
+
}
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!this.minutes.has(parts.minute)) {
|
|
477
|
+
const nextM = findNextInSet(this.minutes, parts.minute);
|
|
478
|
+
if (nextM !== null) {
|
|
479
|
+
d.setUTCMinutes(d.getUTCMinutes() + (nextM - parts.minute), 0, 0);
|
|
480
|
+
} else {
|
|
481
|
+
d.setUTCHours(d.getUTCHours() + 1, 0, 0, 0);
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return d;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
280
490
|
/**
|
|
281
491
|
* Return the next N matching Dates
|
|
282
492
|
* @param {number} count
|
|
283
|
-
* @param {Date} [fromDate=new Date()]
|
|
493
|
+
* @param {Date|string|number} [fromDate=new Date()]
|
|
284
494
|
* @returns {Date[]}
|
|
285
495
|
*/
|
|
286
496
|
getNextDates(count = 5, fromDate = new Date()) {
|
|
287
497
|
const dates = [];
|
|
288
498
|
let current = fromDate;
|
|
499
|
+
const limit = Math.max(1, parseInt(count, 10) || 5);
|
|
289
500
|
|
|
290
|
-
for (let i = 0; i <
|
|
501
|
+
for (let i = 0; i < limit; i++) {
|
|
291
502
|
const next = this.getNextDate(current);
|
|
292
503
|
if (!next) break;
|
|
293
504
|
dates.push(next);
|
|
@@ -300,11 +511,12 @@ class CronExpression {
|
|
|
300
511
|
/**
|
|
301
512
|
* Static helper to validate a cron expression string
|
|
302
513
|
* @param {string} expression
|
|
514
|
+
* @param {Object} [options]
|
|
303
515
|
* @returns {boolean}
|
|
304
516
|
*/
|
|
305
|
-
static isValid(expression) {
|
|
517
|
+
static isValid(expression, options) {
|
|
306
518
|
try {
|
|
307
|
-
new CronExpression(expression);
|
|
519
|
+
new CronExpression(expression, options);
|
|
308
520
|
return true;
|
|
309
521
|
} catch (_) {
|
|
310
522
|
return false;
|
|
@@ -329,3 +541,4 @@ module.exports = {
|
|
|
329
541
|
parseCron: CronExpression.parse,
|
|
330
542
|
CRON_ALIASES,
|
|
331
543
|
};
|
|
544
|
+
|
package/core/queue/manager.js
CHANGED
|
@@ -10,7 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const { Job } = require('./job');
|
|
12
12
|
const { MemoryQueueAdapter } = require('./adapters/memory');
|
|
13
|
-
const { CronExpression } = require('./cron');
|
|
13
|
+
const { CronExpression, CRON_ALIASES } = require('./cron');
|
|
14
14
|
const { TaskScheduler } = require('./scheduler');
|
|
15
15
|
|
|
16
16
|
class QueueManager extends EventEmitter {
|
|
@@ -317,6 +317,24 @@ class QueueManager extends EventEmitter {
|
|
|
317
317
|
return this.scheduler.getSchedule(idOrJobName);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Manually trigger a scheduled job immediately
|
|
322
|
+
* @param {string} idOrJobName
|
|
323
|
+
* @returns {Promise<Job|null>}
|
|
324
|
+
*/
|
|
325
|
+
triggerSchedule(idOrJobName) {
|
|
326
|
+
return this.scheduler.trigger(idOrJobName);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Alias for triggerSchedule()
|
|
331
|
+
* @param {string} idOrJobName
|
|
332
|
+
* @returns {Promise<Job|null>}
|
|
333
|
+
*/
|
|
334
|
+
triggerCron(idOrJobName) {
|
|
335
|
+
return this.scheduler.trigger(idOrJobName);
|
|
336
|
+
}
|
|
337
|
+
|
|
320
338
|
/**
|
|
321
339
|
* Delete completed or failed jobs older than a cutoff date
|
|
322
340
|
* @param {Object} [options={}]
|
|
@@ -453,7 +471,7 @@ class QueueManager extends EventEmitter {
|
|
|
453
471
|
if (typeof handler === 'function') {
|
|
454
472
|
this.define(jobName, handler, options);
|
|
455
473
|
|
|
456
|
-
const cronExpr = mod.cron || mod.schedule || (isCronFile ? (mod.expression || `@${nameWithoutExt}`) : null);
|
|
474
|
+
const cronExpr = mod.cron || mod.schedule || (isCronFile ? (mod.expression || (CRON_ALIASES[`@${nameWithoutExt}`] ? `@${nameWithoutExt}` : null)) : null);
|
|
457
475
|
if (cronExpr) {
|
|
458
476
|
this.schedule(
|
|
459
477
|
cronExpr,
|