@ecdt/server-common 1.5.1 → 1.6.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 +33 -0
- package/package.json +1 -1
- package/src/index.d.ts +11 -0
- package/src/index.js +2 -1
- package/src/services/motorEventosService.js +56 -0
- package/test/motorEventosService.test.js +165 -0
package/README.md
CHANGED
|
@@ -158,6 +158,39 @@ export default defineEventHandler(async (event) => {
|
|
|
158
158
|
|
|
159
159
|
---
|
|
160
160
|
|
|
161
|
+
### `publicarEvento(options)`
|
|
162
|
+
|
|
163
|
+
Envia um evento para o coletor de eventos configurado, sem bloquear o fluxo que o gerou.
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
const { publicarEvento } = require('@ecdt/server-common');
|
|
167
|
+
|
|
168
|
+
publicarEvento({
|
|
169
|
+
tipo: 'meu_evento',
|
|
170
|
+
autorizacao: req.headers.authorization,
|
|
171
|
+
payload: { campo: 'valor' },
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Opção | Default | Descrição |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `tipo` | — | Nome do evento, em `snake_case` |
|
|
178
|
+
| `autorizacao` | — | Header `Authorization` da requisição que originou o evento |
|
|
179
|
+
| `payload` | `{}` | Campos do evento |
|
|
180
|
+
| `url` | `process.env.MOTOR_EVENTOS_URL` | URL completa do coletor; a função não monta caminho nenhum |
|
|
181
|
+
| `timeoutMs` | `2000` | Aborta a requisição |
|
|
182
|
+
| `aoFalhar` | `console.error` | Recebe a mensagem quando a publicação não acontece |
|
|
183
|
+
|
|
184
|
+
A identidade sai do token, nunca do corpo: o coletor resolve o usuário a partir do `Authorization`
|
|
185
|
+
repassado. Sem `tipo` ou sem `autorizacao` a função não faz nada; sem `url` ela ainda registra um
|
|
186
|
+
`console.warn`, porque url ausente é configuração faltando e não um evento que não se aplica.
|
|
187
|
+
|
|
188
|
+
**A função nunca lança e nunca devolve promise.** O evento é secundário ao fluxo que o gerou —
|
|
189
|
+
falha de publicação não pode virar erro de quem chamou, e o retorno `undefined` impede que alguém
|
|
190
|
+
consiga dar `await` e acoplar a latência do request ao motor.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
161
194
|
## Instalação
|
|
162
195
|
|
|
163
196
|
```bash
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -135,3 +135,14 @@ export declare function metricsHandler(registry?: MetricsRegistry): RequestHandl
|
|
|
135
135
|
export declare function renderMetrics(
|
|
136
136
|
registry?: MetricsRegistry
|
|
137
137
|
): Promise<{ contentType: string; body: string }>;
|
|
138
|
+
|
|
139
|
+
export interface PublicarEventoOptions {
|
|
140
|
+
tipo: string;
|
|
141
|
+
autorizacao: string;
|
|
142
|
+
payload?: Record<string, unknown>;
|
|
143
|
+
url?: string;
|
|
144
|
+
timeoutMs?: number;
|
|
145
|
+
aoFalhar?: (mensagem: string) => void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export declare function publicarEvento(options?: PublicarEventoOptions): void;
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const { devMktTokenSanitaze } = require("./services/tokenService.js");
|
|
|
3
3
|
const { expressCors } = require("./services/corsService.js");
|
|
4
4
|
const { setupGracefulShutdown } = require("./services/shutdownService.js");
|
|
5
5
|
const { setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics } = require("./services/redMetricsService.js");
|
|
6
|
+
const { publicarEvento } = require("./services/motorEventosService.js");
|
|
6
7
|
|
|
7
8
|
function expressCommonMiddlewares({ cookieName } = {}){
|
|
8
9
|
return [bodyParser.text(), bodyParser.json(), bodyParser.urlencoded({extended: false}), devMktTokenSanitaze({ cookieName })];
|
|
@@ -11,4 +12,4 @@ function expressCommonMiddlewares({ cookieName } = {}){
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
module.exports = { expressCommonMiddlewares, expressCors, setupGracefulShutdown, setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics }
|
|
15
|
+
module.exports = { expressCommonMiddlewares, expressCors, setupGracefulShutdown, setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics, publicarEvento }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const TIMEOUT_PADRAO_MS = 2000;
|
|
2
|
+
|
|
3
|
+
function urlConfigurada(url) {
|
|
4
|
+
const configurada = url ?? process.env.MOTOR_EVENTOS_URL;
|
|
5
|
+
|
|
6
|
+
if (typeof configurada !== "string" || configurada.trim() === "") {
|
|
7
|
+
console.warn("motorEventos: url do coletor ausente, evento nao publicado");
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return configurada.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function publicarEvento({
|
|
15
|
+
tipo,
|
|
16
|
+
autorizacao,
|
|
17
|
+
payload,
|
|
18
|
+
url,
|
|
19
|
+
timeoutMs = TIMEOUT_PADRAO_MS,
|
|
20
|
+
aoFalhar = (mensagem) => console.error(mensagem),
|
|
21
|
+
} = {}) {
|
|
22
|
+
try {
|
|
23
|
+
const destino = urlConfigurada(url);
|
|
24
|
+
|
|
25
|
+
if (!destino || !tipo || !autorizacao) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fetch(destino, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json", Authorization: autorizacao },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
tipo,
|
|
34
|
+
dt_evento: new Date().toISOString(),
|
|
35
|
+
payload: payload ?? {},
|
|
36
|
+
}),
|
|
37
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
38
|
+
})
|
|
39
|
+
.then((resposta) => {
|
|
40
|
+
if (!resposta.ok) {
|
|
41
|
+
aoFalhar(`motorEventos: ${tipo} recusado com status ${resposta.status}`);
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.catch((erro) => {
|
|
45
|
+
aoFalhar(`motorEventos: ${tipo} nao publicado (${erro?.message})`);
|
|
46
|
+
});
|
|
47
|
+
} catch (erro) {
|
|
48
|
+
try {
|
|
49
|
+
aoFalhar(`motorEventos: ${tipo} nao publicado (${erro?.message})`);
|
|
50
|
+
} catch (_erroNoCallback) {
|
|
51
|
+
/* empty */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { publicarEvento };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
|
|
4
|
+
const { publicarEvento } = require('../src/services/motorEventosService.js');
|
|
5
|
+
|
|
6
|
+
const URL = 'http://coletor.interno/eventos';
|
|
7
|
+
|
|
8
|
+
const esperarMicrotasks = () => new Promise((resolve) => setImmediate(resolve));
|
|
9
|
+
|
|
10
|
+
describe('publicarEvento', () => {
|
|
11
|
+
let fetchOriginal;
|
|
12
|
+
let warnOriginal;
|
|
13
|
+
let urlOriginal;
|
|
14
|
+
let chamadas;
|
|
15
|
+
let falhas;
|
|
16
|
+
let avisos;
|
|
17
|
+
|
|
18
|
+
const aoFalhar = (mensagem) => falhas.push(mensagem);
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
fetchOriginal = global.fetch;
|
|
22
|
+
warnOriginal = console.warn;
|
|
23
|
+
urlOriginal = process.env.MOTOR_EVENTOS_URL;
|
|
24
|
+
chamadas = [];
|
|
25
|
+
falhas = [];
|
|
26
|
+
avisos = [];
|
|
27
|
+
console.warn = (mensagem) => avisos.push(mensagem);
|
|
28
|
+
global.fetch = (url, opcoes) => {
|
|
29
|
+
chamadas.push({ url, opcoes });
|
|
30
|
+
return Promise.resolve({ ok: true, status: 201 });
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
global.fetch = fetchOriginal;
|
|
36
|
+
console.warn = warnOriginal;
|
|
37
|
+
if (urlOriginal === undefined) {
|
|
38
|
+
delete process.env.MOTOR_EVENTOS_URL;
|
|
39
|
+
} else {
|
|
40
|
+
process.env.MOTOR_EVENTOS_URL = urlOriginal;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('envia POST na url configurada com o token de quem chamou', async () => {
|
|
45
|
+
publicarEvento({
|
|
46
|
+
tipo: 'meu_evento',
|
|
47
|
+
autorizacao: 'Bearer jwt',
|
|
48
|
+
payload: { campo: 'valor' },
|
|
49
|
+
url: URL,
|
|
50
|
+
aoFalhar,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await esperarMicrotasks();
|
|
54
|
+
|
|
55
|
+
assert.equal(chamadas.length, 1);
|
|
56
|
+
assert.equal(chamadas[0].url, URL);
|
|
57
|
+
assert.equal(chamadas[0].opcoes.method, 'POST');
|
|
58
|
+
assert.equal(chamadas[0].opcoes.headers.Authorization, 'Bearer jwt');
|
|
59
|
+
|
|
60
|
+
const corpo = JSON.parse(chamadas[0].opcoes.body);
|
|
61
|
+
assert.equal(corpo.tipo, 'meu_evento');
|
|
62
|
+
assert.deepEqual(corpo.payload, { campo: 'valor' });
|
|
63
|
+
assert.ok(!Number.isNaN(Date.parse(corpo.dt_evento)));
|
|
64
|
+
assert.deepEqual(falhas, []);
|
|
65
|
+
assert.deepEqual(avisos, []);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('nao acrescenta caminho nenhum a url configurada', async () => {
|
|
69
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: 'http://coletor.interno/v2/x', aoFalhar });
|
|
70
|
+
await esperarMicrotasks();
|
|
71
|
+
|
|
72
|
+
assert.equal(chamadas[0].url, 'http://coletor.interno/v2/x');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('cai na MOTOR_EVENTOS_URL quando a url nao e passada', async () => {
|
|
76
|
+
process.env.MOTOR_EVENTOS_URL = URL;
|
|
77
|
+
|
|
78
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', aoFalhar });
|
|
79
|
+
await esperarMicrotasks();
|
|
80
|
+
|
|
81
|
+
assert.equal(chamadas[0].url, URL);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('avisa e nao publica quando a url esta ausente', async () => {
|
|
85
|
+
delete process.env.MOTOR_EVENTOS_URL;
|
|
86
|
+
|
|
87
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', aoFalhar });
|
|
88
|
+
await esperarMicrotasks();
|
|
89
|
+
|
|
90
|
+
assert.equal(chamadas.length, 0);
|
|
91
|
+
assert.equal(avisos.length, 1);
|
|
92
|
+
assert.match(avisos[0], /url do coletor ausente/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('avisa quando a url nao e string ou e so espaco', async () => {
|
|
96
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: 123, aoFalhar });
|
|
97
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: ' ', aoFalhar });
|
|
98
|
+
await esperarMicrotasks();
|
|
99
|
+
|
|
100
|
+
assert.equal(chamadas.length, 0);
|
|
101
|
+
assert.equal(avisos.length, 2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('nao publica sem tipo ou sem autorizacao', async () => {
|
|
105
|
+
publicarEvento({ autorizacao: 'Bearer jwt', url: URL, aoFalhar });
|
|
106
|
+
publicarEvento({ tipo: 'meu_evento', url: URL, aoFalhar });
|
|
107
|
+
await esperarMicrotasks();
|
|
108
|
+
|
|
109
|
+
assert.equal(chamadas.length, 0);
|
|
110
|
+
assert.deepEqual(falhas, []);
|
|
111
|
+
assert.deepEqual(avisos, []);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('nao quebra quando chamado sem argumento nenhum', () => {
|
|
115
|
+
delete process.env.MOTOR_EVENTOS_URL;
|
|
116
|
+
|
|
117
|
+
assert.doesNotThrow(() => publicarEvento());
|
|
118
|
+
assert.equal(chamadas.length, 0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('nao propaga rejeicao do fetch e reporta a falha', async () => {
|
|
122
|
+
global.fetch = () => Promise.reject(new Error('connect ECONNREFUSED'));
|
|
123
|
+
|
|
124
|
+
assert.doesNotThrow(() =>
|
|
125
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: URL, aoFalhar })
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await esperarMicrotasks();
|
|
129
|
+
|
|
130
|
+
assert.equal(falhas.length, 1);
|
|
131
|
+
assert.match(falhas[0], /meu_evento/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('reporta resposta de erro sem lancar', async () => {
|
|
135
|
+
global.fetch = () => Promise.resolve({ ok: false, status: 400 });
|
|
136
|
+
|
|
137
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: URL, aoFalhar });
|
|
138
|
+
await esperarMicrotasks();
|
|
139
|
+
|
|
140
|
+
assert.equal(falhas.length, 1);
|
|
141
|
+
assert.match(falhas[0], /400/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('nao propaga erro sincrono do fetch', () => {
|
|
145
|
+
global.fetch = () => {
|
|
146
|
+
throw new TypeError('opcoes invalidas');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
assert.doesNotThrow(() =>
|
|
150
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: URL, aoFalhar })
|
|
151
|
+
);
|
|
152
|
+
assert.equal(falhas.length, 1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('nao devolve promise, para ninguem conseguir esperar por ela', () => {
|
|
156
|
+
const retorno = publicarEvento({
|
|
157
|
+
tipo: 'meu_evento',
|
|
158
|
+
autorizacao: 'Bearer jwt',
|
|
159
|
+
url: URL,
|
|
160
|
+
aoFalhar,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.equal(retorno, undefined);
|
|
164
|
+
});
|
|
165
|
+
});
|