servcraft 0.1.0 → 0.1.1
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/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { logger } from '../../core/logger.js';
|
|
3
|
+
import { NotFoundError, BadRequestError } from '../../utils/errors.js';
|
|
4
|
+
import type { CronJob, JobOptions } from './types.js';
|
|
5
|
+
import type { QueueService } from './queue.service.js';
|
|
6
|
+
|
|
7
|
+
// In-memory storage
|
|
8
|
+
const cronJobs = new Map<string, CronJob>();
|
|
9
|
+
const cronTimers = new Map<string, NodeJS.Timeout>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cron Job Manager
|
|
13
|
+
* Manages scheduled jobs with cron expressions
|
|
14
|
+
*/
|
|
15
|
+
export class CronJobManager {
|
|
16
|
+
constructor(private queueService: QueueService) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a cron job
|
|
20
|
+
*/
|
|
21
|
+
async createCronJob(
|
|
22
|
+
name: string,
|
|
23
|
+
cron: string,
|
|
24
|
+
queueName: string,
|
|
25
|
+
jobName: string,
|
|
26
|
+
data?: unknown,
|
|
27
|
+
_options?: JobOptions
|
|
28
|
+
): Promise<CronJob> {
|
|
29
|
+
// Validate cron expression
|
|
30
|
+
if (!this.isValidCron(cron)) {
|
|
31
|
+
throw new BadRequestError('Invalid cron expression');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cronJob: CronJob = {
|
|
35
|
+
id: randomUUID(),
|
|
36
|
+
name,
|
|
37
|
+
cron,
|
|
38
|
+
queueName,
|
|
39
|
+
jobName,
|
|
40
|
+
data,
|
|
41
|
+
enabled: true,
|
|
42
|
+
createdAt: new Date(),
|
|
43
|
+
nextRun: this.calculateNextRun(cron) ?? undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
cronJobs.set(cronJob.id, cronJob);
|
|
47
|
+
|
|
48
|
+
// Schedule the cron job
|
|
49
|
+
this.scheduleCronJob(cronJob.id);
|
|
50
|
+
|
|
51
|
+
logger.info(
|
|
52
|
+
{ cronJobId: cronJob.id, name, cron, nextRun: cronJob.nextRun },
|
|
53
|
+
'Cron job created'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return cronJob;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get a cron job
|
|
61
|
+
*/
|
|
62
|
+
async getCronJob(id: string): Promise<CronJob> {
|
|
63
|
+
const cronJob = cronJobs.get(id);
|
|
64
|
+
if (!cronJob) {
|
|
65
|
+
throw new NotFoundError('Cron job not found');
|
|
66
|
+
}
|
|
67
|
+
return cronJob;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* List all cron jobs
|
|
72
|
+
*/
|
|
73
|
+
async listCronJobs(): Promise<CronJob[]> {
|
|
74
|
+
return Array.from(cronJobs.values());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Update a cron job
|
|
79
|
+
*/
|
|
80
|
+
async updateCronJob(
|
|
81
|
+
id: string,
|
|
82
|
+
updates: {
|
|
83
|
+
name?: string;
|
|
84
|
+
cron?: string;
|
|
85
|
+
data?: unknown;
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
}
|
|
88
|
+
): Promise<CronJob> {
|
|
89
|
+
const cronJob = await this.getCronJob(id);
|
|
90
|
+
|
|
91
|
+
if (updates.name) {
|
|
92
|
+
cronJob.name = updates.name;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (updates.cron) {
|
|
96
|
+
if (!this.isValidCron(updates.cron)) {
|
|
97
|
+
throw new BadRequestError('Invalid cron expression');
|
|
98
|
+
}
|
|
99
|
+
cronJob.cron = updates.cron;
|
|
100
|
+
cronJob.nextRun = this.calculateNextRun(updates.cron) ?? undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (updates.data !== undefined) {
|
|
104
|
+
cronJob.data = updates.data;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (updates.enabled !== undefined) {
|
|
108
|
+
cronJob.enabled = updates.enabled;
|
|
109
|
+
|
|
110
|
+
if (updates.enabled) {
|
|
111
|
+
this.scheduleCronJob(id);
|
|
112
|
+
} else {
|
|
113
|
+
this.unscheduleCronJob(id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
cronJobs.set(id, cronJob);
|
|
118
|
+
|
|
119
|
+
logger.info({ cronJobId: id, updates }, 'Cron job updated');
|
|
120
|
+
|
|
121
|
+
return cronJob;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Delete a cron job
|
|
126
|
+
*/
|
|
127
|
+
async deleteCronJob(id: string): Promise<void> {
|
|
128
|
+
const cronJob = await this.getCronJob(id);
|
|
129
|
+
|
|
130
|
+
this.unscheduleCronJob(id);
|
|
131
|
+
cronJobs.delete(id);
|
|
132
|
+
|
|
133
|
+
logger.info({ cronJobId: id, name: cronJob.name }, 'Cron job deleted');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Manually trigger a cron job
|
|
138
|
+
*/
|
|
139
|
+
async triggerCronJob(id: string): Promise<void> {
|
|
140
|
+
const cronJob = await this.getCronJob(id);
|
|
141
|
+
|
|
142
|
+
await this.executeCronJob(cronJob);
|
|
143
|
+
|
|
144
|
+
logger.info({ cronJobId: id, name: cronJob.name }, 'Cron job triggered manually');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Schedule a cron job
|
|
149
|
+
*/
|
|
150
|
+
private scheduleCronJob(id: string): void {
|
|
151
|
+
const cronJob = cronJobs.get(id);
|
|
152
|
+
if (!cronJob || !cronJob.enabled) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clear existing timer
|
|
157
|
+
this.unscheduleCronJob(id);
|
|
158
|
+
|
|
159
|
+
// Calculate next run time
|
|
160
|
+
const nextRun = this.calculateNextRun(cronJob.cron);
|
|
161
|
+
if (!nextRun) {
|
|
162
|
+
logger.warn({ cronJobId: id }, 'Could not calculate next run time');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
cronJob.nextRun = nextRun;
|
|
167
|
+
|
|
168
|
+
// Schedule execution
|
|
169
|
+
const delay = nextRun.getTime() - Date.now();
|
|
170
|
+
|
|
171
|
+
const timer = setTimeout(async () => {
|
|
172
|
+
await this.executeCronJob(cronJob);
|
|
173
|
+
this.scheduleCronJob(id); // Reschedule for next run
|
|
174
|
+
}, delay);
|
|
175
|
+
|
|
176
|
+
cronTimers.set(id, timer);
|
|
177
|
+
|
|
178
|
+
logger.debug({ cronJobId: id, name: cronJob.name, nextRun }, 'Cron job scheduled');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unschedule a cron job
|
|
183
|
+
*/
|
|
184
|
+
private unscheduleCronJob(id: string): void {
|
|
185
|
+
const timer = cronTimers.get(id);
|
|
186
|
+
if (timer) {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
cronTimers.delete(id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Execute a cron job
|
|
194
|
+
*/
|
|
195
|
+
private async executeCronJob(cronJob: CronJob): Promise<void> {
|
|
196
|
+
try {
|
|
197
|
+
cronJob.lastRun = new Date();
|
|
198
|
+
|
|
199
|
+
logger.info({ cronJobId: cronJob.id, name: cronJob.name }, 'Executing cron job');
|
|
200
|
+
|
|
201
|
+
// Add job to queue
|
|
202
|
+
await this.queueService.addJob(cronJob.queueName, cronJob.jobName, cronJob.data || {});
|
|
203
|
+
|
|
204
|
+
logger.info({ cronJobId: cronJob.id, name: cronJob.name }, 'Cron job executed successfully');
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.error(
|
|
207
|
+
{ error, cronJobId: cronJob.id, name: cronJob.name },
|
|
208
|
+
'Error executing cron job'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Validate cron expression
|
|
215
|
+
*/
|
|
216
|
+
private isValidCron(cron: string): boolean {
|
|
217
|
+
// Basic validation for cron expression
|
|
218
|
+
// Format: minute hour day month dayOfWeek
|
|
219
|
+
// Support for */n, ranges, lists
|
|
220
|
+
const parts = cron.split(' ');
|
|
221
|
+
|
|
222
|
+
if (parts.length !== 5) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Validate each part
|
|
227
|
+
const ranges = [
|
|
228
|
+
[0, 59], // minute
|
|
229
|
+
[0, 23], // hour
|
|
230
|
+
[1, 31], // day
|
|
231
|
+
[1, 12], // month
|
|
232
|
+
[0, 6], // dayOfWeek
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < 5; i++) {
|
|
236
|
+
const part = parts[i];
|
|
237
|
+
const range = ranges[i];
|
|
238
|
+
if (!part || !range) continue;
|
|
239
|
+
const [min, max] = range;
|
|
240
|
+
if (min === undefined || max === undefined) continue;
|
|
241
|
+
|
|
242
|
+
if (part === '*') continue;
|
|
243
|
+
|
|
244
|
+
// Handle */n
|
|
245
|
+
if (part.startsWith('*/')) {
|
|
246
|
+
const step = parseInt(part.substring(2), 10);
|
|
247
|
+
if (isNaN(step) || step < 1) return false;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Handle ranges (e.g., 1-5)
|
|
252
|
+
if (part.includes('-')) {
|
|
253
|
+
const rangeParts = part.split('-').map((n) => parseInt(n, 10));
|
|
254
|
+
const start = rangeParts[0];
|
|
255
|
+
const end = rangeParts[1];
|
|
256
|
+
if (
|
|
257
|
+
start === undefined ||
|
|
258
|
+
end === undefined ||
|
|
259
|
+
isNaN(start) ||
|
|
260
|
+
isNaN(end) ||
|
|
261
|
+
start < min ||
|
|
262
|
+
end > max ||
|
|
263
|
+
start > end
|
|
264
|
+
) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Handle lists (e.g., 1,3,5)
|
|
271
|
+
if (part.includes(',')) {
|
|
272
|
+
const values = part.split(',').map((n) => parseInt(n, 10));
|
|
273
|
+
if (values.some((v) => isNaN(v) || v < min || v > max)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Handle single value
|
|
280
|
+
const value = parseInt(part, 10);
|
|
281
|
+
if (isNaN(value) || value < min || value > max) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Calculate next run time for cron expression
|
|
291
|
+
*/
|
|
292
|
+
private calculateNextRun(cron: string): Date | null {
|
|
293
|
+
const now = new Date();
|
|
294
|
+
const parts = cron.split(' ');
|
|
295
|
+
|
|
296
|
+
if (parts.length !== 5) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Parse cron parts
|
|
301
|
+
const minute = this.parseCronPart(parts[0] ?? '*', 0, 59);
|
|
302
|
+
const hour = this.parseCronPart(parts[1] ?? '*', 0, 23);
|
|
303
|
+
const day = this.parseCronPart(parts[2] ?? '*', 1, 31);
|
|
304
|
+
const month = this.parseCronPart(parts[3] ?? '*', 1, 12);
|
|
305
|
+
const dayOfWeek = this.parseCronPart(parts[4] ?? '*', 0, 6);
|
|
306
|
+
|
|
307
|
+
// Find next valid time
|
|
308
|
+
// This is a simplified implementation
|
|
309
|
+
// For production, use a library like node-cron or cron-parser
|
|
310
|
+
|
|
311
|
+
const next = new Date(now);
|
|
312
|
+
next.setSeconds(0);
|
|
313
|
+
next.setMilliseconds(0);
|
|
314
|
+
|
|
315
|
+
// Add one minute to start from next minute
|
|
316
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
317
|
+
|
|
318
|
+
// Simple implementation: check next 1000 minutes
|
|
319
|
+
for (let i = 0; i < 1000; i++) {
|
|
320
|
+
const m = next.getMinutes();
|
|
321
|
+
const h = next.getHours();
|
|
322
|
+
const d = next.getDate();
|
|
323
|
+
const mo = next.getMonth() + 1;
|
|
324
|
+
const dow = next.getDay();
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
minute.includes(m) &&
|
|
328
|
+
hour.includes(h) &&
|
|
329
|
+
day.includes(d) &&
|
|
330
|
+
month.includes(mo) &&
|
|
331
|
+
dayOfWeek.includes(dow)
|
|
332
|
+
) {
|
|
333
|
+
return next;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Parse a cron part into array of values
|
|
344
|
+
*/
|
|
345
|
+
private parseCronPart(part: string, min: number, max: number): number[] {
|
|
346
|
+
if (part === '*') {
|
|
347
|
+
const values = [];
|
|
348
|
+
for (let i = min; i <= max; i++) {
|
|
349
|
+
values.push(i);
|
|
350
|
+
}
|
|
351
|
+
return values;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (part.startsWith('*/')) {
|
|
355
|
+
const step = parseInt(part.substring(2), 10);
|
|
356
|
+
const values = [];
|
|
357
|
+
for (let i = min; i <= max; i += step) {
|
|
358
|
+
values.push(i);
|
|
359
|
+
}
|
|
360
|
+
return values;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (part.includes('-')) {
|
|
364
|
+
const rangeParts = part.split('-').map((n) => parseInt(n, 10));
|
|
365
|
+
const start = rangeParts[0] ?? min;
|
|
366
|
+
const end = rangeParts[1] ?? max;
|
|
367
|
+
const values: number[] = [];
|
|
368
|
+
for (let i = start; i <= end; i++) {
|
|
369
|
+
values.push(i);
|
|
370
|
+
}
|
|
371
|
+
return values;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (part.includes(',')) {
|
|
375
|
+
return part.split(',').map((n) => parseInt(n, 10));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return [parseInt(part, 10)];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Initialize cron jobs (call on startup)
|
|
383
|
+
*/
|
|
384
|
+
async initialize(): Promise<void> {
|
|
385
|
+
const jobs = Array.from(cronJobs.values());
|
|
386
|
+
|
|
387
|
+
for (const job of jobs) {
|
|
388
|
+
if (job.enabled) {
|
|
389
|
+
this.scheduleCronJob(job.id);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
logger.info({ count: jobs.length }, 'Cron jobs initialized');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Shutdown cron jobs (call on app shutdown)
|
|
398
|
+
*/
|
|
399
|
+
async shutdown(): Promise<void> {
|
|
400
|
+
for (const id of cronTimers.keys()) {
|
|
401
|
+
this.unscheduleCronJob(id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
logger.info('Cron jobs shut down');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Pre-configured cron schedules
|
|
410
|
+
*/
|
|
411
|
+
export const CronSchedules = {
|
|
412
|
+
/** Every minute */
|
|
413
|
+
EVERY_MINUTE: '* * * * *',
|
|
414
|
+
/** Every 5 minutes */
|
|
415
|
+
EVERY_5_MINUTES: '*/5 * * * *',
|
|
416
|
+
/** Every 15 minutes */
|
|
417
|
+
EVERY_15_MINUTES: '*/15 * * * *',
|
|
418
|
+
/** Every 30 minutes */
|
|
419
|
+
EVERY_30_MINUTES: '*/30 * * * *',
|
|
420
|
+
/** Every hour */
|
|
421
|
+
EVERY_HOUR: '0 * * * *',
|
|
422
|
+
/** Every 6 hours */
|
|
423
|
+
EVERY_6_HOURS: '0 */6 * * *',
|
|
424
|
+
/** Every 12 hours */
|
|
425
|
+
EVERY_12_HOURS: '0 */12 * * *',
|
|
426
|
+
/** Daily at midnight */
|
|
427
|
+
DAILY: '0 0 * * *',
|
|
428
|
+
/** Daily at noon */
|
|
429
|
+
DAILY_NOON: '0 12 * * *',
|
|
430
|
+
/** Weekly on Sunday at midnight */
|
|
431
|
+
WEEKLY: '0 0 * * 0',
|
|
432
|
+
/** Monthly on 1st at midnight */
|
|
433
|
+
MONTHLY: '0 0 1 * *',
|
|
434
|
+
/** Weekdays at 9 AM */
|
|
435
|
+
WEEKDAYS_9AM: '0 9 * * 1-5',
|
|
436
|
+
/** Weekends at 10 AM */
|
|
437
|
+
WEEKENDS_10AM: '0 10 * * 0,6',
|
|
438
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue/Jobs Module
|
|
3
|
+
*
|
|
4
|
+
* Provides background job processing with:
|
|
5
|
+
* - Multiple queue management
|
|
6
|
+
* - Job priority and retry strategies
|
|
7
|
+
* - Cron-based scheduling
|
|
8
|
+
* - Pre-built workers for common tasks
|
|
9
|
+
* - Real-time monitoring and metrics
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { QueueService, CronJobManager, defaultWorkers, CronSchedules } from './modules/queue';
|
|
14
|
+
*
|
|
15
|
+
* // Create queue service
|
|
16
|
+
* const queueService = new QueueService({
|
|
17
|
+
* redis: {
|
|
18
|
+
* host: 'localhost',
|
|
19
|
+
* port: 6379
|
|
20
|
+
* },
|
|
21
|
+
* metrics: true
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Register workers
|
|
25
|
+
* import { emailWorker, imageProcessingWorker } from './modules/queue';
|
|
26
|
+
* queueService.registerWorker('emails', emailWorker);
|
|
27
|
+
* queueService.registerWorker('images', imageProcessingWorker);
|
|
28
|
+
*
|
|
29
|
+
* // Add jobs
|
|
30
|
+
* await queueService.addJob('emails', 'send-email', {
|
|
31
|
+
* to: 'user@example.com',
|
|
32
|
+
* subject: 'Welcome!',
|
|
33
|
+
* html: '<h1>Welcome to our service</h1>'
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Create cron jobs
|
|
37
|
+
* const cronManager = new CronJobManager(queueService);
|
|
38
|
+
* await cronManager.createCronJob(
|
|
39
|
+
* 'Daily Backup',
|
|
40
|
+
* CronSchedules.DAILY,
|
|
41
|
+
* 'maintenance',
|
|
42
|
+
* 'database-backup',
|
|
43
|
+
* { databases: ['main', 'analytics'] }
|
|
44
|
+
* );
|
|
45
|
+
*
|
|
46
|
+
* // Add routes
|
|
47
|
+
* app.use('/api/queue', authMiddleware, createQueueRoutes(queueService, cronManager));
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* ## Pre-built Workers
|
|
51
|
+
*
|
|
52
|
+
* - `emailWorker` - Send emails
|
|
53
|
+
* - `imageProcessingWorker` - Process images (resize, crop, watermark)
|
|
54
|
+
* - `notificationWorker` - Send push/SMS/email notifications
|
|
55
|
+
* - `webhookWorker` - Send HTTP webhooks
|
|
56
|
+
* - `dataExportWorker` - Export data to CSV/Excel/PDF
|
|
57
|
+
* - `reportGenerationWorker` - Generate reports
|
|
58
|
+
* - `databaseBackupWorker` - Create database backups
|
|
59
|
+
* - `cacheWarmingWorker` - Warm up cache
|
|
60
|
+
* - `dataCleanupWorker` - Clean old data
|
|
61
|
+
* - `batchProcessingWorker` - Process items in batches
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
// Types
|
|
65
|
+
export * from './types.js';
|
|
66
|
+
|
|
67
|
+
// Services
|
|
68
|
+
export { QueueService } from './queue.service.js';
|
|
69
|
+
export { CronJobManager, CronSchedules } from './cron.js';
|
|
70
|
+
|
|
71
|
+
// Workers
|
|
72
|
+
export {
|
|
73
|
+
emailWorker,
|
|
74
|
+
imageProcessingWorker,
|
|
75
|
+
notificationWorker,
|
|
76
|
+
webhookWorker,
|
|
77
|
+
dataExportWorker,
|
|
78
|
+
reportGenerationWorker,
|
|
79
|
+
databaseBackupWorker,
|
|
80
|
+
cacheWarmingWorker,
|
|
81
|
+
dataCleanupWorker,
|
|
82
|
+
batchProcessingWorker,
|
|
83
|
+
defaultWorkers,
|
|
84
|
+
} from './workers.js';
|
|
85
|
+
|
|
86
|
+
// Routes
|
|
87
|
+
export { createQueueRoutes } from './queue.routes.js';
|