rl-core-api 0.7.0 → 0.8.0
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/README.md +54 -0
- package/dist/core/config/env.schema.d.ts +3 -0
- package/dist/core/config/env.schema.js +3 -0
- package/dist/core/core.module.js +3 -1
- package/dist/core/events/appEvent.enum.d.ts +6 -0
- package/dist/core/events/appEvent.enum.js +1 -0
- package/dist/core/health/health.controller.d.ts +4 -1
- package/dist/core/health/health.controller.js +9 -3
- package/dist/core/queue/bullConnection.module.d.ts +2 -0
- package/dist/core/queue/bullConnection.module.js +27 -0
- package/dist/core/queue/job.interface.d.ts +70 -0
- package/dist/core/queue/job.interface.js +2 -0
- package/dist/core/queue/job.producer.d.ts +15 -0
- package/dist/core/queue/job.producer.js +124 -0
- package/dist/core/queue/jobEvents.interface.d.ts +4 -0
- package/dist/core/queue/jobEvents.interface.js +4 -0
- package/dist/core/queue/jobEvents.service.d.ts +12 -0
- package/dist/core/queue/jobEvents.service.js +53 -0
- package/dist/core/queue/jobStatus.controller.d.ts +8 -0
- package/dist/core/queue/jobStatus.controller.js +44 -0
- package/dist/core/queue/jobStatus.response.d.ts +20 -0
- package/dist/core/queue/jobStatus.response.js +84 -0
- package/dist/core/queue/jobWorkerHost.d.ts +17 -0
- package/dist/core/queue/jobWorkerHost.js +72 -0
- package/dist/core/queue/queue.constants.d.ts +11 -0
- package/dist/core/queue/queue.constants.js +18 -0
- package/dist/core/queue/queue.module.d.ts +2 -0
- package/dist/core/queue/queue.module.js +38 -0
- package/dist/core/queue/queueAdmin.controller.d.ts +11 -0
- package/dist/core/queue/queueAdmin.controller.js +93 -0
- package/dist/core/queue/queueAdmin.response.d.ts +23 -0
- package/dist/core/queue/queueAdmin.response.js +87 -0
- package/dist/core/queue/queueAdmin.service.d.ts +15 -0
- package/dist/core/queue/queueAdmin.service.js +85 -0
- package/dist/core/queue/redisConnection.d.ts +2 -0
- package/dist/core/queue/redisConnection.js +11 -0
- package/dist/core/websocket/baseGateway.d.ts +1 -0
- package/dist/core/websocket/baseGateway.js +3 -0
- package/dist/features/audit/audit.module.js +1 -0
- package/dist/features/audit/presentation/index.d.ts +1 -0
- package/dist/features/audit/presentation/index.js +1 -0
- package/dist/features/audit/presentation/userPermissionsChanged.listener.d.ts +7 -0
- package/dist/features/audit/presentation/userPermissionsChanged.listener.js +43 -0
- package/dist/features/notifications/notifications.module.js +1 -1
- package/dist/features/notifications/presentation/domainEvents.listener.d.ts +2 -1
- package/dist/features/notifications/presentation/domainEvents.listener.js +19 -0
- package/dist/features/rbac/domain/rbac.catalog.js +15 -0
- package/dist/features/rbac/domain/rbac.service.d.ts +11 -1
- package/dist/features/rbac/domain/rbac.service.js +72 -2
- package/dist/features/rbac/presentation/commands/setUserPermissions.command.d.ts +3 -0
- package/dist/features/rbac/presentation/commands/setUserPermissions.command.js +24 -0
- package/dist/features/rbac/presentation/index.d.ts +1 -0
- package/dist/features/rbac/presentation/index.js +1 -0
- package/dist/features/rbac/presentation/rbac.controller.d.ts +5 -0
- package/dist/features/rbac/presentation/rbac.controller.js +57 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +33 -2
- package/dist/rlCore.module.js +8 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ migrations do core vêm no pacote — você nunca copia nem reescreve nenhuma.
|
|
|
61
61
|
| **Auditoria** | trilha de alterações de dados, log de requisições e log de erros, mantidos indefinidamente (sem retenção automática) |
|
|
62
62
|
| **Notificações** | WebSocket com handshake por cookie ou Bearer, escopo resolvido na leitura |
|
|
63
63
|
| **Listagens** | paginação, ordenação e filtro dinâmico com catálogo por listagem (`$AND`/`$OR` aninhados, 17 operadores) |
|
|
64
|
+
| **Fila** | processamento em segundo plano com progresso em tempo real (BullMQ + Redis), opcional por ambiente |
|
|
64
65
|
| **Infra** | config validada com Zod, mailer, agendador, rate-limit, CSRF, filtro global de exceções, logger Winston |
|
|
65
66
|
|
|
66
67
|
## Configuração
|
|
@@ -69,6 +70,59 @@ O pacote **não traz `.env`** — ele declara o que precisa e quebra no boot se
|
|
|
69
70
|
faltar. Cada projeto tem os valores dele (banco próprio, segredo JWT próprio).
|
|
70
71
|
Comece pelo `.env.example` do repositório.
|
|
71
72
|
|
|
73
|
+
### Fila de processamento (opcional)
|
|
74
|
+
|
|
75
|
+
Sem `REDIS_HOST` a aplicação sobe **sem fila e sem erro** — é o padrão.
|
|
76
|
+
Declarado, o Redis passa a ser obrigatório: a subida falha se ele não responder,
|
|
77
|
+
em vez de a falha aparecer no primeiro job.
|
|
78
|
+
|
|
79
|
+
```env
|
|
80
|
+
REDIS_HOST=localhost
|
|
81
|
+
REDIS_PORT=6379
|
|
82
|
+
REDIS_PASSWORD=redis
|
|
83
|
+
REDIS_PREFIX=controlx # um Redis para vários projetos: o prefixo os separa
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Concorrência, tentativas, backoff e retenção não são variáveis de ambiente —
|
|
87
|
+
são constantes em `queue.constants.ts`. São decisão de arquitetura da fila, e um
|
|
88
|
+
número desses ajustado no `.env` de produção sem ninguém revisar é como se
|
|
89
|
+
descobre, tarde, por que os jobs empilharam.
|
|
90
|
+
|
|
91
|
+
O processador é sempre do projeto — o core não conhece o que a feature faz:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
@Processor(JOBS_QUEUE, { concurrency: JOB_CONCURRENCY, autorun: false })
|
|
95
|
+
export class FunkosImportProcessor extends JobWorkerHost<Payload, Summary> {
|
|
96
|
+
constructor(
|
|
97
|
+
jobEvents: JobEventsService,
|
|
98
|
+
private readonly imports: FunkosImportService,
|
|
99
|
+
) {
|
|
100
|
+
super(jobEvents);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async handle(job: JobContext<Payload>): Promise<Summary> {
|
|
104
|
+
return this.imports.run(job.payload, (done, total) => job.report(done, total));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Registre a classe como provider do módulo da feature, **atrás do
|
|
110
|
+
`isRedisConfigured()`** — sem Redis não há conexão para o worker abrir:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
@Module({ providers: isRedisConfigured() ? [FunkosImportProcessor] : [] })
|
|
114
|
+
export class FunkosModule {}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Na rota, `jobs.enqueue(name, payload, { ownerId })` devolve o `jobId` na hora. O
|
|
118
|
+
progresso vai para a sala do dono pela **mesma** conexão de socket das
|
|
119
|
+
notificações (`job:progress`, `job:completed`, `job:failed`), e `GET /jobs/:id`
|
|
120
|
+
responde a quem perdeu o socket.
|
|
121
|
+
|
|
122
|
+
Quem opera vê tudo pela tela de **Filas** (`QueuesScreen`, no `rl-core-front`):
|
|
123
|
+
contagem por estado, o motivo cru da falha, reprocessar e remover — com
|
|
124
|
+
`queues:read:any` e `queues:manage:any`.
|
|
125
|
+
|
|
72
126
|
### Identidade do sistema
|
|
73
127
|
|
|
74
128
|
O core não tem nome próprio. `APP_NAME` é o do projeto, e dele saem o título do
|
|
@@ -57,6 +57,9 @@ export declare const envSchema: z.ZodObject<{
|
|
|
57
57
|
PASSWORD_RESET_TOKEN_EXPIRY: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
58
58
|
FIRST_ACCESS_TOKEN_EXPIRY: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
59
59
|
PASSWORD_MAX_AGE_DAYS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
60
|
+
REDIS_HOST: z.ZodOptional<z.ZodString>;
|
|
61
|
+
REDIS_PORT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
62
|
+
REDIS_PASSWORD: z.ZodOptional<z.ZodString>;
|
|
60
63
|
TOKEN_CLEANUP_CRON: z.ZodDefault<z.ZodString>;
|
|
61
64
|
AUDIT_SLOW_REQUEST_MS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
62
65
|
AUDIT_IP_SALT: z.ZodOptional<z.ZodString>;
|
|
@@ -68,6 +68,9 @@ exports.envSchema = zod_1.z
|
|
|
68
68
|
.positive()
|
|
69
69
|
.default(1440),
|
|
70
70
|
PASSWORD_MAX_AGE_DAYS: zod_1.z.coerce.number().int().positive().default(90),
|
|
71
|
+
REDIS_HOST: zod_1.z.string().min(1).optional(),
|
|
72
|
+
REDIS_PORT: zod_1.z.coerce.number().int().positive().default(6379),
|
|
73
|
+
REDIS_PASSWORD: zod_1.z.string().optional(),
|
|
71
74
|
TOKEN_CLEANUP_CRON: zod_1.z.string().default("0 */6 * * *"),
|
|
72
75
|
AUDIT_SLOW_REQUEST_MS: zod_1.z.coerce.number().int().positive().default(1000),
|
|
73
76
|
AUDIT_IP_SALT: zod_1.z.string().optional(),
|
package/dist/core/core.module.js
CHANGED
|
@@ -21,6 +21,7 @@ const config_module_1 = require("./config/config.module");
|
|
|
21
21
|
const env_service_1 = require("./config/env.service");
|
|
22
22
|
const health_controller_1 = require("./health/health.controller");
|
|
23
23
|
const mailer_module_1 = require("./mailer/mailer.module");
|
|
24
|
+
const queue_module_1 = require("./queue/queue.module");
|
|
24
25
|
let CoreModule = CoreModule_1 = class CoreModule {
|
|
25
26
|
static forRoot(options) {
|
|
26
27
|
return {
|
|
@@ -28,6 +29,7 @@ let CoreModule = CoreModule_1 = class CoreModule {
|
|
|
28
29
|
imports: [
|
|
29
30
|
config_module_1.ConfigModule.forRoot({ validateEnv: options.validateEnv }),
|
|
30
31
|
mailer_module_1.MailerModule,
|
|
32
|
+
queue_module_1.QueueModule,
|
|
31
33
|
schedule_1.ScheduleModule.forRoot(),
|
|
32
34
|
event_emitter_1.EventEmitterModule.forRoot(),
|
|
33
35
|
typeorm_1.TypeOrmModule.forRootAsync({
|
|
@@ -63,7 +65,7 @@ let CoreModule = CoreModule_1 = class CoreModule {
|
|
|
63
65
|
{ provide: core_1.APP_GUARD, useClass: csrf_guard_1.CsrfGuard },
|
|
64
66
|
{ provide: core_1.APP_FILTER, useClass: allExceptions_filter_1.AllExceptionsFilter },
|
|
65
67
|
],
|
|
66
|
-
exports: [mailer_module_1.MailerModule],
|
|
68
|
+
exports: [mailer_module_1.MailerModule, queue_module_1.QueueModule],
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
};
|
|
@@ -2,6 +2,7 @@ export declare enum AppEvent {
|
|
|
2
2
|
USER_LOCKED = "user.locked",
|
|
3
3
|
USER_TWO_FACTOR_RESET = "user.two-factor-reset",
|
|
4
4
|
USER_ROLES_CHANGED = "user.roles-changed",
|
|
5
|
+
USER_PERMISSIONS_CHANGED = "user.permissions-changed",
|
|
5
6
|
USER_PASSWORD_RESET_REQUESTED = "user.password-reset-requested",
|
|
6
7
|
USER_PASSWORD_EXPIRING = "user.password-expiring",
|
|
7
8
|
SYSTEM_CLEANUP_FAILED = "system.cleanup-failed",
|
|
@@ -20,6 +21,11 @@ export interface UserRolesChangedEvent {
|
|
|
20
21
|
roleNames: string[];
|
|
21
22
|
previousRoleNames: string[];
|
|
22
23
|
}
|
|
24
|
+
export interface UserPermissionsChangedEvent {
|
|
25
|
+
userId: string;
|
|
26
|
+
codes: string[];
|
|
27
|
+
previousCodes: string[];
|
|
28
|
+
}
|
|
23
29
|
export interface UserPasswordResetRequestedEvent {
|
|
24
30
|
userId: string;
|
|
25
31
|
}
|
|
@@ -6,6 +6,7 @@ var AppEvent;
|
|
|
6
6
|
AppEvent["USER_LOCKED"] = "user.locked";
|
|
7
7
|
AppEvent["USER_TWO_FACTOR_RESET"] = "user.two-factor-reset";
|
|
8
8
|
AppEvent["USER_ROLES_CHANGED"] = "user.roles-changed";
|
|
9
|
+
AppEvent["USER_PERMISSIONS_CHANGED"] = "user.permissions-changed";
|
|
9
10
|
AppEvent["USER_PASSWORD_RESET_REQUESTED"] = "user.password-reset-requested";
|
|
10
11
|
AppEvent["USER_PASSWORD_EXPIRING"] = "user.password-expiring";
|
|
11
12
|
AppEvent["SYSTEM_CLEANUP_FAILED"] = "system.cleanup-failed";
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { DataSource } from "typeorm";
|
|
2
|
+
import { JobProducer } from "../queue/job.producer";
|
|
2
3
|
export declare class HealthController {
|
|
3
4
|
private readonly ds;
|
|
5
|
+
private readonly jobs;
|
|
4
6
|
private readonly logger;
|
|
5
|
-
constructor(ds: DataSource);
|
|
7
|
+
constructor(ds: DataSource, jobs: JobProducer);
|
|
6
8
|
check(): Promise<{
|
|
7
9
|
status: string;
|
|
8
10
|
database: string;
|
|
11
|
+
queue: string;
|
|
9
12
|
timestamp: string;
|
|
10
13
|
}>;
|
|
11
14
|
}
|
|
@@ -19,9 +19,11 @@ const swagger_1 = require("@nestjs/swagger");
|
|
|
19
19
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
20
20
|
const typeorm_2 = require("typeorm");
|
|
21
21
|
const public_decorator_1 = require("../auth/decorators/public.decorator");
|
|
22
|
+
const job_producer_1 = require("../queue/job.producer");
|
|
22
23
|
let HealthController = HealthController_1 = class HealthController {
|
|
23
|
-
constructor(ds) {
|
|
24
|
+
constructor(ds, jobs) {
|
|
24
25
|
this.ds = ds;
|
|
26
|
+
this.jobs = jobs;
|
|
25
27
|
this.logger = new common_1.Logger(HealthController_1.name);
|
|
26
28
|
}
|
|
27
29
|
async check() {
|
|
@@ -33,9 +35,12 @@ let HealthController = HealthController_1 = class HealthController {
|
|
|
33
35
|
this.logger.error(`Healthcheck do banco falhou: ${err.message}`);
|
|
34
36
|
database = "down";
|
|
35
37
|
}
|
|
38
|
+
const queue = await this.jobs.redisStatus();
|
|
39
|
+
const healthy = database === "up" && queue !== "down";
|
|
36
40
|
return {
|
|
37
|
-
status:
|
|
41
|
+
status: healthy ? "ok" : "degraded",
|
|
38
42
|
database,
|
|
43
|
+
queue,
|
|
39
44
|
timestamp: new Date().toISOString(),
|
|
40
45
|
};
|
|
41
46
|
}
|
|
@@ -52,5 +57,6 @@ exports.HealthController = HealthController = HealthController_1 = __decorate([
|
|
|
52
57
|
(0, swagger_1.ApiTags)("Health"),
|
|
53
58
|
(0, common_1.Controller)({ path: "health", version: "1" }),
|
|
54
59
|
__param(0, (0, typeorm_1.InjectDataSource)()),
|
|
55
|
-
__metadata("design:paramtypes", [typeorm_2.DataSource
|
|
60
|
+
__metadata("design:paramtypes", [typeorm_2.DataSource,
|
|
61
|
+
job_producer_1.JobProducer])
|
|
56
62
|
], HealthController);
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.BullConnectionModule = void 0;
|
|
10
|
+
const bullmq_1 = require("@nestjs/bullmq");
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const queue_constants_1 = require("./queue.constants");
|
|
13
|
+
const redisConnection_1 = require("./redisConnection");
|
|
14
|
+
let BullConnectionModule = class BullConnectionModule {
|
|
15
|
+
};
|
|
16
|
+
exports.BullConnectionModule = BullConnectionModule;
|
|
17
|
+
exports.BullConnectionModule = BullConnectionModule = __decorate([
|
|
18
|
+
(0, common_1.Module)({
|
|
19
|
+
imports: [
|
|
20
|
+
bullmq_1.BullModule.forRoot({
|
|
21
|
+
connection: (0, redisConnection_1.buildRedisOptions)(),
|
|
22
|
+
prefix: (0, queue_constants_1.queuePrefix)(),
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
exports: [bullmq_1.BullModule],
|
|
26
|
+
})
|
|
27
|
+
], BullConnectionModule);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type JobState = "waiting" | "active" | "completed" | "failed" | "delayed" | "unknown";
|
|
2
|
+
export interface JobRef {
|
|
3
|
+
jobId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface JobProgress {
|
|
7
|
+
jobId: string;
|
|
8
|
+
name: string;
|
|
9
|
+
processed: number;
|
|
10
|
+
total: number;
|
|
11
|
+
percent: number;
|
|
12
|
+
message?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface JobCompleted<TSummary = unknown> {
|
|
15
|
+
jobId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
summary: TSummary;
|
|
18
|
+
}
|
|
19
|
+
export interface JobFailed {
|
|
20
|
+
jobId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
errorCode: string | null;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
export interface JobStatus<TSummary = unknown> {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
state: JobState;
|
|
29
|
+
progress: Omit<JobProgress, "jobId" | "name"> | null;
|
|
30
|
+
summary: TSummary | null;
|
|
31
|
+
error: {
|
|
32
|
+
code: string | null;
|
|
33
|
+
message: string;
|
|
34
|
+
} | null;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
finishedAt: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface EnqueueOptions {
|
|
39
|
+
ownerId: string;
|
|
40
|
+
jobId?: string;
|
|
41
|
+
attempts?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface QueueJobSummary {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
state: JobState;
|
|
47
|
+
ownerId: string | null;
|
|
48
|
+
attemptsMade: number;
|
|
49
|
+
percent: number | null;
|
|
50
|
+
failedReason: string | null;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
finishedAt: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface QueueCounts {
|
|
55
|
+
waiting: number;
|
|
56
|
+
active: number;
|
|
57
|
+
completed: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
delayed: number;
|
|
60
|
+
}
|
|
61
|
+
export interface JobContext<TPayload = unknown> {
|
|
62
|
+
jobId: string;
|
|
63
|
+
ownerId: string;
|
|
64
|
+
payload: TPayload;
|
|
65
|
+
report: (processed: number, total: number, message?: string) => Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export interface JobEnvelope<TPayload = unknown> {
|
|
68
|
+
ownerId: string;
|
|
69
|
+
payload: TPayload;
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OnApplicationBootstrap } from "@nestjs/common";
|
|
2
|
+
import { Queue } from "bullmq";
|
|
3
|
+
import { EnqueueOptions, JobEnvelope, JobRef, JobStatus } from "./job.interface";
|
|
4
|
+
export declare class JobProducer implements OnApplicationBootstrap {
|
|
5
|
+
private readonly queue?;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
constructor(queue?: Queue<JobEnvelope> | undefined);
|
|
8
|
+
onApplicationBootstrap(): Promise<void>;
|
|
9
|
+
isAvailable(): boolean;
|
|
10
|
+
enqueue<TPayload>(name: string, payload: TPayload, options: EnqueueOptions): Promise<JobRef>;
|
|
11
|
+
status<TSummary>(jobId: string, ownerId: string): Promise<JobStatus<TSummary>>;
|
|
12
|
+
redisStatus(): Promise<"up" | "down" | "off">;
|
|
13
|
+
requireQueue(): Queue<JobEnvelope>;
|
|
14
|
+
private assertRedisReachable;
|
|
15
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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 __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
var JobProducer_1;
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.JobProducer = void 0;
|
|
20
|
+
const bullmq_1 = require("@nestjs/bullmq");
|
|
21
|
+
const common_1 = require("@nestjs/common");
|
|
22
|
+
const bullmq_2 = require("bullmq");
|
|
23
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
24
|
+
const queue_constants_1 = require("./queue.constants");
|
|
25
|
+
const redisConnection_1 = require("./redisConnection");
|
|
26
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
27
|
+
const HEALTH_TIMEOUT_MS = 1500;
|
|
28
|
+
const isStoredProgress = (value) => typeof value === "object" && value !== null && "percent" in value;
|
|
29
|
+
let JobProducer = JobProducer_1 = class JobProducer {
|
|
30
|
+
constructor(queue) {
|
|
31
|
+
this.queue = queue;
|
|
32
|
+
this.logger = new common_1.Logger(JobProducer_1.name);
|
|
33
|
+
}
|
|
34
|
+
async onApplicationBootstrap() {
|
|
35
|
+
if (!(0, queue_constants_1.isRedisConfigured)()) {
|
|
36
|
+
this.logger.log("Fila desligada: REDIS_HOST não declarado");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await this.assertRedisReachable();
|
|
40
|
+
}
|
|
41
|
+
isAvailable() {
|
|
42
|
+
return Boolean(this.queue);
|
|
43
|
+
}
|
|
44
|
+
async enqueue(name, payload, options) {
|
|
45
|
+
const queue = this.requireQueue();
|
|
46
|
+
const envelope = {
|
|
47
|
+
ownerId: options.ownerId,
|
|
48
|
+
payload,
|
|
49
|
+
};
|
|
50
|
+
const job = await queue.add(name, envelope, {
|
|
51
|
+
jobId: options.jobId,
|
|
52
|
+
attempts: options.attempts ?? queue_constants_1.JOB_ATTEMPTS,
|
|
53
|
+
backoff: { type: "exponential", delay: queue_constants_1.JOB_BACKOFF_DELAY_MS },
|
|
54
|
+
removeOnComplete: { age: queue_constants_1.COMPLETED_RETENTION_SECONDS, count: 1000 },
|
|
55
|
+
removeOnFail: { age: queue_constants_1.FAILED_RETENTION_SECONDS },
|
|
56
|
+
});
|
|
57
|
+
return { jobId: String(job.id), name };
|
|
58
|
+
}
|
|
59
|
+
async status(jobId, ownerId) {
|
|
60
|
+
const queue = this.requireQueue();
|
|
61
|
+
const job = await queue.getJob(jobId);
|
|
62
|
+
if (!job || job.data?.ownerId !== ownerId) {
|
|
63
|
+
throw new common_1.NotFoundException("Job não encontrado");
|
|
64
|
+
}
|
|
65
|
+
const state = (await job.getState());
|
|
66
|
+
return {
|
|
67
|
+
id: String(job.id),
|
|
68
|
+
name: job.name,
|
|
69
|
+
state,
|
|
70
|
+
progress: isStoredProgress(job.progress) ? job.progress : null,
|
|
71
|
+
summary: job.returnvalue ?? null,
|
|
72
|
+
error: job.failedReason ? { code: null, message: job.failedReason } : null,
|
|
73
|
+
createdAt: new Date(job.timestamp).toISOString(),
|
|
74
|
+
finishedAt: job.finishedOn ? new Date(job.finishedOn).toISOString() : null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async redisStatus() {
|
|
78
|
+
if (!(0, queue_constants_1.isRedisConfigured)()) {
|
|
79
|
+
return "off";
|
|
80
|
+
}
|
|
81
|
+
if (!this.queue) {
|
|
82
|
+
return "down";
|
|
83
|
+
}
|
|
84
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve("down"), HEALTH_TIMEOUT_MS).unref());
|
|
85
|
+
return Promise.race([
|
|
86
|
+
this.queue
|
|
87
|
+
.getJobCounts("waiting")
|
|
88
|
+
.then(() => "up")
|
|
89
|
+
.catch(() => "down"),
|
|
90
|
+
timeout,
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
requireQueue() {
|
|
94
|
+
if (!this.queue) {
|
|
95
|
+
throw new common_1.ServiceUnavailableException("Processamento em segundo plano indisponível neste ambiente");
|
|
96
|
+
}
|
|
97
|
+
return this.queue;
|
|
98
|
+
}
|
|
99
|
+
async assertRedisReachable() {
|
|
100
|
+
const probe = new ioredis_1.default({
|
|
101
|
+
...(0, redisConnection_1.buildRedisOptions)(),
|
|
102
|
+
lazyConnect: true,
|
|
103
|
+
maxRetriesPerRequest: 1,
|
|
104
|
+
retryStrategy: () => null,
|
|
105
|
+
connectTimeout: PROBE_TIMEOUT_MS,
|
|
106
|
+
});
|
|
107
|
+
const failure = await probe
|
|
108
|
+
.connect()
|
|
109
|
+
.then(() => probe.ping())
|
|
110
|
+
.then(() => null)
|
|
111
|
+
.catch((err) => err);
|
|
112
|
+
probe.disconnect();
|
|
113
|
+
if (failure) {
|
|
114
|
+
throw new Error(`REDIS_HOST está declarado mas o Redis não respondeu: ${failure.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
exports.JobProducer = JobProducer;
|
|
119
|
+
exports.JobProducer = JobProducer = JobProducer_1 = __decorate([
|
|
120
|
+
(0, common_1.Injectable)(),
|
|
121
|
+
__param(0, (0, common_1.Optional)()),
|
|
122
|
+
__param(0, (0, bullmq_1.InjectQueue)(queue_constants_1.JOBS_QUEUE)),
|
|
123
|
+
__metadata("design:paramtypes", [bullmq_2.Queue])
|
|
124
|
+
], JobProducer);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ModuleRef } from "@nestjs/core";
|
|
2
|
+
export declare class JobEventsService {
|
|
3
|
+
private readonly moduleRef;
|
|
4
|
+
private readonly logger;
|
|
5
|
+
private publisher;
|
|
6
|
+
constructor(moduleRef: ModuleRef);
|
|
7
|
+
progress(ownerId: string, data: unknown): void;
|
|
8
|
+
completed(ownerId: string, data: unknown): void;
|
|
9
|
+
failed(ownerId: string, data: unknown): void;
|
|
10
|
+
private emit;
|
|
11
|
+
private resolve;
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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 __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var JobEventsService_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.JobEventsService = void 0;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const core_1 = require("@nestjs/core");
|
|
16
|
+
const jobEvents_interface_1 = require("./jobEvents.interface");
|
|
17
|
+
const queue_constants_1 = require("./queue.constants");
|
|
18
|
+
let JobEventsService = JobEventsService_1 = class JobEventsService {
|
|
19
|
+
constructor(moduleRef) {
|
|
20
|
+
this.moduleRef = moduleRef;
|
|
21
|
+
this.logger = new common_1.Logger(JobEventsService_1.name);
|
|
22
|
+
}
|
|
23
|
+
progress(ownerId, data) {
|
|
24
|
+
this.emit(ownerId, queue_constants_1.JOB_PROGRESS_EVENT, data);
|
|
25
|
+
}
|
|
26
|
+
completed(ownerId, data) {
|
|
27
|
+
this.emit(ownerId, queue_constants_1.JOB_COMPLETED_EVENT, data);
|
|
28
|
+
}
|
|
29
|
+
failed(ownerId, data) {
|
|
30
|
+
this.emit(ownerId, queue_constants_1.JOB_FAILED_EVENT, data);
|
|
31
|
+
}
|
|
32
|
+
emit(ownerId, event, data) {
|
|
33
|
+
this.resolve()?.emitToUser(ownerId, event, data);
|
|
34
|
+
}
|
|
35
|
+
resolve() {
|
|
36
|
+
if (this.publisher !== undefined) {
|
|
37
|
+
return this.publisher;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
this.publisher = this.moduleRef.get(jobEvents_interface_1.JOB_EVENTS_PUBLISHER, { strict: false });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
this.logger.warn("Fila sem canal de tempo real: o andamento só sai por GET /jobs/:id");
|
|
44
|
+
this.publisher = null;
|
|
45
|
+
}
|
|
46
|
+
return this.publisher;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
exports.JobEventsService = JobEventsService;
|
|
50
|
+
exports.JobEventsService = JobEventsService = JobEventsService_1 = __decorate([
|
|
51
|
+
(0, common_1.Injectable)(),
|
|
52
|
+
__metadata("design:paramtypes", [core_1.ModuleRef])
|
|
53
|
+
], JobEventsService);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AuthenticatedUser } from "../auth/authenticatedUser.interface";
|
|
2
|
+
import { JobStatus } from "./job.interface";
|
|
3
|
+
import { JobProducer } from "./job.producer";
|
|
4
|
+
export declare class JobStatusController {
|
|
5
|
+
private readonly jobs;
|
|
6
|
+
constructor(jobs: JobProducer);
|
|
7
|
+
status(id: string, actor: AuthenticatedUser): Promise<JobStatus>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.JobStatusController = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const swagger_1 = require("@nestjs/swagger");
|
|
18
|
+
const currentUser_decorator_1 = require("../auth/decorators/currentUser.decorator");
|
|
19
|
+
const job_producer_1 = require("./job.producer");
|
|
20
|
+
const jobStatus_response_1 = require("./jobStatus.response");
|
|
21
|
+
let JobStatusController = class JobStatusController {
|
|
22
|
+
constructor(jobs) {
|
|
23
|
+
this.jobs = jobs;
|
|
24
|
+
}
|
|
25
|
+
status(id, actor) {
|
|
26
|
+
return this.jobs.status(id, actor.id);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
exports.JobStatusController = JobStatusController;
|
|
30
|
+
__decorate([
|
|
31
|
+
(0, common_1.Get)(":id"),
|
|
32
|
+
(0, swagger_1.ApiOkResponse)({ type: jobStatus_response_1.JobStatusResponse }),
|
|
33
|
+
__param(0, (0, common_1.Param)("id")),
|
|
34
|
+
__param(1, (0, currentUser_decorator_1.CurrentUser)()),
|
|
35
|
+
__metadata("design:type", Function),
|
|
36
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
37
|
+
__metadata("design:returntype", Promise)
|
|
38
|
+
], JobStatusController.prototype, "status", null);
|
|
39
|
+
exports.JobStatusController = JobStatusController = __decorate([
|
|
40
|
+
(0, swagger_1.ApiTags)("Jobs"),
|
|
41
|
+
(0, swagger_1.ApiBearerAuth)(),
|
|
42
|
+
(0, common_1.Controller)({ path: "jobs", version: "1" }),
|
|
43
|
+
__metadata("design:paramtypes", [job_producer_1.JobProducer])
|
|
44
|
+
], JobStatusController);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class JobProgressResponse {
|
|
2
|
+
processed: number;
|
|
3
|
+
total: number;
|
|
4
|
+
percent: number;
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class JobErrorResponse {
|
|
8
|
+
code: string | null;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class JobStatusResponse {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
state: string;
|
|
15
|
+
progress: JobProgressResponse | null;
|
|
16
|
+
summary: unknown;
|
|
17
|
+
error: JobErrorResponse | null;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
finishedAt: string | null;
|
|
20
|
+
}
|