@ecdt/events-engine 1.0.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 ADDED
@@ -0,0 +1,143 @@
1
+ # @ecdt/events-engine
2
+
3
+ Publicador de eventos para o coletor do motor de eventos. Não tem dependências nem usa API exclusiva do
4
+ Node, então roda no servidor e no browser.
5
+
6
+ ---
7
+
8
+ ## Instalação
9
+
10
+ ```bash
11
+ npm install @ecdt/events-engine
12
+ ```
13
+
14
+ ---
15
+
16
+ ## `criarMotorEventos(options)`
17
+
18
+ Cria o publicador de um produtor, uma vez, na inicialização do serviço.
19
+
20
+ ```js
21
+ const { criarMotorEventos } = require('@ecdt/events-engine');
22
+
23
+ const motor = criarMotorEventos({ produtor: 'meu-servico' });
24
+
25
+ motor.publicarEventoDaRequisicao(req, {
26
+ tipo: 'meu_evento',
27
+ payload: { campo: 'valor' },
28
+ });
29
+ ```
30
+
31
+ | Opção | Default | Descrição |
32
+ |---|---|---|
33
+ | `produtor` | `desconhecido` | Identificador de quem publica, no formato `^[a-z0-9-]+$` |
34
+ | `url` | `MOTOR_EVENTOS_URL` | URL completa do coletor; a lib não monta caminho nenhum |
35
+ | `cookieVisitante` | `MOTOR_EVENTOS_COOKIE_VISITANTE` | Nome do cookie que guarda o identificador anônimo |
36
+ | `timeoutMs` | `2000` | Aborta a requisição |
37
+ | `aoFalhar` | `console.error` | Recebe a mensagem quando a publicação não acontece |
38
+
39
+ O produtor vai no header `x-ecdt-produtor` de toda publicação. Informe-o como constante no código:
40
+ fora do formato, a lib avisa com `console.warn` e publica como `desconhecido`, sem lançar. Ele só se
41
+ define na inicialização; a chamada não consegue trocá-lo.
42
+
43
+ As demais opções valem de padrão para toda publicação, e a chamada prevalece sobre elas. As variáveis de
44
+ ambiente são lidas a cada publicação, e só quando existe `process`; no browser, passe `url` na
45
+ inicialização.
46
+
47
+ O objeto devolvido expõe `produtor` (o valor efetivo, já validado), `publicarEvento` e
48
+ `publicarEventoDaRequisicao`.
49
+
50
+ ---
51
+
52
+ ### `publicarEvento(options)`
53
+
54
+ Envia um evento para o coletor, sem bloquear o fluxo que o gerou.
55
+
56
+ ```js
57
+ motor.publicarEvento({
58
+ tipo: 'meu_evento',
59
+ autorizacao: req.headers.authorization,
60
+ payload: { campo: 'valor' },
61
+ });
62
+ ```
63
+
64
+ | Opção | Default | Descrição |
65
+ |---|---|---|
66
+ | `tipo` | — | Nome do evento, em `snake_case` |
67
+ | `autorizacao` | — | Header `Authorization` da requisição que originou o evento |
68
+ | `idVisitante` | — | Identificador anônimo, para quando não há token |
69
+ | `payload` | `{}` | Campos do evento |
70
+ | `url`, `timeoutMs`, `aoFalhar` | os da inicialização | Sobrescrevem o padrão só nesta chamada |
71
+
72
+ A identidade do usuário sai do token, nunca do corpo: o coletor a resolve a partir do `Authorization`
73
+ repassado. Quando não há token — rota pública, visitante anônimo — vale o `idVisitante`, que vai no
74
+ corpo como `id_visitante`. Ele não é identidade autenticada, é um identificador de correlação, e por
75
+ isso pode vir do corpo da requisição. Basta um dos dois.
76
+
77
+ Sem `tipo`, ou sem nenhum dos dois identificadores, a função não faz nada; sem `url` ela ainda registra
78
+ um `console.warn`, porque url ausente é configuração faltando e não um evento que não se aplica.
79
+
80
+ **A função nunca lança e nunca devolve promise.** O evento é secundário ao fluxo que o gerou —
81
+ falha de publicação não pode virar erro de quem chamou, e o retorno `undefined` impede que alguém
82
+ consiga dar `await` e acoplar a latência do request ao coletor.
83
+
84
+ ---
85
+
86
+ ### `publicarEventoDaRequisicao(req, options)`
87
+
88
+ O mesmo publish, montado a partir da requisição que gerou o evento. Resolve o token pelo header
89
+ `Authorization` e o identificador anônimo pelo corpo (`id_visitante`) ou pelo cookie, nessa ordem.
90
+
91
+ | Opção | Default | Descrição |
92
+ |---|---|---|
93
+ | `tipo`, `payload`, `url`, `timeoutMs`, `aoFalhar` | — | Idênticos ao `publicarEvento` |
94
+ | `cookieVisitante` | o da inicialização | Nome do cookie que guarda o identificador anônimo |
95
+
96
+ Sem `cookieVisitante` configurado, só o corpo alimenta o `id_visitante`. Também não lança em
97
+ nenhuma hipótese, inclusive com `req` sem `headers` nem `body`.
98
+
99
+ ---
100
+
101
+ ## `lerCookie`, `idDoCookie` e `sanitizarId`
102
+
103
+ Leitura do header `Cookie`, sem dependência de framework.
104
+
105
+ ```js
106
+ const { idDoCookie } = require('@ecdt/events-engine');
107
+
108
+ const idVisitante = idDoCookie(req.headers.cookie, 'nome_do_cookie');
109
+ ```
110
+
111
+ | Função | Devolve |
112
+ |---|---|
113
+ | `lerCookie(cabecalho, nome)` | Valor cru do cookie, ou `null` |
114
+ | `idDoCookie(cabecalho, nome, { maxLen })` | O identificador: aceita o cookie como string simples ou como JSON com campo `id`, decodifica o valor e corta em `maxLen` (100 por padrão) |
115
+ | `sanitizarId(valor, maxLen)` | O mesmo corte e validação, para identificador que veio de outro lugar |
116
+
117
+ O match do nome é exato, então `nome_antigo` não casa com `nome`. Nenhuma das três lança: entrada
118
+ que não é string vira `null`.
119
+
120
+ ---
121
+
122
+ ## Migrando do `@ecdt/server-common`
123
+
124
+ O publicador e as funções de cookie saíram do `@ecdt/server-common` na 2.0.0. As assinaturas das
125
+ chamadas não mudaram; o que muda é criar o publicador antes, com o produtor.
126
+
127
+ ```diff
128
+ -const { publicarEventoDaRequisicao } = require('@ecdt/server-common');
129
+ +const { criarMotorEventos } = require('@ecdt/events-engine');
130
+ +
131
+ +const { publicarEventoDaRequisicao } = criarMotorEventos({ produtor: 'meu-servico' });
132
+
133
+ publicarEventoDaRequisicao(req, { tipo, payload, cookieVisitante });
134
+ ```
135
+
136
+ `lerCookie`, `idDoCookie` e `sanitizarId` só trocam de pacote no `require`.
137
+
138
+ ---
139
+
140
+ ## TypeScript
141
+
142
+ O pacote publica os tipos em `src/index.d.ts`. As interfaces exportadas são `OpcoesDoMotor`,
143
+ `MotorEventos`, `PublicarEventoOptions`, `PublicarEventoDaRequisicaoOptions` e `IdDoCookieOptions`.
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@ecdt/events-engine",
3
+ "version": "1.0.0",
4
+ "description": "Publicador de eventos para o coletor do motor de eventos, sem dependencias e sem API exclusiva do Node",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "scripts": {
11
+ "test": "node --test"
12
+ },
13
+ "author": "",
14
+ "license": "ISC",
15
+ "engines": {
16
+ "node": ">=18.17.0"
17
+ }
18
+ }
package/src/cookie.js ADDED
@@ -0,0 +1,43 @@
1
+ const TAMANHO_MAXIMO_PADRAO = 100;
2
+
3
+ function sanitizarId(valor, maxLen = TAMANHO_MAXIMO_PADRAO) {
4
+ if (typeof valor !== "string") return null;
5
+
6
+ const limpo = valor.trim();
7
+ return limpo ? limpo.slice(0, maxLen) : null;
8
+ }
9
+
10
+ function lerCookie(cabecalho, nome) {
11
+ if (typeof cabecalho !== "string" || typeof nome !== "string") return null;
12
+
13
+ for (const parte of cabecalho.split(";")) {
14
+ const separador = parte.indexOf("=");
15
+ if (separador === -1) continue;
16
+ if (parte.slice(0, separador).trim() !== nome) continue;
17
+ return parte.slice(separador + 1).trim();
18
+ }
19
+
20
+ return null;
21
+ }
22
+
23
+ function idDoCookie(cabecalho, nome, { maxLen = TAMANHO_MAXIMO_PADRAO } = {}) {
24
+ const bruto = lerCookie(cabecalho, nome);
25
+ if (!bruto) return null;
26
+
27
+ let valor = bruto;
28
+ try {
29
+ valor = decodeURIComponent(bruto);
30
+ } catch (_erroDeEncoding) {
31
+ valor = bruto;
32
+ }
33
+
34
+ try {
35
+ const conteudo = JSON.parse(valor);
36
+ const id = conteudo && typeof conteudo === "object" ? conteudo.id : conteudo;
37
+ return sanitizarId(typeof id === "string" ? id : String(id ?? ""), maxLen);
38
+ } catch (_naoEhJson) {
39
+ return sanitizarId(valor, maxLen);
40
+ }
41
+ }
42
+
43
+ module.exports = { lerCookie, idDoCookie, sanitizarId };
package/src/index.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ export interface OpcoesDoMotor {
2
+ /** Identificador de quem publica, no formato `^[a-z0-9-]+$`. Ausente ou fora do formato vira `desconhecido`. */
3
+ produtor?: string;
4
+ /** URL completa do coletor. Default: env `MOTOR_EVENTOS_URL`, lida a cada publicação. */
5
+ url?: string;
6
+ /** Nome do cookie com o identificador anônimo. Default: env `MOTOR_EVENTOS_COOKIE_VISITANTE`. */
7
+ cookieVisitante?: string;
8
+ /** Tempo máximo de cada publicação. Default: 2000. */
9
+ timeoutMs?: number;
10
+ /** Recebe a mensagem quando a publicação não acontece. Default: `console.error`. */
11
+ aoFalhar?: (mensagem: string) => void;
12
+ }
13
+
14
+ export interface PublicarEventoOptions {
15
+ tipo: string;
16
+ autorizacao?: string;
17
+ idVisitante?: string;
18
+ payload?: Record<string, unknown>;
19
+ url?: string;
20
+ timeoutMs?: number;
21
+ aoFalhar?: (mensagem: string) => void;
22
+ }
23
+
24
+ export interface PublicarEventoDaRequisicaoOptions
25
+ extends Omit<PublicarEventoOptions, 'autorizacao' | 'idVisitante'> {
26
+ cookieVisitante?: string;
27
+ }
28
+
29
+ export interface MotorEventos {
30
+ /** O produtor efetivo, já validado. */
31
+ readonly produtor: string;
32
+ publicarEvento(options?: PublicarEventoOptions): void;
33
+ publicarEventoDaRequisicao(req: unknown, options?: PublicarEventoDaRequisicaoOptions): void;
34
+ }
35
+
36
+ /** Opções da inicialização viram padrão de toda publicação; o produtor só se define aqui. */
37
+ export declare function criarMotorEventos(options?: OpcoesDoMotor): MotorEventos;
38
+
39
+ export interface IdDoCookieOptions {
40
+ maxLen?: number;
41
+ }
42
+
43
+ export declare function lerCookie(cabecalho: unknown, nome: string): string | null;
44
+
45
+ export declare function idDoCookie(
46
+ cabecalho: unknown,
47
+ nome: string,
48
+ options?: IdDoCookieOptions
49
+ ): string | null;
50
+
51
+ export declare function sanitizarId(valor: unknown, maxLen?: number): string | null;
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ const { criarMotorEventos } = require("./publicador.js");
2
+ const { lerCookie, idDoCookie, sanitizarId } = require("./cookie.js");
3
+
4
+ module.exports = { criarMotorEventos, lerCookie, idDoCookie, sanitizarId };
@@ -0,0 +1,149 @@
1
+ const { idDoCookie, sanitizarId } = require("./cookie.js");
2
+
3
+ const TIMEOUT_PADRAO_MS = 2000;
4
+ const PRODUTOR_PADRAO = "desconhecido";
5
+ const FORMATO_DO_PRODUTOR = /^[a-z0-9-]+$/;
6
+ const CABECALHO_DO_PRODUTOR = "x-ecdt-produtor";
7
+
8
+ function lerAmbiente(nome) {
9
+ return typeof process !== "undefined" ? process.env?.[nome] : undefined;
10
+ }
11
+
12
+ function resolverProdutor(produtor) {
13
+ if (produtor === undefined || produtor === null) {
14
+ return PRODUTOR_PADRAO;
15
+ }
16
+
17
+ if (typeof produtor === "string" && FORMATO_DO_PRODUTOR.test(produtor)) {
18
+ return produtor;
19
+ }
20
+
21
+ console.warn(`motorEventos: produtor fora do formato ${FORMATO_DO_PRODUTOR}, publicando como ${PRODUTOR_PADRAO}`);
22
+ return PRODUTOR_PADRAO;
23
+ }
24
+
25
+ function urlConfigurada(url) {
26
+ const configurada = url ?? lerAmbiente("MOTOR_EVENTOS_URL");
27
+
28
+ if (typeof configurada !== "string" || configurada.trim() === "") {
29
+ console.warn("motorEventos: url do coletor ausente, evento nao publicado");
30
+ return null;
31
+ }
32
+
33
+ return configurada.trim();
34
+ }
35
+
36
+ function textoNaoVazio(valor) {
37
+ return typeof valor === "string" && valor.trim() !== "" ? valor.trim() : null;
38
+ }
39
+
40
+ function semIndefinidos(opcoes) {
41
+ return Object.fromEntries(Object.entries(opcoes ?? {}).filter(([, valor]) => valor !== undefined));
42
+ }
43
+
44
+ function publicarEvento(produtor, {
45
+ tipo,
46
+ autorizacao,
47
+ idVisitante,
48
+ payload,
49
+ url,
50
+ timeoutMs = TIMEOUT_PADRAO_MS,
51
+ aoFalhar = (mensagem) => console.error(mensagem),
52
+ } = {}) {
53
+ try {
54
+ const destino = urlConfigurada(url);
55
+ const visitante = textoNaoVazio(idVisitante);
56
+
57
+ if (!destino || !tipo || (!autorizacao && !visitante)) {
58
+ return;
59
+ }
60
+
61
+ const cabecalhos = {
62
+ "Content-Type": "application/json",
63
+ [CABECALHO_DO_PRODUTOR]: produtor,
64
+ };
65
+ if (autorizacao) {
66
+ cabecalhos.Authorization = autorizacao;
67
+ }
68
+
69
+ const corpo = {
70
+ tipo,
71
+ dt_evento: new Date().toISOString(),
72
+ payload: payload ?? {},
73
+ };
74
+ if (visitante) {
75
+ corpo.id_visitante = visitante;
76
+ }
77
+
78
+ fetch(destino, {
79
+ method: "POST",
80
+ headers: cabecalhos,
81
+ body: JSON.stringify(corpo),
82
+ signal: AbortSignal.timeout(timeoutMs),
83
+ })
84
+ .then((resposta) => {
85
+ if (!resposta.ok) {
86
+ aoFalhar(`motorEventos: ${tipo} recusado com status ${resposta.status}`);
87
+ }
88
+ })
89
+ .catch((erro) => {
90
+ aoFalhar(`motorEventos: ${tipo} nao publicado (${erro?.message})`);
91
+ });
92
+ } catch (erro) {
93
+ try {
94
+ aoFalhar(`motorEventos: ${tipo} nao publicado (${erro?.message})`);
95
+ } catch (_erroNoCallback) {
96
+ /* empty */
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Tira o token do header Authorization e o visitante do corpo ou do cookie, nessa ordem; nunca lanca.
103
+ */
104
+ function publicarEventoDaRequisicao(produtor, req, {
105
+ tipo,
106
+ payload,
107
+ cookieVisitante = lerAmbiente("MOTOR_EVENTOS_COOKIE_VISITANTE"),
108
+ ...resto
109
+ } = {}) {
110
+ try {
111
+ const doCorpo = sanitizarId(req?.body?.id_visitante);
112
+ const doCookie = cookieVisitante ? idDoCookie(req?.headers?.cookie, cookieVisitante) : null;
113
+
114
+ publicarEvento(produtor, {
115
+ tipo,
116
+ payload,
117
+ autorizacao: req?.headers?.authorization,
118
+ idVisitante: doCorpo ?? doCookie,
119
+ ...resto,
120
+ });
121
+ } catch (erro) {
122
+ console.error(`motorEventos: ${tipo} nao publicado (${erro?.message})`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Opcoes da inicializacao viram padrao de toda publicacao; o produtor so se define aqui.
128
+ *
129
+ * @param {object} [opcoes]
130
+ * @param {string} [opcoes.produtor] Identificador de quem publica, no formato ^[a-z0-9-]+$. Ausente ou fora do formato vira "desconhecido".
131
+ * @param {string} [opcoes.url] URL completa do coletor.
132
+ * @param {string} [opcoes.cookieVisitante] Nome do cookie com o identificador anonimo.
133
+ * @param {number} [opcoes.timeoutMs] Tempo maximo de cada publicacao.
134
+ * @param {(mensagem: string) => void} [opcoes.aoFalhar] Recebe a mensagem quando a publicacao nao acontece.
135
+ */
136
+ function criarMotorEventos({ produtor, url, cookieVisitante, timeoutMs, aoFalhar } = {}) {
137
+ const identificador = resolverProdutor(produtor);
138
+ const padroes = semIndefinidos({ url, cookieVisitante, timeoutMs, aoFalhar });
139
+ const comPadroes = (opcoes) => ({ ...padroes, ...semIndefinidos(opcoes) });
140
+
141
+ return {
142
+ produtor: identificador,
143
+ publicarEvento: (opcoes) => publicarEvento(identificador, comPadroes(opcoes)),
144
+ publicarEventoDaRequisicao: (req, opcoes) =>
145
+ publicarEventoDaRequisicao(identificador, req, comPadroes(opcoes)),
146
+ };
147
+ }
148
+
149
+ module.exports = { criarMotorEventos };