webspresso 0.0.97 → 0.0.99
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 +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- 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/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- 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 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +4 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
|
@@ -164,6 +164,68 @@ class MemoryQueueAdapter {
|
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Delete completed or failed jobs older than a cutoff date
|
|
169
|
+
* @param {Object} [options={}]
|
|
170
|
+
* @param {string|string[]} [options.status=['completed']]
|
|
171
|
+
* @param {Date|string|number} [options.olderThan]
|
|
172
|
+
* @param {number} [options.days=7]
|
|
173
|
+
* @param {number} [options.retentionDays]
|
|
174
|
+
* @param {number} [options.limit]
|
|
175
|
+
* @returns {Promise<number>}
|
|
176
|
+
*/
|
|
177
|
+
async clean(options = {}) {
|
|
178
|
+
let olderThan;
|
|
179
|
+
if (options.olderThan !== undefined) {
|
|
180
|
+
olderThan = options.olderThan instanceof Date
|
|
181
|
+
? options.olderThan
|
|
182
|
+
: new Date(options.olderThan);
|
|
183
|
+
|
|
184
|
+
if (Number.isNaN(olderThan.getTime())) {
|
|
185
|
+
throw new Error('clean: invalid olderThan date');
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const rawDays = options.days !== undefined
|
|
189
|
+
? options.days
|
|
190
|
+
: (options.retentionDays !== undefined ? options.retentionDays : 7);
|
|
191
|
+
const days = typeof rawDays === 'number' ? rawDays : Number(rawDays);
|
|
192
|
+
if (Number.isNaN(days) || days < 0) {
|
|
193
|
+
throw new Error('clean: days must be a non-negative number');
|
|
194
|
+
}
|
|
195
|
+
olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let statuses = ['completed'];
|
|
199
|
+
if (options.status) {
|
|
200
|
+
if (Array.isArray(options.status)) {
|
|
201
|
+
statuses = options.status;
|
|
202
|
+
} else if (typeof options.status === 'string') {
|
|
203
|
+
if (options.status === '*' || options.status === 'all') {
|
|
204
|
+
statuses = ['completed', 'failed', 'pending'];
|
|
205
|
+
} else {
|
|
206
|
+
statuses = options.status.split(',').map((s) => s.trim()).filter(Boolean);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const cutoffTime = olderThan.getTime();
|
|
212
|
+
let deletedCount = 0;
|
|
213
|
+
const limit = typeof options.limit === 'number' && options.limit > 0 ? options.limit : Infinity;
|
|
214
|
+
|
|
215
|
+
for (const [id, job] of this.jobs.entries()) {
|
|
216
|
+
if (deletedCount >= limit) break;
|
|
217
|
+
if (!statuses.includes(job.status)) continue;
|
|
218
|
+
|
|
219
|
+
const jobTime = (job.updatedAt instanceof Date ? job.updatedAt.getTime() : (job.createdAt instanceof Date ? job.createdAt.getTime() : 0));
|
|
220
|
+
if (jobTime < cutoffTime) {
|
|
221
|
+
this.jobs.delete(id);
|
|
222
|
+
deletedCount++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return deletedCount;
|
|
227
|
+
}
|
|
228
|
+
|
|
167
229
|
/**
|
|
168
230
|
* Clear all jobs
|
|
169
231
|
* @returns {Promise<void>}
|
|
@@ -336,6 +336,86 @@ class RedisQueueAdapter {
|
|
|
336
336
|
};
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Delete completed or failed jobs older than a cutoff date
|
|
341
|
+
* @param {Object} [options={}]
|
|
342
|
+
* @param {string|string[]} [options.status=['completed']]
|
|
343
|
+
* @param {Date|string|number} [options.olderThan]
|
|
344
|
+
* @param {number} [options.days=7]
|
|
345
|
+
* @param {number} [options.retentionDays]
|
|
346
|
+
* @param {number} [options.limit]
|
|
347
|
+
* @returns {Promise<number>}
|
|
348
|
+
*/
|
|
349
|
+
async clean(options = {}) {
|
|
350
|
+
let olderThan;
|
|
351
|
+
if (options.olderThan !== undefined) {
|
|
352
|
+
olderThan = options.olderThan instanceof Date
|
|
353
|
+
? options.olderThan
|
|
354
|
+
: new Date(options.olderThan);
|
|
355
|
+
|
|
356
|
+
if (Number.isNaN(olderThan.getTime())) {
|
|
357
|
+
throw new Error('clean: invalid olderThan date');
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
const rawDays = options.days !== undefined
|
|
361
|
+
? options.days
|
|
362
|
+
: (options.retentionDays !== undefined ? options.retentionDays : 7);
|
|
363
|
+
const days = typeof rawDays === 'number' ? rawDays : Number(rawDays);
|
|
364
|
+
if (Number.isNaN(days) || days < 0) {
|
|
365
|
+
throw new Error('clean: days must be a non-negative number');
|
|
366
|
+
}
|
|
367
|
+
olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let statuses = ['completed'];
|
|
371
|
+
if (options.status) {
|
|
372
|
+
if (Array.isArray(options.status)) {
|
|
373
|
+
statuses = options.status;
|
|
374
|
+
} else if (typeof options.status === 'string') {
|
|
375
|
+
if (options.status === '*' || options.status === 'all') {
|
|
376
|
+
statuses = ['completed', 'failed'];
|
|
377
|
+
} else {
|
|
378
|
+
statuses = options.status.split(',').map((s) => s.trim()).filter(Boolean);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const cutoffTime = olderThan.getTime();
|
|
384
|
+
let deletedCount = 0;
|
|
385
|
+
const limit = typeof options.limit === 'number' && options.limit > 0 ? options.limit : Infinity;
|
|
386
|
+
|
|
387
|
+
for (const status of statuses) {
|
|
388
|
+
const setKey = this._key(status);
|
|
389
|
+
let jobIds = [];
|
|
390
|
+
try {
|
|
391
|
+
if (typeof this.client.smembers === 'function') {
|
|
392
|
+
jobIds = (await this.client.smembers(setKey)) || [];
|
|
393
|
+
} else if (typeof this.client.sMembers === 'function') {
|
|
394
|
+
jobIds = (await this.client.sMembers(setKey)) || [];
|
|
395
|
+
}
|
|
396
|
+
} catch {}
|
|
397
|
+
|
|
398
|
+
for (const id of jobIds) {
|
|
399
|
+
if (deletedCount >= limit) break;
|
|
400
|
+
const job = await this.getJob(id);
|
|
401
|
+
const jobTime = job?.updatedAt ? new Date(job.updatedAt).getTime() : 0;
|
|
402
|
+
if (!job || jobTime < cutoffTime) {
|
|
403
|
+
if (typeof this.client.srem === 'function') {
|
|
404
|
+
await this.client.srem(setKey, id);
|
|
405
|
+
} else if (typeof this.client.sRem === 'function') {
|
|
406
|
+
await this.client.sRem(setKey, id);
|
|
407
|
+
}
|
|
408
|
+
if (typeof this.client.del === 'function') {
|
|
409
|
+
await this.client.del(this._key('job:' + id));
|
|
410
|
+
}
|
|
411
|
+
deletedCount++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return deletedCount;
|
|
417
|
+
}
|
|
418
|
+
|
|
339
419
|
/**
|
|
340
420
|
* Clear all queue keys
|
|
341
421
|
* @returns {Promise<void>}
|
|
@@ -0,0 +1,331 @@
|
|
|
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
|
+
'@daily': '0 0 * * *',
|
|
24
|
+
'@midnight': '0 0 * * *',
|
|
25
|
+
'@hourly': '0 * * * *',
|
|
26
|
+
'@every_minute': '* * * * *',
|
|
27
|
+
'@minutely': '* * * * *',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves alias string (e.g. '@daily', '@every 5m') into a 5-part cron expression.
|
|
32
|
+
* @param {string} str
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function resolveCronAlias(str) {
|
|
36
|
+
const trimmed = str.trim();
|
|
37
|
+
const lower = trimmed.toLowerCase();
|
|
38
|
+
|
|
39
|
+
if (CRON_ALIASES[lower]) {
|
|
40
|
+
return CRON_ALIASES[lower];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Support '@every 5m', '@every 15m', '@every 2h'
|
|
44
|
+
const everyMatch = lower.match(/^@every\s+(\d+)\s*([mh])$/);
|
|
45
|
+
if (everyMatch) {
|
|
46
|
+
const amount = parseInt(everyMatch[1], 10);
|
|
47
|
+
const unit = everyMatch[2];
|
|
48
|
+
if (unit === 'm' && amount >= 1 && amount <= 59) {
|
|
49
|
+
return `*/${amount} * * * *`;
|
|
50
|
+
}
|
|
51
|
+
if (unit === 'h' && amount >= 1 && amount <= 23) {
|
|
52
|
+
return `0 */${amount} * * *`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse a single cron field with ranges, steps, and lists.
|
|
61
|
+
* @param {string} field
|
|
62
|
+
* @param {number} min
|
|
63
|
+
* @param {number} max
|
|
64
|
+
* @param {Record<string, number>} [nameMap]
|
|
65
|
+
* @returns {Set<number>}
|
|
66
|
+
*/
|
|
67
|
+
function parseField(field, min, max, nameMap = null) {
|
|
68
|
+
let str = field.trim().toLowerCase();
|
|
69
|
+
|
|
70
|
+
if (nameMap) {
|
|
71
|
+
for (const [name, val] of Object.entries(nameMap)) {
|
|
72
|
+
str = str.replace(new RegExp(`\\b${name}\\b`, 'g'), String(val));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const values = new Set();
|
|
77
|
+
const parts = str.split(',');
|
|
78
|
+
|
|
79
|
+
for (const part of parts) {
|
|
80
|
+
if (!part) {
|
|
81
|
+
throw new Error(`Invalid empty cron segment in '${field}'`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let step = 1;
|
|
85
|
+
let rangeStr = part;
|
|
86
|
+
|
|
87
|
+
if (part.includes('/')) {
|
|
88
|
+
const sub = part.split('/');
|
|
89
|
+
if (sub.length !== 2) {
|
|
90
|
+
throw new Error(`Invalid step syntax in '${part}'`);
|
|
91
|
+
}
|
|
92
|
+
rangeStr = sub[0];
|
|
93
|
+
step = parseInt(sub[1], 10);
|
|
94
|
+
if (isNaN(step) || step <= 0) {
|
|
95
|
+
throw new Error(`Invalid step value '${sub[1]}' in '${part}'`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let start;
|
|
100
|
+
let end;
|
|
101
|
+
|
|
102
|
+
if (rangeStr === '*' || rangeStr === '') {
|
|
103
|
+
start = min;
|
|
104
|
+
end = max;
|
|
105
|
+
} else if (rangeStr.includes('-')) {
|
|
106
|
+
const sub = rangeStr.split('-');
|
|
107
|
+
if (sub.length !== 2) {
|
|
108
|
+
throw new Error(`Invalid range syntax in '${rangeStr}'`);
|
|
109
|
+
}
|
|
110
|
+
start = parseInt(sub[0], 10);
|
|
111
|
+
end = parseInt(sub[1], 10);
|
|
112
|
+
if (isNaN(start) || isNaN(end) || start > end) {
|
|
113
|
+
throw new Error(`Invalid range boundary '${rangeStr}' in '${part}'`);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
start = parseInt(rangeStr, 10);
|
|
117
|
+
end = start;
|
|
118
|
+
if (isNaN(start)) {
|
|
119
|
+
throw new Error(`Invalid numeric value '${rangeStr}' in '${part}'`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
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
|
+
if (start < min || end > max) {
|
|
130
|
+
throw new Error(`Cron value out of range [${min}-${max}]: '${part}'`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (let i = start; i <= end; i += step) {
|
|
134
|
+
values.add(i === 7 && max === 6 ? 0 : i);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return values;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class CronExpression {
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} expression - Standard 5-field cron or alias
|
|
144
|
+
* @param {Object} [options]
|
|
145
|
+
* @param {boolean} [options.utc=false] - Evaluate dates in UTC instead of local time
|
|
146
|
+
*/
|
|
147
|
+
constructor(expression, options = {}) {
|
|
148
|
+
if (typeof expression !== 'string' || !expression.trim()) {
|
|
149
|
+
throw new Error('Cron expression must be a non-empty string');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.rawExpression = expression;
|
|
153
|
+
this.resolvedExpression = resolveCronAlias(expression);
|
|
154
|
+
this.utc = !!options.utc;
|
|
155
|
+
|
|
156
|
+
const parts = this.resolvedExpression.trim().split(/\s+/);
|
|
157
|
+
if (parts.length !== 5) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Invalid cron expression '${expression}': expected 5 fields (minute hour day-of-month month day-of-week), got ${parts.length}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.domAny = parts[2] === '*';
|
|
164
|
+
this.dowAny = parts[4] === '*';
|
|
165
|
+
|
|
166
|
+
this.minutes = parseField(parts[0], 0, 59);
|
|
167
|
+
this.hours = parseField(parts[1], 0, 23);
|
|
168
|
+
this.doms = parseField(parts[2], 1, 31);
|
|
169
|
+
this.months = parseField(parts[3], 1, 12, MONTH_NAMES);
|
|
170
|
+
this.dows = parseField(parts[4], 0, 6, DAY_NAMES);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if a given Date matches the cron expression
|
|
175
|
+
* @param {Date} [date=new Date()]
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
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();
|
|
184
|
+
|
|
185
|
+
if (!this.minutes.has(minute)) return false;
|
|
186
|
+
if (!this.hours.has(hour)) return false;
|
|
187
|
+
if (!this.months.has(month)) return false;
|
|
188
|
+
|
|
189
|
+
// Standard POSIX / crontab union behavior:
|
|
190
|
+
// If either dom or dow is '*', the other specifies matching day.
|
|
191
|
+
// If both are specified (neither is '*'), either match satisfies.
|
|
192
|
+
if (this.domAny && this.dowAny) return true;
|
|
193
|
+
if (!this.domAny && this.dowAny) return this.doms.has(dom);
|
|
194
|
+
if (this.domAny && !this.dowAny) return this.dows.has(dow);
|
|
195
|
+
return this.doms.has(dom) || this.dows.has(dow);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calculate next matching Date after fromDate (default: now)
|
|
200
|
+
* Uses jumping skips to avoid long iteration loops.
|
|
201
|
+
*
|
|
202
|
+
* @param {Date} [fromDate=new Date()]
|
|
203
|
+
* @returns {Date|null}
|
|
204
|
+
*/
|
|
205
|
+
getNextDate(fromDate = new Date()) {
|
|
206
|
+
const d = new Date(fromDate.getTime());
|
|
207
|
+
|
|
208
|
+
if (this.utc) {
|
|
209
|
+
d.setUTCSeconds(0, 0);
|
|
210
|
+
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
211
|
+
} else {
|
|
212
|
+
d.setSeconds(0, 0);
|
|
213
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const startYear = this.utc ? d.getUTCFullYear() : d.getFullYear();
|
|
217
|
+
const maxYear = startYear + 5;
|
|
218
|
+
|
|
219
|
+
while ((this.utc ? d.getUTCFullYear() : d.getFullYear()) <= maxYear) {
|
|
220
|
+
const month = (this.utc ? d.getUTCMonth() : d.getMonth()) + 1;
|
|
221
|
+
if (!this.months.has(month)) {
|
|
222
|
+
if (this.utc) {
|
|
223
|
+
d.setUTCMonth(d.getUTCMonth() + 1, 1);
|
|
224
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
225
|
+
} else {
|
|
226
|
+
d.setMonth(d.getMonth() + 1, 1);
|
|
227
|
+
d.setHours(0, 0, 0, 0);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const dom = this.utc ? d.getUTCDate() : d.getDate();
|
|
233
|
+
const dow = this.utc ? d.getUTCDay() : d.getDay();
|
|
234
|
+
|
|
235
|
+
const dayMatch = (this.domAny && this.dowAny)
|
|
236
|
+
? true
|
|
237
|
+
: (!this.domAny && this.dowAny)
|
|
238
|
+
? this.doms.has(dom)
|
|
239
|
+
: (this.domAny && !this.dowAny)
|
|
240
|
+
? this.dows.has(dow)
|
|
241
|
+
: (this.doms.has(dom) || this.dows.has(dow));
|
|
242
|
+
|
|
243
|
+
if (!dayMatch) {
|
|
244
|
+
if (this.utc) {
|
|
245
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
246
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
247
|
+
} else {
|
|
248
|
+
d.setDate(d.getDate() + 1);
|
|
249
|
+
d.setHours(0, 0, 0, 0);
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const hour = this.utc ? d.getUTCHours() : d.getHours();
|
|
255
|
+
if (!this.hours.has(hour)) {
|
|
256
|
+
if (this.utc) {
|
|
257
|
+
d.setUTCHours(d.getUTCHours() + 1, 0, 0, 0);
|
|
258
|
+
} else {
|
|
259
|
+
d.setHours(d.getHours() + 1, 0, 0, 0);
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const minute = this.utc ? d.getUTCMinutes() : d.getMinutes();
|
|
265
|
+
if (!this.minutes.has(minute)) {
|
|
266
|
+
if (this.utc) {
|
|
267
|
+
d.setUTCMinutes(d.getUTCMinutes() + 1);
|
|
268
|
+
} else {
|
|
269
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return d;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Return the next N matching Dates
|
|
282
|
+
* @param {number} count
|
|
283
|
+
* @param {Date} [fromDate=new Date()]
|
|
284
|
+
* @returns {Date[]}
|
|
285
|
+
*/
|
|
286
|
+
getNextDates(count = 5, fromDate = new Date()) {
|
|
287
|
+
const dates = [];
|
|
288
|
+
let current = fromDate;
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < count; i++) {
|
|
291
|
+
const next = this.getNextDate(current);
|
|
292
|
+
if (!next) break;
|
|
293
|
+
dates.push(next);
|
|
294
|
+
current = next;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return dates;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Static helper to validate a cron expression string
|
|
302
|
+
* @param {string} expression
|
|
303
|
+
* @returns {boolean}
|
|
304
|
+
*/
|
|
305
|
+
static isValid(expression) {
|
|
306
|
+
try {
|
|
307
|
+
new CronExpression(expression);
|
|
308
|
+
return true;
|
|
309
|
+
} catch (_) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Parse expression into a CronExpression instance
|
|
316
|
+
* @param {string} expression
|
|
317
|
+
* @param {Object} [options]
|
|
318
|
+
* @returns {CronExpression}
|
|
319
|
+
*/
|
|
320
|
+
static parse(expression, options) {
|
|
321
|
+
return new CronExpression(expression, options);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
CronExpression,
|
|
327
|
+
resolveCronAlias,
|
|
328
|
+
isValidCron: CronExpression.isValid,
|
|
329
|
+
parseCron: CronExpression.parse,
|
|
330
|
+
CRON_ALIASES,
|
|
331
|
+
};
|
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
|
};
|