webspresso 0.0.99 → 0.0.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +4 -2
  3. package/.agents/AUTH.md +309 -0
  4. package/.agents/CLI.md +128 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/ERRORS.md +59 -0
  7. package/.agents/EVENTS.md +179 -0
  8. package/.agents/ORM.md +149 -0
  9. package/.agents/PLUGINS.md +44 -7
  10. package/.agents/QUEUE.md +18 -10
  11. package/.agents/ROUTING.md +97 -1
  12. package/.agents/STREAMING.md +35 -0
  13. package/AGENTS.md +2 -0
  14. package/README.md +145 -93
  15. package/bin/commands/add-docker.js +467 -0
  16. package/bin/commands/db-makemigrations.js +83 -0
  17. package/bin/commands/deploy-check.js +339 -0
  18. package/bin/commands/list.js +96 -0
  19. package/bin/commands/make-command.js +93 -0
  20. package/bin/commands/make-event.js +80 -0
  21. package/bin/commands/make-factory.js +72 -0
  22. package/bin/commands/make-mail.js +93 -0
  23. package/bin/commands/make-policy.js +106 -0
  24. package/bin/commands/run.js +195 -0
  25. package/bin/webspresso.js +21 -0
  26. package/core/auth/bouncer.js +196 -0
  27. package/core/auth/index.js +73 -1
  28. package/core/auth/magic-link.js +170 -0
  29. package/core/auth/manager.js +343 -3
  30. package/core/auth/middleware.js +45 -6
  31. package/core/auth/session-manager.js +255 -0
  32. package/core/auth/sessions.js +351 -0
  33. package/core/auth/tokens.js +1 -0
  34. package/core/auth/totp.js +4 -1
  35. package/core/auth/user-agent.js +161 -0
  36. package/core/commands/define-command.js +48 -0
  37. package/core/commands/index.js +18 -0
  38. package/core/commands/registry.js +156 -0
  39. package/core/commands/runner.js +76 -0
  40. package/core/compression/compressible.js +1 -1
  41. package/core/compression/middleware.js +35 -251
  42. package/core/content/service.js +8 -2
  43. package/core/events/discovery.js +142 -0
  44. package/core/events/dispatcher.js +552 -0
  45. package/core/events/index.js +23 -0
  46. package/core/events/signal.js +92 -0
  47. package/core/forms/flash.js +234 -0
  48. package/core/forms/form-builder.js +214 -0
  49. package/core/forms/index.js +16 -0
  50. package/core/mail/index.js +23 -0
  51. package/core/mail/mailable.js +250 -0
  52. package/core/mail/mailer.js +184 -0
  53. package/core/mail/transports.js +238 -0
  54. package/core/orm/factory.js +250 -0
  55. package/core/orm/index.js +8 -1
  56. package/core/orm/migrations/diff.js +185 -0
  57. package/core/orm/migrations/index.js +22 -4
  58. package/core/orm/model.js +55 -1
  59. package/core/orm/repository.js +283 -13
  60. package/core/orm/scopes.js +3 -2
  61. package/core/orm/types.js +6 -1
  62. package/core/queue/cron.js +252 -39
  63. package/core/queue/manager.js +20 -2
  64. package/core/queue/scheduler.js +115 -59
  65. package/core/ssr/stream.js +62 -2
  66. package/index.d.ts +540 -14
  67. package/index.js +44 -0
  68. package/package.json +11 -5
  69. package/plugins/admin-panel/api.js +509 -3
  70. package/plugins/admin-panel/index.js +17 -0
  71. package/plugins/admin-panel/modules/admin-users.js +1 -1
  72. package/plugins/audit-log/admin-component.js +54 -54
  73. package/plugins/audit-log/index.js +2 -2
  74. package/plugins/content/client/inline-edit.js +2 -46
  75. package/plugins/content/models/content-entry.js +4 -3
  76. package/plugins/content/models/content-type.js +4 -3
  77. package/plugins/cors.js +15 -4
  78. package/plugins/email/auth-bridge.js +66 -0
  79. package/plugins/email/bundled-templates.js +15 -0
  80. package/plugins/email/index.js +48 -0
  81. package/plugins/email/preview-handler.js +555 -0
  82. package/plugins/email/service.js +23 -12
  83. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  84. package/plugins/email/transport.js +30 -11
  85. package/plugins/file-manager/admin-component.js +49 -49
  86. package/plugins/file-manager/index.js +4 -4
  87. package/plugins/index.js +2 -0
  88. package/plugins/redirect/index.js +1 -1
  89. package/plugins/webhooks/admin-component.js +608 -0
  90. package/plugins/webhooks/api-handlers.js +237 -0
  91. package/plugins/webhooks/crypto.js +144 -0
  92. package/plugins/webhooks/dispatcher.js +708 -0
  93. package/plugins/webhooks/index.js +263 -0
  94. package/plugins/webhooks/matcher.js +113 -0
  95. package/plugins/webhooks/migration-template.js +75 -0
  96. package/plugins/webhooks/models.js +98 -0
  97. package/plugins/webhooks/sender.js +176 -0
  98. package/src/api/api-loader.js +1 -1
  99. package/src/app-context.js +48 -6
  100. package/src/bundle/index.js +2 -1
  101. package/src/dev-reload/index.js +9 -1
  102. package/src/discovery/scan-routes.js +10 -1
  103. package/src/file-router.js +173 -4
  104. package/src/helpers.js +150 -1
  105. package/src/njk-frontmatter.js +3 -0
  106. package/src/pages/page-loader.js +1 -1
  107. package/src/routing/cascading.js +120 -4
  108. package/src/server.js +180 -26
@@ -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*([mh])$/);
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 (const part of parts) {
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
- step = parseInt(sub[1], 10);
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
- start = parseInt(sub[0], 10);
111
- end = parseInt(sub[1], 10);
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
- end = start;
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
- values.add(i === 7 && max === 6 ? 0 : i);
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, 6, DAY_NAMES);
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 minute = this.utc ? date.getUTCMinutes() : date.getMinutes();
180
- const hour = this.utc ? date.getUTCHours() : date.getHours();
181
- const month = (this.utc ? date.getUTCMonth() : date.getMonth()) + 1;
182
- const dom = this.utc ? date.getUTCDate() : date.getDate();
183
- const dow = this.utc ? date.getUTCDay() : date.getDay();
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 d = new Date(fromDate.getTime());
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
- if (this.utc) {
257
- d.setUTCHours(d.getUTCHours() + 1, 0, 0, 0);
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
- d.setHours(d.getHours() + 1, 0, 0, 0);
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
- if (this.utc) {
267
- d.setUTCMinutes(d.getUTCMinutes() + 1);
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
- d.setMinutes(d.getMinutes() + 1);
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 < count; 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
+
@@ -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,