rl-core-api 0.19.7 → 0.19.9
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 +17 -0
- package/dist/core/mailer/mailTemplate.service.d.ts +10 -0
- package/dist/core/mailer/mailTemplate.service.js +79 -0
- package/dist/core/mailer/mailer.module.js +2 -1
- package/dist/core/mailer/mailer.service.d.ts +3 -3
- package/dist/core/mailer/mailer.service.js +34 -65
- package/dist/core/mailer/templates/firstAccess.hbs +9 -0
- package/dist/core/mailer/templates/header.hbs +12 -0
- package/dist/core/mailer/templates/layout.hbs +18 -0
- package/dist/core/mailer/templates/passwordReset.hbs +9 -0
- package/dist/core/mailer/templates/twoFactorCode.hbs +9 -0
- package/dist/core/queue/job.interface.d.ts +13 -0
- package/dist/core/queue/queue.constants.d.ts +4 -0
- package/dist/core/queue/queue.constants.js +5 -1
- package/dist/core/queue/queueAdmin.controller.d.ts +2 -2
- package/dist/core/queue/queueAdmin.controller.js +5 -4
- package/dist/core/queue/queueAdmin.response.d.ts +16 -1
- package/dist/core/queue/queueAdmin.response.js +52 -8
- package/dist/core/queue/queueAdmin.service.d.ts +6 -5
- package/dist/core/queue/queueAdmin.service.js +56 -18
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -139,6 +139,23 @@ Swagger, o remetente dos emails e o emissor exibido no app autenticador:
|
|
|
139
139
|
Quem já declarava `TOTP_ISSUER` ou `EMAIL_FROM_NAME` continua igual: eles só
|
|
140
140
|
caem no `APP_NAME` quando estão ausentes.
|
|
141
141
|
|
|
142
|
+
### Os emails
|
|
143
|
+
|
|
144
|
+
O HTML das três mensagens do core — código 2FA, redefinição de senha e
|
|
145
|
+
primeiro acesso — mora em `src/core/mailer/templates/`, um arquivo `.hbs` por
|
|
146
|
+
mensagem, mais o `layout.hbs` (cabeçalho, título e rodapé) e o `header.hbs`
|
|
147
|
+
(logo ou nome). O `MailerService` só monta os dados e chama o
|
|
148
|
+
`MailTemplateService`, que compila e guarda o template compilado.
|
|
149
|
+
|
|
150
|
+
Handlebars escapa o que interpola, então nome de usuário entra no email sem
|
|
151
|
+
ninguém precisar lembrar de escapar. O que é HTML montado pelo próprio serviço
|
|
152
|
+
— o corpo dentro da moldura — passa por `raw()`, que é a única porta de
|
|
153
|
+
entrada sem escape.
|
|
154
|
+
|
|
155
|
+
Os `.hbs` viajam no pacote publicado (`assets` do `nest-cli.json` os copia
|
|
156
|
+
para o `dist`) e são resolvidos por `__dirname`, então valem igual em
|
|
157
|
+
desenvolvimento e instalados em `node_modules`.
|
|
158
|
+
|
|
142
159
|
Para acrescentar variáveis suas, estenda o schema:
|
|
143
160
|
|
|
144
161
|
```ts
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Handlebars from "handlebars";
|
|
2
|
+
export type MailTemplateContext = Record<string, string | number | boolean | null | undefined | Handlebars.SafeString>;
|
|
3
|
+
export declare class MailTemplateService {
|
|
4
|
+
private readonly directory;
|
|
5
|
+
private readonly compiled;
|
|
6
|
+
render(name: string, context: MailTemplateContext): string;
|
|
7
|
+
raw(html: string): Handlebars.SafeString;
|
|
8
|
+
private templateOf;
|
|
9
|
+
private sourceOf;
|
|
10
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
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;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.MailTemplateService = void 0;
|
|
43
|
+
const node_fs_1 = require("node:fs");
|
|
44
|
+
const node_path_1 = require("node:path");
|
|
45
|
+
const common_1 = require("@nestjs/common");
|
|
46
|
+
const Handlebars = __importStar(require("handlebars"));
|
|
47
|
+
let MailTemplateService = class MailTemplateService {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.directory = (0, node_path_1.join)(__dirname, "templates");
|
|
50
|
+
this.compiled = new Map();
|
|
51
|
+
}
|
|
52
|
+
render(name, context) {
|
|
53
|
+
return this.templateOf(name)(context);
|
|
54
|
+
}
|
|
55
|
+
raw(html) {
|
|
56
|
+
return new Handlebars.SafeString(html);
|
|
57
|
+
}
|
|
58
|
+
templateOf(name) {
|
|
59
|
+
const cached = this.compiled.get(name);
|
|
60
|
+
if (cached) {
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
const template = Handlebars.compile(this.sourceOf(name));
|
|
64
|
+
this.compiled.set(name, template);
|
|
65
|
+
return template;
|
|
66
|
+
}
|
|
67
|
+
sourceOf(name) {
|
|
68
|
+
try {
|
|
69
|
+
return (0, node_fs_1.readFileSync)((0, node_path_1.join)(this.directory, `${name}.hbs`), "utf8");
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error(`Template de email não encontrado: ${name}.hbs`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
exports.MailTemplateService = MailTemplateService;
|
|
77
|
+
exports.MailTemplateService = MailTemplateService = __decorate([
|
|
78
|
+
(0, common_1.Injectable)()
|
|
79
|
+
], MailTemplateService);
|
|
@@ -9,13 +9,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.MailerModule = void 0;
|
|
10
10
|
const common_1 = require("@nestjs/common");
|
|
11
11
|
const mailer_service_1 = require("./mailer.service");
|
|
12
|
+
const mailTemplate_service_1 = require("./mailTemplate.service");
|
|
12
13
|
let MailerModule = class MailerModule {
|
|
13
14
|
};
|
|
14
15
|
exports.MailerModule = MailerModule;
|
|
15
16
|
exports.MailerModule = MailerModule = __decorate([
|
|
16
17
|
(0, common_1.Global)(),
|
|
17
18
|
(0, common_1.Module)({
|
|
18
|
-
providers: [mailer_service_1.MailerService],
|
|
19
|
+
providers: [mailTemplate_service_1.MailTemplateService, mailer_service_1.MailerService],
|
|
19
20
|
exports: [mailer_service_1.MailerService],
|
|
20
21
|
})
|
|
21
22
|
], MailerModule);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { EnvService } from "../config/env.service";
|
|
2
|
+
import { MailTemplateService } from "./mailTemplate.service";
|
|
2
3
|
export declare class MailerService {
|
|
3
4
|
private readonly env;
|
|
5
|
+
private readonly templates;
|
|
4
6
|
private readonly logger;
|
|
5
7
|
private readonly transporter;
|
|
6
|
-
constructor(env: EnvService);
|
|
8
|
+
constructor(env: EnvService, templates: MailTemplateService);
|
|
7
9
|
private send;
|
|
8
|
-
private escapeHtml;
|
|
9
|
-
private header;
|
|
10
10
|
private layout;
|
|
11
11
|
sendTwoFactorCode(to: string, code: string, userName: string): Promise<void>;
|
|
12
12
|
sendPasswordReset(to: string, token: string, userName: string): Promise<void>;
|
|
@@ -47,9 +47,11 @@ exports.MailerService = void 0;
|
|
|
47
47
|
const common_1 = require("@nestjs/common");
|
|
48
48
|
const nodemailer = __importStar(require("nodemailer"));
|
|
49
49
|
const env_service_1 = require("../config/env.service");
|
|
50
|
+
const mailTemplate_service_1 = require("./mailTemplate.service");
|
|
50
51
|
let MailerService = MailerService_1 = class MailerService {
|
|
51
|
-
constructor(env) {
|
|
52
|
+
constructor(env, templates) {
|
|
52
53
|
this.env = env;
|
|
54
|
+
this.templates = templates;
|
|
53
55
|
this.logger = new common_1.Logger(MailerService_1.name);
|
|
54
56
|
this.transporter = nodemailer.createTransport({
|
|
55
57
|
host: this.env.get("EMAIL_HOST"),
|
|
@@ -72,64 +74,35 @@ let MailerService = MailerService_1 = class MailerService {
|
|
|
72
74
|
throw error;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
|
-
escapeHtml(value) {
|
|
76
|
-
return value
|
|
77
|
-
.replace(/&/g, "&")
|
|
78
|
-
.replace(/</g, "<")
|
|
79
|
-
.replace(/>/g, ">")
|
|
80
|
-
.replace(/"/g, """)
|
|
81
|
-
.replace(/'/g, "'");
|
|
82
|
-
}
|
|
83
|
-
header(brand) {
|
|
84
|
-
if (brand.logoUrl) {
|
|
85
|
-
return `<img src="${brand.logoUrl}" alt="${this.escapeHtml(brand.name)}" style="max-height:40px;max-width:200px;">`;
|
|
86
|
-
}
|
|
87
|
-
return `<span style="font-size:20px;font-weight:bold;color:${brand.color};">${this.escapeHtml(brand.name)}</span>`;
|
|
88
|
-
}
|
|
89
77
|
layout(title, body) {
|
|
90
78
|
const brand = this.env.brand();
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
</p>
|
|
102
|
-
</div>`;
|
|
79
|
+
return this.templates.render("layout", {
|
|
80
|
+
title,
|
|
81
|
+
body: this.templates.raw(body),
|
|
82
|
+
header: this.templates.raw(this.templates.render("header", {
|
|
83
|
+
logoUrl: brand.logoUrl,
|
|
84
|
+
name: brand.name,
|
|
85
|
+
color: brand.color,
|
|
86
|
+
})),
|
|
87
|
+
brandName: brand.name,
|
|
88
|
+
});
|
|
103
89
|
}
|
|
104
90
|
async sendTwoFactorCode(to, code, userName) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<div style="background:#f5f5f5;padding:20px;border-radius:5px;display:inline-block;">
|
|
112
|
-
<h1 style="margin:0;font-size:36px;letter-spacing:8px;color:${color};">${code}</h1>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
<p><strong>Este código expira em ${expiry} minutos.</strong></p>
|
|
116
|
-
<p>Se você não tentou fazer login, proteja sua conta imediatamente.</p>`;
|
|
91
|
+
const body = this.templates.render("twoFactorCode", {
|
|
92
|
+
userName,
|
|
93
|
+
code,
|
|
94
|
+
color: this.env.brand().color,
|
|
95
|
+
expiry: this.env.get("TWO_FACTOR_EMAIL_CODE_EXPIRY"),
|
|
96
|
+
});
|
|
117
97
|
await this.send(to, "Seu código de verificação (2FA)", this.layout("Autenticação em duas etapas", body));
|
|
118
98
|
}
|
|
119
99
|
async sendPasswordReset(to, token, userName) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<div style="text-align:center;margin:30px 0;">
|
|
127
|
-
<a href="${url}" style="background:${color};color:#fff;padding:12px 30px;text-decoration:none;border-radius:5px;display:inline-block;">Redefinir senha</a>
|
|
128
|
-
</div>
|
|
129
|
-
<p>Ou copie e cole este link no navegador:</p>
|
|
130
|
-
<p style="word-break:break-all;color:#666;">${url}</p>
|
|
131
|
-
<p><strong>Este link expira em ${expiry} minutos.</strong></p>
|
|
132
|
-
<p>Se você não solicitou, ignore este email.</p>`;
|
|
100
|
+
const body = this.templates.render("passwordReset", {
|
|
101
|
+
userName,
|
|
102
|
+
url: this.templates.raw(`${this.env.get("APP_URL")}/reset-password?token=${token}`),
|
|
103
|
+
color: this.env.brand().color,
|
|
104
|
+
expiry: this.env.get("PASSWORD_RESET_TOKEN_EXPIRY"),
|
|
105
|
+
});
|
|
133
106
|
await this.send(to, "Redefinição de senha", this.layout("Redefinição de senha", body));
|
|
134
107
|
}
|
|
135
108
|
formatExpiry(minutes) {
|
|
@@ -140,24 +113,20 @@ let MailerService = MailerService_1 = class MailerService {
|
|
|
140
113
|
return hours === 1 ? "1 hora" : `${hours} horas`;
|
|
141
114
|
}
|
|
142
115
|
async sendFirstAccess(to, token, userName) {
|
|
143
|
-
const url = `${this.env.get("APP_URL")}/reset-password?token=${token}`;
|
|
144
|
-
const expiry = this.formatExpiry(this.env.get("FIRST_ACCESS_TOKEN_EXPIRY"));
|
|
145
116
|
const brand = this.env.brand();
|
|
146
|
-
const body =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<p style="word-break:break-all;color:#666;">${url}</p>
|
|
154
|
-
<p><strong>Este link expira em ${expiry}.</strong></p>
|
|
155
|
-
<p>No primeiro login você também precisará configurar a autenticação em duas etapas (2FA).</p>`;
|
|
117
|
+
const body = this.templates.render("firstAccess", {
|
|
118
|
+
userName,
|
|
119
|
+
brandName: brand.name,
|
|
120
|
+
url: this.templates.raw(`${this.env.get("APP_URL")}/reset-password?token=${token}`),
|
|
121
|
+
color: brand.color,
|
|
122
|
+
expiry: this.formatExpiry(this.env.get("FIRST_ACCESS_TOKEN_EXPIRY")),
|
|
123
|
+
});
|
|
156
124
|
await this.send(to, "Defina sua senha de primeiro acesso", this.layout("Primeiro acesso", body));
|
|
157
125
|
}
|
|
158
126
|
};
|
|
159
127
|
exports.MailerService = MailerService;
|
|
160
128
|
exports.MailerService = MailerService = MailerService_1 = __decorate([
|
|
161
129
|
(0, common_1.Injectable)(),
|
|
162
|
-
__metadata("design:paramtypes", [env_service_1.EnvService
|
|
130
|
+
__metadata("design:paramtypes", [env_service_1.EnvService,
|
|
131
|
+
mailTemplate_service_1.MailTemplateService])
|
|
163
132
|
], MailerService);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<p>Olá {{userName}},</p>
|
|
2
|
+
<p>Sua conta em {{brandName}} foi criada. Para acessar, defina sua senha clicando no botão abaixo:</p>
|
|
3
|
+
<div style="text-align:center;margin:30px 0;">
|
|
4
|
+
<a href="{{url}}" style="background:{{color}};color:#fff;padding:12px 30px;text-decoration:none;border-radius:5px;display:inline-block;">Definir minha senha</a>
|
|
5
|
+
</div>
|
|
6
|
+
<p>Ou copie e cole este link no navegador:</p>
|
|
7
|
+
<p style="word-break:break-all;color:#666;">{{url}}</p>
|
|
8
|
+
<p><strong>Este link expira em {{expiry}}.</strong></p>
|
|
9
|
+
<p>No primeiro login você também precisará configurar a autenticação em duas etapas (2FA).</p>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{{!--
|
|
2
|
+
O cabeçalho da marca: a logo quando houver uma, senão o nome.
|
|
3
|
+
|
|
4
|
+
A logo precisa ser URL absoluta e pública (`APP_LOGO_URL`) — o cliente de
|
|
5
|
+
email busca a imagem do servidor dele e não alcança os assets da interface.
|
|
6
|
+
O `alt` com o nome cobre quem bloqueia imagem por padrão, que é a maioria.
|
|
7
|
+
--}}
|
|
8
|
+
{{#if logoUrl}}
|
|
9
|
+
<img src="{{logoUrl}}" alt="{{name}}" style="max-height:40px;max-width:200px;">
|
|
10
|
+
{{else}}
|
|
11
|
+
<span style="font-size:20px;font-weight:bold;color:{{color}};">{{name}}</span>
|
|
12
|
+
{{/if}}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{{!--
|
|
2
|
+
A moldura de toda mensagem: cabeçalho da marca, título, corpo e rodapé.
|
|
3
|
+
|
|
4
|
+
`header` e `body` chegam como HTML pronto, marcado por `raw()` no serviço; o
|
|
5
|
+
resto é escapado pelo Handlebars, que é o que tira do serviço o trabalho de
|
|
6
|
+
lembrar do escape em cada nome de usuário interpolado.
|
|
7
|
+
--}}
|
|
8
|
+
<div style="font-family: Arial, sans-serif; padding: 20px; max-width: 600px; margin: 0 auto;">
|
|
9
|
+
<div style="padding-bottom:20px;border-bottom:1px solid #ddd;margin-bottom:24px;">
|
|
10
|
+
{{header}}
|
|
11
|
+
</div>
|
|
12
|
+
<h2 style="color: #333;">{{title}}</h2>
|
|
13
|
+
{{body}}
|
|
14
|
+
<hr style="border: none; border-top: 1px solid #ddd; margin: 30px 0;">
|
|
15
|
+
<p style="color: #999; font-size: 12px;">
|
|
16
|
+
Mensagem automática de {{brandName}}, não responda.
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<p>Olá {{userName}},</p>
|
|
2
|
+
<p>Recebemos uma solicitação para redefinir sua senha. Clique no botão abaixo:</p>
|
|
3
|
+
<div style="text-align:center;margin:30px 0;">
|
|
4
|
+
<a href="{{url}}" style="background:{{color}};color:#fff;padding:12px 30px;text-decoration:none;border-radius:5px;display:inline-block;">Redefinir senha</a>
|
|
5
|
+
</div>
|
|
6
|
+
<p>Ou copie e cole este link no navegador:</p>
|
|
7
|
+
<p style="word-break:break-all;color:#666;">{{url}}</p>
|
|
8
|
+
<p><strong>Este link expira em {{expiry}} minutos.</strong></p>
|
|
9
|
+
<p>Se você não solicitou, ignore este email.</p>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<p>Olá {{userName}},</p>
|
|
2
|
+
<p>Seu código de verificação (2FA) é:</p>
|
|
3
|
+
<div style="text-align:center;margin:30px 0;">
|
|
4
|
+
<div style="background:#f5f5f5;padding:20px;border-radius:5px;display:inline-block;">
|
|
5
|
+
<h1 style="margin:0;font-size:36px;letter-spacing:8px;color:{{color}};">{{code}}</h1>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<p><strong>Este código expira em {{expiry}} minutos.</strong></p>
|
|
9
|
+
<p>Se você não tentou fazer login, proteja sua conta imediatamente.</p>
|
|
@@ -44,14 +44,20 @@ export interface EnqueueOptions {
|
|
|
44
44
|
}
|
|
45
45
|
export interface QueueJobSummary {
|
|
46
46
|
id: string;
|
|
47
|
+
queue: QueueName;
|
|
47
48
|
name: string;
|
|
48
49
|
state: JobState;
|
|
49
50
|
ownerId: string | null;
|
|
50
51
|
payload: unknown;
|
|
51
52
|
attemptsMade: number;
|
|
53
|
+
maxAttempts: number;
|
|
52
54
|
percent: number | null;
|
|
55
|
+
progressMessage: string | null;
|
|
53
56
|
failedReason: string | null;
|
|
57
|
+
stacktrace: string[] | null;
|
|
58
|
+
result: unknown;
|
|
54
59
|
createdAt: string;
|
|
60
|
+
startedAt: string | null;
|
|
55
61
|
finishedAt: string | null;
|
|
56
62
|
}
|
|
57
63
|
export interface QueueCounts {
|
|
@@ -61,6 +67,13 @@ export interface QueueCounts {
|
|
|
61
67
|
failed: number;
|
|
62
68
|
delayed: number;
|
|
63
69
|
}
|
|
70
|
+
export interface QueueCountsRow {
|
|
71
|
+
queue: QueueName;
|
|
72
|
+
counts: QueueCounts;
|
|
73
|
+
}
|
|
74
|
+
export interface QueueCountsSummary extends QueueCounts {
|
|
75
|
+
byQueue: QueueCountsRow[];
|
|
76
|
+
}
|
|
64
77
|
export interface JobContext<TPayload = unknown> {
|
|
65
78
|
jobId: string;
|
|
66
79
|
ownerId: string;
|
|
@@ -4,6 +4,10 @@ export declare const IMAGES_QUEUE = "images";
|
|
|
4
4
|
export declare const QUEUE_NAMES: readonly ["jobs", "exports", "images"];
|
|
5
5
|
export type QueueName = (typeof QUEUE_NAMES)[number];
|
|
6
6
|
export declare const isQueueName: (value: unknown) => value is QueueName;
|
|
7
|
+
export declare const ALL_QUEUES = "all";
|
|
8
|
+
export type QueueScope = QueueName | typeof ALL_QUEUES;
|
|
9
|
+
export declare const isQueueScope: (value: unknown) => value is QueueScope;
|
|
10
|
+
export declare const QUEUE_SCOPES: readonly ["jobs", "exports", "images", "all"];
|
|
7
11
|
export declare const JOB_PROGRESS_EVENT = "job:progress";
|
|
8
12
|
export declare const JOB_COMPLETED_EVENT = "job:completed";
|
|
9
13
|
export declare const JOB_FAILED_EVENT = "job:failed";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.queuePrefix = exports.isRedisConfigured = exports.FAILED_RETENTION_SECONDS = exports.COMPLETED_RETENTION_SECONDS = exports.JOB_BACKOFF_DELAY_MS = exports.IMAGE_LOCK_DURATION_MS = exports.IMAGE_CONCURRENCY = exports.EXPORT_LOCK_DURATION_MS = exports.EXPORT_CONCURRENCY = exports.JOB_ATTEMPTS = exports.JOB_CONCURRENCY = exports.JOB_FAILED_EVENT = exports.JOB_COMPLETED_EVENT = exports.JOB_PROGRESS_EVENT = exports.isQueueName = exports.QUEUE_NAMES = exports.IMAGES_QUEUE = exports.EXPORTS_QUEUE = exports.JOBS_QUEUE = void 0;
|
|
3
|
+
exports.queuePrefix = exports.isRedisConfigured = exports.FAILED_RETENTION_SECONDS = exports.COMPLETED_RETENTION_SECONDS = exports.JOB_BACKOFF_DELAY_MS = exports.IMAGE_LOCK_DURATION_MS = exports.IMAGE_CONCURRENCY = exports.EXPORT_LOCK_DURATION_MS = exports.EXPORT_CONCURRENCY = exports.JOB_ATTEMPTS = exports.JOB_CONCURRENCY = exports.JOB_FAILED_EVENT = exports.JOB_COMPLETED_EVENT = exports.JOB_PROGRESS_EVENT = exports.QUEUE_SCOPES = exports.isQueueScope = exports.ALL_QUEUES = exports.isQueueName = exports.QUEUE_NAMES = exports.IMAGES_QUEUE = exports.EXPORTS_QUEUE = exports.JOBS_QUEUE = void 0;
|
|
4
4
|
const dotenv_1 = require("dotenv");
|
|
5
5
|
(0, dotenv_1.config)();
|
|
6
6
|
exports.JOBS_QUEUE = "jobs";
|
|
@@ -9,6 +9,10 @@ exports.IMAGES_QUEUE = "images";
|
|
|
9
9
|
exports.QUEUE_NAMES = [exports.JOBS_QUEUE, exports.EXPORTS_QUEUE, exports.IMAGES_QUEUE];
|
|
10
10
|
const isQueueName = (value) => exports.QUEUE_NAMES.includes(value);
|
|
11
11
|
exports.isQueueName = isQueueName;
|
|
12
|
+
exports.ALL_QUEUES = "all";
|
|
13
|
+
const isQueueScope = (value) => value === exports.ALL_QUEUES || (0, exports.isQueueName)(value);
|
|
14
|
+
exports.isQueueScope = isQueueScope;
|
|
15
|
+
exports.QUEUE_SCOPES = [...exports.QUEUE_NAMES, exports.ALL_QUEUES];
|
|
12
16
|
exports.JOB_PROGRESS_EVENT = "job:progress";
|
|
13
17
|
exports.JOB_COMPLETED_EVENT = "job:completed";
|
|
14
18
|
exports.JOB_FAILED_EVENT = "job:failed";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueueCountsSummary, QueueJobSummary } from "./job.interface";
|
|
2
2
|
import { QueueAdminService } from "./queueAdmin.service";
|
|
3
3
|
import { Paginated } from "../utils/pagination.util";
|
|
4
4
|
export declare class QueueAdminController {
|
|
5
5
|
private readonly queues;
|
|
6
6
|
constructor(queues: QueueAdminService);
|
|
7
|
-
counts(queue?: string): Promise<
|
|
7
|
+
counts(queue?: string): Promise<QueueCountsSummary>;
|
|
8
8
|
jobs(state: string, page?: string, limit?: string, sortBy?: string, sortDir?: string, queue?: string): Promise<Paginated<QueueJobSummary>>;
|
|
9
9
|
retry(id: string, queue?: string): Promise<void>;
|
|
10
10
|
remove(id: string, queue?: string): Promise<void>;
|
|
@@ -26,10 +26,10 @@ let QueueAdminController = class QueueAdminController {
|
|
|
26
26
|
this.queues = queues;
|
|
27
27
|
}
|
|
28
28
|
counts(queue) {
|
|
29
|
-
return this.queues.counts(
|
|
29
|
+
return this.queues.counts(toScope(queue));
|
|
30
30
|
}
|
|
31
31
|
jobs(state, page, limit, sortBy, sortDir, queue) {
|
|
32
|
-
return this.queues.list(toState(state), toPositive(page, 1), Math.min(toPositive(limit, DEFAULT_LIMIT), MAX_LIMIT), sortBy, sortDir === "ASC" ? "ASC" : "DESC",
|
|
32
|
+
return this.queues.list(toState(state), toPositive(page, 1), Math.min(toPositive(limit, DEFAULT_LIMIT), MAX_LIMIT), sortBy, sortDir === "ASC" ? "ASC" : "DESC", toScope(queue));
|
|
33
33
|
}
|
|
34
34
|
retry(id, queue) {
|
|
35
35
|
return this.queues.retry(id, toQueue(queue));
|
|
@@ -42,7 +42,7 @@ exports.QueueAdminController = QueueAdminController;
|
|
|
42
42
|
__decorate([
|
|
43
43
|
(0, common_1.Get)("counts"),
|
|
44
44
|
(0, requirePermission_decorator_1.RequirePermission)("queues:read:any"),
|
|
45
|
-
(0, swagger_1.ApiQuery)({ name: "queue", enum: queue_constants_1.
|
|
45
|
+
(0, swagger_1.ApiQuery)({ name: "queue", enum: queue_constants_1.QUEUE_SCOPES, required: false }),
|
|
46
46
|
(0, swagger_1.ApiOkResponse)({ type: queueAdmin_response_1.QueueCountsResponse }),
|
|
47
47
|
__param(0, (0, common_1.Query)("queue")),
|
|
48
48
|
__metadata("design:type", Function),
|
|
@@ -53,7 +53,7 @@ __decorate([
|
|
|
53
53
|
(0, common_1.Get)("jobs"),
|
|
54
54
|
(0, requirePermission_decorator_1.RequirePermission)("queues:read:any"),
|
|
55
55
|
(0, swagger_1.ApiQuery)({ name: "state", enum: queueAdmin_service_1.LISTABLE_STATES }),
|
|
56
|
-
(0, swagger_1.ApiQuery)({ name: "queue", enum: queue_constants_1.
|
|
56
|
+
(0, swagger_1.ApiQuery)({ name: "queue", enum: queue_constants_1.QUEUE_SCOPES, required: false }),
|
|
57
57
|
(0, swagger_1.ApiQuery)({ name: "sortBy", enum: queueAdmin_service_1.SORTABLE_JOB_FIELDS, required: false }),
|
|
58
58
|
(0, swagger_1.ApiQuery)({ name: "sortDir", enum: ["ASC", "DESC"], required: false }),
|
|
59
59
|
(0, swagger_1.ApiOkResponse)({ type: queueAdmin_response_1.PaginatedQueueJobsResponse }),
|
|
@@ -96,6 +96,7 @@ exports.QueueAdminController = QueueAdminController = __decorate([
|
|
|
96
96
|
__metadata("design:paramtypes", [queueAdmin_service_1.QueueAdminService])
|
|
97
97
|
], QueueAdminController);
|
|
98
98
|
const toQueue = (value) => (0, queue_constants_1.isQueueName)(value) ? value : queue_constants_1.JOBS_QUEUE;
|
|
99
|
+
const toScope = (value) => (0, queue_constants_1.isQueueScope)(value) ? value : queue_constants_1.JOBS_QUEUE;
|
|
99
100
|
const toState = (value) => queueAdmin_service_1.LISTABLE_STATES.includes(value)
|
|
100
101
|
? value
|
|
101
102
|
: "failed";
|
|
@@ -1,21 +1,36 @@
|
|
|
1
|
+
import { QueueCounts } from "./job.interface";
|
|
2
|
+
import { QueueName } from "./queue.constants";
|
|
1
3
|
import { PaginationMetaResponse } from "../utils/paginationMeta.response";
|
|
2
|
-
export declare class
|
|
4
|
+
export declare class QueueStateCountsResponse implements QueueCounts {
|
|
3
5
|
waiting: number;
|
|
4
6
|
active: number;
|
|
5
7
|
completed: number;
|
|
6
8
|
failed: number;
|
|
7
9
|
delayed: number;
|
|
8
10
|
}
|
|
11
|
+
export declare class QueueCountsRowResponse {
|
|
12
|
+
queue: QueueName;
|
|
13
|
+
counts: QueueStateCountsResponse;
|
|
14
|
+
}
|
|
15
|
+
export declare class QueueCountsResponse extends QueueStateCountsResponse {
|
|
16
|
+
byQueue: QueueCountsRowResponse[];
|
|
17
|
+
}
|
|
9
18
|
export declare class QueueJobResponse {
|
|
10
19
|
id: string;
|
|
20
|
+
queue: QueueName;
|
|
11
21
|
name: string;
|
|
12
22
|
state: string;
|
|
13
23
|
ownerId: string | null;
|
|
14
24
|
payload: unknown;
|
|
15
25
|
attemptsMade: number;
|
|
26
|
+
maxAttempts: number;
|
|
16
27
|
percent: number | null;
|
|
28
|
+
progressMessage: string | null;
|
|
17
29
|
failedReason: string | null;
|
|
30
|
+
stacktrace: string[] | null;
|
|
31
|
+
result: unknown;
|
|
18
32
|
createdAt: string;
|
|
33
|
+
startedAt: string | null;
|
|
19
34
|
finishedAt: string | null;
|
|
20
35
|
}
|
|
21
36
|
export declare class PaginatedQueueJobsResponse {
|
|
@@ -9,32 +9,51 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.PaginatedQueueJobsResponse = exports.QueueJobResponse = exports.QueueCountsResponse = void 0;
|
|
12
|
+
exports.PaginatedQueueJobsResponse = exports.QueueJobResponse = exports.QueueCountsResponse = exports.QueueCountsRowResponse = exports.QueueStateCountsResponse = void 0;
|
|
13
13
|
const swagger_1 = require("@nestjs/swagger");
|
|
14
|
+
const queue_constants_1 = require("./queue.constants");
|
|
14
15
|
const paginationMeta_response_1 = require("../utils/paginationMeta.response");
|
|
15
|
-
class
|
|
16
|
+
class QueueStateCountsResponse {
|
|
16
17
|
}
|
|
17
|
-
exports.
|
|
18
|
+
exports.QueueStateCountsResponse = QueueStateCountsResponse;
|
|
18
19
|
__decorate([
|
|
19
20
|
(0, swagger_1.ApiProperty)({ example: 3 }),
|
|
20
21
|
__metadata("design:type", Number)
|
|
21
|
-
],
|
|
22
|
+
], QueueStateCountsResponse.prototype, "waiting", void 0);
|
|
22
23
|
__decorate([
|
|
23
24
|
(0, swagger_1.ApiProperty)({ example: 1 }),
|
|
24
25
|
__metadata("design:type", Number)
|
|
25
|
-
],
|
|
26
|
+
], QueueStateCountsResponse.prototype, "active", void 0);
|
|
26
27
|
__decorate([
|
|
27
28
|
(0, swagger_1.ApiProperty)({ example: 128 }),
|
|
28
29
|
__metadata("design:type", Number)
|
|
29
|
-
],
|
|
30
|
+
], QueueStateCountsResponse.prototype, "completed", void 0);
|
|
30
31
|
__decorate([
|
|
31
32
|
(0, swagger_1.ApiProperty)({ example: 2 }),
|
|
32
33
|
__metadata("design:type", Number)
|
|
33
|
-
],
|
|
34
|
+
], QueueStateCountsResponse.prototype, "failed", void 0);
|
|
34
35
|
__decorate([
|
|
35
36
|
(0, swagger_1.ApiProperty)({ example: 0 }),
|
|
36
37
|
__metadata("design:type", Number)
|
|
37
|
-
],
|
|
38
|
+
], QueueStateCountsResponse.prototype, "delayed", void 0);
|
|
39
|
+
class QueueCountsRowResponse {
|
|
40
|
+
}
|
|
41
|
+
exports.QueueCountsRowResponse = QueueCountsRowResponse;
|
|
42
|
+
__decorate([
|
|
43
|
+
(0, swagger_1.ApiProperty)({ enum: queue_constants_1.QUEUE_NAMES, example: "exports" }),
|
|
44
|
+
__metadata("design:type", String)
|
|
45
|
+
], QueueCountsRowResponse.prototype, "queue", void 0);
|
|
46
|
+
__decorate([
|
|
47
|
+
(0, swagger_1.ApiProperty)({ type: QueueStateCountsResponse }),
|
|
48
|
+
__metadata("design:type", QueueStateCountsResponse)
|
|
49
|
+
], QueueCountsRowResponse.prototype, "counts", void 0);
|
|
50
|
+
class QueueCountsResponse extends QueueStateCountsResponse {
|
|
51
|
+
}
|
|
52
|
+
exports.QueueCountsResponse = QueueCountsResponse;
|
|
53
|
+
__decorate([
|
|
54
|
+
(0, swagger_1.ApiProperty)({ type: QueueCountsRowResponse, isArray: true }),
|
|
55
|
+
__metadata("design:type", Array)
|
|
56
|
+
], QueueCountsResponse.prototype, "byQueue", void 0);
|
|
38
57
|
class QueueJobResponse {
|
|
39
58
|
}
|
|
40
59
|
exports.QueueJobResponse = QueueJobResponse;
|
|
@@ -42,6 +61,10 @@ __decorate([
|
|
|
42
61
|
(0, swagger_1.ApiProperty)({ example: "42" }),
|
|
43
62
|
__metadata("design:type", String)
|
|
44
63
|
], QueueJobResponse.prototype, "id", void 0);
|
|
64
|
+
__decorate([
|
|
65
|
+
(0, swagger_1.ApiProperty)({ enum: queue_constants_1.QUEUE_NAMES, example: "exports" }),
|
|
66
|
+
__metadata("design:type", String)
|
|
67
|
+
], QueueJobResponse.prototype, "queue", void 0);
|
|
45
68
|
__decorate([
|
|
46
69
|
(0, swagger_1.ApiProperty)({ example: "funkos:import" }),
|
|
47
70
|
__metadata("design:type", String)
|
|
@@ -57,6 +80,7 @@ __decorate([
|
|
|
57
80
|
__decorate([
|
|
58
81
|
(0, swagger_1.ApiProperty)({
|
|
59
82
|
type: Object,
|
|
83
|
+
additionalProperties: true,
|
|
60
84
|
nullable: true,
|
|
61
85
|
example: { handler: "funkos", fileName: "funkos_2026-09-04.xlsx" },
|
|
62
86
|
}),
|
|
@@ -66,18 +90,38 @@ __decorate([
|
|
|
66
90
|
(0, swagger_1.ApiProperty)({ example: 2 }),
|
|
67
91
|
__metadata("design:type", Number)
|
|
68
92
|
], QueueJobResponse.prototype, "attemptsMade", void 0);
|
|
93
|
+
__decorate([
|
|
94
|
+
(0, swagger_1.ApiProperty)({ example: 3 }),
|
|
95
|
+
__metadata("design:type", Number)
|
|
96
|
+
], QueueJobResponse.prototype, "maxAttempts", void 0);
|
|
69
97
|
__decorate([
|
|
70
98
|
(0, swagger_1.ApiProperty)({ type: Number, nullable: true, example: 69 }),
|
|
71
99
|
__metadata("design:type", Object)
|
|
72
100
|
], QueueJobResponse.prototype, "percent", void 0);
|
|
101
|
+
__decorate([
|
|
102
|
+
(0, swagger_1.ApiProperty)({ type: String, nullable: true, example: "5000 linha(s)" }),
|
|
103
|
+
__metadata("design:type", Object)
|
|
104
|
+
], QueueJobResponse.prototype, "progressMessage", void 0);
|
|
73
105
|
__decorate([
|
|
74
106
|
(0, swagger_1.ApiProperty)({ type: String, nullable: true }),
|
|
75
107
|
__metadata("design:type", Object)
|
|
76
108
|
], QueueJobResponse.prototype, "failedReason", void 0);
|
|
109
|
+
__decorate([
|
|
110
|
+
(0, swagger_1.ApiProperty)({ type: String, isArray: true, nullable: true }),
|
|
111
|
+
__metadata("design:type", Object)
|
|
112
|
+
], QueueJobResponse.prototype, "stacktrace", void 0);
|
|
113
|
+
__decorate([
|
|
114
|
+
(0, swagger_1.ApiProperty)({ type: Object, additionalProperties: true, nullable: true }),
|
|
115
|
+
__metadata("design:type", Object)
|
|
116
|
+
], QueueJobResponse.prototype, "result", void 0);
|
|
77
117
|
__decorate([
|
|
78
118
|
(0, swagger_1.ApiProperty)({ format: "date-time" }),
|
|
79
119
|
__metadata("design:type", String)
|
|
80
120
|
], QueueJobResponse.prototype, "createdAt", void 0);
|
|
121
|
+
__decorate([
|
|
122
|
+
(0, swagger_1.ApiProperty)({ type: String, format: "date-time", nullable: true }),
|
|
123
|
+
__metadata("design:type", Object)
|
|
124
|
+
], QueueJobResponse.prototype, "startedAt", void 0);
|
|
81
125
|
__decorate([
|
|
82
126
|
(0, swagger_1.ApiProperty)({ type: String, format: "date-time", nullable: true }),
|
|
83
127
|
__metadata("design:type", Object)
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueueCountsSummary, QueueJobSummary } from "./job.interface";
|
|
2
2
|
import { JobProducer } from "./job.producer";
|
|
3
|
-
import { QueueName } from "./queue.constants";
|
|
3
|
+
import { QueueName, QueueScope } from "./queue.constants";
|
|
4
4
|
import { Paginated, SortDir } from "../utils/pagination.util";
|
|
5
5
|
export declare const LISTABLE_STATES: readonly ["waiting", "active", "completed", "failed", "delayed"];
|
|
6
6
|
export type ListableState = (typeof LISTABLE_STATES)[number];
|
|
7
|
-
export declare const SORTABLE_JOB_FIELDS: readonly ["name", "attemptsMade", "percent", "createdAt", "failedReason"];
|
|
7
|
+
export declare const SORTABLE_JOB_FIELDS: readonly ["name", "queue", "attemptsMade", "percent", "createdAt", "failedReason"];
|
|
8
8
|
export type SortableJobField = (typeof SORTABLE_JOB_FIELDS)[number];
|
|
9
9
|
export declare const SORT_SCAN_LIMIT = 1000;
|
|
10
10
|
export declare class QueueAdminService {
|
|
11
11
|
private readonly producer;
|
|
12
12
|
constructor(producer: JobProducer);
|
|
13
|
-
counts(
|
|
14
|
-
|
|
13
|
+
counts(scope?: QueueScope): Promise<QueueCountsSummary>;
|
|
14
|
+
private countsOf;
|
|
15
|
+
list(state: ListableState, page: number, limit: number, sortBy?: string, sortDir?: SortDir, scope?: QueueScope): Promise<Paginated<QueueJobSummary>>;
|
|
15
16
|
retry(jobId: string, queueName?: QueueName): Promise<void>;
|
|
16
17
|
remove(jobId: string, queueName?: QueueName): Promise<void>;
|
|
17
18
|
private findJob;
|
|
@@ -23,6 +23,7 @@ exports.LISTABLE_STATES = [
|
|
|
23
23
|
];
|
|
24
24
|
exports.SORTABLE_JOB_FIELDS = [
|
|
25
25
|
"name",
|
|
26
|
+
"queue",
|
|
26
27
|
"attemptsMade",
|
|
27
28
|
"percent",
|
|
28
29
|
"createdAt",
|
|
@@ -33,7 +34,14 @@ let QueueAdminService = class QueueAdminService {
|
|
|
33
34
|
constructor(producer) {
|
|
34
35
|
this.producer = producer;
|
|
35
36
|
}
|
|
36
|
-
async counts(
|
|
37
|
+
async counts(scope = queue_constants_1.JOBS_QUEUE) {
|
|
38
|
+
const byQueue = await Promise.all(queuesOf(scope).map(async (queue) => ({
|
|
39
|
+
queue,
|
|
40
|
+
counts: await this.countsOf(queue),
|
|
41
|
+
})));
|
|
42
|
+
return { ...sumCounts(byQueue), byQueue };
|
|
43
|
+
}
|
|
44
|
+
async countsOf(queueName) {
|
|
37
45
|
const queue = this.producer.requireQueue(queueName);
|
|
38
46
|
const counts = await queue.getJobCounts(...exports.LISTABLE_STATES);
|
|
39
47
|
return {
|
|
@@ -44,23 +52,38 @@ let QueueAdminService = class QueueAdminService {
|
|
|
44
52
|
delayed: counts.delayed ?? 0,
|
|
45
53
|
};
|
|
46
54
|
}
|
|
47
|
-
async list(state, page, limit, sortBy, sortDir,
|
|
48
|
-
const
|
|
55
|
+
async list(state, page, limit, sortBy, sortDir, scope = queue_constants_1.JOBS_QUEUE) {
|
|
56
|
+
const names = queuesOf(scope);
|
|
49
57
|
const field = exports.SORTABLE_JOB_FIELDS.find((name) => name === sortBy);
|
|
50
58
|
const start = (page - 1) * limit;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const direct = !field && names.length === 1;
|
|
60
|
+
const scanEnd = field
|
|
61
|
+
? exports.SORT_SCAN_LIMIT
|
|
62
|
+
: Math.min(start + limit, exports.SORT_SCAN_LIMIT);
|
|
63
|
+
const range = direct
|
|
64
|
+
? [start, start + limit - 1]
|
|
65
|
+
: [0, scanEnd - 1];
|
|
66
|
+
const parts = await Promise.all(names.map(async (name) => {
|
|
67
|
+
const queue = this.producer.requireQueue(name);
|
|
68
|
+
const [jobs, counts] = await Promise.all([
|
|
69
|
+
queue.getJobs([state], range[0], range[1]),
|
|
70
|
+
queue.getJobCounts(state),
|
|
71
|
+
]);
|
|
72
|
+
return {
|
|
73
|
+
items: jobs
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((job) => this.toSummary(job, state, name)),
|
|
76
|
+
total: counts[state] ?? 0,
|
|
77
|
+
};
|
|
78
|
+
}));
|
|
79
|
+
const total = parts.reduce((sum, part) => sum + part.total, 0);
|
|
80
|
+
const items = parts.flatMap((part) => part.items);
|
|
81
|
+
if (direct) {
|
|
61
82
|
return new pagination_util_1.Paginated(items, total, page, limit);
|
|
62
83
|
}
|
|
63
|
-
const ordered = [...items].sort(
|
|
84
|
+
const ordered = [...items].sort(field
|
|
85
|
+
? compareBy(field, sortDir ?? "DESC")
|
|
86
|
+
: compareBy("createdAt", "DESC"));
|
|
64
87
|
return new pagination_util_1.Paginated(ordered.slice(start, start + limit), total, page, limit);
|
|
65
88
|
}
|
|
66
89
|
async retry(jobId, queueName) {
|
|
@@ -79,20 +102,27 @@ let QueueAdminService = class QueueAdminService {
|
|
|
79
102
|
}
|
|
80
103
|
return job;
|
|
81
104
|
}
|
|
82
|
-
toSummary(job, state) {
|
|
105
|
+
toSummary(job, state, queue) {
|
|
83
106
|
const progress = job.progress;
|
|
107
|
+
const reported = typeof progress === "object" && progress !== null;
|
|
84
108
|
return {
|
|
85
109
|
id: String(job.id),
|
|
110
|
+
queue,
|
|
86
111
|
name: job.name,
|
|
87
112
|
state,
|
|
88
113
|
ownerId: job.data?.ownerId ?? null,
|
|
89
114
|
payload: job.data?.payload ?? null,
|
|
90
115
|
attemptsMade: job.attemptsMade,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
maxAttempts: job.opts?.attempts ?? queue_constants_1.JOB_ATTEMPTS,
|
|
117
|
+
percent: reported ? (progress.percent ?? null) : null,
|
|
118
|
+
progressMessage: reported ? (progress.message ?? null) : null,
|
|
94
119
|
failedReason: job.failedReason ?? null,
|
|
120
|
+
stacktrace: job.stacktrace?.length ? job.stacktrace : null,
|
|
121
|
+
result: job.returnvalue ?? null,
|
|
95
122
|
createdAt: new Date(job.timestamp).toISOString(),
|
|
123
|
+
startedAt: job.processedOn
|
|
124
|
+
? new Date(job.processedOn).toISOString()
|
|
125
|
+
: null,
|
|
96
126
|
finishedAt: job.finishedOn ? new Date(job.finishedOn).toISOString() : null,
|
|
97
127
|
};
|
|
98
128
|
}
|
|
@@ -102,6 +132,14 @@ exports.QueueAdminService = QueueAdminService = __decorate([
|
|
|
102
132
|
(0, common_1.Injectable)(),
|
|
103
133
|
__metadata("design:paramtypes", [job_producer_1.JobProducer])
|
|
104
134
|
], QueueAdminService);
|
|
135
|
+
const queuesOf = (scope) => scope === queue_constants_1.ALL_QUEUES ? [...queue_constants_1.QUEUE_NAMES] : [scope];
|
|
136
|
+
const sumCounts = (rows) => rows.reduce((total, row) => ({
|
|
137
|
+
waiting: total.waiting + row.counts.waiting,
|
|
138
|
+
active: total.active + row.counts.active,
|
|
139
|
+
completed: total.completed + row.counts.completed,
|
|
140
|
+
failed: total.failed + row.counts.failed,
|
|
141
|
+
delayed: total.delayed + row.counts.delayed,
|
|
142
|
+
}), { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0 });
|
|
105
143
|
const compareBy = (field, direction) => (a, b) => {
|
|
106
144
|
const left = a[field];
|
|
107
145
|
const right = b[field];
|
package/dist/index.d.ts
CHANGED
|
@@ -67,13 +67,13 @@ export { ErrorType } from "./features/audit/domain/enums/errorType.enum";
|
|
|
67
67
|
export { RequestOutcome } from "./features/audit/domain/enums/requestOutcome.enum";
|
|
68
68
|
export { Auditable } from "./features/audit/presentation/decorators/auditable.decorator";
|
|
69
69
|
export { BullConnectionModule } from "./core/queue/bullConnection.module";
|
|
70
|
-
export { EnqueueOptions, JobCompleted, JobContext, JobEnvelope, JobFailed, JobProgress, JobRef, JobState, JobStatus, QueueCounts, QueueJobSummary, } from "./core/queue/job.interface";
|
|
70
|
+
export { EnqueueOptions, JobCompleted, JobContext, JobEnvelope, JobFailed, JobProgress, JobRef, JobState, JobStatus, QueueCounts, QueueCountsRow, QueueCountsSummary, QueueJobSummary, } from "./core/queue/job.interface";
|
|
71
71
|
export { JobProducer } from "./core/queue/job.producer";
|
|
72
72
|
export { JOB_EVENTS_PUBLISHER, JobEventsPublisher, } from "./core/queue/jobEvents.interface";
|
|
73
73
|
export { JobEventsService } from "./core/queue/jobEvents.service";
|
|
74
74
|
export { JobErrorResponse, JobProgressResponse, JobStatusResponse, } from "./core/queue/jobStatus.response";
|
|
75
75
|
export { JobWorkerHost, toSafeError } from "./core/queue/jobWorkerHost";
|
|
76
|
-
export { COMPLETED_RETENTION_SECONDS, EXPORT_CONCURRENCY, EXPORT_LOCK_DURATION_MS, EXPORTS_QUEUE, FAILED_RETENTION_SECONDS, IMAGE_CONCURRENCY, IMAGE_LOCK_DURATION_MS, IMAGES_QUEUE, isQueueName, isRedisConfigured, JOB_ATTEMPTS, JOB_BACKOFF_DELAY_MS, JOB_COMPLETED_EVENT, JOB_CONCURRENCY, JOB_FAILED_EVENT, JOB_PROGRESS_EVENT, JOBS_QUEUE, QUEUE_NAMES, QueueName, queuePrefix, } from "./core/queue/queue.constants";
|
|
76
|
+
export { ALL_QUEUES, COMPLETED_RETENTION_SECONDS, EXPORT_CONCURRENCY, EXPORT_LOCK_DURATION_MS, EXPORTS_QUEUE, FAILED_RETENTION_SECONDS, IMAGE_CONCURRENCY, IMAGE_LOCK_DURATION_MS, IMAGES_QUEUE, isQueueName, isQueueScope, isRedisConfigured, JOB_ATTEMPTS, JOB_BACKOFF_DELAY_MS, JOB_COMPLETED_EVENT, JOB_CONCURRENCY, JOB_FAILED_EVENT, JOB_PROGRESS_EVENT, JOBS_QUEUE, QUEUE_NAMES, QUEUE_SCOPES, QueueName, queuePrefix, QueueScope, } from "./core/queue/queue.constants";
|
|
77
77
|
export { QueueModule } from "./core/queue/queue.module";
|
|
78
78
|
export { buildRedisOptions } from "./core/queue/redisConnection";
|
|
79
79
|
export { RedisJobEventsBridge } from "./core/queue/redisJobEvents.bridge";
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FilterFieldResponse = exports.FilterOperator = exports.FilterCombinator = exports.FilterFieldType = exports.FILTER_MAX_NODES = exports.FILTER_MAX_LENGTH = exports.FILTER_MAX_IN_ITEMS = exports.FILTER_MAX_DEPTH = exports.DEFAULT_OPERATORS = exports.applyOwnershipScope = exports.applyFilter = exports.SCOPE_TEAM = exports.SCOPE_OWN = exports.SCOPE_ANY = exports.anyOrTeam = exports.anyOrOwn = exports.INSUFFICIENT_PERMISSION = exports.hasPermission = exports.hasAnyPermission = exports.assertPermission = exports.assertAnyPermission = exports.resolveOwnershipScope = exports.PermissionsGuard = exports.PasswordPolicyGuard = exports.JwtAuthGuard = exports.CsrfGuard = exports.AppThrottlerGuard = exports.RequirePermission = exports.RequireAnyPermission = exports.PERMISSIONS_KEY = exports.ANY_PERMISSIONS_KEY = exports.Public = exports.IS_PUBLIC_KEY = exports.CurrentUser = exports.AllowPendingPassword = exports.setAuthCookies = exports.REFRESH_COOKIE = exports.clearAuthCookies = exports.ACCESS_COOKIE = exports.EnvService = exports.validateEnv = exports.envSchema = exports.ConfigModule = exports.RlCoreModule = exports.coreEntities = exports.coreMigrations = exports.createCoreDataSource = exports.CoreModule = exports.bootstrapWorker = exports.bootstrapCore = void 0;
|
|
4
4
|
exports.JobEventsService = exports.JOB_EVENTS_PUBLISHER = exports.JobProducer = exports.BullConnectionModule = exports.Auditable = exports.RequestOutcome = exports.ErrorType = exports.codedUnauthorized = exports.codedTooManyRequests = exports.codedBadRequest = exports.AppErrorCode = exports.AppEvent = exports.auditContext = exports.User = exports.PasswordHistory = exports.Scope = exports.Role = exports.Resource = exports.Permission = exports.Group = exports.Action = exports.NotificationsPublisher = exports.NotificationsModule = exports.NotificationScope = exports.NotificationRead = exports.Notification = exports.NotificationsService = exports.ScopeType = exports.NotificationType = exports.SCOPES = exports.ROLES = exports.RESOURCES = exports.PERMISSIONS = exports.ACTIONS = exports.AuthToken = exports.RequestLog = exports.AuditDataChange = exports.seedPermissions = exports.seedAdmin = exports.BaseEntity = exports.PaginationMetaResponse = exports.resolveOrder = exports.PaginationQuery = exports.Paginated = exports.parseFilter = exports.paginateWithFilter = exports.applySearch = exports.resolveCatalogOrder = exports.toFilterSchema = exports.FilterOptionResponse = void 0;
|
|
5
|
-
exports.
|
|
6
|
-
exports.normalizeName = exports.normalizeIp = exports.sha256 = exports.randomToken = exports.numericCode = exports.backupCode = exports.encryptSecret = exports.decryptSecret = exports.MailerService = exports.createAppLogger = exports.WS_USER_RESOLVER = exports.WsAuthService = exports.wrapSocketEvent = exports.SOCKET_EVENT_VERSION = exports.userRoom = exports.SESSION_REFRESH_EVENT = exports.roleRoom = exports.NOTIFICATIONS_NAMESPACE = exports.BaseGateway = exports.UploadImageFromUrlCommand = exports.ImageStatusResponse = exports.ImageProfileResponse = exports.ImageAcceptedResponse = exports.ImagesWorkerModule = exports.ImageStorageService = exports.ImagesApiModule = exports.ImagesService = exports.ImagesProcessor = exports.ImagesModule = exports.ImagesController = exports.ImageProfileRegistry = exports.ImageUploadProfile = exports.ImageSourceKind = exports.ImageFormat = exports.ImageFit = exports.IMAGE_PROFILE = exports.EXTENSION_BY_FORMAT = exports.ImageProcessingService = exports.isBlockedAddress = exports.ImageFetchService = exports.ImageCleanupService = exports.RequestExportCommand = exports.PdfDocumentService = exports.PDF_TABLE_LAYOUT = exports.PDF_STYLE = void 0;
|
|
5
|
+
exports.ExportKind = exports.ExportHandlerRegistry = exports.ExportReport = exports.EXPORT_HANDLER = exports.ExportExtension = exports.EXPORT_CONTENT_TYPES = exports.ExportEngineService = exports.ExportDocument = exports.EXPORT_DOCUMENT = exports.ExportColumnFormat = exports.ExportCleanupService = exports.ExportApiModule = exports.ExportAcceptedResponse = exports.ExportService = exports.ExportProcessor = exports.ExportModule = exports.ExportController = exports.EXPORT_PARAMS_MAX_LENGTH = exports.RedisJobEventsPublisher = exports.jobEventsChannel = exports.RedisJobEventsBridge = exports.buildRedisOptions = exports.QueueModule = exports.queuePrefix = exports.QUEUE_SCOPES = exports.QUEUE_NAMES = exports.JOBS_QUEUE = exports.JOB_PROGRESS_EVENT = exports.JOB_FAILED_EVENT = exports.JOB_CONCURRENCY = exports.JOB_COMPLETED_EVENT = exports.JOB_BACKOFF_DELAY_MS = exports.JOB_ATTEMPTS = exports.isRedisConfigured = exports.isQueueScope = exports.isQueueName = exports.IMAGES_QUEUE = exports.IMAGE_LOCK_DURATION_MS = exports.IMAGE_CONCURRENCY = exports.FAILED_RETENTION_SECONDS = exports.EXPORTS_QUEUE = exports.EXPORT_LOCK_DURATION_MS = exports.EXPORT_CONCURRENCY = exports.COMPLETED_RETENTION_SECONDS = exports.ALL_QUEUES = exports.toSafeError = exports.JobWorkerHost = exports.JobStatusResponse = exports.JobProgressResponse = exports.JobErrorResponse = void 0;
|
|
6
|
+
exports.normalizeName = exports.normalizeIp = exports.sha256 = exports.randomToken = exports.numericCode = exports.backupCode = exports.encryptSecret = exports.decryptSecret = exports.MailerService = exports.createAppLogger = exports.WS_USER_RESOLVER = exports.WsAuthService = exports.wrapSocketEvent = exports.SOCKET_EVENT_VERSION = exports.userRoom = exports.SESSION_REFRESH_EVENT = exports.roleRoom = exports.NOTIFICATIONS_NAMESPACE = exports.BaseGateway = exports.UploadImageFromUrlCommand = exports.ImageStatusResponse = exports.ImageProfileResponse = exports.ImageAcceptedResponse = exports.ImagesWorkerModule = exports.ImageStorageService = exports.ImagesApiModule = exports.ImagesService = exports.ImagesProcessor = exports.ImagesModule = exports.ImagesController = exports.ImageProfileRegistry = exports.ImageUploadProfile = exports.ImageSourceKind = exports.ImageFormat = exports.ImageFit = exports.IMAGE_PROFILE = exports.EXTENSION_BY_FORMAT = exports.ImageProcessingService = exports.isBlockedAddress = exports.ImageFetchService = exports.ImageCleanupService = exports.RequestExportCommand = exports.PdfDocumentService = exports.PDF_TABLE_LAYOUT = exports.PDF_STYLE = exports.ExportWorkerModule = exports.ExportStorageService = exports.ExportStatusResponse = void 0;
|
|
7
7
|
var bootstrapCore_util_1 = require("./core/bootstrap/bootstrapCore.util");
|
|
8
8
|
Object.defineProperty(exports, "bootstrapCore", { enumerable: true, get: function () { return bootstrapCore_util_1.bootstrapCore; } });
|
|
9
9
|
var bootstrapWorker_util_1 = require("./core/bootstrap/bootstrapWorker.util");
|
|
@@ -180,6 +180,7 @@ var jobWorkerHost_1 = require("./core/queue/jobWorkerHost");
|
|
|
180
180
|
Object.defineProperty(exports, "JobWorkerHost", { enumerable: true, get: function () { return jobWorkerHost_1.JobWorkerHost; } });
|
|
181
181
|
Object.defineProperty(exports, "toSafeError", { enumerable: true, get: function () { return jobWorkerHost_1.toSafeError; } });
|
|
182
182
|
var queue_constants_1 = require("./core/queue/queue.constants");
|
|
183
|
+
Object.defineProperty(exports, "ALL_QUEUES", { enumerable: true, get: function () { return queue_constants_1.ALL_QUEUES; } });
|
|
183
184
|
Object.defineProperty(exports, "COMPLETED_RETENTION_SECONDS", { enumerable: true, get: function () { return queue_constants_1.COMPLETED_RETENTION_SECONDS; } });
|
|
184
185
|
Object.defineProperty(exports, "EXPORT_CONCURRENCY", { enumerable: true, get: function () { return queue_constants_1.EXPORT_CONCURRENCY; } });
|
|
185
186
|
Object.defineProperty(exports, "EXPORT_LOCK_DURATION_MS", { enumerable: true, get: function () { return queue_constants_1.EXPORT_LOCK_DURATION_MS; } });
|
|
@@ -189,6 +190,7 @@ Object.defineProperty(exports, "IMAGE_CONCURRENCY", { enumerable: true, get: fun
|
|
|
189
190
|
Object.defineProperty(exports, "IMAGE_LOCK_DURATION_MS", { enumerable: true, get: function () { return queue_constants_1.IMAGE_LOCK_DURATION_MS; } });
|
|
190
191
|
Object.defineProperty(exports, "IMAGES_QUEUE", { enumerable: true, get: function () { return queue_constants_1.IMAGES_QUEUE; } });
|
|
191
192
|
Object.defineProperty(exports, "isQueueName", { enumerable: true, get: function () { return queue_constants_1.isQueueName; } });
|
|
193
|
+
Object.defineProperty(exports, "isQueueScope", { enumerable: true, get: function () { return queue_constants_1.isQueueScope; } });
|
|
192
194
|
Object.defineProperty(exports, "isRedisConfigured", { enumerable: true, get: function () { return queue_constants_1.isRedisConfigured; } });
|
|
193
195
|
Object.defineProperty(exports, "JOB_ATTEMPTS", { enumerable: true, get: function () { return queue_constants_1.JOB_ATTEMPTS; } });
|
|
194
196
|
Object.defineProperty(exports, "JOB_BACKOFF_DELAY_MS", { enumerable: true, get: function () { return queue_constants_1.JOB_BACKOFF_DELAY_MS; } });
|
|
@@ -198,6 +200,7 @@ Object.defineProperty(exports, "JOB_FAILED_EVENT", { enumerable: true, get: func
|
|
|
198
200
|
Object.defineProperty(exports, "JOB_PROGRESS_EVENT", { enumerable: true, get: function () { return queue_constants_1.JOB_PROGRESS_EVENT; } });
|
|
199
201
|
Object.defineProperty(exports, "JOBS_QUEUE", { enumerable: true, get: function () { return queue_constants_1.JOBS_QUEUE; } });
|
|
200
202
|
Object.defineProperty(exports, "QUEUE_NAMES", { enumerable: true, get: function () { return queue_constants_1.QUEUE_NAMES; } });
|
|
203
|
+
Object.defineProperty(exports, "QUEUE_SCOPES", { enumerable: true, get: function () { return queue_constants_1.QUEUE_SCOPES; } });
|
|
201
204
|
Object.defineProperty(exports, "queuePrefix", { enumerable: true, get: function () { return queue_constants_1.queuePrefix; } });
|
|
202
205
|
var queue_module_1 = require("./core/queue/queue.module");
|
|
203
206
|
Object.defineProperty(exports, "QueueModule", { enumerable: true, get: function () { return queue_module_1.QueueModule; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-core-api",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.9",
|
|
4
4
|
"description": "Core NestJS: autenticação com 2FA, RBAC, auditoria, notificações e listagens com filtro dinâmico",
|
|
5
5
|
"author": "Rodrigo Liberti",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"cookie-parser": "^1.4.7",
|
|
85
85
|
"dotenv": "^17.4.2",
|
|
86
86
|
"exceljs": "^4.4.0",
|
|
87
|
+
"handlebars": "^4.7.9",
|
|
87
88
|
"helmet": "^8.3.0",
|
|
88
89
|
"ioredis": "^5.11.1",
|
|
89
90
|
"mysql2": "^3.23.2",
|