@luisrodrigues/nestjs-scheduler-dashboard 0.0.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.
Files changed (41) hide show
  1. package/dist/auth.d.ts +5 -0
  2. package/dist/auth.js +31 -0
  3. package/dist/basic-auth.guard.d.ts +7 -0
  4. package/dist/basic-auth.guard.js +44 -0
  5. package/dist/dashboard.server.d.ts +4 -0
  6. package/dist/dashboard.server.js +63 -0
  7. package/dist/decorators/job-concurrency.d.ts +22 -0
  8. package/dist/decorators/job-concurrency.js +108 -0
  9. package/dist/decorators/track-job.decorator.d.ts +6 -0
  10. package/dist/decorators/track-job.decorator.js +77 -0
  11. package/dist/embedded-server.d.ts +4 -0
  12. package/dist/embedded-server.js +47 -0
  13. package/dist/index.d.ts +10 -0
  14. package/dist/index.js +41 -0
  15. package/dist/jobs-tracker.service.d.ts +9 -0
  16. package/dist/jobs-tracker.service.js +40 -0
  17. package/dist/jobs.controller.d.ts +25 -0
  18. package/dist/jobs.controller.js +72 -0
  19. package/dist/jobs.service.d.ts +25 -0
  20. package/dist/jobs.service.js +34 -0
  21. package/dist/scheduler-dash.context.d.ts +7 -0
  22. package/dist/scheduler-dash.context.js +10 -0
  23. package/dist/scheduler-dash.module.d.ts +5 -0
  24. package/dist/scheduler-dash.module.js +39 -0
  25. package/dist/scheduler-dash.options.d.ts +13 -0
  26. package/dist/scheduler-dash.options.js +2 -0
  27. package/dist/scheduler-dash.schema.d.ts +17 -0
  28. package/dist/scheduler-dash.schema.js +29 -0
  29. package/dist/standalone-server.d.ts +5 -0
  30. package/dist/standalone-server.js +70 -0
  31. package/dist/storage/job-execution.interface.d.ts +8 -0
  32. package/dist/storage/job-execution.interface.js +2 -0
  33. package/dist/storage/job-metrics.interface.d.ts +5 -0
  34. package/dist/storage/job-metrics.interface.js +2 -0
  35. package/dist/storage/memory.storage.d.ts +16 -0
  36. package/dist/storage/memory.storage.js +71 -0
  37. package/dist/storage/storage.abstract.d.ts +15 -0
  38. package/dist/storage/storage.abstract.js +9 -0
  39. package/dist/ui/dashboard.d.ts +1 -0
  40. package/dist/ui/dashboard.js +4 -0
  41. package/package.json +46 -0
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JobsService = void 0;
4
+ const job_concurrency_1 = require("./decorators/job-concurrency");
5
+ class JobsService {
6
+ constructor(schedulerRegistry, storage) {
7
+ this.schedulerRegistry = schedulerRegistry;
8
+ this.storage = storage;
9
+ }
10
+ getJobs() {
11
+ const cron = [...this.schedulerRegistry.getCronJobs().entries()].map(([name, job]) => ({
12
+ name,
13
+ cronExpression: job.cronTime.source?.toString() ?? null,
14
+ running: job.running ?? false,
15
+ nextRun: job.nextDate().toISO(),
16
+ history: this.storage.findByJob(name),
17
+ metrics: this.storage.getMetrics(name),
18
+ }));
19
+ const intervals = this.schedulerRegistry.getIntervals().map((name) => ({ name }));
20
+ const timeouts = this.schedulerRegistry.getTimeouts().map((name) => ({ name }));
21
+ return { cron, intervals, timeouts };
22
+ }
23
+ triggerJob(name) {
24
+ const job = this.schedulerRegistry.getCronJobs().get(name);
25
+ if (!job)
26
+ return false;
27
+ job.fireOnTick();
28
+ return true;
29
+ }
30
+ stopExecution(executionId) {
31
+ return (0, job_concurrency_1.stopExecutionById)(executionId);
32
+ }
33
+ }
34
+ exports.JobsService = JobsService;
@@ -0,0 +1,7 @@
1
+ import { Storage } from './storage/storage.abstract';
2
+ export declare class SchedulerDashContext {
3
+ static storage: Storage | null;
4
+ static basePath: string;
5
+ static noOverlap: boolean;
6
+ static maxConcurrent: number | undefined;
7
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SchedulerDashContext = void 0;
4
+ class SchedulerDashContext {
5
+ }
6
+ exports.SchedulerDashContext = SchedulerDashContext;
7
+ SchedulerDashContext.storage = null;
8
+ SchedulerDashContext.basePath = '_jobs';
9
+ SchedulerDashContext.noOverlap = false;
10
+ SchedulerDashContext.maxConcurrent = undefined;
@@ -0,0 +1,5 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { SchedulerDashOptions } from './scheduler-dash.options';
3
+ export declare class SchedulerDashModule {
4
+ static forRoot(options: SchedulerDashOptions): DynamicModule;
5
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var SchedulerDashModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SchedulerDashModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const jobs_controller_1 = require("./jobs.controller");
13
+ const jobs_service_1 = require("./jobs.service");
14
+ const jobs_tracker_service_1 = require("./jobs-tracker.service");
15
+ const basic_auth_guard_1 = require("./basic-auth.guard");
16
+ const scheduler_dash_options_1 = require("./scheduler-dash.options");
17
+ let SchedulerDashModule = SchedulerDashModule_1 = class SchedulerDashModule {
18
+ static forRoot(options) {
19
+ return {
20
+ module: SchedulerDashModule_1,
21
+ providers: [
22
+ { provide: scheduler_dash_options_1.SCHEDULER_DASH_OPTIONS, useValue: options },
23
+ { provide: scheduler_dash_options_1.SCHEDULER_DASH_STORAGE, useValue: options.storage },
24
+ ],
25
+ };
26
+ }
27
+ };
28
+ exports.SchedulerDashModule = SchedulerDashModule;
29
+ exports.SchedulerDashModule = SchedulerDashModule = SchedulerDashModule_1 = __decorate([
30
+ (0, common_1.Module)({
31
+ controllers: [jobs_controller_1.JobsController],
32
+ providers: [
33
+ basic_auth_guard_1.BasicAuthGuard,
34
+ jobs_service_1.JobsService,
35
+ jobs_tracker_service_1.JobsTrackerService,
36
+ ],
37
+ exports: [jobs_service_1.JobsService],
38
+ })
39
+ ], SchedulerDashModule);
@@ -0,0 +1,13 @@
1
+ import { Storage } from './storage/storage.abstract';
2
+ export interface SchedulerDashAuth {
3
+ username: string;
4
+ password: string;
5
+ }
6
+ export interface SchedulerDashOptions {
7
+ storage?: Storage;
8
+ basePath?: string;
9
+ port?: number;
10
+ maxConcurrent?: number;
11
+ noOverlap?: boolean;
12
+ auth?: SchedulerDashAuth;
13
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { Storage } from './storage/storage.abstract';
3
+ export declare const SchedulerDashAuthSchema: z.ZodObject<{
4
+ username: z.ZodString;
5
+ password: z.ZodString;
6
+ }, z.core.$strip>;
7
+ export declare const SchedulerDashOptionsSchema: z.ZodObject<{
8
+ storage: z.ZodOptional<z.ZodCustom<Storage, Storage>>;
9
+ basePath: z.ZodOptional<z.ZodString>;
10
+ port: z.ZodOptional<z.ZodNumber>;
11
+ maxConcurrent: z.ZodOptional<z.ZodNumber>;
12
+ noOverlap: z.ZodOptional<z.ZodBoolean>;
13
+ auth: z.ZodOptional<z.ZodObject<{
14
+ username: z.ZodString;
15
+ password: z.ZodString;
16
+ }, z.core.$strip>>;
17
+ }, z.core.$strip>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SchedulerDashOptionsSchema = exports.SchedulerDashAuthSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const storage_abstract_1 = require("./storage/storage.abstract");
6
+ exports.SchedulerDashAuthSchema = zod_1.z.object({
7
+ username: zod_1.z.string().min(1, 'auth.username must not be empty'),
8
+ password: zod_1.z.string().min(1, 'auth.password must not be empty'),
9
+ });
10
+ exports.SchedulerDashOptionsSchema = zod_1.z.object({
11
+ storage: zod_1.z.instanceof(storage_abstract_1.Storage).optional(),
12
+ basePath: zod_1.z
13
+ .string()
14
+ .regex(/^[a-zA-Z0-9_\-/]+$/, 'basePath must contain only alphanumeric characters, hyphens, underscores, or slashes')
15
+ .optional(),
16
+ port: zod_1.z
17
+ .number()
18
+ .int('port must be an integer')
19
+ .min(1, 'port must be >= 1')
20
+ .max(65535, 'port must be <= 65535')
21
+ .optional(),
22
+ maxConcurrent: zod_1.z
23
+ .number()
24
+ .int('maxConcurrent must be an integer')
25
+ .min(1, 'maxConcurrent must be >= 1')
26
+ .optional(),
27
+ noOverlap: zod_1.z.boolean().optional(),
28
+ auth: exports.SchedulerDashAuthSchema.optional(),
29
+ });
@@ -0,0 +1,5 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+ import { Logger } from '@nestjs/common';
3
+ import { JobsService } from './jobs.service';
4
+ import { SchedulerDashAuth } from './scheduler-dash.options';
5
+ export declare function startStandaloneServer(port: number, basePath: string, jobsService: JobsService, auth: SchedulerDashAuth | undefined, logger: Logger): import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startStandaloneServer = startStandaloneServer;
4
+ const http_1 = require("http");
5
+ const auth_1 = require("./auth");
6
+ const dashboard_1 = require("./ui/dashboard");
7
+ const PLACEHOLDER = '__SCHEDULER_BASE_PLACEHOLDER__';
8
+ function renderHtml(base) {
9
+ return dashboard_1.dashboardHtml.replace(PLACEHOLDER, base);
10
+ }
11
+ function sendHtml(res, base) {
12
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
13
+ res.end(renderHtml(base));
14
+ }
15
+ function sendJson(res, status, body) {
16
+ res.writeHead(status, { 'Content-Type': 'application/json' });
17
+ res.end(JSON.stringify(body));
18
+ }
19
+ function handleGetApi(res, jobsService) {
20
+ sendJson(res, 200, jobsService.getJobs());
21
+ }
22
+ function handleTrigger(res, jobsService, encodedName) {
23
+ const name = decodeURIComponent(encodedName);
24
+ const ok = jobsService.triggerJob(name);
25
+ sendJson(res, ok ? 200 : 404, ok ? { triggered: name } : { message: `Job "${name}" not found` });
26
+ }
27
+ function handleNotFound(res) {
28
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
29
+ res.end('Not Found');
30
+ }
31
+ function handleStopExecution(res, jobsService, encodedId) {
32
+ const id = decodeURIComponent(encodedId);
33
+ const ok = jobsService.stopExecution(id);
34
+ sendJson(res, ok ? 200 : 404, ok ? { stopped: id } : { message: `Execution "${id}" not found or already finished` });
35
+ }
36
+ function createRequestHandler(base, jobsService, auth) {
37
+ const triggerRe = new RegExp(`^${base}/api/([^/]+)/trigger$`);
38
+ const stopExecutionRe = new RegExp(`^${base}/api/executions/([^/]+)/stop$`);
39
+ return (req, res) => {
40
+ const url = req.url ?? '/';
41
+ const method = req.method ?? 'GET';
42
+ if (auth && !(0, auth_1.checkBasicAuth)(req, auth)) {
43
+ return (0, auth_1.rejectUnauthorized)(res);
44
+ }
45
+ if (method === 'GET' && (url === base || url === `${base}/` || url.startsWith(`${base}/jobs/`))) {
46
+ return sendHtml(res, base);
47
+ }
48
+ if (method === 'GET' && url === `${base}/api`) {
49
+ return handleGetApi(res, jobsService);
50
+ }
51
+ const triggerMatch = url.match(triggerRe);
52
+ if (method === 'POST' && triggerMatch) {
53
+ return handleTrigger(res, jobsService, triggerMatch[1]);
54
+ }
55
+ const stopExecutionMatch = url.match(stopExecutionRe);
56
+ if (method === 'POST' && stopExecutionMatch) {
57
+ return handleStopExecution(res, jobsService, stopExecutionMatch[1]);
58
+ }
59
+ handleNotFound(res);
60
+ };
61
+ }
62
+ function startStandaloneServer(port, basePath, jobsService, auth, logger) {
63
+ const base = `/${basePath}`;
64
+ const handler = createRequestHandler(base, jobsService, auth);
65
+ const server = (0, http_1.createServer)(handler);
66
+ server.listen(port, () => {
67
+ logger.log(`Dashboard running at http://localhost:${port}${base}`);
68
+ });
69
+ return server;
70
+ }
@@ -0,0 +1,8 @@
1
+ export interface JobExecution {
2
+ id: string;
3
+ jobName: string;
4
+ startedAt: Date;
5
+ finishedAt: Date | null;
6
+ status: 'running' | 'queued' | 'completed' | 'failed' | 'stopped';
7
+ error?: string;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ export interface JobMetrics {
2
+ totalRuns: number;
3
+ failedRuns: number;
4
+ avgDurationMs: number;
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,16 @@
1
+ import { Storage, IStorageOptions } from './storage.abstract';
2
+ import { JobExecution } from './job-execution.interface';
3
+ import { JobMetrics } from './job-metrics.interface';
4
+ export declare class MemoryStorage extends Storage {
5
+ private readonly store;
6
+ private readonly metricsStore;
7
+ constructor(options?: IStorageOptions);
8
+ save(execution: JobExecution): void;
9
+ update(id: string, data: Partial<Pick<JobExecution, 'finishedAt' | 'status' | 'error'>>): void;
10
+ findByJob(jobName: string): JobExecution[];
11
+ findAll(): Record<string, JobExecution[]>;
12
+ getMetrics(jobName: string): JobMetrics;
13
+ getAllMetrics(): Record<string, JobMetrics>;
14
+ private recordMetric;
15
+ private toJobMetrics;
16
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryStorage = void 0;
4
+ const storage_abstract_1 = require("./storage.abstract");
5
+ class MemoryStorage extends storage_abstract_1.Storage {
6
+ constructor(options = {}) {
7
+ super(options);
8
+ this.store = new Map();
9
+ this.metricsStore = new Map();
10
+ }
11
+ save(execution) {
12
+ const history = this.store.get(execution.jobName) ?? [];
13
+ history.push(execution);
14
+ if (this._options.historyRetention !== undefined && history.length > this._options.historyRetention) {
15
+ history.shift();
16
+ }
17
+ this.store.set(execution.jobName, history);
18
+ }
19
+ update(id, data) {
20
+ for (const [jobName, history] of this.store.entries()) {
21
+ const execution = history.find((e) => e.id === id);
22
+ if (!execution)
23
+ continue;
24
+ const wasFinished = execution.finishedAt !== null;
25
+ Object.assign(execution, data);
26
+ if (!wasFinished && data.finishedAt && data.status) {
27
+ this.recordMetric(jobName, execution, data.status);
28
+ }
29
+ return;
30
+ }
31
+ }
32
+ findByJob(jobName) {
33
+ return this.store.get(jobName) ?? [];
34
+ }
35
+ findAll() {
36
+ return Object.fromEntries(this.store.entries());
37
+ }
38
+ getMetrics(jobName) {
39
+ return this.toJobMetrics(this.metricsStore.get(jobName));
40
+ }
41
+ getAllMetrics() {
42
+ const result = {};
43
+ for (const [jobName, acc] of this.metricsStore.entries()) {
44
+ result[jobName] = this.toJobMetrics(acc);
45
+ }
46
+ return result;
47
+ }
48
+ recordMetric(jobName, execution, status) {
49
+ const acc = this.metricsStore.get(jobName) ?? { totalRuns: 0, failedRuns: 0, durationSum: 0, durationCount: 0 };
50
+ acc.totalRuns++;
51
+ if (status === 'failed') {
52
+ acc.failedRuns++;
53
+ }
54
+ if (execution.finishedAt) {
55
+ const duration = execution.finishedAt.getTime() - execution.startedAt.getTime();
56
+ acc.durationSum += duration;
57
+ acc.durationCount++;
58
+ }
59
+ this.metricsStore.set(jobName, acc);
60
+ }
61
+ toJobMetrics(acc) {
62
+ if (!acc)
63
+ return { totalRuns: 0, failedRuns: 0, avgDurationMs: 0 };
64
+ return {
65
+ totalRuns: acc.totalRuns,
66
+ failedRuns: acc.failedRuns,
67
+ avgDurationMs: acc.durationCount > 0 ? Math.round(acc.durationSum / acc.durationCount) : 0,
68
+ };
69
+ }
70
+ }
71
+ exports.MemoryStorage = MemoryStorage;
@@ -0,0 +1,15 @@
1
+ import { JobExecution } from './job-execution.interface';
2
+ import { JobMetrics } from './job-metrics.interface';
3
+ export interface IStorageOptions {
4
+ historyRetention?: number;
5
+ }
6
+ export declare abstract class Storage {
7
+ protected readonly _options: IStorageOptions;
8
+ constructor(_options?: IStorageOptions);
9
+ abstract save(execution: JobExecution): void;
10
+ abstract update(id: string, data: Partial<Pick<JobExecution, 'finishedAt' | 'status' | 'error'>>): void;
11
+ abstract findByJob(jobName: string): JobExecution[];
12
+ abstract findAll(): Record<string, JobExecution[]>;
13
+ abstract getMetrics(jobName: string): JobMetrics;
14
+ abstract getAllMetrics(): Record<string, JobMetrics>;
15
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Storage = void 0;
4
+ class Storage {
5
+ constructor(_options = {}) {
6
+ this._options = _options;
7
+ }
8
+ }
9
+ exports.Storage = Storage;