webspresso 0.0.98 → 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/ADMIN.md +11 -1
- package/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +415 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +178 -4
- package/.agents/PLUGINS.md +113 -8
- package/.agents/QUEUE.md +92 -0
- package/.agents/ROUTING.md +160 -7
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -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 +123 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +477 -3
- package/core/auth/middleware.js +118 -5
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- 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 +507 -0
- package/core/auth/user-agent.js +161 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +10 -1
- package/core/orm/model.js +55 -1
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +327 -16
- package/core/orm/scopes.js +3 -2
- package/core/orm/search.js +213 -0
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +544 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +100 -1
- package/core/queue/scheduler.js +415 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +794 -6
- package/index.js +48 -0
- package/package.json +7 -5
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- 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 +69 -53
- package/plugins/file-manager/index.js +25 -8
- package/plugins/index.js +8 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -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/api/api-loader.js +1 -0
- package/src/bundle/index.js +2 -1
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +310 -104
- package/src/helpers.js +16 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +490 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
package/core/orm/types.js
CHANGED
|
@@ -79,12 +79,17 @@
|
|
|
79
79
|
* @typedef {'belongsTo'|'hasMany'|'hasOne'} RelationType
|
|
80
80
|
*/
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {boolean|'delete'|'restore'|'all'|string[]|{delete?: boolean, restore?: boolean}} RelationCascadeOption
|
|
84
|
+
*/
|
|
85
|
+
|
|
82
86
|
/**
|
|
83
87
|
* @typedef {Object} RelationDefinition
|
|
84
88
|
* @property {RelationType} type - Type of relation
|
|
85
|
-
* @property {() => ModelDefinition} model -
|
|
89
|
+
* @property {string|(() => ModelDefinition|string)} model - Related model reference (function or model name)
|
|
86
90
|
* @property {string} foreignKey - Foreign key column name
|
|
87
91
|
* @property {string} [localKey='id'] - Local key column name (for hasMany/hasOne)
|
|
92
|
+
* @property {RelationCascadeOption} [cascade=false] - Cascading delete and restore configuration
|
|
88
93
|
*/
|
|
89
94
|
|
|
90
95
|
/**
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Native Cron Expression Parser & Matcher
|
|
3
|
+
* Pure Node.js standard 5-part cron implementation (zero external dependencies)
|
|
4
|
+
* @module core/queue/cron
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const MONTH_NAMES = {
|
|
10
|
+
jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,
|
|
11
|
+
jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DAY_NAMES = {
|
|
15
|
+
sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const CRON_ALIASES = {
|
|
19
|
+
'@yearly': '0 0 1 1 *',
|
|
20
|
+
'@annually': '0 0 1 1 *',
|
|
21
|
+
'@monthly': '0 0 1 * *',
|
|
22
|
+
'@weekly': '0 0 * * 0',
|
|
23
|
+
'@weekdays': '0 0 * * 1-5',
|
|
24
|
+
'@workdays': '0 0 * * 1-5',
|
|
25
|
+
'@weekends': '0 0 * * 6,0',
|
|
26
|
+
'@daily': '0 0 * * *',
|
|
27
|
+
'@midnight': '0 0 * * *',
|
|
28
|
+
'@hourly': '0 * * * *',
|
|
29
|
+
'@every_hour': '0 * * * *',
|
|
30
|
+
'@every_minute': '* * * * *',
|
|
31
|
+
'@minutely': '* * * * *',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolves alias string (e.g. '@daily', '@every 5m', '@every 2h', '@every 1d') into a 5-part cron expression.
|
|
36
|
+
* @param {string} str
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function resolveCronAlias(str) {
|
|
40
|
+
if (typeof str !== 'string') return str;
|
|
41
|
+
const trimmed = str.trim();
|
|
42
|
+
const lower = trimmed.toLowerCase();
|
|
43
|
+
|
|
44
|
+
if (CRON_ALIASES[lower]) {
|
|
45
|
+
return CRON_ALIASES[lower];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Support '@every 5m', '@every 15m', '@every 2h', '@every 1d'
|
|
49
|
+
const everyMatch = lower.match(/^@every\s+(\d+)\s*([mhd])$/);
|
|
50
|
+
if (everyMatch) {
|
|
51
|
+
const amount = parseInt(everyMatch[1], 10);
|
|
52
|
+
const unit = everyMatch[2];
|
|
53
|
+
if (unit === 'm' && amount >= 1 && amount <= 59) {
|
|
54
|
+
return `*/${amount} * * * *`;
|
|
55
|
+
}
|
|
56
|
+
if (unit === 'h' && amount >= 1 && amount <= 23) {
|
|
57
|
+
return `0 */${amount} * * *`;
|
|
58
|
+
}
|
|
59
|
+
if (unit === 'd' && amount >= 1 && amount <= 30) {
|
|
60
|
+
return `0 0 */${amount} * *`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse a single cron field with ranges, steps, and lists.
|
|
69
|
+
* @param {string} field
|
|
70
|
+
* @param {number} min
|
|
71
|
+
* @param {number} max
|
|
72
|
+
* @param {Record<string, number>} [nameMap]
|
|
73
|
+
* @param {boolean} [isDow=false]
|
|
74
|
+
* @returns {Set<number>}
|
|
75
|
+
*/
|
|
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
|
+
|
|
81
|
+
let str = field.trim().toLowerCase();
|
|
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
|
+
|
|
90
|
+
if (nameMap) {
|
|
91
|
+
for (const [name, val] of Object.entries(nameMap)) {
|
|
92
|
+
str = str.replace(new RegExp(`\\b${name}\\b`, 'g'), String(val));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const values = new Set();
|
|
97
|
+
const parts = str.split(',');
|
|
98
|
+
|
|
99
|
+
for (let rawPart of parts) {
|
|
100
|
+
const part = rawPart.trim();
|
|
101
|
+
if (!part) {
|
|
102
|
+
throw new Error(`Invalid empty cron segment in '${field}'`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let step = 1;
|
|
106
|
+
let rangeStr = part;
|
|
107
|
+
let hasStep = false;
|
|
108
|
+
|
|
109
|
+
if (part.includes('/')) {
|
|
110
|
+
const sub = part.split('/');
|
|
111
|
+
if (sub.length !== 2) {
|
|
112
|
+
throw new Error(`Invalid step syntax in '${part}'`);
|
|
113
|
+
}
|
|
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);
|
|
120
|
+
if (isNaN(step) || step <= 0) {
|
|
121
|
+
throw new Error(`Invalid step value '${sub[1]}' in '${part}'`);
|
|
122
|
+
}
|
|
123
|
+
hasStep = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let start;
|
|
127
|
+
let end;
|
|
128
|
+
|
|
129
|
+
if (rangeStr === '*' || rangeStr === '?' || rangeStr === '') {
|
|
130
|
+
start = min;
|
|
131
|
+
end = max;
|
|
132
|
+
} else if (rangeStr.includes('-')) {
|
|
133
|
+
const sub = rangeStr.split('-');
|
|
134
|
+
if (sub.length !== 2) {
|
|
135
|
+
throw new Error(`Invalid range syntax in '${rangeStr}'`);
|
|
136
|
+
}
|
|
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);
|
|
144
|
+
if (isNaN(start) || isNaN(end) || start > end) {
|
|
145
|
+
throw new Error(`Invalid range boundary '${rangeStr}' in '${part}'`);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
if (!/^\d+$/.test(rangeStr)) {
|
|
149
|
+
throw new Error(`Invalid numeric value '${rangeStr}' in '${part}'`);
|
|
150
|
+
}
|
|
151
|
+
start = parseInt(rangeStr, 10);
|
|
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;
|
|
154
|
+
if (isNaN(start)) {
|
|
155
|
+
throw new Error(`Invalid numeric value '${rangeStr}' in '${part}'`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (start < min || end > max) {
|
|
160
|
+
throw new Error(`Cron value out of range [${min}-${max}]: '${part}'`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (let i = start; i <= end; i += step) {
|
|
164
|
+
// In standard cron, day of week 7 is Sunday, same as 0
|
|
165
|
+
values.add(isDow && i === 7 ? 0 : i);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return values;
|
|
170
|
+
}
|
|
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
|
+
|
|
245
|
+
class CronExpression {
|
|
246
|
+
/**
|
|
247
|
+
* @param {string} expression - Standard 5-field cron or alias
|
|
248
|
+
* @param {Object} [options]
|
|
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
|
|
252
|
+
*/
|
|
253
|
+
constructor(expression, options = {}) {
|
|
254
|
+
if (typeof expression !== 'string' || !expression.trim()) {
|
|
255
|
+
throw new Error('Cron expression must be a non-empty string');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.rawExpression = expression;
|
|
259
|
+
this.resolvedExpression = resolveCronAlias(expression);
|
|
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
|
+
}
|
|
270
|
+
|
|
271
|
+
const parts = this.resolvedExpression.trim().split(/\s+/);
|
|
272
|
+
if (parts.length !== 5) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Invalid cron expression '${expression}': expected 5 fields (minute hour day-of-month month day-of-week), got ${parts.length}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.domAny = parts[2] === '*' || parts[2] === '?';
|
|
279
|
+
this.dowAny = parts[4] === '*' || parts[4] === '?';
|
|
280
|
+
|
|
281
|
+
this.minutes = parseField(parts[0], 0, 59);
|
|
282
|
+
this.hours = parseField(parts[1], 0, 23);
|
|
283
|
+
this.doms = parseField(parts[2], 1, 31);
|
|
284
|
+
this.months = parseField(parts[3], 1, 12, MONTH_NAMES);
|
|
285
|
+
this.dows = parseField(parts[4], 0, 7, DAY_NAMES, true);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if a given Date matches the cron expression
|
|
290
|
+
* @param {Date|string|number} [date=new Date()]
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
match(date = new Date()) {
|
|
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;
|
|
299
|
+
|
|
300
|
+
if (!this.minutes.has(minute)) return false;
|
|
301
|
+
if (!this.hours.has(hour)) return false;
|
|
302
|
+
if (!this.months.has(month)) return false;
|
|
303
|
+
|
|
304
|
+
// Standard POSIX / crontab union behavior:
|
|
305
|
+
// If either dom or dow is '*', the other specifies matching day.
|
|
306
|
+
// If both are specified (neither is '*'), either match satisfies.
|
|
307
|
+
if (this.domAny && this.dowAny) return true;
|
|
308
|
+
if (!this.domAny && this.dowAny) return this.doms.has(dom);
|
|
309
|
+
if (this.domAny && !this.dowAny) return this.dows.has(dow);
|
|
310
|
+
return this.doms.has(dom) || this.dows.has(dow);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Calculate next matching Date after fromDate (default: now)
|
|
315
|
+
* Uses jumping skips to avoid long iteration loops.
|
|
316
|
+
*
|
|
317
|
+
* @param {Date|string|number} [fromDate=new Date()]
|
|
318
|
+
* @returns {Date|null}
|
|
319
|
+
*/
|
|
320
|
+
getNextDate(fromDate = new Date()) {
|
|
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
|
+
}
|
|
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
|
|
334
|
+
if (this.utc) {
|
|
335
|
+
d.setUTCSeconds(0, 0);
|
|
336
|
+
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
337
|
+
} else {
|
|
338
|
+
d.setSeconds(0, 0);
|
|
339
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const startYear = this.utc ? d.getUTCFullYear() : d.getFullYear();
|
|
343
|
+
const maxYear = startYear + 5;
|
|
344
|
+
|
|
345
|
+
while ((this.utc ? d.getUTCFullYear() : d.getFullYear()) <= maxYear) {
|
|
346
|
+
const month = (this.utc ? d.getUTCMonth() : d.getMonth()) + 1;
|
|
347
|
+
if (!this.months.has(month)) {
|
|
348
|
+
if (this.utc) {
|
|
349
|
+
d.setUTCMonth(d.getUTCMonth() + 1, 1);
|
|
350
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
351
|
+
} else {
|
|
352
|
+
d.setMonth(d.getMonth() + 1, 1);
|
|
353
|
+
d.setHours(0, 0, 0, 0);
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const dom = this.utc ? d.getUTCDate() : d.getDate();
|
|
359
|
+
const dow = this.utc ? d.getUTCDay() : d.getDay();
|
|
360
|
+
|
|
361
|
+
const dayMatch = (this.domAny && this.dowAny)
|
|
362
|
+
? true
|
|
363
|
+
: (!this.domAny && this.dowAny)
|
|
364
|
+
? this.doms.has(dom)
|
|
365
|
+
: (this.domAny && !this.dowAny)
|
|
366
|
+
? this.dows.has(dow)
|
|
367
|
+
: (this.doms.has(dom) || this.dows.has(dow));
|
|
368
|
+
|
|
369
|
+
if (!dayMatch) {
|
|
370
|
+
if (this.utc) {
|
|
371
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
372
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
373
|
+
} else {
|
|
374
|
+
d.setDate(d.getDate() + 1);
|
|
375
|
+
d.setHours(0, 0, 0, 0);
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const hour = this.utc ? d.getUTCHours() : d.getHours();
|
|
381
|
+
if (!this.hours.has(hour)) {
|
|
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
|
+
}
|
|
389
|
+
} else {
|
|
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
|
+
}
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const minute = this.utc ? d.getUTCMinutes() : d.getMinutes();
|
|
402
|
+
if (!this.minutes.has(minute)) {
|
|
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
|
+
}
|
|
410
|
+
} else {
|
|
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
|
+
}
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return d;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
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
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Return the next N matching Dates
|
|
492
|
+
* @param {number} count
|
|
493
|
+
* @param {Date|string|number} [fromDate=new Date()]
|
|
494
|
+
* @returns {Date[]}
|
|
495
|
+
*/
|
|
496
|
+
getNextDates(count = 5, fromDate = new Date()) {
|
|
497
|
+
const dates = [];
|
|
498
|
+
let current = fromDate;
|
|
499
|
+
const limit = Math.max(1, parseInt(count, 10) || 5);
|
|
500
|
+
|
|
501
|
+
for (let i = 0; i < limit; i++) {
|
|
502
|
+
const next = this.getNextDate(current);
|
|
503
|
+
if (!next) break;
|
|
504
|
+
dates.push(next);
|
|
505
|
+
current = next;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return dates;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Static helper to validate a cron expression string
|
|
513
|
+
* @param {string} expression
|
|
514
|
+
* @param {Object} [options]
|
|
515
|
+
* @returns {boolean}
|
|
516
|
+
*/
|
|
517
|
+
static isValid(expression, options) {
|
|
518
|
+
try {
|
|
519
|
+
new CronExpression(expression, options);
|
|
520
|
+
return true;
|
|
521
|
+
} catch (_) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Parse expression into a CronExpression instance
|
|
528
|
+
* @param {string} expression
|
|
529
|
+
* @param {Object} [options]
|
|
530
|
+
* @returns {CronExpression}
|
|
531
|
+
*/
|
|
532
|
+
static parse(expression, options) {
|
|
533
|
+
return new CronExpression(expression, options);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
module.exports = {
|
|
538
|
+
CronExpression,
|
|
539
|
+
resolveCronAlias,
|
|
540
|
+
isValidCron: CronExpression.isValid,
|
|
541
|
+
parseCron: CronExpression.parse,
|
|
542
|
+
CRON_ALIASES,
|
|
543
|
+
};
|
|
544
|
+
|
package/core/queue/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Webspresso Core Background Job Queue Engine
|
|
2
|
+
* Webspresso Core Background Job Queue & Task Scheduler Engine
|
|
3
3
|
* @module core/queue
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -10,6 +10,8 @@ const { QueueManager, createQueueManager } = require('./manager');
|
|
|
10
10
|
const { MemoryQueueAdapter } = require('./adapters/memory');
|
|
11
11
|
const { DatabaseQueueAdapter } = require('./adapters/database');
|
|
12
12
|
const { RedisQueueAdapter } = require('./adapters/redis');
|
|
13
|
+
const { CronExpression, isValidCron, parseCron, resolveCronAlias } = require('./cron');
|
|
14
|
+
const { TaskScheduler } = require('./scheduler');
|
|
13
15
|
|
|
14
16
|
module.exports = {
|
|
15
17
|
Job,
|
|
@@ -18,4 +20,9 @@ module.exports = {
|
|
|
18
20
|
MemoryQueueAdapter,
|
|
19
21
|
DatabaseQueueAdapter,
|
|
20
22
|
RedisQueueAdapter,
|
|
23
|
+
CronExpression,
|
|
24
|
+
isValidCron,
|
|
25
|
+
parseCron,
|
|
26
|
+
resolveCronAlias,
|
|
27
|
+
TaskScheduler,
|
|
21
28
|
};
|